tempo_revm/
handler.rs

1//! Tempo EVM Handler implementation.
2
3use std::{cmp::Ordering, fmt::Debug};
4
5use alloy_primitives::{Address, B256, U256, b256};
6use reth_evm::EvmError;
7use revm::{
8    Database,
9    context::{
10        Block, Cfg, ContextTr, JournalTr, LocalContextTr, Transaction,
11        result::{EVMError, ExecutionResult, InvalidTransaction},
12        transaction::{AccessListItem, AccessListItemTr},
13    },
14    handler::{
15        EvmTr, FrameResult, FrameTr, Handler, MainnetHandler,
16        pre_execution::{self, calculate_caller_fee},
17        validation,
18    },
19    inspector::{Inspector, InspectorHandler},
20    interpreter::{
21        Gas, InitialAndFloorGas,
22        gas::{
23            ACCESS_LIST_ADDRESS, ACCESS_LIST_STORAGE_KEY, CALLVALUE, COLD_ACCOUNT_ACCESS_COST,
24            CREATE, STANDARD_TOKEN_COST, calc_tx_floor_cost, get_tokens_in_calldata, initcode_cost,
25        },
26        interpreter::EthInterpreter,
27    },
28    primitives::eip7702,
29    state::Bytecode,
30};
31use tempo_contracts::{
32    DEFAULT_7702_DELEGATE_ADDRESS,
33    precompiles::{IAccountKeychain::SignatureType as PrecompileSignatureType, TIPFeeAMMError},
34};
35use tempo_precompiles::{
36    account_keychain::{AccountKeychain, TokenLimit, authorizeKeyCall},
37    error::TempoPrecompileError,
38    nonce::{INonce::getNonceCall, NonceManager},
39    storage::StorageCtx,
40    tip_fee_manager::TipFeeManager,
41    tip20::{self, ITIP20::InsufficientBalance, TIP20Error, TIP20Token},
42};
43use tempo_primitives::transaction::{
44    PrimitiveSignature, RecoveredTempoAuthorization, SignatureType, TempoSignature,
45    calc_gas_balance_spending,
46};
47
48use crate::{
49    TempoEvm, TempoInvalidTransaction,
50    common::TempoStateAccess,
51    error::{FeePaymentError, TempoHaltReason},
52    evm::TempoContext,
53};
54
55/// Additional gas for P256 signature verification
56/// P256 precompile cost (6900 from EIP-7951) + 1100 for 129 bytes extra signature size - ecrecover savings (3000)
57const P256_VERIFY_GAS: u64 = 5_000;
58
59/// Hashed account code of default 7702 delegate deployment
60const DEFAULT_7702_DELEGATE_CODE_HASH: B256 =
61    b256!("e7b3e4597bdbdd0cc4eb42f9b799b580f23068f54e472bb802cb71efb1570482");
62
63/// Calculates the gas cost for verifying a primitive signature.
64///
65/// Returns the additional gas required beyond the base transaction cost:
66/// - Secp256k1: 0 (already included in base 21k)
67/// - P256: 5000 gas
68/// - WebAuthn: 5000 gas + calldata cost for webauthn_data
69#[inline]
70fn primitive_signature_verification_gas(signature: &PrimitiveSignature) -> u64 {
71    match signature {
72        PrimitiveSignature::Secp256k1(_) => 0,
73        PrimitiveSignature::P256(_) => P256_VERIFY_GAS,
74        PrimitiveSignature::WebAuthn(webauthn_sig) => {
75            let tokens = get_tokens_in_calldata(&webauthn_sig.webauthn_data, true);
76            P256_VERIFY_GAS + tokens * STANDARD_TOKEN_COST
77        }
78    }
79}
80
81/// Calculates the gas cost for verifying an AA signature.
82///
83/// For Keychain signatures, unwraps to the inner primitive signature for gas calculation.
84/// Returns the additional gas required beyond the base transaction cost.
85#[inline]
86fn tempo_signature_verification_gas(signature: &TempoSignature) -> u64 {
87    match signature {
88        TempoSignature::Primitive(prim_sig) => primitive_signature_verification_gas(prim_sig),
89        TempoSignature::Keychain(keychain_sig) => {
90            // Keychain wraps a primitive signature - calculate gas for the inner signature
91            primitive_signature_verification_gas(&keychain_sig.signature)
92        }
93    }
94}
95
96/// Tempo EVM [`Handler`] implementation with Tempo specific modifications:
97///
98/// Fees are paid in fee tokens instead of account balance.
99#[derive(Debug)]
100pub struct TempoEvmHandler<DB, I> {
101    /// Fee token used for the transaction.
102    fee_token: Address,
103    /// Fee payer for the transaction.
104    fee_payer: Address,
105    /// Phantom data to avoid type inference issues.
106    _phantom: core::marker::PhantomData<(DB, I)>,
107}
108
109impl<DB, I> TempoEvmHandler<DB, I> {
110    /// Create a new [`TempoEvmHandler`] handler instance
111    pub fn new() -> Self {
112        Self {
113            fee_token: Address::default(),
114            fee_payer: Address::default(),
115            _phantom: core::marker::PhantomData,
116        }
117    }
118}
119
120impl<DB: alloy_evm::Database, I> TempoEvmHandler<DB, I> {
121    fn load_fee_fields(
122        &mut self,
123        evm: &mut TempoEvm<DB, I>,
124    ) -> Result<(), EVMError<DB::Error, TempoInvalidTransaction>> {
125        let ctx = evm.ctx_mut();
126
127        self.fee_payer = ctx.tx.fee_payer()?;
128        self.fee_token = ctx.journaled_state.get_fee_token(
129            &ctx.tx,
130            ctx.block.beneficiary,
131            self.fee_payer,
132            ctx.cfg.spec,
133        )?;
134
135        // Skip fee token validity check for cases when the transaction is free and is not a part of a subblock.
136        if (!ctx.tx.max_balance_spending()?.is_zero() || ctx.tx.is_subblock_transaction())
137            && !ctx
138                .journaled_state
139                .is_valid_fee_token(self.fee_token, ctx.cfg.spec)?
140        {
141            return Err(TempoInvalidTransaction::InvalidFeeToken(self.fee_token).into());
142        }
143
144        Ok(())
145    }
146}
147
148impl<DB, I> TempoEvmHandler<DB, I>
149where
150    DB: alloy_evm::Database,
151{
152    /// Generic single-call execution that works with both standard and inspector exec loops.
153    ///
154    /// This is the core implementation that both `execute_single_call` and inspector-aware
155    /// execution can use by providing the appropriate exec loop function.
156    fn execute_single_call_with<F>(
157        &mut self,
158        evm: &mut TempoEvm<DB, I>,
159        init_and_floor_gas: &InitialAndFloorGas,
160        mut run_loop: F,
161    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
162    where
163        F: FnMut(
164            &mut Self,
165            &mut TempoEvm<DB, I>,
166            <<TempoEvm<DB, I> as EvmTr>::Frame as FrameTr>::FrameInit,
167        ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
168    {
169        let gas_limit = evm.ctx().tx().gas_limit() - init_and_floor_gas.initial_gas;
170
171        // Create first frame action
172        let first_frame_input = self.first_frame_input(evm, gas_limit)?;
173
174        // Run execution loop (standard or inspector)
175        let mut frame_result = run_loop(self, evm, first_frame_input)?;
176
177        // Handle last frame result
178        self.last_frame_result(evm, &mut frame_result)?;
179
180        Ok(frame_result)
181    }
182
183    /// Executes a standard single-call transaction using the default handler logic.
184    ///
185    /// This calls the same helper methods used by the default Handler::execution() implementation.
186    fn execute_single_call(
187        &mut self,
188        evm: &mut TempoEvm<DB, I>,
189        init_and_floor_gas: &InitialAndFloorGas,
190    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
191        self.execute_single_call_with(evm, init_and_floor_gas, Self::run_exec_loop)
192    }
193
194    /// Generic multi-call execution that works with both standard and inspector exec loops.
195    ///
196    /// This is the core implementation for atomic batch execution that both `execute_multi_call`
197    /// and inspector-aware execution can use by providing the appropriate single-call function.
198    ///
199    /// Provides atomic batch execution for AA transactions with multiple calls:
200    /// 1. Creates a checkpoint before executing any calls
201    /// 2. Executes each call sequentially, updating gas tracking
202    /// 3. If ANY call fails, reverts ALL state changes atomically
203    /// 4. If all calls succeed, commits ALL state changes atomically
204    ///
205    /// The atomicity is guaranteed by the checkpoint/revert/commit mechanism:
206    /// - Each individual call creates its own internal checkpoint
207    /// - The outer checkpoint (created here) captures state before any calls execute
208    /// - Reverting the outer checkpoint undoes all nested changes
209    fn execute_multi_call_with<F>(
210        &mut self,
211        evm: &mut TempoEvm<DB, I>,
212        init_and_floor_gas: &InitialAndFloorGas,
213        calls: Vec<tempo_primitives::transaction::Call>,
214        mut execute_single: F,
215    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
216    where
217        F: FnMut(
218            &mut Self,
219            &mut TempoEvm<DB, I>,
220            &InitialAndFloorGas,
221        ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
222    {
223        // Create checkpoint for atomic execution - captures state before any calls
224        let checkpoint = evm.ctx().journal_mut().checkpoint();
225
226        let gas_limit = evm.ctx().tx().gas_limit();
227        let mut remaining_gas = gas_limit - init_and_floor_gas.initial_gas;
228        let mut accumulated_gas_refund = 0i64;
229
230        // Store original TxEnv values to restore after batch execution
231        let original_kind = evm.ctx().tx().kind();
232        let original_value = evm.ctx().tx().value();
233        let original_data = evm.ctx().tx().input().clone();
234
235        let mut final_result = None;
236
237        for call in calls.iter() {
238            // Update TxEnv to point to this specific call
239            {
240                let tx = &mut evm.ctx().tx;
241                tx.inner.kind = call.to;
242                tx.inner.value = call.value;
243                tx.inner.data = call.input.clone();
244                tx.inner.gas_limit = remaining_gas;
245            }
246
247            // Execute call with NO additional initial gas (already deducted upfront in validation)
248            let zero_init_gas = InitialAndFloorGas::new(0, 0);
249            let frame_result = execute_single(self, evm, &zero_init_gas);
250
251            // Restore original TxEnv immediately after execution, even if execution failed
252            {
253                let tx = &mut evm.ctx().tx;
254                tx.inner.kind = original_kind;
255                tx.inner.value = original_value;
256                tx.inner.data = original_data.clone();
257                tx.inner.gas_limit = gas_limit;
258            }
259
260            let mut frame_result = frame_result?;
261
262            // Check if call succeeded
263            let instruction_result = frame_result.instruction_result();
264            if !instruction_result.is_ok() {
265                // Revert checkpoint - rolls back ALL state changes from ALL calls
266                evm.ctx().journal_mut().checkpoint_revert(checkpoint);
267
268                // Include gas from all previous successful calls + failed call
269                let gas_used_by_failed_call = frame_result.gas().used();
270                let total_gas_used = (gas_limit - remaining_gas) + gas_used_by_failed_call;
271
272                // Create new Gas with correct limit, because Gas does not have a set_limit method
273                // (the frame_result has the limit from just the last call)
274                let mut corrected_gas = Gas::new(gas_limit);
275                if instruction_result.is_revert() {
276                    corrected_gas.set_spent(total_gas_used);
277                } else {
278                    corrected_gas.spend_all();
279                }
280                corrected_gas.set_refund(0); // No refunds when batch fails and all state is reverted
281                *frame_result.gas_mut() = corrected_gas;
282
283                return Ok(frame_result);
284            }
285
286            // Call succeeded - accumulate gas usage and refunds
287            let gas_used = frame_result.gas().used();
288            let gas_refunded = frame_result.gas().refunded();
289
290            accumulated_gas_refund = accumulated_gas_refund.saturating_add(gas_refunded);
291            // Subtract only execution gas (intrinsic gas already deducted upfront)
292            remaining_gas = remaining_gas.saturating_sub(gas_used);
293
294            final_result = Some(frame_result);
295        }
296
297        // All calls succeeded - commit checkpoint to finalize ALL state changes
298        evm.ctx().journal_mut().checkpoint_commit();
299
300        // Fix gas accounting for the entire batch
301        let mut result =
302            final_result.ok_or_else(|| EVMError::Custom("No calls executed".into()))?;
303
304        let total_gas_used = gas_limit - remaining_gas;
305
306        // Create new Gas with correct limit, because Gas does not have a set_limit method
307        // (the frame_result has the limit from just the last call)
308        let mut corrected_gas = Gas::new(gas_limit);
309        corrected_gas.set_spent(total_gas_used);
310        corrected_gas.set_refund(accumulated_gas_refund);
311        *result.gas_mut() = corrected_gas;
312
313        Ok(result)
314    }
315
316    /// Executes a multi-call AA transaction atomically.
317    fn execute_multi_call(
318        &mut self,
319        evm: &mut TempoEvm<DB, I>,
320        init_and_floor_gas: &InitialAndFloorGas,
321        calls: Vec<tempo_primitives::transaction::Call>,
322    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
323        self.execute_multi_call_with(evm, init_and_floor_gas, calls, Self::execute_single_call)
324    }
325
326    /// Executes a standard single-call transaction with inspector support.
327    ///
328    /// This is the inspector-aware version of execute_single_call that uses
329    /// inspect_run_exec_loop instead of run_exec_loop.
330    fn inspect_execute_single_call(
331        &mut self,
332        evm: &mut TempoEvm<DB, I>,
333        init_and_floor_gas: &InitialAndFloorGas,
334    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
335    where
336        I: Inspector<TempoContext<DB>, EthInterpreter>,
337    {
338        self.execute_single_call_with(evm, init_and_floor_gas, Self::inspect_run_exec_loop)
339    }
340
341    /// Executes a multi-call AA transaction atomically with inspector support.
342    ///
343    /// This is the inspector-aware version of execute_multi_call that uses
344    /// inspect_execute_single_call instead of execute_single_call.
345    fn inspect_execute_multi_call(
346        &mut self,
347        evm: &mut TempoEvm<DB, I>,
348        init_and_floor_gas: &InitialAndFloorGas,
349        calls: Vec<tempo_primitives::transaction::Call>,
350    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
351    where
352        I: Inspector<TempoContext<DB>, EthInterpreter>,
353    {
354        self.execute_multi_call_with(
355            evm,
356            init_and_floor_gas,
357            calls,
358            Self::inspect_execute_single_call,
359        )
360    }
361}
362
363impl<DB, I> Default for TempoEvmHandler<DB, I> {
364    fn default() -> Self {
365        Self::new()
366    }
367}
368
369impl<DB, I> Handler for TempoEvmHandler<DB, I>
370where
371    DB: alloy_evm::Database,
372{
373    type Evm = TempoEvm<DB, I>;
374    type Error = EVMError<DB::Error, TempoInvalidTransaction>;
375    type HaltReason = TempoHaltReason;
376
377    #[inline]
378    fn run(
379        &mut self,
380        evm: &mut Self::Evm,
381    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
382        self.load_fee_fields(evm)?;
383
384        // Standard handler flow - execution() handles single vs multi-call dispatch
385        match self.run_without_catch_error(evm) {
386            Ok(output) => Ok(output),
387            Err(err) => self.catch_error(evm, err),
388        }
389    }
390
391    /// Overridden execution method that handles AA vs standard transactions.
392    ///
393    /// Dispatches based on transaction type:
394    /// - AA transactions (type 0x5): Use batch execution path with calls field
395    /// - All other transactions: Use standard single-call execution
396    #[inline]
397    fn execution(
398        &mut self,
399        evm: &mut Self::Evm,
400        init_and_floor_gas: &InitialAndFloorGas,
401    ) -> Result<FrameResult, Self::Error> {
402        // Check if this is an AA transaction by checking for tempo_tx_env
403        if let Some(tempo_tx_env) = evm.ctx().tx().tempo_tx_env.as_ref() {
404            // AA transaction - use batch execution with calls field
405            let calls = tempo_tx_env.aa_calls.clone();
406            self.execute_multi_call(evm, init_and_floor_gas, calls)
407        } else {
408            // Standard transaction - use single-call execution
409            self.execute_single_call(evm, init_and_floor_gas)
410        }
411    }
412
413    /// Take logs from the Journal if outcome is Halt Or Revert.
414    #[inline]
415    fn execution_result(
416        &mut self,
417        evm: &mut Self::Evm,
418        result: <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
419    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
420        evm.logs.clear();
421        if !result.instruction_result().is_ok() {
422            evm.logs = evm.journal_mut().take_logs();
423        }
424
425        MainnetHandler::default()
426            .execution_result(evm, result)
427            .map(|result| result.map_haltreason(Into::into))
428    }
429
430    /// Override apply_eip7702_auth_list to support AA transactions with authorization lists.
431    ///
432    /// The default implementation only processes authorization lists for TransactionType::Eip7702 (0x04).
433    /// This override extends support to AA transactions (type 0x76) by checking for the presence
434    /// of an aa_authorization_list in the tempo_tx_env.
435    #[inline]
436    fn apply_eip7702_auth_list(&self, evm: &mut Self::Evm) -> Result<u64, Self::Error> {
437        let ctx = evm.ctx();
438
439        // Check if this is an AA transaction with an authorization list
440        let has_aa_auth_list = ctx
441            .tx()
442            .tempo_tx_env
443            .as_ref()
444            .map(|aa_env| !aa_env.tempo_authorization_list.is_empty())
445            .unwrap_or(false);
446
447        // If it's an AA transaction with authorization list, we need to apply it manually
448        // since the default implementation only checks for TransactionType::Eip7702
449        if has_aa_auth_list {
450            // TODO(@rakita) could we have a helper function for this logic in revm?
451            // For AA transactions, we need to apply the authorization list ourselves
452            // because pre_execution::apply_eip7702_auth_list returns early for non-0x04 tx types
453
454            let chain_id = ctx.cfg().chain_id();
455            let (tx, journal) = evm.ctx().tx_journal_mut();
456
457            let tempo_tx_env = tx.tempo_tx_env.as_ref().unwrap();
458            let mut refunded_accounts = 0;
459
460            for authorization in &tempo_tx_env.tempo_authorization_list {
461                let Some(authority) = authorization.authority() else {
462                    // invalid signature, we need to skip
463                    continue;
464                };
465
466                // 1. Verify the chain id is either 0 or the chain's current ID.
467                let auth_chain_id = authorization.chain_id;
468                if !auth_chain_id.is_zero() && auth_chain_id != U256::from(chain_id) {
469                    continue;
470                }
471
472                // 2. Verify the `nonce` is less than `2**64 - 1`.
473                if authorization.nonce == u64::MAX {
474                    continue;
475                }
476
477                // 3. Add `authority` to `accessed_addresses` (warm the account)
478                let mut authority_acc = journal.load_account_with_code_mut(authority)?;
479
480                // 4. Verify the code of `authority` is either empty or already delegated.
481                if let Some(bytecode) = &authority_acc.info.code {
482                    // if it is not empty and it is not eip7702
483                    if !bytecode.is_empty() && !bytecode.is_eip7702() {
484                        continue;
485                    }
486                }
487
488                // 5. Verify the nonce of `authority` is equal to `nonce`.
489                if authorization.nonce != authority_acc.info.nonce {
490                    continue;
491                }
492
493                // 6. Add gas refund if authority already exists
494                if !(authority_acc.is_empty()
495                    && authority_acc.is_loaded_as_not_existing_not_touched())
496                {
497                    refunded_accounts += 1;
498                }
499
500                // 7. Set the code of `authority` to be `0xef0100 || address`. This is a delegation designation.
501                //  * As a special case, if `address` is `0x0000000000000000000000000000000000000000` do not write the designation.
502                //    Clear the accounts code and reset the account's code hash to the empty hash `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`.
503                // 8. Increase the nonce of `authority` by one.
504                authority_acc.delegate(*authorization.address());
505            }
506
507            let refunded_gas =
508                refunded_accounts * (eip7702::PER_EMPTY_ACCOUNT_COST - eip7702::PER_AUTH_BASE_COST);
509            return Ok(refunded_gas);
510        }
511
512        // For standard EIP-7702 transactions, use the default implementation
513        pre_execution::apply_eip7702_auth_list(evm.ctx())
514    }
515
516    #[inline]
517    fn validate_against_state_and_deduct_caller(
518        &self,
519        evm: &mut Self::Evm,
520    ) -> Result<(), Self::Error> {
521        let (block, tx, cfg, journal, _, _) = evm.ctx().all_mut();
522
523        // Load the fee payer balance
524        let account_balance = get_token_balance(journal, self.fee_token, self.fee_payer)?;
525
526        // Load caller's account
527        let mut caller_account = journal.load_account_with_code_mut(tx.caller())?.data;
528
529        if caller_account.info.has_no_code_and_nonce() {
530            caller_account.set_code(
531                DEFAULT_7702_DELEGATE_CODE_HASH,
532                Bytecode::new_eip7702(DEFAULT_7702_DELEGATE_ADDRESS),
533            );
534        }
535
536        let nonce_key = tx
537            .tempo_tx_env
538            .as_ref()
539            .map(|aa| aa.nonce_key)
540            .unwrap_or_default();
541
542        // Validate account nonce and code (EIP-3607) using upstream helper
543        pre_execution::validate_account_nonce_and_code(
544            &caller_account.info,
545            tx.nonce(),
546            cfg.is_eip3607_disabled(),
547            // skip nonce check if 2D nonce is used
548            cfg.is_nonce_check_disabled() || !nonce_key.is_zero(),
549        )?;
550
551        // modify account nonce and touch the account.
552        caller_account.touch();
553
554        if !nonce_key.is_zero() {
555            StorageCtx::enter_evm(journal, block, cfg, || {
556                let mut nonce_manager = NonceManager::new();
557
558                if !cfg.is_nonce_check_disabled() {
559                    let tx_nonce = tx.nonce();
560                    let state = nonce_manager
561                        .get_nonce(getNonceCall {
562                            account: tx.caller(),
563                            nonceKey: nonce_key,
564                        })
565                        .map_err(|err| match err {
566                            TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
567                            err => {
568                                TempoInvalidTransaction::NonceManagerError(err.to_string()).into()
569                            }
570                        })?;
571
572                    match tx_nonce.cmp(&state) {
573                        Ordering::Greater => {
574                            return Err(TempoInvalidTransaction::EthInvalidTransaction(
575                                InvalidTransaction::NonceTooHigh {
576                                    tx: tx_nonce,
577                                    state,
578                                },
579                            )
580                            .into());
581                        }
582                        Ordering::Less => {
583                            return Err(TempoInvalidTransaction::EthInvalidTransaction(
584                                InvalidTransaction::NonceTooLow {
585                                    tx: tx_nonce,
586                                    state,
587                                },
588                            )
589                            .into());
590                        }
591                        _ => {}
592                    }
593                }
594
595                // Always increment nonce for AA transactions with non-zero nonce keys.
596                nonce_manager
597                    .increment_nonce(tx.caller(), nonce_key)
598                    .map_err(|err| match err {
599                        TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
600                        err => TempoInvalidTransaction::NonceManagerError(err.to_string()).into(),
601                    })?;
602
603                Result::<(), EVMError<DB::Error, TempoInvalidTransaction>>::Ok(())
604            })?;
605        } else {
606            // Bump the nonce for calls. Nonce for CREATE will be bumped in `make_create_frame`.
607            //
608            // Always bump nonce for AA transactions.
609            if tx.tempo_tx_env.is_some() || tx.kind().is_call() {
610                caller_account.bump_nonce();
611            }
612        }
613
614        // calculate the new balance after the fee is collected.
615        let new_balance = calculate_caller_fee(account_balance, tx, block, cfg)?;
616        // doing max to avoid underflow as new_balance can be more than
617        // account balance if `cfg.is_balance_check_disabled()` is true.
618        let gas_balance_spending = core::cmp::max(account_balance, new_balance) - new_balance;
619
620        // Note: Signature verification happens during recover_signer() before entering the pool
621        // Note: Transaction parameter validation (priority fee, time window) happens in validate_env()
622
623        // If the transaction includes a KeyAuthorization, validate and authorize the key
624        if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
625            && let Some(key_auth) = &tempo_tx_env.key_authorization
626        {
627            // Check if this TX is using a Keychain signature (access key)
628            // Access keys cannot authorize new keys UNLESS it's the same key being authorized (same-tx auth+use)
629            if let Some(keychain_sig) = tempo_tx_env.signature.as_keychain() {
630                // Get the access key address (recovered during Tx->TxEnv conversion and cached)
631                let access_key_addr =
632                    keychain_sig
633                        .key_id(&tempo_tx_env.signature_hash)
634                        .map_err(|_| {
635                            EVMError::Transaction(
636                            TempoInvalidTransaction::AccessKeyAuthorizationFailed {
637                                reason:
638                                    "Failed to recover access key address from Keychain signature"
639                                        .to_string(),
640                            },
641                        )
642                        })?;
643
644                // Only allow if authorizing the same key that's being used (same-tx auth+use)
645                if access_key_addr != key_auth.key_id {
646                    return Err(EVMError::Transaction(
647                            TempoInvalidTransaction::AccessKeyAuthorizationFailed {
648                                reason: "Access keys cannot authorize other keys. Only the root key can authorize new keys.".to_string(),
649                            },
650                        ));
651                }
652            }
653
654            // Validate that the KeyAuthorization is signed by the root account
655            let root_account = &tx.caller;
656
657            // Recover the signer of the KeyAuthorization
658            let auth_signer = key_auth.recover_signer().map_err(|_| {
659                EVMError::Transaction(TempoInvalidTransaction::AccessKeyAuthorizationFailed {
660                    reason: "Failed to recover signer from KeyAuthorization signature".to_string(),
661                })
662            })?;
663
664            // Verify the KeyAuthorization is signed by the root account
665            if auth_signer != *root_account {
666                return Err(EVMError::Transaction(
667                    TempoInvalidTransaction::AccessKeyAuthorizationFailed {
668                        reason: format!(
669                            "KeyAuthorization must be signed by root account {root_account}, but was signed by {auth_signer}",
670                        ),
671                    },
672                ));
673            }
674
675            // Validate KeyAuthorization chain_id (following EIP-7702 pattern)
676            // chain_id == 0 allows replay on any chain (wildcard)
677            let expected_chain_id = cfg.chain_id();
678            if key_auth.chain_id != 0 && key_auth.chain_id != expected_chain_id {
679                return Err(EVMError::Transaction(
680                    TempoInvalidTransaction::KeyAuthorizationChainIdMismatch {
681                        expected: expected_chain_id,
682                        got: key_auth.chain_id,
683                    },
684                ));
685            }
686
687            // Now authorize the key in the precompile
688            StorageCtx::enter_precompile(journal, block, cfg, |mut keychain: AccountKeychain| {
689                let access_key_addr = key_auth.key_id;
690
691                // Convert signature type to precompile SignatureType enum
692                // Use the key_type field which specifies the type of key being authorized
693                let signature_type = match key_auth.key_type {
694                    SignatureType::Secp256k1 => PrecompileSignatureType::Secp256k1,
695                    SignatureType::P256 => PrecompileSignatureType::P256,
696                    SignatureType::WebAuthn => PrecompileSignatureType::WebAuthn,
697                };
698
699                // Handle expiry: None means never expires (store as u64::MAX)
700                let expiry = key_auth.expiry.unwrap_or(u64::MAX);
701
702                // Validate expiry is not in the past
703                let current_timestamp = block.timestamp().saturating_to::<u64>();
704                if expiry <= current_timestamp {
705                    return Err(EVMError::Transaction(
706                        TempoInvalidTransaction::AccessKeyAuthorizationFailed {
707                            reason: format!(
708                                "Key expiry {expiry} is in the past (current timestamp: {current_timestamp})"
709                            ),
710                        },
711                    ));
712                }
713
714                // Handle limits: None means unlimited spending (enforce_limits=false)
715                // Some([]) means no spending allowed (enforce_limits=true)
716                // Some([...]) means specific limits (enforce_limits=true)
717                let enforce_limits = key_auth.limits.is_some();
718                let precompile_limits: Vec<TokenLimit> = key_auth
719                    .limits
720                    .as_ref()
721                    .map(|limits| {
722                        limits
723                            .iter()
724                            .map(|limit| TokenLimit {
725                                token: limit.token,
726                                amount: limit.limit,
727                            })
728                            .collect()
729                    })
730                    .unwrap_or_default();
731
732                // Create the authorize key call
733                let authorize_call = authorizeKeyCall {
734                    keyId: access_key_addr,
735                    signatureType: signature_type,
736                    expiry,
737                    enforceLimits: enforce_limits,
738                    limits: precompile_limits,
739                };
740
741                // Call precompile to authorize the key (same phase as nonce increment)
742                keychain
743                    .authorize_key(*root_account, authorize_call)
744                    .map_err(|err| match err {
745                        TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
746                        err => TempoInvalidTransaction::AccessKeyAuthorizationFailed {
747                            reason: err.to_string(),
748                        }
749                        .into(),
750                    })
751            })?;
752        }
753
754        // For Keychain signatures, validate that the keychain is authorized in the precompile
755        // UNLESS this transaction also includes a KeyAuthorization (same-tx auth+use case)
756        if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
757            && let Some(keychain_sig) = tempo_tx_env.signature.as_keychain()
758        {
759            // The user_address is the root account this transaction is being executed for
760            // This should match tx.caller (which comes from recover_signer on the outer signature)
761            let user_address = &keychain_sig.user_address;
762
763            // Sanity check: user_address should match tx.caller
764            if *user_address != tx.caller {
765                return Err(EVMError::Transaction(
766                    TempoInvalidTransaction::AccessKeyAuthorizationFailed {
767                        reason: format!(
768                            "Keychain user_address {} does not match transaction caller {}",
769                            user_address, tx.caller
770                        ),
771                    },
772                ));
773            }
774
775            // Get the access key address (recovered during pool validation and cached)
776            let access_key_addr =
777                keychain_sig
778                    .key_id(&tempo_tx_env.signature_hash)
779                    .map_err(|_| {
780                        EVMError::Transaction(
781                            TempoInvalidTransaction::AccessKeyAuthorizationFailed {
782                                reason: "Failed to recover access key address from inner signature"
783                                    .to_string(),
784                            },
785                        )
786                    })?;
787
788            // Check if this transaction includes a KeyAuthorization for the same key
789            // If so, skip validation here - the key was just validated and authorized
790            let is_authorizing_this_key = tempo_tx_env
791                .key_authorization
792                .as_ref()
793                .map(|key_auth| key_auth.key_id == access_key_addr)
794                .unwrap_or(false);
795
796            // Always need to set the transaction key for Keychain signatures
797            StorageCtx::enter_precompile(journal, block, cfg, |mut keychain: AccountKeychain| {
798                if !is_authorizing_this_key {
799                    // Not authorizing this key in the same transaction, so validate it exists now
800                    // Validate that user_address has authorized this access key in the keychain
801                    keychain
802                        .validate_keychain_authorization(
803                            *user_address,
804                            access_key_addr,
805                            block.timestamp().to::<u64>(),
806                        )
807                        .map_err(|e| {
808                            EVMError::Transaction(
809                                TempoInvalidTransaction::AccessKeyAuthorizationFailed {
810                                    reason: format!("Keychain validation failed: {e:?}"),
811                                },
812                            )
813                        })?;
814                }
815
816                // Set the transaction key in the keychain precompile
817                // This marks that the current transaction is using an access key
818                // The TIP20 precompile will read this during execution to enforce spending limits
819                keychain
820                    .set_transaction_key(access_key_addr)
821                    .map_err(|e| EVMError::Custom(e.to_string()))
822            })?;
823        }
824
825        if gas_balance_spending.is_zero() {
826            return Ok(());
827        }
828
829        let checkpoint = journal.checkpoint();
830
831        let result = StorageCtx::enter_evm(journal, &block, cfg, || {
832            TipFeeManager::new().collect_fee_pre_tx(
833                self.fee_payer,
834                self.fee_token,
835                gas_balance_spending,
836                block.beneficiary(),
837            )
838        });
839
840        if let Err(err) = result {
841            // Revert the journal to checkpoint before `collectFeePreTx` call if something went wrong.
842            journal.checkpoint_revert(checkpoint);
843
844            // Map fee collection errors to transaction validation errors since they
845            // indicate the transaction cannot be included (e.g., insufficient liquidity
846            // in FeeAMM pool for fee swaps)
847            Err(match err {
848                TempoPrecompileError::TIPFeeAMMError(TIPFeeAMMError::InsufficientLiquidity(_)) => {
849                    FeePaymentError::InsufficientAmmLiquidity {
850                        fee: gas_balance_spending,
851                    }
852                    .into()
853                }
854
855                TempoPrecompileError::TIP20(TIP20Error::InsufficientBalance(
856                    InsufficientBalance { available, .. },
857                )) => EVMError::Transaction(
858                    FeePaymentError::InsufficientFeeTokenBalance {
859                        fee: gas_balance_spending,
860                        balance: available,
861                    }
862                    .into(),
863                ),
864
865                TempoPrecompileError::Fatal(e) => EVMError::Custom(e),
866
867                _ => EVMError::Transaction(FeePaymentError::Other(err.to_string()).into()),
868            })
869        } else {
870            journal.checkpoint_commit();
871            evm.collected_fee = gas_balance_spending;
872
873            Ok(())
874        }
875    }
876
877    fn reimburse_caller(
878        &self,
879        evm: &mut Self::Evm,
880        exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
881    ) -> Result<(), Self::Error> {
882        // Call collectFeePostTx on TipFeeManager precompile
883        let context = &mut evm.inner.ctx;
884        let tx = context.tx();
885        let basefee = context.block().basefee() as u128;
886        let effective_gas_price = tx.effective_gas_price(basefee);
887        let gas = exec_result.gas();
888
889        // Calculate actual used and refund amounts
890        let actual_spending = calc_gas_balance_spending(gas.used(), effective_gas_price);
891        let refund_amount = tx.effective_balance_spending(
892            context.block.basefee.into(),
893            context.block.blob_gasprice().unwrap_or_default(),
894        )? - tx.value
895            - actual_spending;
896
897        // Skip `collectFeePostTx` call if the initial fee collected in
898        // `collectFeePreTx` was zero, but spending is non-zero.
899        //
900        // This is normally unreachable unless the gas price was increased mid-transaction,
901        // which is only possible when there are some EVM customizations involved (e.g Foundry EVM).
902        if context.cfg.disable_fee_charge
903            && evm.collected_fee.is_zero()
904            && !actual_spending.is_zero()
905        {
906            return Ok(());
907        }
908
909        // Create storage provider and fee manager
910        let (journal, block) = (&mut context.journaled_state, &context.block);
911        let beneficiary = block.beneficiary();
912
913        StorageCtx::enter_evm(&mut *journal, block, &context.cfg, || {
914            let mut fee_manager = TipFeeManager::new();
915
916            if !actual_spending.is_zero() || !refund_amount.is_zero() {
917                // Call collectFeePostTx (handles both refund and fee queuing)
918                fee_manager
919                    .collect_fee_post_tx(
920                        self.fee_payer,
921                        actual_spending,
922                        refund_amount,
923                        self.fee_token,
924                        beneficiary,
925                    )
926                    .map_err(|e| EVMError::Custom(format!("{e:?}")))?;
927            }
928
929            Ok(())
930        })
931    }
932
933    #[inline]
934    fn reward_beneficiary(
935        &self,
936        _evm: &mut Self::Evm,
937        _exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
938    ) -> Result<(), Self::Error> {
939        // All fee handling (refunds and queuing) is done in reimburse_caller via collectFeePostTx
940        // The actual swap and transfer to validator happens in executeBlock at the end of block processing
941        Ok(())
942    }
943
944    /// Validates transaction environment with custom handling for AA transactions.
945    ///
946    /// Performs standard validation plus AA-specific checks:
947    /// - Priority fee validation (EIP-1559)
948    /// - Time window validation (validAfter/validBefore)
949    #[inline]
950    fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
951        // All accounts have zero balance so transfer of value is not possible.
952        // Check added in https://github.com/tempoxyz/tempo/pull/759
953        if !evm.ctx.tx.value().is_zero() {
954            return Err(TempoInvalidTransaction::ValueTransferNotAllowed.into());
955        }
956
957        // First perform standard validation (header + transaction environment)
958        // This validates: prevrandao, excess_blob_gas, chain_id, gas limits, tx type support, etc.
959        validation::validate_env::<_, Self::Error>(evm.ctx())?;
960
961        // AA-specific validations
962        let cfg = evm.ctx_ref().cfg();
963        let tx = evm.ctx_ref().tx();
964
965        if let Some(aa_env) = tx.tempo_tx_env.as_ref() {
966            let has_keychain_fields =
967                aa_env.key_authorization.is_some() || aa_env.signature.is_keychain();
968
969            // Validate that keychain operations are only supported after Allegretto
970            if has_keychain_fields && !cfg.spec.is_allegretto() {
971                return Err(TempoInvalidTransaction::KeychainOpBeforeAllegretto.into());
972            }
973
974            if aa_env.subblock_transaction {
975                if !cfg.spec.is_allegretto() {
976                    if tx.max_fee_per_gas() > 0 {
977                        return Err(
978                            TempoInvalidTransaction::SubblockTransactionMustHaveZeroFee.into()
979                        );
980                    }
981                } else if has_keychain_fields {
982                    return Err(TempoInvalidTransaction::KeychainOpInSubblockTransaction.into());
983                }
984            }
985
986            // Validate priority fee for AA transactions using revm's validate_priority_fee_tx
987            //
988            // Skip basefee check for subblock transactions pre-Allegretto as they must always be free.
989            let base_fee = if cfg.is_base_fee_check_disabled()
990                || (aa_env.subblock_transaction && !cfg.spec.is_allegretto())
991            {
992                None
993            } else {
994                Some(evm.ctx_ref().block().basefee() as u128)
995            };
996
997            validation::validate_priority_fee_tx(
998                tx.max_fee_per_gas(),
999                tx.max_priority_fee_per_gas().unwrap_or_default(),
1000                base_fee,
1001                cfg.is_priority_fee_check_disabled(),
1002            )
1003            .map_err(TempoInvalidTransaction::EthInvalidTransaction)?;
1004
1005            // Validate time window for AA transactions
1006            let block_timestamp = evm.ctx_ref().block().timestamp().saturating_to();
1007            validate_time_window(aa_env.valid_after, aa_env.valid_before, block_timestamp)?;
1008        }
1009
1010        Ok(())
1011    }
1012
1013    /// Calculates initial gas costs with custom handling for AA transactions.
1014    ///
1015    /// AA transactions have variable intrinsic gas based on signature type:
1016    /// - secp256k1 (64/65 bytes): Standard 21k base
1017    /// - P256 (129 bytes): 21k base + 5k for P256 verification
1018    /// - WebAuthn (>129 bytes): 21k base + 5k + calldata gas for variable data
1019    #[inline]
1020    fn validate_initial_tx_gas(&self, evm: &Self::Evm) -> Result<InitialAndFloorGas, Self::Error> {
1021        let tx = evm.ctx_ref().tx();
1022
1023        // Route to appropriate gas calculation based on transaction type
1024        if tx.tempo_tx_env.is_some() {
1025            // AA transaction - use batch gas calculation
1026            validate_aa_initial_tx_gas(evm)
1027        } else {
1028            // Standard transaction - use default revm validation
1029            let spec = evm.ctx_ref().cfg().spec().into();
1030            Ok(
1031                validation::validate_initial_tx_gas(tx, spec, evm.ctx.cfg.is_eip7623_disabled())
1032                    .map_err(TempoInvalidTransaction::EthInvalidTransaction)?,
1033            )
1034        }
1035    }
1036
1037    fn catch_error(
1038        &self,
1039        evm: &mut Self::Evm,
1040        error: Self::Error,
1041    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
1042        // For subblock transactions that failed `collectFeePreTx` call we catch error and treat such transactions as valid.
1043        if evm.ctx.tx.is_subblock_transaction()
1044            && evm.cfg.spec.is_allegretto()
1045            && let Some(
1046                TempoInvalidTransaction::CollectFeePreTx(_)
1047                | TempoInvalidTransaction::EthInvalidTransaction(
1048                    InvalidTransaction::LackOfFundForMaxFee { .. },
1049                ),
1050            ) = error.as_invalid_tx_err()
1051        {
1052            // Commit the transaction.
1053            //
1054            // `collectFeePreTx` call will happen after the nonce bump so this will only commit the nonce increment.
1055            evm.ctx.journaled_state.commit_tx();
1056
1057            evm.ctx().local_mut().clear();
1058            evm.frame_stack().clear();
1059
1060            Ok(ExecutionResult::Halt {
1061                reason: TempoHaltReason::SubblockTxFeePayment,
1062                gas_used: 0,
1063            })
1064        } else {
1065            MainnetHandler::default()
1066                .catch_error(evm, error)
1067                .map(|result| result.map_haltreason(Into::into))
1068        }
1069    }
1070}
1071
1072/// Calculates intrinsic gas for an AA transaction batch using revm helpers.
1073///
1074/// This includes:
1075/// - Base 21k stipend (once for the transaction)
1076/// - Signature verification gas (P256: 5k, WebAuthn: 5k + webauthn_data)
1077/// - Per-call account access cost (COLD_ACCOUNT_ACCESS_COST * calls.len())
1078/// - Per-call input data gas (calldata tokens * 4 gas)
1079/// - Per-call CREATE costs (if applicable):
1080///   - Additional 32k base (CREATE constant)
1081///   - Initcode analysis gas (2 per 32-byte chunk, Shanghai+)
1082/// - Check that value transfer is zero.
1083/// - Access list costs (shared across batch)
1084/// - Floor gas calculation (EIP-7623, Prague+)
1085fn calculate_aa_batch_intrinsic_gas<'a>(
1086    calls: &[tempo_primitives::transaction::Call],
1087    signature: &TempoSignature,
1088    access_list: Option<impl Iterator<Item = &'a AccessListItem>>,
1089    authorization_list: &[RecoveredTempoAuthorization],
1090) -> Result<InitialAndFloorGas, TempoInvalidTransaction> {
1091    let mut gas = InitialAndFloorGas::default();
1092
1093    // 1. Base stipend (21k, once per transaction)
1094    gas.initial_gas += 21_000;
1095
1096    // 2. Signature verification gas
1097    gas.initial_gas += tempo_signature_verification_gas(signature);
1098
1099    // 3. Per-call overhead: cold account access
1100    // if the `to` address has not appeared in the call batch before.
1101    gas.initial_gas += COLD_ACCOUNT_ACCESS_COST * calls.len() as u64;
1102
1103    // 4. Authorization list costs (EIP-7702)
1104    gas.initial_gas += authorization_list.len() as u64 * eip7702::PER_EMPTY_ACCOUNT_COST;
1105    // Add signature verification costs for each authorization
1106    for auth in authorization_list {
1107        gas.initial_gas += tempo_signature_verification_gas(auth.signature());
1108    }
1109
1110    // 4. Per-call costs
1111    let mut total_tokens = 0u64;
1112
1113    for call in calls {
1114        // 4a. Calldata gas using revm helper
1115        let tokens = get_tokens_in_calldata(&call.input, true);
1116        total_tokens += tokens;
1117
1118        // 4b. CREATE-specific costs
1119        if call.to.is_create() {
1120            // CREATE costs 32000 additional gas
1121            gas.initial_gas += CREATE; // 32000 gas
1122
1123            // EIP-3860: Initcode analysis gas using revm helper
1124            gas.initial_gas += initcode_cost(call.input.len());
1125        }
1126
1127        // Note: Transaction value is not allowed in AA transactions as there is no balances in accounts yet.
1128        // Check added in https://github.com/tempoxyz/tempo/pull/759
1129        if !call.value.is_zero() {
1130            return Err(TempoInvalidTransaction::ValueTransferNotAllowedInAATx);
1131        }
1132
1133        // 4c. Value transfer cost using revm constant
1134        // left here for future reference.
1135        if !call.value.is_zero() && call.to.is_call() {
1136            gas.initial_gas += CALLVALUE; // 9000 gas
1137        }
1138    }
1139
1140    gas.initial_gas += total_tokens * STANDARD_TOKEN_COST;
1141
1142    // 5. Access list costs using revm constants
1143    if let Some(access_list) = access_list {
1144        let (accounts, storages) =
1145            access_list.fold((0u64, 0u64), |(acc_count, storage_count), item| {
1146                (
1147                    acc_count + 1,
1148                    storage_count + item.storage_slots().count() as u64,
1149                )
1150            });
1151        gas.initial_gas += accounts * ACCESS_LIST_ADDRESS; // 2400 per account
1152        gas.initial_gas += storages * ACCESS_LIST_STORAGE_KEY; // 1900 per storage
1153    }
1154
1155    // 6. Floor gas  using revm helper
1156    gas.floor_gas = calc_tx_floor_cost(total_tokens); // tokens * 10 + 21000
1157
1158    Ok(gas)
1159}
1160
1161/// Validates and calculates initial transaction gas for AA transactions.
1162///
1163/// Calculates intrinsic gas based on:
1164/// - Signature type (secp256k1: 21k, P256: 26k, WebAuthn: 26k + calldata)
1165/// - Batch call costs (per-call overhead, calldata, CREATE, value transfers)
1166fn validate_aa_initial_tx_gas<DB, I>(
1167    evm: &TempoEvm<DB, I>,
1168) -> Result<InitialAndFloorGas, EVMError<DB::Error, TempoInvalidTransaction>>
1169where
1170    DB: alloy_evm::Database,
1171{
1172    let tx = evm.ctx_ref().tx();
1173
1174    // This function should only be called for AA transactions
1175    let aa_env = tx
1176        .tempo_tx_env
1177        .as_ref()
1178        .expect("validate_aa_initial_tx_gas called for non-AA transaction");
1179
1180    let calls = &aa_env.aa_calls;
1181    let gas_limit = tx.gas_limit();
1182
1183    // Validate all CREATE calls' initcode size upfront (EIP-3860)
1184    let max_initcode_size = evm.ctx_ref().cfg().max_initcode_size();
1185    for call in calls {
1186        if call.to.is_create() && call.input.len() > max_initcode_size {
1187            return Err(EVMError::Transaction(
1188                TempoInvalidTransaction::EthInvalidTransaction(
1189                    InvalidTransaction::CreateInitCodeSizeLimit,
1190                ),
1191            ));
1192        }
1193    }
1194
1195    // Calculate batch intrinsic gas using helper
1196    let mut batch_gas = calculate_aa_batch_intrinsic_gas(
1197        calls,
1198        &aa_env.signature,
1199        tx.access_list(),
1200        &aa_env.tempo_authorization_list,
1201    )?;
1202
1203    if evm.ctx.cfg.is_eip7623_disabled() {
1204        batch_gas.floor_gas = 0u64;
1205    }
1206
1207    // Validate gas limit is sufficient for initial gas
1208    if gas_limit < batch_gas.initial_gas {
1209        return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1210            gas_limit,
1211            intrinsic_gas: batch_gas.initial_gas,
1212        }
1213        .into());
1214    }
1215
1216    // Validate floor gas (Prague+)
1217    if !evm.ctx.cfg.is_eip7623_disabled() && gas_limit < batch_gas.floor_gas {
1218        return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1219            gas_limit,
1220            intrinsic_gas: batch_gas.floor_gas,
1221        }
1222        .into());
1223    }
1224
1225    Ok(batch_gas)
1226}
1227
1228/// IMPORTANT: the caller must ensure `token` is a valid TIP20Token address.
1229pub fn get_token_balance<JOURNAL>(
1230    journal: &mut JOURNAL,
1231    token: Address,
1232    sender: Address,
1233) -> Result<U256, <JOURNAL::Database as Database>::Error>
1234where
1235    JOURNAL: JournalTr,
1236{
1237    // Address has already been validated
1238    let token_id = tip20::address_to_token_id_unchecked(token);
1239
1240    journal.load_account(token)?;
1241    let balance_slot = TIP20Token::new(token_id).balances.at(sender).slot();
1242    let balance = journal.sload(token, balance_slot)?.data;
1243
1244    Ok(balance)
1245}
1246
1247impl<DB, I> InspectorHandler for TempoEvmHandler<DB, I>
1248where
1249    DB: alloy_evm::Database,
1250    I: Inspector<TempoContext<DB>>,
1251{
1252    type IT = EthInterpreter;
1253
1254    fn inspect_run(
1255        &mut self,
1256        evm: &mut Self::Evm,
1257    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
1258        self.load_fee_fields(evm)?;
1259
1260        match self.inspect_run_without_catch_error(evm) {
1261            Ok(output) => Ok(output),
1262            Err(e) => self.catch_error(evm, e),
1263        }
1264    }
1265
1266    /// Overridden execution method with inspector support that handles AA vs standard transactions.
1267    ///
1268    /// Dispatches based on transaction type:
1269    /// - AA transactions (type 0x76): Use batch execution path with calls field
1270    /// - All other transactions: Use standard single-call execution
1271    ///
1272    /// This mirrors the logic in Handler::execution but uses inspector-aware execution methods.
1273    #[inline]
1274    fn inspect_execution(
1275        &mut self,
1276        evm: &mut Self::Evm,
1277        init_and_floor_gas: &InitialAndFloorGas,
1278    ) -> Result<FrameResult, Self::Error> {
1279        // Check if this is an AA transaction by checking for tempo_tx_env
1280        if let Some(tempo_tx_env) = evm.ctx().tx().tempo_tx_env.as_ref() {
1281            // AA transaction - use batch execution with calls field
1282            let calls = tempo_tx_env.aa_calls.clone();
1283            self.inspect_execute_multi_call(evm, init_and_floor_gas, calls)
1284        } else {
1285            // Standard transaction - use single-call execution
1286            self.inspect_execute_single_call(evm, init_and_floor_gas)
1287        }
1288    }
1289}
1290
1291/// Validates time window for AA transactions
1292///
1293/// AA transactions can have optional validBefore and validAfter fields:
1294/// - validAfter: Transaction can only be included after this timestamp
1295/// - validBefore: Transaction can only be included before this timestamp
1296///
1297/// This ensures transactions are only valid within a specific time window.
1298pub fn validate_time_window(
1299    valid_after: Option<u64>,
1300    valid_before: Option<u64>,
1301    block_timestamp: u64,
1302) -> Result<(), TempoInvalidTransaction> {
1303    // Validate validAfter constraint
1304    if let Some(after) = valid_after
1305        && block_timestamp < after
1306    {
1307        return Err(TempoInvalidTransaction::ValidAfter {
1308            current: block_timestamp,
1309            valid_after: after,
1310        });
1311    }
1312
1313    // Validate validBefore constraint
1314    if let Some(before) = valid_before
1315        && block_timestamp >= before
1316    {
1317        return Err(TempoInvalidTransaction::ValidBefore {
1318            current: block_timestamp,
1319            valid_before: before,
1320        });
1321    }
1322
1323    Ok(())
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328    use super::*;
1329    use crate::{TempoBlockEnv, TempoTxEnv};
1330    use alloy_primitives::{Address, U256};
1331    use revm::{
1332        Context, Journal, MainContext,
1333        database::{CacheDB, EmptyDB},
1334        interpreter::instructions::utility::IntoU256,
1335        primitives::hardfork::SpecId,
1336        state::Account,
1337    };
1338    use tempo_chainspec::hardfork::TempoHardfork;
1339    use tempo_precompiles::{DEFAULT_FEE_TOKEN_POST_ALLEGRETTO, TIP_FEE_MANAGER_ADDRESS};
1340
1341    fn create_test_journal() -> Journal<CacheDB<EmptyDB>> {
1342        let db = CacheDB::new(EmptyDB::default());
1343        Journal::new(db)
1344    }
1345
1346    #[test]
1347    fn test_get_token_balance() -> eyre::Result<()> {
1348        let mut journal = create_test_journal();
1349        let token = Address::random();
1350        let account = Address::random();
1351        let expected_balance = U256::random();
1352
1353        // Set up initial balance
1354        let token_id = tip20::address_to_token_id_unchecked(token);
1355        let balance_slot = TIP20Token::new(token_id).balances.at(account).slot();
1356        journal.load_account(token)?;
1357        journal
1358            .sstore(token, balance_slot, expected_balance)
1359            .unwrap();
1360
1361        let balance = get_token_balance(&mut journal, token, account)?;
1362        assert_eq!(balance, expected_balance);
1363
1364        Ok(())
1365    }
1366
1367    #[test]
1368    fn test_get_fee_token() -> eyre::Result<()> {
1369        let journal = create_test_journal();
1370        let mut ctx: TempoContext<_> = Context::mainnet()
1371            .with_db(CacheDB::new(EmptyDB::default()))
1372            .with_block(TempoBlockEnv::default())
1373            .with_cfg(Default::default())
1374            .with_tx(TempoTxEnv::default())
1375            .with_new_journal(journal);
1376        let user = Address::random();
1377        ctx.tx.inner.caller = user;
1378        let validator = Address::random();
1379        ctx.block.beneficiary = validator;
1380        let user_fee_token = Address::random();
1381        let validator_fee_token = Address::random();
1382        let tx_fee_token = Address::random();
1383
1384        // Set validator token
1385        let validator_slot = TipFeeManager::new().validator_tokens.at(validator).slot();
1386        ctx.journaled_state.load_account(TIP_FEE_MANAGER_ADDRESS)?;
1387        ctx.journaled_state
1388            .sstore(
1389                TIP_FEE_MANAGER_ADDRESS,
1390                validator_slot,
1391                validator_fee_token.into_u256(),
1392            )
1393            .unwrap();
1394
1395        let fee_token = ctx.journaled_state.get_fee_token(
1396            &ctx.tx,
1397            validator,
1398            user,
1399            TempoHardfork::default(),
1400        )?;
1401        assert_eq!(DEFAULT_FEE_TOKEN_POST_ALLEGRETTO, fee_token);
1402
1403        // Set user token
1404        let user_slot = TipFeeManager::new().user_tokens.at(user).slot();
1405        ctx.journaled_state
1406            .sstore(
1407                TIP_FEE_MANAGER_ADDRESS,
1408                user_slot,
1409                user_fee_token.into_u256(),
1410            )
1411            .unwrap();
1412
1413        let fee_token = ctx.journaled_state.get_fee_token(
1414            &ctx.tx,
1415            validator,
1416            user,
1417            TempoHardfork::default(),
1418        )?;
1419        assert_eq!(user_fee_token, fee_token);
1420
1421        // Set tx fee token
1422        ctx.tx.fee_token = Some(tx_fee_token);
1423        let fee_token = ctx.journaled_state.get_fee_token(
1424            &ctx.tx,
1425            validator,
1426            user,
1427            TempoHardfork::default(),
1428        )?;
1429        assert_eq!(tx_fee_token, fee_token);
1430
1431        Ok(())
1432    }
1433
1434    #[test]
1435    fn test_delegate_code_hash() {
1436        let mut account = Account::default();
1437        account
1438            .info
1439            .set_code(Bytecode::new_eip7702(DEFAULT_7702_DELEGATE_ADDRESS));
1440        assert_eq!(account.info.code_hash, DEFAULT_7702_DELEGATE_CODE_HASH);
1441    }
1442
1443    #[test]
1444    fn test_aa_gas_single_call_vs_normal_tx() {
1445        use crate::TempoBatchCallEnv;
1446        use alloy_primitives::{Bytes, TxKind};
1447        use revm::interpreter::gas::calculate_initial_tx_gas;
1448        use tempo_primitives::transaction::{Call, TempoSignature};
1449
1450        // Test that AA tx with secp256k1 and single call matches normal tx + per-call overhead
1451        let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); // 5 non-zero bytes
1452        let to = Address::random();
1453
1454        // Single call for AA
1455        let call = Call {
1456            to: TxKind::Call(to),
1457            value: U256::ZERO,
1458            input: calldata.clone(),
1459        };
1460
1461        let aa_env = TempoBatchCallEnv {
1462            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1463                alloy_primitives::Signature::test_signature(),
1464            )), // dummy secp256k1 sig
1465            aa_calls: vec![call],
1466            key_authorization: None,
1467            signature_hash: B256::ZERO,
1468            ..Default::default()
1469        };
1470
1471        // Calculate AA gas
1472        let spec = tempo_chainspec::hardfork::TempoHardfork::default();
1473        let aa_gas = calculate_aa_batch_intrinsic_gas(
1474            &aa_env.aa_calls,
1475            &aa_env.signature,
1476            None::<std::iter::Empty<&AccessListItem>>, // no access list
1477            &aa_env.tempo_authorization_list,
1478        )
1479        .unwrap();
1480
1481        // Calculate expected gas using revm's function for equivalent normal tx
1482        let normal_tx_gas = calculate_initial_tx_gas(
1483            spec.into(),
1484            &calldata,
1485            false, // not create
1486            0,     // no access list accounts
1487            0,     // no access list storage
1488            0,     // no authorization list
1489        );
1490
1491        // AA should be: normal tx + per-call overhead (COLD_ACCOUNT_ACCESS_COST)
1492        let expected_initial = normal_tx_gas.initial_gas + COLD_ACCOUNT_ACCESS_COST;
1493        assert_eq!(
1494            aa_gas.initial_gas, expected_initial,
1495            "AA secp256k1 single call should match normal tx + per-call overhead"
1496        );
1497    }
1498
1499    #[test]
1500    fn test_aa_gas_multiple_calls_overhead() {
1501        use crate::TempoBatchCallEnv;
1502        use alloy_primitives::{Bytes, TxKind};
1503        use revm::interpreter::gas::calculate_initial_tx_gas;
1504        use tempo_primitives::transaction::{Call, TempoSignature};
1505
1506        let calldata = Bytes::from(vec![1, 2, 3]); // 3 non-zero bytes
1507
1508        let calls = vec![
1509            Call {
1510                to: TxKind::Call(Address::random()),
1511                value: U256::ZERO,
1512                input: calldata.clone(),
1513            },
1514            Call {
1515                to: TxKind::Call(Address::random()),
1516                value: U256::ZERO,
1517                input: calldata.clone(),
1518            },
1519            Call {
1520                to: TxKind::Call(Address::random()),
1521                value: U256::ZERO,
1522                input: calldata.clone(),
1523            },
1524        ];
1525
1526        let aa_env = TempoBatchCallEnv {
1527            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1528                alloy_primitives::Signature::test_signature(),
1529            )),
1530            aa_calls: calls.clone(),
1531            key_authorization: None,
1532            signature_hash: B256::ZERO,
1533            ..Default::default()
1534        };
1535
1536        let spec = tempo_chainspec::hardfork::TempoHardfork::default();
1537        let gas = calculate_aa_batch_intrinsic_gas(
1538            &calls,
1539            &aa_env.signature,
1540            None::<std::iter::Empty<&AccessListItem>>,
1541            &aa_env.tempo_authorization_list,
1542        )
1543        .unwrap();
1544
1545        // Calculate base gas for a single normal tx
1546        let base_tx_gas = calculate_initial_tx_gas(spec.into(), &calldata, false, 0, 0, 0);
1547
1548        // For 3 calls: base (21k) + 3*calldata + 3*per-call overhead
1549        // = 21k + 2*(calldata cost) + 3*COLD_ACCOUNT_ACCESS_COST
1550        let expected = base_tx_gas.initial_gas
1551            + 2 * (calldata.len() as u64 * 16)
1552            + 3 * COLD_ACCOUNT_ACCESS_COST;
1553        assert_eq!(
1554            gas.initial_gas, expected,
1555            "Should charge per-call overhead for each call"
1556        );
1557    }
1558
1559    #[test]
1560    fn test_aa_gas_p256_signature() {
1561        use crate::TempoBatchCallEnv;
1562        use alloy_primitives::{B256, Bytes, TxKind};
1563        use revm::interpreter::gas::calculate_initial_tx_gas;
1564        use tempo_primitives::transaction::{
1565            Call, TempoSignature, tt_signature::P256SignatureWithPreHash,
1566        };
1567
1568        let spec = SpecId::CANCUN;
1569        let calldata = Bytes::from(vec![1, 2]);
1570
1571        let call = Call {
1572            to: TxKind::Call(Address::random()),
1573            value: U256::ZERO,
1574            input: calldata.clone(),
1575        };
1576
1577        let aa_env = TempoBatchCallEnv {
1578            signature: TempoSignature::Primitive(PrimitiveSignature::P256(
1579                P256SignatureWithPreHash {
1580                    r: B256::ZERO,
1581                    s: B256::ZERO,
1582                    pub_key_x: B256::ZERO,
1583                    pub_key_y: B256::ZERO,
1584                    pre_hash: false,
1585                },
1586            )),
1587            aa_calls: vec![call],
1588            key_authorization: None,
1589            signature_hash: B256::ZERO,
1590            ..Default::default()
1591        };
1592
1593        let gas = calculate_aa_batch_intrinsic_gas(
1594            &aa_env.aa_calls,
1595            &aa_env.signature,
1596            None::<std::iter::Empty<&AccessListItem>>,
1597            &aa_env.tempo_authorization_list,
1598        )
1599        .unwrap();
1600
1601        // Calculate base gas for normal tx
1602        let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
1603
1604        // Expected: normal tx + P256_VERIFY_GAS + per-call overhead
1605        let expected = base_gas.initial_gas + P256_VERIFY_GAS + COLD_ACCOUNT_ACCESS_COST;
1606        assert_eq!(
1607            gas.initial_gas, expected,
1608            "Should include P256 verification gas"
1609        );
1610    }
1611
1612    #[test]
1613    fn test_aa_gas_create_call() {
1614        use crate::TempoBatchCallEnv;
1615        use alloy_primitives::{Bytes, TxKind};
1616        use revm::interpreter::gas::calculate_initial_tx_gas;
1617        use tempo_primitives::transaction::{Call, TempoSignature};
1618
1619        let spec = SpecId::CANCUN; // Post-Shanghai
1620        let initcode = Bytes::from(vec![0x60, 0x80]); // 2 bytes
1621
1622        let call = Call {
1623            to: TxKind::Create,
1624            value: U256::ZERO,
1625            input: initcode.clone(),
1626        };
1627
1628        let aa_env = TempoBatchCallEnv {
1629            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1630                alloy_primitives::Signature::test_signature(),
1631            )),
1632            aa_calls: vec![call],
1633            key_authorization: None,
1634            signature_hash: B256::ZERO,
1635            ..Default::default()
1636        };
1637
1638        let gas = calculate_aa_batch_intrinsic_gas(
1639            &aa_env.aa_calls,
1640            &aa_env.signature,
1641            None::<std::iter::Empty<&AccessListItem>>,
1642            &aa_env.tempo_authorization_list,
1643        )
1644        .unwrap();
1645
1646        // Calculate expected using revm's function for CREATE tx
1647        let base_gas = calculate_initial_tx_gas(
1648            spec, &initcode, true, // is_create = true
1649            0, 0, 0,
1650        );
1651
1652        // AA CREATE should be: normal CREATE + per-call overhead
1653        let expected = base_gas.initial_gas + COLD_ACCOUNT_ACCESS_COST;
1654        assert_eq!(gas.initial_gas, expected, "Should include CREATE costs");
1655    }
1656
1657    #[test]
1658    fn test_aa_gas_value_transfer() {
1659        use crate::TempoBatchCallEnv;
1660        use alloy_primitives::{Bytes, TxKind};
1661        use tempo_primitives::transaction::{Call, TempoSignature};
1662
1663        let calldata = Bytes::from(vec![1]);
1664
1665        let call = Call {
1666            to: TxKind::Call(Address::random()),
1667            value: U256::from(1000), // Non-zero value
1668            input: calldata,
1669        };
1670
1671        let aa_env = TempoBatchCallEnv {
1672            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1673                alloy_primitives::Signature::test_signature(),
1674            )),
1675            aa_calls: vec![call],
1676            key_authorization: None,
1677            signature_hash: B256::ZERO,
1678            ..Default::default()
1679        };
1680
1681        let res = calculate_aa_batch_intrinsic_gas(
1682            &aa_env.aa_calls,
1683            &aa_env.signature,
1684            None::<std::iter::Empty<&AccessListItem>>,
1685            &aa_env.tempo_authorization_list,
1686        );
1687
1688        assert_eq!(
1689            res.unwrap_err(),
1690            TempoInvalidTransaction::ValueTransferNotAllowedInAATx
1691        );
1692    }
1693
1694    #[test]
1695    fn test_aa_gas_access_list() {
1696        use crate::TempoBatchCallEnv;
1697        use alloy_primitives::{Bytes, TxKind};
1698        use revm::interpreter::gas::calculate_initial_tx_gas;
1699        use tempo_primitives::transaction::{Call, TempoSignature};
1700
1701        let spec = SpecId::CANCUN;
1702        let calldata = Bytes::from(vec![]);
1703
1704        let call = Call {
1705            to: TxKind::Call(Address::random()),
1706            value: U256::ZERO,
1707            input: calldata.clone(),
1708        };
1709
1710        let aa_env = TempoBatchCallEnv {
1711            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1712                alloy_primitives::Signature::test_signature(),
1713            )),
1714            aa_calls: vec![call],
1715            key_authorization: None,
1716            signature_hash: B256::ZERO,
1717            ..Default::default()
1718        };
1719
1720        // Test without access list
1721        let gas = calculate_aa_batch_intrinsic_gas(
1722            &aa_env.aa_calls,
1723            &aa_env.signature,
1724            None::<std::iter::Empty<&AccessListItem>>,
1725            &aa_env.tempo_authorization_list,
1726        )
1727        .unwrap();
1728
1729        // Calculate expected using revm's function
1730        let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
1731
1732        // Expected: normal tx + per-call overhead (no access list in this test)
1733        let expected = base_gas.initial_gas + COLD_ACCOUNT_ACCESS_COST;
1734        assert_eq!(
1735            gas.initial_gas, expected,
1736            "Should match normal tx + per-call overhead"
1737        );
1738    }
1739
1740    #[test]
1741    fn test_key_authorization_rlp_encoding() {
1742        use alloy_primitives::{Address, U256};
1743        use tempo_primitives::transaction::{
1744            SignatureType, TokenLimit, key_authorization::KeyAuthorization,
1745        };
1746
1747        // Create test data
1748        let chain_id = 1u64;
1749        let key_type = SignatureType::Secp256k1;
1750        let key_id = Address::random();
1751        let expiry = 1000u64;
1752        let limits = vec![
1753            TokenLimit {
1754                token: Address::random(),
1755                limit: U256::from(100),
1756            },
1757            TokenLimit {
1758                token: Address::random(),
1759                limit: U256::from(200),
1760            },
1761        ];
1762
1763        // Compute hash using the helper function
1764        let hash1 = KeyAuthorization {
1765            chain_id,
1766            key_type,
1767            key_id,
1768            expiry: Some(expiry),
1769            limits: Some(limits.clone()),
1770        }
1771        .signature_hash();
1772
1773        // Compute again to verify consistency
1774        let hash2 = KeyAuthorization {
1775            chain_id,
1776            key_type,
1777            key_id,
1778            expiry: Some(expiry),
1779            limits: Some(limits.clone()),
1780        }
1781        .signature_hash();
1782
1783        assert_eq!(hash1, hash2, "Hash computation should be deterministic");
1784
1785        // Verify that different chain_id produces different hash
1786        let hash3 = KeyAuthorization {
1787            chain_id: 2,
1788            key_type,
1789            key_id,
1790            expiry: Some(expiry),
1791            limits: Some(limits),
1792        }
1793        .signature_hash();
1794        assert_ne!(
1795            hash1, hash3,
1796            "Different chain_id should produce different hash"
1797        );
1798    }
1799
1800    #[test]
1801    fn test_aa_gas_floor_gas_prague() {
1802        use crate::TempoBatchCallEnv;
1803        use alloy_primitives::{Bytes, TxKind};
1804        use revm::interpreter::gas::calculate_initial_tx_gas;
1805        use tempo_primitives::transaction::{Call, TempoSignature};
1806
1807        let spec = SpecId::PRAGUE;
1808        let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); // 5 non-zero bytes
1809
1810        let call = Call {
1811            to: TxKind::Call(Address::random()),
1812            value: U256::ZERO,
1813            input: calldata.clone(),
1814        };
1815
1816        let aa_env = TempoBatchCallEnv {
1817            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1818                alloy_primitives::Signature::test_signature(),
1819            )),
1820            aa_calls: vec![call],
1821            key_authorization: None,
1822            signature_hash: B256::ZERO,
1823            ..Default::default()
1824        };
1825
1826        let gas = calculate_aa_batch_intrinsic_gas(
1827            &aa_env.aa_calls,
1828            &aa_env.signature,
1829            None::<std::iter::Empty<&AccessListItem>>,
1830            &aa_env.tempo_authorization_list,
1831        )
1832        .unwrap();
1833
1834        // Calculate expected floor gas using revm's function
1835        let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
1836
1837        // Floor gas should match revm's calculation for same calldata
1838        assert_eq!(
1839            gas.floor_gas, base_gas.floor_gas,
1840            "Should calculate floor gas for Prague matching revm"
1841        );
1842    }
1843
1844    /// This test will start failing once we get the balance transfer enabled
1845    /// PR that introduced [`TempoInvalidTransaction::ValueTransferNotAllowed`] https://github.com/tempoxyz/tempo/pull/759
1846    #[test]
1847    fn test_zero_value_transfer() -> eyre::Result<()> {
1848        use crate::TempoEvm;
1849
1850        // Create a test context with a transaction that has a non-zero value
1851        let ctx = Context::mainnet()
1852            .with_db(CacheDB::new(EmptyDB::default()))
1853            .with_block(Default::default())
1854            .with_cfg(Default::default())
1855            .with_tx(TempoTxEnv::default());
1856        let mut evm = TempoEvm::new(ctx, ());
1857
1858        // Set a non-zero value on the transaction
1859        evm.ctx.tx.inner.value = U256::from(1000);
1860
1861        // Create the handler
1862        let handler = TempoEvmHandler::<_, ()>::new();
1863
1864        // Call validate_env and expect it to fail with ValueTransferNotAllowed
1865        let result = handler.validate_env(&mut evm);
1866
1867        if let Err(EVMError::Transaction(err)) = result {
1868            assert_eq!(err, TempoInvalidTransaction::ValueTransferNotAllowed);
1869        } else {
1870            panic!("Expected ValueTransferNotAllowed error");
1871        }
1872
1873        Ok(())
1874    }
1875}