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, post_execution,
22        pre_execution::{self, apply_auth_list, calculate_caller_fee},
23        precompile_output_to_interpreter_result, validation,
24    },
25    inspector::{Inspector, InspectorHandler},
26    interpreter::{
27        CallOutcome, CreateOutcome, 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    precompile::PrecompileError,
35};
36use tempo_chainspec::constants::gas::STORAGE_CREDIT_VALUE;
37use tempo_contracts::precompiles::{
38    IAccountKeychain::SignatureType as PrecompileSignatureType, TIPFeeAMMError,
39};
40use tempo_precompiles::{
41    ECRECOVER_GAS,
42    account_keychain::{
43        AccountKeychain, AuthorizedKey, CallScope as PrecompileCallScope, KeyRestrictions,
44        SelectorRule as PrecompileSelectorRule, TokenLimit,
45    },
46    error::TempoPrecompileError,
47    nonce::{
48        EXPIRING_NONCE_MAX_EXPIRY_SECS, EXPIRING_NONCE_SET_CAPACITY, INonce::getNonceCall,
49        NonceManager,
50    },
51    storage::{
52        Handler as _, PrecompileStorageProvider, StorageActions, StorageCtx,
53        evm::EvmPrecompileStorageProvider,
54    },
55    tip_fee_manager::TipFeeManager,
56    tip20::{ITIP20::InsufficientBalance, TIP20Error, TIP20Token},
57    tip20_channel_reserve::TIP20ChannelReserve,
58};
59use tempo_primitives::{
60    TempoAddressExt,
61    transaction::{
62        PrimitiveSignature, SignatureType, TEMPO_EXPIRING_NONCE_KEY, TempoSignature,
63        calc_gas_balance_spending, validate_calls,
64    },
65};
66
67use crate::{
68    TempoBatchCallEnv, TempoEvm, TempoInvalidTransaction, TempoTxEnv,
69    common::TempoStateAccess,
70    error::{FeePaymentError, TempoHaltReason},
71    evm::TempoContext,
72    gas_credits,
73};
74
75/// Additional gas for P256 signature verification
76/// P256 precompile cost (6900 from EIP-7951) + 1100 for 129 bytes extra signature size - ecrecover savings (3000)
77const P256_VERIFY_GAS: u64 = 5_000;
78
79/// Additional gas for Keychain signatures (key validation overhead: COLD_SLOAD_COST + 900 processing)
80const KEYCHAIN_VALIDATION_GAS: u64 = COLD_SLOAD_COST + 900;
81
82/// Base gas for KeyAuthorization (22k storage + 5k buffer), signature gas added at runtime
83const KEY_AUTH_BASE_GAS: u64 = 27_000;
84
85/// Gas per spending limit in KeyAuthorization
86const KEY_AUTH_PER_LIMIT_GAS: u64 = 22_000;
87
88/// Rounded buffer for each extra LOG3/no-data event emitted by key authorizations.
89const KEY_AUTH_EXTRA_EVENT_BUFFER: u64 = 1_500;
90
91/// Gas cost for expiring nonce transactions (replay check + insert).
92///
93/// See [TIP-1009] for full specification.
94///
95/// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
96///
97/// Operations charged:
98/// - 2 cold SLOADs: `seen[tx_hash]`, `ring[idx]` (unique slots per tx)
99/// - 1 warm SLOAD: `seen[old_hash]` (warm because we just read `ring[idx]` which points to it)
100/// - 3 SSTOREs at RESET price: `seen[old_hash]=0`, `ring[idx]=tx_hash`, `seen[tx_hash]=valid_before`
101///
102/// Excluded from gas calculation:
103/// - `ring_ptr` SLOAD/SSTORE: Accessed by almost every expiring nonce tx in a block, so
104///   amortized cost approaches ~200 gas. May be moved out of EVM storage in the future.
105///
106/// Why SSTORE_RESET (2,900) instead of SSTORE_SET (20,000) for `seen[tx_hash]`:
107/// - SSTORE_SET cost exists to penalize permanent state growth
108/// - Expiring nonce data is ephemeral: evicted within 30 seconds, fixed-size buffer (300k)
109/// - No permanent state growth, so the 20k penalty doesn't apply
110///
111/// Total: 2*2100 + 100 + 3*2900 = 13,000 gas
112pub const EXPIRING_NONCE_GAS: u64 = 2 * COLD_SLOAD_COST + 100 + 3 * WARM_SSTORE_RESET;
113
114/// Calculates the gas cost for verifying a primitive signature.
115///
116/// Returns the additional gas required beyond the base transaction cost:
117/// - Secp256k1: 0 (already included in base 21k)
118/// - P256: 5000 gas
119/// - WebAuthn: 5000 gas + calldata cost for webauthn_data
120#[inline]
121fn primitive_signature_verification_gas(signature: &PrimitiveSignature) -> u64 {
122    match signature {
123        PrimitiveSignature::Secp256k1(_) => 0,
124        PrimitiveSignature::P256(_) => P256_VERIFY_GAS,
125        PrimitiveSignature::WebAuthn(webauthn_sig) => {
126            let tokens = get_tokens_in_calldata_istanbul(&webauthn_sig.webauthn_data);
127            P256_VERIFY_GAS + tokens * STANDARD_TOKEN_COST
128        }
129    }
130}
131
132/// Calculates the gas cost for verifying an AA signature.
133///
134/// For Keychain signatures, adds key validation overhead to the inner signature cost
135/// Returns the additional gas required beyond the base transaction cost.
136#[inline]
137fn tempo_signature_verification_gas(signature: &TempoSignature) -> u64 {
138    match signature {
139        TempoSignature::Primitive(prim_sig) => primitive_signature_verification_gas(prim_sig),
140        TempoSignature::Keychain(keychain_sig) => {
141            // Keychain = inner signature + key validation overhead (SLOAD + processing)
142            primitive_signature_verification_gas(&keychain_sig.signature) + KEYCHAIN_VALIDATION_GAS
143        }
144    }
145}
146
147#[derive(Debug, Clone)]
148struct LoadedTxAccessKey {
149    key_id: Address,
150    key: AuthorizedKey,
151}
152
153/// Counts the scope storage rows that pay the dynamic SSTORE-set path for the active spec.
154///
155/// T3 keeps the broader all-persisted-rows accounting from current main. T4 narrows this to rows
156/// that actually create storage, so repeated same-tx set length rewrites no longer count as fresh
157/// SSTORE-set rows. The helper bookkeeping around scope persistence is charged separately via a
158/// rounded surcharge.
159fn call_scope_storage_slots(
160    auth: &tempo_primitives::transaction::KeyAuthorization,
161    spec: tempo_chainspec::hardfork::TempoHardfork,
162) -> u64 {
163    match auth.allowed_calls.as_ref() {
164        None => 0,
165        Some(scopes) if scopes.is_empty() => 1,
166        Some(scopes) => {
167            let is_t4 = spec.is_t4();
168            let mut selector_sets = 0u64;
169            let mut selectors = 0u64;
170            let mut constrained_selectors = 0u64;
171            let mut recipients = 0u64;
172
173            for scope in scopes {
174                if is_t4 && !scope.selector_rules.is_empty() {
175                    selector_sets += 1;
176                }
177
178                selectors += scope.selector_rules.len() as u64;
179                for rule in &scope.selector_rules {
180                    if !rule.recipients.is_empty() {
181                        constrained_selectors += 1;
182                        recipients += rule.recipients.len() as u64;
183                    }
184                }
185            }
186
187            if is_t4 {
188                // Storage-creating rows only:
189                // - account mode write: 1
190                // - target set values+positions: +2 per target, plus one length slot for the set
191                // - selector set values+positions: +2 per selector, plus one length slot per
192                //   target that persists selectors
193                // - recipient-constrained selectors persist one recipient set length slot each
194                // - recipient set values+positions: +2 per recipient
195                1 + scopes.len() as u64 * 2
196                    + 1
197                    + selectors * 2
198                    + selector_sets
199                    + constrained_selectors
200                    + recipients * 2
201            } else {
202                // All persisted rows:
203                // - account mode write: 1
204                // - each target insertion: 3
205                // - each selector insertion: 3
206                // - recipient-constrained selectors also write recipient set length: +1 per
207                //   selector
208                // - recipient set values+positions: +2 per recipient
209                1 + scopes.len() as u64 * 3 + selectors * 3 + constrained_selectors + recipients * 2
210            }
211        }
212    }
213}
214
215/// Charges the unpriced scope-helper bookkeeping for T4 key authorizations.
216/// The dynamic SSTORE rows are already counted by `call_scope_storage_slots()`. What remains is the
217/// helper work around them: clearing the empty scope tree for fresh keys, target/set maintenance,
218/// selector/set maintenance, and recipient-set writes. We use rounded constants here because the
219/// goal is to stop the undercharge without mirroring every storage helper exactly.
220///
221/// The chosen values intentionally round upward:
222/// - base 5k covers the always-run empty-tree clear and restricted/unrestricted mode bookkeeping,
223/// - 7k per target and 7k per selector cover the set-maintenance work around each scope layer,
224/// - 5k per recipient covers the extra recipient-set persistence.
225///
226/// The objective is to stay roughly aligned with authorization pricing while avoiding materially low
227/// charges on larger scope trees, even if that means slight overcharging in simpler cases.
228///
229/// TODO: Refactor intrinsic gas accounting so this and the other intrinsic surcharges come from one
230/// shared model instead of per-feature heuristics.
231fn call_scope_extra_gas(auth: &tempo_primitives::transaction::KeyAuthorization) -> u64 {
232    const BASE_SCOPE_GAS: u64 = 5_000;
233    const TARGET_SCOPE_GAS: u64 = 7_000;
234    const SELECTOR_SCOPE_GAS: u64 = 7_000;
235    const RECIPIENT_SCOPE_GAS: u64 = 5_000;
236
237    let Some(scopes) = auth.allowed_calls.as_ref() else {
238        return BASE_SCOPE_GAS;
239    };
240
241    let num_targets = scopes.len() as u64;
242    let num_selectors = scopes
243        .iter()
244        .map(|scope| scope.selector_rules.len() as u64)
245        .sum::<u64>();
246    let num_recipients = scopes
247        .iter()
248        .flat_map(|scope| &scope.selector_rules)
249        .map(|rule| rule.recipients.len() as u64)
250        .sum::<u64>();
251
252    BASE_SCOPE_GAS
253        + TARGET_SCOPE_GAS.saturating_mul(num_targets)
254        + SELECTOR_SCOPE_GAS.saturating_mul(num_selectors)
255        + RECIPIENT_SCOPE_GAS.saturating_mul(num_recipients)
256}
257
258/// Rewrites a failed batch step's gas accounting to match whole-transaction semantics.
259///
260/// `frame_result` initially only reflects the final failed step. For atomic AA batches we surface
261/// one top-level transaction result instead, so the gas field must be normalized to the full tx
262/// budget. Reverts preserve the exact gas spent across prior successful steps plus the failed step,
263/// while halts such as OOG consume the entire remaining transaction budget.
264///
265/// NOTE: in AA batches, non-refundable state-gas charges that are known upfront, are already
266/// included in `initial_state_gas`, so this only refunds per-step execution state gas on failure.
267fn normalize_failed_batch_result_gas(
268    frame_result: &mut FrameResult,
269    final_gas_limit: u64,
270    accumulated_state_gas_spent: i64,
271) {
272    // Create new Gas with correct limit, because Gas does not have a set_limit method
273    // (the frame_result limit only covers the failed step).
274    let mut corrected_gas = Gas::new_spent_with_reservoir(final_gas_limit, 0);
275    if frame_result.instruction_result().is_revert() {
276        corrected_gas.erase_cost(frame_result.gas().remaining());
277    }
278    // No refunds when batch fails and all state is reverted.
279    corrected_gas.set_refund(0);
280    // No state gas spending for failed calls
281    corrected_gas.set_state_gas_spent(0);
282    // Reservoir and state gas are refunded on failure
283    corrected_gas.set_reservoir(
284        frame_result
285            .gas()
286            .reservoir()
287            .saturating_add_signed(accumulated_state_gas_spent)
288            .saturating_add_signed(frame_result.gas().state_gas_spent()),
289    );
290    *frame_result.gas_mut() = corrected_gas;
291}
292
293fn translate_allowed_calls_for_precompile(
294    key_auth: &tempo_primitives::transaction::SignedKeyAuthorization,
295) -> Vec<PrecompileCallScope> {
296    let Some(scopes) = key_auth.allowed_calls.as_ref() else {
297        return Vec::new();
298    };
299
300    scopes
301        .iter()
302        .map(|scope| PrecompileCallScope {
303            target: scope.target,
304            selectorRules: scope
305                .selector_rules
306                .iter()
307                .map(|rule| PrecompileSelectorRule {
308                    selector: rule.selector.into(),
309                    recipients: rule.recipients.clone(),
310                })
311                .collect(),
312        })
313        .collect()
314}
315
316/// Calculates the intrinsic gas cost for a KeyAuthorization.
317///
318/// This is charged before execution as part of transaction validation.
319///
320/// Pre-T1B: Gas = BASE (27k) + signature verification + (22k per spending limit)
321///   On T1/T1A this was double-charged alongside the gas-metered precompile call.
322///
323/// T1B+: Gas = signature verification + SLOAD (existing key check) +
324///   SSTORE (write key) + N × SSTORE (per spending limit)
325///   This is the sole gas accounting — the precompile runs with unlimited gas.
326///
327/// Returns `(total_gas, state_gas)` where `total_gas` includes the state gas portion.
328/// On T4+, each storage-creating SSTORE contributes `sstore_set_state_gas` to state gas
329/// per TIP-1016.
330#[inline]
331fn calculate_key_authorization_gas(
332    key_auth: &tempo_primitives::transaction::SignedKeyAuthorization,
333    gas_params: &GasParams,
334    spec: tempo_chainspec::hardfork::TempoHardfork,
335) -> (u64, u64) {
336    // All signature types pay ECRECOVER_GAS (3k) as the baseline since
337    // primitive_signature_verification_gas assumes ecrecover is already in base 21k.
338    // For KeyAuthorization, we're doing an additional signature verification.
339    let sig_gas = ECRECOVER_GAS + primitive_signature_verification_gas(&key_auth.signature);
340
341    let num_limits = key_auth
342        .authorization
343        .limits
344        .as_ref()
345        .map(|limits| limits.len() as u64)
346        .unwrap_or(0);
347
348    if spec.is_t1b() {
349        // T1B+: Accurate gas matching actual precompile storage operations.
350        // authorize_key does: 1 SLOAD (read existing key) + 1 SSTORE (write key)
351        //   + N SSTOREs (one per spending limit) + 2k buffer (TSTORE + keccak + event)
352        // T5 witness and T6 admin authorizations emit additional LOG3 events with no data.
353        const BUFFER: u64 = 2_000;
354        let sload_cost =
355            gas_params.warm_storage_read_cost() + gas_params.cold_storage_additional_cost();
356
357        let limit_slots = if spec.is_t3() {
358            // T3 periodic limits write 2 storage slots per token:
359            // spending_limits[token].remaining + packed {max, period, period_end}
360            num_limits.saturating_mul(2)
361        } else {
362            num_limits
363        };
364
365        let has_t5_witness = key_auth.has_witness();
366        let mut num_sstores = 1 + limit_slots;
367
368        if spec.is_t3() {
369            num_sstores += call_scope_storage_slots(&key_auth.authorization, spec);
370        }
371
372        let mut sstore_cost = gas_params.get(GasId::sstore_set_without_load_cost());
373        if spec.is_t7() {
374            // T7 exposes only the SSTORE residual in the gas table. Since key-auth storage is
375            // intrinsic-only, we must also add the creditable portion here.
376            sstore_cost = sstore_cost.saturating_add(STORAGE_CREDIT_VALUE);
377        }
378        let mut regular_gas = sig_gas + sload_cost + sstore_cost * num_sstores + BUFFER;
379
380        if has_t5_witness {
381            regular_gas += sload_cost + KEY_AUTH_EXTRA_EVENT_BUFFER;
382        }
383
384        if spec.is_t6() && key_auth.is_admin() {
385            regular_gas += KEY_AUTH_EXTRA_EVENT_BUFFER;
386        }
387
388        // T4+: include extra gas for call scopes configuration
389        if spec.is_t4() {
390            regular_gas += call_scope_extra_gas(&key_auth.authorization);
391        }
392
393        // TIP-1016: each storage-creating SSTORE also incurs state gas.
394        let state_gas = gas_params
395            .get(GasId::sstore_set_state_gas())
396            .saturating_mul(num_sstores);
397
398        (regular_gas, state_gas)
399    } else {
400        // Pre-T1B: Original heuristic constants
401        (
402            KEY_AUTH_BASE_GAS + sig_gas + num_limits * KEY_AUTH_PER_LIMIT_GAS,
403            0,
404        )
405    }
406}
407
408/// Tempo EVM [`Handler`] implementation with Tempo specific modifications:
409///
410/// Fees are paid in fee tokens instead of account balance.
411#[derive(Debug)]
412pub struct TempoEvmHandler<DB, I> {
413    /// Phantom data to avoid type inference issues.
414    _phantom: core::marker::PhantomData<(DB, I)>,
415}
416
417impl<DB, I> TempoEvmHandler<DB, I> {
418    /// Create a new [`TempoEvmHandler`] handler instance
419    pub fn new() -> Self {
420        Self {
421            _phantom: core::marker::PhantomData,
422        }
423    }
424}
425
426impl<DB: alloy_evm::Database, I> TempoEvmHandler<DB, I> {
427    fn seed_precompile_tx_context(
428        &self,
429        evm: &mut TempoEvm<DB, I>,
430    ) -> Result<(), EVMError<DB::Error, TempoInvalidTransaction>> {
431        let ctx = evm.ctx_mut();
432        let channel_open_context_hash = ctx.tx.channel_open_context_hash();
433
434        // Seed transient precompile transaction context for both regular execution and RPC
435        // simulations (`eth_call` / `eth_estimateGas`) that go through handler execution.
436        StorageCtx::enter_evm(
437            &mut ctx.journaled_state,
438            &ctx.block,
439            &ctx.cfg,
440            &ctx.tx,
441            StorageActions::disabled(),
442            || {
443                let mut keychain = AccountKeychain::new();
444                keychain.set_tx_origin(ctx.tx.caller())?;
445
446                if let Some(channel_open_context_hash) = channel_open_context_hash {
447                    let mut channel_reserve = TIP20ChannelReserve::new();
448                    channel_reserve.set_channel_open_context_hash(channel_open_context_hash)?;
449                }
450
451                Ok::<(), TempoPrecompileError>(())
452            },
453        )
454        .map_err(|e| EVMError::Custom(e.to_string()))
455    }
456}
457
458impl<DB, I> TempoEvmHandler<DB, I>
459where
460    DB: alloy_evm::Database,
461{
462    fn prevalidate_keychain_call_scopes(
463        &self,
464        evm: &mut TempoEvm<DB, I>,
465        calls: &[tempo_primitives::transaction::Call],
466        remaining_gas: &mut u64,
467        reservoir: u64,
468    ) -> Result<Option<FrameResult>, EVMError<DB::Error, TempoInvalidTransaction>> {
469        let spec = *evm.ctx().cfg().spec();
470        if !spec.is_t3() {
471            return Ok(None);
472        }
473
474        // Call-scope matching scales with batch size, so it runs under a metered storage provider.
475        // This keeps unpaid transaction validation bounded while still failing before the first
476        // user call executes.
477
478        let (access_key_addr, user_address) = {
479            let ctx = evm.ctx();
480            let tx = ctx.tx();
481            let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref() else {
482                return Ok(None);
483            };
484            let Some(keychain_sig) = tempo_tx_env.signature.as_keychain() else {
485                return Ok(None);
486            };
487
488            let access_key_addr = if let Some(override_key_id) = tempo_tx_env.override_key_id {
489                override_key_id
490            } else {
491                keychain_sig
492                    .key_id(&tempo_tx_env.signature_hash)
493                    .map_err(|_| {
494                        EVMError::Custom(
495                            "keychain access key recovery failed after validation".into(),
496                        )
497                    })?
498            };
499
500            (access_key_addr, keychain_sig.user_address)
501        };
502        let Some(kind) = calls.first().map(|call| call.to) else {
503            return Err(EVMError::Custom(
504                "AA transactions must contain at least one call".into(),
505            ));
506        };
507
508        // It's fine to set reservoir to 0 because this won't create any state.
509        let actions = evm.actions.clone();
510        let (validation, gas_used) = StorageCtx::enter_ctx_with_gas_limit(
511            evm.ctx_mut(),
512            *remaining_gas,
513            reservoir,
514            actions,
515            || {
516                let keychain = AccountKeychain::default();
517                for call in calls {
518                    keychain.validate_call_scope_for_transaction(
519                        user_address,
520                        access_key_addr,
521                        &call.to,
522                        call.input.as_ref(),
523                    )?;
524                }
525                Ok::<(), TempoPrecompileError>(())
526            },
527        );
528
529        match validation {
530            Ok(()) => {
531                *remaining_gas = remaining_gas.saturating_sub(gas_used);
532                Ok(None)
533            }
534            Err(err) => match err.into_precompile_result(gas_used, reservoir) {
535                Ok(output) => {
536                    let interpreter_result =
537                        precompile_output_to_interpreter_result(output, *remaining_gas);
538
539                    let frame_result = if kind.is_call() {
540                        FrameResult::Call(CallOutcome::new(interpreter_result, 0..0))
541                    } else {
542                        FrameResult::Create(CreateOutcome::new(interpreter_result, None))
543                    };
544
545                    Ok(Some(frame_result))
546                }
547                Err(PrecompileError::Fatal(err)) => Err(EVMError::Custom(err)),
548                Err(err) => Err(EVMError::Custom(err.to_string())),
549            },
550        }
551    }
552
553    /// Generic single-call execution that works with both standard and inspector exec loops.
554    ///
555    /// This is the core implementation that both `execute_single_call` and inspector-aware
556    /// execution can use by providing the appropriate exec loop function.
557    fn execute_single_call_with<F>(
558        &mut self,
559        evm: &mut TempoEvm<DB, I>,
560        gas_limit: u64,
561        reservoir: u64,
562        mut run_loop: F,
563    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
564    where
565        F: FnMut(
566            &mut Self,
567            &mut TempoEvm<DB, I>,
568            <<TempoEvm<DB, I> as EvmTr>::Frame as FrameTr>::FrameInit,
569        ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
570    {
571        // Create first frame action
572        let first_frame_input = self.first_frame_input(evm, gas_limit, reservoir)?;
573
574        // Run execution loop (standard or inspector)
575        let mut frame_result = run_loop(self, evm, first_frame_input)?;
576
577        // Handle last frame result
578        self.last_frame_result(evm, reservoir, &mut frame_result)?;
579
580        Ok(frame_result)
581    }
582
583    /// Executes a standard single-call transaction using the default handler logic.
584    ///
585    /// This calls the same helper methods used by the default [`Handler::execution`] implementation.
586    fn execute_single_call(
587        &mut self,
588        evm: &mut TempoEvm<DB, I>,
589        gas_limit: u64,
590        reservoir: u64,
591    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
592        self.execute_single_call_with(evm, gas_limit, reservoir, Self::run_exec_loop)
593    }
594
595    /// Generic multi-call execution that works with both standard and inspector exec loops.
596    ///
597    /// This is the core implementation for atomic batch execution that both `execute_multi_call`
598    /// and inspector-aware execution can use by providing the appropriate single-call function.
599    ///
600    /// Provides atomic batch execution for AA transactions with multiple calls:
601    /// 1. Creates a checkpoint before executing any calls
602    /// 2. Executes each call sequentially, updating gas tracking
603    /// 3. If ANY call fails, reverts ALL state changes atomically
604    /// 4. If all calls succeed, commits ALL state changes atomically
605    ///
606    /// The atomicity is guaranteed by the checkpoint/revert/commit mechanism:
607    /// - Each individual call creates its own internal checkpoint
608    /// - The outer checkpoint (created here) captures state before any calls execute
609    /// - Reverting the outer checkpoint undoes all nested changes
610    ///
611    /// This checkpoint only covers user-call execution. Inline key authorization attached to the
612    /// transaction is applied earlier during validation/pre-execution and intentionally remains
613    /// persisted if scope prevalidation fails here or if a later user call reverts the batch.
614    fn execute_multi_call_with<F>(
615        &mut self,
616        evm: &mut TempoEvm<DB, I>,
617        mut remaining_gas: u64,
618        mut reservoir: u64,
619        calls: Vec<tempo_primitives::transaction::Call>,
620        mut execute_single: F,
621    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
622    where
623        F: FnMut(
624            &mut Self,
625            &mut TempoEvm<DB, I>,
626            u64,
627            u64,
628        ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
629    {
630        // Create checkpoint for atomic execution - captures state before any calls
631        let checkpoint = evm.ctx().journal_mut().checkpoint();
632        let mut accumulated_gas_refund = 0i64;
633        let mut accumulated_state_gas_spent = 0i64;
634
635        // Store original TxEnv values to restore after batch execution
636        let original_kind = evm.ctx().tx().kind();
637        let original_value = evm.ctx().tx().value();
638        let original_data = evm.ctx().tx().input().clone();
639        let original_gas_limit = evm.ctx().tx().gas_limit();
640
641        let mut final_result = None;
642
643        if let Some(mut frame_result) =
644            self.prevalidate_keychain_call_scopes(evm, &calls, &mut remaining_gas, reservoir)?
645        {
646            // This path only runs for keychain batches that already passed the structural CREATE
647            // rejection in validation, so there is no first-call CREATE nonce to preserve here.
648            normalize_failed_batch_result_gas(
649                &mut frame_result,
650                evm.ctx().tx().gas_limit(),
651                accumulated_state_gas_spent,
652            );
653            return Ok(frame_result);
654        }
655
656        for call in calls.iter() {
657            // Update TxEnv to point to this specific call
658            {
659                let tx = &mut evm.ctx().tx;
660                tx.inner.kind = call.to;
661                tx.inner.value = call.value;
662                tx.inner.data = call.input.clone();
663                tx.inner.gas_limit = remaining_gas;
664            }
665
666            // Execute call with NO additional initial gas (already deducted upfront in validation)
667            let frame_result = execute_single(self, evm, remaining_gas, reservoir);
668
669            // Restore original TxEnv immediately after execution, even if execution failed
670            {
671                let tx = &mut evm.ctx().tx;
672                tx.inner.kind = original_kind;
673                tx.inner.value = original_value;
674                tx.inner.data = original_data.clone();
675                tx.inner.gas_limit = original_gas_limit;
676            }
677
678            let mut frame_result = frame_result?;
679
680            // Check if call succeeded
681            if !frame_result.instruction_result().is_ok() {
682                // Revert checkpoint - rolls back ALL state changes from all executed calls.
683                evm.ctx().journal_mut().checkpoint_revert(checkpoint);
684
685                // For AA transactions with CREATE as the first call, the nonce was bumped by
686                // make_create_frame during execution. Since checkpoint_revert rolled that back,
687                // we need to manually bump the nonce here to ensure it persists even on failure.
688                //
689                // However, this only applies when using the protocol nonce (nonce_key == 0).
690                // When using 2D nonces (nonce_key != 0), replay protection is handled by the
691                // NonceManager, and the protocol nonce is only used for CREATE address derivation.
692                // Since the CREATE reverted, no contract was deployed, so the address wasn't
693                // "claimed" and we don't need to burn the protocol nonce.
694                let uses_protocol_nonce = evm
695                    .ctx()
696                    .tx()
697                    .tempo_tx_env
698                    .as_ref()
699                    .map(|aa| aa.nonce_key.is_zero())
700                    .unwrap_or(true);
701
702                if uses_protocol_nonce && calls.first().map(|c| c.to.is_create()).unwrap_or(false) {
703                    let caller = evm.ctx().tx().caller();
704                    if let Ok(mut caller_acc) =
705                        evm.ctx().journal_mut().load_account_with_code_mut(caller)
706                    {
707                        caller_acc.data.bump_nonce();
708                    }
709                }
710
711                normalize_failed_batch_result_gas(
712                    &mut frame_result,
713                    evm.ctx().tx().gas_limit(),
714                    accumulated_state_gas_spent,
715                );
716
717                return Ok(frame_result);
718            }
719
720            // Call succeeded - accumulate gas usage, refunds, and state gas
721            accumulated_gas_refund =
722                accumulated_gas_refund.saturating_add(frame_result.gas().refunded());
723            accumulated_state_gas_spent =
724                accumulated_state_gas_spent.saturating_add(frame_result.gas().state_gas_spent());
725
726            // Update gas limit and reservoir to remaining values
727            remaining_gas = frame_result.gas().remaining();
728            reservoir = frame_result.gas().reservoir();
729
730            final_result = Some(frame_result);
731        }
732
733        // All calls succeeded - commit checkpoint to finalize ALL state changes
734        evm.ctx().journal_mut().checkpoint_commit();
735
736        // Fix gas accounting for the entire batch
737        let mut result =
738            final_result.ok_or_else(|| EVMError::Custom("No calls executed".into()))?;
739
740        // Create new Gas with correct limit, because Gas does not have a set_limit method
741        // (the frame_result has the limit from just the last call)
742        let mut corrected_gas = Gas::new(evm.ctx().tx().gas_limit());
743        corrected_gas.set_remaining(result.gas().remaining());
744        corrected_gas.set_refund(accumulated_gas_refund);
745        corrected_gas.set_state_gas_spent(accumulated_state_gas_spent);
746        corrected_gas.set_reservoir(reservoir);
747
748        *result.gas_mut() = corrected_gas;
749
750        Ok(result)
751    }
752
753    /// Executes a multi-call AA transaction atomically.
754    fn execute_multi_call(
755        &mut self,
756        evm: &mut TempoEvm<DB, I>,
757        gas_limit: u64,
758        reservoir: u64,
759        calls: Vec<tempo_primitives::transaction::Call>,
760    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
761        self.execute_multi_call_with(evm, gas_limit, reservoir, calls, Self::execute_single_call)
762    }
763
764    /// Executes a standard single-call transaction with inspector support.
765    ///
766    /// This is the inspector-aware version of execute_single_call that uses
767    /// inspect_run_exec_loop instead of run_exec_loop.
768    fn inspect_execute_single_call(
769        &mut self,
770        evm: &mut TempoEvm<DB, I>,
771        gas_limit: u64,
772        reservoir: u64,
773    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
774    where
775        I: Inspector<TempoContext<DB>, EthInterpreter>,
776    {
777        self.execute_single_call_with(evm, gas_limit, reservoir, Self::inspect_run_exec_loop)
778    }
779
780    /// Executes a multi-call AA transaction atomically with inspector support.
781    ///
782    /// This is the inspector-aware version of execute_multi_call that uses
783    /// inspect_execute_single_call instead of execute_single_call.
784    fn inspect_execute_multi_call(
785        &mut self,
786        evm: &mut TempoEvm<DB, I>,
787        gas_limit: u64,
788        reservoir: u64,
789        calls: Vec<tempo_primitives::transaction::Call>,
790    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
791    where
792        I: Inspector<TempoContext<DB>, EthInterpreter>,
793    {
794        self.execute_multi_call_with(
795            evm,
796            gas_limit,
797            reservoir,
798            calls,
799            Self::inspect_execute_single_call,
800        )
801    }
802}
803
804impl<DB, I> Default for TempoEvmHandler<DB, I> {
805    fn default() -> Self {
806        Self::new()
807    }
808}
809
810impl<DB, I> Handler for TempoEvmHandler<DB, I>
811where
812    DB: alloy_evm::Database,
813{
814    type Evm = TempoEvm<DB, I>;
815    type Error = EVMError<DB::Error, TempoInvalidTransaction>;
816    type HaltReason = TempoHaltReason;
817
818    /// Overridden execution method that handles AA vs standard transactions.
819    ///
820    /// Dispatches based on transaction type:
821    /// - AA transactions (type 0x5): Use batch execution path with calls field
822    /// - All other transactions: Use standard single-call execution
823    #[inline]
824    fn execution(
825        &mut self,
826        evm: &mut Self::Evm,
827        init_and_floor_gas: &InitialAndFloorGas,
828    ) -> Result<FrameResult, Self::Error> {
829        let spec = evm.ctx_ref().cfg().spec();
830        let tx = evm.tx();
831
832        if let Some(oog) = check_gas_limit(*spec, tx, init_and_floor_gas) {
833            return Ok(oog);
834        }
835
836        let (gas_limit, reservoir) = evm.initial_gas_and_reservoir(init_and_floor_gas);
837
838        if let Some(tempo_tx_env) = evm.ctx().tx().tempo_tx_env.as_ref() {
839            let calls = tempo_tx_env.aa_calls.clone();
840            self.execute_multi_call(evm, gas_limit, reservoir, calls)
841        } else {
842            self.execute_single_call(evm, gas_limit, reservoir)
843        }
844    }
845
846    /// Applies Tempo-specific post-execution accounting before the standard gas refund flow.
847    #[inline]
848    fn post_execution(
849        &self,
850        evm: &mut Self::Evm,
851        exec_result: &mut FrameResult,
852        init_and_floor_gas: InitialAndFloorGas,
853        eip7702_gas_refund: i64,
854    ) -> Result<ResultGas, Self::Error> {
855        if exec_result.instruction_result().is_ok() {
856            gas_credits::apply_refund(evm, exec_result.gas_mut())?;
857        }
858        self.refund(evm, exec_result, eip7702_gas_refund);
859
860        let result_gas = post_execution::build_result_gas(
861            exec_result.instruction_result().is_halt(),
862            exec_result.gas(),
863            init_and_floor_gas,
864        );
865
866        self.eip7623_check_gas_floor(evm, exec_result, init_and_floor_gas);
867        self.reimburse_caller(evm, exec_result)?;
868        self.reward_beneficiary(evm, exec_result)?;
869
870        Ok(result_gas)
871    }
872
873    /// Applies gas refunds, dropping the EIP-3529 one-fifth refund cap on T7+.
874    ///
875    /// TIP-1060 removes the standard EVM refund cap: the `Refund`-mode
876    /// storage-credit settlement refund and the preserved baseline SSTORE
877    /// refunds are credited to the transaction's gas refund counter in full,
878    /// regardless of the transaction's gas used. Pre-T7 keeps the standard
879    /// capped behavior (`Gas::set_final_refund`).
880    #[inline]
881    fn refund(&self, evm: &mut Self::Evm, exec_result: &mut FrameResult, eip7702_refund: i64) {
882        let spec = evm.ctx.cfg.spec;
883        let gas = exec_result.gas_mut();
884        if spec.is_t7() {
885            // No cap: leave the accumulated refund counter untouched after
886            // recording the EIP-7702 auth refund.
887            gas.record_refund(eip7702_refund);
888        } else {
889            post_execution::refund(spec.into(), gas, eip7702_refund);
890        }
891    }
892
893    /// Take logs from the Journal if outcome is Halt Or Revert.
894    #[inline]
895    fn execution_result(
896        &mut self,
897        evm: &mut Self::Evm,
898        result: <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
899        result_gas: ResultGas,
900    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
901        evm.clear();
902
903        MainnetHandler::default()
904            .execution_result(evm, result, result_gas)
905            .map(|result| result.map_haltreason(Into::into))
906    }
907
908    /// Override apply_eip7702_auth_list to support AA transactions with authorization lists.
909    ///
910    /// The default implementation only processes authorization lists for TransactionType::Eip7702 (0x04).
911    /// This override extends support to AA transactions (type 0x76) by checking for the presence
912    /// of an aa_authorization_list in the tempo_tx_env.
913    #[inline]
914    fn apply_eip7702_auth_list(
915        &self,
916        evm: &mut Self::Evm,
917        _init_and_floor_gas: &mut InitialAndFloorGas,
918    ) -> Result<u64, Self::Error> {
919        let ctx = &mut evm.ctx;
920        let spec = ctx.cfg.spec;
921
922        // Check if this is an AA transaction with an authorization list
923        let has_aa_auth_list = ctx
924            .tx
925            .tempo_tx_env
926            .as_ref()
927            .map(|aa_env| !aa_env.tempo_authorization_list.is_empty())
928            .unwrap_or(false);
929
930        let refunded_accounts = if has_aa_auth_list {
931            let tempo_tx_env = ctx.tx.tempo_tx_env.as_ref().unwrap();
932
933            apply_auth_list::<_, Self::Error>(
934                ctx.cfg.chain_id,
935                tempo_tx_env
936                    .tempo_authorization_list
937                    .iter()
938                    // T0 hardfork: skip keychain signatures in auth list processing
939                    .filter(|auth| !(spec.is_t0() && auth.signature().is_keychain())),
940                &mut ctx.journaled_state,
941            )?
942            .0
943        } else {
944            apply_auth_list::<_, Self::Error>(
945                ctx.cfg.chain_id,
946                ctx.tx.authorization_list(),
947                &mut ctx.journaled_state,
948            )?
949            .0
950        };
951
952        let refunded_gas = ctx
953            .cfg
954            .gas_params
955            .tx_eip7702_auth_refund_regular()
956            .saturating_mul(refunded_accounts);
957
958        Ok(refunded_gas)
959    }
960
961    #[inline]
962    fn validate_against_state_and_deduct_caller(
963        &self,
964        evm: &mut Self::Evm,
965        init_gas: &mut InitialAndFloorGas,
966    ) -> Result<(), Self::Error> {
967        self.seed_precompile_tx_context(evm)?;
968
969        let actions = evm.actions.clone();
970        let block = &evm.inner.ctx.block;
971        let tx = &evm.inner.ctx.tx;
972        let cfg = &evm.inner.ctx.cfg;
973        let journal = &mut evm.inner.ctx.journaled_state;
974
975        let fee_payer = tx.fee_payer().expect("pre-validated in `validate_env`");
976        let fee_token = journal
977            .get_fee_token(tx, fee_payer, cfg.spec, actions.clone())
978            .map_err(|err| EVMError::Custom(err.to_string()))?;
979
980        evm.fee_token = Some(fee_token);
981
982        // Always validate TIP20 prefix to prevent panics in get_token_balance.
983        // This is a protocol-level check since validators could bypass initial validation.
984        if !fee_token.is_tip20() {
985            return Err(TempoInvalidTransaction::FeeTokenNotTip20 { address: fee_token }.into());
986        }
987
988        // Skip USD currency check for cases when the transaction is free and is not a part of a subblock.
989        // Since we already validated the TIP20 prefix above, we only need to check the USD currency.
990        if !tx.max_balance_spending()?.is_zero() || tx.is_subblock_transaction() {
991            journal.ensure_tip20_usd(cfg.spec, fee_token, actions.clone())?;
992        }
993
994        // Load the fee payer balance
995        let account_balance = get_token_balance(journal, fee_token, fee_payer)?;
996
997        // Load caller's account
998        let mut caller_account = journal.load_account_with_code_mut(tx.caller())?.data;
999
1000        let nonce_key = tx
1001            .tempo_tx_env
1002            .as_ref()
1003            .map(|aa| aa.nonce_key)
1004            .unwrap_or_default();
1005
1006        let spec = cfg.spec();
1007
1008        // Only treat as expiring nonce if T1 is active, otherwise treat as regular 2D nonce
1009        let is_expiring_nonce = nonce_key == TEMPO_EXPIRING_NONCE_KEY && spec.is_t1();
1010
1011        // Validate account nonce and code (EIP-3607) using upstream helper
1012        pre_execution::validate_account_nonce_and_code(
1013            &caller_account.account().info,
1014            tx.nonce(),
1015            cfg.is_eip3607_disabled(),
1016            // skip nonce check if 2D nonce or expiring nonce is used
1017            cfg.is_nonce_check_disabled() || !nonce_key.is_zero(),
1018        )?;
1019
1020        // modify account nonce and touch the account.
1021        caller_account.touch();
1022
1023        // add additional gas for CREATE tx with 2d nonce and account nonce is 0.
1024        // This case would create a new account for caller.
1025        // We only check first call of the transaction because CREATE is only allowed
1026        // to appear as the first call in the batch (validated in `validate_calls`)
1027        if !nonce_key.is_zero()
1028            && tx.first_call().is_some_and(|(kind, _)| kind.is_create())
1029            && caller_account.nonce() == 0
1030        {
1031            init_gas.initial_regular_gas += cfg.gas_params.get(GasId::new_account_cost());
1032            init_gas.initial_state_gas += cfg.gas_params.new_account_state_gas();
1033
1034            // do the gas limit check again (include state gas for T4+).
1035            if tx.gas_limit() < init_gas.initial_total_gas() {
1036                return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
1037                    gas_limit: tx.gas_limit(),
1038                    initial_gas: init_gas.initial_total_gas(),
1039                }
1040                .into());
1041            }
1042
1043            // Validate that regular gas does not exceed the cap.
1044            if cfg.is_amsterdam_eip8037_enabled()
1045                && init_gas.initial_regular_gas().max(init_gas.floor_gas) > cfg.tx_gas_limit_cap()
1046            {
1047                return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
1048                    gas_floor: init_gas.initial_regular_gas(),
1049                    gas_limit: cfg.tx_gas_limit_cap(),
1050                }
1051                .into());
1052            }
1053        }
1054
1055        if is_expiring_nonce {
1056            // Expiring nonce transaction replay protection:
1057            // - Pre-T1B: use tx_hash for backwards-compatible behavior.
1058            // - T1B+: use the sender-scoped tx identifier (keccak256(encode_for_signing || sender))
1059            //   to prevent replay via different fee payer signatures.
1060            let tempo_tx_env = tx
1061                .tempo_tx_env
1062                .as_ref()
1063                .ok_or(TempoInvalidTransaction::ExpiringNonceMissingTxEnv)?;
1064
1065            // Expiring nonce txs must have nonce == 0
1066            if tx.nonce() != 0 {
1067                return Err(TempoInvalidTransaction::ExpiringNonceNonceNotZero.into());
1068            }
1069
1070            let replay_hash = if spec.is_t1b() {
1071                tx.unique_tx_identifier()
1072                    .ok_or(TempoInvalidTransaction::ExpiringNonceMissingTxEnv)?
1073            } else {
1074                tempo_tx_env.tx_hash
1075            };
1076            let valid_before = tempo_tx_env
1077                .valid_before
1078                .ok_or(TempoInvalidTransaction::ExpiringNonceMissingValidBefore)?;
1079
1080            let block_timestamp = block.timestamp().saturating_to::<u64>();
1081            StorageCtx::enter_evm_without_tip1060_accounting(
1082                journal,
1083                block,
1084                cfg,
1085                tx,
1086                actions.clone(),
1087                || {
1088                    let mut nonce_manager = NonceManager::new();
1089
1090                    let prev_ptr = if let Some(expiring_nonce_idx) = tempo_tx_env.expiring_nonce_idx
1091                    {
1092                        let ptr = nonce_manager
1093                            .expiring_nonce_ring_ptr
1094                            .read()
1095                            .map_err(|err| EVMError::Custom(err.to_string()))?;
1096
1097                        let next = (ptr + expiring_nonce_idx as u32) % EXPIRING_NONCE_SET_CAPACITY;
1098
1099                        nonce_manager
1100                            .expiring_nonce_ring_ptr
1101                            .write(next)
1102                            .map_err(|err| EVMError::Custom(err.to_string()))?;
1103
1104                        Some(ptr)
1105                    } else {
1106                        None
1107                    };
1108
1109                    nonce_manager
1110                    .check_and_mark_expiring_nonce(replay_hash, valid_before)
1111                    .map_err(|err| match err {
1112                        TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
1113                        TempoPrecompileError::NonceError(
1114                            tempo_contracts::precompiles::NonceError::InvalidExpiringNonceExpiry(_),
1115                        ) => {
1116                            let max_allowed =
1117                                block_timestamp.saturating_add(EXPIRING_NONCE_MAX_EXPIRY_SECS);
1118                            if valid_before <= block_timestamp {
1119                                TempoInvalidTransaction::NonceManagerError(format!(
1120                                    "expiring nonce transaction expired: valid_before ({valid_before}) <= block timestamp ({block_timestamp})"
1121                                ))
1122                                .into()
1123                            } else {
1124                                TempoInvalidTransaction::NonceManagerError(format!(
1125                                    "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}"
1126                                ))
1127                                .into()
1128                            }
1129                        }
1130                        err => TempoInvalidTransaction::NonceManagerError(err.to_string()).into(),
1131                    })?;
1132
1133                    if let Some(prev_ptr) = prev_ptr {
1134                        nonce_manager
1135                            .expiring_nonce_ring_ptr
1136                            .write(prev_ptr)
1137                            .map_err(|err| EVMError::Custom(err.to_string()))?;
1138                    }
1139
1140                    Ok::<_, EVMError<DB::Error, TempoInvalidTransaction>>(())
1141                },
1142            )?;
1143        } else if !nonce_key.is_zero() {
1144            // 2D nonce transaction
1145            StorageCtx::enter_evm_without_tip1060_accounting(
1146                journal,
1147                block,
1148                cfg,
1149                tx,
1150                actions.clone(),
1151                || {
1152                    let mut nonce_manager = NonceManager::new();
1153
1154                    if !cfg.is_nonce_check_disabled() {
1155                        let tx_nonce = tx.nonce();
1156                        let state = nonce_manager
1157                            .get_nonce(getNonceCall {
1158                                account: tx.caller(),
1159                                nonceKey: nonce_key,
1160                            })
1161                            .map_err(|err| match err {
1162                                TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
1163                                err => TempoInvalidTransaction::NonceManagerError(err.to_string())
1164                                    .into(),
1165                            })?;
1166
1167                        match tx_nonce.cmp(&state) {
1168                            Ordering::Greater => {
1169                                return Err(InvalidTransaction::NonceTooHigh {
1170                                    tx: tx_nonce,
1171                                    state,
1172                                }
1173                                .into());
1174                            }
1175                            Ordering::Less => {
1176                                return Err(InvalidTransaction::NonceTooLow {
1177                                    tx: tx_nonce,
1178                                    state,
1179                                }
1180                                .into());
1181                            }
1182                            _ => {}
1183                        }
1184                    }
1185
1186                    // Always increment nonce for AA transactions with non-zero nonce keys.
1187                    nonce_manager
1188                        .increment_nonce(tx.caller(), nonce_key)
1189                        .map_err(|err| match err {
1190                            TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
1191                            err => {
1192                                TempoInvalidTransaction::NonceManagerError(err.to_string()).into()
1193                            }
1194                        })?;
1195
1196                    Ok::<_, EVMError<DB::Error, TempoInvalidTransaction>>(())
1197                },
1198            )?;
1199        } else {
1200            // Protocol nonce (nonce_key == 0)
1201            // Bump the nonce for calls. Nonce for CREATE will be bumped in `make_create_frame`.
1202            // This applies uniformly to both standard and AA transactions - we only bump here
1203            // for CALLs, letting make_create_frame handle the nonce for CREATE operations.
1204            if tx.kind().is_call() {
1205                caller_account.bump_nonce();
1206            }
1207        }
1208
1209        // calculate the new balance after the fee is collected.
1210        let new_balance = calculate_caller_fee(account_balance, tx, block, cfg)?;
1211        // doing max to avoid underflow as new_balance can be more than account
1212        // balance if `cfg.is_balance_check_disabled()` is true.
1213        let gas_balance_spending = core::cmp::max(account_balance, new_balance) - new_balance;
1214
1215        // Note: Signature verification happens during recover_signer() before entering the pool
1216        // Note: Transaction parameter validation (priority fee, time window) happens in validate_env()
1217
1218        // For Keychain signatures, validate the acting access key before fee collection when it
1219        // already exists. Same-tx auth+use is the exception: that key is registered only after fees
1220        // are collected, so fee-limit validation uses the inline authorization payload instead.
1221        let mut loaded_tx_access_key = None;
1222        let mut same_tx_key_authorization_use = false;
1223        if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
1224            && let Some(keychain_sig) = tempo_tx_env.signature.as_keychain()
1225        {
1226            // The user_address is the root account this transaction is being executed for.
1227            // This should match tx.caller (which comes from recover_signer on the outer signature).
1228            let user_address = &keychain_sig.user_address;
1229
1230            // Sanity check: user_address should match tx.caller
1231            if *user_address != tx.caller {
1232                return Err(TempoInvalidTransaction::KeychainUserAddressMismatch {
1233                    user_address: *user_address,
1234                    caller: tx.caller,
1235                }
1236                .into());
1237            }
1238
1239            // Use override_key_id if provided (for gas estimation), otherwise recover from signature.
1240            let access_key_addr = if let Some(override_key_id) = tempo_tx_env.override_key_id {
1241                override_key_id
1242            } else {
1243                keychain_sig
1244                    .key_id(&tempo_tx_env.signature_hash)
1245                    .map_err(|_| TempoInvalidTransaction::AccessKeyRecoveryFailed)?
1246            };
1247
1248            let key_auth = tempo_tx_env.key_authorization.as_ref();
1249            // Classify whether this keychain-signed tx is using the same access key that the
1250            // inline authorization registers.
1251            same_tx_key_authorization_use =
1252                key_auth.is_some_and(|key_auth| access_key_addr == key_auth.key_id);
1253
1254            if same_tx_key_authorization_use {
1255                let key_auth = key_auth.expect("same-tx auth/use requires inline authorization");
1256
1257                // Same-tx auth+use path: the access key does not exist in storage yet, so the fee
1258                // check must use the inline limits directly. `collectFeePreTx` cannot enforce this
1259                // because `transaction_key` is intentionally not set until after authorization.
1260                if !gas_balance_spending.is_zero()
1261                    && fee_payer == tx.caller
1262                    && let Some(limits) = key_auth.limits.as_ref()
1263                {
1264                    let remaining = limits
1265                        .iter()
1266                        .rev()
1267                        .find(|limit| limit.token == fee_token)
1268                        .map(|limit| limit.limit)
1269                        .unwrap_or_default();
1270
1271                    if gas_balance_spending > remaining {
1272                        return Err(
1273                            FeePaymentError::Other("SpendingLimitExceeded".to_string()).into()
1274                        );
1275                    }
1276                }
1277            } else {
1278                // Existing-key path:
1279                // - ordinary keychain txs must validate the acting access key before fees are paid
1280                // - T6 delegated key authorizations also validate the acting key here, then reuse
1281                //   the loaded admin/signature-type facts below when the sidecar signer is the same key
1282                let loaded_key = StorageCtx::enter_precompile(
1283                    journal,
1284                    block,
1285                    cfg,
1286                    tx,
1287                    actions.clone(),
1288                    |mut keychain: AccountKeychain| {
1289                        // Extract the signature type from the inner signature to validate it matches
1290                        // the key_type stored in the keychain. This prevents using a signature of one
1291                        // type to authenticate as a key registered with a different type.
1292                        // Only validate signature type on T1+ to maintain backward compatibility
1293                        // with historical blocks during re-execution.
1294                        let tx_sig_type = keychain_sig.signature.signature_type().into();
1295                        let sig_type = (key_auth.is_some() || spec.is_t1()).then_some(tx_sig_type);
1296
1297                        let key = keychain
1298                            .validate_keychain_authorization(
1299                                *user_address,
1300                                access_key_addr,
1301                                block.timestamp().to::<u64>(),
1302                                sig_type,
1303                            )
1304                            .map_err(|e| TempoInvalidTransaction::KeychainValidationFailed {
1305                                reason: format!("{e:?}"),
1306                            })?;
1307
1308                        // T6 adds admin delegation: a keychain signer may authorize a different
1309                        // child key only if the acting transaction key is itself an active admin key.
1310                        if key_auth.is_some() && !key.is_admin {
1311                            return Err(
1312                                TempoInvalidTransaction::AccessKeyCannotAuthorizeOtherKeys.into()
1313                            );
1314                        }
1315
1316                        // Set the transaction key in the keychain precompile.
1317                        // The TIP20 precompile will read this during fee collection and
1318                        // execution to enforce spending limits for existing keys.
1319                        keychain
1320                            .set_transaction_key(access_key_addr)
1321                            .map_err(|e| EVMError::Custom(e.to_string()))?;
1322
1323                        Ok::<_, EVMError<_, TempoInvalidTransaction>>(LoadedTxAccessKey {
1324                            key_id: access_key_addr,
1325                            key,
1326                        })
1327                    },
1328                )?;
1329
1330                evm.key_expiry = Some(loaded_key.key.expiry);
1331                loaded_tx_access_key = Some(loaded_key);
1332            }
1333        }
1334
1335        // T6 stateless signer/account checks run in `validate_env`. This state-aware phase only
1336        // proves that a non-root sidecar signer is an active admin key for the caller account.
1337        if cfg.spec.is_t6()
1338            && let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
1339            && let Some(key_auth) = tempo_tx_env.key_authorization.as_ref()
1340        {
1341            let auth_signer = key_auth
1342                .recover_signer()
1343                .map_err(|_| TempoInvalidTransaction::KeyAuthorizationSignatureRecoveryFailed)?;
1344
1345            if auth_signer != tx.caller {
1346                let key_auth_sig_type: u8 = key_auth.signature.signature_type().into();
1347                let signer_is_admin = match loaded_tx_access_key {
1348                    Some(loaded_key)
1349                        if loaded_key.key_id == auth_signer
1350                            && (loaded_key.key.signature_type as u8) == key_auth_sig_type =>
1351                    {
1352                        loaded_key.key.is_admin
1353                    }
1354                    Some(_) | None => {
1355                        return Err(TempoInvalidTransaction::KeychainValidationFailed {
1356                            reason:
1357                                "admin-signed key authorization must be signed by transaction key"
1358                                    .to_string(),
1359                        }
1360                        .into());
1361                    }
1362                };
1363
1364                if !signer_is_admin {
1365                    return Err(TempoInvalidTransaction::KeyAuthorizationNotSignedByRoot {
1366                        expected: tx.caller,
1367                        actual: auth_signer,
1368                    }
1369                    .into());
1370                }
1371            }
1372        }
1373
1374        // Collect fees for the transaction.
1375        if !gas_balance_spending.is_zero() {
1376            let checkpoint = journal.checkpoint();
1377
1378            let skip_liquidity_check = evm.skip_liquidity_check;
1379            let result = StorageCtx::enter_evm_without_tip1060_accounting(
1380                journal,
1381                &block,
1382                cfg,
1383                tx,
1384                actions.clone(),
1385                || {
1386                    TipFeeManager::new().collect_fee_pre_tx(
1387                        fee_payer,
1388                        fee_token,
1389                        gas_balance_spending,
1390                        block.beneficiary(),
1391                        skip_liquidity_check,
1392                    )
1393                },
1394            );
1395
1396            if let Err(err) = result {
1397                // Revert the journal to checkpoint before `collectFeePreTx` call if something went wrong.
1398                journal.checkpoint_revert(checkpoint);
1399
1400                // Map fee collection errors to transaction validation errors since they
1401                // indicate the transaction cannot be included (e.g., insufficient liquidity
1402                // in FeeAMM pool for fee swaps)
1403                return Err(match err {
1404                    TempoPrecompileError::TIPFeeAMMError(
1405                        TIPFeeAMMError::InsufficientLiquidity(_),
1406                    ) => FeePaymentError::InsufficientAmmLiquidity {
1407                        fee: gas_balance_spending,
1408                    }
1409                    .into(),
1410
1411                    TempoPrecompileError::TIP20(TIP20Error::InsufficientBalance(
1412                        InsufficientBalance { available, .. },
1413                    )) => FeePaymentError::InsufficientFeeTokenBalance {
1414                        fee: gas_balance_spending,
1415                        balance: available,
1416                    }
1417                    .into(),
1418
1419                    TempoPrecompileError::TIP20(TIP20Error::ContractPaused(_)) => {
1420                        TempoInvalidTransaction::FeeTokenPaused { address: fee_token }.into()
1421                    }
1422
1423                    TempoPrecompileError::Fatal(e) => EVMError::Custom(e),
1424
1425                    _ => FeePaymentError::Other(err.to_string()).into(),
1426                });
1427            }
1428
1429            journal.checkpoint_commit();
1430            evm.collected_fee = gas_balance_spending;
1431        }
1432
1433        // If the transaction includes a KeyAuthorization, validate and authorize the key
1434        // only after fee collection has succeeded. This pre-execution write is deliberately
1435        // outside the later user-call batch checkpoint, so same-transaction authorize-and-use
1436        // keeps the newly registered key even if scoped-call prevalidation or execution fails.
1437        if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
1438            && let Some(key_auth) = &tempo_tx_env.key_authorization
1439        {
1440            let keychain_checkpoint = if spec.is_t1() {
1441                Some(journal.checkpoint())
1442            } else {
1443                None
1444            };
1445
1446            let amsterdam_eip8037_enabled = cfg.enable_amsterdam_eip8037;
1447            let internals = EvmInternals::new(journal, block, cfg, tx);
1448
1449            // T1/T1A: Apply gas metering for the keychain precompile call.
1450            // Pre-T1 and T1B+: Use unlimited gas.
1451            // T1B+ disables gas metering here because gas is already accounted for
1452            // in intrinsic gas via `calculate_key_authorization_gas`. Running with
1453            // unlimited gas also eliminates the OOG path that caused the CREATE
1454            // nonce replay vulnerability (protocol nonce not bumped on OOG).
1455            let gas_limit = if spec.is_t1() && !spec.is_t1b() {
1456                tx.gas_limit() - init_gas.initial_total_gas()
1457            } else {
1458                u64::MAX
1459            };
1460
1461            // Create gas_params with only sstore increase for key authorization
1462            let gas_params = if spec.is_t1() {
1463                static TABLE: OnceLock<GasParams> = OnceLock::new();
1464                // only enabled SSTORE and warm storage read gas params for T1 fork in keychain.
1465                TABLE
1466                    .get_or_init(|| {
1467                        let mut table = [0u64; 256];
1468                        table[GasId::sstore_set_without_load_cost().as_usize()] =
1469                            cfg.gas_params.get(GasId::sstore_set_without_load_cost());
1470                        table[GasId::warm_storage_read_cost().as_usize()] =
1471                            cfg.gas_params.get(GasId::warm_storage_read_cost());
1472                        GasParams::new(Arc::new(table))
1473                    })
1474                    .clone()
1475            } else {
1476                cfg.gas_params.clone()
1477            };
1478
1479            // It's ok to set reservoir to 0 because pre-T1B it doesn't matter and post-T1B we have unlimited gas anyway.
1480            let mut provider = EvmPrecompileStorageProvider::new(
1481                internals,
1482                gas_limit,
1483                0,
1484                cfg.spec,
1485                amsterdam_eip8037_enabled,
1486                false,
1487                gas_params,
1488            )
1489            .with_actions(actions.clone());
1490            provider.set_tip1060_storage_credits(false);
1491
1492            // The core logic of setting up thread-local storage is here.
1493            let out_of_gas = StorageCtx::enter(&mut provider, || {
1494                let mut keychain = AccountKeychain::default();
1495                let access_key_addr = key_auth.key_id;
1496
1497                // Convert signature type to precompile SignatureType enum
1498                // Use the key_type field which specifies the type of key being authorized
1499                let signature_type = match key_auth.key_type {
1500                    SignatureType::Secp256k1 => PrecompileSignatureType::Secp256k1,
1501                    SignatureType::P256 => PrecompileSignatureType::P256,
1502                    SignatureType::WebAuthn => PrecompileSignatureType::WebAuthn,
1503                };
1504
1505                // Handle expiry: None means never expires (store as u64::MAX)
1506                let expiry = key_auth.expiry.map_or(u64::MAX, |expiry| expiry.get());
1507
1508                // Handle limits: None means unlimited spending (enforce_limits=false)
1509                // Some([]) means no spending allowed (enforce_limits=true)
1510                // Some([...]) means specific limits (enforce_limits=true)
1511                let enforce_limits = key_auth.limits.is_some();
1512                let precompile_limits: Vec<TokenLimit> = key_auth
1513                    .limits
1514                    .as_ref()
1515                    .map(|limits| {
1516                        limits
1517                            .iter()
1518                            .map(|limit| TokenLimit {
1519                                token: limit.token,
1520                                amount: limit.limit,
1521                                period: limit.period,
1522                            })
1523                            .collect()
1524                    })
1525                    .unwrap_or_default();
1526
1527                let allow_any_calls = key_auth.allowed_calls.is_none();
1528                let precompile_allowed_calls = translate_allowed_calls_for_precompile(key_auth);
1529
1530                let config = KeyRestrictions {
1531                    expiry,
1532                    enforceLimits: enforce_limits,
1533                    limits: precompile_limits,
1534                    allowAnyCalls: allow_any_calls,
1535                    allowedCalls: precompile_allowed_calls,
1536                };
1537
1538                // Call precompile to authorize the key (same phase as nonce increment).
1539                let result = if key_auth.is_admin() {
1540                    keychain.authorize_admin_key(
1541                        tx.caller,
1542                        access_key_addr,
1543                        signature_type,
1544                        key_auth.witness(),
1545                    )
1546                } else {
1547                    keychain.authorize_key(
1548                        tx.caller,
1549                        access_key_addr,
1550                        signature_type,
1551                        config,
1552                        key_auth.witness(),
1553                    )
1554                };
1555
1556                match result {
1557                    // all is good, we can do execution.
1558                    Ok(_) => Ok(false),
1559                    // on out of gas we are skipping execution but not invalidating the transaction.
1560                    Err(TempoPrecompileError::OutOfGas) => Ok(true),
1561                    Err(TempoPrecompileError::Fatal(err)) => Err(EVMError::Custom(err)),
1562                    Err(err) => Err(TempoInvalidTransaction::KeychainPrecompileError {
1563                        reason: err.to_string(),
1564                    }
1565                    .into()),
1566                }
1567            })?;
1568
1569            let gas_used = provider.gas_used();
1570            drop(provider);
1571
1572            // Cache inline key authorization expiry.
1573            if let Some(expiry) = key_auth.expiry {
1574                evm.key_expiry = Some(expiry.get());
1575            }
1576
1577            // activated only on T1/T1A fork.
1578            // T1B+: Skip adding precompile gas to initial_gas since it is already
1579            // accounted for in intrinsic gas. The precompile runs with unlimited gas
1580            // on T1B+ so out_of_gas is never true.
1581            if let Some(keychain_checkpoint) = keychain_checkpoint {
1582                if spec.is_t1b() {
1583                    journal.checkpoint_commit();
1584                } else if out_of_gas {
1585                    init_gas.initial_regular_gas = u64::MAX;
1586                    journal.checkpoint_revert(keychain_checkpoint);
1587                } else {
1588                    init_gas.initial_regular_gas += gas_used;
1589                    journal.checkpoint_commit();
1590                };
1591            }
1592
1593            // If this is a same tx auth+use, set the transient key_id to the newly authorized
1594            // key and decrement the fee from its spending limit. Admin delegation must keep the
1595            // actual signer as the transaction key.
1596            if same_tx_key_authorization_use {
1597                StorageCtx::enter_evm_without_tip1060_accounting(
1598                    journal,
1599                    block,
1600                    cfg,
1601                    tx,
1602                    actions,
1603                    || {
1604                        let mut keychain = AccountKeychain::new();
1605                        keychain
1606                            .set_transaction_key(key_auth.key_id)
1607                            .map_err(|e| EVMError::Custom(e.to_string()))?;
1608
1609                        if evm.collected_fee.is_zero() {
1610                            return Ok(());
1611                        }
1612
1613                        keychain
1614                            .authorize_transfer(fee_payer, fee_token, evm.collected_fee)
1615                            .map_err(|err| match err {
1616                                TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
1617                                err => FeePaymentError::Other(err.to_string()).into(),
1618                            })
1619                    },
1620                )?;
1621            }
1622        }
1623
1624        Ok(())
1625    }
1626
1627    fn reimburse_caller(
1628        &self,
1629        evm: &mut Self::Evm,
1630        exec_result: &mut FrameResult,
1631    ) -> Result<(), Self::Error> {
1632        let actions = evm.actions.clone();
1633        // Call collectFeePostTx on TipFeeManager precompile
1634        let context = &mut evm.inner.ctx;
1635        let tx = context.tx();
1636        let basefee = u128::from(context.block().basefee());
1637        let effective_gas_price = tx.effective_gas_price(basefee);
1638        let gas = exec_result.gas();
1639
1640        let actual_spending = calc_gas_balance_spending(
1641            gas.used().saturating_sub(gas.reservoir()),
1642            effective_gas_price,
1643        );
1644        let refund_amount = tx.effective_balance_spending(
1645            context.block.basefee.into(),
1646            context.block.blob_gasprice().unwrap_or_default(),
1647        )? - tx.value
1648            - actual_spending;
1649
1650        // Skip `collectFeePostTx` call if the initial fee collected in
1651        // `collectFeePreTx` was zero, but spending is non-zero.
1652        //
1653        // This is normally unreachable unless the gas price was increased mid-transaction,
1654        // which is only possible when there are some EVM customizations involved (e.g Foundry EVM).
1655        if context.cfg.disable_fee_charge
1656            && evm.collected_fee.is_zero()
1657            && !actual_spending.is_zero()
1658        {
1659            return Ok(());
1660        }
1661
1662        // Create storage provider and fee manager
1663        let (journal, block, tx) = (&mut context.journaled_state, &context.block, &context.tx);
1664        let beneficiary = context.block.beneficiary();
1665
1666        let credited = StorageCtx::enter_evm_without_tip1060_accounting(
1667            &mut *journal,
1668            block,
1669            &context.cfg,
1670            tx,
1671            actions,
1672            || {
1673                let mut fee_manager = TipFeeManager::new();
1674
1675                if !actual_spending.is_zero() || !refund_amount.is_zero() {
1676                    let fee_payer = tx.fee_payer().expect("pre-validated in `validate_env`");
1677                    let fee_token = evm
1678                        .fee_token
1679                        .expect("set in `validate_against_state_and_deduct_caller`");
1680                    // Call collectFeePostTx (handles both refund and fee queuing)
1681                    fee_manager
1682                        .collect_fee_post_tx(
1683                            fee_payer,
1684                            actual_spending,
1685                            refund_amount,
1686                            fee_token,
1687                            beneficiary,
1688                        )
1689                        .map_err(|e| EVMError::Custom(format!("{e:?}")))
1690                } else {
1691                    Ok(U256::ZERO)
1692                }
1693            },
1694        )?;
1695
1696        // Stash the per-tx credit so `TempoBlockExecutor` can surface it on `TempoTxResult`
1697        // for payload scoring. Reset to zero on every tx entry below in `validate_env`.
1698        evm.validator_fee = credited;
1699        Ok(())
1700    }
1701
1702    #[inline]
1703    fn reward_beneficiary(
1704        &self,
1705        _evm: &mut Self::Evm,
1706        _exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
1707    ) -> Result<(), Self::Error> {
1708        // Fee handling (refunds and swaps) are done in `reimburse_caller()` via `collectFeePostTx`.
1709        // Validators call distributeFees() to claim their accumulated fees.
1710        Ok(())
1711    }
1712
1713    /// Validates transaction environment with custom handling for AA transactions.
1714    ///
1715    /// Performs standard validation plus AA-specific checks:
1716    /// - Priority fee validation (EIP-1559)
1717    /// - Time window validation (validAfter/validBefore)
1718    #[inline]
1719    fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
1720        // Reset per-tx fee state.
1721        evm.collected_fee = U256::ZERO;
1722        evm.validator_fee = U256::ZERO;
1723
1724        // Validate the fee payer signature
1725        let fee_payer = evm.ctx.tx.fee_payer()?;
1726
1727        if evm.ctx.cfg.spec.is_t2()
1728            && evm.ctx.tx.has_fee_payer_signature()
1729            && fee_payer == evm.ctx.tx.caller()
1730        {
1731            return Err(TempoInvalidTransaction::SelfSponsoredFeePayer.into());
1732        }
1733
1734        // All accounts have zero balance so transfer of value is not possible.
1735        // Check added in https://github.com/tempoxyz/tempo/pull/759
1736        if !evm.ctx.tx.value().is_zero() {
1737            return Err(TempoInvalidTransaction::ValueTransferNotAllowed.into());
1738        }
1739
1740        // First perform standard validation (header + transaction environment)
1741        // This validates: prevrandao, excess_blob_gas, chain_id, gas limits, tx type support, etc.
1742        validation::validate_env::<_, Self::Error>(evm.ctx())?;
1743
1744        // AA-specific validations
1745        let cfg = &evm.inner.cfg;
1746        let tx = &evm.inner.tx;
1747
1748        if let Some(aa_env) = tx.tempo_tx_env.as_ref() {
1749            // Validate AA transaction structure (calls list, CREATE rules)
1750            validate_calls(
1751                &aa_env.aa_calls,
1752                !aa_env.tempo_authorization_list.is_empty(),
1753            )
1754            .map_err(TempoInvalidTransaction::from)?;
1755
1756            // Access-key CREATE is a cheap structural rejection that does not depend on any
1757            // per-call scope walk or state mutation. Rejecting it here keeps validation work
1758            // constant and avoids entering CREATE execution paths that require special protocol-
1759            // nonce preservation on failure.
1760            if cfg.spec().is_t3()
1761                && aa_env.signature.is_keychain()
1762                && aa_env
1763                    .aa_calls
1764                    .first()
1765                    .is_some_and(|call| call.to.is_create())
1766            {
1767                return Err(TempoInvalidTransaction::CallsValidation(
1768                    "access-key transactions cannot use CREATE as the first call",
1769                )
1770                .into());
1771            }
1772
1773            // Validate keychain signature version (outer + authorization list).
1774            aa_env
1775                .signature
1776                .validate_version(cfg.spec().is_t1c())
1777                .map_err(TempoInvalidTransaction::from)?;
1778            for auth in &aa_env.tempo_authorization_list {
1779                auth.signature()
1780                    .validate_version(cfg.spec().is_t1c())
1781                    .map_err(TempoInvalidTransaction::from)?;
1782            }
1783
1784            let has_keychain_fields =
1785                aa_env.key_authorization.is_some() || aa_env.signature.is_keychain();
1786
1787            if aa_env.subblock_transaction && has_keychain_fields {
1788                return Err(TempoInvalidTransaction::KeychainOpInSubblockTransaction.into());
1789            }
1790
1791            if let Some(key_auth) = &aa_env.key_authorization {
1792                // Check if this TX is using a Keychain signature (access key). Non-admin access
1793                // keys cannot authorize other keys; T6 admin keys can.
1794                let mut same_tx_auth_use = false;
1795                if let Some(keychain_sig) = aa_env.signature.as_keychain() {
1796                    // Use override_key_id if provided (for gas estimation), otherwise recover from signature
1797                    let access_key_addr = if let Some(override_key_id) = aa_env.override_key_id {
1798                        override_key_id
1799                    } else {
1800                        // Get the access key address (recovered during Tx->TxEnv conversion and cached)
1801                        keychain_sig
1802                            .key_id(&aa_env.signature_hash)
1803                            .map_err(|_| TempoInvalidTransaction::AccessKeyRecoveryFailed)?
1804                    };
1805
1806                    same_tx_auth_use = access_key_addr == key_auth.key_id;
1807                    if !same_tx_auth_use && !cfg.spec.is_t6() {
1808                        return Err(
1809                            TempoInvalidTransaction::AccessKeyCannotAuthorizeOtherKeys.into()
1810                        );
1811                    }
1812
1813                    if same_tx_auth_use
1814                        && cfg.spec.is_t3()
1815                        && key_auth.key_type != keychain_sig.signature.signature_type()
1816                    {
1817                        return Err(TempoInvalidTransaction::KeychainValidationFailed {
1818                                reason: "key authorization key_type does not match the keychain signature type"
1819                                    .to_string(),
1820                            }
1821                            .into());
1822                    }
1823                }
1824
1825                if (key_auth.is_admin || key_auth.account.is_some()) && !cfg.spec.is_t6() {
1826                    return Err(TempoInvalidTransaction::KeychainValidationFailed {
1827                        reason: "T6 key authorization fields are not active before T6".to_string(),
1828                    }
1829                    .into());
1830                }
1831
1832                if cfg.spec.is_t6() && key_auth.account.is_some_and(|account| account != tx.caller)
1833                {
1834                    // T6 allows existing admin keys to sign `KeyAuthorization`s for an
1835                    // account. Any named account must match the transaction caller so the
1836                    // signed payload cannot be replayed against another account where the
1837                    // same admin key is also authorized.
1838                    let reason = if key_auth.is_admin() {
1839                        "admin key authorization account mismatch"
1840                    } else {
1841                        "key authorization account mismatch"
1842                    };
1843
1844                    return Err(TempoInvalidTransaction::KeychainValidationFailed {
1845                        reason: reason.to_string(),
1846                    }
1847                    .into());
1848                }
1849
1850                if key_auth.is_admin()
1851                    && (key_auth.expiry.is_some()
1852                        || key_auth.limits.is_some()
1853                        || key_auth.allowed_calls.is_some())
1854                {
1855                    return Err(TempoInvalidTransaction::KeychainValidationFailed {
1856                        reason:
1857                            "admin key authorizations cannot carry expiry, limits, or call scopes"
1858                                .to_string(),
1859                    }
1860                    .into());
1861                }
1862
1863                if !cfg.spec.is_t6() {
1864                    let auth_signer = key_auth.recover_signer().map_err(|_| {
1865                        TempoInvalidTransaction::KeyAuthorizationSignatureRecoveryFailed
1866                    })?;
1867
1868                    if auth_signer != tx.caller {
1869                        return Err(TempoInvalidTransaction::KeyAuthorizationNotSignedByRoot {
1870                            expected: tx.caller,
1871                            actual: auth_signer,
1872                        }
1873                        .into());
1874                    }
1875                }
1876
1877                // Validate KeyAuthorization chain_id.
1878                // T1C+: chain_id must exactly match (wildcard 0 is no longer allowed).
1879                // Pre-T1C: chain_id == 0 allows replay on any chain (wildcard).
1880                key_auth
1881                    .validate_chain_id(cfg.chain_id(), cfg.spec.is_t1c())
1882                    .map_err(TempoInvalidTransaction::from)?;
1883
1884                if key_auth.has_witness() && !cfg.spec.is_t5() {
1885                    return Err(TempoInvalidTransaction::KeychainValidationFailed {
1886                        reason: "key authorization witnesses are not active before T5".to_string(),
1887                    }
1888                    .into());
1889                }
1890
1891                // T3 gates all TIP-1011 fields. Before activation, transaction semantics must stay
1892                // unchanged, so periodic limits and call scopes are rejected.
1893                if !cfg.spec.is_t3() {
1894                    if key_auth.has_periodic_limits() {
1895                        return Err(TempoInvalidTransaction::KeychainValidationFailed {
1896                            reason: "periodic token limits are not active before T3".to_string(),
1897                        }
1898                        .into());
1899                    }
1900
1901                    if key_auth.has_call_scopes() {
1902                        return Err(TempoInvalidTransaction::KeychainValidationFailed {
1903                            reason: "call scopes are not active before T3".to_string(),
1904                        }
1905                        .into());
1906                    }
1907                }
1908
1909                if cfg.spec.is_t6() {
1910                    let auth_signer = key_auth.recover_signer().map_err(|_| {
1911                        TempoInvalidTransaction::KeyAuthorizationSignatureRecoveryFailed
1912                    })?;
1913                    if auth_signer != tx.caller && key_auth.account.is_none() {
1914                        return Err(TempoInvalidTransaction::KeychainValidationFailed {
1915                            reason: "admin-signed key authorization account mismatch".to_string(),
1916                        }
1917                        .into());
1918                    }
1919
1920                    if auth_signer == tx.caller
1921                        && aa_env.signature.is_keychain()
1922                        && !same_tx_auth_use
1923                    {
1924                        return Err(TempoInvalidTransaction::KeychainValidationFailed {
1925                            reason:
1926                                "root-signed key authorization must use root transaction signature"
1927                                    .to_string(),
1928                        }
1929                        .into());
1930                    }
1931
1932                    if auth_signer != tx.caller {
1933                        let Some(keychain_sig) = aa_env.signature.as_keychain() else {
1934                            return Err(TempoInvalidTransaction::KeychainValidationFailed {
1935                                reason:
1936                                    "admin-signed key authorization must be signed by transaction key"
1937                                        .to_string(),
1938                            }
1939                            .into());
1940                        };
1941
1942                        let access_key_addr = if let Some(override_key_id) = aa_env.override_key_id
1943                        {
1944                            override_key_id
1945                        } else {
1946                            keychain_sig
1947                                .key_id(&aa_env.signature_hash)
1948                                .map_err(|_| TempoInvalidTransaction::AccessKeyRecoveryFailed)?
1949                        };
1950
1951                        if access_key_addr != auth_signer {
1952                            return Err(TempoInvalidTransaction::KeychainValidationFailed {
1953                                reason:
1954                                    "admin-signed key authorization must be signed by transaction key"
1955                                        .to_string(),
1956                            }
1957                            .into());
1958                        }
1959
1960                        if key_auth.signature.signature_type()
1961                            != keychain_sig.signature.signature_type()
1962                        {
1963                            return Err(TempoInvalidTransaction::KeychainValidationFailed {
1964                                reason:
1965                                    "admin-signed key authorization signature type does not match transaction key signature type"
1966                                        .to_string(),
1967                            }
1968                            .into());
1969                        }
1970                    }
1971                }
1972
1973                // Cache inline key authorization expiry.
1974                if let Some(expiry) = key_auth.expiry {
1975                    evm.key_expiry = Some(expiry.get());
1976                }
1977            }
1978
1979            // Validate priority fee for AA transactions using revm's validate_priority_fee_tx
1980            let base_fee = if cfg.is_base_fee_check_disabled() {
1981                None
1982            } else {
1983                Some(u128::from(evm.ctx_ref().block().basefee()))
1984            };
1985
1986            validation::validate_priority_fee_tx(
1987                tx.max_fee_per_gas(),
1988                tx.max_priority_fee_per_gas().unwrap_or_default(),
1989                base_fee,
1990                cfg.is_priority_fee_check_disabled(),
1991            )?;
1992
1993            // Validate time window for AA transactions
1994            let block_timestamp = evm.ctx_ref().block().timestamp().saturating_to();
1995            let valid_after = aa_env.valid_after.filter(|_| !evm.skip_valid_after_check);
1996            validate_time_window(valid_after, aa_env.valid_before, block_timestamp)?;
1997        }
1998
1999        Ok(())
2000    }
2001
2002    /// Calculates initial gas costs with custom handling for AA transactions.
2003    ///
2004    /// AA transactions have variable intrinsic gas based on signature type:
2005    /// - secp256k1 (64/65 bytes): Standard 21k base
2006    /// - P256 (129 bytes): 21k base + 5k for P256 verification
2007    /// - WebAuthn (>129 bytes): 21k base + 5k + calldata gas for variable data
2008    #[inline]
2009    fn validate_initial_tx_gas(
2010        &self,
2011        evm: &mut Self::Evm,
2012    ) -> Result<InitialAndFloorGas, Self::Error> {
2013        let tx = evm.ctx_ref().tx();
2014        let spec = evm.ctx_ref().cfg().spec();
2015        let gas_params = evm.ctx_ref().cfg().gas_params();
2016        let gas_limit = tx.gas_limit();
2017
2018        // Route to appropriate gas calculation and validation based on transaction type
2019        let mut init_gas = if tx.tempo_tx_env.is_some() {
2020            // AA transaction - use batch gas calculation (includes validation)
2021            validate_aa_initial_tx_gas(evm)?
2022        } else {
2023            let mut acc = 0;
2024            let mut storage = 0;
2025            // legacy is only tx type that does not have access list.
2026            if tx.tx_type() != TransactionType::Legacy {
2027                (acc, storage) = tx
2028                    .access_list()
2029                    .map(|al| {
2030                        al.fold((0, 0), |(acc, storage), item| {
2031                            (acc + 1, storage + item.storage_slots().count())
2032                        })
2033                    })
2034                    .unwrap_or_default();
2035            };
2036            let mut init_gas = gas_params.initial_tx_gas(
2037                tx.input(),
2038                tx.kind().is_create(),
2039                acc as u64,
2040                storage as u64,
2041                tx.authorization_list_len() as u64,
2042            );
2043            // TIP-1000: Storage pricing updates for launch
2044            // EIP-7702 authorisation list entries with `auth_list.nonce == 0` require an additional 250,000 gas.
2045            // no need for v1 fork check as gas_params would be zero
2046            for auth in tx.authorization_list() {
2047                if spec.is_t1() && auth.nonce == 0 {
2048                    init_gas.initial_regular_gas += gas_params.get(GasId::new_account_cost());
2049                    init_gas.initial_state_gas += gas_params.new_account_state_gas();
2050                }
2051            }
2052
2053            // TIP-1000: Storage pricing updates for launch
2054            // Transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas.
2055            if spec.is_t1() && tx.nonce == 0 {
2056                // Add both execution and state portions to initial_total_gas
2057                // (revm's invariant: initial_total_gas >= initial_state_gas)
2058                init_gas.initial_regular_gas += gas_params.get(GasId::new_account_cost());
2059                init_gas.initial_state_gas += gas_params.new_account_state_gas();
2060            }
2061
2062            // Validate gas limit is sufficient for initial gas
2063            if gas_limit < init_gas.initial_total_gas() {
2064                return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
2065                    gas_limit,
2066                    initial_gas: init_gas.initial_total_gas(),
2067                }
2068                .into());
2069            }
2070
2071            init_gas
2072        };
2073
2074        if evm.ctx.cfg.is_eip7623_disabled() {
2075            init_gas.floor_gas = 0u64;
2076        }
2077
2078        // Validate floor gas (Prague+)
2079        if gas_limit < init_gas.floor_gas {
2080            return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
2081                gas_limit,
2082                gas_floor: init_gas.floor_gas,
2083            }
2084            .into());
2085        }
2086
2087        // Validate that regular gas does not exceed the cap.
2088        if evm.ctx.cfg.is_amsterdam_eip8037_enabled()
2089            && init_gas.initial_regular_gas().max(init_gas.floor_gas)
2090                > evm.ctx.cfg.tx_gas_limit_cap()
2091        {
2092            return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
2093                gas_floor: init_gas.initial_regular_gas(),
2094                gas_limit: evm.ctx.cfg.tx_gas_limit_cap(),
2095            }
2096            .into());
2097        }
2098
2099        Ok(init_gas)
2100    }
2101
2102    fn catch_error(
2103        &self,
2104        evm: &mut Self::Evm,
2105        error: Self::Error,
2106    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
2107        evm.clear();
2108
2109        // For subblock transactions that failed `collectFeePreTx` call we catch error and treat such transactions as valid.
2110        if evm.ctx.tx.is_subblock_transaction()
2111            && let Some(
2112                TempoInvalidTransaction::CollectFeePreTx(_)
2113                | TempoInvalidTransaction::FeeTokenPaused { .. }
2114                | TempoInvalidTransaction::EthInvalidTransaction(
2115                    InvalidTransaction::LackOfFundForMaxFee { .. },
2116                ),
2117            ) = error.as_invalid_tx_err()
2118        {
2119            // Commit the transaction.
2120            //
2121            // `collectFeePreTx` call will happen after the nonce bump so this will only commit the nonce increment.
2122            evm.ctx.journaled_state.commit_tx();
2123
2124            evm.ctx().local_mut().clear();
2125            evm.frame_stack().clear();
2126
2127            // On fee payment failure, treat the transaction as a halt that consumed entire regular gas limit.
2128            let total_spent = core::cmp::min(evm.ctx.tx.gas_limit, evm.ctx.cfg.tx_gas_limit_cap());
2129
2130            Ok(ExecutionResult::Halt {
2131                reason: TempoHaltReason::SubblockTxFeePayment,
2132                logs: Default::default(),
2133                gas: ResultGas::new_with_state_gas(total_spent, 0, 0, 0),
2134            })
2135        } else {
2136            MainnetHandler::default()
2137                .catch_error(evm, error)
2138                .map(|result| result.map_haltreason(Into::into))
2139        }
2140    }
2141}
2142
2143impl<DB, I> TempoEvmHandler<DB, I>
2144where
2145    DB: alloy_evm::Database,
2146{
2147    /// Runs the full transaction validation pipeline without executing the transaction.
2148    ///
2149    /// Returns a [`ValidationContext`] with context relevant for the transaction pool.
2150    pub fn validate_transaction(
2151        &mut self,
2152        evm: &mut TempoEvm<DB, I>,
2153    ) -> Result<ValidationContext, EVMError<DB::Error, TempoInvalidTransaction>> {
2154        let mut init_and_floor_gas = self.validate(evm)?;
2155        self.pre_execution(evm, &mut init_and_floor_gas)?;
2156        let result = ValidationContext {
2157            fee_token: evm
2158                .fee_token
2159                .expect("set in `validate_against_state_and_deduct_caller`"),
2160            key_expiry: evm.key_expiry,
2161        };
2162        evm.clear();
2163        Ok(result)
2164    }
2165}
2166
2167/// Context returned by [`TempoEvmHandler::validate_transaction`] with resolved
2168/// fee token and key expiry information for use by the transaction pool.
2169#[derive(Debug, Clone)]
2170pub struct ValidationContext {
2171    /// The resolved fee token address used to pay for this transaction.
2172    pub fee_token: Address,
2173    /// The expiry timestamp of the access key used by this transaction.
2174    /// Populated for keychain-signed transactions or transactions carrying a KeyAuthorization.
2175    pub key_expiry: Option<u64>,
2176}
2177
2178/// Calculates intrinsic gas for an AA transaction batch using revm helpers.
2179///
2180/// This includes:
2181/// - Base 21k stipend (once for the transaction)
2182/// - Signature verification gas (P256: 5k, WebAuthn: 5k + webauthn_data)
2183/// - Per-call account access cost (COLD_ACCOUNT_ACCESS_COST * calls.len())
2184/// - Per-call input data gas (calldata tokens * 4 gas)
2185/// - Per-call CREATE costs (if applicable):
2186///   - Additional 32k base (CREATE constant)
2187///   - Initcode analysis gas (2 per 32-byte chunk, Shanghai+)
2188/// - Check that value transfer is zero.
2189/// - Access list costs (shared across batch)
2190/// - Key authorization costs (if present):
2191///   - Pre-T1B: 27k base + 3k ecrecover + 22k per spending limit
2192///   - T1B+: ecrecover + SLOAD + SSTORE × (1 + N limits)
2193/// - Floor gas calculation (EIP-7623, Prague+)
2194pub fn calculate_aa_batch_intrinsic_gas<'a>(
2195    aa_env: &TempoBatchCallEnv,
2196    gas_params: &GasParams,
2197    access_list: Option<impl Iterator<Item = &'a AccessListItem>>,
2198    spec: tempo_chainspec::hardfork::TempoHardfork,
2199) -> Result<InitialAndFloorGas, TempoInvalidTransaction> {
2200    let calls = &aa_env.aa_calls;
2201    let signature = &aa_env.signature;
2202    let authorization_list = &aa_env.tempo_authorization_list;
2203    let key_authorization = aa_env.key_authorization.as_ref();
2204    let mut gas = InitialAndFloorGas::default();
2205
2206    // 1. Base stipend (21k, once per transaction)
2207    gas.initial_regular_gas += gas_params.tx_base_stipend();
2208
2209    // 2. Signature verification gas
2210    gas.initial_regular_gas += tempo_signature_verification_gas(signature);
2211
2212    let cold_account_cost =
2213        gas_params.warm_storage_read_cost() + gas_params.cold_account_additional_cost();
2214
2215    // 3. Per-call overhead: cold account access
2216    // if the `to` address has not appeared in the call batch before.
2217    gas.initial_regular_gas += cold_account_cost * calls.len().saturating_sub(1) as u64;
2218
2219    // 4. Authorization list costs (EIP-7702)
2220    let num_auths = authorization_list.len() as u64;
2221    gas.initial_regular_gas +=
2222        num_auths * gas_params.get(GasId::tx_eip7702_per_empty_account_cost());
2223    // TIP-1016: Track state gas portion of per-auth cost (225k on T4, 0 pre-T4).
2224    gas.initial_state_gas += num_auths * gas_params.tx_eip7702_state_gas();
2225
2226    // Add signature verification costs for each authorization
2227    // No need for v1 fork check as gas_params would be zero
2228    for auth in authorization_list {
2229        gas.initial_regular_gas += tempo_signature_verification_gas(auth.signature());
2230        // TIP-1000: Storage pricing updates for launch
2231        // EIP-7702 authorisation list entries with `auth_list.nonce == 0` require an additional 250,000 gas.
2232        if spec.is_t1() && auth.nonce == 0 {
2233            gas.initial_regular_gas += gas_params.get(GasId::new_account_cost());
2234            gas.initial_state_gas += gas_params.new_account_state_gas();
2235        }
2236    }
2237
2238    // 5. Key authorization costs (if present)
2239    if let Some(key_auth) = key_authorization {
2240        let (key_auth_regular_gas, key_auth_state_gas) =
2241            calculate_key_authorization_gas(key_auth, gas_params, spec);
2242        gas.initial_regular_gas += key_auth_regular_gas;
2243        gas.initial_state_gas += key_auth_state_gas;
2244    }
2245
2246    // 6. Per-call costs
2247    let mut total_tokens = 0u64;
2248
2249    for call in calls {
2250        // 4a. Calldata gas using revm helper
2251        let tokens = get_tokens_in_calldata_istanbul(&call.input);
2252        total_tokens += tokens;
2253
2254        // 4b. CREATE-specific costs
2255        if call.to.is_create() {
2256            // CREATE costs 500,000 gas in TIP-1000 (T1), 32,000 before
2257            gas.initial_regular_gas += gas_params.create_cost();
2258
2259            // EIP-3860: Initcode analysis gas using revm helper
2260            gas.initial_regular_gas += gas_params.tx_initcode_cost(call.input.len());
2261
2262            // TIP-1016: Track predictable state gas for CREATE calls
2263            gas.initial_state_gas += gas_params.create_state_gas();
2264        }
2265
2266        // Note: Transaction value is not allowed in AA transactions as there is no balances in accounts yet.
2267        // Check added in https://github.com/tempoxyz/tempo/pull/759
2268        if !call.value.is_zero() {
2269            return Err(TempoInvalidTransaction::ValueTransferNotAllowedInAATx);
2270        }
2271
2272        // 4c. Value transfer cost using revm constant
2273        // left here for future reference.
2274        if !call.value.is_zero() && call.to.is_call() {
2275            gas.initial_regular_gas += gas_params.get(GasId::transfer_value_cost()); // 9000 gas
2276        }
2277    }
2278
2279    gas.initial_regular_gas += total_tokens * gas_params.tx_token_cost();
2280
2281    // 5. Access list costs using revm constants
2282    if let Some(access_list) = access_list {
2283        let (accounts, storages) = access_list.fold((0, 0), |(acc_count, storage_count), item| {
2284            (acc_count + 1, storage_count + item.storage_slots().count())
2285        });
2286        gas.initial_regular_gas += accounts * gas_params.tx_access_list_address_cost(); // 2400 per account
2287        gas.initial_regular_gas += storages as u64 * gas_params.tx_access_list_storage_key_cost(); // 1900 per storage
2288    }
2289
2290    // 6. Floor gas using revm helper
2291    gas.floor_gas = gas_params.tx_floor_cost_with_tokens(total_tokens); // tokens * 10 + 21000
2292
2293    Ok(gas)
2294}
2295
2296/// Validates and calculates initial transaction gas for AA transactions.
2297///
2298/// Calculates intrinsic gas based on:
2299/// - Signature type (secp256k1: 21k, P256: 26k, WebAuthn: 26k + calldata)
2300/// - Batch call costs (per-call overhead, calldata, CREATE, value transfers)
2301fn validate_aa_initial_tx_gas<DB, I>(
2302    evm: &TempoEvm<DB, I>,
2303) -> Result<InitialAndFloorGas, EVMError<DB::Error, TempoInvalidTransaction>>
2304where
2305    DB: alloy_evm::Database,
2306{
2307    let (_, tx, cfg, _, _, _, _) = evm.ctx_ref().all();
2308    let gas_limit = tx.gas_limit();
2309    let gas_params = cfg.gas_params();
2310    let spec = *cfg.spec();
2311
2312    // This function should only be called for AA transactions
2313    let aa_env = tx
2314        .tempo_tx_env
2315        .as_ref()
2316        .expect("validate_aa_initial_tx_gas called for non-AA transaction");
2317
2318    let calls = &aa_env.aa_calls;
2319
2320    // Validate all CREATE calls' initcode size upfront (EIP-3860)
2321    let max_initcode_size = evm.ctx_ref().cfg().max_initcode_size();
2322    for call in calls {
2323        if call.to.is_create() && call.input.len() > max_initcode_size {
2324            return Err(InvalidTransaction::CreateInitCodeSizeLimit.into());
2325        }
2326    }
2327
2328    // Calculate batch intrinsic gas using helper
2329    let mut batch_gas =
2330        calculate_aa_batch_intrinsic_gas(aa_env, gas_params, tx.access_list(), spec)?;
2331
2332    let mut nonce_2d_gas = 0;
2333
2334    // Calculate 2D nonce gas if nonce_key is non-zero
2335    // If tx nonce is 0, it's a new key (0 -> 1 transition), otherwise existing key
2336    if spec.is_t1() {
2337        if aa_env.nonce_key == TEMPO_EXPIRING_NONCE_KEY {
2338            // Calculate nonce gas based on nonce type:
2339            // - Expiring nonce (nonce_key == MAX, T1 active): ring buffer + seen mapping operations
2340            // - 2D nonce (nonce_key != 0): SLOAD + SSTORE for nonce increment
2341            // - Regular nonce (nonce_key == 0): no additional gas
2342            batch_gas.initial_regular_gas += EXPIRING_NONCE_GAS;
2343        } else if tx.nonce == 0 {
2344            // TIP-1000: Storage pricing updates for launch
2345            // Tempo transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas
2346            batch_gas.initial_regular_gas += gas_params.get(GasId::new_account_cost());
2347            batch_gas.initial_state_gas += gas_params.new_account_state_gas();
2348        } else if !aa_env.nonce_key.is_zero() {
2349            // Existing 2D nonce key usage (nonce > 0)
2350            // TIP-1000 Invariant 3: existing state updates must charge +5,000 gas
2351            batch_gas.initial_regular_gas += spec.gas_existing_nonce_key();
2352        }
2353    } else if let Some(aa_env) = &tx.tempo_tx_env
2354        && !aa_env.nonce_key.is_zero()
2355    {
2356        nonce_2d_gas = if tx.nonce() == 0 {
2357            spec.gas_new_nonce_key()
2358        } else {
2359            spec.gas_existing_nonce_key()
2360        };
2361    };
2362
2363    // For T0+, include 2D nonce gas in validation (charged upfront)
2364    // For pre-T0 (Genesis), 2D nonce gas is added AFTER validation to allow transactions
2365    // with gas_limit < intrinsic + nonce_2d_gas to pass validation, but the gas is still
2366    // charged during execution via init_and_floor_gas (not evm.initial_gas)
2367    if spec.is_t0() {
2368        batch_gas.initial_regular_gas += nonce_2d_gas;
2369    }
2370
2371    // Validate gas limit is sufficient for initial gas.
2372    // initial_total_gas already includes initial_state_gas as a subset,
2373    // so no need to add state gas separately.
2374    if gas_limit < batch_gas.initial_total_gas() {
2375        return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
2376            gas_limit,
2377            initial_gas: batch_gas.initial_total_gas(),
2378        }
2379        .into());
2380    }
2381
2382    // For pre-T0 (Genesis), add 2D nonce gas after validation
2383    // This gas will be charged via init_and_floor_gas, not evm.initial_gas
2384    if !spec.is_t0() {
2385        batch_gas.initial_regular_gas += nonce_2d_gas;
2386    }
2387
2388    Ok(batch_gas)
2389}
2390
2391/// IMPORTANT: the caller must ensure `token` is a valid TIP20Token address.
2392pub fn get_token_balance<JOURNAL>(
2393    journal: &mut JOURNAL,
2394    token: Address,
2395    sender: Address,
2396) -> Result<U256, <JOURNAL::Database as Database>::Error>
2397where
2398    JOURNAL: JournalTr,
2399{
2400    // Address has already been validated as having TIP20 prefix
2401    journal.load_account(token)?;
2402    let balance_slot = TIP20Token::from_address(token)
2403        .expect("TIP20 prefix already validated")
2404        .balances[sender]
2405        .slot();
2406    let balance = journal.sload(token, balance_slot)?.data;
2407
2408    Ok(balance)
2409}
2410
2411impl<DB, I> InspectorHandler for TempoEvmHandler<DB, I>
2412where
2413    DB: alloy_evm::Database,
2414    I: Inspector<TempoContext<DB>>,
2415{
2416    type IT = EthInterpreter;
2417
2418    /// Overridden execution method with inspector support that handles AA vs standard transactions.
2419    #[inline]
2420    fn inspect_execution(
2421        &mut self,
2422        evm: &mut Self::Evm,
2423        init_and_floor_gas: &InitialAndFloorGas,
2424    ) -> Result<FrameResult, Self::Error> {
2425        let spec = evm.ctx_ref().cfg().spec();
2426        let tx = evm.tx();
2427
2428        if let Some(oog) = check_gas_limit(*spec, tx, init_and_floor_gas) {
2429            return Ok(oog);
2430        }
2431
2432        let (gas_limit, reservoir) = evm.initial_gas_and_reservoir(init_and_floor_gas);
2433
2434        if let Some(tempo_tx_env) = evm.ctx().tx().tempo_tx_env.as_ref() {
2435            let calls = tempo_tx_env.aa_calls.clone();
2436            self.inspect_execute_multi_call(evm, gas_limit, reservoir, calls)
2437        } else {
2438            self.inspect_execute_single_call(evm, gas_limit, reservoir)
2439        }
2440    }
2441}
2442
2443/// Helper function to create a frame result for an out of gas error.
2444///
2445/// Use native fn when new revm version is released.
2446#[inline]
2447fn oog_frame_result(kind: TxKind, gas_limit: u64) -> FrameResult {
2448    if kind.is_call() {
2449        FrameResult::new_call_oog(gas_limit, 0..0, 0)
2450    } else {
2451        FrameResult::new_create_oog(gas_limit, 0)
2452    }
2453}
2454
2455/// Checks if gas limit is sufficient and returns OOG frame result if not.
2456///
2457/// For T0+, validates gas limit covers intrinsic gas. For pre-T0, skips check
2458/// to maintain backward compatibility.
2459#[inline]
2460fn check_gas_limit(
2461    spec: tempo_chainspec::hardfork::TempoHardfork,
2462    tx: &TempoTxEnv,
2463    adjusted_gas: &InitialAndFloorGas,
2464) -> Option<FrameResult> {
2465    if spec.is_t0() && tx.gas_limit() < adjusted_gas.initial_total_gas() {
2466        let kind = *tx
2467            .first_call()
2468            .expect("we already checked that there is at least one call in aa tx")
2469            .0;
2470        return Some(oog_frame_result(kind, tx.gas_limit()));
2471    }
2472    None
2473}
2474
2475/// Validates time window for AA transactions
2476///
2477/// AA transactions can have optional validBefore and validAfter fields:
2478/// - validAfter: Transaction can only be included after this timestamp
2479/// - validBefore: Transaction can only be included before this timestamp
2480///
2481/// This ensures transactions are only valid within a specific time window.
2482pub fn validate_time_window(
2483    valid_after: Option<u64>,
2484    valid_before: Option<u64>,
2485    block_timestamp: u64,
2486) -> Result<(), TempoInvalidTransaction> {
2487    // Validate validAfter constraint
2488    if let Some(after) = valid_after
2489        && block_timestamp < after
2490    {
2491        return Err(TempoInvalidTransaction::ValidAfter {
2492            current: block_timestamp,
2493            valid_after: after,
2494        });
2495    }
2496
2497    // Validate validBefore constraint
2498    // IMPORTANT: must be aligned with `RecoveredSubBlock::has_expired_transactions`.
2499    if let Some(before) = valid_before
2500        && block_timestamp >= before
2501    {
2502        return Err(TempoInvalidTransaction::ValidBefore {
2503            current: block_timestamp,
2504            valid_before: before,
2505        });
2506    }
2507
2508    Ok(())
2509}
2510
2511#[cfg(test)]
2512mod tests {
2513    use super::*;
2514    use crate::{
2515        TempoBlockEnv, TempoTxEnv, evm::TempoEvm, gas_params::tempo_gas_params,
2516        tx::TempoBatchCallEnv,
2517    };
2518    use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
2519    use proptest::prelude::*;
2520    use revm::{
2521        Context, Journal, MainContext,
2522        context::CfgEnv,
2523        database::{CacheDB, EmptyDB},
2524        handler::Handler,
2525        interpreter::{
2526            InstructionResult, InterpreterResult, gas::COLD_ACCOUNT_ACCESS_COST,
2527            instructions::utility::IntoU256,
2528        },
2529        primitives::hardfork::SpecId,
2530    };
2531    use tempo_chainspec::hardfork::TempoHardfork;
2532    use tempo_contracts::precompiles::DEFAULT_FEE_TOKEN;
2533    use tempo_precompiles::{
2534        PATH_USD_ADDRESS, TIP_FEE_MANAGER_ADDRESS, storage::ContractStorage, test_util::TIP20Setup,
2535    };
2536    use tempo_primitives::transaction::{
2537        Call, RecoveredTempoAuthorization, TempoSignature, TempoSignedAuthorization,
2538        tt_signature::{P256SignatureWithPreHash, WebAuthnSignature},
2539    };
2540
2541    fn create_test_journal() -> Journal<CacheDB<EmptyDB>> {
2542        let db = CacheDB::new(EmptyDB::default());
2543        Journal::new(db)
2544    }
2545
2546    type TestHandlerEvmResult<T> =
2547        Result<T, EVMError<<CacheDB<EmptyDB> as revm::Database>::Error, TempoInvalidTransaction>>;
2548
2549    struct TestHandlerEvm {
2550        evm: TempoEvm<CacheDB<EmptyDB>, ()>,
2551        handler: TempoEvmHandler<CacheDB<EmptyDB>, ()>,
2552    }
2553
2554    impl TestHandlerEvm {
2555        fn tx(spec: TempoHardfork, configure_tx_env: impl FnOnce(&mut TempoTxEnv)) -> Self {
2556            let mut tx_env = TempoTxEnv::default();
2557            configure_tx_env(&mut tx_env);
2558            Self::new(spec, tx_env)
2559        }
2560
2561        fn aa(
2562            spec: TempoHardfork,
2563            aa_env: TempoBatchCallEnv,
2564            configure_tx_env: impl FnOnce(&mut TempoTxEnv),
2565        ) -> Self {
2566            let mut tx_env = TempoTxEnv {
2567                tempo_tx_env: Some(Box::new(aa_env)),
2568                ..Default::default()
2569            };
2570            configure_tx_env(&mut tx_env);
2571            Self::new(spec, tx_env)
2572        }
2573
2574        fn new(spec: TempoHardfork, tx_env: TempoTxEnv) -> Self {
2575            Self::with_cfg(spec, tx_env, |_| {})
2576        }
2577
2578        fn with_cfg(
2579            spec: TempoHardfork,
2580            tx_env: TempoTxEnv,
2581            configure: impl FnOnce(&mut CfgEnv<TempoHardfork>),
2582        ) -> Self {
2583            let mut cfg = CfgEnv::<TempoHardfork>::default();
2584            cfg.spec = spec;
2585            cfg.gas_params = tempo_gas_params(spec);
2586            configure(&mut cfg);
2587
2588            let ctx = Context::mainnet()
2589                .with_db(CacheDB::new(EmptyDB::default()))
2590                .with_block(TempoBlockEnv::default())
2591                .with_cfg(cfg)
2592                .with_tx(tx_env)
2593                .with_new_journal(create_test_journal());
2594
2595            Self {
2596                evm: TempoEvm::new(ctx, ()),
2597                handler: TempoEvmHandler::new(),
2598            }
2599        }
2600
2601        fn cfg(&mut self) -> &CfgEnv<TempoHardfork> {
2602            &self.evm.ctx().cfg
2603        }
2604
2605        fn gas_params(&mut self) -> &GasParams {
2606            &self.cfg().gas_params
2607        }
2608
2609        fn validate_env(&mut self) -> TestHandlerEvmResult<()> {
2610            self.handler.validate_env(&mut self.evm)
2611        }
2612
2613        fn validate_initial_tx_gas(&mut self) -> InitialAndFloorGas {
2614            self.handler
2615                .validate_initial_tx_gas(&mut self.evm)
2616                .expect("initial gas validation should succeed")
2617        }
2618
2619        fn validate_against_state_and_deduct_caller(&mut self) -> TestHandlerEvmResult<()> {
2620            self.handler
2621                .validate_against_state_and_deduct_caller(&mut self.evm, &mut Default::default())
2622        }
2623
2624        fn execute(&mut self, init_gas: &InitialAndFloorGas) -> FrameResult {
2625            self.handler
2626                .execution(&mut self.evm, init_gas)
2627                .expect("execution should return a frame result")
2628        }
2629    }
2630
2631    #[test]
2632    fn test_invalid_fee_token_rejected() {
2633        // Test that an invalid fee token (non-TIP20 address) is rejected with a typed error
2634        // rather than panicking. This validates the check in validate_against_state_and_deduct_caller that
2635        // guards against invalid tokens reaching get_token_balance.
2636        let invalid_token = Address::random(); // Random address won't have TIP20 prefix
2637        assert!(
2638            !invalid_token.is_tip20(),
2639            "Test requires a non-TIP20 address"
2640        );
2641
2642        let mut test = TestHandlerEvm::tx(TempoHardfork::default(), |tx_env| {
2643            tx_env.fee_token = Some(invalid_token);
2644        });
2645
2646        let result = test.validate_against_state_and_deduct_caller();
2647
2648        assert!(
2649            matches!(
2650                result,
2651                Err(EVMError::Transaction(TempoInvalidTransaction::FeeTokenNotTip20 { address })) if address == invalid_token
2652            ),
2653            "Should reject non-TIP20 fee token with FeeTokenNotTip20 error"
2654        );
2655    }
2656
2657    #[test]
2658    fn test_non_usd_fee_token_rejected() {
2659        let admin = Address::random();
2660        let mut test = TestHandlerEvm::tx(TempoHardfork::default(), |tx_env| {
2661            tx_env.inner.gas_limit = 100_000;
2662            tx_env.inner.gas_price = 1_000_000_000;
2663            tx_env.inner.gas_priority_fee = Some(1_000_000_000);
2664        });
2665
2666        let fee_token =
2667            StorageCtx::enter_ctx(&mut test.evm.inner.ctx, StorageActions::disabled(), || {
2668                TIP20Setup::create("Euro", "EUR", admin)
2669                    .currency("EUR")
2670                    .apply()
2671                    .map(|token| token.address())
2672            })
2673            .expect("EUR token setup succeeds");
2674
2675        test.evm.inner.ctx.tx.fee_token = Some(fee_token);
2676
2677        let result = test.validate_against_state_and_deduct_caller();
2678
2679        assert!(
2680            matches!(
2681                result,
2682                Err(EVMError::Transaction(TempoInvalidTransaction::FeeTokenNotUsdCurrency {
2683                    address,
2684                    currency,
2685                })) if address == fee_token && currency == "EUR"
2686            ),
2687            "Should reject non-USD fee token with FeeTokenNotUsdCurrency error"
2688        );
2689    }
2690
2691    #[test]
2692    fn test_paused_fee_token_rejected() {
2693        let admin = Address::random();
2694        let fee_payer = Address::random();
2695        let fee = U256::from(100_000_000_000_000_u64);
2696        let mut test = TestHandlerEvm::tx(TempoHardfork::default(), |tx_env| {
2697            tx_env.inner.caller = fee_payer;
2698            tx_env.inner.gas_limit = 100_000;
2699            tx_env.inner.gas_price = 1_000_000_000;
2700            tx_env.inner.gas_priority_fee = Some(1_000_000_000);
2701        });
2702
2703        let fee_token =
2704            StorageCtx::enter_ctx(&mut test.evm.inner.ctx, StorageActions::disabled(), || {
2705                let mut token = TIP20Setup::create("Paused USD", "PUSD", admin)
2706                    .with_issuer(admin)
2707                    .with_role(admin, *tempo_precompiles::tip20::PAUSE_ROLE)
2708                    .with_mint(fee_payer, fee)
2709                    .apply()?;
2710                token.pause(admin, tempo_precompiles::tip20::ITIP20::pauseCall {})?;
2711                Ok::<_, TempoPrecompileError>(token.address())
2712            })
2713            .expect("paused USD token setup succeeds");
2714
2715        test.evm.inner.ctx.tx.fee_token = Some(fee_token);
2716
2717        let result = test.validate_against_state_and_deduct_caller();
2718
2719        assert!(
2720            matches!(
2721                result,
2722                Err(EVMError::Transaction(TempoInvalidTransaction::FeeTokenPaused { address })) if address == fee_token
2723            ),
2724            "Should reject paused fee token with FeeTokenPaused error"
2725        );
2726    }
2727
2728    #[test]
2729    fn test_self_sponsored_fee_payer_rejected_post_t2() {
2730        let caller = Address::random();
2731        let invalid_token = Address::random();
2732
2733        let mut test = TestHandlerEvm::tx(TempoHardfork::T2, |tx_env| {
2734            tx_env.inner.caller = caller;
2735            tx_env.fee_token = Some(invalid_token);
2736            tx_env.fee_payer = Some(Some(caller));
2737        });
2738
2739        let result = test.validate_env();
2740        assert!(matches!(
2741            result,
2742            Err(EVMError::Transaction(
2743                TempoInvalidTransaction::SelfSponsoredFeePayer
2744            ))
2745        ));
2746    }
2747
2748    #[test]
2749    fn test_self_sponsored_fee_payer_not_rejected_pre_t4() {
2750        let caller = Address::random();
2751        let invalid_token = Address::random();
2752
2753        let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
2754        let mut cfg = CfgEnv::<TempoHardfork>::default();
2755        cfg.spec = TempoHardfork::T1C;
2756
2757        let tx_env = TempoTxEnv {
2758            inner: revm::context::TxEnv {
2759                caller,
2760                ..Default::default()
2761            },
2762            fee_token: Some(invalid_token),
2763            fee_payer: Some(Some(caller)),
2764            ..Default::default()
2765        };
2766
2767        let mut evm: TempoEvm<CacheDB<EmptyDB>, ()> = TempoEvm::new(
2768            Context::mainnet()
2769                .with_db(CacheDB::new(EmptyDB::default()))
2770                .with_block(TempoBlockEnv::default())
2771                .with_cfg(cfg)
2772                .with_tx(tx_env),
2773            (),
2774        );
2775
2776        let result = handler.validate_env(&mut evm);
2777        assert!(result.is_ok());
2778    }
2779
2780    #[test]
2781    fn test_get_token_balance() -> eyre::Result<()> {
2782        let mut journal = create_test_journal();
2783        // Use PATH_USD_ADDRESS which has the TIP20 prefix
2784        let token = PATH_USD_ADDRESS;
2785        let account = Address::random();
2786        let expected_balance = U256::random();
2787
2788        // Set up initial balance
2789        let balance_slot = TIP20Token::from_address(token)?.balances[account].slot();
2790        journal.load_account(token)?;
2791        journal
2792            .sstore(token, balance_slot, expected_balance)
2793            .unwrap();
2794
2795        let balance = get_token_balance(&mut journal, token, account)?;
2796        assert_eq!(balance, expected_balance);
2797
2798        Ok(())
2799    }
2800
2801    #[test]
2802    fn test_get_fee_token() -> eyre::Result<()> {
2803        let journal = create_test_journal();
2804        let mut ctx: TempoContext<_> = Context::mainnet()
2805            .with_db(CacheDB::new(EmptyDB::default()))
2806            .with_block(TempoBlockEnv::default())
2807            .with_cfg(Default::default())
2808            .with_tx(TempoTxEnv::default())
2809            .with_new_journal(journal);
2810        let user = Address::random();
2811        ctx.tx.inner.caller = user;
2812        let validator = Address::random();
2813        ctx.block.beneficiary = validator;
2814        let user_fee_token = Address::random();
2815        let validator_fee_token = Address::random();
2816        let tx_fee_token = Address::random();
2817
2818        // Set validator token
2819        let validator_slot = TipFeeManager::new().validator_tokens[validator].slot();
2820        ctx.journaled_state.load_account(TIP_FEE_MANAGER_ADDRESS)?;
2821        ctx.journaled_state
2822            .sstore(
2823                TIP_FEE_MANAGER_ADDRESS,
2824                validator_slot,
2825                validator_fee_token.into_u256(),
2826            )
2827            .unwrap();
2828
2829        {
2830            let fee_token = ctx.journaled_state.get_fee_token(
2831                &ctx.tx,
2832                user,
2833                ctx.cfg.spec,
2834                tempo_precompiles::storage::StorageActions::disabled(),
2835            )?;
2836            assert_eq!(DEFAULT_FEE_TOKEN, fee_token);
2837        }
2838
2839        // Set user token
2840        let user_slot = TipFeeManager::new().user_tokens[user].slot();
2841        ctx.journaled_state
2842            .sstore(
2843                TIP_FEE_MANAGER_ADDRESS,
2844                user_slot,
2845                user_fee_token.into_u256(),
2846            )
2847            .unwrap();
2848
2849        {
2850            let fee_token = ctx.journaled_state.get_fee_token(
2851                &ctx.tx,
2852                user,
2853                ctx.cfg.spec,
2854                tempo_precompiles::storage::StorageActions::disabled(),
2855            )?;
2856            assert_eq!(user_fee_token, fee_token);
2857        }
2858
2859        // Set tx fee token
2860        ctx.tx.fee_token = Some(tx_fee_token);
2861        let fee_token = ctx.journaled_state.get_fee_token(
2862            &ctx.tx,
2863            user,
2864            ctx.cfg.spec,
2865            tempo_precompiles::storage::StorageActions::disabled(),
2866        )?;
2867        assert_eq!(tx_fee_token, fee_token);
2868
2869        Ok(())
2870    }
2871
2872    #[test]
2873    fn test_aa_gas_single_call_vs_normal_tx() {
2874        use crate::TempoBatchCallEnv;
2875        use alloy_primitives::{Bytes, TxKind};
2876        use revm::interpreter::gas::calculate_initial_tx_gas;
2877        use tempo_primitives::transaction::{Call, TempoSignature};
2878        let gas_params = GasParams::default();
2879
2880        // Test that AA tx with secp256k1 and single call matches normal tx + per-call overhead
2881        let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); // 5 non-zero bytes
2882        let to = Address::random();
2883
2884        // Single call for AA
2885        let call = Call {
2886            to: TxKind::Call(to),
2887            value: U256::ZERO,
2888            input: calldata.clone(),
2889        };
2890
2891        let aa_env = TempoBatchCallEnv {
2892            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2893                alloy_primitives::Signature::test_signature(),
2894            )), // dummy secp256k1 sig
2895            aa_calls: vec![call],
2896            key_authorization: None,
2897            signature_hash: B256::ZERO,
2898            ..Default::default()
2899        };
2900
2901        // Calculate AA gas
2902        let spec = tempo_chainspec::hardfork::TempoHardfork::default();
2903        let aa_gas = calculate_aa_batch_intrinsic_gas(
2904            &aa_env,
2905            &gas_params,
2906            None::<std::iter::Empty<&AccessListItem>>, // no access list
2907            spec,
2908        )
2909        .unwrap();
2910
2911        // Calculate expected gas using revm's function for equivalent normal tx
2912        let normal_tx_gas = calculate_initial_tx_gas(
2913            spec.into(),
2914            &calldata,
2915            false, // not create
2916            0,     // no access list accounts
2917            0,     // no access list storage
2918            0,     // no authorization list
2919        );
2920
2921        // AA with secp256k1 + single call should match normal tx exactly
2922        assert_eq!(
2923            aa_gas.initial_total_gas(),
2924            normal_tx_gas.initial_total_gas()
2925        );
2926    }
2927
2928    #[test]
2929    fn test_aa_gas_multiple_calls_overhead() {
2930        use crate::TempoBatchCallEnv;
2931        use alloy_primitives::{Bytes, TxKind};
2932        use revm::interpreter::gas::calculate_initial_tx_gas;
2933        use tempo_primitives::transaction::{Call, TempoSignature};
2934
2935        let calldata = Bytes::from(vec![1, 2, 3]); // 3 non-zero bytes
2936
2937        let calls = vec![
2938            Call {
2939                to: TxKind::Call(Address::random()),
2940                value: U256::ZERO,
2941                input: calldata.clone(),
2942            },
2943            Call {
2944                to: TxKind::Call(Address::random()),
2945                value: U256::ZERO,
2946                input: calldata.clone(),
2947            },
2948            Call {
2949                to: TxKind::Call(Address::random()),
2950                value: U256::ZERO,
2951                input: calldata.clone(),
2952            },
2953        ];
2954
2955        let aa_env = TempoBatchCallEnv {
2956            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2957                alloy_primitives::Signature::test_signature(),
2958            )),
2959            aa_calls: calls,
2960            key_authorization: None,
2961            signature_hash: B256::ZERO,
2962            ..Default::default()
2963        };
2964
2965        let spec = tempo_chainspec::hardfork::TempoHardfork::default();
2966        let gas = calculate_aa_batch_intrinsic_gas(
2967            &aa_env,
2968            &GasParams::default(),
2969            None::<std::iter::Empty<&AccessListItem>>,
2970            spec,
2971        )
2972        .unwrap();
2973
2974        // Calculate base gas for a single normal tx
2975        let base_tx_gas = calculate_initial_tx_gas(spec.into(), &calldata, false, 0, 0, 0);
2976
2977        // For 3 calls: base (21k) + 3*calldata + 2*per-call overhead (calls 2 and 3)
2978        // = 21k + 2*(calldata cost) + 2*COLD_ACCOUNT_ACCESS_COST
2979        let expected = base_tx_gas.initial_total_gas()
2980            + 2 * (calldata.len() as u64 * 16)
2981            + 2 * COLD_ACCOUNT_ACCESS_COST;
2982        // Should charge per-call overhead for calls beyond the first
2983        assert_eq!(gas.initial_total_gas(), expected,);
2984    }
2985
2986    #[test]
2987    fn test_aa_gas_p256_signature() {
2988        use crate::TempoBatchCallEnv;
2989        use alloy_primitives::{B256, Bytes, TxKind};
2990        use revm::interpreter::gas::calculate_initial_tx_gas;
2991        use tempo_primitives::transaction::{
2992            Call, TempoSignature, tt_signature::P256SignatureWithPreHash,
2993        };
2994
2995        let spec = SpecId::CANCUN;
2996        let calldata = Bytes::from(vec![1, 2]);
2997
2998        let call = Call {
2999            to: TxKind::Call(Address::random()),
3000            value: U256::ZERO,
3001            input: calldata.clone(),
3002        };
3003
3004        let aa_env = TempoBatchCallEnv {
3005            signature: TempoSignature::Primitive(PrimitiveSignature::P256(
3006                P256SignatureWithPreHash {
3007                    r: B256::ZERO,
3008                    s: B256::ZERO,
3009                    pub_key_x: B256::ZERO,
3010                    pub_key_y: B256::ZERO,
3011                    pre_hash: false,
3012                },
3013            )),
3014            aa_calls: vec![call],
3015            key_authorization: None,
3016            signature_hash: B256::ZERO,
3017            ..Default::default()
3018        };
3019
3020        let gas = calculate_aa_batch_intrinsic_gas(
3021            &aa_env,
3022            &GasParams::default(),
3023            None::<std::iter::Empty<&AccessListItem>>,
3024            tempo_chainspec::hardfork::TempoHardfork::default(),
3025        )
3026        .unwrap();
3027
3028        // Calculate base gas for normal tx
3029        let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
3030
3031        // Expected: normal tx + P256_VERIFY_GAS
3032        let expected = base_gas.initial_total_gas() + P256_VERIFY_GAS;
3033        assert_eq!(gas.initial_total_gas(), expected,);
3034    }
3035
3036    #[test]
3037    fn test_aa_gas_create_call() {
3038        use crate::TempoBatchCallEnv;
3039        use alloy_primitives::{Bytes, TxKind};
3040        use revm::interpreter::gas::calculate_initial_tx_gas;
3041        use tempo_primitives::transaction::{Call, TempoSignature};
3042
3043        let spec = SpecId::CANCUN; // Post-Shanghai
3044        let initcode = Bytes::from(vec![0x60, 0x80]); // 2 bytes
3045
3046        let call = Call {
3047            to: TxKind::Create,
3048            value: U256::ZERO,
3049            input: initcode.clone(),
3050        };
3051
3052        let aa_env = TempoBatchCallEnv {
3053            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3054                alloy_primitives::Signature::test_signature(),
3055            )),
3056            aa_calls: vec![call],
3057            key_authorization: None,
3058            signature_hash: B256::ZERO,
3059            ..Default::default()
3060        };
3061
3062        let gas = calculate_aa_batch_intrinsic_gas(
3063            &aa_env,
3064            &GasParams::default(),
3065            None::<std::iter::Empty<&AccessListItem>>,
3066            tempo_chainspec::hardfork::TempoHardfork::default(),
3067        )
3068        .unwrap();
3069
3070        // Calculate expected using revm's function for CREATE tx
3071        let base_gas = calculate_initial_tx_gas(
3072            spec, &initcode, true, // is_create = true
3073            0, 0, 0,
3074        );
3075
3076        // AA CREATE should match normal CREATE exactly
3077        assert_eq!(gas.initial_total_gas(), base_gas.initial_total_gas(),);
3078    }
3079
3080    #[test]
3081    fn test_aa_gas_value_transfer() {
3082        use crate::TempoBatchCallEnv;
3083        use alloy_primitives::{Bytes, TxKind};
3084        use tempo_primitives::transaction::{Call, TempoSignature};
3085
3086        let calldata = Bytes::from(vec![1]);
3087
3088        let call = Call {
3089            to: TxKind::Call(Address::random()),
3090            value: U256::from(1000), // Non-zero value
3091            input: calldata,
3092        };
3093
3094        let aa_env = TempoBatchCallEnv {
3095            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3096                alloy_primitives::Signature::test_signature(),
3097            )),
3098            aa_calls: vec![call],
3099            key_authorization: None,
3100            signature_hash: B256::ZERO,
3101            ..Default::default()
3102        };
3103
3104        let res = calculate_aa_batch_intrinsic_gas(
3105            &aa_env,
3106            &GasParams::default(),
3107            None::<std::iter::Empty<&AccessListItem>>,
3108            tempo_chainspec::hardfork::TempoHardfork::default(),
3109        );
3110
3111        assert_eq!(
3112            res.unwrap_err(),
3113            TempoInvalidTransaction::ValueTransferNotAllowedInAATx
3114        );
3115    }
3116
3117    #[test]
3118    fn test_aa_gas_access_list() {
3119        use crate::TempoBatchCallEnv;
3120        use alloy_primitives::{Bytes, TxKind};
3121        use revm::interpreter::gas::calculate_initial_tx_gas;
3122        use tempo_primitives::transaction::{Call, TempoSignature};
3123
3124        let spec = SpecId::CANCUN;
3125        let calldata = Bytes::from(vec![]);
3126
3127        let call = Call {
3128            to: TxKind::Call(Address::random()),
3129            value: U256::ZERO,
3130            input: calldata.clone(),
3131        };
3132
3133        let aa_env = TempoBatchCallEnv {
3134            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3135                alloy_primitives::Signature::test_signature(),
3136            )),
3137            aa_calls: vec![call],
3138            key_authorization: None,
3139            signature_hash: B256::ZERO,
3140            ..Default::default()
3141        };
3142
3143        // Test without access list
3144        let gas = calculate_aa_batch_intrinsic_gas(
3145            &aa_env,
3146            &GasParams::default(),
3147            None::<std::iter::Empty<&AccessListItem>>,
3148            tempo_chainspec::hardfork::TempoHardfork::default(),
3149        )
3150        .unwrap();
3151
3152        // Calculate expected using revm's function
3153        let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
3154
3155        // Expected: normal tx
3156        assert_eq!(gas.initial_total_gas(), base_gas.initial_total_gas(),);
3157    }
3158
3159    #[test]
3160    fn test_key_authorization_rlp_encoding() {
3161        use alloy_primitives::{Address, U256};
3162        use tempo_primitives::transaction::{
3163            SignatureType, TokenLimit, key_authorization::KeyAuthorization,
3164        };
3165
3166        // Create test data
3167        let chain_id = 1u64;
3168        let key_type = SignatureType::Secp256k1;
3169        let key_id = Address::random();
3170        let expiry = 1000u64;
3171        let limits = vec![
3172            TokenLimit {
3173                token: Address::random(),
3174                limit: U256::from(100),
3175                period: 0,
3176            },
3177            TokenLimit {
3178                token: Address::random(),
3179                limit: U256::from(200),
3180                period: 0,
3181            },
3182        ];
3183
3184        // Compute hash using the helper function
3185        let hash1 = KeyAuthorization::unrestricted(chain_id, key_type, key_id)
3186            .with_expiry(expiry)
3187            .with_limits(limits.clone())
3188            .signature_hash();
3189
3190        // Compute again to verify consistency
3191        let hash2 = KeyAuthorization::unrestricted(chain_id, key_type, key_id)
3192            .with_expiry(expiry)
3193            .with_limits(limits.clone())
3194            .signature_hash();
3195
3196        assert_eq!(hash1, hash2, "Hash computation should be deterministic");
3197
3198        // Verify that different chain_id produces different hash
3199        let hash3 = KeyAuthorization::unrestricted(2, key_type, key_id)
3200            .with_expiry(expiry)
3201            .with_limits(limits)
3202            .signature_hash();
3203        assert_ne!(
3204            hash1, hash3,
3205            "Different chain_id should produce different hash"
3206        );
3207    }
3208
3209    #[test]
3210    fn test_aa_gas_floor_gas_prague() {
3211        use crate::TempoBatchCallEnv;
3212        use alloy_primitives::{Bytes, TxKind};
3213        use revm::interpreter::gas::calculate_initial_tx_gas;
3214        use tempo_primitives::transaction::{Call, TempoSignature};
3215
3216        let spec = SpecId::PRAGUE;
3217        let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); // 5 non-zero bytes
3218
3219        let call = Call {
3220            to: TxKind::Call(Address::random()),
3221            value: U256::ZERO,
3222            input: calldata.clone(),
3223        };
3224
3225        let aa_env = TempoBatchCallEnv {
3226            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3227                alloy_primitives::Signature::test_signature(),
3228            )),
3229            aa_calls: vec![call],
3230            key_authorization: None,
3231            signature_hash: B256::ZERO,
3232            ..Default::default()
3233        };
3234
3235        let gas = calculate_aa_batch_intrinsic_gas(
3236            &aa_env,
3237            &GasParams::default(),
3238            None::<std::iter::Empty<&AccessListItem>>,
3239            tempo_chainspec::hardfork::TempoHardfork::default(),
3240        )
3241        .unwrap();
3242
3243        // Calculate expected floor gas using revm's function
3244        let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
3245
3246        // Floor gas should match revm's calculation for same calldata
3247        assert_eq!(
3248            gas.floor_gas, base_gas.floor_gas,
3249            "Should calculate floor gas for Prague matching revm"
3250        );
3251    }
3252
3253    /// This test will start failing once we get the balance transfer enabled
3254    /// PR that introduced [`TempoInvalidTransaction::ValueTransferNotAllowed`] https://github.com/tempoxyz/tempo/pull/759
3255    #[test]
3256    fn test_zero_value_transfer() -> eyre::Result<()> {
3257        use crate::TempoEvm;
3258
3259        // Create a test context with a transaction that has a non-zero value
3260        let ctx = Context::mainnet()
3261            .with_db(CacheDB::new(EmptyDB::default()))
3262            .with_block(Default::default())
3263            .with_cfg(Default::default())
3264            .with_tx(TempoTxEnv::default());
3265        let mut evm = TempoEvm::new(ctx, ());
3266
3267        // Set a non-zero value on the transaction
3268        evm.ctx.tx.inner.value = U256::from(1000);
3269
3270        // Create the handler
3271        let handler = TempoEvmHandler::<_, ()>::new();
3272
3273        // Call validate_env and expect it to fail with ValueTransferNotAllowed
3274        let result = handler.validate_env(&mut evm);
3275
3276        if let Err(EVMError::Transaction(err)) = result {
3277            assert_eq!(err, TempoInvalidTransaction::ValueTransferNotAllowed);
3278        } else {
3279            panic!("Expected ValueTransferNotAllowed error");
3280        }
3281
3282        Ok(())
3283    }
3284
3285    #[test]
3286    fn test_key_authorization_gas_with_limits() {
3287        use tempo_primitives::transaction::{
3288            KeyAuthorization, SignatureType, SignedKeyAuthorization, TokenLimit,
3289        };
3290
3291        // Helper to create key auth with N limits
3292        let create_key_auth = |num_limits: usize| -> SignedKeyAuthorization {
3293            let mut auth =
3294                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random());
3295            if num_limits > 0 {
3296                auth = auth.with_limits(
3297                    (0..num_limits)
3298                        .map(|_| TokenLimit {
3299                            token: Address::random(),
3300                            limit: U256::from(1000),
3301                            period: 0,
3302                        })
3303                        .collect(),
3304                );
3305            }
3306            auth.into_signed(PrimitiveSignature::Secp256k1(
3307                alloy_primitives::Signature::test_signature(),
3308            ))
3309        };
3310
3311        // Test 0 limits: base (27k) + ecrecover (3k) = 30,000
3312        let (gas_0, state_0) = calculate_key_authorization_gas(
3313            &create_key_auth(0),
3314            &GasParams::default(),
3315            tempo_chainspec::hardfork::TempoHardfork::default(),
3316        );
3317        assert_eq!(
3318            gas_0,
3319            KEY_AUTH_BASE_GAS + ECRECOVER_GAS,
3320            "0 limits should be 30,000"
3321        );
3322        assert_eq!(state_0, 0, "pre-T1B has no state gas");
3323
3324        // Test 1 limit: 30,000 + 22,000 = 52,000
3325        let (gas_1, state_1) = calculate_key_authorization_gas(
3326            &create_key_auth(1),
3327            &GasParams::default(),
3328            tempo_chainspec::hardfork::TempoHardfork::default(),
3329        );
3330        assert_eq!(
3331            gas_1,
3332            KEY_AUTH_BASE_GAS + ECRECOVER_GAS + KEY_AUTH_PER_LIMIT_GAS,
3333            "1 limit should be 52,000"
3334        );
3335        assert_eq!(state_1, 0, "pre-T1B has no state gas");
3336
3337        // Test 2 limits: 30,000 + 44,000 = 74,000
3338        let (gas_2, _) = calculate_key_authorization_gas(
3339            &create_key_auth(2),
3340            &GasParams::default(),
3341            tempo_chainspec::hardfork::TempoHardfork::default(),
3342        );
3343        assert_eq!(
3344            gas_2,
3345            KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 2 * KEY_AUTH_PER_LIMIT_GAS,
3346            "2 limits should be 74,000"
3347        );
3348
3349        // Test 3 limits: 30,000 + 66,000 = 96,000
3350        let (gas_3, _) = calculate_key_authorization_gas(
3351            &create_key_auth(3),
3352            &GasParams::default(),
3353            tempo_chainspec::hardfork::TempoHardfork::default(),
3354        );
3355        assert_eq!(
3356            gas_3,
3357            KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 3 * KEY_AUTH_PER_LIMIT_GAS,
3358            "3 limits should be 96,000"
3359        );
3360
3361        // T1B branch: gas = sig_gas + SLOAD + SSTORE * (1 + num_limits) + buffer
3362        let t1b_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T1B);
3363        let sstore =
3364            t1b_gas_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
3365        let sload =
3366            t1b_gas_params.warm_storage_read_cost() + t1b_gas_params.cold_storage_additional_cost();
3367        const BUFFER: u64 = 2_000;
3368
3369        for num_limits in 0..=3 {
3370            let (gas, state_gas) = calculate_key_authorization_gas(
3371                &create_key_auth(num_limits),
3372                &t1b_gas_params,
3373                TempoHardfork::T1B,
3374            );
3375            let expected = ECRECOVER_GAS + sload + sstore * (1 + num_limits as u64) + BUFFER;
3376            assert_eq!(gas, expected, "T1B with {num_limits} limits");
3377            assert_eq!(state_gas, 0, "T1B has no state gas");
3378        }
3379
3380        let t3_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T3);
3381        let t3_sstore =
3382            t3_gas_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
3383        let t3_sload =
3384            t3_gas_params.warm_storage_read_cost() + t3_gas_params.cold_storage_additional_cost();
3385
3386        for num_limits in 0..=3 {
3387            let num_sstores = 1 + 2 * num_limits as u64;
3388            let (gas, state_gas) = calculate_key_authorization_gas(
3389                &create_key_auth(num_limits),
3390                &t3_gas_params,
3391                TempoHardfork::T3,
3392            );
3393            let expected = ECRECOVER_GAS + t3_sload + t3_sstore * num_sstores + BUFFER;
3394            assert_eq!(gas, expected, "T3 with {num_limits} limits");
3395            assert_eq!(state_gas, 0, "T3 has no state gas");
3396        }
3397
3398        // T4 with T4 gas params: regular sstore = 19,900, state gas = 230,000 per SSTORE
3399        let t4_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T4);
3400        let t4_sstore =
3401            t4_gas_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
3402        let t4_sload =
3403            t4_gas_params.warm_storage_read_cost() + t4_gas_params.cold_storage_additional_cost();
3404        let t4_sstore_state =
3405            t4_gas_params.get(revm::context_interface::cfg::GasId::sstore_set_state_gas());
3406
3407        for num_limits in 0..=3 {
3408            let num_sstores = 1 + 2 * num_limits as u64;
3409            let (gas, state_gas) = calculate_key_authorization_gas(
3410                &create_key_auth(num_limits),
3411                &t4_gas_params,
3412                TempoHardfork::T4,
3413            );
3414            let expected_state = t4_sstore_state * num_sstores;
3415            let expected = ECRECOVER_GAS
3416                + t4_sload
3417                + t4_sstore * num_sstores
3418                + BUFFER
3419                + 5_000
3420                + expected_state;
3421            assert_eq!(gas, expected, "T4 with {num_limits} limits");
3422            assert_eq!(
3423                state_gas, expected_state,
3424                "T4 state gas with {num_limits} limits"
3425            );
3426        }
3427
3428        let t5_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T5);
3429        let t5_sload =
3430            t5_gas_params.warm_storage_read_cost() + t5_gas_params.cold_storage_additional_cost();
3431        let base_t5_key_auth = create_key_auth(0);
3432        let mut witness_t5_key_auth = create_key_auth(0);
3433        witness_t5_key_auth.authorization = witness_t5_key_auth
3434            .authorization
3435            .with_witness(B256::repeat_byte(0x53));
3436
3437        let (base_t5_gas, base_t5_state_gas) =
3438            calculate_key_authorization_gas(&base_t5_key_auth, &t5_gas_params, TempoHardfork::T5);
3439        let (witness_t5_gas, witness_t5_state_gas) = calculate_key_authorization_gas(
3440            &witness_t5_key_auth,
3441            &t5_gas_params,
3442            TempoHardfork::T5,
3443        );
3444
3445        assert_eq!(
3446            witness_t5_gas - base_t5_gas,
3447            t5_sload + KEY_AUTH_EXTRA_EVENT_BUFFER,
3448            "T5 witness adds one burned-witness SLOAD and one event"
3449        );
3450        assert_eq!(
3451            witness_t5_state_gas - base_t5_state_gas,
3452            0,
3453            "T5 witness authorization does not add state gas"
3454        );
3455
3456        let t6_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T6);
3457        let base_t6_key_auth = create_key_auth(0);
3458        let mut account_bound_t6_key_auth = create_key_auth(0);
3459        account_bound_t6_key_auth.authorization = account_bound_t6_key_auth
3460            .authorization
3461            .with_account(Address::random());
3462        let mut admin_t6_key_auth = create_key_auth(0);
3463        admin_t6_key_auth.authorization = admin_t6_key_auth
3464            .authorization
3465            .into_admin(Address::random());
3466        let mut unbound_admin_t6_key_auth = create_key_auth(0);
3467        unbound_admin_t6_key_auth.authorization.is_admin = true;
3468
3469        let (base_t6_gas, base_t6_state_gas) =
3470            calculate_key_authorization_gas(&base_t6_key_auth, &t6_gas_params, TempoHardfork::T6);
3471        let (account_bound_t6_gas, account_bound_t6_state_gas) = calculate_key_authorization_gas(
3472            &account_bound_t6_key_auth,
3473            &t6_gas_params,
3474            TempoHardfork::T6,
3475        );
3476        let (admin_t6_gas, admin_t6_state_gas) =
3477            calculate_key_authorization_gas(&admin_t6_key_auth, &t6_gas_params, TempoHardfork::T6);
3478        let (unbound_admin_t6_gas, unbound_admin_t6_state_gas) = calculate_key_authorization_gas(
3479            &unbound_admin_t6_key_auth,
3480            &t6_gas_params,
3481            TempoHardfork::T6,
3482        );
3483
3484        assert_eq!(
3485            account_bound_t6_gas - base_t6_gas,
3486            0,
3487            "T6 account-bound authorization does not add key authorization gas"
3488        );
3489        assert_eq!(
3490            admin_t6_gas - base_t6_gas,
3491            KEY_AUTH_EXTRA_EVENT_BUFFER,
3492            "T6 account-bound admin authorization charges one extra event buffer"
3493        );
3494        assert_eq!(
3495            admin_t6_gas - account_bound_t6_gas,
3496            KEY_AUTH_EXTRA_EVENT_BUFFER,
3497            "T6 admin authorization pays one extra event buffer over non-admin account-bound authorization"
3498        );
3499        assert_eq!(
3500            unbound_admin_t6_gas - base_t6_gas,
3501            KEY_AUTH_EXTRA_EVENT_BUFFER,
3502            "T6 root-signed admin authorization without account charges only the extra event buffer"
3503        );
3504        assert_eq!(
3505            account_bound_t6_state_gas, base_t6_state_gas,
3506            "T6 account binding does not add state gas"
3507        );
3508        assert_eq!(
3509            admin_t6_state_gas, base_t6_state_gas,
3510            "T6 admin authorization event buffer does not add state gas"
3511        );
3512        assert_eq!(
3513            unbound_admin_t6_state_gas, base_t6_state_gas,
3514            "T6 unbound admin authorization does not add state gas"
3515        );
3516
3517        let scoped = KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3518            .with_allowed_calls(vec![tempo_primitives::transaction::CallScope {
3519                target: Address::random(),
3520                selector_rules: vec![tempo_primitives::transaction::SelectorRule {
3521                    selector: [0xa9, 0x05, 0x9c, 0xbb],
3522                    recipients: vec![Address::random(), Address::random()],
3523                }],
3524            }])
3525            .into_signed(PrimitiveSignature::Secp256k1(
3526                alloy_primitives::Signature::test_signature(),
3527            ));
3528
3529        let (gas, state_gas) =
3530            calculate_key_authorization_gas(&scoped, &t3_gas_params, TempoHardfork::T3);
3531        let expected = ECRECOVER_GAS + t3_sload + t3_sstore * (1 + 12) + BUFFER;
3532        assert_eq!(
3533            gas, expected,
3534            "T3 scope writes should keep current main accounting"
3535        );
3536        assert_eq!(state_gas, 0, "T3 has no state gas");
3537
3538        let (gas, state_gas) =
3539            calculate_key_authorization_gas(&scoped, &t4_gas_params, TempoHardfork::T4);
3540        // 1 key write + 12 scope slots = 13 SSTOREs:
3541        // account mode(1) + target insertion rows(3) + selector insertion rows(3)
3542        // + constrained selector recipient-length(1) + recipients values+positions(2*2).
3543        // The rounded surcharge adds 5k base + 7k per target + 7k per selector + 5k per
3544        // recipient, which keeps larger scope trees from being materially underpriced.
3545        let num_sstores = 1 + 12;
3546        let expected_state = t4_sstore_state * num_sstores;
3547        let expected =
3548            ECRECOVER_GAS + t4_sload + t4_sstore * num_sstores + BUFFER + 29_000 + expected_state;
3549        assert_eq!(gas, expected, "T4 scope writes should be fully charged");
3550        assert_eq!(state_gas, expected_state, "T4 scope state gas");
3551        let multi_scope =
3552            KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3553                .with_allowed_calls(vec![
3554                    tempo_primitives::transaction::CallScope {
3555                        target: Address::random(),
3556                        selector_rules: vec![
3557                            tempo_primitives::transaction::SelectorRule {
3558                                selector: [0xa9, 0x05, 0x9c, 0xbb],
3559                                recipients: vec![],
3560                            },
3561                            tempo_primitives::transaction::SelectorRule {
3562                                selector: [0x09, 0x5e, 0xa7, 0xb3],
3563                                recipients: vec![],
3564                            },
3565                        ],
3566                    },
3567                    tempo_primitives::transaction::CallScope {
3568                        target: Address::random(),
3569                        selector_rules: vec![],
3570                    },
3571                ])
3572                .into_signed(PrimitiveSignature::Secp256k1(
3573                    alloy_primitives::Signature::test_signature(),
3574                ));
3575
3576        let (gas, state_gas) =
3577            calculate_key_authorization_gas(&multi_scope, &t3_gas_params, TempoHardfork::T3);
3578        let expected = ECRECOVER_GAS + t3_sload + t3_sstore * 14 + BUFFER;
3579        assert_eq!(
3580            gas, expected,
3581            "T3 scope writes should keep current main accounting"
3582        );
3583        assert_eq!(state_gas, 0, "T3 has no state gas");
3584
3585        let (gas, state_gas) =
3586            calculate_key_authorization_gas(&multi_scope, &t4_gas_params, TempoHardfork::T4);
3587        let expected_state = t4_sstore_state * 12;
3588        let expected = ECRECOVER_GAS + t4_sload + t4_sstore * 12 + BUFFER + 33_000 + expected_state;
3589        assert_eq!(
3590            gas, expected,
3591            "T4 scope writes should only charge storage-creating rows"
3592        );
3593        assert_eq!(state_gas, expected_state, "T4 scope state gas");
3594    }
3595
3596    #[test]
3597    fn test_t4_key_authorization_matches_tip1016_sstore_regular_cost() {
3598        use tempo_primitives::transaction::{KeyAuthorization, SignatureType};
3599
3600        let key_auth =
3601            KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3602                .into_signed(PrimitiveSignature::Secp256k1(
3603                    alloy_primitives::Signature::test_signature(),
3604                ));
3605
3606        // TIP-1016 is opt-in via amsterdam_eip8037; manually enable for this test.
3607        let gas_params =
3608            crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
3609
3610        let sig_gas = ECRECOVER_GAS + primitive_signature_verification_gas(&key_auth.signature);
3611        let sload = gas_params.warm_storage_read_cost() + gas_params.cold_storage_additional_cost();
3612        let scope_extra_gas = call_scope_extra_gas(&key_auth.authorization);
3613        let (regular_gas, state_gas) =
3614            calculate_key_authorization_gas(&key_auth, &gas_params, TempoHardfork::T4);
3615        let helper_sstore_regular = regular_gas - sig_gas - sload - 2_000 - scope_extra_gas;
3616
3617        assert_eq!(helper_sstore_regular, 20_000);
3618        assert_eq!(state_gas, 230_000);
3619    }
3620
3621    #[test]
3622    fn test_t7_key_authorization_intrinsic_includes_storage_credit_value() {
3623        use tempo_chainspec::constants::gas::SSTORE_CREATE_COST;
3624        use tempo_primitives::transaction::{KeyAuthorization, SignatureType};
3625
3626        let key_auth =
3627            KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3628                .into_signed(PrimitiveSignature::Secp256k1(
3629                    alloy_primitives::Signature::test_signature(),
3630                ));
3631
3632        let gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T7);
3633        let sig_gas = ECRECOVER_GAS + primitive_signature_verification_gas(&key_auth.signature);
3634        let sload = gas_params.warm_storage_read_cost() + gas_params.cold_storage_additional_cost();
3635        let scope_extra_gas = call_scope_extra_gas(&key_auth.authorization);
3636        let (regular_gas, state_gas) =
3637            calculate_key_authorization_gas(&key_auth, &gas_params, TempoHardfork::T7);
3638        let helper_sstore_regular = regular_gas - sig_gas - sload - 2_000 - scope_extra_gas;
3639
3640        assert_eq!(
3641            gas_params.get(GasId::sstore_set_without_load_cost()),
3642            SSTORE_CREATE_COST - STORAGE_CREDIT_VALUE,
3643            "T7 gas table should expose only the SSTORE residual"
3644        );
3645        assert_eq!(
3646            helper_sstore_regular, SSTORE_CREATE_COST,
3647            "key authorization intrinsic gas must include the TIP-1060 creditable portion"
3648        );
3649        assert_eq!(state_gas, 0, "T7 without TIP-1016 has no state gas split");
3650    }
3651
3652    #[test]
3653    fn test_translate_allowed_calls_for_precompile_preserves_empty_nested_allow_all_lists() {
3654        use tempo_primitives::transaction::{
3655            CallScope, KeyAuthorization, SelectorRule, SignatureType,
3656        };
3657
3658        let empty_selector_rules =
3659            KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3660                .with_allowed_calls(vec![CallScope {
3661                    target: Address::random(),
3662                    selector_rules: vec![],
3663                }])
3664                .into_signed(PrimitiveSignature::Secp256k1(
3665                    alloy_primitives::Signature::test_signature(),
3666                ));
3667
3668        let translated = translate_allowed_calls_for_precompile(&empty_selector_rules);
3669        assert_eq!(translated.len(), 1);
3670        assert!(translated[0].selectorRules.is_empty());
3671
3672        let empty_recipients =
3673            KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3674                .with_allowed_calls(vec![CallScope {
3675                    target: Address::random(),
3676                    selector_rules: vec![SelectorRule {
3677                        selector: [0xa9, 0x05, 0x9c, 0xbb],
3678                        recipients: vec![],
3679                    }],
3680                }])
3681                .into_signed(PrimitiveSignature::Secp256k1(
3682                    alloy_primitives::Signature::test_signature(),
3683                ));
3684
3685        let translated = translate_allowed_calls_for_precompile(&empty_recipients);
3686        assert_eq!(translated.len(), 1);
3687        assert_eq!(translated[0].selectorRules.len(), 1);
3688        assert!(translated[0].selectorRules[0].recipients.is_empty());
3689    }
3690
3691    #[test]
3692    fn test_key_authorization_gas_in_batch() {
3693        use crate::TempoBatchCallEnv;
3694        use alloy_primitives::{Bytes, TxKind};
3695        use revm::interpreter::gas::calculate_initial_tx_gas;
3696        use tempo_primitives::transaction::{
3697            Call, KeyAuthorization, SignatureType, SignedKeyAuthorization, TempoSignature,
3698            TokenLimit,
3699        };
3700
3701        let calldata = Bytes::from(vec![1, 2, 3]);
3702
3703        let call = Call {
3704            to: TxKind::Call(Address::random()),
3705            value: U256::ZERO,
3706            input: calldata.clone(),
3707        };
3708
3709        // Create key authorization with 2 limits
3710        let key_auth: SignedKeyAuthorization =
3711            KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3712                .with_limits(vec![
3713                    TokenLimit {
3714                        token: Address::random(),
3715                        limit: U256::from(1000),
3716                        period: 0,
3717                    },
3718                    TokenLimit {
3719                        token: Address::random(),
3720                        limit: U256::from(2000),
3721                        period: 0,
3722                    },
3723                ])
3724                .into_signed(PrimitiveSignature::Secp256k1(
3725                    alloy_primitives::Signature::test_signature(),
3726                ));
3727
3728        let aa_env_with_key_auth = TempoBatchCallEnv {
3729            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3730                alloy_primitives::Signature::test_signature(),
3731            )),
3732            aa_calls: vec![call.clone()],
3733            key_authorization: Some(key_auth),
3734            signature_hash: B256::ZERO,
3735            ..Default::default()
3736        };
3737
3738        let aa_env_without_key_auth = TempoBatchCallEnv {
3739            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3740                alloy_primitives::Signature::test_signature(),
3741            )),
3742            aa_calls: vec![call],
3743            key_authorization: None,
3744            signature_hash: B256::ZERO,
3745            ..Default::default()
3746        };
3747
3748        // Calculate gas WITH key authorization
3749        let gas_with_key_auth = calculate_aa_batch_intrinsic_gas(
3750            &aa_env_with_key_auth,
3751            &GasParams::default(),
3752            None::<std::iter::Empty<&AccessListItem>>,
3753            tempo_chainspec::hardfork::TempoHardfork::default(),
3754        )
3755        .unwrap();
3756
3757        // Calculate gas WITHOUT key authorization
3758        let gas_without_key_auth = calculate_aa_batch_intrinsic_gas(
3759            &aa_env_without_key_auth,
3760            &GasParams::default(),
3761            None::<std::iter::Empty<&AccessListItem>>,
3762            tempo_chainspec::hardfork::TempoHardfork::default(),
3763        )
3764        .unwrap();
3765
3766        // Expected key auth gas: 30,000 (base + ecrecover) + 2 * 22,000 (limits) = 74,000
3767        let expected_key_auth_gas = KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 2 * KEY_AUTH_PER_LIMIT_GAS;
3768
3769        assert_eq!(
3770            gas_with_key_auth.initial_total_gas() - gas_without_key_auth.initial_total_gas(),
3771            expected_key_auth_gas,
3772            "Key authorization should add exactly {expected_key_auth_gas} gas to batch",
3773        );
3774
3775        // Also verify absolute values
3776        let spec = tempo_chainspec::hardfork::TempoHardfork::default();
3777        let base_tx_gas = calculate_initial_tx_gas(spec.into(), &calldata, false, 0, 0, 0);
3778        let expected_without = base_tx_gas.initial_total_gas(); // no cold access for single call
3779        let expected_with = expected_without + expected_key_auth_gas;
3780
3781        assert_eq!(
3782            gas_without_key_auth.initial_total_gas(),
3783            expected_without,
3784            "Gas without key auth should match expected"
3785        );
3786        assert_eq!(
3787            gas_with_key_auth.initial_total_gas(),
3788            expected_with,
3789            "Gas with key auth should match expected"
3790        );
3791    }
3792
3793    #[test]
3794    fn test_2d_nonce_gas_in_intrinsic_gas() {
3795        use crate::gas_params::tempo_gas_params;
3796        use revm::{context_interface::cfg::GasId, handler::Handler};
3797
3798        const BASE_INTRINSIC_GAS: u64 = 21_000;
3799
3800        for spec in [
3801            TempoHardfork::Genesis,
3802            TempoHardfork::T0,
3803            TempoHardfork::T1,
3804            TempoHardfork::T1A,
3805            TempoHardfork::T1B,
3806            TempoHardfork::T2,
3807        ] {
3808            let gas_params = tempo_gas_params(spec);
3809
3810            let make_evm = |nonce: u64, nonce_key: U256| {
3811                let journal = Journal::new(CacheDB::new(EmptyDB::default()));
3812                let mut cfg = CfgEnv::<TempoHardfork>::default();
3813                cfg.spec = spec;
3814                cfg.gas_params = gas_params.clone();
3815                let ctx = Context::mainnet()
3816                    .with_db(CacheDB::new(EmptyDB::default()))
3817                    .with_block(TempoBlockEnv::default())
3818                    .with_cfg(cfg)
3819                    .with_tx(TempoTxEnv {
3820                        inner: revm::context::TxEnv {
3821                            gas_limit: 1_000_000,
3822                            nonce,
3823                            ..Default::default()
3824                        },
3825                        tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3826                            aa_calls: vec![Call {
3827                                to: TxKind::Call(Address::random()),
3828                                value: U256::ZERO,
3829                                input: Bytes::new(),
3830                            }],
3831                            nonce_key,
3832                            ..Default::default()
3833                        })),
3834                        ..Default::default()
3835                    })
3836                    .with_new_journal(journal);
3837                TempoEvm::<_, ()>::new(ctx, ())
3838            };
3839
3840            let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
3841
3842            // Case 1: Protocol nonce (nonce_key == 0, nonce > 0) - no additional gas
3843            {
3844                let mut evm = make_evm(5, U256::ZERO);
3845                let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
3846                assert_eq!(
3847                    gas.initial_total_gas(),
3848                    BASE_INTRINSIC_GAS,
3849                    "{spec:?}: protocol nonce (nonce_key=0, nonce>0) should have no extra gas"
3850                );
3851            }
3852
3853            // Case 2: nonce_key != 0, nonce == 0
3854            {
3855                let expected = if spec.is_t1() {
3856                    // T1+: any nonce==0 charges new_account_cost (250k)
3857                    BASE_INTRINSIC_GAS + gas_params.get(GasId::new_account_cost())
3858                } else {
3859                    // Pre-T1: charges gas_new_nonce_key for new 2D key
3860                    BASE_INTRINSIC_GAS + spec.gas_new_nonce_key()
3861                };
3862                let mut evm = make_evm(0, U256::ONE);
3863                let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
3864                assert_eq!(
3865                    gas.initial_total_gas(),
3866                    expected,
3867                    "{spec:?}: nonce_key!=0, nonce==0 gas mismatch"
3868                );
3869            }
3870
3871            // Case 3: Existing 2D nonce key (nonce_key != 0, nonce > 0)
3872            {
3873                let mut evm = make_evm(5, U256::ONE);
3874                let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
3875                assert_eq!(
3876                    gas.initial_total_gas(),
3877                    BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(),
3878                    "{spec:?}: existing 2D nonce key gas mismatch"
3879                );
3880            }
3881        }
3882    }
3883
3884    #[test]
3885    fn test_2d_nonce_gas_limit_validation() {
3886        use crate::gas_params::tempo_gas_params;
3887        use revm::{context_interface::cfg::GasId, handler::Handler};
3888
3889        const BASE_INTRINSIC_GAS: u64 = 21_000;
3890
3891        for spec in [
3892            TempoHardfork::Genesis,
3893            TempoHardfork::T0,
3894            TempoHardfork::T1,
3895            TempoHardfork::T2,
3896        ] {
3897            let gas_params = tempo_gas_params(spec);
3898
3899            // Build spec-specific test cases: (gas_limit, nonce, expected_result)
3900            let nonce_zero_gas = if spec.is_t1() {
3901                gas_params.get(GasId::new_account_cost())
3902            } else {
3903                spec.gas_new_nonce_key()
3904            };
3905            let nonce_zero_state_gas = gas_params.new_account_state_gas();
3906            let nonce_zero_total = nonce_zero_gas + nonce_zero_state_gas;
3907
3908            let cases = if spec.is_t0() {
3909                let mut cases = vec![
3910                    (BASE_INTRINSIC_GAS + nonce_zero_total, 0, true), // Exactly sufficient for nonce==0 (exec + state)
3911                    (BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(), 1, true), // Exactly sufficient for existing key
3912                ];
3913                // Insufficient: below total required for nonce==0
3914                cases.push((BASE_INTRINSIC_GAS + nonce_zero_total - 1, 0u64, false));
3915                cases
3916            } else {
3917                // Genesis: nonce gas is added AFTER validation, so lower gas_limit still passes
3918                vec![
3919                    (BASE_INTRINSIC_GAS + 10_000, 0u64, true), // Passes validation (nonce gas added after)
3920                    (BASE_INTRINSIC_GAS + nonce_zero_gas, 0, true), // Also passes
3921                    (BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(), 1, true), // Also passes
3922                    (BASE_INTRINSIC_GAS - 1, 0, false),        // Below base intrinsic gas
3923                ]
3924            };
3925
3926            for (gas_limit, nonce, should_succeed) in cases {
3927                let journal = Journal::new(CacheDB::new(EmptyDB::default()));
3928                let mut cfg = CfgEnv::<TempoHardfork>::default();
3929                cfg.spec = spec;
3930                cfg.gas_params = gas_params.clone();
3931                let ctx = Context::mainnet()
3932                    .with_db(CacheDB::new(EmptyDB::default()))
3933                    .with_block(TempoBlockEnv::default())
3934                    .with_cfg(cfg)
3935                    .with_tx(TempoTxEnv {
3936                        inner: revm::context::TxEnv {
3937                            gas_limit,
3938                            nonce,
3939                            ..Default::default()
3940                        },
3941                        tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3942                            aa_calls: vec![Call {
3943                                to: TxKind::Call(Address::random()),
3944                                value: U256::ZERO,
3945                                input: Bytes::new(),
3946                            }],
3947                            nonce_key: U256::ONE,
3948                            ..Default::default()
3949                        })),
3950                        ..Default::default()
3951                    })
3952                    .with_new_journal(journal);
3953
3954                let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
3955                let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
3956                let result = handler.validate_initial_tx_gas(&mut evm);
3957
3958                if should_succeed {
3959                    assert!(
3960                        result.is_ok(),
3961                        "{spec:?}: gas_limit={gas_limit}, nonce={nonce}: expected success but got error"
3962                    );
3963                } else {
3964                    let err = result.expect_err(&format!(
3965                        "{spec:?}: gas_limit={gas_limit}, nonce={nonce}: should fail"
3966                    ));
3967                    assert!(
3968                        matches!(
3969                            err.as_invalid_tx_err(),
3970                            Some(TempoInvalidTransaction::EthInvalidTransaction(
3971                                InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
3972                            ))
3973                        ),
3974                        "Expected CallGasCostMoreThanGasLimit, got: {err:?}"
3975                    );
3976                }
3977            }
3978        }
3979    }
3980
3981    #[test]
3982    fn test_t3_scope_validation_moves_to_execution() {
3983        const CALL_SCOPE_SELECTOR: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
3984
3985        let caller = Address::repeat_byte(0x11);
3986        let access_key = Address::repeat_byte(0x22);
3987        let target = DEFAULT_FEE_TOKEN;
3988
3989        let signature =
3990            TempoSignature::Keychain(tempo_primitives::transaction::KeychainSignature::new(
3991                caller,
3992                tempo_primitives::transaction::PrimitiveSignature::Secp256k1(
3993                    alloy_primitives::Signature::test_signature(),
3994                ),
3995            ));
3996
3997        let mut cfg = CfgEnv::<TempoHardfork>::default();
3998        cfg.spec = TempoHardfork::T3;
3999
4000        let tx_env = TempoTxEnv {
4001            inner: revm::context::TxEnv {
4002                caller,
4003                gas_limit: 1_000_000,
4004                kind: TxKind::Call(target),
4005                ..Default::default()
4006            },
4007            fee_token: Some(DEFAULT_FEE_TOKEN),
4008            tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4009                signature,
4010                aa_calls: vec![Call {
4011                    to: TxKind::Call(target),
4012                    value: U256::ZERO,
4013                    input: Bytes::from_static(&CALL_SCOPE_SELECTOR),
4014                }],
4015                signature_hash: B256::ZERO,
4016                override_key_id: Some(access_key),
4017                ..Default::default()
4018            })),
4019            ..Default::default()
4020        };
4021
4022        let mut test = TestHandlerEvm::with_cfg(TempoHardfork::T3, tx_env, |cfg_override| {
4023            *cfg_override = cfg;
4024        });
4025
4026        StorageCtx::enter_ctx(&mut test.evm.inner.ctx, StorageActions::disabled(), || {
4027            let mut keychain = AccountKeychain::new();
4028
4029            keychain.initialize().expect("keychain initialized");
4030            keychain
4031                .set_transaction_key(Address::ZERO)
4032                .expect("root key setup succeeds");
4033            keychain
4034                .set_tx_origin(caller)
4035                .expect("tx.origin setup succeeds");
4036            keychain
4037                .authorize_key(
4038                    caller,
4039                    access_key,
4040                    PrecompileSignatureType::Secp256k1,
4041                    KeyRestrictions {
4042                        expiry: u64::MAX,
4043                        enforceLimits: false,
4044                        limits: vec![],
4045                        allowAnyCalls: false,
4046                        allowedCalls: vec![PrecompileCallScope {
4047                            target,
4048                            selectorRules: vec![PrecompileSelectorRule {
4049                                selector: CALL_SCOPE_SELECTOR.into(),
4050                                recipients: vec![],
4051                            }],
4052                        }],
4053                    },
4054                    None,
4055                )
4056                .expect("access key authorization succeeds");
4057        });
4058
4059        let init_gas = test.validate_initial_tx_gas();
4060        assert!(
4061            init_gas.floor_gas <= init_gas.initial_total_gas(),
4062            "test requires floor gas to not exceed intrinsic gas"
4063        );
4064
4065        test.evm.inner.ctx.tx.inner.gas_limit = init_gas.initial_total_gas();
4066
4067        test.validate_against_state_and_deduct_caller()
4068            .expect("scope validation no longer runs during state validation");
4069
4070        let result = test.execute(&init_gas);
4071
4072        assert!(
4073            matches!(
4074                result.instruction_result(),
4075                revm::interpreter::InstructionResult::PrecompileOOG
4076            ),
4077            "expected scope validation to fail during execution with OOG, got: {:?}",
4078            result.instruction_result()
4079        );
4080        assert_eq!(
4081            result.gas().limit(),
4082            init_gas.initial_total_gas(),
4083            "batch OOG should report the full tx gas budget"
4084        );
4085        assert_eq!(
4086            result.gas().total_gas_spent(),
4087            init_gas.initial_total_gas(),
4088            "batch OOG should consume the full tx gas budget"
4089        );
4090        assert_eq!(result.gas().refunded(), 0);
4091    }
4092
4093    #[test]
4094    fn test_t3_scope_validation_returns_call_not_allowed_revert_data() {
4095        use alloy_sol_types::SolInterface;
4096        use tempo_contracts::precompiles::AccountKeychainError;
4097
4098        const ALLOWED_SELECTOR: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
4099        const DENIED_SELECTOR: [u8; 4] = [0xca, 0xfe, 0xba, 0xbe];
4100
4101        let caller = Address::repeat_byte(0x11);
4102        let access_key = Address::repeat_byte(0x22);
4103        let target = DEFAULT_FEE_TOKEN;
4104
4105        let signature =
4106            TempoSignature::Keychain(tempo_primitives::transaction::KeychainSignature::new(
4107                caller,
4108                tempo_primitives::transaction::PrimitiveSignature::Secp256k1(
4109                    alloy_primitives::Signature::test_signature(),
4110                ),
4111            ));
4112
4113        let mut cfg = CfgEnv::<TempoHardfork>::default();
4114        cfg.spec = TempoHardfork::T3;
4115
4116        let tx_env = TempoTxEnv {
4117            inner: revm::context::TxEnv {
4118                caller,
4119                gas_limit: 1_000_000,
4120                kind: TxKind::Call(target),
4121                ..Default::default()
4122            },
4123            fee_token: Some(DEFAULT_FEE_TOKEN),
4124            tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4125                signature,
4126                aa_calls: vec![Call {
4127                    to: TxKind::Call(target),
4128                    value: U256::ZERO,
4129                    input: Bytes::from_static(&DENIED_SELECTOR),
4130                }],
4131                signature_hash: B256::ZERO,
4132                override_key_id: Some(access_key),
4133                ..Default::default()
4134            })),
4135            ..Default::default()
4136        };
4137
4138        let ctx = Context::mainnet()
4139            .with_db(CacheDB::new(EmptyDB::default()))
4140            .with_block(TempoBlockEnv::default())
4141            .with_cfg(cfg)
4142            .with_tx(tx_env.clone())
4143            .with_new_journal(create_test_journal());
4144
4145        let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
4146        let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4147
4148        StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
4149            let mut keychain = AccountKeychain::new();
4150
4151            keychain.initialize().expect("keychain initialized");
4152            keychain
4153                .set_transaction_key(Address::ZERO)
4154                .expect("root key setup succeeds");
4155            keychain
4156                .set_tx_origin(caller)
4157                .expect("tx.origin setup succeeds");
4158            keychain
4159                .authorize_key(
4160                    caller,
4161                    access_key,
4162                    PrecompileSignatureType::Secp256k1,
4163                    KeyRestrictions {
4164                        expiry: u64::MAX,
4165                        enforceLimits: false,
4166                        limits: vec![],
4167                        allowAnyCalls: false,
4168                        allowedCalls: vec![PrecompileCallScope {
4169                            target,
4170                            selectorRules: vec![PrecompileSelectorRule {
4171                                selector: ALLOWED_SELECTOR.into(),
4172                                recipients: vec![],
4173                            }],
4174                        }],
4175                    },
4176                    None,
4177                )
4178                .expect("access key authorization succeeds");
4179        });
4180
4181        let init_gas = handler
4182            .validate_initial_tx_gas(&mut evm)
4183            .expect("initial gas validation should succeed");
4184
4185        handler
4186            .validate_against_state_and_deduct_caller(&mut evm, &mut Default::default())
4187            .expect("scope validation no longer runs during state validation");
4188
4189        let result = handler
4190            .execution(&mut evm, &init_gas)
4191            .expect("execution should return a frame result");
4192
4193        let expected_revert: Bytes = AccountKeychainError::call_not_allowed().abi_encode().into();
4194
4195        assert_eq!(result.instruction_result(), InstructionResult::Revert);
4196        assert_eq!(result.output().data(), &expected_revert);
4197        assert!(
4198            result.gas().total_gas_spent() < tx_env.gas_limit,
4199            "prevalidate revert must not consume the full gas_limit"
4200        );
4201    }
4202
4203    #[test]
4204    fn test_t3_scope_validation_empty_calls_returns_custom_error() {
4205        let caller = Address::repeat_byte(0x11);
4206        let access_key = Address::repeat_byte(0x22);
4207
4208        let signature =
4209            TempoSignature::Keychain(tempo_primitives::transaction::KeychainSignature::new(
4210                caller,
4211                tempo_primitives::transaction::PrimitiveSignature::Secp256k1(
4212                    alloy_primitives::Signature::test_signature(),
4213                ),
4214            ));
4215
4216        let mut cfg = CfgEnv::<TempoHardfork>::default();
4217        cfg.spec = TempoHardfork::T3;
4218
4219        let tx_env = TempoTxEnv {
4220            inner: revm::context::TxEnv {
4221                caller,
4222                gas_limit: 1_000_000,
4223                ..Default::default()
4224            },
4225            tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4226                signature,
4227                aa_calls: vec![],
4228                signature_hash: B256::ZERO,
4229                override_key_id: Some(access_key),
4230                ..Default::default()
4231            })),
4232            ..Default::default()
4233        };
4234
4235        let ctx = Context::mainnet()
4236            .with_db(CacheDB::new(EmptyDB::default()))
4237            .with_block(TempoBlockEnv::default())
4238            .with_cfg(cfg)
4239            .with_tx(tx_env)
4240            .with_new_journal(create_test_journal());
4241
4242        let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
4243        let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4244        let mut remaining_gas = 100_000;
4245
4246        let err = handler
4247            .prevalidate_keychain_call_scopes(&mut evm, &[], &mut remaining_gas, 0)
4248            .expect_err("empty calls should return an error instead of panicking");
4249
4250        match err {
4251            EVMError::Custom(msg) => {
4252                assert_eq!(msg, "AA transactions must contain at least one call");
4253            }
4254            other => panic!("expected custom error, got: {other:?}"),
4255        }
4256    }
4257
4258    /// TIP-1060: T7 removes the EIP-3529 one-fifth refund cap; pre-T7 keeps it.
4259    #[test]
4260    fn test_refund_cap_removed_on_t7() {
4261        use revm::{
4262            Context, Journal,
4263            context::CfgEnv,
4264            database::{CacheDB, EmptyDB},
4265            handler::FrameResult,
4266            interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult},
4267        };
4268
4269        // Refund (50k) deliberately exceeds one fifth of the gas used (100k / 5 = 20k).
4270        const SPENT: u64 = 100_000;
4271        const REFUND: i64 = 50_000;
4272        const CAPPED: i64 = (SPENT / 5) as i64;
4273
4274        let refunded_for_spec = |spec: TempoHardfork| -> i64 {
4275            let mut cfg = CfgEnv::<TempoHardfork>::default();
4276            cfg.spec = spec;
4277            let ctx = Context::mainnet()
4278                .with_db(CacheDB::new(EmptyDB::default()))
4279                .with_block(TempoBlockEnv::default())
4280                .with_cfg(cfg)
4281                .with_tx(TempoTxEnv::default())
4282                .with_new_journal(Journal::new(CacheDB::new(EmptyDB::default())));
4283            let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
4284            let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4285
4286            let mut gas = Gas::new(SPENT);
4287            gas.set_spent(SPENT);
4288            gas.record_refund(REFUND);
4289            let mut frame_result = FrameResult::Call(CallOutcome::new(
4290                InterpreterResult::new(InstructionResult::Stop, Bytes::new(), gas),
4291                0..0,
4292            ));
4293
4294            handler.refund(&mut evm, &mut frame_result, 0);
4295            frame_result.gas().refunded()
4296        };
4297
4298        assert_eq!(
4299            refunded_for_spec(TempoHardfork::T6),
4300            CAPPED,
4301            "pre-T7 must cap the refund at one fifth of gas used"
4302        );
4303        assert_eq!(
4304            refunded_for_spec(TempoHardfork::T7),
4305            REFUND,
4306            "T7 must credit the full refund, with no EIP-3529 cap"
4307        );
4308    }
4309
4310    #[test]
4311    fn test_multicall_gas_refund_accounting() {
4312        use crate::evm::TempoEvm;
4313        use alloy_primitives::{Bytes, TxKind};
4314        use revm::{
4315            Context, Journal,
4316            context::CfgEnv,
4317            database::{CacheDB, EmptyDB},
4318            handler::FrameResult,
4319            interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult},
4320        };
4321        use tempo_primitives::transaction::Call;
4322
4323        const GAS_LIMIT: u64 = 1_000_000;
4324        const INTRINSIC_GAS: u64 = 21_000;
4325        // Mock call's gas: (CALL_0, CALL_1)
4326        const SPENT: (u64, u64) = (1000, 500);
4327        const REFUND: (i64, i64) = (100, 50);
4328
4329        // Create minimal EVM context
4330        let db = CacheDB::new(EmptyDB::default());
4331        let journal = Journal::new(db);
4332        let ctx = Context::mainnet()
4333            .with_db(CacheDB::new(EmptyDB::default()))
4334            .with_block(TempoBlockEnv::default())
4335            .with_cfg(CfgEnv::default())
4336            .with_tx(TempoTxEnv {
4337                inner: revm::context::TxEnv {
4338                    gas_limit: GAS_LIMIT,
4339                    ..Default::default()
4340                },
4341                ..Default::default()
4342            })
4343            .with_new_journal(journal);
4344
4345        let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
4346        let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4347
4348        // Create mock calls
4349        let calls = vec![
4350            Call {
4351                to: TxKind::Call(Address::random()),
4352                value: U256::ZERO,
4353                input: Bytes::new(),
4354            },
4355            Call {
4356                to: TxKind::Call(Address::random()),
4357                value: U256::ZERO,
4358                input: Bytes::new(),
4359            },
4360        ];
4361
4362        let (mut call_idx, calls_gas) = (0, [(SPENT.0, REFUND.0), (SPENT.1, REFUND.1)]);
4363        let result = handler.execute_multi_call_with(
4364            &mut evm,
4365            GAS_LIMIT - INTRINSIC_GAS,
4366            0,
4367            calls,
4368            |_handler, _evm, gas, _reservoir| {
4369                let (spent, refund) = calls_gas[call_idx];
4370                call_idx += 1;
4371
4372                // Create gas with specific spent and refund values
4373                let mut gas = Gas::new(gas);
4374                gas.set_spent(spent);
4375                gas.record_refund(refund);
4376
4377                // Mock successful frame result
4378                Ok(FrameResult::Call(CallOutcome::new(
4379                    InterpreterResult::new(InstructionResult::Stop, Bytes::new(), gas),
4380                    0..0,
4381                )))
4382            },
4383        );
4384
4385        let result = result.expect("execute_multi_call_with should succeed");
4386        let final_gas = result.gas();
4387
4388        assert_eq!(
4389            final_gas.total_gas_spent(),
4390            INTRINSIC_GAS + SPENT.0 + SPENT.1,
4391            "Total spent should be intrinsic_gas + sum of all calls' spent values"
4392        );
4393        assert_eq!(
4394            final_gas.refunded(),
4395            REFUND.0 + REFUND.1,
4396            "Total refund should be sum of all calls' refunded values"
4397        );
4398        assert_eq!(
4399            final_gas.used(),
4400            INTRINSIC_GAS + SPENT.0 + SPENT.1 - (REFUND.0 + REFUND.1) as u64,
4401            "used() should be spent - refund"
4402        );
4403    }
4404
4405    /// Strategy for optional u64 timestamps.
4406    fn arb_opt_timestamp() -> impl Strategy<Value = Option<u64>> {
4407        prop_oneof![Just(None), any::<u64>().prop_map(Some)]
4408    }
4409
4410    /// Helper to create a secp256k1 signature for testing gas calculations.
4411    ///
4412    /// Note: We use a test signature rather than real valid/invalid signatures because
4413    /// these gas calculation functions only depend on the signature *type* (Secp256k1,
4414    /// P256, WebAuthn), not on cryptographic validity. Signature verification happens
4415    /// separately during `recover_signer()` before transactions enter the pool.
4416    fn secp256k1_sig() -> TempoSignature {
4417        TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
4418            alloy_primitives::Signature::test_signature(),
4419        ))
4420    }
4421
4422    /// Helper to create a TempoBatchCallEnv with specified calls.
4423    fn make_aa_env(calls: Vec<Call>) -> TempoBatchCallEnv {
4424        TempoBatchCallEnv {
4425            signature: secp256k1_sig(),
4426            aa_calls: calls,
4427            key_authorization: None,
4428            signature_hash: B256::ZERO,
4429            ..Default::default()
4430        }
4431    }
4432
4433    /// Helper to create a single-call TempoBatchCallEnv with given calldata.
4434    fn make_single_call_env(calldata: Bytes) -> TempoBatchCallEnv {
4435        make_aa_env(vec![Call {
4436            to: TxKind::Call(Address::ZERO),
4437            value: U256::ZERO,
4438            input: calldata,
4439        }])
4440    }
4441
4442    /// Helper to create a multi-call TempoBatchCallEnv with N empty calls.
4443    fn make_multi_call_env(num_calls: usize) -> TempoBatchCallEnv {
4444        make_aa_env(
4445            (0..num_calls)
4446                .map(|_| Call {
4447                    to: TxKind::Call(Address::ZERO),
4448                    value: U256::ZERO,
4449                    input: Bytes::new(),
4450                })
4451                .collect(),
4452        )
4453    }
4454
4455    /// Helper to compute AA batch gas with no access list.
4456    fn compute_aa_gas(env: &TempoBatchCallEnv) -> InitialAndFloorGas {
4457        calculate_aa_batch_intrinsic_gas(
4458            env,
4459            &GasParams::default(),
4460            None::<std::iter::Empty<&AccessListItem>>,
4461            tempo_chainspec::hardfork::TempoHardfork::default(),
4462        )
4463        .unwrap()
4464    }
4465
4466    proptest! {
4467        #![proptest_config(ProptestConfig::with_cases(500))]
4468
4469        /// Property: validate_time_window returns Ok if (after <= ts < before)
4470        #[test]
4471        fn proptest_validate_time_window_correctness(
4472            valid_after in arb_opt_timestamp(),
4473            valid_before in arb_opt_timestamp(),
4474            block_timestamp in any::<u64>(),
4475        ) {
4476            let result = validate_time_window(valid_after, valid_before, block_timestamp);
4477
4478            let after_ok = valid_after.is_none_or(|after| block_timestamp >= after);
4479            let before_ok = valid_before.is_none_or(|before| block_timestamp < before);
4480            let expected_valid = after_ok && before_ok;
4481
4482            prop_assert_eq!(result.is_ok(), expected_valid,
4483                "valid_after={:?}, valid_before={:?}, block_ts={}, result={:?}",
4484                valid_after, valid_before, block_timestamp, result);
4485        }
4486
4487        /// Property: validate_time_window with None constraints always succeeds
4488        #[test]
4489        fn proptest_validate_time_window_none_always_valid(block_timestamp in any::<u64>()) {
4490            prop_assert!(validate_time_window(None, None, block_timestamp).is_ok());
4491        }
4492
4493        /// Property: validate_time_window with valid_after=0 is equivalent to None
4494        ///
4495        /// This tests the equivalence property: Some(0) and None for valid_after should produce
4496        /// identical results regardless of what valid_before is. We intentionally don't constrain
4497        /// valid_before because we're testing that the equivalence holds in all cases (both when
4498        /// valid_before causes success and when it causes failure).
4499        #[test]
4500        fn proptest_validate_time_window_zero_after_equivalent_to_none(
4501            valid_before in arb_opt_timestamp(),
4502            block_timestamp in any::<u64>(),
4503        ) {
4504            let with_zero = validate_time_window(Some(0), valid_before, block_timestamp);
4505            let with_none = validate_time_window(None, valid_before, block_timestamp);
4506            prop_assert_eq!(with_zero.is_ok(), with_none.is_ok());
4507        }
4508
4509        /// Property: validate_time_window - if before <= after, the window is empty
4510        #[test]
4511        fn proptest_validate_time_window_empty_window(
4512            valid_after in 1u64..=u64::MAX,
4513            offset in 0u64..1000u64,
4514        ) {
4515            let valid_before = valid_after.saturating_sub(offset);
4516            let result = validate_time_window(Some(valid_after), Some(valid_before), valid_after);
4517            prop_assert!(result.is_err(), "Empty window should reject all timestamps");
4518        }
4519
4520        /// Property: signature gas ordering is consistent: secp256k1 <= p256 <= webauthn
4521        #[test]
4522        fn proptest_signature_gas_ordering(webauthn_data_len in 0usize..1000) {
4523            let secp_sig = PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature());
4524            let p256_sig = PrimitiveSignature::P256(P256SignatureWithPreHash {
4525                r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO, pre_hash: false,
4526            });
4527            let webauthn_sig = PrimitiveSignature::WebAuthn(WebAuthnSignature {
4528                r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO,
4529                webauthn_data: Bytes::from(vec![0u8; webauthn_data_len]),
4530            });
4531
4532            let secp_gas = primitive_signature_verification_gas(&secp_sig);
4533            let p256_gas = primitive_signature_verification_gas(&p256_sig);
4534            let webauthn_gas = primitive_signature_verification_gas(&webauthn_sig);
4535
4536            prop_assert!(secp_gas <= p256_gas, "secp256k1 should be <= p256");
4537            prop_assert!(p256_gas <= webauthn_gas, "p256 should be <= webauthn");
4538        }
4539
4540        /// Property: gas calculation monotonicity - more calldata means more gas (non-zero bytes)
4541        /// Non-zero bytes cost 16 gas each, so monotonicity holds for uniform non-zero calldata.
4542        #[test]
4543        fn proptest_gas_monotonicity_calldata_nonzero(
4544            calldata_len1 in 0usize..1000,
4545            calldata_len2 in 0usize..1000,
4546        ) {
4547            let gas1 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len1])));
4548            let gas2 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len2])));
4549
4550            if calldata_len1 <= calldata_len2 {
4551                prop_assert!(gas1.initial_total_gas() <= gas2.initial_total_gas(),
4552                    "More calldata should mean more gas: len1={}, gas1={}, len2={}, gas2={}",
4553                    calldata_len1, gas1.initial_total_gas(), calldata_len2, gas2.initial_total_gas());
4554            } else {
4555                prop_assert!(gas1.initial_total_gas() >= gas2.initial_total_gas(),
4556                    "Less calldata should mean less gas: len1={}, gas1={}, len2={}, gas2={}",
4557                    calldata_len1, gas1.initial_total_gas(), calldata_len2, gas2.initial_total_gas());
4558            }
4559        }
4560
4561        /// Property: gas calculation monotonicity - more calldata means more gas (zero bytes)
4562        /// Zero bytes cost 4 gas each, so monotonicity holds for uniform zero calldata.
4563        #[test]
4564        fn proptest_gas_monotonicity_calldata_zero(
4565            calldata_len1 in 0usize..1000,
4566            calldata_len2 in 0usize..1000,
4567        ) {
4568            let gas1 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len1])));
4569            let gas2 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len2])));
4570
4571            if calldata_len1 <= calldata_len2 {
4572                prop_assert!(gas1.initial_total_gas() <= gas2.initial_total_gas(),
4573                    "More zero-byte calldata should mean more gas: len1={}, gas1={}, len2={}, gas2={}",
4574                    calldata_len1, gas1.initial_total_gas(), calldata_len2, gas2.initial_total_gas());
4575            } else {
4576                prop_assert!(gas1.initial_total_gas() >= gas2.initial_total_gas(),
4577                    "Less zero-byte calldata should mean less gas: len1={}, gas1={}, len2={}, gas2={}",
4578                    calldata_len1, gas1.initial_total_gas(), calldata_len2, gas2.initial_total_gas());
4579            }
4580        }
4581
4582        /// Property: zero-byte calldata costs less gas than non-zero byte calldata of same length.
4583        /// Zero bytes cost 4 gas each, non-zero bytes cost 16 gas each.
4584        #[test]
4585        fn proptest_zero_bytes_cheaper_than_nonzero(calldata_len in 1usize..1000) {
4586            let zero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len])));
4587            let nonzero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len])));
4588
4589            prop_assert!(zero_gas.initial_total_gas() < nonzero_gas.initial_total_gas(),
4590                "Zero-byte calldata should cost less: len={}, zero_gas={}, nonzero_gas={}",
4591                calldata_len, zero_gas.initial_total_gas(), nonzero_gas.initial_total_gas());
4592        }
4593
4594        /// Property: mixed calldata gas is bounded by all-zero and all-nonzero extremes.
4595        /// Gas for mixed calldata should be between gas for all-zero and all-nonzero of same length.
4596        #[test]
4597        fn proptest_mixed_calldata_gas_bounded(
4598            calldata_len in 1usize..500,
4599            nonzero_ratio in 0u8..=100,
4600        ) {
4601            // Create mixed calldata where nonzero_ratio% of bytes are non-zero
4602            let calldata: Vec<u8> = (0..calldata_len)
4603                .map(|i| if (i * 100 / calldata_len) < nonzero_ratio as usize { 1u8 } else { 0u8 })
4604                .collect();
4605
4606            let mixed_gas = compute_aa_gas(&make_single_call_env(Bytes::from(calldata)));
4607            let zero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len])));
4608            let nonzero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len])));
4609
4610            prop_assert!(mixed_gas.initial_total_gas() >= zero_gas.initial_total_gas(),
4611                "Mixed calldata gas should be >= all-zero gas: mixed={}, zero={}",
4612                mixed_gas.initial_total_gas(), zero_gas.initial_total_gas());
4613            prop_assert!(mixed_gas.initial_total_gas() <= nonzero_gas.initial_total_gas(),
4614                "Mixed calldata gas should be <= all-nonzero gas: mixed={}, nonzero={}",
4615                mixed_gas.initial_total_gas(), nonzero_gas.initial_total_gas());
4616        }
4617
4618        /// Property: gas calculation monotonicity - more calls means more gas
4619        #[test]
4620        fn proptest_gas_monotonicity_call_count(
4621            num_calls1 in 1usize..10,
4622            num_calls2 in 1usize..10,
4623        ) {
4624            let gas1 = compute_aa_gas(&make_multi_call_env(num_calls1));
4625            let gas2 = compute_aa_gas(&make_multi_call_env(num_calls2));
4626
4627            if num_calls1 <= num_calls2 {
4628                prop_assert!(gas1.initial_total_gas() <= gas2.initial_total_gas(),
4629                    "More calls should mean more gas: calls1={}, gas1={}, calls2={}, gas2={}",
4630                    num_calls1, gas1.initial_total_gas(), num_calls2, gas2.initial_total_gas());
4631            } else {
4632                prop_assert!(gas1.initial_total_gas() >= gas2.initial_total_gas(),
4633                    "Fewer calls should mean less gas: calls1={}, gas1={}, calls2={}, gas2={}",
4634                    num_calls1, gas1.initial_total_gas(), num_calls2, gas2.initial_total_gas());
4635            }
4636        }
4637
4638        /// Property: AA batch gas with Secp256k1 signature equals exactly 21k base + cold access
4639        ///
4640        /// For minimal AA transactions (Secp256k1 sig, no calldata, no access list):
4641        /// - Base: 21,000 (same base stipend as regular transactions)
4642        /// - Plus: COLD_ACCOUNT_ACCESS_COST per additional call beyond the first
4643        ///
4644        /// AA transactions use the same 21k base as regular transactions because
4645        /// Secp256k1 signature verification adds 0 extra gas. Other signature types
4646        /// (P256, WebAuthn) add 5,000+ gas beyond this base.
4647        #[test]
4648        fn proptest_gas_aa_secp256k1_exact_bounds(num_calls in 1usize..5) {
4649            let gas = compute_aa_gas(&make_multi_call_env(num_calls));
4650
4651            // Expected exactly: 21k base + cold account access for each additional call
4652            let expected = 21_000 + COLD_ACCOUNT_ACCESS_COST * (num_calls.saturating_sub(1) as u64);
4653            prop_assert_eq!(gas.initial_total_gas(), expected,
4654                "Gas {} should equal expected {} for {} calls (21k + {}*COLD_ACCOUNT_ACCESS_COST)",
4655                gas.initial_total_gas(), expected, num_calls, num_calls.saturating_sub(1));
4656        }
4657
4658        /// Property: first_call returns the first call for AA transactions with any number of calls
4659        #[test]
4660        fn proptest_first_call_returns_first_for_aa(num_calls in 1usize..10) {
4661            let calls: Vec<Call> = (0..num_calls)
4662                .map(|i| Call {
4663                    to: TxKind::Call(Address::with_last_byte(i as u8)),
4664                    value: U256::ZERO,
4665                    input: Bytes::from(vec![i as u8; i + 1]),
4666                })
4667                .collect();
4668
4669            let expected_addr = Address::with_last_byte(0);
4670            let expected_input = vec![0u8; 1];
4671
4672            let tx_env = TempoTxEnv {
4673                inner: revm::context::TxEnv::default(),
4674                tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4675                    aa_calls: calls,
4676                    signature: secp256k1_sig(),
4677                    signature_hash: B256::ZERO,
4678                    ..Default::default()
4679                })),
4680                ..Default::default()
4681            };
4682
4683            let first = tx_env.first_call();
4684            prop_assert!(first.is_some(), "first_call should return Some for non-empty AA calls");
4685
4686            let (kind, input) = first.unwrap();
4687            prop_assert_eq!(*kind, TxKind::Call(expected_addr), "Should return first call's address");
4688            prop_assert_eq!(input, expected_input.as_slice(), "Should return first call's input");
4689        }
4690
4691        /// Property: first_call returns None for AA transaction with zero calls
4692        #[test]
4693        fn proptest_first_call_empty_aa(_dummy in 0u8..1) {
4694            let tx_env = TempoTxEnv {
4695                inner: revm::context::TxEnv::default(),
4696                tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4697                    aa_calls: vec![],
4698                    signature: secp256k1_sig(),
4699                    signature_hash: B256::ZERO,
4700                    ..Default::default()
4701                })),
4702                ..Default::default()
4703            };
4704
4705            prop_assert!(tx_env.first_call().is_none(), "first_call should return None for empty AA calls");
4706        }
4707
4708        /// Property: first_call returns inner tx data for non-AA transactions
4709        #[test]
4710        fn proptest_first_call_non_aa(calldata_len in 0usize..100) {
4711            let calldata = Bytes::from(vec![0xab_u8; calldata_len]);
4712            let target = Address::random();
4713
4714            let tx_env = TempoTxEnv {
4715                inner: revm::context::TxEnv {
4716                    kind: TxKind::Call(target),
4717                    data: calldata.clone(),
4718                    ..Default::default()
4719                },
4720                tempo_tx_env: None,
4721                ..Default::default()
4722            };
4723
4724            let first = tx_env.first_call();
4725            prop_assert!(first.is_some(), "first_call should return Some for non-AA tx");
4726
4727            let (kind, input) = first.unwrap();
4728            prop_assert_eq!(*kind, TxKind::Call(target), "Should return inner tx kind");
4729            prop_assert_eq!(input, calldata.as_ref(), "Should return inner tx data");
4730        }
4731
4732        /// Property: calculate_key_authorization_gas is monotonic in number of limits
4733        #[test]
4734        fn proptest_key_auth_gas_monotonic_limits(
4735            num_limits1 in 0usize..10,
4736            num_limits2 in 0usize..10,
4737        ) {
4738            use tempo_primitives::transaction::{
4739                SignatureType, SignedKeyAuthorization,
4740                key_authorization::KeyAuthorization,
4741                TokenLimit as PrimTokenLimit,
4742            };
4743
4744            let make_key_auth = |num_limits: usize| -> SignedKeyAuthorization {
4745                let mut auth =
4746                    KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::ZERO);
4747                if num_limits > 0 {
4748                    auth = auth.with_limits((0..num_limits).map(|i| PrimTokenLimit {
4749                        token: Address::with_last_byte(i as u8),
4750                        limit: U256::from(1000),
4751                        period: 0,
4752                    }).collect());
4753                }
4754                auth.into_signed(PrimitiveSignature::Secp256k1(
4755                    alloy_primitives::Signature::test_signature(),
4756                ))
4757            };
4758
4759            // Test both pre-T1B and T1B branches
4760            for (gas_params, spec) in [
4761                (GasParams::default(), tempo_chainspec::hardfork::TempoHardfork::default()),
4762                (crate::gas_params::tempo_gas_params(TempoHardfork::T1B), TempoHardfork::T1B),
4763            ] {
4764                let (gas1, _) = calculate_key_authorization_gas(&make_key_auth(num_limits1), &gas_params, spec);
4765                let (gas2, _) = calculate_key_authorization_gas(&make_key_auth(num_limits2), &gas_params, spec);
4766
4767                if num_limits1 <= num_limits2 {
4768                    prop_assert!(gas1 <= gas2,
4769                        "{spec:?}: More limits should mean more gas: limits1={}, gas1={}, limits2={}, gas2={}",
4770                        num_limits1, gas1, num_limits2, gas2);
4771                } else {
4772                    prop_assert!(gas1 >= gas2,
4773                        "{spec:?}: Fewer limits should mean less gas: limits1={}, gas1={}, limits2={}, gas2={}",
4774                        num_limits1, gas1, num_limits2, gas2);
4775                }
4776            }
4777        }
4778
4779        /// Property: calculate_key_authorization_gas minimum is KEY_AUTH_BASE_GAS + ECRECOVER_GAS
4780        #[test]
4781        fn proptest_key_auth_gas_minimum(
4782            sig_type in 0u8..3,
4783            num_limits in 0usize..5,
4784        ) {
4785            use tempo_primitives::transaction::{
4786                SignatureType, TokenLimit as PrimTokenLimit, key_authorization::KeyAuthorization,
4787            };
4788
4789            let signature = match sig_type {
4790                0 => PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature()),
4791                1 => PrimitiveSignature::P256(P256SignatureWithPreHash {
4792                    r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO, pre_hash: false,
4793                }),
4794                _ => PrimitiveSignature::WebAuthn(WebAuthnSignature {
4795                    r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO,
4796                    webauthn_data: Bytes::new(),
4797                }),
4798            };
4799
4800            let mut auth =
4801                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::ZERO);
4802            if num_limits > 0 {
4803                auth = auth.with_limits((0..num_limits).map(|i| PrimTokenLimit {
4804                    token: Address::with_last_byte(i as u8),
4805                    limit: U256::from(1000),
4806                    period: 0,
4807                }).collect());
4808            }
4809            let key_auth = auth.into_signed(signature);
4810
4811            // Pre-T1B: minimum is KEY_AUTH_BASE_GAS + ECRECOVER_GAS
4812            let (gas, _) = calculate_key_authorization_gas(&key_auth, &GasParams::default(), tempo_chainspec::hardfork::TempoHardfork::default());
4813            let min_gas = KEY_AUTH_BASE_GAS + ECRECOVER_GAS;
4814            prop_assert!(gas >= min_gas,
4815                "Pre-T1B: Key auth gas should be at least {min_gas}, got {gas}");
4816
4817            // T1B: minimum is ECRECOVER_GAS + sload + sstore (0 limits)
4818            let t1b_params = crate::gas_params::tempo_gas_params(TempoHardfork::T1B);
4819            let (gas_t1b, _) = calculate_key_authorization_gas(&key_auth, &t1b_params, TempoHardfork::T1B);
4820            let sstore = t1b_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
4821            let sload = t1b_params.warm_storage_read_cost() + t1b_params.cold_storage_additional_cost();
4822            let min_t1b = ECRECOVER_GAS + sload + sstore;
4823            prop_assert!(gas_t1b >= min_t1b,
4824                "T1B: Key auth gas should be at least {min_t1b}, got {gas_t1b}");
4825        }
4826    }
4827
4828    /// Test that T1 hardfork correctly charges 250k gas for nonce == 0.
4829    ///
4830    /// This test validates [TIP-1000]'s requirement:
4831    /// "Tempo transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas"
4832    ///
4833    /// The test proves the audit finding (claiming only 22,100 gas is charged) is a false positive
4834    /// by using delta-based assertions: gas(nonce=0) - gas(nonce>0) == new_account_cost.
4835    ///
4836    /// [TIP-1000]: <https://docs.tempo.xyz/protocol/tips/tip-1000>
4837    #[test]
4838    fn test_t1_2d_nonce_key_charges_250k_gas() {
4839        use crate::gas_params::tempo_gas_params;
4840        use revm::{context_interface::cfg::GasId, handler::Handler};
4841
4842        // Deterministic test addresses
4843        const TEST_TARGET: Address = Address::new([0xAA; 20]);
4844        const TEST_NONCE_KEY: U256 = U256::from_limbs([42, 0, 0, 0]);
4845        const SPEC: TempoHardfork = TempoHardfork::T1;
4846        const NEW_NONCE_KEY_GAS: u64 = SPEC.gas_new_nonce_key();
4847        const EXISTING_NONCE_KEY_GAS: u64 = SPEC.gas_existing_nonce_key();
4848
4849        // Create T1 config with TIP-1000 gas params
4850        let mut cfg = CfgEnv::<TempoHardfork>::default();
4851        cfg.spec = SPEC;
4852        cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
4853
4854        // Get the expected new_account_cost dynamically from gas params
4855        let new_account_cost = cfg.gas_params.get(GasId::new_account_cost());
4856        assert_eq!(
4857            new_account_cost, 250_000,
4858            "T1 gas params should have 250k new_account_cost"
4859        );
4860
4861        // Helper to create EVM context for testing
4862        let make_evm = |cfg: CfgEnv<TempoHardfork>, nonce: u64, nonce_key: U256| {
4863            let journal = Journal::new(CacheDB::new(EmptyDB::default()));
4864            let ctx = Context::mainnet()
4865                .with_db(CacheDB::new(EmptyDB::default()))
4866                .with_block(TempoBlockEnv::default())
4867                .with_cfg(cfg)
4868                .with_tx(TempoTxEnv {
4869                    inner: revm::context::TxEnv {
4870                        gas_limit: 1_000_000,
4871                        nonce,
4872                        ..Default::default()
4873                    },
4874                    tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4875                        aa_calls: vec![Call {
4876                            to: TxKind::Call(TEST_TARGET),
4877                            value: U256::ZERO,
4878                            input: Bytes::new(),
4879                        }],
4880                        nonce_key,
4881                        ..Default::default()
4882                    })),
4883                    ..Default::default()
4884                })
4885                .with_new_journal(journal);
4886            TempoEvm::<_, ()>::new(ctx, ())
4887        };
4888
4889        // Case 1: nonce == 0 with 2D nonce key -> should include new_account_cost
4890        let mut evm_nonce_zero = make_evm(cfg.clone(), 0, TEST_NONCE_KEY);
4891        let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4892        let gas_nonce_zero = handler
4893            .validate_initial_tx_gas(&mut evm_nonce_zero)
4894            .unwrap();
4895
4896        // Case 2: nonce > 0 with same 2D nonce key -> should charge EXISTING_NONCE_KEY_GAS (5k)
4897        // This tests that existing 2D nonce keys are charged 5k gas per TIP-1000 Invariant 3
4898        let mut evm_nonce_five = make_evm(cfg.clone(), 5, TEST_NONCE_KEY);
4899        let gas_nonce_five = handler
4900            .validate_initial_tx_gas(&mut evm_nonce_five)
4901            .unwrap();
4902
4903        // Delta-based assertion: the difference should be new_account_cost - EXISTING_NONCE_KEY_GAS
4904        // nonce=0 charges 250k (new account), nonce>0 charges 5k (existing key update)
4905        let gas_delta = gas_nonce_zero.initial_total_gas() - gas_nonce_five.initial_total_gas();
4906        let expected_delta = new_account_cost - EXISTING_NONCE_KEY_GAS;
4907        assert_eq!(
4908            gas_delta, expected_delta,
4909            "T1 gas difference between nonce=0 and nonce>0 should be {expected_delta} (new_account_cost - EXISTING_NONCE_KEY_GAS), got {gas_delta}"
4910        );
4911
4912        // Verify it's NOT using the pre-T1 NEW_NONCE_KEY_GAS (22,100)
4913        assert_ne!(
4914            gas_delta, NEW_NONCE_KEY_GAS,
4915            "T1 should NOT use pre-T1 NEW_NONCE_KEY_GAS ({NEW_NONCE_KEY_GAS}) for nonce=0 transactions"
4916        );
4917
4918        // Case 3: nonce == 0 with regular nonce (nonce_key=0) -> same +250k charge
4919        let mut evm_regular_nonce = make_evm(cfg, 0, U256::ZERO);
4920        let gas_regular = handler
4921            .validate_initial_tx_gas(&mut evm_regular_nonce)
4922            .unwrap();
4923
4924        assert_eq!(
4925            gas_nonce_zero.initial_total_gas(),
4926            gas_regular.initial_total_gas(),
4927            "nonce=0 should charge the same regardless of nonce_key (2D vs regular)"
4928        );
4929    }
4930
4931    /// Test that T1 hardfork correctly charges 5k gas for existing 2D nonce keys (nonce > 0).
4932    ///
4933    /// This test validates [TIP-1000] Invariant 3:
4934    /// "SSTORE operations that modify existing non-zero state (non-zero to non-zero)
4935    /// MUST continue to charge 5,000 gas"
4936    ///
4937    /// When using an existing 2D nonce key (nonce_key != 0 && nonce > 0), the nonce value
4938    /// transitions from N to N+1 (non-zero to non-zero), which must charge EXISTING_NONCE_KEY_GAS.
4939    ///
4940    /// [TIP-1000]: <https://docs.tempo.xyz/protocol/tips/tip-1000>
4941    #[test]
4942    fn test_t1_existing_2d_nonce_key_charges_5k_gas() {
4943        use crate::gas_params::tempo_gas_params;
4944        use revm::handler::Handler;
4945
4946        const BASE_INTRINSIC_GAS: u64 = 21_000;
4947        const TEST_TARGET: Address = Address::new([0xBB; 20]);
4948        const TEST_NONCE_KEY: U256 = U256::from_limbs([99, 0, 0, 0]);
4949        const SPEC: TempoHardfork = TempoHardfork::T1;
4950        const EXISTING_NONCE_KEY_GAS: u64 = SPEC.gas_existing_nonce_key();
4951
4952        let mut cfg = CfgEnv::<TempoHardfork>::default();
4953        cfg.spec = SPEC;
4954        cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
4955
4956        let make_evm = |cfg: CfgEnv<TempoHardfork>, nonce: u64, nonce_key: U256| {
4957            let journal = Journal::new(CacheDB::new(EmptyDB::default()));
4958            let ctx = Context::mainnet()
4959                .with_db(CacheDB::new(EmptyDB::default()))
4960                .with_block(TempoBlockEnv::default())
4961                .with_cfg(cfg)
4962                .with_tx(TempoTxEnv {
4963                    inner: revm::context::TxEnv {
4964                        gas_limit: 1_000_000,
4965                        nonce,
4966                        ..Default::default()
4967                    },
4968                    tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4969                        aa_calls: vec![Call {
4970                            to: TxKind::Call(TEST_TARGET),
4971                            value: U256::ZERO,
4972                            input: Bytes::new(),
4973                        }],
4974                        nonce_key,
4975                        ..Default::default()
4976                    })),
4977                    ..Default::default()
4978                })
4979                .with_new_journal(journal);
4980            TempoEvm::<_, ()>::new(ctx, ())
4981        };
4982
4983        let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4984
4985        // Case 1: Existing 2D nonce key (nonce > 0) should charge EXISTING_NONCE_KEY_GAS
4986        let mut evm_existing_key = make_evm(cfg.clone(), 5, TEST_NONCE_KEY);
4987        let gas_existing = handler
4988            .validate_initial_tx_gas(&mut evm_existing_key)
4989            .unwrap();
4990
4991        assert_eq!(
4992            gas_existing.initial_total_gas(),
4993            BASE_INTRINSIC_GAS + EXISTING_NONCE_KEY_GAS,
4994            "T1 existing 2D nonce key (nonce>0) should charge BASE + EXISTING_NONCE_KEY_GAS ({EXISTING_NONCE_KEY_GAS})"
4995        );
4996
4997        // Case 2: Regular nonce (nonce_key = 0) with nonce > 0 should NOT charge extra gas
4998        let mut evm_regular = make_evm(cfg, 5, U256::ZERO);
4999        let gas_regular = handler.validate_initial_tx_gas(&mut evm_regular).unwrap();
5000
5001        assert_eq!(
5002            gas_regular.initial_total_gas(),
5003            BASE_INTRINSIC_GAS,
5004            "T1 regular nonce (nonce_key=0, nonce>0) should only charge BASE intrinsic gas"
5005        );
5006
5007        // Verify the delta between 2D and regular nonce is exactly EXISTING_NONCE_KEY_GAS
5008        let gas_delta = gas_existing.initial_total_gas() - gas_regular.initial_total_gas();
5009        assert_eq!(
5010            gas_delta, EXISTING_NONCE_KEY_GAS,
5011            "Difference between existing 2D nonce and regular nonce should be EXISTING_NONCE_KEY_GAS ({EXISTING_NONCE_KEY_GAS})"
5012        );
5013    }
5014
5015    mod keychain {
5016        use super::*;
5017        use alloy_signer::SignerSync;
5018        use alloy_signer_local::PrivateKeySigner;
5019        use tempo_precompiles::ACCOUNT_KEYCHAIN_ADDRESS;
5020        use tempo_primitives::transaction::{
5021            KeychainSignature, KeychainVersion, SignatureType,
5022            key_authorization::{KeyAuthorization, TokenLimit as PrimTokenLimit},
5023        };
5024
5025        fn generate_keypair() -> (PrivateKeySigner, Address) {
5026            let signer = PrivateKeySigner::random();
5027            let addr = signer.address();
5028            (signer, addr)
5029        }
5030
5031        fn sign_key_auth(
5032            signer: &PrivateKeySigner,
5033            key_auth: KeyAuthorization,
5034        ) -> tempo_primitives::transaction::SignedKeyAuthorization {
5035            let sig = signer
5036                .sign_hash_sync(&key_auth.signature_hash())
5037                .expect("signing failed");
5038            key_auth.into_signed(PrimitiveSignature::Secp256k1(sig))
5039        }
5040
5041        fn test_sig() -> PrimitiveSignature {
5042            PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature())
5043        }
5044
5045        /// Build EVM + handler with a keychain-signature AA tx.
5046        ///
5047        /// - `signature`: outer keychain signature; when `None` a default V2
5048        ///   keychain sig for `user` is used.
5049        /// - `seed_key`: when `true` the access key is pre-authorized in
5050        ///   keychain storage (existing-key path).
5051        fn make_evm(
5052            user: Address,
5053            access_key: Address,
5054            key_auth: Option<tempo_primitives::transaction::SignedKeyAuthorization>,
5055            spec: TempoHardfork,
5056            signature: Option<TempoSignature>,
5057            seed_key: bool,
5058        ) -> (
5059            TempoEvm<CacheDB<EmptyDB>, ()>,
5060            TempoEvmHandler<CacheDB<EmptyDB>, ()>,
5061        ) {
5062            let sig = signature.unwrap_or_else(|| {
5063                TempoSignature::Keychain(KeychainSignature::new(user, test_sig()))
5064            });
5065            let mut cfg = CfgEnv::<TempoHardfork>::default();
5066            cfg.spec = spec;
5067
5068            let tx = TempoTxEnv {
5069                inner: revm::context::TxEnv {
5070                    caller: user,
5071                    gas_limit: 1_000_000,
5072                    kind: TxKind::Call(Address::ZERO),
5073                    ..Default::default()
5074                },
5075                fee_token: Some(DEFAULT_FEE_TOKEN),
5076                tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
5077                    signature: sig,
5078                    aa_calls: vec![Call {
5079                        to: TxKind::Call(Address::ZERO),
5080                        value: U256::ZERO,
5081                        input: Bytes::new(),
5082                    }],
5083                    key_authorization: key_auth,
5084                    signature_hash: B256::ZERO,
5085                    override_key_id: Some(access_key),
5086                    ..Default::default()
5087                })),
5088                ..Default::default()
5089            };
5090
5091            let ctx = Context::mainnet()
5092                .with_db(CacheDB::new(EmptyDB::default()))
5093                .with_block(TempoBlockEnv::default())
5094                .with_cfg(cfg)
5095                .with_tx(tx)
5096                .with_new_journal(create_test_journal());
5097
5098            let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
5099
5100            StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5101                let mut kc = AccountKeychain::new();
5102                kc.initialize().unwrap();
5103                kc.set_transaction_key(Address::ZERO).unwrap();
5104                kc.set_tx_origin(user).unwrap();
5105                if seed_key {
5106                    kc.authorize_key(
5107                        user,
5108                        access_key,
5109                        PrecompileSignatureType::Secp256k1,
5110                        KeyRestrictions {
5111                            expiry: u64::MAX,
5112                            enforceLimits: false,
5113                            limits: vec![],
5114                            allowAnyCalls: true,
5115                            allowedCalls: vec![],
5116                        },
5117                        None,
5118                    )
5119                    .unwrap();
5120                }
5121            });
5122
5123            (evm, TempoEvmHandler::new())
5124        }
5125
5126        #[test]
5127        fn test_key_authorization_invalid_signature_rejected() {
5128            let (_, user) = generate_keypair();
5129            let key = Address::random();
5130            let (bad_signer, _) = generate_keypair();
5131
5132            let signed = sign_key_auth(
5133                &bad_signer,
5134                KeyAuthorization::unrestricted(1337, SignatureType::Secp256k1, key),
5135            );
5136            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T2, None, true);
5137
5138            assert!(matches!(
5139                h.validate_env(&mut evm),
5140                Err(EVMError::Transaction(
5141                    TempoInvalidTransaction::KeyAuthorizationNotSignedByRoot { .. }
5142                ))
5143            ));
5144        }
5145
5146        #[test]
5147        fn test_key_authorization_mismatched_key_id_rejected() {
5148            let (signer, user) = generate_keypair();
5149            let wrong_key = Address::random();
5150            let tx_key = Address::random();
5151
5152            let signed = sign_key_auth(
5153                &signer,
5154                KeyAuthorization::unrestricted(1337, SignatureType::Secp256k1, wrong_key),
5155            );
5156            let (mut evm, h) = make_evm(user, tx_key, Some(signed), TempoHardfork::T2, None, true);
5157
5158            assert!(matches!(
5159                h.validate_env(&mut evm),
5160                Err(EVMError::Transaction(
5161                    TempoInvalidTransaction::AccessKeyCannotAuthorizeOtherKeys
5162                ))
5163            ));
5164        }
5165
5166        #[test]
5167        fn test_key_authorization_chain_id_wildcard() {
5168            for spec in [TempoHardfork::T1B, TempoHardfork::T2] {
5169                let (signer, user) = generate_keypair();
5170                let key = Address::random();
5171                let signed = sign_key_auth(
5172                    &signer,
5173                    KeyAuthorization::unrestricted(0, SignatureType::Secp256k1, key),
5174                );
5175                let (mut evm, h) = make_evm(user, key, Some(signed), spec, None, false);
5176
5177                if !spec.is_t1c()
5178                    && let Some(aa_env) = evm.tx.tempo_tx_env.as_mut()
5179                    && let TempoSignature::Keychain(keychain_sig) = &mut aa_env.signature
5180                {
5181                    // Overwrite the signature version pre-T1C to bypass the version check.
5182                    keychain_sig.version = KeychainVersion::V1;
5183                }
5184                let result = h.validate_env(&mut evm);
5185                if !spec.is_t1c() {
5186                    assert!(
5187                        result.is_ok(),
5188                        "{spec:?}: chain_id=0 wildcard should be accepted pre-T1C, got: {result:?}"
5189                    );
5190                } else {
5191                    assert!(
5192                        result.is_err(),
5193                        "{spec:?}: chain_id=0 wildcard should be rejected post-T1C, got: {result:?}"
5194                    );
5195                }
5196            }
5197        }
5198
5199        #[test]
5200        fn test_key_authorization_chain_id_wrong_and_matching() {
5201            // Both pre-T1C and post-T1C: wrong chain_id rejected, matching accepted.
5202            for spec in [TempoHardfork::T1B, TempoHardfork::T2] {
5203                // Wrong chain_id → rejected
5204                let (signer, user) = generate_keypair();
5205                let key = Address::random();
5206                let signed = sign_key_auth(
5207                    &signer,
5208                    KeyAuthorization::unrestricted(99999, SignatureType::Secp256k1, key),
5209                );
5210                let (mut evm, h) = make_evm(user, key, Some(signed), spec, None, true);
5211                assert!(
5212                    h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default())
5213                        .is_err(),
5214                    "{spec:?}: wrong chain_id should be rejected"
5215                );
5216
5217                // Matching chain_id (1 = default CfgEnv) → accepted
5218                let (signer, user) = generate_keypair();
5219                let key = Address::random();
5220                let signed = sign_key_auth(
5221                    &signer,
5222                    KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key),
5223                );
5224                let (mut evm, h) = make_evm(user, key, Some(signed), spec, None, true);
5225                let result =
5226                    h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5227                assert!(
5228                    !matches!(&result, Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason })) if reason.contains("chain_id")),
5229                    "{spec:?}: matching chain_id should be accepted, got: {result:?}"
5230                );
5231            }
5232        }
5233
5234        #[test]
5235        fn test_key_authorization_expiry_cached_for_pool_maintenance() {
5236            let (signer, user) = generate_keypair();
5237            let key = Address::random();
5238            let expiry = u64::MAX - 1;
5239
5240            let signed = sign_key_auth(
5241                &signer,
5242                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5243                    .with_expiry(expiry),
5244            );
5245            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T2, None, false);
5246
5247            let _ = h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5248            assert_eq!(evm.key_expiry, Some(expiry));
5249        }
5250
5251        #[test]
5252        fn test_key_authorization_witness_rejected_before_t5() {
5253            let (signer, user) = generate_keypair();
5254            let key = Address::random();
5255            let signed = sign_key_auth(
5256                &signer,
5257                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5258                    .with_witness(B256::repeat_byte(0x53)),
5259            );
5260            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T4, None, false);
5261
5262            let result = h.validate_env(&mut evm);
5263            assert!(
5264                matches!(
5265                    &result,
5266                    Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5267                        if reason.contains("before T5")
5268                ),
5269                "witness-bearing key authorization should be rejected before T5, got: {result:?}"
5270            );
5271        }
5272
5273        #[test]
5274        fn test_t5_key_authorization_witness_is_not_burned_in_state() {
5275            use tempo_precompiles::account_keychain::isKeyAuthorizationWitnessBurnedCall;
5276
5277            let (signer, user) = generate_keypair();
5278            let key = Address::random();
5279            let witness = B256::repeat_byte(0x54);
5280            let signed = sign_key_auth(
5281                &signer,
5282                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5283                    .with_witness(witness),
5284            );
5285            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T5, None, false);
5286
5287            let result =
5288                h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5289            assert!(
5290                result.is_ok(),
5291                "T5 witness authorization should pass: {result:?}"
5292            );
5293
5294            StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5295                let keychain = AccountKeychain::new();
5296                assert!(
5297                    !keychain
5298                        .is_key_authorization_witness_burned(isKeyAuthorizationWitnessBurnedCall {
5299                            account: user,
5300                            witness,
5301                        })
5302                        .expect("witness read succeeds"),
5303                    "T5 key authorization must not burn its witness"
5304                );
5305            });
5306        }
5307
5308        #[test]
5309        fn test_t6_admin_key_authorization_fields_rejected_before_t6() {
5310            let (signer, user) = generate_keypair();
5311            let key = Address::random();
5312            let signed = sign_key_auth(
5313                &signer,
5314                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key).into_admin(user),
5315            );
5316            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T5, None, false);
5317
5318            let result = h.validate_env(&mut evm);
5319            assert!(
5320                matches!(
5321                    &result,
5322                    Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5323                        if reason.contains("not active before T6")
5324                ),
5325                "admin key authorization fields should be rejected before T6, got: {result:?}"
5326            );
5327        }
5328
5329        #[test]
5330        fn test_t6_admin_key_authorization_rejects_account_mismatch() {
5331            let (signer, user) = generate_keypair();
5332            let key = Address::random();
5333            let wrong_account = Address::random();
5334            let signed = sign_key_auth(
5335                &signer,
5336                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5337                    .into_admin(wrong_account),
5338            );
5339            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T6, None, false);
5340
5341            let result = h.validate_env(&mut evm);
5342            assert!(
5343                matches!(
5344                    &result,
5345                    Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5346                        if reason.contains("account mismatch")
5347                ),
5348                "admin key authorization should be bound to tx.caller, got: {result:?}"
5349            );
5350        }
5351
5352        #[test]
5353        fn test_t6_root_admin_key_authorization_allows_omitted_account() {
5354            let (signer, user) = generate_keypair();
5355            let key = Address::random();
5356            let mut key_auth = KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key);
5357            key_auth.is_admin = true;
5358            assert_eq!(key_auth.account, None);
5359
5360            let signed = sign_key_auth(&signer, key_auth);
5361            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T6, None, false);
5362
5363            let env_result = h.validate_env(&mut evm);
5364            assert!(
5365                env_result.is_ok(),
5366                "root-signed admin key authorization should pass stateless validation, got: {env_result:?}"
5367            );
5368
5369            let result =
5370                h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5371            assert!(
5372                result.is_ok(),
5373                "root-signed admin key authorization should not require account, got: {result:?}"
5374            );
5375
5376            StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5377                let keychain = AccountKeychain::new();
5378                assert!(
5379                    keychain
5380                        .is_admin_key(user, key)
5381                        .expect("admin key status read succeeds"),
5382                    "root-signed admin key should be registered as admin"
5383                );
5384            });
5385        }
5386
5387        #[test]
5388        fn test_t6_root_signed_key_authorization_rejects_admin_keychain_submission() {
5389            let (root_signer, user) = generate_keypair();
5390            let (_, admin_key) = generate_keypair();
5391            let child_key = Address::random();
5392            let signed = sign_key_auth(
5393                &root_signer,
5394                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key),
5395            );
5396            let (mut evm, h) = make_evm(
5397                user,
5398                admin_key,
5399                Some(signed),
5400                TempoHardfork::T6,
5401                None,
5402                false,
5403            );
5404
5405            let env_result = h.validate_env(&mut evm);
5406            assert!(
5407                matches!(
5408                    &env_result,
5409                    Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5410                        if reason.contains("root transaction signature")
5411                ),
5412                "root-signed key authorization should require a root transaction signature, got: {env_result:?}"
5413            );
5414        }
5415
5416        #[test]
5417        fn test_t6_root_key_authorization_rejects_account_mismatch() {
5418            let (signer, user) = generate_keypair();
5419            let key = Address::random();
5420            let wrong_account = Address::random();
5421            let signed = sign_key_auth(
5422                &signer,
5423                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5424                    .with_account(wrong_account),
5425            );
5426            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T6, None, false);
5427
5428            let result = h.validate_env(&mut evm);
5429            assert!(
5430                matches!(
5431                    &result,
5432                    Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5433                        if reason.contains("key authorization account mismatch")
5434                ),
5435                "root-signed key authorization should be bound to tx.caller, got: {result:?}"
5436            );
5437        }
5438
5439        #[test]
5440        fn test_t6_admin_key_authorization_rejects_restrictions() {
5441            let (signer, user) = generate_keypair();
5442            let key = Address::random();
5443            let signed = sign_key_auth(
5444                &signer,
5445                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5446                    .with_expiry(u64::MAX)
5447                    .into_admin(user),
5448            );
5449            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T6, None, false);
5450
5451            let result = h.validate_env(&mut evm);
5452            assert!(
5453                matches!(
5454                    &result,
5455                    Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5456                        if reason.contains("cannot carry expiry")
5457                ),
5458                "admin key authorization should reject restrictions, got: {result:?}"
5459            );
5460        }
5461
5462        #[test]
5463        fn test_t6_admin_access_key_can_authorize_different_admin_key() {
5464            let (admin_signer, admin_key) = generate_keypair();
5465            let user = Address::random();
5466            let child_key = Address::random();
5467            let signed = sign_key_auth(
5468                &admin_signer,
5469                KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, child_key)
5470                    .into_admin(user),
5471            );
5472            let (mut evm, h) = make_evm(
5473                user,
5474                admin_key,
5475                Some(signed),
5476                TempoHardfork::T6,
5477                None,
5478                false,
5479            );
5480
5481            let env_result = h.validate_env(&mut evm);
5482            assert!(
5483                env_result.is_ok(),
5484                "admin access key authorization should pass stateless validation, got: {env_result:?}"
5485            );
5486
5487            StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5488                let mut keychain = AccountKeychain::new();
5489                keychain
5490                    .authorize_admin_key(user, admin_key, PrecompileSignatureType::Secp256k1, None)
5491                    .expect("root authorizes admin key");
5492            });
5493
5494            let result =
5495                h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5496            assert!(
5497                result.is_ok(),
5498                "admin access key should authorize a different admin key, got: {result:?}"
5499            );
5500
5501            StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5502                let keychain = AccountKeychain::new();
5503                assert!(
5504                    keychain
5505                        .is_admin_key(user, child_key)
5506                        .expect("admin key status read succeeds"),
5507                    "child key should be registered as admin"
5508                );
5509            });
5510        }
5511
5512        #[test]
5513        fn test_t6_admin_key_authorization_rejects_different_transaction_admin_key() {
5514            let (authorization_signer, authorization_admin_key) = generate_keypair();
5515            let (_, tx_admin_key) = generate_keypair();
5516            let user = Address::random();
5517            let child_key = Address::random();
5518            let signed = sign_key_auth(
5519                &authorization_signer,
5520                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key)
5521                    .with_account(user),
5522            );
5523            let (mut evm, h) = make_evm(
5524                user,
5525                tx_admin_key,
5526                Some(signed),
5527                TempoHardfork::T6,
5528                None,
5529                false,
5530            );
5531
5532            let result = h.validate_env(&mut evm);
5533            assert!(
5534                matches!(
5535                    &result,
5536                    Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5537                        if reason.contains("must be signed by transaction key")
5538                ),
5539                "admin-signed key authorization must use the transaction admin key; auth signer {authorization_admin_key}, tx signer {tx_admin_key}, got: {result:?}"
5540            );
5541        }
5542
5543        #[test]
5544        fn test_t6_admin_access_key_non_admin_authorization_requires_account_binding() {
5545            let (admin_signer, admin_key) = generate_keypair();
5546            let user = Address::random();
5547            let child_key = Address::random();
5548            let signed = sign_key_auth(
5549                &admin_signer,
5550                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key),
5551            );
5552            let (mut evm, h) = make_evm(
5553                user,
5554                admin_key,
5555                Some(signed),
5556                TempoHardfork::T6,
5557                None,
5558                false,
5559            );
5560
5561            let result = h.validate_env(&mut evm);
5562            assert!(
5563                matches!(
5564                    &result,
5565                    Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5566                        if reason.contains("admin-signed key authorization account mismatch")
5567                ),
5568                "admin-signed non-admin authorization without account binding should fail in validate_env, got: {result:?}"
5569            );
5570        }
5571
5572        #[test]
5573        fn test_t6_admin_key_authorization_rejects_admin_signature_type_mismatch() {
5574            let (admin_signer, admin_key) = generate_keypair();
5575            let user = Address::random();
5576            let child_key = Address::random();
5577            let signed = sign_key_auth(
5578                &admin_signer,
5579                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key)
5580                    .with_account(user),
5581            );
5582            let (mut evm, h) = make_evm(
5583                user,
5584                admin_key,
5585                Some(signed),
5586                TempoHardfork::T6,
5587                None,
5588                false,
5589            );
5590
5591            let env_result = h.validate_env(&mut evm);
5592            assert!(
5593                env_result.is_ok(),
5594                "admin-signed key authorization should pass stateless validation, got: {env_result:?}"
5595            );
5596
5597            StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5598                let mut keychain = AccountKeychain::new();
5599                keychain
5600                    .authorize_admin_key(user, admin_key, PrecompileSignatureType::WebAuthn, None)
5601                    .expect("root authorizes WebAuthn admin key");
5602            });
5603
5604            let result =
5605                h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5606            assert!(
5607                matches!(
5608                    &result,
5609                    Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5610                        if reason.contains("SignatureTypeMismatch")
5611                ),
5612                "admin-signed key authorization should reject sidecar signature type mismatch, got: {result:?}"
5613            );
5614        }
5615
5616        #[test]
5617        fn test_t6_admin_access_key_non_admin_authorization_rejects_account_replay() {
5618            use tempo_precompiles::account_keychain::getKeyCall;
5619
5620            let (admin_signer, admin_key) = generate_keypair();
5621            let alice = Address::random();
5622            let bob = Address::random();
5623            let child_key = Address::random();
5624            let signed = sign_key_auth(
5625                &admin_signer,
5626                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key)
5627                    .with_account(alice),
5628            );
5629
5630            let (mut alice_evm, alice_handler) = make_evm(
5631                alice,
5632                admin_key,
5633                Some(signed.clone()),
5634                TempoHardfork::T6,
5635                None,
5636                false,
5637            );
5638            let alice_env_result = alice_handler.validate_env(&mut alice_evm);
5639            assert!(
5640                alice_env_result.is_ok(),
5641                "account-bound authorization should pass Alice stateless validation, got: {alice_env_result:?}"
5642            );
5643
5644            StorageCtx::enter_ctx(&mut alice_evm.inner.ctx, StorageActions::disabled(), || {
5645                let mut keychain = AccountKeychain::new();
5646                keychain
5647                    .authorize_admin_key(alice, admin_key, PrecompileSignatureType::Secp256k1, None)
5648                    .expect("root authorizes Alice admin key");
5649            });
5650
5651            let alice_result = alice_handler
5652                .validate_against_state_and_deduct_caller(&mut alice_evm, &mut Default::default());
5653            assert!(
5654                alice_result.is_ok(),
5655                "account-bound admin-signed non-admin authorization should pass for Alice, got: {alice_result:?}"
5656            );
5657            StorageCtx::enter_ctx(&mut alice_evm.inner.ctx, StorageActions::disabled(), || {
5658                let keychain = AccountKeychain::new();
5659                let key = keychain
5660                    .get_key(getKeyCall {
5661                        account: alice,
5662                        keyId: child_key,
5663                    })
5664                    .expect("child key read succeeds");
5665                assert_eq!(key.keyId, child_key, "child key should be registered");
5666                assert!(
5667                    !keychain
5668                        .is_admin_key(alice, child_key)
5669                        .expect("admin key status read succeeds"),
5670                    "child key should not be admin"
5671                );
5672            });
5673
5674            let (mut bob_evm, bob_handler) =
5675                make_evm(bob, admin_key, Some(signed), TempoHardfork::T6, None, false);
5676
5677            let bob_result = bob_handler.validate_env(&mut bob_evm);
5678            assert!(
5679                matches!(
5680                    &bob_result,
5681                    Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5682                        if reason.contains("key authorization account mismatch")
5683                ),
5684                "Alice-bound authorization should not replay for Bob, got: {bob_result:?}"
5685            );
5686        }
5687
5688        #[test]
5689        fn test_t6_admin_delegation_does_not_apply_child_fee_limit() {
5690            let (admin_signer, admin_key) = generate_keypair();
5691            let user = Address::random();
5692            let child_key = Address::random();
5693            let gas_limit = 100_000;
5694            let fee = U256::from(gas_limit);
5695            let child_spending_limit = fee - U256::ONE;
5696
5697            let signed = sign_key_auth(
5698                &admin_signer,
5699                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key)
5700                    .with_limits(vec![PrimTokenLimit {
5701                        token: DEFAULT_FEE_TOKEN,
5702                        limit: child_spending_limit,
5703                        period: 60,
5704                    }])
5705                    .with_account(user),
5706            );
5707            let (mut evm, h) = make_evm(
5708                user,
5709                admin_key,
5710                Some(signed),
5711                TempoHardfork::T6,
5712                None,
5713                false,
5714            );
5715            evm.inner.ctx.tx.inner.gas_limit = gas_limit;
5716            evm.inner.ctx.tx.inner.gas_price = 1_000_000_000_000;
5717            evm.inner.ctx.tx.inner.gas_priority_fee = Some(1_000_000_000_000);
5718
5719            let env_result = h.validate_env(&mut evm);
5720            assert!(
5721                env_result.is_ok(),
5722                "admin delegation should pass stateless validation, got: {env_result:?}"
5723            );
5724
5725            StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5726                TIP20Setup::path_usd(user)
5727                    .with_issuer(user)
5728                    .with_mint(user, fee * U256::from(2))
5729                    .apply()
5730                    .expect("pathUSD setup succeeds");
5731
5732                let mut keychain = AccountKeychain::new();
5733                keychain
5734                    .authorize_admin_key(user, admin_key, PrecompileSignatureType::Secp256k1, None)
5735                    .expect("root authorizes admin key");
5736            });
5737
5738            let result =
5739                h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5740            assert!(
5741                result.is_ok(),
5742                "admin delegation should not precharge fees against child key limits, got: {result:?}"
5743            );
5744        }
5745
5746        #[test]
5747        fn test_t6_admin_delegation_preserves_admin_transaction_key() {
5748            use tempo_precompiles::account_keychain::getTransactionKeyCall;
5749
5750            let (admin_signer, admin_key) = generate_keypair();
5751            let user = Address::random();
5752            let child_key = Address::random();
5753            let signed = sign_key_auth(
5754                &admin_signer,
5755                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key)
5756                    .with_account(user),
5757            );
5758            let (mut evm, h) = make_evm(
5759                user,
5760                admin_key,
5761                Some(signed),
5762                TempoHardfork::T6,
5763                None,
5764                false,
5765            );
5766
5767            let env_result = h.validate_env(&mut evm);
5768            assert!(
5769                env_result.is_ok(),
5770                "admin delegation should pass stateless validation, got: {env_result:?}"
5771            );
5772
5773            StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5774                let mut keychain = AccountKeychain::new();
5775                keychain
5776                    .authorize_admin_key(user, admin_key, PrecompileSignatureType::Secp256k1, None)
5777                    .expect("root authorizes admin key");
5778            });
5779
5780            let result =
5781                h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5782            assert!(
5783                result.is_ok(),
5784                "admin delegation should pass, got: {result:?}"
5785            );
5786
5787            StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5788                let keychain = AccountKeychain::new();
5789                let transaction_key = keychain
5790                    .get_transaction_key(getTransactionKeyCall {}, user)
5791                    .expect("transaction key read succeeds");
5792                assert_eq!(
5793                    transaction_key, admin_key,
5794                    "admin delegation must preserve the signer key as transaction key"
5795                );
5796            });
5797        }
5798
5799        #[test]
5800        fn test_keychain_signature_with_valid_authorized_key() {
5801            let (mut evm, h) = make_evm(
5802                Address::repeat_byte(0x11),
5803                Address::repeat_byte(0x22),
5804                None,
5805                TempoHardfork::T2,
5806                None,
5807                true,
5808            );
5809
5810            let result =
5811                h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5812            assert!(
5813                !matches!(
5814                    result,
5815                    Err(EVMError::Transaction(
5816                        TempoInvalidTransaction::KeychainValidationFailed { .. }
5817                    ))
5818                ),
5819                "Valid authorized key should pass, got: {result:?}"
5820            );
5821        }
5822
5823        #[test]
5824        fn test_keychain_version_rejection() {
5825            let caller = Address::random();
5826
5827            // V1 (legacy) rejected post-T1C
5828            let v1 = TempoSignature::Keychain(KeychainSignature::new_v1(caller, test_sig()));
5829            let (mut evm, h) = make_evm(
5830                caller,
5831                Address::ZERO,
5832                None,
5833                TempoHardfork::T2,
5834                Some(v1),
5835                false,
5836            );
5837            assert!(matches!(
5838                h.validate_env(&mut evm),
5839                Err(EVMError::Transaction(
5840                    TempoInvalidTransaction::LegacyKeychainSignature
5841                ))
5842            ));
5843
5844            // V2 rejected pre-T1C
5845            let v2 = TempoSignature::Keychain(KeychainSignature::new(caller, test_sig()));
5846            let (mut evm, h) = make_evm(
5847                caller,
5848                Address::ZERO,
5849                None,
5850                TempoHardfork::T1B,
5851                Some(v2),
5852                false,
5853            );
5854            assert!(matches!(
5855                h.validate_env(&mut evm),
5856                Err(EVMError::Transaction(
5857                    TempoInvalidTransaction::V2KeychainBeforeActivation
5858                ))
5859            ));
5860        }
5861
5862        #[test]
5863        fn test_key_authorization_without_existing_key_passes() {
5864            let (signer, user) = generate_keypair();
5865            let key = Address::random();
5866            let signed = sign_key_auth(
5867                &signer,
5868                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key),
5869            );
5870            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T2, None, false);
5871
5872            let result =
5873                h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5874            assert!(
5875                !matches!(
5876                    result,
5877                    Err(EVMError::Transaction(
5878                        TempoInvalidTransaction::KeychainValidationFailed { .. }
5879                            | TempoInvalidTransaction::AccessKeyCannotAuthorizeOtherKeys
5880                            | TempoInvalidTransaction::KeyAuthorizationNotSignedByRoot { .. }
5881                            | TempoInvalidTransaction::KeychainPrecompileError { .. }
5882                    ))
5883                ),
5884                "Same-tx auth+use should pass when key does not exist, got: {result:?}"
5885            );
5886        }
5887
5888        #[test]
5889        fn test_same_tx_key_authorization_rejects_fee_above_new_limit_before_auth() {
5890            let (signer, user) = generate_keypair();
5891            let key = Address::random();
5892            let gas_limit = 100_000;
5893            let fee = U256::from(gas_limit);
5894            let spending_limit = fee - U256::ONE;
5895
5896            let signed = sign_key_auth(
5897                &signer,
5898                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key).with_limits(vec![
5899                    PrimTokenLimit {
5900                        token: DEFAULT_FEE_TOKEN,
5901                        limit: spending_limit,
5902                        period: 60,
5903                    },
5904                ]),
5905            );
5906            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T3, None, false);
5907            evm.inner.ctx.tx.inner.gas_limit = gas_limit;
5908            evm.inner.ctx.tx.inner.gas_price = 1_000_000_000_000;
5909            evm.inner.ctx.tx.inner.gas_priority_fee = Some(1_000_000_000_000);
5910
5911            StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5912                TIP20Setup::path_usd(user)
5913                    .with_issuer(user)
5914                    .with_mint(user, fee * U256::from(2))
5915                    .apply()
5916                    .expect("pathUSD setup succeeds");
5917            });
5918
5919            let result =
5920                h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5921
5922            assert!(
5923                matches!(
5924                    &result,
5925                    Err(EVMError::Transaction(TempoInvalidTransaction::CollectFeePreTx(
5926                        FeePaymentError::Other(reason)
5927                    ))) if reason.contains("SpendingLimitExceeded")
5928                ),
5929                "same-tx auth+use should reject fee above the new key limit before auth, got: {result:?}"
5930            );
5931            assert_eq!(evm.collected_fee, U256::ZERO);
5932            assert!(
5933                evm.inner
5934                    .ctx
5935                    .journaled_state
5936                    .inner
5937                    .logs
5938                    .iter()
5939                    .all(|log| log.address != ACCOUNT_KEYCHAIN_ADDRESS),
5940                "fee-limit rejection must happen before key authorization emits events"
5941            );
5942        }
5943
5944        #[test]
5945        fn test_stale_collected_fee_not_charged_to_zero_fee_same_tx_auth_use() {
5946            let (signer, user) = generate_keypair();
5947            let key = Address::random();
5948            let stale_fee = U256::from(100_000);
5949            let spending_limit = stale_fee - U256::ONE;
5950
5951            let signed = sign_key_auth(
5952                &signer,
5953                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key).with_limits(vec![
5954                    PrimTokenLimit {
5955                        token: DEFAULT_FEE_TOKEN,
5956                        limit: spending_limit,
5957                        period: 60,
5958                    },
5959                ]),
5960            );
5961            let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T3, None, false);
5962            evm.collected_fee = stale_fee;
5963            evm.inner.ctx.tx.inner.gas_limit = 100_000;
5964            evm.inner.ctx.tx.inner.gas_price = 0;
5965            evm.inner.ctx.tx.inner.gas_priority_fee = Some(0);
5966
5967            StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5968                TIP20Setup::path_usd(user)
5969                    .with_issuer(user)
5970                    .with_mint(user, stale_fee * U256::from(2))
5971                    .apply()
5972                    .expect("pathUSD setup succeeds");
5973            });
5974
5975            h.validate_env(&mut evm)
5976                .expect("zero-fee same-tx auth/use env validation should pass");
5977            assert_eq!(evm.collected_fee, U256::ZERO);
5978
5979            let result =
5980                h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5981
5982            assert!(
5983                result.is_ok(),
5984                "zero-fee same-tx auth/use must not charge stale fee, got: {result:?}"
5985            );
5986            assert_eq!(evm.collected_fee, U256::ZERO);
5987        }
5988    }
5989
5990    /// TIP-1016: Standard CREATE tx should populate initial_state_gas with
5991    /// create_state_gas when state gas is enabled (T4+).
5992    /// Note: new_account_state_gas for the caller (nonce==0 with 2D nonce) is added
5993    /// later in validate_against_state_and_deduct_caller, not in upstream initial_tx_gas.
5994    #[test]
5995    fn test_state_gas_standard_create_tx_populates_initial_state_gas() {
5996        // TIP-1016 is opt-in via amsterdam_eip8037; manually enable for this test.
5997        let gas_params =
5998            crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
5999        let initcode = Bytes::from(vec![0x60, 0x80]);
6000
6001        let init_gas = gas_params.initial_tx_gas(
6002            &initcode, true, // is_create
6003            0, 0, 0,
6004        );
6005
6006        let expected_state_gas = gas_params.create_state_gas();
6007
6008        assert!(
6009            expected_state_gas > 0,
6010            "State gas constants should be non-zero"
6011        );
6012        assert_eq!(
6013            init_gas.initial_state_gas,
6014            expected_state_gas,
6015            "CREATE tx should have initial_state_gas = create_state_gas ({})",
6016            gas_params.create_state_gas()
6017        );
6018    }
6019
6020    /// TIP-1016: Standard CALL tx should have zero initial_state_gas.
6021    #[test]
6022    fn test_state_gas_standard_call_tx_zero_initial_state_gas() {
6023        let gas_params = tempo_gas_params(TempoHardfork::T4);
6024        let calldata = Bytes::from(vec![1, 2, 3]);
6025
6026        let init_gas = gas_params.initial_tx_gas(
6027            &calldata, false, // not create
6028            0, 0, 0,
6029        );
6030
6031        assert_eq!(
6032            init_gas.initial_state_gas, 0,
6033            "CALL tx should have zero initial_state_gas"
6034        );
6035    }
6036
6037    /// TIP-1016: AA CREATE tx should populate initial_state_gas.
6038    #[test]
6039    fn test_state_gas_aa_create_tx_populates_initial_state_gas() {
6040        let gas_params = tempo_gas_params(TempoHardfork::T4);
6041        let initcode = Bytes::from(vec![0x60, 0x80]);
6042
6043        let call = Call {
6044            to: TxKind::Create,
6045            value: U256::ZERO,
6046            input: initcode,
6047        };
6048
6049        let aa_env = TempoBatchCallEnv {
6050            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6051                alloy_primitives::Signature::test_signature(),
6052            )),
6053            aa_calls: vec![call],
6054            key_authorization: None,
6055            signature_hash: B256::ZERO,
6056            ..Default::default()
6057        };
6058
6059        let gas = calculate_aa_batch_intrinsic_gas(
6060            &aa_env,
6061            &gas_params,
6062            None::<std::iter::Empty<&AccessListItem>>,
6063            TempoHardfork::T4,
6064        )
6065        .unwrap();
6066
6067        let expected_state_gas = gas_params.create_state_gas();
6068
6069        assert_eq!(
6070            gas.initial_state_gas, expected_state_gas,
6071            "AA CREATE tx should have initial_state_gas = create_state_gas"
6072        );
6073    }
6074
6075    /// TIP-1016: AA CALL tx should have zero initial_state_gas.
6076    #[test]
6077    fn test_state_gas_aa_call_tx_zero_initial_state_gas() {
6078        let gas_params = tempo_gas_params(TempoHardfork::T4);
6079        let calldata = Bytes::from(vec![1, 2, 3]);
6080
6081        let call = Call {
6082            to: TxKind::Call(Address::random()),
6083            value: U256::ZERO,
6084            input: calldata,
6085        };
6086
6087        let aa_env = TempoBatchCallEnv {
6088            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6089                alloy_primitives::Signature::test_signature(),
6090            )),
6091            aa_calls: vec![call],
6092            key_authorization: None,
6093            signature_hash: B256::ZERO,
6094            ..Default::default()
6095        };
6096
6097        let gas = calculate_aa_batch_intrinsic_gas(
6098            &aa_env,
6099            &gas_params,
6100            None::<std::iter::Empty<&AccessListItem>>,
6101            TempoHardfork::T4,
6102        )
6103        .unwrap();
6104
6105        assert_eq!(
6106            gas.initial_state_gas, 0,
6107            "AA CALL tx should have zero initial_state_gas"
6108        );
6109    }
6110
6111    /// TIP-1016: validate_initial_tx_gas for standard CREATE tx should set
6112    /// initial_state_gas when T4 is active and state gas is enabled.
6113    #[test]
6114    fn test_state_gas_validate_initial_tx_gas_create_t4() {
6115        let initcode = Bytes::from(vec![0x60, 0x80]);
6116        let mut test = TestHandlerEvm::tx(TempoHardfork::T4, |tx_env| {
6117            tx_env.inner.gas_limit = 60_000_000;
6118            tx_env.inner.kind = TxKind::Create;
6119            tx_env.inner.data = initcode;
6120        });
6121        let init_gas = test.validate_initial_tx_gas();
6122
6123        // create_state_gas (from upstream initial_tx_gas for CREATE) +
6124        // new_account_state_gas (from Tempo's nonce==0 check for the caller)
6125        let expected_state_gas =
6126            test.gas_params().create_state_gas() + test.gas_params().new_account_state_gas();
6127
6128        assert_eq!(
6129            init_gas.initial_state_gas, expected_state_gas,
6130            "T4 CREATE tx with nonce==0 should have create_state_gas + new_account_state_gas"
6131        );
6132    }
6133
6134    /// TIP-1016: When enable_amsterdam_eip8037 is true, tx gas limit can exceed the cap
6135    /// (upstream revm validation skips the cap check).
6136    #[test]
6137    fn test_state_gas_tx_gas_limit_above_cap_allowed() {
6138        let calldata = Bytes::from(vec![1, 2, 3]);
6139
6140        let tx_env = TempoTxEnv {
6141            inner: revm::context::TxEnv {
6142                gas_limit: 60_000_000,
6143                kind: TxKind::Call(Address::random()),
6144                data: calldata,
6145                ..Default::default()
6146            },
6147            ..Default::default()
6148        };
6149
6150        // TIP-1016 is opt-in via amsterdam_eip8037; manually enable for this test.
6151        let mut test = TestHandlerEvm::with_cfg(TempoHardfork::T4, tx_env, |cfg| {
6152            cfg.tx_gas_limit_cap = Some(30_000_000);
6153            cfg.enable_amsterdam_eip8037 = true;
6154            cfg.gas_params =
6155                crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
6156        });
6157
6158        // validate_env should pass even though gas_limit > cap
6159        let result = test.validate_env();
6160        assert!(
6161            result.is_ok(),
6162            "With enable_amsterdam_eip8037=true, tx gas limit above cap should be allowed, got: {:?}",
6163            result.err()
6164        );
6165    }
6166
6167    /// TIP-1016: When enable_amsterdam_eip8037 is false (pre-T4), tx gas limit above cap is rejected.
6168    #[test]
6169    fn test_state_gas_tx_gas_limit_above_cap_rejected_pre_t4() {
6170        let calldata = Bytes::from(vec![1, 2, 3]);
6171
6172        let tx_env = TempoTxEnv {
6173            inner: revm::context::TxEnv {
6174                gas_limit: 60_000_000, // Double the cap
6175                kind: TxKind::Call(Address::random()),
6176                data: calldata,
6177                ..Default::default()
6178            },
6179            ..Default::default()
6180        };
6181
6182        let mut test = TestHandlerEvm::with_cfg(TempoHardfork::T1, tx_env, |cfg| {
6183            cfg.tx_gas_limit_cap = Some(30_000_000);
6184        });
6185
6186        // validate_env should reject: gas_limit > cap with state gas disabled
6187        let result = test.validate_env();
6188        assert!(
6189            result.is_err(),
6190            "With enable_amsterdam_eip8037=false, tx gas limit above cap should be rejected"
6191        );
6192    }
6193
6194    /// TIP-1016 regression: subblock fee-payment halt must not exceed the gas cap.
6195    #[test]
6196    fn test_subblock_fee_payment_halt_clamps_to_gas_cap_t4() {
6197        const CAP: u64 = 30_000_000;
6198        const TX_GAS_LIMIT: u64 = 60_000_000;
6199
6200        let aa_env = TempoBatchCallEnv {
6201            subblock_transaction: true,
6202            ..Default::default()
6203        };
6204        let tx_env = TempoTxEnv {
6205            inner: revm::context::TxEnv {
6206                gas_limit: TX_GAS_LIMIT,
6207                kind: TxKind::Call(Address::random()),
6208                ..Default::default()
6209            },
6210            tempo_tx_env: Some(Box::new(aa_env)),
6211            ..Default::default()
6212        };
6213
6214        let mut test = TestHandlerEvm::with_cfg(TempoHardfork::T4, tx_env, |cfg| {
6215            cfg.tx_gas_limit_cap = Some(CAP);
6216            cfg.enable_amsterdam_eip8037 = true;
6217            cfg.gas_params =
6218                crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
6219        });
6220
6221        // Sanity: T4 must actually have the cap-skip enabled so tx_gas_limit > cap is legal.
6222        assert!(
6223            test.cfg().enable_amsterdam_eip8037,
6224            "T4 must enable enable_amsterdam_eip8037 for this regression to apply"
6225        );
6226
6227        let err = EVMError::Transaction(TempoInvalidTransaction::EthInvalidTransaction(
6228            InvalidTransaction::LackOfFundForMaxFee {
6229                fee: Box::new(U256::ZERO),
6230                balance: Box::new(U256::ZERO),
6231            },
6232        ));
6233
6234        let result = test
6235            .handler
6236            .catch_error(&mut test.evm, err)
6237            .expect("subblock fee-payment failure must be converted to a halt, not a hard error");
6238
6239        match result {
6240            ExecutionResult::Halt { reason, gas, .. } => {
6241                assert!(
6242                    matches!(reason, TempoHaltReason::SubblockTxFeePayment),
6243                    "expected SubblockTxFeePayment halt, got {reason:?}"
6244                );
6245                assert_eq!(
6246                    gas.total_gas_spent(),
6247                    CAP,
6248                    "regular gas charged on subblock fee-payment halt must be clamped to \
6249                     tx_gas_limit_cap (got {} for tx.gas_limit={} cap={})",
6250                    gas.total_gas_spent(),
6251                    TX_GAS_LIMIT,
6252                    CAP,
6253                );
6254                assert_eq!(
6255                    gas.state_gas_spent_final(),
6256                    0,
6257                    "halt reports zero state gas"
6258                );
6259            }
6260            other => panic!("expected ExecutionResult::Halt, got {other:?}"),
6261        }
6262    }
6263
6264    #[test]
6265    fn test_subblock_paused_fee_token_halts_as_fee_payment_failure() {
6266        let aa_env = TempoBatchCallEnv {
6267            subblock_transaction: true,
6268            ..Default::default()
6269        };
6270        let tx_env = TempoTxEnv {
6271            inner: revm::context::TxEnv {
6272                gas_limit: 100_000,
6273                kind: TxKind::Call(Address::random()),
6274                ..Default::default()
6275            },
6276            tempo_tx_env: Some(Box::new(aa_env)),
6277            ..Default::default()
6278        };
6279
6280        let mut test = TestHandlerEvm::with_cfg(TempoHardfork::T4, tx_env, |cfg| {
6281            cfg.tx_gas_limit_cap = Some(30_000_000);
6282            cfg.enable_amsterdam_eip8037 = true;
6283            cfg.gas_params =
6284                crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
6285        });
6286
6287        let err = EVMError::Transaction(TempoInvalidTransaction::FeeTokenPaused {
6288            address: PATH_USD_ADDRESS,
6289        });
6290
6291        let result = test
6292            .handler
6293            .catch_error(&mut test.evm, err)
6294            .expect("subblock paused fee-token failure must be converted to a halt");
6295
6296        match result {
6297            ExecutionResult::Halt { reason, gas, .. } => {
6298                assert!(
6299                    matches!(reason, TempoHaltReason::SubblockTxFeePayment),
6300                    "expected SubblockTxFeePayment halt, got {reason:?}"
6301                );
6302                assert_eq!(gas.total_gas_spent(), 100_000);
6303                assert_eq!(
6304                    gas.state_gas_spent_final(),
6305                    0,
6306                    "halt reports zero state gas"
6307                );
6308            }
6309            other => panic!("expected ExecutionResult::Halt, got {other:?}"),
6310        }
6311    }
6312
6313    /// TIP-1016: Pre-T4 behavior unchanged - initial_state_gas is still populated
6314    /// by upstream revm for CREATE txs (it's a property of gas_params, not gating).
6315    /// But enable_amsterdam_eip8037=false means the reservoir won't be used.
6316    #[test]
6317    fn test_state_gas_backward_compat_t1_no_state_gas_enabled() {
6318        let mut cfg = CfgEnv::<TempoHardfork>::default();
6319        cfg.spec = TempoHardfork::T1;
6320        cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
6321
6322        assert!(
6323            !cfg.enable_amsterdam_eip8037,
6324            "Pre-T4 should NOT have enable_amsterdam_eip8037"
6325        );
6326
6327        let calldata = Bytes::from(vec![1, 2, 3]);
6328
6329        let journal = Journal::new(CacheDB::new(EmptyDB::default()));
6330        let tx_env = TempoTxEnv {
6331            inner: revm::context::TxEnv {
6332                gas_limit: 1_000_000,
6333                kind: TxKind::Call(Address::random()),
6334                data: calldata,
6335                ..Default::default()
6336            },
6337            ..Default::default()
6338        };
6339
6340        let ctx = Context::mainnet()
6341            .with_db(CacheDB::new(EmptyDB::default()))
6342            .with_block(TempoBlockEnv::default())
6343            .with_cfg(cfg)
6344            .with_tx(tx_env)
6345            .with_new_journal(journal);
6346        let mut evm = TempoEvm::<_, ()>::new(ctx, ());
6347        let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
6348
6349        let init_gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
6350
6351        // CALL tx - no state gas in either case
6352        assert_eq!(init_gas.initial_state_gas, 0);
6353    }
6354
6355    /// TIP-1016: AA batch with multiple calls including CREATE should track
6356    /// state gas for the CREATE call only.
6357    #[test]
6358    fn test_state_gas_aa_mixed_batch_create_and_call() {
6359        let gas_params = tempo_gas_params(TempoHardfork::T4);
6360        let calldata = Bytes::from(vec![1, 2, 3]);
6361        let initcode = Bytes::from(vec![0x60, 0x80]);
6362
6363        let calls = vec![
6364            Call {
6365                to: TxKind::Call(Address::random()),
6366                value: U256::ZERO,
6367                input: calldata,
6368            },
6369            Call {
6370                to: TxKind::Create,
6371                value: U256::ZERO,
6372                input: initcode,
6373            },
6374        ];
6375
6376        let aa_env = TempoBatchCallEnv {
6377            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6378                alloy_primitives::Signature::test_signature(),
6379            )),
6380            aa_calls: calls,
6381            key_authorization: None,
6382            signature_hash: B256::ZERO,
6383            ..Default::default()
6384        };
6385
6386        let gas = calculate_aa_batch_intrinsic_gas(
6387            &aa_env,
6388            &gas_params,
6389            None::<std::iter::Empty<&AccessListItem>>,
6390            TempoHardfork::T4,
6391        )
6392        .unwrap();
6393
6394        // Only the CREATE call contributes state gas
6395        let expected_state_gas = gas_params.create_state_gas();
6396
6397        assert_eq!(
6398            gas.initial_state_gas, expected_state_gas,
6399            "Mixed batch should have state gas only from CREATE call"
6400        );
6401    }
6402
6403    /// TIP-1016: AA batch with multiple CREATE calls accumulates state gas.
6404    #[test]
6405    fn test_state_gas_aa_multiple_create_calls() {
6406        let gas_params = tempo_gas_params(TempoHardfork::T4);
6407        let initcode = Bytes::from(vec![0x60, 0x80]);
6408
6409        let calls = vec![
6410            Call {
6411                to: TxKind::Create,
6412                value: U256::ZERO,
6413                input: initcode.clone(),
6414            },
6415            Call {
6416                to: TxKind::Create,
6417                value: U256::ZERO,
6418                input: initcode,
6419            },
6420        ];
6421
6422        let aa_env = TempoBatchCallEnv {
6423            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6424                alloy_primitives::Signature::test_signature(),
6425            )),
6426            aa_calls: calls,
6427            key_authorization: None,
6428            signature_hash: B256::ZERO,
6429            ..Default::default()
6430        };
6431
6432        let gas = calculate_aa_batch_intrinsic_gas(
6433            &aa_env,
6434            &gas_params,
6435            None::<std::iter::Empty<&AccessListItem>>,
6436            TempoHardfork::T4,
6437        )
6438        .unwrap();
6439
6440        // Two CREATE calls should accumulate state gas
6441        let per_create_state_gas = gas_params.create_state_gas();
6442
6443        assert_eq!(
6444            gas.initial_state_gas,
6445            per_create_state_gas * 2,
6446            "Multiple CREATE calls should accumulate initial_state_gas"
6447        );
6448    }
6449
6450    /// TIP-1016: In multi-call execution, per-call init gas uses
6451    /// `InitialAndFloorGas::new(0, 0)` so state gas is only deducted once
6452    /// upfront via `calculate_aa_batch_intrinsic_gas`, not per call.
6453    #[test]
6454    fn test_state_gas_multi_call_per_call_init_has_zero_state_gas() {
6455        let zero_init_gas = InitialAndFloorGas::new(0, 0);
6456        assert_eq!(
6457            zero_init_gas.initial_state_gas, 0,
6458            "Per-call init gas in multi-call must have zero initial_state_gas; \
6459             state gas is deducted once upfront, not per call"
6460        );
6461    }
6462
6463    /// TIP-1016: Multi-call corrected gas (success path) must use flattened
6464    /// reconstruction (Gas::new_spent + erase_cost) to be robust under the
6465    /// EIP-8037 reservoir model, and must preserve accumulated state_gas_spent.
6466    #[test]
6467    fn test_state_gas_multi_call_corrected_gas_success_preserves_state_gas() {
6468        let gas_limit: u64 = 1_000_000;
6469        let total_gas_spent: u64 = 400_000;
6470        let accumulated_state_gas: i64 = 150_000;
6471        let accumulated_refund: i64 = 5_000;
6472
6473        // Simulate flattened gas reconstruction (same pattern as execute_multi_call_with)
6474        let mut corrected_gas = Gas::new_spent_with_reservoir(gas_limit, 0);
6475        corrected_gas.erase_cost(gas_limit - total_gas_spent);
6476        corrected_gas.set_refund(accumulated_refund);
6477        corrected_gas.set_state_gas_spent(accumulated_state_gas);
6478
6479        assert_eq!(
6480            corrected_gas.total_gas_spent(),
6481            total_gas_spent,
6482            "Flattened gas must have correct spent"
6483        );
6484        assert_eq!(
6485            corrected_gas.used(),
6486            total_gas_spent - accumulated_refund as u64,
6487            "Flattened gas must have correct used (spent - refunded)"
6488        );
6489        assert_eq!(
6490            corrected_gas.state_gas_spent(),
6491            accumulated_state_gas,
6492            "Corrected gas must preserve accumulated state_gas_spent"
6493        );
6494        assert_eq!(
6495            corrected_gas.reservoir(),
6496            0,
6497            "Flattened gas must have zero reservoir"
6498        );
6499    }
6500
6501    /// TIP-1016: AA auth list entries with nonce==0 should track state gas.
6502    #[test]
6503    fn test_state_gas_aa_auth_list_nonce_zero() {
6504        // TIP-1016 is opt-in via amsterdam_eip8037; manually enable for this test.
6505        let gas_params =
6506            crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
6507
6508        let aa_env = TempoBatchCallEnv {
6509            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6510                alloy_primitives::Signature::test_signature(),
6511            )),
6512            aa_calls: vec![Call {
6513                to: TxKind::Call(Address::random()),
6514                value: U256::ZERO,
6515                input: Bytes::from(vec![1, 2, 3]),
6516            }],
6517            tempo_authorization_list: vec![RecoveredTempoAuthorization::new(
6518                TempoSignedAuthorization::new_unchecked(
6519                    alloy_eips::eip7702::Authorization {
6520                        chain_id: U256::ONE,
6521                        address: Address::random(),
6522                        nonce: 0,
6523                    },
6524                    TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6525                        alloy_primitives::Signature::test_signature(),
6526                    )),
6527                ),
6528            )],
6529            ..Default::default()
6530        };
6531
6532        let gas = calculate_aa_batch_intrinsic_gas(
6533            &aa_env,
6534            &gas_params,
6535            None::<std::iter::Empty<&AccessListItem>>,
6536            TempoHardfork::T4,
6537        )
6538        .unwrap();
6539
6540        // State gas = per-auth state gas (225k) + nonce==0 account creation state gas (225k)
6541        // Use hard-coded expected values to catch missing gas_params overrides.
6542        assert_eq!(
6543            gas.initial_state_gas,
6544            225_000 + 225_000,
6545            "Auth list entry should track per-auth state gas (225k) + nonce==0 account creation state gas (225k)"
6546        );
6547    }
6548
6549    /// TIP-1016: AA nonce==0 new account should track state gas in T4.
6550    #[test]
6551    fn test_state_gas_aa_nonce_zero_new_account() {
6552        let aa_env = TempoBatchCallEnv {
6553            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6554                alloy_primitives::Signature::test_signature(),
6555            )),
6556            aa_calls: vec![Call {
6557                to: TxKind::Call(Address::random()),
6558                value: U256::ZERO,
6559                input: Bytes::from(vec![1, 2, 3]),
6560            }],
6561            nonce_key: U256::ONE,
6562            ..Default::default()
6563        };
6564
6565        let mut test = TestHandlerEvm::aa(TempoHardfork::T4, aa_env, |tx_env| {
6566            tx_env.inner.gas_limit = 60_000_000;
6567            tx_env.inner.nonce = 0;
6568        });
6569        let init_gas = test.validate_initial_tx_gas();
6570
6571        assert_eq!(
6572            init_gas.initial_state_gas,
6573            test.gas_params().new_account_state_gas(),
6574            "AA tx with nonce==0 should track new_account_state_gas in T4"
6575        );
6576    }
6577
6578    /// TIP-1016: Auth list state gas (GasId 254) must be zero on T1.
6579    #[test]
6580    fn test_state_gas_auth_list_zero_on_t1() {
6581        let gas_params = tempo_gas_params(TempoHardfork::T1);
6582        assert_eq!(
6583            gas_params.new_account_state_gas(),
6584            0,
6585            "Auth account creation state gas must be zero on T1"
6586        );
6587
6588        let aa_env = TempoBatchCallEnv {
6589            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6590                alloy_primitives::Signature::test_signature(),
6591            )),
6592            aa_calls: vec![Call {
6593                to: TxKind::Call(Address::random()),
6594                value: U256::ZERO,
6595                input: Bytes::from(vec![1, 2, 3]),
6596            }],
6597            tempo_authorization_list: vec![RecoveredTempoAuthorization::new(
6598                TempoSignedAuthorization::new_unchecked(
6599                    alloy_eips::eip7702::Authorization {
6600                        chain_id: U256::ONE,
6601                        address: Address::random(),
6602                        nonce: 0,
6603                    },
6604                    TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6605                        alloy_primitives::Signature::test_signature(),
6606                    )),
6607                ),
6608            )],
6609            ..Default::default()
6610        };
6611
6612        let gas = calculate_aa_batch_intrinsic_gas(
6613            &aa_env,
6614            &gas_params,
6615            None::<std::iter::Empty<&AccessListItem>>,
6616            TempoHardfork::T1,
6617        )
6618        .unwrap();
6619
6620        assert_eq!(
6621            gas.initial_state_gas, 0,
6622            "T1 auth list nonce==0 should have zero initial_state_gas"
6623        );
6624    }
6625
6626    /// TIP-1016: Standard tx with nonce==0 should track state gas on T4 only.
6627    #[test]
6628    fn test_state_gas_standard_tx_nonce_zero_t4() {
6629        let calldata = Bytes::from(vec![1, 2, 3]);
6630        let mut test = TestHandlerEvm::tx(TempoHardfork::T4, |tx_env| {
6631            tx_env.inner.gas_limit = 60_000_000;
6632            tx_env.inner.kind = TxKind::Call(Address::random());
6633            tx_env.inner.nonce = 0;
6634            tx_env.inner.data = calldata;
6635        });
6636        let init_gas = test.validate_initial_tx_gas();
6637
6638        assert_eq!(
6639            init_gas.initial_state_gas,
6640            test.gas_params().new_account_state_gas(),
6641            "T4 standard tx with nonce==0 should track new_account_state_gas"
6642        );
6643    }
6644
6645    /// TIP-1016: Standard tx with nonce==0 should NOT track state gas on T1.
6646    #[test]
6647    fn test_state_gas_standard_tx_nonce_zero_t1_no_state_gas() {
6648        let calldata = Bytes::from(vec![1, 2, 3]);
6649
6650        let mut test = TestHandlerEvm::tx(TempoHardfork::T1, |tx_env| {
6651            tx_env.inner.gas_limit = 60_000_000;
6652            tx_env.inner.kind = TxKind::Call(Address::random());
6653            tx_env.inner.nonce = 0;
6654            tx_env.inner.data = calldata;
6655        });
6656        let init_gas = test.validate_initial_tx_gas();
6657
6658        assert_eq!(
6659            init_gas.initial_state_gas, 0,
6660            "T1 standard tx with nonce==0 must NOT track state gas"
6661        );
6662    }
6663
6664    /// TIP-1016: `initial_total_gas >= initial_state_gas` invariant must hold for
6665    /// AA CREATE calls. Without this, `execute_multi_call_with()` computes
6666    /// `regular_initial_gas = initial_total_gas.saturating_sub(initial_state_gas)` as 0,
6667    /// giving the transaction its full gas_limit for free.
6668    #[test]
6669    fn test_state_gas_aa_create_total_gas_includes_state_gas() {
6670        let gas_params = tempo_gas_params(TempoHardfork::T4);
6671        let initcode = Bytes::from(vec![0x60, 0x80]);
6672
6673        let call = Call {
6674            to: TxKind::Create,
6675            value: U256::ZERO,
6676            input: initcode,
6677        };
6678
6679        let aa_env = TempoBatchCallEnv {
6680            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6681                alloy_primitives::Signature::test_signature(),
6682            )),
6683            aa_calls: vec![call],
6684            key_authorization: None,
6685            signature_hash: B256::ZERO,
6686            ..Default::default()
6687        };
6688
6689        let gas = calculate_aa_batch_intrinsic_gas(
6690            &aa_env,
6691            &gas_params,
6692            None::<std::iter::Empty<&AccessListItem>>,
6693            TempoHardfork::T4,
6694        )
6695        .unwrap();
6696
6697        assert!(
6698            gas.initial_total_gas() >= gas.initial_state_gas,
6699            "invariant violated: initial_total_gas ({}) < initial_state_gas ({})",
6700            gas.initial_total_gas(),
6701            gas.initial_state_gas,
6702        );
6703    }
6704
6705    /// TIP-1016: `initial_total_gas >= initial_state_gas` invariant must hold for
6706    /// AA auth list entries with nonce==0.
6707    #[test]
6708    fn test_state_gas_aa_auth_nonce_zero_total_gas_includes_state_gas() {
6709        let gas_params = tempo_gas_params(TempoHardfork::T4);
6710
6711        let aa_env = TempoBatchCallEnv {
6712            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6713                alloy_primitives::Signature::test_signature(),
6714            )),
6715            aa_calls: vec![Call {
6716                to: TxKind::Call(Address::random()),
6717                value: U256::ZERO,
6718                input: Bytes::from(vec![1, 2, 3]),
6719            }],
6720            tempo_authorization_list: vec![RecoveredTempoAuthorization::new(
6721                TempoSignedAuthorization::new_unchecked(
6722                    alloy_eips::eip7702::Authorization {
6723                        chain_id: U256::ONE,
6724                        address: Address::random(),
6725                        nonce: 0,
6726                    },
6727                    TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6728                        alloy_primitives::Signature::test_signature(),
6729                    )),
6730                ),
6731            )],
6732            ..Default::default()
6733        };
6734
6735        let gas = calculate_aa_batch_intrinsic_gas(
6736            &aa_env,
6737            &gas_params,
6738            None::<std::iter::Empty<&AccessListItem>>,
6739            TempoHardfork::T4,
6740        )
6741        .unwrap();
6742
6743        assert!(
6744            gas.initial_total_gas() >= gas.initial_state_gas,
6745            "invariant violated: initial_total_gas ({}) < initial_state_gas ({})",
6746            gas.initial_total_gas(),
6747            gas.initial_state_gas,
6748        );
6749    }
6750
6751    /// TIP-1016: CREATE state gas is charged upfront and must be spent even if a later AA step reverts.
6752    #[test]
6753    fn test_state_gas_failed_batch_preserves_upfront_create_intrinsic_gas() {
6754        let tx_gas_limit = 1_000_000u64;
6755        let (calls, call_results) = (
6756            vec![
6757                Call {
6758                    to: TxKind::Create,
6759                    value: U256::ZERO,
6760                    input: Bytes::from(vec![0x60, 0x80]),
6761                },
6762                Call {
6763                    to: TxKind::Call(Address::random()),
6764                    value: U256::ZERO,
6765                    input: Bytes::new(),
6766                },
6767            ],
6768            [
6769                (InstructionResult::Stop, 10_000u64),
6770                (InstructionResult::Revert, 7_000u64),
6771            ],
6772        );
6773
6774        let aa_env = make_aa_env(calls.clone());
6775        let mut test = TestHandlerEvm::aa(TempoHardfork::T4, aa_env, |tx_env| {
6776            tx_env.inner.caller = Address::random();
6777            tx_env.inner.gas_limit = tx_gas_limit;
6778            // Keep nonce != 0 so this isolates CREATE state gas from caller account-creation gas.
6779            tx_env.inner.nonce = 1;
6780        });
6781
6782        let init_gas = test.validate_initial_tx_gas();
6783        assert_eq!(
6784            init_gas.initial_state_gas,
6785            test.gas_params().create_state_gas(),
6786            "first-call CREATE should contribute create_state_gas to AA intrinsic gas"
6787        );
6788        let (gas_limit, reservoir) = test.evm.initial_gas_and_reservoir(&init_gas);
6789
6790        let mut call_idx = 0usize;
6791        let result = test
6792            .handler
6793            .execute_multi_call_with(
6794                &mut test.evm,
6795                gas_limit,
6796                reservoir,
6797                calls,
6798                |_handler, _evm, gas, _reservoir| {
6799                    // Feed the batch executor deterministic per-call outcomes without running real EVM code.
6800                    let (instruction_result, spent) = call_results[call_idx];
6801                    call_idx += 1;
6802
6803                    let mut gas = Gas::new(gas);
6804                    gas.set_spent(spent);
6805
6806                    Ok(FrameResult::Call(CallOutcome::new(
6807                        InterpreterResult::new(instruction_result, Bytes::new(), gas),
6808                        0..0,
6809                    )))
6810                },
6811            )
6812            .expect("execute_multi_call_with should return a failed frame result");
6813
6814        let expected_spent =
6815            init_gas.initial_total_gas() + call_results.iter().map(|(_, spent)| spent).sum::<u64>();
6816
6817        // Pays CREATE state gas + both call costs. CREATE is charged upfront via intrinsic gas, and NOT refunded.
6818        assert_eq!(result.instruction_result(), InstructionResult::Revert);
6819        assert_eq!(result.gas().total_gas_spent(), expected_spent);
6820        assert_eq!(result.gas().remaining(), tx_gas_limit - expected_spent);
6821        assert_eq!(result.gas().state_gas_spent(), 0);
6822        assert_eq!(result.gas().reservoir(), 0);
6823    }
6824}