Skip to main content

tempo_evm/
evm.rs

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