Skip to main content

tempo_revm/
evm.rs

1use crate::{TempoBlockEnv, TempoTxEnv, instructions};
2use alloy_evm::{Database, precompiles::PrecompilesMap};
3use alloy_primitives::{Address, U256};
4use revm::{
5    Context, Inspector,
6    context::{Cfg, CfgEnv, ContextError, Evm, FrameStack},
7    handler::{
8        EthFrame, EvmTr, FrameInitOrResult, FrameTr, ItemOrResult, instructions::EthInstructions,
9    },
10    inspector::InspectorEvmTr,
11    interpreter::{InitialAndFloorGas, interpreter::EthInterpreter},
12};
13use tempo_chainspec::hardfork::TempoHardfork;
14
15/// The Tempo EVM context type.
16pub type TempoContext<DB> = Context<TempoBlockEnv, TempoTxEnv, CfgEnv<TempoHardfork>, DB>;
17
18/// TempoEvm extends the Evm with Tempo specific types and logic.
19#[derive(Debug, derive_more::Deref, derive_more::DerefMut)]
20#[expect(clippy::type_complexity)]
21pub struct TempoEvm<DB: Database, I> {
22    /// Inner EVM type.
23    #[deref]
24    #[deref_mut]
25    pub inner: Evm<
26        TempoContext<DB>,
27        I,
28        EthInstructions<EthInterpreter, TempoContext<DB>>,
29        PrecompilesMap,
30        EthFrame<EthInterpreter>,
31    >,
32    /// The fee collected in `collectFeePreTx` call.
33    pub(crate) collected_fee: U256,
34    /// The fee token used to pay fees for the current transaction.
35    pub(crate) fee_token: Option<Address>,
36    /// The expiry timestamp of the access key used by the current transaction.
37    /// Populated during validation for keychain-signed transactions or transactions carrying a KeyAuthorization.
38    pub(crate) key_expiry: Option<u64>,
39    /// When true, skips the `valid_after` time-window check during validation.
40    ///
41    /// The transaction pool sets this because it intentionally accepts transactions
42    /// with a future `valid_after` (queued until executable).
43    pub skip_valid_after_check: bool,
44    /// When true, skips the AMM liquidity check in `collect_fee_pre_tx`.
45    ///
46    /// The transaction pool sets this because it performs its own liquidity
47    /// validation against a cached view of the AMM state.
48    pub skip_liquidity_check: bool,
49}
50
51impl<DB: Database, I> TempoEvm<DB, I> {
52    /// Create a new Tempo EVM.
53    pub fn new(ctx: TempoContext<DB>, inspector: I) -> Self {
54        let precompiles = tempo_precompiles::tempo_precompiles(&ctx.cfg);
55
56        Self::new_inner(Evm {
57            instruction: instructions::tempo_instructions(ctx.cfg.spec),
58            ctx,
59            inspector,
60            precompiles,
61            frame_stack: FrameStack::new(),
62        })
63    }
64
65    /// Inner helper function to create a new Tempo EVM with empty logs.
66    #[inline]
67    #[expect(clippy::type_complexity)]
68    fn new_inner(
69        inner: Evm<
70            TempoContext<DB>,
71            I,
72            EthInstructions<EthInterpreter, TempoContext<DB>>,
73            PrecompilesMap,
74            EthFrame<EthInterpreter>,
75        >,
76    ) -> Self {
77        Self {
78            inner,
79            collected_fee: U256::ZERO,
80            fee_token: None,
81            key_expiry: None,
82            skip_valid_after_check: false,
83            skip_liquidity_check: false,
84        }
85    }
86
87    /// Computes initial gas limit and reservoir for a transaction given its initial gas spending.
88    pub(crate) fn initial_gas_and_reservoir(
89        &self,
90        init_and_floor_gas: &InitialAndFloorGas,
91    ) -> (u64, u64) {
92        // Pre-T0 it could happen that the initial gas spending is greater than the gas limit due to faulty validation.
93        //
94        // Before that it would overflow, so we are reproducing this behavior here by setting the gas limit to u64::MAX and the reservoir to 0.
95        if !self.cfg.spec.is_t0() && init_and_floor_gas.initial_total_gas > self.tx.gas_limit {
96            (u64::MAX, 0)
97        } else {
98            init_and_floor_gas.initial_gas_and_reservoir(
99                self.tx.gas_limit,
100                self.cfg.tx_gas_limit_cap(),
101                self.cfg.is_amsterdam_eip8037_enabled(),
102            )
103        }
104    }
105}
106
107impl<DB: Database, I> TempoEvm<DB, I> {
108    /// Consumed self and returns a new Evm type with given Inspector.
109    pub fn with_inspector<OINSP>(self, inspector: OINSP) -> TempoEvm<DB, OINSP> {
110        TempoEvm::new_inner(self.inner.with_inspector(inspector))
111    }
112
113    /// Consumes self and returns a new Evm type with given Precompiles.
114    pub fn with_precompiles(self, precompiles: PrecompilesMap) -> Self {
115        Self::new_inner(self.inner.with_precompiles(precompiles))
116    }
117
118    /// Consumes self and returns the inner Inspector.
119    pub fn into_inspector(self) -> I {
120        self.inner.into_inspector()
121    }
122
123    /// Clears all intermediate state from the EVM.
124    pub fn clear(&mut self) {
125        self.fee_token = None;
126        self.key_expiry = None;
127    }
128}
129
130impl<DB, I> EvmTr for TempoEvm<DB, I>
131where
132    DB: Database,
133{
134    type Context = TempoContext<DB>;
135    type Instructions = EthInstructions<EthInterpreter, TempoContext<DB>>;
136    type Precompiles = PrecompilesMap;
137    type Frame = EthFrame<EthInterpreter>;
138
139    fn all(
140        &self,
141    ) -> (
142        &Self::Context,
143        &Self::Instructions,
144        &Self::Precompiles,
145        &FrameStack<Self::Frame>,
146    ) {
147        self.inner.all()
148    }
149
150    fn all_mut(
151        &mut self,
152    ) -> (
153        &mut Self::Context,
154        &mut Self::Instructions,
155        &mut Self::Precompiles,
156        &mut FrameStack<Self::Frame>,
157    ) {
158        self.inner.all_mut()
159    }
160
161    fn frame_stack(&mut self) -> &mut FrameStack<Self::Frame> {
162        &mut self.inner.frame_stack
163    }
164
165    fn frame_init(
166        &mut self,
167        frame_input: <Self::Frame as FrameTr>::FrameInit,
168    ) -> Result<
169        ItemOrResult<&mut Self::Frame, <Self::Frame as FrameTr>::FrameResult>,
170        ContextError<DB::Error>,
171    > {
172        self.inner.frame_init(frame_input)
173    }
174
175    fn frame_run(&mut self) -> Result<FrameInitOrResult<Self::Frame>, ContextError<DB::Error>> {
176        self.inner.frame_run()
177    }
178
179    fn frame_return_result(
180        &mut self,
181        result: <Self::Frame as FrameTr>::FrameResult,
182    ) -> Result<Option<<Self::Frame as FrameTr>::FrameResult>, ContextError<DB::Error>> {
183        self.inner.frame_return_result(result)
184    }
185}
186
187impl<DB, I> InspectorEvmTr for TempoEvm<DB, I>
188where
189    DB: Database,
190    I: Inspector<TempoContext<DB>>,
191{
192    type Inspector = I;
193
194    fn all_inspector(
195        &self,
196    ) -> (
197        &Self::Context,
198        &Self::Instructions,
199        &Self::Precompiles,
200        &FrameStack<Self::Frame>,
201        &Self::Inspector,
202    ) {
203        self.inner.all_inspector()
204    }
205
206    fn all_mut_inspector(
207        &mut self,
208    ) -> (
209        &mut Self::Context,
210        &mut Self::Instructions,
211        &mut Self::Precompiles,
212        &mut FrameStack<Self::Frame>,
213        &mut Self::Inspector,
214    ) {
215        self.inner.all_mut_inspector()
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use crate::gas_params::tempo_gas_params;
222    use alloy_eips::eip7702::Authorization;
223    use alloy_evm::FromRecoveredTx;
224    use alloy_primitives::{Address, Bytes, TxKind, U256, bytes, hex};
225    use alloy_sol_types::SolCall;
226    use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
227    use p256::{
228        ecdsa::{SigningKey, signature::hazmat::PrehashSigner},
229        elliptic_curve::rand_core::OsRng,
230    };
231    use reth_evm::EvmInternals;
232    use revm::{
233        Context, DatabaseRef, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainContext,
234        bytecode::opcode,
235        context::{
236            CfgEnv, ContextTr, TxEnv,
237            result::{ExecutionResult, HaltReason},
238        },
239        database::{CacheDB, EmptyDB},
240        handler::system_call::SystemCallEvm,
241        inspector::{CountInspector, InspectSystemCallEvm},
242        state::{AccountInfo, Bytecode},
243    };
244    use sha2::{Digest, Sha256};
245    use tempo_chainspec::hardfork::TempoHardfork;
246    use tempo_precompiles::{
247        AuthorizedKey, NONCE_PRECOMPILE_ADDRESS, PATH_USD_ADDRESS,
248        nonce::NonceManager,
249        storage::{Handler, StorageCtx, evm::EvmPrecompileStorageProvider},
250        test_util::TIP20Setup,
251        tip20::{ITIP20, TIP20Token},
252    };
253    use tempo_primitives::{
254        TempoTransaction,
255        transaction::{
256            KeyAuthorization, KeychainSignature, SignatureType, TempoSignedAuthorization,
257            tempo_transaction::Call,
258            tt_signature::{
259                PrimitiveSignature, TempoSignature, WebAuthnSignature, derive_p256_address,
260                normalize_p256_s,
261            },
262        },
263    };
264
265    use crate::{TempoBlockEnv, TempoEvm, TempoHaltReason, TempoInvalidTransaction, TempoTxEnv};
266    use revm::context::result::InvalidTransaction;
267
268    // ==================== Test Constants ====================
269
270    /// Default balance for funded accounts (1 ETH)
271    const DEFAULT_BALANCE: u128 = 1_000_000_000_000_000_000;
272
273    /// Identity precompile address (0x04)
274    const IDENTITY_PRECOMPILE: Address = Address::new([
275        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x04,
276    ]);
277
278    // ==================== Test Utility Functions ====================
279
280    /// Create an empty EVM instance with default settings and no inspector.
281    fn create_evm() -> TempoEvm<CacheDB<EmptyDB>, ()> {
282        let db = CacheDB::new(EmptyDB::new());
283        let ctx = Context::mainnet()
284            .with_db(db)
285            .with_block(Default::default())
286            .with_cfg(Default::default())
287            .with_tx(Default::default());
288        TempoEvm::new(ctx, ())
289    }
290
291    /// Create an EVM instance with a specific block timestamp.
292    fn create_evm_with_timestamp(timestamp: u64) -> TempoEvm<CacheDB<EmptyDB>, ()> {
293        let db = CacheDB::new(EmptyDB::new());
294        let mut block = TempoBlockEnv::default();
295        block.inner.timestamp = U256::from(timestamp);
296
297        let ctx = Context::mainnet()
298            .with_db(db)
299            .with_block(block)
300            .with_cfg(Default::default())
301            .with_tx(Default::default());
302
303        TempoEvm::new(ctx, ())
304    }
305
306    /// Fund an account with the default balance (1 ETH).
307    fn fund_account(evm: &mut TempoEvm<CacheDB<EmptyDB>, ()>, address: Address) {
308        evm.ctx.db_mut().insert_account_info(
309            address,
310            AccountInfo {
311                balance: U256::from(DEFAULT_BALANCE),
312                ..Default::default()
313            },
314        );
315    }
316
317    /// Create an EVM with a funded account at the given address.
318    fn create_funded_evm(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
319        let mut evm = create_evm();
320        fund_account(&mut evm, address);
321        evm
322    }
323
324    /// Create an EVM with T1C hardfork enabled and a funded account.
325    /// This applies TIP-1000 gas params via `tempo_gas_params()`.
326    fn create_funded_evm_t1(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
327        let db = CacheDB::new(EmptyDB::new());
328        let mut cfg = CfgEnv::<TempoHardfork>::default();
329        cfg.spec = TempoHardfork::T1C;
330        // Apply TIP-1000 gas params for T1C hardfork
331        cfg.gas_params = tempo_gas_params(TempoHardfork::T1C);
332
333        let ctx = Context::mainnet()
334            .with_db(db)
335            .with_block(Default::default())
336            .with_cfg(cfg)
337            .with_tx(Default::default());
338
339        let mut evm = TempoEvm::new(ctx, ());
340        fund_account(&mut evm, address);
341        evm
342    }
343
344    /// Create an EVM with T3 hardfork enabled and a funded account.
345    fn create_funded_evm_t3(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
346        let db = CacheDB::new(EmptyDB::new());
347        let mut cfg = CfgEnv::<TempoHardfork>::default();
348        cfg.spec = TempoHardfork::T3;
349        cfg.gas_params = tempo_gas_params(TempoHardfork::T3);
350
351        let ctx = Context::mainnet()
352            .with_db(db)
353            .with_block(Default::default())
354            .with_cfg(cfg)
355            .with_tx(Default::default());
356
357        let mut evm = TempoEvm::new(ctx, ());
358        fund_account(&mut evm, address);
359        evm
360    }
361
362    /// Create an EVM with T4 hardfork enabled and a funded account.
363    fn create_funded_evm_t4(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
364        let db = CacheDB::new(EmptyDB::new());
365        let mut cfg = CfgEnv::<TempoHardfork>::default();
366        cfg.spec = TempoHardfork::T4;
367        cfg.gas_params = tempo_gas_params(TempoHardfork::T4);
368        cfg.enable_amsterdam_eip8037 = true;
369
370        let ctx = Context::mainnet()
371            .with_db(db)
372            .with_block(Default::default())
373            .with_cfg(cfg)
374            .with_tx(Default::default());
375
376        let mut evm = TempoEvm::new(ctx, ());
377        fund_account(&mut evm, address);
378        evm
379    }
380
381    /// Create an EVM with a specific timestamp and a funded account.
382    fn create_funded_evm_with_timestamp(
383        address: Address,
384        timestamp: u64,
385    ) -> TempoEvm<CacheDB<EmptyDB>, ()> {
386        let mut evm = create_evm_with_timestamp(timestamp);
387        fund_account(&mut evm, address);
388        evm
389    }
390
391    /// Create an EVM with T1 hardfork, a specific timestamp, and a funded account.
392    fn create_funded_evm_t1_with_timestamp(
393        address: Address,
394        timestamp: u64,
395    ) -> TempoEvm<CacheDB<EmptyDB>, ()> {
396        let db = CacheDB::new(EmptyDB::new());
397        let mut cfg = CfgEnv::<TempoHardfork>::default();
398        cfg.spec = TempoHardfork::T1;
399        cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
400
401        let mut block = TempoBlockEnv::default();
402        block.inner.timestamp = U256::from(timestamp);
403
404        let ctx = Context::mainnet()
405            .with_db(db)
406            .with_block(block)
407            .with_cfg(cfg)
408            .with_tx(Default::default());
409
410        let mut evm = TempoEvm::new(ctx, ());
411        fund_account(&mut evm, address);
412        evm
413    }
414
415    /// Create an EVM instance with a custom inspector.
416    fn create_evm_with_inspector<I>(inspector: I) -> TempoEvm<CacheDB<EmptyDB>, I> {
417        let db = CacheDB::new(EmptyDB::new());
418        let ctx = Context::mainnet()
419            .with_db(db)
420            .with_block(Default::default())
421            .with_cfg(Default::default())
422            .with_tx(Default::default());
423        TempoEvm::new(ctx, inspector)
424    }
425
426    /// Helper struct for managing P256 key pairs in tests.
427    struct P256KeyPair {
428        signing_key: SigningKey,
429        pub_key_x: alloy_primitives::B256,
430        pub_key_y: alloy_primitives::B256,
431        address: Address,
432    }
433
434    impl P256KeyPair {
435        /// Generate a new random P256 key pair.
436        fn random() -> Self {
437            let signing_key = SigningKey::random(&mut OsRng);
438            let verifying_key = signing_key.verifying_key();
439            let encoded_point = verifying_key.to_encoded_point(false);
440            let pub_key_x = alloy_primitives::B256::from_slice(encoded_point.x().unwrap().as_ref());
441            let pub_key_y = alloy_primitives::B256::from_slice(encoded_point.y().unwrap().as_ref());
442            let address = derive_p256_address(&pub_key_x, &pub_key_y);
443
444            Self {
445                signing_key,
446                pub_key_x,
447                pub_key_y,
448                address,
449            }
450        }
451
452        /// Create a WebAuthn signature for the given challenge.
453        fn sign_webauthn(&self, challenge: &[u8]) -> eyre::Result<WebAuthnSignature> {
454            // Create authenticator data
455            let mut authenticator_data = vec![0u8; 37];
456            authenticator_data[0..32].copy_from_slice(&[0xAA; 32]); // rpIdHash
457            authenticator_data[32] = 0x01; // UP flag set
458            authenticator_data[33..37].copy_from_slice(&[0, 0, 0, 0]); // signCount
459
460            // Create client data JSON
461            let challenge_b64url = URL_SAFE_NO_PAD.encode(challenge);
462            let client_data_json = format!(
463                r#"{{"type":"webauthn.get","challenge":"{challenge_b64url}","origin":"https://example.com","crossOrigin":false}}"#
464            );
465
466            // Compute message hash
467            let client_data_hash = Sha256::digest(client_data_json.as_bytes());
468            let mut final_hasher = Sha256::new();
469            final_hasher.update(&authenticator_data);
470            final_hasher.update(client_data_hash);
471            let message_hash = final_hasher.finalize();
472
473            // Sign
474            let signature: p256::ecdsa::Signature = self.signing_key.sign_prehash(&message_hash)?;
475            let sig_bytes = signature.to_bytes();
476
477            // Construct WebAuthn data
478            let mut webauthn_data = Vec::new();
479            webauthn_data.extend_from_slice(&authenticator_data);
480            webauthn_data.extend_from_slice(client_data_json.as_bytes());
481
482            Ok(WebAuthnSignature {
483                webauthn_data: Bytes::from(webauthn_data),
484                r: alloy_primitives::B256::from_slice(&sig_bytes[0..32]),
485                s: normalize_p256_s(&sig_bytes[32..64]).map_err(|e| eyre::eyre!(e))?,
486                pub_key_x: self.pub_key_x,
487                pub_key_y: self.pub_key_y,
488            })
489        }
490
491        /// Create a signed EIP-7702 authorization for the given delegate address.
492        fn create_signed_authorization(
493            &self,
494            delegate_address: Address,
495        ) -> eyre::Result<TempoSignedAuthorization> {
496            let auth = Authorization {
497                chain_id: U256::from(1),
498                address: delegate_address,
499                nonce: 0,
500            };
501
502            let mut sig_buf = Vec::new();
503            sig_buf.push(tempo_primitives::transaction::tt_authorization::MAGIC);
504            alloy_rlp::Encodable::encode(&auth, &mut sig_buf);
505            let auth_sig_hash = alloy_primitives::keccak256(&sig_buf);
506
507            let webauthn_sig = self.sign_webauthn(auth_sig_hash.as_slice())?;
508            let aa_sig = TempoSignature::Primitive(PrimitiveSignature::WebAuthn(webauthn_sig));
509
510            Ok(TempoSignedAuthorization::new_unchecked(auth, aa_sig))
511        }
512
513        /// Sign a transaction and return it ready for execution.
514        fn sign_tx(&self, tx: TempoTransaction) -> eyre::Result<tempo_primitives::AASigned> {
515            let webauthn_sig = self.sign_webauthn(tx.signature_hash().as_slice())?;
516            Ok(
517                tx.into_signed(TempoSignature::Primitive(PrimitiveSignature::WebAuthn(
518                    webauthn_sig,
519                ))),
520            )
521        }
522
523        /// Sign a transaction with KeychainSignature wrapper (V2).
524        fn sign_tx_keychain(
525            &self,
526            tx: TempoTransaction,
527        ) -> eyre::Result<tempo_primitives::AASigned> {
528            // V2: sign keccak256(0x04 || sig_hash || user_address)
529            let sig_hash = tx.signature_hash();
530            let effective_hash = alloy_primitives::keccak256(
531                [&[0x04], sig_hash.as_slice(), self.address.as_slice()].concat(),
532            );
533            let webauthn_sig = self.sign_webauthn(effective_hash.as_slice())?;
534            let keychain_sig =
535                KeychainSignature::new(self.address, PrimitiveSignature::WebAuthn(webauthn_sig));
536            Ok(tx.into_signed(TempoSignature::Keychain(keychain_sig)))
537        }
538    }
539
540    /// Builder for creating test transactions with sensible defaults.
541    struct TxBuilder {
542        calls: Vec<Call>,
543        nonce: u64,
544        nonce_key: U256,
545        gas_limit: u64,
546        max_fee_per_gas: u128,
547        max_priority_fee_per_gas: u128,
548        valid_before: Option<u64>,
549        valid_after: Option<u64>,
550        authorization_list: Vec<TempoSignedAuthorization>,
551        key_authorization: Option<tempo_primitives::transaction::SignedKeyAuthorization>,
552    }
553
554    impl Default for TxBuilder {
555        fn default() -> Self {
556            Self {
557                calls: vec![],
558                nonce: 0,
559                nonce_key: U256::ZERO,
560                gas_limit: 1_000_000,
561                max_fee_per_gas: 0,
562                max_priority_fee_per_gas: 0,
563                valid_before: Some(u64::MAX),
564                valid_after: None,
565                authorization_list: vec![],
566                key_authorization: None,
567            }
568        }
569    }
570
571    impl TxBuilder {
572        fn new() -> Self {
573            Self::default()
574        }
575
576        /// Add a call to the identity precompile with the given input.
577        fn call_identity(mut self, input: &[u8]) -> Self {
578            self.calls.push(Call {
579                to: TxKind::Call(IDENTITY_PRECOMPILE),
580                value: U256::ZERO,
581                input: Bytes::from(input.to_vec()),
582            });
583            self
584        }
585
586        /// Add a call to a specific address.
587        fn call(mut self, to: Address, input: &[u8]) -> Self {
588            self.calls.push(Call {
589                to: TxKind::Call(to),
590                value: U256::ZERO,
591                input: Bytes::from(input.to_vec()),
592            });
593            self
594        }
595
596        /// Add a create call with the given initcode.
597        fn create(mut self, initcode: &[u8]) -> Self {
598            self.calls.push(Call {
599                to: TxKind::Create,
600                value: U256::ZERO,
601                input: Bytes::from(initcode.to_vec()),
602            });
603            self
604        }
605
606        /// Add a call with a specific value transfer.
607        fn call_with_value(mut self, to: Address, input: &[u8], value: U256) -> Self {
608            self.calls.push(Call {
609                to: TxKind::Call(to),
610                value,
611                input: Bytes::from(input.to_vec()),
612            });
613            self
614        }
615
616        fn nonce(mut self, nonce: u64) -> Self {
617            self.nonce = nonce;
618            self
619        }
620
621        fn nonce_key(mut self, nonce_key: U256) -> Self {
622            self.nonce_key = nonce_key;
623            self
624        }
625
626        fn gas_limit(mut self, gas_limit: u64) -> Self {
627            self.gas_limit = gas_limit;
628            self
629        }
630
631        fn with_max_fee_per_gas(mut self, max_fee_per_gas: u128) -> Self {
632            self.max_fee_per_gas = max_fee_per_gas;
633            self
634        }
635
636        fn with_max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: u128) -> Self {
637            self.max_priority_fee_per_gas = max_priority_fee_per_gas;
638            self
639        }
640
641        fn valid_before(mut self, valid_before: Option<u64>) -> Self {
642            self.valid_before = valid_before;
643            self
644        }
645
646        fn valid_after(mut self, valid_after: Option<u64>) -> Self {
647            self.valid_after = valid_after;
648            self
649        }
650
651        fn authorization(mut self, auth: TempoSignedAuthorization) -> Self {
652            self.authorization_list.push(auth);
653            self
654        }
655
656        fn key_authorization(
657            mut self,
658            key_auth: tempo_primitives::transaction::SignedKeyAuthorization,
659        ) -> Self {
660            self.key_authorization = Some(key_auth);
661            self
662        }
663
664        fn build(self) -> TempoTransaction {
665            TempoTransaction {
666                chain_id: 1,
667                fee_token: None,
668                max_priority_fee_per_gas: self.max_priority_fee_per_gas,
669                max_fee_per_gas: self.max_fee_per_gas,
670                gas_limit: self.gas_limit,
671                calls: self.calls,
672                access_list: Default::default(),
673                nonce_key: self.nonce_key,
674                nonce: self.nonce,
675                fee_payer_signature: None,
676                valid_before: self.valid_before.and_then(core::num::NonZeroU64::new),
677                valid_after: self.valid_after.and_then(core::num::NonZeroU64::new),
678                key_authorization: self.key_authorization,
679                tempo_authorization_list: self.authorization_list,
680            }
681        }
682    }
683
684    // ==================== End Test Utility Functions ====================
685
686    #[test_case::test_case(TempoHardfork::T1)]
687    #[test_case::test_case(TempoHardfork::T1C)]
688    fn test_access_millis_timestamp(spec: TempoHardfork) -> eyre::Result<()> {
689        let db = CacheDB::new(EmptyDB::new());
690
691        let mut ctx = Context::mainnet()
692            .with_db(db)
693            .with_block(TempoBlockEnv::default())
694            .with_cfg(CfgEnv::<TempoHardfork>::default())
695            .with_tx(Default::default());
696
697        ctx.cfg.spec = spec;
698        ctx.block.timestamp = U256::from(1000);
699        ctx.block.timestamp_millis_part = 100;
700
701        let mut tempo_evm = TempoEvm::new(ctx, ());
702        let ctx = &mut tempo_evm.ctx;
703
704        let internals = EvmInternals::new(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, &ctx.tx);
705        let mut storage = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
706
707        _ = StorageCtx::enter(&mut storage, || TIP20Setup::path_usd(Address::ZERO).apply())?;
708        drop(storage);
709
710        let contract = Address::random();
711
712        // Create a simple contract that returns output of the opcode.
713        ctx.db_mut().insert_account_info(
714            contract,
715            AccountInfo {
716                // MILLISTIMESTAMP PUSH0 MSTORE PUSH1 0x20 PUSH0 RETURN
717                code: Some(Bytecode::new_raw(bytes!("0x4F5F5260205FF3"))),
718                ..Default::default()
719            },
720        );
721
722        let tx_env = TxEnv {
723            kind: contract.into(),
724            ..Default::default()
725        };
726        let result = tempo_evm.transact_one(tx_env.into())?;
727
728        if !spec.is_t1c() {
729            assert!(result.is_success());
730            assert_eq!(
731                U256::from_be_slice(result.output().unwrap()),
732                U256::from(1000100)
733            );
734        } else {
735            assert!(matches!(
736                result,
737                ExecutionResult::Halt {
738                    reason: TempoHaltReason::Ethereum(HaltReason::OpcodeNotFound),
739                    ..
740                }
741            ));
742        }
743
744        Ok(())
745    }
746
747    #[test]
748    fn test_inspector_calls() -> eyre::Result<()> {
749        // This test calls TIP20 setSupplyCap which emits a SupplyCapUpdate log event
750        let caller = Address::repeat_byte(0x01);
751        let contract = Address::repeat_byte(0x42);
752
753        let input_bytes = ITIP20::setSupplyCapCall {
754            newSupplyCap: U256::from(100),
755        }
756        .abi_encode();
757
758        // Create bytecode that calls setSupplyCap(uint256 newSupplyCap) on PATH_USD
759        // it is 36 bytes long
760        let mut bytecode_bytes = vec![];
761
762        for (i, &byte) in input_bytes.iter().enumerate() {
763            bytecode_bytes.extend_from_slice(&[
764                opcode::PUSH1,
765                byte,
766                opcode::PUSH1,
767                i as u8,
768                opcode::MSTORE8,
769            ]);
770        }
771
772        // CALL to PATH_USD precompile
773        // CALL(gas, addr, value, argsOffset, argsSize, retOffset, retSize)
774        bytecode_bytes.extend_from_slice(&[
775            opcode::PUSH1,
776            0x00, // retSize
777            opcode::PUSH1,
778            0x00, // retOffset
779            opcode::PUSH1,
780            0x24, // argsSize (4 + 32 = 36 = 0x24)
781            opcode::PUSH1,
782            0x00, // argsOffset
783            opcode::PUSH1,
784            0x00, // value = 0
785        ]);
786
787        // PUSH20 PATH_USD_ADDRESS
788        bytecode_bytes.push(opcode::PUSH20);
789        bytecode_bytes.extend_from_slice(PATH_USD_ADDRESS.as_slice());
790
791        bytecode_bytes.extend_from_slice(&[
792            opcode::PUSH2,
793            0xFF,
794            0xFF, // gas
795            opcode::CALL,
796            opcode::POP, // pop success/failure
797            opcode::STOP,
798        ]);
799
800        let bytecode = Bytecode::new_raw(bytecode_bytes.into());
801
802        // Set up EVM with TIP20 infrastructure
803        let mut evm = create_evm_with_inspector(CountInspector::new());
804        // Set up TIP20 using the storage context pattern
805        {
806            let ctx = &mut evm.ctx;
807            let internals =
808                EvmInternals::new(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, &ctx.tx);
809
810            let mut storage = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
811            StorageCtx::enter(&mut storage, || {
812                TIP20Setup::path_usd(caller)
813                    .with_issuer(caller)
814                    .with_admin(contract) // Grant admin role to contract so it can call setSupplyCap
815                    .apply()
816            })?;
817        }
818
819        // Deploy the contract bytecode
820        evm.ctx.db_mut().insert_account_info(
821            contract,
822            AccountInfo {
823                code: Some(bytecode),
824                ..Default::default()
825            },
826        );
827
828        // Execute a call to the contract
829        let tx_env = TxEnv {
830            caller,
831            kind: TxKind::Call(contract),
832            gas_limit: 1_000_000,
833            ..Default::default()
834        };
835        let result = evm
836            .inspect_tx(tx_env.into())
837            .expect("execution should succeed");
838
839        assert!(result.result.is_success());
840
841        // Verify that a SupplyCapUpdate log was emitted by the TIP20 precompile
842        assert_eq!(result.result.logs().len(), 3);
843        // Log should be from TIP20_FACTORY
844        assert_eq!(result.result.logs()[0].address, PATH_USD_ADDRESS);
845
846        // Get the inspector and verify counts
847        let inspector = &evm.inspector;
848
849        // Verify CALL opcode was executed (the call to PATH_USD)
850        assert_eq!(inspector.get_count(opcode::CALL), 1);
851
852        assert_eq!(inspector.get_count(opcode::STOP), 1);
853
854        // Verify log count
855        assert_eq!(inspector.log_count(), 1);
856
857        // Verify call count (initial tx + CALL to PATH_USD)
858        assert_eq!(inspector.call_count(), 2);
859
860        // Should have 2 call ends
861        assert_eq!(inspector.call_end_count(), 2);
862
863        // ==================== Multi-call Tempo transaction test ====================
864        // Test inspector with a Tempo transaction that has multiple calls
865
866        let key_pair = P256KeyPair::random();
867        let tempo_caller = key_pair.address;
868
869        // Create signed authorization for Tempo tx
870        let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
871
872        // Create a transaction with 3 calls to identity precompile
873        let tx = TxBuilder::new()
874            .call_identity(&[0x01, 0x02])
875            .call_identity(&[0x03, 0x04])
876            .call_identity(&[0x05, 0x06])
877            .authorization(signed_auth)
878            .build();
879
880        let signed_tx = key_pair.sign_tx(tx)?;
881        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, tempo_caller);
882
883        // Create a new EVM with fresh inspector for multi-call test
884        let mut multi_evm = create_evm_with_inspector(CountInspector::new());
885        multi_evm.ctx.db_mut().insert_account_info(
886            tempo_caller,
887            AccountInfo {
888                balance: U256::from(DEFAULT_BALANCE),
889                ..Default::default()
890            },
891        );
892
893        // Execute the multi-call transaction with inspector
894        let multi_result = multi_evm.inspect_tx(tx_env)?;
895        assert!(multi_result.result.is_success(),);
896
897        // Verify inspector tracked all 3 calls
898        let multi_inspector = &multi_evm.inspector;
899
900        // Multi-call Tempo transactions execute each call as a separate frame
901        // call_count = 3 (one for each identity precompile call)
902        assert_eq!(multi_inspector.call_count(), 3,);
903        assert_eq!(multi_inspector.call_end_count(), 3,);
904
905        Ok(())
906    }
907
908    #[test]
909    fn test_tempo_tx_initial_gas() -> eyre::Result<()> {
910        let key_pair = P256KeyPair::random();
911        let caller = key_pair.address;
912
913        // Create EVM
914        let mut evm = create_funded_evm(caller);
915        evm.block.basefee = 100_000_000_000;
916
917        // Set up TIP20 first (required for fee token validation)
918        let block = TempoBlockEnv::default();
919        let ctx = &mut evm.ctx;
920        let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
921        let mut provider =
922            EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
923
924        StorageCtx::enter(&mut provider, || {
925            TIP20Setup::path_usd(caller)
926                .with_issuer(caller)
927                .with_mint(caller, U256::from(100_000))
928                .apply()
929        })?;
930
931        drop(provider);
932
933        // First tx: single call
934        let tx1 = TxBuilder::new()
935            .call_identity(&[])
936            .gas_limit(300_000)
937            .with_max_fee_per_gas(200_000_000_000)
938            .with_max_priority_fee_per_gas(0)
939            .build();
940
941        let signed_tx1 = key_pair.sign_tx(tx1)?;
942        let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller);
943
944        let ctx = &mut evm.ctx;
945        let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
946        let mut provider =
947            EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
948
949        let slot = StorageCtx::enter(&mut provider, || {
950            TIP20Token::from_address(PATH_USD_ADDRESS)?.balances[caller].read()
951        })?;
952        drop(provider);
953
954        assert_eq!(slot, U256::from(100_000));
955
956        let result1 = evm.transact_commit(tx_env1)?;
957        assert!(result1.is_success());
958        assert_eq!(result1.tx_gas_used(), 28_671);
959
960        let ctx = &mut evm.ctx;
961        let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
962        let mut provider =
963            EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
964
965        let slot = StorageCtx::enter(&mut provider, || {
966            TIP20Token::from_address(PATH_USD_ADDRESS)?.balances[caller].read()
967        })?;
968        drop(provider);
969
970        assert_eq!(slot, U256::from(97_132));
971
972        // Second tx: two calls
973        let tx2 = TxBuilder::new()
974            .call_identity(&[])
975            .call_identity(&[])
976            .nonce(1)
977            .gas_limit(35_000)
978            .with_max_fee_per_gas(200_000_000_000)
979            .with_max_priority_fee_per_gas(0)
980            .build();
981
982        let signed_tx2 = key_pair.sign_tx(tx2)?;
983        let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller);
984
985        let result2 = evm.transact_commit(tx_env2)?;
986        assert!(result2.is_success());
987        assert_eq!(result2.tx_gas_used(), 31_286);
988
989        let ctx = &mut evm.ctx;
990        let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
991        let mut provider =
992            EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
993
994        let slot = StorageCtx::enter(&mut provider, || {
995            TIP20Token::from_address(PATH_USD_ADDRESS)?.balances[caller].read()
996        })?;
997        drop(provider);
998
999        assert_eq!(slot, U256::from(94_003));
1000
1001        Ok(())
1002    }
1003
1004    /// Test creating and executing a Tempo transaction with:
1005    /// - WebAuthn signature
1006    /// - Authorization list (aa_auth_list)
1007    /// - Two calls to the identity precompile (0x04)
1008    #[test]
1009    fn test_tempo_tx() -> eyre::Result<()> {
1010        let key_pair = P256KeyPair::random();
1011        let caller = key_pair.address;
1012
1013        // Create signed authorization
1014        let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
1015
1016        // Create and sign transaction with two calls to identity precompile
1017        let tx = TxBuilder::new()
1018            .call_identity(&[0x01, 0x02, 0x03, 0x04])
1019            .call_identity(&[0xAA, 0xBB, 0xCC, 0xDD])
1020            .authorization(signed_auth.clone())
1021            .build();
1022
1023        let signed_tx = key_pair.sign_tx(tx)?;
1024        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1025
1026        // Verify transaction has AA auth list
1027        assert!(tx_env.tempo_tx_env.is_some(),);
1028        let tempo_env = tx_env.tempo_tx_env.as_ref().unwrap();
1029        assert_eq!(tempo_env.tempo_authorization_list.len(), 1);
1030        assert_eq!(tempo_env.aa_calls.len(), 2);
1031
1032        // Create EVM with T1C (required for V2 keychain signatures) and execute transaction
1033        let mut evm = create_funded_evm_t1(caller);
1034
1035        // Execute the transaction and commit state changes
1036        let result = evm.transact_commit(tx_env)?;
1037        assert!(result.is_success());
1038
1039        // Test with KeychainSignature using key_authorization to provision the access key
1040        let key_auth = KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, caller);
1041        let key_auth_webauthn_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1042        let signed_key_auth =
1043            key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_webauthn_sig));
1044
1045        // Create transaction with incremented nonce and key_authorization
1046        let tx2 = TxBuilder::new()
1047            .call_identity(&[0x01, 0x02, 0x03, 0x04])
1048            .call_identity(&[0xAA, 0xBB, 0xCC, 0xDD])
1049            .authorization(signed_auth)
1050            .nonce(1)
1051            .gas_limit(1_000_000)
1052            .key_authorization(signed_key_auth)
1053            .build();
1054
1055        let signed_tx = key_pair.sign_tx_keychain(tx2)?;
1056        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1057
1058        // Explicitly test tempo_tx_env.signature.as_keychain()
1059        let tempo_env_keychain = tx_env
1060            .tempo_tx_env
1061            .as_ref()
1062            .expect("Transaction should have tempo_tx_env");
1063        let keychain_sig = tempo_env_keychain
1064            .signature
1065            .as_keychain()
1066            .expect("Signature should be a KeychainSignature");
1067
1068        // Validate KeychainSignature properties
1069        // KeychainSignature user_address should match the caller
1070        assert_eq!(keychain_sig.user_address, caller,);
1071
1072        // Verify the inner signature is WebAuthn
1073        assert!(matches!(
1074            keychain_sig.signature,
1075            PrimitiveSignature::WebAuthn(_)
1076        ));
1077
1078        // Verify key_id recovery works correctly using the transaction signature hash
1079        let recovered_key_id = keychain_sig
1080            .key_id(&tempo_env_keychain.signature_hash)
1081            .expect("Key ID recovery should succeed");
1082        assert_eq!(recovered_key_id, caller,);
1083
1084        // Execute the transaction with keychain signature and commit state changes
1085        let result = evm.transact_commit(tx_env)?;
1086        assert!(result.is_success());
1087
1088        // Test a transaction with a failing call to TIP20 contract with wrong input
1089        let tx_fail = TxBuilder::new()
1090            .call(PATH_USD_ADDRESS, &[0x01, 0x02]) // Too short for TIP20
1091            .nonce(2)
1092            .build();
1093
1094        let signed_tx_fail = key_pair.sign_tx_keychain(tx_fail)?;
1095        let tx_env_fail = TempoTxEnv::from_recovered_tx(&signed_tx_fail, caller);
1096
1097        let result_fail = evm.transact(tx_env_fail)?;
1098        assert!(!result_fail.result.is_success());
1099
1100        // Test 2D nonce transaction (nonce_key > 0)
1101        let nonce_key_2d = U256::from(42);
1102
1103        let tx_2d = TxBuilder::new()
1104            .call_identity(&[0x2D, 0x2D, 0x2D, 0x2D])
1105            .nonce_key(nonce_key_2d)
1106            .build();
1107
1108        let signed_tx_2d = key_pair.sign_tx_keychain(tx_2d)?;
1109        let tx_env_2d = TempoTxEnv::from_recovered_tx(&signed_tx_2d, caller);
1110
1111        assert!(tx_env_2d.tempo_tx_env.is_some());
1112        assert_eq!(
1113            tx_env_2d.tempo_tx_env.as_ref().unwrap().nonce_key,
1114            nonce_key_2d
1115        );
1116
1117        let result_2d = evm.transact_commit(tx_env_2d)?;
1118        assert!(result_2d.is_success());
1119
1120        // Verify 2D nonce was incremented
1121        let nonce_slot = NonceManager::new().nonces[caller][nonce_key_2d].slot();
1122        let stored_nonce = evm
1123            .ctx
1124            .db()
1125            .storage_ref(NONCE_PRECOMPILE_ADDRESS, nonce_slot)
1126            .unwrap_or_default();
1127        assert_eq!(stored_nonce, U256::from(1));
1128
1129        // Test second 2D nonce transaction
1130        let tx_2d_2 = TxBuilder::new()
1131            .call_identity(&[0x2E, 0x2E, 0x2E, 0x2E])
1132            .nonce_key(nonce_key_2d)
1133            .nonce(1)
1134            .build();
1135
1136        let signed_tx_2d_2 = key_pair.sign_tx_keychain(tx_2d_2)?;
1137        let tx_env_2d_2 = TempoTxEnv::from_recovered_tx(&signed_tx_2d_2, caller);
1138
1139        let result_2d_2 = evm.transact_commit(tx_env_2d_2)?;
1140        assert!(result_2d_2.is_success());
1141
1142        // Verify nonce incremented again
1143        let stored_nonce_2 = evm
1144            .ctx
1145            .db()
1146            .storage_ref(NONCE_PRECOMPILE_ADDRESS, nonce_slot)
1147            .unwrap_or_default();
1148        assert_eq!(stored_nonce_2, U256::from(2));
1149
1150        Ok(())
1151    }
1152
1153    #[test]
1154    fn test_t3_key_authorization_deny_all_scopes_blocks_same_tx_call() -> eyre::Result<()> {
1155        let key_pair = P256KeyPair::random();
1156        let caller = key_pair.address;
1157
1158        let mut evm = create_funded_evm_t3(caller);
1159
1160        // Set up TIP20 for fee payment.
1161        let block = TempoBlockEnv::default();
1162        {
1163            let ctx = &mut evm.ctx;
1164            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1165            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
1166
1167            StorageCtx::enter(&mut provider, || {
1168                TIP20Setup::path_usd(caller)
1169                    .with_issuer(caller)
1170                    .with_mint(caller, U256::from(10_000_000))
1171                    .apply()
1172            })?;
1173        }
1174
1175        // Explicit deny-all marker in protocol payload: Some([]).
1176        let key_auth =
1177            KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, caller).with_no_calls();
1178        let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1179        let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
1180
1181        let tx = TxBuilder::new()
1182            .call_identity(&[0x01])
1183            .key_authorization(signed_key_auth)
1184            .gas_limit(5_000_000)
1185            .build();
1186
1187        // Use keychain signature so call-scope validation runs in the same tx.
1188        let signed_tx = key_pair.sign_tx_keychain(tx)?;
1189        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1190
1191        let result = evm.transact_commit(tx_env)?;
1192        assert!(
1193            !result.is_success(),
1194            "deny-all scope should now fail during paid execution"
1195        );
1196        assert!(
1197            result.tx_gas_used() > 0,
1198            "failed execution should still consume gas"
1199        );
1200
1201        Ok(())
1202    }
1203
1204    #[test]
1205    fn test_t3_key_authorization_accepts_empty_recipient_allowlist_as_unconstrained()
1206    -> eyre::Result<()> {
1207        let key_pair = P256KeyPair::random();
1208        let caller = key_pair.address;
1209
1210        let mut evm = create_funded_evm_t3(caller);
1211
1212        let block = TempoBlockEnv::default();
1213        {
1214            let ctx = &mut evm.ctx;
1215            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1216            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
1217
1218            StorageCtx::enter(&mut provider, || {
1219                TIP20Setup::path_usd(caller)
1220                    .with_issuer(caller)
1221                    .with_mint(caller, U256::from(10_000_000))
1222                    .apply()
1223            })?;
1224        }
1225
1226        let transfer_to = Address::repeat_byte(0xaa);
1227        let transfer_input = ITIP20::transferCall {
1228            to: transfer_to,
1229            amount: U256::from(1_u64),
1230        }
1231        .abi_encode();
1232
1233        let key_auth = KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, caller)
1234            .with_allowed_calls(vec![tempo_primitives::transaction::CallScope {
1235                target: PATH_USD_ADDRESS,
1236                selector_rules: vec![tempo_primitives::transaction::SelectorRule {
1237                    selector: ITIP20::transferCall::SELECTOR,
1238                    recipients: Vec::new(),
1239                }],
1240            }]);
1241        let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1242        let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
1243
1244        let tx = TxBuilder::new()
1245            .call(PATH_USD_ADDRESS, &transfer_input)
1246            .key_authorization(signed_key_auth)
1247            .gas_limit(5_000_000)
1248            .build();
1249
1250        let signed_tx = key_pair.sign_tx_keychain(tx)?;
1251        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1252
1253        evm.transact_commit(tx_env)
1254            .expect("empty recipient allowlist should allow the call");
1255
1256        Ok(())
1257    }
1258
1259    #[test]
1260    fn test_same_tx_key_authorization_rejects_key_type_mismatch() -> eyre::Result<()> {
1261        let key_pair = P256KeyPair::random();
1262        let caller = key_pair.address;
1263
1264        let mut evm = create_funded_evm_t3(caller);
1265
1266        let key_auth = KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, caller);
1267        let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1268        let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
1269
1270        let tx = TxBuilder::new()
1271            .call_identity(&[0x01])
1272            .key_authorization(signed_key_auth)
1273            .gas_limit(5_000_000)
1274            .build();
1275
1276        let signed_tx = key_pair.sign_tx_keychain(tx)?;
1277        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1278
1279        let err = evm
1280            .transact_commit(tx_env)
1281            .expect_err("mismatched key_type should reject same-tx auth+use");
1282
1283        assert!(
1284            matches!(
1285                err,
1286                revm::context::result::EVMError::Transaction(
1287                    TempoInvalidTransaction::KeychainValidationFailed { .. }
1288                )
1289            ),
1290            "expected KeychainValidationFailed, got: {err:?}"
1291        );
1292
1293        Ok(())
1294    }
1295
1296    /// Test that Tempo transaction time window validation works correctly.
1297    /// Tests `valid_after` and `valid_before` fields against block timestamp.
1298    #[test]
1299    fn test_tempo_tx_time_window() -> eyre::Result<()> {
1300        let key_pair = P256KeyPair::random();
1301        let caller = key_pair.address;
1302
1303        // Create signed authorization
1304        let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
1305
1306        // Helper to create and sign a transaction with time window parameters
1307        let create_signed_tx = |valid_after: Option<u64>, valid_before: Option<u64>| {
1308            let tx = TxBuilder::new()
1309                .call_identity(&[0x01, 0x02, 0x03, 0x04])
1310                .authorization(signed_auth.clone())
1311                .valid_after(valid_after)
1312                .valid_before(valid_before)
1313                .build();
1314            key_pair.sign_tx(tx)
1315        };
1316
1317        // Test case 1: Transaction fails when block_timestamp < valid_after
1318        {
1319            let mut evm = create_funded_evm_with_timestamp(caller, 100);
1320            let signed_tx = create_signed_tx(Some(200), None)?;
1321            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1322
1323            let result = evm.transact(tx_env);
1324            assert!(result.is_err());
1325            let err = result.unwrap_err();
1326            assert!(
1327                matches!(
1328                    err,
1329                    revm::context::result::EVMError::Transaction(
1330                        TempoInvalidTransaction::ValidAfter {
1331                            current: 100,
1332                            valid_after: 200
1333                        }
1334                    )
1335                ),
1336                "Expected ValidAfter error, got: {err:?}"
1337            );
1338        }
1339
1340        // Test case 2: Transaction fails when block_timestamp >= valid_before
1341        {
1342            let mut evm = create_funded_evm_with_timestamp(caller, 200);
1343            let signed_tx = create_signed_tx(None, Some(200))?;
1344            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1345
1346            let result = evm.transact(tx_env);
1347            assert!(result.is_err());
1348            let err = result.unwrap_err();
1349            assert!(
1350                matches!(
1351                    err,
1352                    revm::context::result::EVMError::Transaction(
1353                        TempoInvalidTransaction::ValidBefore {
1354                            current: 200,
1355                            valid_before: 200
1356                        }
1357                    )
1358                ),
1359                "Expected ValidBefore error, got: {err:?}"
1360            );
1361        }
1362
1363        // Test case 3: Transaction fails when block_timestamp > valid_before
1364        {
1365            let mut evm = create_funded_evm_with_timestamp(caller, 300);
1366            let signed_tx = create_signed_tx(None, Some(200))?;
1367            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1368
1369            let result = evm.transact(tx_env);
1370            assert!(result.is_err());
1371            let err = result.unwrap_err();
1372            assert!(
1373                matches!(
1374                    err,
1375                    revm::context::result::EVMError::Transaction(
1376                        TempoInvalidTransaction::ValidBefore {
1377                            current: 300,
1378                            valid_before: 200
1379                        }
1380                    )
1381                ),
1382                "Expected ValidBefore error, got: {err:?}"
1383            );
1384        }
1385
1386        // Test case 4: Transaction succeeds when exactly at valid_after boundary
1387        {
1388            let mut evm = create_funded_evm_with_timestamp(caller, 200);
1389            let signed_tx = create_signed_tx(Some(200), None)?;
1390            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1391
1392            let result = evm.transact(tx_env)?;
1393            assert!(result.result.is_success());
1394        }
1395
1396        // Test case 5: Transaction succeeds when within time window
1397        {
1398            let mut evm = create_funded_evm_with_timestamp(caller, 150);
1399            let signed_tx = create_signed_tx(Some(100), Some(200))?;
1400            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1401
1402            let result = evm.transact(tx_env)?;
1403            assert!(result.result.is_success());
1404        }
1405
1406        // Test case 6: Transaction fails when block_timestamp < valid_after in a window
1407        {
1408            let mut evm = create_funded_evm_with_timestamp(caller, 50);
1409            let signed_tx = create_signed_tx(Some(100), Some(200))?;
1410            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1411
1412            let result = evm.transact(tx_env);
1413            assert!(result.is_err());
1414            let err = result.unwrap_err();
1415            assert!(
1416                matches!(
1417                    err,
1418                    revm::context::result::EVMError::Transaction(
1419                        TempoInvalidTransaction::ValidAfter {
1420                            current: 50,
1421                            valid_after: 100
1422                        }
1423                    )
1424                ),
1425                "Expected ValidAfter error, got: {err:?}"
1426            );
1427        }
1428
1429        // Test case 7: Transaction fails when block_timestamp >= valid_before in a window
1430        {
1431            let mut evm = create_funded_evm_with_timestamp(caller, 200);
1432            let signed_tx = create_signed_tx(Some(100), Some(200))?;
1433            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1434
1435            let result = evm.transact(tx_env);
1436            assert!(result.is_err());
1437            let err = result.unwrap_err();
1438            assert!(
1439                matches!(
1440                    err,
1441                    revm::context::result::EVMError::Transaction(
1442                        TempoInvalidTransaction::ValidBefore {
1443                            current: 200,
1444                            valid_before: 200
1445                        }
1446                    )
1447                ),
1448                "Expected ValidBefore error, got: {err:?}"
1449            );
1450        }
1451
1452        Ok(())
1453    }
1454
1455    /// Test executing a Tempo transaction where the first call is a Create kind.
1456    /// This should succeed as CREATE is allowed as the first call.
1457    #[test]
1458    fn test_tempo_tx_create_first_call() -> eyre::Result<()> {
1459        let key_pair = P256KeyPair::random();
1460        let caller = key_pair.address;
1461
1462        // Simple contract that just returns: PUSH1 0x00 PUSH1 0x00 RETURN
1463        let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
1464
1465        // Create transaction with CREATE as first call (no authorization list)
1466        let tx = TxBuilder::new()
1467            .create(&initcode)
1468            .call_identity(&[0x01, 0x02])
1469            .gas_limit(200_000)
1470            .build();
1471
1472        let signed_tx = key_pair.sign_tx(tx)?;
1473        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1474
1475        // Create EVM and execute
1476        let mut evm = create_funded_evm(caller);
1477        let result = evm.transact_commit(tx_env)?;
1478
1479        assert!(result.is_success(), "CREATE as first call should succeed");
1480
1481        Ok(())
1482    }
1483
1484    /// Test that a Tempo transaction fails when CREATE is the second call.
1485    /// CREATE must be the first call if used.
1486    #[test]
1487    fn test_tempo_tx_create_second_call_fails() -> eyre::Result<()> {
1488        let key_pair = P256KeyPair::random();
1489        let caller = key_pair.address;
1490
1491        // Simple initcode
1492        let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
1493
1494        // Create transaction with a regular call first, then CREATE second
1495        let tx = TxBuilder::new()
1496            .call_identity(&[0x01, 0x02])
1497            .create(&initcode)
1498            .gas_limit(200_000)
1499            .build();
1500
1501        let signed_tx = key_pair.sign_tx(tx)?;
1502        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1503
1504        // Create EVM and execute - should fail validation
1505        let mut evm = create_funded_evm(caller);
1506        let result = evm.transact(tx_env);
1507
1508        assert!(result.is_err(), "CREATE as second call should fail");
1509        let err = result.unwrap_err();
1510        assert!(
1511            matches!(
1512                err,
1513                revm::context::result::EVMError::Transaction(
1514                    TempoInvalidTransaction::CallsValidation(msg)
1515                ) if msg.contains("first call")
1516            ),
1517            "Expected CallsValidation error about 'first call', got: {err:?}"
1518        );
1519
1520        Ok(())
1521    }
1522
1523    /// Test validate_aa_initial_tx_gas error cases.
1524    /// Tests all error paths in the AA initial transaction gas validation:
1525    /// - CreateInitCodeSizeLimit: when initcode exceeds max size
1526    /// - ValueTransferNotAllowedInAATx: when a call has non-zero value
1527    /// - CallGasCostMoreThanGasLimit: when gas_limit < intrinsic_gas
1528    #[test]
1529    fn test_validate_aa_initial_tx_gas_errors() -> eyre::Result<()> {
1530        use revm::{context::result::EVMError, handler::Handler};
1531
1532        use crate::handler::TempoEvmHandler;
1533
1534        let key_pair = P256KeyPair::random();
1535        let caller = key_pair.address;
1536
1537        // Helper to create EVM with signed transaction
1538        let create_evm_with_tx =
1539            |tx: TempoTransaction| -> eyre::Result<TempoEvm<CacheDB<EmptyDB>, ()>> {
1540                let signed_tx = key_pair.sign_tx(tx)?;
1541                let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1542                let mut evm = create_funded_evm(caller);
1543                evm.ctx.tx = tx_env;
1544                Ok(evm)
1545            };
1546
1547        let handler = TempoEvmHandler::default();
1548
1549        // Test 1: CreateInitCodeSizeLimit - initcode exceeds max size
1550        {
1551            // Default max initcode size is 49152 bytes (2 * MAX_CODE_SIZE)
1552            let oversized_initcode = vec![0x60; 50_000];
1553
1554            let mut evm = create_evm_with_tx(
1555                TxBuilder::new()
1556                    .create(&oversized_initcode)
1557                    .gas_limit(10_000_000)
1558                    .build(),
1559            )?;
1560
1561            let result = handler.validate_initial_tx_gas(&mut evm);
1562            assert!(
1563                matches!(
1564                    result,
1565                    Err(EVMError::Transaction(
1566                        TempoInvalidTransaction::EthInvalidTransaction(
1567                            revm::context::result::InvalidTransaction::CreateInitCodeSizeLimit
1568                        )
1569                    ))
1570                ),
1571                "Expected CreateInitCodeSizeLimit error, got: {result:?}"
1572            );
1573        }
1574
1575        // Test 2: ValueTransferNotAllowedInAATx - call has non-zero value
1576        {
1577            let mut evm = create_evm_with_tx(
1578                TxBuilder::new()
1579                    .call_with_value(IDENTITY_PRECOMPILE, &[0x01, 0x02], U256::from(1000))
1580                    .build(),
1581            )?;
1582
1583            let result = handler.validate_initial_tx_gas(&mut evm);
1584            assert!(
1585                matches!(
1586                    result,
1587                    Err(EVMError::Transaction(
1588                        TempoInvalidTransaction::ValueTransferNotAllowedInAATx
1589                    ))
1590                ),
1591                "Expected ValueTransferNotAllowedInAATx error, got: {result:?}"
1592            );
1593        }
1594
1595        // Test 3: CallGasCostMoreThanGasLimit - gas_limit < intrinsic_gas
1596        {
1597            let mut evm = create_evm_with_tx(
1598                TxBuilder::new()
1599                    .call_identity(&[0x01, 0x02, 0x03, 0x04])
1600                    .gas_limit(1000) // Way too low, intrinsic cost is at least 21000
1601                    .build(),
1602            )?;
1603
1604            let result = handler.validate_initial_tx_gas(&mut evm);
1605            assert!(
1606                matches!(
1607                    result,
1608                    Err(EVMError::Transaction(
1609                        TempoInvalidTransaction::EthInvalidTransaction(
1610                            InvalidTransaction::CallGasCostMoreThanGasLimit {
1611                                gas_limit: 1000,
1612                                initial_gas
1613                            }
1614                        )
1615                    )) if initial_gas > 1000
1616                ),
1617                "Expected CallGasCostMoreThanGasLimit error, got: {result:?}"
1618            );
1619        }
1620
1621        // Test 4: gas_limit < floor_gas (EIP-7623)
1622        // For AA transactions, intrinsic gas is higher than for standard txs, so with
1623        // gas_limit=31000 the intrinsic gas check fires first (CallGasCostMoreThanGasLimit).
1624        // The floor gas error (GasFloorMoreThanGasLimit) would only appear if gas_limit
1625        // were between intrinsic_gas and floor_gas, but AA intrinsic gas already exceeds
1626        // both values here.
1627        {
1628            let large_calldata = vec![0x42; 1000]; // 1000 non-zero bytes = 1000 tokens
1629
1630            let mut evm = create_evm_with_tx(
1631                TxBuilder::new()
1632                    .call_identity(&large_calldata)
1633                    .gas_limit(31_000)
1634                    .build(),
1635            )?;
1636
1637            let result = handler.validate_initial_tx_gas(&mut evm);
1638
1639            assert!(
1640                matches!(
1641                    result,
1642                    Err(EVMError::Transaction(
1643                        TempoInvalidTransaction::EthInvalidTransaction(
1644                            InvalidTransaction::CallGasCostMoreThanGasLimit {
1645                                gas_limit: 31_000,
1646                                initial_gas
1647                            }
1648                        )
1649                    )) if initial_gas > 31_000
1650                ),
1651                "Expected CallGasCostMoreThanGasLimit, got: {result:?}"
1652            );
1653        }
1654
1655        // Test 5: Success when gas_limit >= both initial_gas and floor_gas
1656        // Verifies floor_gas > initial_gas for large calldata (EIP-7623 scenario)
1657        {
1658            let large_calldata = vec![0x42; 1000];
1659
1660            let mut evm = create_evm_with_tx(
1661                TxBuilder::new()
1662                    .call_identity(&large_calldata)
1663                    .gas_limit(1_000_000) // Plenty of gas for both initial and floor
1664                    .build(),
1665            )?;
1666
1667            let result = handler.validate_initial_tx_gas(&mut evm);
1668            assert!(
1669                result.is_ok(),
1670                "Expected success with sufficient gas, got: {result:?}"
1671            );
1672
1673            let gas = result.unwrap();
1674            // Verify floor_gas > initial_total_gas for this calldata (EIP-7623 scenario)
1675            assert!(
1676                gas.floor_gas > gas.initial_total_gas,
1677                "Expected floor_gas ({}) > initial_total_gas ({}) for large calldata",
1678                gas.floor_gas,
1679                gas.initial_total_gas
1680            );
1681        }
1682
1683        // Test 6: Success case - sufficient gas provided (small calldata)
1684        {
1685            let mut evm = create_evm_with_tx(
1686                TxBuilder::new()
1687                    .call_identity(&[0x01, 0x02, 0x03, 0x04])
1688                    .gas_limit(1_000_000)
1689                    .build(),
1690            )?;
1691
1692            let result = handler.validate_initial_tx_gas(&mut evm);
1693            assert!(result.is_ok(), "Expected success, got: {result:?}");
1694
1695            let gas = result.unwrap();
1696            assert!(
1697                gas.initial_total_gas >= 21_000,
1698                "Initial gas should be at least 21k base"
1699            );
1700        }
1701
1702        Ok(())
1703    }
1704
1705    // ==================== TIP-1000 EVM Configuration Tests ====================
1706
1707    /// Test AA transaction gas usage for simple identity precompile call.
1708    /// This establishes a baseline for gas comparison.
1709    /// Uses T1 hardfork for TIP-1000 gas costs.
1710    #[test]
1711    fn test_aa_tx_gas_baseline_identity_call() -> eyre::Result<()> {
1712        let key_pair = P256KeyPair::random();
1713        let caller = key_pair.address;
1714
1715        let mut evm = create_funded_evm_t1(caller);
1716
1717        // Simple call to identity precompile
1718        // T1 adds 250k for new account creation (nonce == 0)
1719        let tx = TxBuilder::new()
1720            .call_identity(&[0x01, 0x02, 0x03, 0x04])
1721            .gas_limit(500_000)
1722            .build();
1723
1724        let signed_tx = key_pair.sign_tx(tx)?;
1725        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1726
1727        let result = evm.transact_commit(tx_env)?;
1728        assert!(result.is_success());
1729
1730        // With T1 TIP-1000: new account cost (250k) + base intrinsic (21k) + WebAuthn (~3.4k) + calldata
1731        let gas_used = result.tx_gas_used();
1732        assert_eq!(
1733            gas_used, 278738,
1734            "T1 baseline identity call gas should be exact"
1735        );
1736
1737        Ok(())
1738    }
1739
1740    /// Test AA transaction gas usage with SSTORE to a new storage slot.
1741    /// This tests TIP-1000's increased SSTORE cost (250,000 gas for new slot).
1742    /// Uses T1 hardfork for TIP-1000 gas costs.
1743    #[test]
1744    fn test_aa_tx_gas_sstore_new_slot() -> eyre::Result<()> {
1745        let key_pair = P256KeyPair::random();
1746        let caller = key_pair.address;
1747        let contract = Address::repeat_byte(0x55);
1748
1749        let mut evm = create_funded_evm_t1(caller);
1750
1751        // Deploy contract that does SSTORE to slot 0:
1752        // PUSH1 0x42 PUSH1 0x00 SSTORE STOP
1753        // This stores value 0x42 at slot 0
1754        let sstore_bytecode = Bytecode::new_raw(bytes!("60426000555B00"));
1755        evm.ctx.db_mut().insert_account_info(
1756            contract,
1757            AccountInfo {
1758                code: Some(sstore_bytecode),
1759                ..Default::default()
1760            },
1761        );
1762
1763        // T1 costs: new account (250k) + SSTORE new slot (250k) + base costs
1764        let tx = TxBuilder::new()
1765            .call(contract, &[])
1766            .gas_limit(600_000)
1767            .build();
1768
1769        let signed_tx = key_pair.sign_tx(tx)?;
1770        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1771
1772        let result = evm.transact_commit(tx_env)?;
1773        assert!(result.is_success(), "SSTORE transaction should succeed");
1774
1775        // With TIP-1000: new account (250k) + SSTORE to new slot (250k) + base costs
1776        let gas_used = result.tx_gas_used();
1777        assert_eq!(
1778            gas_used, 530863,
1779            "T1 SSTORE to new slot gas should be exact"
1780        );
1781
1782        Ok(())
1783    }
1784
1785    /// Test AA transaction gas usage with SSTORE to an existing storage slot (warm).
1786    /// Warm SSTORE should be much cheaper than cold SSTORE to a new slot.
1787    /// Uses T1 hardfork for TIP-1000 gas costs.
1788    #[test]
1789    fn test_aa_tx_gas_sstore_warm_slot() -> eyre::Result<()> {
1790        let key_pair = P256KeyPair::random();
1791        let caller = key_pair.address;
1792        let contract = Address::repeat_byte(0x56);
1793
1794        let mut evm = create_funded_evm_t1(caller);
1795
1796        // Deploy contract that does SSTORE to slot 0:
1797        // PUSH1 0x42 PUSH1 0x00 SSTORE STOP
1798        let sstore_bytecode = Bytecode::new_raw(bytes!("60426000555B00"));
1799        evm.ctx.db_mut().insert_account_info(
1800            contract,
1801            AccountInfo {
1802                code: Some(sstore_bytecode),
1803                ..Default::default()
1804            },
1805        );
1806
1807        // Pre-populate storage slot 0 with a non-zero value
1808        evm.ctx
1809            .db_mut()
1810            .insert_account_storage(contract, U256::ZERO, U256::from(1))
1811            .unwrap();
1812
1813        // T1 costs: new account (250k) + SSTORE reset (not new slot) + base costs
1814        let tx = TxBuilder::new()
1815            .call(contract, &[])
1816            .gas_limit(500_000)
1817            .build();
1818
1819        let signed_tx = key_pair.sign_tx(tx)?;
1820        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1821
1822        let result = evm.transact_commit(tx_env)?;
1823        assert!(
1824            result.is_success(),
1825            "SSTORE to existing slot should succeed"
1826        );
1827
1828        // SSTORE to existing non-zero slot (reset) doesn't trigger the 250k new slot cost
1829        // But still has new account cost (250k) + cold SLOAD (2100) + warm SSTORE reset (~2900)
1830        let gas_used = result.tx_gas_used();
1831        assert_eq!(
1832            gas_used, 283663,
1833            "T1 SSTORE to existing slot gas should be exact"
1834        );
1835
1836        Ok(())
1837    }
1838
1839    /// Test AA transaction gas comparison: multiple SSTORE operations.
1840    /// Uses T1 hardfork for TIP-1000 gas costs.
1841    #[test]
1842    fn test_aa_tx_gas_multiple_sstores() -> eyre::Result<()> {
1843        let key_pair = P256KeyPair::random();
1844        let caller = key_pair.address;
1845        let contract = Address::repeat_byte(0x57);
1846
1847        let mut evm = create_funded_evm_t1(caller);
1848
1849        // Deploy contract that does 2 SSTOREs to different slots:
1850        // PUSH1 0x11 PUSH1 0x00 SSTORE  (store 0x11 at slot 0)
1851        // PUSH1 0x22 PUSH1 0x01 SSTORE  (store 0x22 at slot 1)
1852        // STOP
1853        let multi_sstore_bytecode = Bytecode::new_raw(bytes!("601160005560226001555B00"));
1854        evm.ctx.db_mut().insert_account_info(
1855            contract,
1856            AccountInfo {
1857                code: Some(multi_sstore_bytecode),
1858                ..Default::default()
1859            },
1860        );
1861
1862        // T1 costs: new account (250k) + 2 SSTORE new slots (2 * 250k) + base costs
1863        let tx = TxBuilder::new()
1864            .call(contract, &[])
1865            .gas_limit(1_000_000)
1866            .build();
1867
1868        let signed_tx = key_pair.sign_tx(tx)?;
1869        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1870
1871        let result = evm.transact_commit(tx_env)?;
1872        assert!(
1873            result.is_success(),
1874            "Multiple SSTORE transaction should succeed"
1875        );
1876
1877        // With TIP-1000: new account (250k) + 2 SSTOREs to new slots (2 * 250k) = 750k + base
1878        let gas_used = result.tx_gas_used();
1879        assert_eq!(gas_used, 783069, "T1 multiple SSTOREs gas should be exact");
1880
1881        Ok(())
1882    }
1883
1884    /// Test AA transaction gas for contract creation (CREATE).
1885    /// TIP-1000 increases TX create cost to 500,000 and new account cost to 250,000.
1886    /// Uses T1 hardfork for TIP-1000 gas costs.
1887    #[test]
1888    fn test_aa_tx_gas_create_contract() -> eyre::Result<()> {
1889        let key_pair = P256KeyPair::random();
1890        let caller = key_pair.address;
1891
1892        let mut evm = create_funded_evm_t1(caller);
1893
1894        // Simple initcode: PUSH1 0x00 PUSH1 0x00 RETURN (deploys empty contract)
1895        let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
1896
1897        // T1 costs: CREATE cost (500k, fixed upfront contract creation cost) + new account for sender (250k) + base costs
1898        let tx = TxBuilder::new()
1899            .create(&initcode)
1900            .gas_limit(1_000_000)
1901            .build();
1902
1903        let signed_tx = key_pair.sign_tx(tx)?;
1904        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1905
1906        let result = evm.transact_commit(tx_env)?;
1907        assert!(result.is_success(), "CREATE transaction should succeed");
1908
1909        // With TIP-1000: CREATE cost (500k) + new account for sender (250k) + base costs
1910        let gas_used = result.tx_gas_used();
1911        assert_eq!(gas_used, 778720, "T1 CREATE contract gas should be exact");
1912
1913        Ok(())
1914    }
1915
1916    /// TIP-1016: generic EVM CREATE charges deployed-bytecode HASH_COST(L)
1917    /// in addition to CREATE base gas and code deposit gas on the success path.
1918    #[test]
1919    fn test_t4_create_tx_charges_hash_cost() -> eyre::Result<()> {
1920        let key_pair = P256KeyPair::random();
1921        let caller = key_pair.address;
1922
1923        // Initcode that returns a 1-byte runtime (`STOP`), so HASH_COST(L) = 6.
1924        let tx = TxBuilder::new()
1925            .create(&hex!("6001600c60003960016000f300"))
1926            .gas_limit(1_000_000)
1927            .build();
1928        let signed_tx = key_pair.sign_tx(tx)?;
1929
1930        let run_create = |without_word_cost: bool| -> eyre::Result<u64> {
1931            let mut evm = create_funded_evm_t4(caller);
1932            if without_word_cost {
1933                evm.ctx.cfg.gas_params.override_gas(vec![(
1934                    revm::context_interface::cfg::GasId::keccak256_per_word(),
1935                    0,
1936                )]);
1937            }
1938
1939            let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_tx, caller))?;
1940            assert!(
1941                result.is_success(),
1942                "T4 CREATE transaction should succeed with keccak256_per_word={without_word_cost:?}"
1943            );
1944            Ok(result.tx_gas_used())
1945        };
1946
1947        assert_eq!(
1948            run_create(false)? - run_create(true)?, // gas_with_hash - gas_without_hash (test fixture)
1949            tempo_gas_params(TempoHardfork::T4).keccak256_cost(1),
1950            "generic CREATE should add HASH_COST(L) on top of the non-hash baseline"
1951        );
1952        Ok(())
1953    }
1954
1955    /// Test AA transaction gas for CREATE with 2D nonce (nonce_key != 0).
1956    /// When caller account nonce is 0, an additional 250k gas is charged for account creation.
1957    /// Uses T1 hardfork for TIP-1000 gas costs.
1958    #[test]
1959    fn test_aa_tx_gas_create_with_2d_nonce() -> eyre::Result<()> {
1960        let key_pair = P256KeyPair::random();
1961        let caller = key_pair.address;
1962
1963        let mut evm = create_funded_evm_t1(caller);
1964
1965        // Simple initcode: PUSH1 0x00 PUSH1 0x00 RETURN (deploys empty contract)
1966        let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
1967        let nonce_key_2d = U256::from(42);
1968
1969        // Test 1: CREATE tx with 2D nonce, caller account nonce = 0
1970        // Should include: CREATE cost (500k) + new account for sender (250k) + 2D nonce sender creation (250k)
1971        let tx1 = TxBuilder::new()
1972            .create(&initcode)
1973            .nonce_key(nonce_key_2d)
1974            .gas_limit(2_000_000)
1975            .build();
1976
1977        // Verify that account nonce is 0 before transaction
1978        assert_eq!(
1979            evm.ctx
1980                .db()
1981                .basic_ref(caller)
1982                .ok()
1983                .flatten()
1984                .map(|a| a.nonce)
1985                .unwrap_or(0),
1986            0,
1987            "Caller account nonce should be 0 before first tx"
1988        );
1989
1990        let signed_tx1 = key_pair.sign_tx(tx1)?;
1991        let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller);
1992
1993        let result1 = evm.transact_commit(tx_env1)?;
1994        assert!(result1.is_success(), "CREATE with 2D nonce should succeed");
1995
1996        // With TIP-1000: CREATE cost (500k) + new account (250k) + 2D nonce sender creation (250k) + base
1997        assert_eq!(
1998            result1.tx_gas_used(),
1999            1028720,
2000            "T1 CREATE with 2D nonce (caller.nonce=0) gas should be exact"
2001        );
2002
2003        // Test 2: Second CREATE tx with 2D nonce (different nonce_key)
2004        // Caller account nonce is now 1, so no extra 250k for caller account creation
2005        // Should include: CREATE cost (500k) + new account for sender (250k from nonce==0 check)
2006        // but NOT the extra 250k for 2D nonce caller creation since account.nonce != 0
2007        let nonce_key_2d_2 = U256::from(43);
2008        let tx2 = TxBuilder::new()
2009            .create(&initcode)
2010            .nonce_key(nonce_key_2d_2)
2011            .nonce(0) // 2D nonce = 0 (new key, starts at 0)
2012            .gas_limit(2_000_000)
2013            .build();
2014
2015        let signed_tx2 = key_pair.sign_tx(tx2)?;
2016        let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller);
2017
2018        let result2 = evm.transact_commit(tx_env2)?;
2019        assert!(
2020            result2.is_success(),
2021            "Second CREATE with 2D nonce should succeed"
2022        );
2023
2024        // With TIP-1000: CREATE cost (500k) + new account (250k) + base (no extra 250k since caller.nonce != 0)
2025        assert_eq!(
2026            result2.tx_gas_used(),
2027            778720,
2028            "T1 CREATE with 2D nonce (caller.nonce=1) gas should be exact"
2029        );
2030
2031        // Verify the gas difference is exactly 250,000 (new_account_cost)
2032        let gas_difference = result1.tx_gas_used() - result2.tx_gas_used();
2033        assert_eq!(
2034            gas_difference, 250_000,
2035            "Gas difference should be exactly new_account_cost (250,000), got {gas_difference:?}",
2036        );
2037
2038        Ok(())
2039    }
2040
2041    /// Test that CREATE with expiring nonce charges 250k new_account_cost when caller.nonce == 0.
2042    /// This validates the fix for audit issue #182.
2043    #[test]
2044    fn test_aa_tx_gas_create_with_expiring_nonce() -> eyre::Result<()> {
2045        use tempo_primitives::transaction::TEMPO_EXPIRING_NONCE_KEY;
2046
2047        let key_pair = P256KeyPair::random();
2048        let caller = key_pair.address;
2049        let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3]; // PUSH0 PUSH0 RETURN
2050        let timestamp = 1000u64;
2051        let valid_before = timestamp + 30;
2052
2053        // CREATE with caller.nonce == 0 (should charge extra 250k)
2054        let mut evm1 = create_funded_evm_t1_with_timestamp(caller, timestamp);
2055        let tx1 = TxBuilder::new()
2056            .create(&initcode)
2057            .nonce_key(TEMPO_EXPIRING_NONCE_KEY)
2058            .valid_before(Some(valid_before))
2059            .gas_limit(2_000_000)
2060            .build();
2061        let result1 = evm1.transact_commit(TempoTxEnv::from_recovered_tx(
2062            &key_pair.sign_tx(tx1)?,
2063            caller,
2064        ))?;
2065        assert!(result1.is_success());
2066        let gas_nonce_zero = result1.tx_gas_used();
2067
2068        // CREATE with caller.nonce == 1 (no extra 250k)
2069        let mut evm2 = create_funded_evm_t1_with_timestamp(caller, timestamp);
2070        evm2.ctx.db_mut().insert_account_info(
2071            caller,
2072            AccountInfo {
2073                balance: U256::from(DEFAULT_BALANCE),
2074                nonce: 1,
2075                ..Default::default()
2076            },
2077        );
2078        let tx2 = TxBuilder::new()
2079            .create(&initcode)
2080            .nonce_key(TEMPO_EXPIRING_NONCE_KEY)
2081            .valid_before(Some(valid_before))
2082            .gas_limit(2_000_000)
2083            .build();
2084        let result2 = evm2.transact_commit(TempoTxEnv::from_recovered_tx(
2085            &key_pair.sign_tx(tx2)?,
2086            caller,
2087        ))?;
2088        assert!(result2.is_success());
2089        let gas_nonce_one = result2.tx_gas_used();
2090
2091        // The fix adds 250k when caller.nonce == 0 for CREATE with non-zero nonce_key
2092        assert_eq!(
2093            gas_nonce_zero - gas_nonce_one,
2094            250_000,
2095            "new_account_cost not charged"
2096        );
2097
2098        Ok(())
2099    }
2100
2101    /// Test gas comparison between single call and multiple calls.
2102    /// Uses T1 hardfork for TIP-1000 gas costs.
2103    #[test]
2104    fn test_aa_tx_gas_single_vs_multiple_calls() -> eyre::Result<()> {
2105        let key_pair = P256KeyPair::random();
2106        let caller = key_pair.address;
2107
2108        // Test 1: Single call
2109        // T1 costs: new account (250k) + base costs
2110        let mut evm1 = create_funded_evm_t1(caller);
2111        let tx1 = TxBuilder::new()
2112            .call_identity(&[0x01, 0x02, 0x03, 0x04])
2113            .gas_limit(500_000)
2114            .build();
2115
2116        let signed_tx1 = key_pair.sign_tx(tx1)?;
2117        let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller);
2118        let result1 = evm1.transact_commit(tx_env1)?;
2119        assert!(result1.is_success());
2120        let gas_single = result1.tx_gas_used();
2121
2122        // Test 2: Three calls
2123        // T1 costs: new account (250k) + 3 calls overhead
2124        let mut evm2 = create_funded_evm_t1(caller);
2125        let tx2 = TxBuilder::new()
2126            .call_identity(&[0x01, 0x02, 0x03, 0x04])
2127            .call_identity(&[0x05, 0x06, 0x07, 0x08])
2128            .call_identity(&[0x09, 0x0A, 0x0B, 0x0C])
2129            .gas_limit(500_000)
2130            .build();
2131
2132        let signed_tx2 = key_pair.sign_tx(tx2)?;
2133        let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller);
2134        let result2 = evm2.transact_commit(tx_env2)?;
2135        assert!(result2.is_success());
2136        let gas_triple = result2.tx_gas_used();
2137
2138        // Three calls should cost more than single call
2139        assert_eq!(gas_single, 278738, "T1 single call gas should be exact");
2140        assert_eq!(gas_triple, 284102, "T1 triple call gas should be exact");
2141        assert!(
2142            gas_triple > gas_single,
2143            "3 calls should cost more than 1 call"
2144        );
2145        assert!(
2146            gas_triple < gas_single * 3,
2147            "3 calls should cost less than 3x single call (base costs shared)"
2148        );
2149
2150        Ok(())
2151    }
2152
2153    /// Test AA transaction gas with SLOAD operation (cold vs warm access).
2154    /// Uses T1 hardfork for TIP-1000 gas costs.
2155    #[test]
2156    fn test_aa_tx_gas_sload_cold_vs_warm() -> eyre::Result<()> {
2157        let key_pair = P256KeyPair::random();
2158        let caller = key_pair.address;
2159        let contract = Address::repeat_byte(0x58);
2160
2161        let mut evm = create_funded_evm_t1(caller);
2162
2163        // Deploy contract that does 2 SLOADs from the same slot:
2164        // PUSH1 0x00 SLOAD POP  (cold SLOAD from slot 0)
2165        // PUSH1 0x00 SLOAD POP  (warm SLOAD from slot 0)
2166        // STOP
2167        let sload_bytecode = Bytecode::new_raw(bytes!("6000545060005450"));
2168        evm.ctx.db_mut().insert_account_info(
2169            contract,
2170            AccountInfo {
2171                code: Some(sload_bytecode),
2172                ..Default::default()
2173            },
2174        );
2175
2176        // Pre-populate storage
2177        evm.ctx
2178            .db_mut()
2179            .insert_account_storage(contract, U256::ZERO, U256::from(0x1234))
2180            .unwrap();
2181
2182        // T1 costs: new account (250k) + SLOAD costs + base costs
2183        let tx = TxBuilder::new()
2184            .call(contract, &[])
2185            .gas_limit(500_000)
2186            .build();
2187
2188        let signed_tx = key_pair.sign_tx(tx)?;
2189        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2190
2191        let result = evm.transact_commit(tx_env)?;
2192        assert!(result.is_success(), "SLOAD transaction should succeed");
2193
2194        // T1 costs: new account (250k) + cold SLOAD (2100) + warm SLOAD (100) + cold account (~2.6k)
2195        let gas_used = result.tx_gas_used();
2196        assert_eq!(gas_used, 280866, "T1 SLOAD cold/warm gas should be exact");
2197
2198        Ok(())
2199    }
2200
2201    // ==================== End TIP-1000 Tests ====================
2202
2203    /// Test system call functions and inspector management.
2204    /// Tests `system_call_one_with_caller`, `inspect_one_system_call_with_caller`, and `set_inspector`.
2205    #[test]
2206    fn test_system_call_and_inspector() -> eyre::Result<()> {
2207        let caller = Address::repeat_byte(0x01);
2208        let contract = Address::repeat_byte(0x42);
2209
2210        // Deploy a simple contract that returns success
2211        // DIFFICULTY NUMBER PUSH1 0x00 PUSH1 0x00 RETURN (returns empty data)
2212        let bytecode = Bytecode::new_raw(bytes!("444360006000F3"));
2213
2214        // Test system_call_one_with_caller (no inspector needed)
2215        {
2216            let mut evm = create_evm();
2217            evm.ctx.db_mut().insert_account_info(
2218                contract,
2219                AccountInfo {
2220                    code: Some(bytecode.clone()),
2221                    ..Default::default()
2222                },
2223            );
2224
2225            let result = evm.system_call_one_with_caller(caller, contract, Bytes::new())?;
2226            assert!(result.is_success());
2227        }
2228
2229        // Test set_inspector and inspect_one_system_call_with_caller
2230        {
2231            let mut evm = create_evm_with_inspector(CountInspector::new());
2232            evm.ctx.db_mut().insert_account_info(
2233                contract,
2234                AccountInfo {
2235                    code: Some(bytecode),
2236                    ..Default::default()
2237                },
2238            );
2239
2240            // Test inspect_one_system_call_with_caller
2241            let result = evm.inspect_one_system_call_with_caller(caller, contract, Bytes::new())?;
2242            assert!(result.is_success());
2243
2244            // Verify inspector was called
2245            assert!(evm.inspector.call_count() > 0,);
2246
2247            // Test set_inspector - replace with a fresh CountInspector
2248            evm.set_inspector(CountInspector::new());
2249
2250            // Verify the new inspector starts fresh
2251            assert_eq!(evm.inspector.call_count(), 0,);
2252
2253            // Run another system call and verify new inspector records it
2254            let result = evm.inspect_one_system_call_with_caller(caller, contract, Bytes::new())?;
2255            assert!(result.is_success());
2256            assert!(evm.inspector.call_count() > 0);
2257        }
2258
2259        Ok(())
2260    }
2261
2262    /// Test that key_authorization works correctly with T1 hardfork.
2263    ///
2264    /// This test verifies the key_authorization flow works in the T1 EVM.
2265    /// It ensures that:
2266    /// 1. Keys are NOT authorized when transaction fails due to insufficient gas
2267    /// 2. Keys ARE authorized when transaction succeeds with sufficient gas
2268    ///
2269    /// Related fix: The handler creates a checkpoint before key_authorization
2270    /// precompile execution and reverts it on OOG. This ensures storage consistency.
2271    #[test]
2272    fn test_key_authorization_t1() -> eyre::Result<()> {
2273        use tempo_precompiles::account_keychain::AccountKeychain;
2274
2275        let key_pair = P256KeyPair::random();
2276        let caller = key_pair.address;
2277
2278        // Create a T1 EVM (the fix only applies to T1)
2279        let mut evm = create_funded_evm_t1(caller);
2280
2281        // Set up TIP20 for fee payment
2282        let block = TempoBlockEnv::default();
2283        {
2284            let ctx = &mut evm.ctx;
2285            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
2286            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
2287
2288            StorageCtx::enter(&mut provider, || {
2289                TIP20Setup::path_usd(caller)
2290                    .with_issuer(caller)
2291                    .with_mint(caller, U256::from(10_000_000))
2292                    .apply()
2293            })?;
2294        }
2295
2296        // ==================== Test 1: INSUFFICIENT gas ====================
2297        // First, try with insufficient gas - key should NOT be authorized
2298
2299        let access_key = P256KeyPair::random();
2300        let key_auth =
2301            KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, access_key.address);
2302        let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
2303        let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
2304
2305        // Verify key does NOT exist before the transaction
2306        {
2307            let ctx = &mut evm.ctx;
2308            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
2309            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
2310
2311            let key_exists = StorageCtx::enter(&mut provider, || {
2312                let keychain = AccountKeychain::default();
2313                keychain.keys[caller][access_key.address].read()
2314            })?;
2315            assert_eq!(
2316                key_exists.expiry, 0,
2317                "Key should not exist before transaction"
2318            );
2319        }
2320
2321        let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
2322
2323        // Insufficient gas - will cause OOG during key_authorization processing
2324        let tx_low_gas = TxBuilder::new()
2325            .call_identity(&[0x01])
2326            .authorization(signed_auth)
2327            .key_authorization(signed_key_auth)
2328            .gas_limit(589_000)
2329            .build();
2330
2331        let signed_tx_low = key_pair.sign_tx(tx_low_gas)?;
2332        let tx_env_low = TempoTxEnv::from_recovered_tx(&signed_tx_low, caller);
2333
2334        // Execute the transaction - it should fail due to insufficient gas
2335        let result_low = evm.transact_commit(tx_env_low);
2336
2337        // Transaction should fail (either rejected or OOG).
2338        // Track whether the nonce was incremented (committed OOG vs validation rejection).
2339        let nonce_incremented = match &result_low {
2340            Ok(result) => {
2341                assert_eq!(
2342                    result.tx_gas_used(),
2343                    589_000,
2344                    "Gas used should be gas limit"
2345                );
2346                assert!(
2347                    !result.is_success(),
2348                    "Transaction with insufficient gas should fail"
2349                );
2350                true // OOG: tx committed, nonce incremented
2351            }
2352            Err(e) => {
2353                // Transaction rejected during validation - must be CallGasCostMoreThanGasLimit
2354                assert!(
2355                    matches!(
2356                        e,
2357                        revm::context::result::EVMError::Transaction(
2358                            TempoInvalidTransaction::EthInvalidTransaction(
2359                                revm::context::result::InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
2360                            )
2361                        )
2362                    ),
2363                    "Expected CallGasCostMoreThanGasLimit, got: {e:?}"
2364                );
2365                false // Validation rejection: nonce NOT incremented
2366            }
2367        };
2368
2369        // CRITICAL: Verify the key was NOT authorized
2370        // This tests that storage changes are properly reverted on failure
2371        {
2372            let ctx = &mut evm.ctx;
2373            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
2374            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
2375
2376            let key_after_fail = StorageCtx::enter(&mut provider, || {
2377                let keychain = AccountKeychain::default();
2378                keychain.keys[caller][access_key.address].read()
2379            })?;
2380
2381            assert_eq!(
2382                key_after_fail,
2383                AuthorizedKey::default(),
2384                "Key should NOT be authorized when transaction fails due to insufficient gas"
2385            );
2386        }
2387
2388        // ==================== Test 2: SUFFICIENT gas ====================
2389        // Now try with sufficient gas - key should be authorized
2390
2391        let access_key2 = P256KeyPair::random();
2392        let key_auth2 =
2393            KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, access_key2.address);
2394        let key_auth_sig2 = key_pair.sign_webauthn(key_auth2.signature_hash().as_slice())?;
2395        let signed_key_auth2 = key_auth2.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig2));
2396
2397        let signed_auth2 = key_pair.create_signed_authorization(Address::repeat_byte(0x43))?;
2398
2399        // Execute transaction with sufficient gas
2400        let next_nonce = if nonce_incremented { 1 } else { 0 };
2401        let tx = TxBuilder::new()
2402            .call_identity(&[0x01])
2403            .authorization(signed_auth2)
2404            .key_authorization(signed_key_auth2)
2405            .nonce(next_nonce)
2406            .gas_limit(1_000_000)
2407            .build();
2408
2409        let signed_tx = key_pair.sign_tx(tx)?;
2410        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2411
2412        let result = evm.transact_commit(tx_env)?;
2413        assert!(result.is_success(), "Transaction should succeed");
2414
2415        // Verify the key was authorized
2416        {
2417            let ctx = &mut evm.ctx;
2418            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
2419            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
2420
2421            let key_after_success = StorageCtx::enter(&mut provider, || {
2422                let keychain = AccountKeychain::default();
2423                keychain.keys[caller][access_key2.address].read()
2424            })?;
2425
2426            assert_eq!(
2427                key_after_success.expiry,
2428                u64::MAX,
2429                "Key should be authorized after successful transaction"
2430            );
2431        }
2432
2433        Ok(())
2434    }
2435
2436    /// Regression: CREATE nonce replay vulnerability — demonstrates the T1
2437    /// bug and verifies the T1B fix.
2438    ///
2439    /// **The bug (T1):** An AA CREATE transaction with a KeyAuthorization runs
2440    /// `authorize_key` in a gas-metered precompile call. TIP-1000 SSTORE costs
2441    /// (250k) easily exceed the remaining gas after intrinsic deduction, causing
2442    /// OutOfGas. The handler then sets `evm.initial_gas = u64::MAX`, which
2443    /// short-circuits execution before `make_create_frame` bumps the protocol
2444    /// nonce. The nonce stays at 0, making the signed transaction replayable.
2445    ///
2446    /// **The fix (T1B):** The precompile runs with `gas_limit = u64::MAX`,
2447    /// eliminating the OOG path. Gas is accounted for solely in intrinsic gas.
2448    /// The CREATE frame is always constructed, the nonce is always bumped, and
2449    /// replay is impossible.
2450    #[test]
2451    fn test_create_nonce_replay_regression() -> eyre::Result<()> {
2452        use tempo_precompiles::account_keychain::AccountKeychain;
2453
2454        /// Run a CREATE+KeyAuth transaction on the given hardfork and return
2455        /// (caller_nonce_after, key_expiry).
2456        fn run_create_with_key_auth(
2457            spec: TempoHardfork,
2458            gas_limit: u64,
2459        ) -> eyre::Result<(u64, u64)> {
2460            let key_pair = P256KeyPair::random();
2461            let caller = key_pair.address;
2462
2463            let db = CacheDB::new(EmptyDB::new());
2464            let mut cfg = CfgEnv::<TempoHardfork>::default();
2465            cfg.spec = spec;
2466            cfg.gas_params = tempo_gas_params(spec);
2467
2468            let ctx = Context::mainnet()
2469                .with_db(db)
2470                .with_block(Default::default())
2471                .with_cfg(cfg)
2472                .with_tx(Default::default());
2473
2474            let mut evm = TempoEvm::new(ctx, ());
2475            fund_account(&mut evm, caller);
2476
2477            let block = TempoBlockEnv::default();
2478            {
2479                let ctx = &mut evm.ctx;
2480                let internals =
2481                    EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
2482                // Use default cfg for TIP20 setup — the test infrastructure's
2483                // `is_initialized` check uses an unsafe `as_hashmap()` cast that
2484                // only works with default gas params.
2485                let mut provider =
2486                    EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
2487                StorageCtx::enter(&mut provider, || {
2488                    TIP20Setup::path_usd(caller)
2489                        .with_issuer(caller)
2490                        .with_mint(caller, U256::from(100_000_000))
2491                        .apply()
2492                })?;
2493            }
2494
2495            let access_key = P256KeyPair::random();
2496            let key_auth =
2497                KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, access_key.address);
2498            let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
2499            let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
2500
2501            let tx = TxBuilder::new()
2502                .create(&[0x60, 0x00, 0x60, 0x00, 0xF3])
2503                .key_authorization(signed_key_auth)
2504                .gas_limit(gas_limit)
2505                .build();
2506
2507            let signed_tx = key_pair.sign_tx(tx)?;
2508            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2509            let _result = evm.transact_commit(tx_env);
2510
2511            let nonce = evm
2512                .ctx
2513                .db()
2514                .basic_ref(caller)
2515                .ok()
2516                .flatten()
2517                .map(|a| a.nonce)
2518                .unwrap_or(0);
2519
2520            let key_expiry = {
2521                let ctx = &mut evm.ctx;
2522                let internals =
2523                    EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
2524                let mut provider =
2525                    EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
2526                let key = StorageCtx::enter(&mut provider, || {
2527                    AccountKeychain::default().keys[caller][access_key.address].read()
2528                })?;
2529                key.expiry
2530            };
2531
2532            Ok((nonce, key_expiry))
2533        }
2534
2535        // --- T1: demonstrate the bug ---
2536        // T1 intrinsic gas for this tx is ~560k (21k base + 500k CREATE + 35k
2537        // KeyAuth heuristic). Gas limit 780k leaves ~220k for the precompile,
2538        // which is below the 250k SSTORE cost → OOG → nonce NOT bumped.
2539        let (t1_nonce, t1_key_expiry) = run_create_with_key_auth(TempoHardfork::T1, 780_000)?;
2540        assert_eq!(
2541            t1_nonce, 0,
2542            "T1 bug: nonce must NOT be bumped when keychain OOGs"
2543        );
2544        assert_eq!(
2545            t1_key_expiry, 0,
2546            "T1 bug: key must NOT be authorized when keychain OOGs"
2547        );
2548
2549        // --- T1B: verify the fix ---
2550        // T1B intrinsic gas is ~1.04M (21k base + 500k CREATE + 260k KeyAuth
2551        // + calldata + sig). Gas limit 1.05M is just enough to pass intrinsic
2552        // validation. The precompile runs with unlimited gas, so the nonce is
2553        // always bumped.
2554        let (t1b_nonce, t1b_key_expiry) = run_create_with_key_auth(TempoHardfork::T1B, 1_050_000)?;
2555        assert_eq!(
2556            t1b_nonce, 1,
2557            "T1B fix: nonce must be bumped after CREATE+KeyAuth"
2558        );
2559        assert_eq!(t1b_key_expiry, u64::MAX, "T1B fix: key must be authorized");
2560
2561        Ok(())
2562    }
2563
2564    /// Regression: double gas charging for KeyAuthorization — demonstrates the
2565    /// T1 bug and verifies the T1B fix.
2566    ///
2567    /// **The bug (T1):** The handler charges both a heuristic intrinsic gas
2568    /// estimate AND the metered precompile gas (`evm.initial_gas += gas_used`),
2569    /// resulting in a double charge. With TIP-1000 SSTORE at 250k, a simple
2570    /// KeyAuthorization (0 limits) costs ~530k on T1 instead of ~280k.
2571    ///
2572    /// **The fix (T1B):** Only the intrinsic gas is charged; the precompile runs
2573    /// with unlimited gas and its cost is NOT added to `initial_gas` afterward.
2574    #[test]
2575    fn test_double_charge_key_authorization_regression() -> eyre::Result<()> {
2576        /// Run a CALL+KeyAuth transaction and return gas_used.
2577        fn run_call_with_key_auth(spec: TempoHardfork) -> eyre::Result<u64> {
2578            let key_pair = P256KeyPair::random();
2579            let caller = key_pair.address;
2580
2581            let db = CacheDB::new(EmptyDB::new());
2582            let mut cfg = CfgEnv::<TempoHardfork>::default();
2583            cfg.spec = spec;
2584            cfg.gas_params = tempo_gas_params(spec);
2585
2586            let ctx = Context::mainnet()
2587                .with_db(db)
2588                .with_block(Default::default())
2589                .with_cfg(cfg)
2590                .with_tx(Default::default());
2591
2592            let mut evm = TempoEvm::new(ctx, ());
2593            fund_account(&mut evm, caller);
2594
2595            let block = TempoBlockEnv::default();
2596            {
2597                let ctx = &mut evm.ctx;
2598                let internals =
2599                    EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
2600                let mut provider =
2601                    EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
2602                StorageCtx::enter(&mut provider, || {
2603                    TIP20Setup::path_usd(caller)
2604                        .with_issuer(caller)
2605                        .with_mint(caller, U256::from(100_000_000))
2606                        .apply()
2607                })?;
2608            }
2609
2610            let access_key = P256KeyPair::random();
2611            let key_auth =
2612                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, access_key.address);
2613            let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
2614            let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
2615
2616            let tx = TxBuilder::new()
2617                .call_identity(&[])
2618                .key_authorization(signed_key_auth)
2619                .gas_limit(2_000_000)
2620                .build();
2621
2622            let signed_tx = key_pair.sign_tx(tx)?;
2623            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2624            let result = evm.transact_commit(tx_env)?;
2625            assert!(result.is_success());
2626            Ok(result.tx_gas_used())
2627        }
2628
2629        let t1_gas = run_call_with_key_auth(TempoHardfork::T1)?;
2630        let t1b_gas = run_call_with_key_auth(TempoHardfork::T1B)?;
2631
2632        // T1 double-charges: intrinsic heuristic (~35k) + metered precompile
2633        // (~250k SSTORE) on top of base tx gas, resulting in >500k.
2634        assert!(
2635            t1_gas > 500_000,
2636            "T1 bug: should double-charge (got {t1_gas}, expected >500k)"
2637        );
2638
2639        // T1B charges only once via accurate intrinsic gas (~255k for
2640        // sig+sload+sstore) + base tx. Total ~541k, well below the ~790k
2641        // that double-charging would produce.
2642        assert!(
2643            t1b_gas < t1_gas,
2644            "T1B fix: gas ({t1b_gas}) must be less than T1 double-charge ({t1_gas})"
2645        );
2646
2647        Ok(())
2648    }
2649
2650    /// Regression: `eth_estimateGas` must NOT add an extra 250k `new_account_cost` for AA
2651    /// token transfers using the `calls` format when `nonce_key != 0` and
2652    /// `caller.nonce == 0`.
2653    ///
2654    /// Root cause: `tx.kind()` reads `inner.to`, which is `None` for the
2655    /// `calls` format, causing it to return `TxKind::Create` for a plain
2656    /// transfer — incorrectly triggering a second 250k account-creation charge
2657    /// on top of the legitimate 250k already charged by `validate_aa_initial_tx_gas`.
2658    ///
2659    /// The fix inspects `aa_calls[0].to` directly for AA transactions instead
2660    /// of relying on `tx.kind()`.
2661    #[test]
2662    fn test_aa_tx_transfer_calls_format_no_extra_250k() -> eyre::Result<()> {
2663        let key_pair = P256KeyPair::random();
2664        let caller = key_pair.address;
2665        let recipient = Address::with_last_byte(0xff);
2666
2667        // Baseline: calls-format transfer with nonce_key=0 (protocol nonce).
2668        // validate_aa_initial_tx_gas charges 250k (nonce==0 branch).
2669        // handler.rs does NOT fire because !nonce_key.is_zero() is false.
2670        let mut evm_baseline = create_funded_evm_t1(caller);
2671        let tx_baseline = TxBuilder::new()
2672            .call(recipient, &[])
2673            .nonce_key(U256::ZERO)
2674            .nonce(0)
2675            .gas_limit(500_000)
2676            .build();
2677        let result_baseline = evm_baseline.transact_commit(TempoTxEnv::from_recovered_tx(
2678            &key_pair.sign_tx(tx_baseline)?,
2679            caller,
2680        ))?;
2681        assert!(
2682            result_baseline.is_success(),
2683            "baseline transfer should succeed"
2684        );
2685        let gas_baseline = result_baseline.tx_gas_used();
2686
2687        // Issue #3178 scenario: calls-format transfer with nonce_key != 0, caller.nonce == 0.
2688        // validate_aa_initial_tx_gas still charges the same 250k (nonce==0 branch).
2689        // Before fix: handler.rs also fired (tx.kind() wrongly returned Create) → extra 250k.
2690        // After fix:  handler.rs does NOT fire (aa_calls[0].to is Call) → no extra 250k.
2691        let nonce_key = U256::from(42);
2692        let mut evm_2d = create_funded_evm_t1(caller);
2693        let tx_2d = TxBuilder::new()
2694            .call(recipient, &[])
2695            .nonce_key(nonce_key)
2696            .nonce(0)
2697            .gas_limit(500_000)
2698            .build();
2699        let result_2d = evm_2d.transact_commit(TempoTxEnv::from_recovered_tx(
2700            &key_pair.sign_tx(tx_2d)?,
2701            caller,
2702        ))?;
2703        assert!(
2704            result_2d.is_success(),
2705            "calls-format transfer with 2D nonce should succeed"
2706        );
2707        let gas_2d = result_2d.tx_gas_used();
2708
2709        // After the fix the gas should be nearly identical for both cases because
2710        // both go through the same validate_aa_initial_tx_gas branch and handler.rs
2711        // no longer fires for transfers.
2712        // Before the fix gas_2d would have been ~250k higher than gas_baseline.
2713        let diff = gas_2d.saturating_sub(gas_baseline);
2714        assert!(
2715            diff < 10_000,
2716            "calls-format transfer with nonceKey={nonce_key} (gas={gas_2d}) must not cost \
2717             ~250k more than baseline (gas={gas_baseline}, diff={diff}). \
2718             A diff near 250_000 means new_account_cost is incorrectly added for \
2719             transfers (issue #3178)."
2720        );
2721
2722        Ok(())
2723    }
2724}