Skip to main content

tempo_evm/
evm.rs

1use alloy_evm::{
2    Database, Evm, EvmEnv, EvmFactory,
3    precompiles::PrecompilesMap,
4    revm::{
5        Context, ExecuteEvm, InspectEvm, Inspector, SystemCallEvm,
6        context::result::{EVMError, ResultAndState, ResultGas},
7        inspector::NoOpInspector,
8    },
9};
10use alloy_primitives::{Address, Bytes, Log, TxKind};
11use reth_revm::{InspectSystemCallEvm, MainContext, context::result::ExecutionResult};
12use std::ops::{Deref, DerefMut};
13use tempo_chainspec::hardfork::TempoHardfork;
14use tempo_revm::{TempoHaltReason, TempoInvalidTransaction, TempoTxEnv, evm::TempoContext};
15
16use crate::TempoBlockEnv;
17
18#[derive(Debug, Default, Clone, Copy)]
19#[non_exhaustive]
20pub struct TempoEvmFactory;
21
22impl EvmFactory for TempoEvmFactory {
23    type Evm<DB: Database, I: Inspector<Self::Context<DB>>> = TempoEvm<DB, I>;
24    type Context<DB: Database> = TempoContext<DB>;
25    type Tx = TempoTxEnv;
26    type Error<DBError: std::error::Error + Send + Sync + 'static> =
27        EVMError<DBError, TempoInvalidTransaction>;
28    type HaltReason = TempoHaltReason;
29    type Spec = TempoHardfork;
30    type BlockEnv = TempoBlockEnv;
31    type Precompiles = PrecompilesMap;
32
33    fn create_evm<DB: Database>(
34        &self,
35        db: DB,
36        input: EvmEnv<Self::Spec, Self::BlockEnv>,
37    ) -> Self::Evm<DB, NoOpInspector> {
38        TempoEvm::new(db, input)
39    }
40
41    fn create_evm_with_inspector<DB: Database, I: Inspector<Self::Context<DB>>>(
42        &self,
43        db: DB,
44        input: EvmEnv<Self::Spec, Self::BlockEnv>,
45        inspector: I,
46    ) -> Self::Evm<DB, I> {
47        TempoEvm::new(db, input).with_inspector(inspector)
48    }
49}
50
51/// Tempo EVM implementation.
52///
53/// This is a wrapper type around the `revm` ethereum evm with optional [`Inspector`] (tracing)
54/// support. [`Inspector`] support is configurable at runtime because it's part of the underlying
55/// `RevmEvm` type.
56#[expect(missing_debug_implementations)]
57pub struct TempoEvm<DB: Database, I = NoOpInspector> {
58    inner: tempo_revm::TempoEvm<DB, I>,
59    inspect: bool,
60}
61
62impl<DB: Database> TempoEvm<DB> {
63    /// Create a new [`TempoEvm`] instance.
64    pub fn new(db: DB, input: EvmEnv<TempoHardfork, TempoBlockEnv>) -> Self {
65        let ctx = Context::mainnet()
66            .with_db(db)
67            .with_block(input.block_env)
68            .with_cfg(input.cfg_env)
69            .with_tx(Default::default());
70
71        Self {
72            inner: tempo_revm::TempoEvm::new(ctx, NoOpInspector {}),
73            inspect: false,
74        }
75    }
76}
77
78impl<DB: Database, I> TempoEvm<DB, I> {
79    /// Provides a reference to the EVM context.
80    pub const fn ctx(&self) -> &TempoContext<DB> {
81        &self.inner.inner.ctx
82    }
83
84    /// Provides a mutable reference to the EVM context.
85    pub fn ctx_mut(&mut self) -> &mut TempoContext<DB> {
86        &mut self.inner.inner.ctx
87    }
88
89    /// Sets the inspector for the EVM.
90    pub fn with_inspector<OINSP>(self, inspector: OINSP) -> TempoEvm<DB, OINSP> {
91        TempoEvm {
92            inner: self.inner.with_inspector(inspector),
93            inspect: true,
94        }
95    }
96
97    /// Takes the inner EVM's revert logs.
98    ///
99    /// This is used as a work around to allow logs to be
100    /// included for reverting transactions.
101    ///
102    /// TODO: remove once revm supports emitting logs for reverted transactions
103    ///
104    /// <https://github.com/tempoxyz/tempo/pull/729>
105    pub fn take_revert_logs(&mut self) -> Vec<Log> {
106        std::mem::take(&mut self.inner.logs)
107    }
108}
109
110impl<DB: Database, I> Deref for TempoEvm<DB, I>
111where
112    DB: Database,
113    I: Inspector<TempoContext<DB>>,
114{
115    type Target = TempoContext<DB>;
116
117    #[inline]
118    fn deref(&self) -> &Self::Target {
119        self.ctx()
120    }
121}
122
123impl<DB: Database, I> DerefMut for TempoEvm<DB, I>
124where
125    DB: Database,
126    I: Inspector<TempoContext<DB>>,
127{
128    #[inline]
129    fn deref_mut(&mut self) -> &mut Self::Target {
130        self.ctx_mut()
131    }
132}
133
134impl<DB, I> Evm for TempoEvm<DB, I>
135where
136    DB: Database,
137    I: Inspector<TempoContext<DB>>,
138{
139    type DB = DB;
140    type Tx = TempoTxEnv;
141    type Error = EVMError<DB::Error, TempoInvalidTransaction>;
142    type HaltReason = TempoHaltReason;
143    type Spec = TempoHardfork;
144    type BlockEnv = TempoBlockEnv;
145    type Precompiles = PrecompilesMap;
146    type Inspector = I;
147
148    fn block(&self) -> &Self::BlockEnv {
149        &self.block
150    }
151
152    fn chain_id(&self) -> u64 {
153        self.cfg.chain_id
154    }
155
156    fn transact_raw(
157        &mut self,
158        tx: Self::Tx,
159    ) -> Result<ResultAndState<Self::HaltReason>, Self::Error> {
160        if tx.is_system_tx {
161            let TxKind::Call(to) = tx.inner.kind else {
162                return Err(TempoInvalidTransaction::SystemTransactionMustBeCall.into());
163            };
164
165            let mut result = if self.inspect {
166                self.inner
167                    .inspect_system_call_with_caller(tx.inner.caller, to, tx.inner.data)?
168            } else {
169                self.inner
170                    .system_call_with_caller(tx.inner.caller, to, tx.inner.data)?
171            };
172
173            // system transactions should not consume any gas
174            let ExecutionResult::Success { gas, .. } = &mut result.result else {
175                return Err(
176                    TempoInvalidTransaction::SystemTransactionFailed(result.result.into()).into(),
177                );
178            };
179
180            *gas = ResultGas::default().with_limit(tx.inner.gas_limit);
181
182            Ok(result)
183        } else if self.inspect {
184            self.inner.inspect_tx(tx)
185        } else {
186            self.inner.transact(tx)
187        }
188    }
189
190    fn transact_system_call(
191        &mut self,
192        caller: Address,
193        contract: Address,
194        data: Bytes,
195    ) -> Result<ResultAndState<Self::HaltReason>, Self::Error> {
196        self.inner.system_call_with_caller(caller, contract, data)
197    }
198
199    fn finish(self) -> (Self::DB, EvmEnv<Self::Spec, Self::BlockEnv>) {
200        let Context {
201            block: block_env,
202            cfg: cfg_env,
203            journaled_state,
204            ..
205        } = self.inner.inner.ctx;
206
207        (journaled_state.database, EvmEnv { block_env, cfg_env })
208    }
209
210    fn set_inspector_enabled(&mut self, enabled: bool) {
211        self.inspect = enabled;
212    }
213
214    fn components(&self) -> (&Self::DB, &Self::Inspector, &Self::Precompiles) {
215        (
216            &self.inner.inner.ctx.journaled_state.database,
217            &self.inner.inner.inspector,
218            &self.inner.inner.precompiles,
219        )
220    }
221
222    fn components_mut(&mut self) -> (&mut Self::DB, &mut Self::Inspector, &mut Self::Precompiles) {
223        (
224            &mut self.inner.inner.ctx.journaled_state.database,
225            &mut self.inner.inner.inspector,
226            &mut self.inner.inner.precompiles,
227        )
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use crate::test_utils::{test_evm, test_evm_with_basefee};
234    use revm::{
235        context::{CfgEnv, TxEnv},
236        database::{EmptyDB, in_memory_db::CacheDB},
237    };
238    use tempo_chainspec::hardfork::TempoHardfork;
239    use tempo_revm::gas_params::tempo_gas_params;
240
241    use super::*;
242
243    #[test]
244    fn can_execute_system_tx() {
245        let mut evm = test_evm(EmptyDB::default());
246        let result = evm
247            .transact(TempoTxEnv {
248                inner: TxEnv {
249                    caller: Address::ZERO,
250                    gas_price: 0,
251                    gas_limit: 21000,
252                    ..Default::default()
253                },
254                is_system_tx: true,
255                ..Default::default()
256            })
257            .unwrap();
258
259        assert!(result.result.is_success());
260    }
261
262    #[test]
263    fn test_transact_raw() {
264        let mut evm = test_evm_with_basefee(EmptyDB::default(), 0);
265
266        let tx = TempoTxEnv {
267            inner: TxEnv {
268                caller: Address::repeat_byte(0x01),
269                gas_price: 0,
270                gas_limit: 21000,
271                kind: TxKind::Call(Address::repeat_byte(0x02)),
272                ..Default::default()
273            },
274            is_system_tx: false,
275            fee_token: None,
276            ..Default::default()
277        };
278
279        let result = evm.transact_raw(tx);
280        assert!(result.is_ok());
281
282        let result = result.unwrap();
283        assert!(result.result.is_success());
284        assert_eq!(result.result.gas_used(), 21000);
285    }
286
287    #[test]
288    fn test_transact_raw_system_tx() {
289        let mut evm = test_evm(EmptyDB::default());
290
291        // System transaction
292        let tx = TempoTxEnv {
293            inner: TxEnv {
294                caller: Address::ZERO,
295                gas_price: 0,
296                gas_limit: 21000,
297                kind: TxKind::Call(Address::repeat_byte(0x01)),
298                ..Default::default()
299            },
300            is_system_tx: true,
301            ..Default::default()
302        };
303
304        let result = evm.transact_raw(tx);
305        assert!(result.is_ok());
306
307        let result = result.unwrap();
308        assert!(result.result.is_success());
309        // System transactions should not consume gas
310        assert_eq!(result.result.gas_used(), 0);
311    }
312
313    #[test]
314    fn test_transact_raw_system_tx_must_be_call() {
315        let mut evm = test_evm(EmptyDB::default());
316
317        // System transaction with Create kind
318        let tx = TempoTxEnv {
319            inner: TxEnv {
320                caller: Address::ZERO,
321                gas_price: 0,
322                gas_limit: 21000,
323                kind: TxKind::Create,
324                ..Default::default()
325            },
326            is_system_tx: true,
327            ..Default::default()
328        };
329
330        let result = evm.transact_raw(tx);
331        assert!(result.is_err());
332
333        let err = result.unwrap_err();
334        assert!(matches!(
335            err,
336            EVMError::Transaction(TempoInvalidTransaction::SystemTransactionMustBeCall)
337        ));
338    }
339
340    #[test]
341    fn test_transact_raw_system_tx_failed() {
342        let mut cache_db = CacheDB::new(EmptyDB::default());
343        // Deploy a contract that always reverts: PUSH1 0x00 PUSH1 0x00 REVERT (0x60006000fd)
344        let revert_code = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xfd]);
345        let contract_addr = Address::repeat_byte(0xaa);
346
347        cache_db.insert_account_info(
348            contract_addr,
349            revm::state::AccountInfo {
350                code_hash: alloy_primitives::keccak256(&revert_code),
351                code: Some(revm::bytecode::Bytecode::new_raw(revert_code)),
352                ..Default::default()
353            },
354        );
355
356        let mut evm = test_evm(cache_db);
357
358        // System transaction that will fail with call to contract that reverts
359        let tx = TempoTxEnv {
360            inner: TxEnv {
361                caller: Address::ZERO,
362                gas_price: 0,
363                gas_limit: 1_000_000,
364                kind: TxKind::Call(contract_addr),
365                ..Default::default()
366            },
367            is_system_tx: true,
368            ..Default::default()
369        };
370
371        let result = evm.transact_raw(tx);
372        assert!(result.is_err());
373
374        let err = result.unwrap_err();
375        assert!(matches!(
376            err,
377            EVMError::Transaction(TempoInvalidTransaction::SystemTransactionFailed(_))
378        ));
379    }
380
381    #[test]
382    fn test_transact_system_call() {
383        let mut evm = test_evm(EmptyDB::default());
384
385        let caller = Address::repeat_byte(0x01);
386        let contract = Address::repeat_byte(0x02);
387        let data = Bytes::from_static(&[0x01, 0x02, 0x03]);
388
389        let result = evm.transact_system_call(caller, contract, data);
390        assert!(result.is_ok());
391
392        let result = result.unwrap();
393        assert!(result.result.is_success());
394    }
395
396    #[test]
397    fn test_take_revert_logs() {
398        let mut evm = test_evm(EmptyDB::default());
399
400        assert!(evm.take_revert_logs().is_empty());
401
402        let log1 = Log::new_unchecked(
403            Address::repeat_byte(0x01),
404            vec![alloy_primitives::B256::repeat_byte(0xaa)],
405            Bytes::from_static(&[0x01, 0x02]),
406        );
407        let log2 = Log::new_unchecked(
408            Address::repeat_byte(0x02),
409            vec![],
410            Bytes::from_static(&[0x03, 0x04]),
411        );
412        evm.inner.logs.push(log1);
413        evm.inner.logs.push(log2);
414
415        let logs = evm.take_revert_logs();
416        assert_eq!(logs.len(), 2);
417        assert_eq!(logs[0].address, Address::repeat_byte(0x01));
418        assert_eq!(logs[1].address, Address::repeat_byte(0x02));
419
420        assert!(evm.take_revert_logs().is_empty());
421    }
422
423    // ==================== TIP-1000 EVM Configuration Tests ====================
424
425    /// Helper to create EvmEnv with a specific hardfork spec.
426    fn evm_env_with_spec(
427        spec: tempo_chainspec::hardfork::TempoHardfork,
428    ) -> EvmEnv<tempo_chainspec::hardfork::TempoHardfork, TempoBlockEnv> {
429        EvmEnv::<tempo_chainspec::hardfork::TempoHardfork, TempoBlockEnv>::new(
430            CfgEnv::new_with_spec_and_gas_params(spec, tempo_gas_params(spec)),
431            TempoBlockEnv::default(),
432        )
433    }
434
435    /// Test that TempoEvm applies custom gas params via `tempo_gas_params()`.
436    /// This verifies the [TIP-1000] gas parameter override mechanism.
437    ///
438    /// [TIP-1000]: <https://docs.tempo.xyz/protocol/tips/tip-1000>
439    #[test]
440    fn test_tempo_evm_applies_gas_params() {
441        // Create EVM with T1 hardfork to get TIP-1000 gas params
442        let evm = TempoEvm::new(EmptyDB::default(), evm_env_with_spec(TempoHardfork::T1));
443
444        // Verify gas params were applied (check a known T1 override)
445        // T1 has tx_eip7702_per_empty_account_cost = 12,500
446        let gas_params = &evm.ctx().cfg.gas_params;
447        assert_eq!(
448            gas_params.tx_eip7702_per_empty_account_cost(),
449            12_500,
450            "T1 should have EIP-7702 per empty account cost of 12,500"
451        );
452    }
453
454    /// Test that TempoEvm respects the gas limit cap passed in via EvmEnv.
455    /// Note: The 30M [TIP-1000] gas cap is set in ConfigureEvm::evm_env(), not here.
456    /// This test verifies that TempoEvm::new() preserves the cap from the input.
457    ///
458    /// [TIP-1000]: <https://docs.tempo.xyz/protocol/tips/tip-1000>
459    #[test]
460    fn test_tempo_evm_respects_gas_cap() {
461        let mut env = evm_env_with_spec(TempoHardfork::T1);
462        env.cfg_env.tx_gas_limit_cap = TempoHardfork::T1.tx_gas_limit_cap();
463
464        let evm = TempoEvm::new(EmptyDB::default(), env);
465
466        // Verify gas limit cap is preserved
467        assert_eq!(
468            evm.ctx().cfg.tx_gas_limit_cap,
469            TempoHardfork::T1.tx_gas_limit_cap(),
470            "TempoEvm should preserve the gas limit cap from input"
471        );
472    }
473
474    /// Test that gas params differ between T0 and T1 hardforks.
475    #[test]
476    fn test_tempo_evm_gas_params_differ_t0_vs_t1() {
477        // Create T0 and T1 EVMs
478        let t0_evm = TempoEvm::new(EmptyDB::default(), evm_env_with_spec(TempoHardfork::T0));
479        let t1_evm = TempoEvm::new(EmptyDB::default(), evm_env_with_spec(TempoHardfork::T1));
480
481        // T0 should have default EIP-7702 cost (25,000)
482        // T1 should have reduced cost (12,500)
483        let t0_eip7702_cost = t0_evm
484            .ctx()
485            .cfg
486            .gas_params
487            .tx_eip7702_per_empty_account_cost();
488        let t1_eip7702_cost = t1_evm
489            .ctx()
490            .cfg
491            .gas_params
492            .tx_eip7702_per_empty_account_cost();
493
494        assert_eq!(t0_eip7702_cost, 25_000, "T0 should have default 25,000");
495        assert_eq!(t1_eip7702_cost, 12_500, "T1 should have reduced 12,500");
496        assert_ne!(
497            t0_eip7702_cost, t1_eip7702_cost,
498            "Gas params should differ between T0 and T1"
499        );
500    }
501
502    /// Test that T1 has significantly higher state creation costs.
503    #[test]
504    fn test_tempo_evm_t1_state_creation_costs() {
505        use revm::context_interface::cfg::GasId;
506
507        let evm = TempoEvm::new(EmptyDB::default(), evm_env_with_spec(TempoHardfork::T1));
508        let gas_params = &evm.ctx().cfg.gas_params;
509
510        // Verify TIP-1000 state creation cost increases
511        assert_eq!(
512            gas_params.get(GasId::sstore_set_without_load_cost()),
513            250_000,
514            "T1 SSTORE set cost should be 250,000"
515        );
516        assert_eq!(
517            gas_params.get(GasId::tx_create_cost()),
518            500_000,
519            "T1 TX create cost should be 500,000"
520        );
521        assert_eq!(
522            gas_params.get(GasId::create()),
523            500_000,
524            "T1 CREATE opcode cost should be 500,000"
525        );
526        assert_eq!(
527            gas_params.get(GasId::new_account_cost()),
528            250_000,
529            "T1 new account cost should be 250,000"
530        );
531        assert_eq!(
532            gas_params.get(GasId::code_deposit_cost()),
533            1_000,
534            "T1 code deposit cost should be 1,000 per byte"
535        );
536    }
537}