Skip to main content

tempo_evm/
lib.rs

1//! Tempo EVM implementation.
2
3#![cfg_attr(not(test), warn(unused_crate_dependencies))]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5
6mod assemble;
7use alloy_consensus::{BlockHeader as _, Transaction};
8use alloy_rlp::Decodable;
9pub use assemble::TempoBlockAssembler;
10mod block;
11pub use block::{TempoBlockExecutor, TempoReceiptBuilder, TempoTxResult};
12mod context;
13pub use context::{TempoBlockExecutionCtx, TempoNextBlockEnvAttributes};
14pub mod consensus;
15#[cfg(feature = "engine")]
16mod engine;
17#[cfg(feature = "engine")]
18use rayon as _;
19mod error;
20pub use error::TempoEvmError;
21pub mod evm;
22use std::{borrow::Cow, sync::Arc};
23
24use alloy_evm::{
25    self, EvmEnv,
26    block::BlockExecutorFactory,
27    eth::{EthBlockExecutionCtx, NextEvmEnvAttributes},
28    revm::Inspector,
29};
30pub use evm::TempoEvmFactory;
31use reth_chainspec::EthChainSpec;
32use reth_evm::{self, ConfigureEvm, EvmEnvFor, block::StateDB};
33use reth_primitives_traits::{SealedBlock, SealedHeader};
34use tempo_primitives::{
35    Block, SubBlockMetadata, TempoHeader, TempoPrimitives, TempoReceipt, TempoTxEnvelope,
36    subblock::PartialValidatorKey,
37};
38
39use crate::evm::TempoEvm;
40use reth_evm_ethereum::EthEvmConfig;
41use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardforks};
42use tempo_revm::{evm::TempoContext, gas_params::tempo_gas_params_with_amsterdam};
43
44pub use tempo_revm::{TempoBlockEnv, TempoHaltReason, TempoInvalidTransaction, TempoStateAccess};
45
46#[cfg(test)]
47mod test_utils;
48
49/// Tempo-related EVM configuration.
50#[derive(Debug, Clone)]
51pub struct TempoEvmConfig {
52    /// Inner evm config
53    pub inner: EthEvmConfig<TempoChainSpec, TempoEvmFactory>,
54
55    /// Block assembler
56    pub block_assembler: TempoBlockAssembler,
57}
58
59impl TempoEvmConfig {
60    /// Create a new [`TempoEvmConfig`] with the given chain spec and EVM factory.
61    pub fn new(chain_spec: Arc<TempoChainSpec>) -> Self {
62        let inner =
63            EthEvmConfig::new_with_evm_factory(chain_spec.clone(), TempoEvmFactory::default());
64        Self {
65            inner,
66            block_assembler: TempoBlockAssembler::new(chain_spec),
67        }
68    }
69
70    /// Returns the chain spec
71    pub const fn chain_spec(&self) -> &Arc<TempoChainSpec> {
72        self.inner.chain_spec()
73    }
74
75    /// Returns the inner EVM config
76    pub const fn inner(&self) -> &EthEvmConfig<TempoChainSpec, TempoEvmFactory> {
77        &self.inner
78    }
79
80    /// Returns the moderato EVM config.
81    pub fn moderato() -> Self {
82        Self::new(Arc::new(TempoChainSpec::moderato()))
83    }
84
85    /// Returns the mainnet EVM config.
86    pub fn mainnet() -> Self {
87        Self::new(Arc::new(TempoChainSpec::mainnet()))
88    }
89}
90
91impl BlockExecutorFactory for TempoEvmConfig {
92    type EvmFactory = TempoEvmFactory;
93    type ExecutionCtx<'a> = TempoBlockExecutionCtx<'a>;
94    type Transaction = TempoTxEnvelope;
95    type Receipt = TempoReceipt;
96    type TxExecutionResult = TempoTxResult;
97    type Executor<'a, DB: StateDB, I: Inspector<TempoContext<DB>>> = TempoBlockExecutor<'a, DB, I>;
98
99    fn evm_factory(&self) -> &Self::EvmFactory {
100        self.inner.executor_factory.evm_factory()
101    }
102
103    fn create_executor<'a, DB, I>(
104        &'a self,
105        evm: TempoEvm<DB, I>,
106        ctx: Self::ExecutionCtx<'a>,
107    ) -> Self::Executor<'a, DB, I>
108    where
109        DB: StateDB,
110        I: Inspector<TempoContext<DB>>,
111    {
112        TempoBlockExecutor::new(evm, ctx, self.chain_spec())
113    }
114}
115
116impl ConfigureEvm for TempoEvmConfig {
117    type Primitives = TempoPrimitives;
118    type Error = TempoEvmError;
119    type NextBlockEnvCtx = TempoNextBlockEnvAttributes;
120    type BlockExecutorFactory = Self;
121    type BlockAssembler = TempoBlockAssembler;
122
123    fn block_executor_factory(&self) -> &Self::BlockExecutorFactory {
124        self
125    }
126
127    fn block_assembler(&self) -> &Self::BlockAssembler {
128        &self.block_assembler
129    }
130
131    fn evm_env(&self, header: &TempoHeader) -> Result<EvmEnvFor<Self>, Self::Error> {
132        let EvmEnv { cfg_env, block_env } = EvmEnv::for_eth_block(
133            header,
134            self.chain_spec(),
135            self.chain_spec().chain().id(),
136            self.chain_spec()
137                .blob_params_at_timestamp(header.timestamp()),
138        );
139
140        let spec = self.chain_spec().tempo_hardfork_at(header.timestamp());
141
142        // Apply TIP-1000 gas params for T1 hardfork.
143        //
144        // TIP-1016 (EIP-8037 state gas split) is gated by `cfg_env.enable_amsterdam_eip8037`
145        // and is independent of the T4 hardfork. The flag is currently left at its default
146        // (`false`) so TIP-1016 is disabled even on T4; flipping it on enables the regular/
147        // state gas split everywhere it is checked downstream.
148        //
149        // TODO(TIP-1016): this is the place where we previously did
150        // `cfg_env.enable_amsterdam_eip8037 = spec.is_t4();`. When TIP-1016 is ready to
151        // ship, re-enable it here (or wire it through chain spec / cfg defaults) so the
152        // state gas split activates on the appropriate hardfork.
153        let amsterdam_eip8037_enabled = cfg_env.enable_amsterdam_eip8037;
154        let mut cfg_env = cfg_env.with_spec_and_gas_params(
155            spec,
156            tempo_gas_params_with_amsterdam(spec, amsterdam_eip8037_enabled),
157        );
158        cfg_env.tx_gas_limit_cap = spec.tx_gas_limit_cap();
159
160        Ok(EvmEnv {
161            cfg_env,
162            block_env: TempoBlockEnv {
163                inner: block_env,
164                timestamp_millis_part: header.timestamp_millis_part,
165            },
166        })
167    }
168
169    fn next_evm_env(
170        &self,
171        parent: &TempoHeader,
172        attributes: &Self::NextBlockEnvCtx,
173    ) -> Result<EvmEnvFor<Self>, Self::Error> {
174        let EvmEnv { cfg_env, block_env } = EvmEnv::for_eth_next_block(
175            parent,
176            NextEvmEnvAttributes {
177                timestamp: attributes.timestamp,
178                suggested_fee_recipient: attributes.suggested_fee_recipient,
179                prev_randao: attributes.prev_randao,
180                gas_limit: attributes.gas_limit,
181                slot_number: attributes.slot_number,
182            },
183            self.chain_spec()
184                .next_block_base_fee(parent, attributes.timestamp)
185                .unwrap_or_default(),
186            self.chain_spec(),
187            self.chain_spec().chain().id(),
188            self.chain_spec()
189                .blob_params_at_timestamp(attributes.timestamp),
190        );
191
192        let spec = self.chain_spec().tempo_hardfork_at(attributes.timestamp);
193
194        // Apply TIP-1000 gas params for T1 hardfork. TIP-1016 is gated by
195        // `cfg_env.enable_amsterdam_eip8037`, independent of the T4 hardfork
196        // (see `evm_env_for_block` for details).
197        //
198        // TODO(TIP-1016): this is the place where we previously did
199        // `cfg_env.enable_amsterdam_eip8037 = spec.is_t4();`. When TIP-1016 is ready to
200        // ship, re-enable it here (or wire it through chain spec / cfg defaults) so the
201        // state gas split activates on the appropriate hardfork.
202        let amsterdam_eip8037_enabled = cfg_env.enable_amsterdam_eip8037;
203        let mut cfg_env = cfg_env.with_spec_and_gas_params(
204            spec,
205            tempo_gas_params_with_amsterdam(spec, amsterdam_eip8037_enabled),
206        );
207        cfg_env.tx_gas_limit_cap = spec.tx_gas_limit_cap();
208
209        Ok(EvmEnv {
210            cfg_env,
211            block_env: TempoBlockEnv {
212                inner: block_env,
213                timestamp_millis_part: attributes.timestamp_millis_part,
214            },
215        })
216    }
217
218    fn context_for_block<'a>(
219        &self,
220        block: &'a SealedBlock<Block>,
221    ) -> Result<TempoBlockExecutionCtx<'a>, Self::Error> {
222        // Decode validator -> fee_recipient mapping from the subblock metadata system transaction.
223        let subblock_fee_recipients = block
224            .body()
225            .transactions
226            .iter()
227            .rev()
228            .filter(|tx| tx.is_system_tx())
229            .find_map(|tx| Vec::<SubBlockMetadata>::decode(&mut tx.input().as_ref()).ok())
230            .unwrap_or_default()
231            .into_iter()
232            .map(|metadata| {
233                (
234                    PartialValidatorKey::from_slice(&metadata.validator[..15]),
235                    metadata.fee_recipient,
236                )
237            })
238            .collect();
239
240        Ok(TempoBlockExecutionCtx {
241            inner: EthBlockExecutionCtx {
242                parent_hash: block.header().parent_hash(),
243                parent_beacon_block_root: block.header().parent_beacon_block_root(),
244                // no ommers in tempo
245                ommers: &[],
246                withdrawals: block
247                    .body()
248                    .withdrawals
249                    .as_ref()
250                    .map(|w| Cow::Borrowed(w.as_slice())),
251                extra_data: block.extra_data().clone(),
252                tx_count_hint: Some(block.body().transactions.len()),
253                slot_number: block.slot_number(),
254            },
255            general_gas_limit: block.header().general_gas_limit,
256            shared_gas_limit: block.header().shared_gas_limit,
257            // Not available when we only have a block body.
258            validator_set: None,
259            consensus_context: block.header().consensus_context,
260            subblock_fee_recipients,
261        })
262    }
263
264    fn context_for_next_block(
265        &self,
266        parent: &SealedHeader<TempoHeader>,
267        attributes: Self::NextBlockEnvCtx,
268    ) -> Result<TempoBlockExecutionCtx<'_>, Self::Error> {
269        Ok(TempoBlockExecutionCtx {
270            inner: EthBlockExecutionCtx {
271                parent_hash: parent.hash(),
272                parent_beacon_block_root: attributes.parent_beacon_block_root,
273                slot_number: attributes.slot_number,
274                ommers: &[],
275                withdrawals: attributes
276                    .inner
277                    .withdrawals
278                    .map(|w| Cow::Owned(w.into_inner())),
279                extra_data: attributes.inner.extra_data,
280                tx_count_hint: None,
281            },
282            general_gas_limit: attributes.general_gas_limit,
283            shared_gas_limit: attributes.shared_gas_limit,
284            // Fine to not validate during block building.
285            validator_set: None,
286            consensus_context: attributes.consensus_context,
287            subblock_fee_recipients: attributes.subblock_fee_recipients,
288        })
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use crate::test_utils::test_chainspec;
296    use alloy_consensus::{BlockHeader, Signed, TxLegacy};
297    use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
298    use alloy_rlp::{Encodable, bytes::BytesMut};
299    use reth_evm::{ConfigureEvm, NextBlockEnvAttributes};
300    use std::collections::HashMap;
301    use tempo_chainspec::hardfork::TempoHardfork;
302    use tempo_primitives::{
303        BlockBody, SubBlockMetadata, subblock::SubBlockVersion,
304        transaction::envelope::TEMPO_SYSTEM_TX_SIGNATURE,
305    };
306
307    #[test]
308    fn test_evm_config_can_query_tempo_hardforks() {
309        let evm_config = TempoEvmConfig::new(test_chainspec());
310        let activation = evm_config
311            .chain_spec()
312            .tempo_fork_activation(TempoHardfork::Genesis);
313        assert_eq!(activation, reth_chainspec::ForkCondition::Timestamp(0));
314    }
315
316    #[test]
317    fn test_evm_env() {
318        let evm_config = TempoEvmConfig::new(test_chainspec());
319
320        let header = TempoHeader {
321            inner: alloy_consensus::Header {
322                number: 100,
323                timestamp: 1000,
324                gas_limit: 30_000_000,
325                base_fee_per_gas: Some(1000),
326                beneficiary: alloy_primitives::Address::repeat_byte(0x01),
327                ..Default::default()
328            },
329            general_gas_limit: 10_000_000,
330            timestamp_millis_part: 500,
331            shared_gas_limit: 3_000_000,
332            ..Default::default()
333        };
334
335        let result = evm_config.evm_env(&header);
336        assert!(result.is_ok());
337
338        let evm_env = result.unwrap();
339
340        // Verify block env fields
341        assert_eq!(evm_env.block_env.inner.number, U256::from(header.number()));
342        assert_eq!(
343            evm_env.block_env.inner.timestamp,
344            U256::from(header.timestamp())
345        );
346        assert_eq!(evm_env.block_env.inner.gas_limit, header.gas_limit());
347        assert_eq!(evm_env.block_env.inner.beneficiary, header.beneficiary());
348
349        // Verify Tempo-specific field
350        assert_eq!(evm_env.block_env.timestamp_millis_part, 500);
351    }
352
353    /// Test that evm_env sets 30M gas limit cap for T1 hardfork as per [TIP-1000].
354    ///
355    /// [TIP-1000]: <https://docs.tempo.xyz/protocol/tips/tip-1000>
356    #[test]
357    fn test_evm_env_t1_gas_cap() {
358        use tempo_chainspec::spec::DEV;
359
360        // DEV chainspec has T1 activated at timestamp 0
361        let chainspec = DEV.clone();
362        let evm_config = TempoEvmConfig::new(chainspec.clone());
363
364        let header = TempoHeader {
365            inner: alloy_consensus::Header {
366                number: 100,
367                timestamp: 1000, // After T1 activation
368                gas_limit: 30_000_000,
369                base_fee_per_gas: Some(1000),
370                ..Default::default()
371            },
372            general_gas_limit: 10_000_000,
373            timestamp_millis_part: 0,
374            shared_gas_limit: 3_000_000,
375            ..Default::default()
376        };
377
378        // Verify we're in T1
379        assert!(chainspec.tempo_hardfork_at(header.timestamp()).is_t1());
380
381        let evm_env = evm_config.evm_env(&header).unwrap();
382
383        // Verify TIP-1000 gas limit cap is set
384        assert_eq!(
385            evm_env.cfg_env.tx_gas_limit_cap,
386            Some(tempo_chainspec::spec::TEMPO_T1_TX_GAS_LIMIT_CAP),
387            "TIP-1000 requires 30M gas limit cap for T1 hardfork"
388        );
389    }
390
391    #[test]
392    fn test_next_evm_env() {
393        let evm_config = TempoEvmConfig::new(test_chainspec());
394
395        let parent = TempoHeader {
396            inner: alloy_consensus::Header {
397                number: 99,
398                timestamp: 900,
399                gas_limit: 30_000_000,
400                base_fee_per_gas: Some(1000),
401                ..Default::default()
402            },
403            general_gas_limit: 10_000_000,
404            timestamp_millis_part: 0,
405            shared_gas_limit: 3_000_000,
406            ..Default::default()
407        };
408
409        let attributes = TempoNextBlockEnvAttributes {
410            inner: NextBlockEnvAttributes {
411                timestamp: 1000,
412                suggested_fee_recipient: alloy_primitives::Address::repeat_byte(0x02),
413                prev_randao: B256::repeat_byte(0x03),
414                gas_limit: 30_000_000,
415                parent_beacon_block_root: Some(B256::ZERO),
416                withdrawals: None,
417                extra_data: Default::default(),
418                slot_number: None,
419            },
420            general_gas_limit: 10_000_000,
421            shared_gas_limit: 3_000_000,
422            timestamp_millis_part: 750,
423            consensus_context: None,
424            subblock_fee_recipients: HashMap::new(),
425        };
426
427        let result = evm_config.next_evm_env(&parent, &attributes);
428        assert!(result.is_ok());
429
430        let evm_env = result.unwrap();
431
432        // Verify block env uses attributes
433        // parent + 1
434        assert_eq!(evm_env.block_env.inner.number, U256::from(100));
435        assert_eq!(evm_env.block_env.inner.timestamp, U256::from(1000));
436        assert_eq!(
437            evm_env.block_env.inner.beneficiary,
438            Address::repeat_byte(0x02)
439        );
440        assert_eq!(evm_env.block_env.inner.gas_limit, 30_000_000);
441
442        // Verify Tempo-specific field
443        assert_eq!(evm_env.block_env.timestamp_millis_part, 750);
444    }
445
446    #[test]
447    fn test_context_for_block() {
448        let chainspec = test_chainspec();
449        let evm_config = TempoEvmConfig::new(chainspec.clone());
450
451        // Create subblock metadata
452        let validator_key = B256::repeat_byte(0x01);
453        let fee_recipient = alloy_primitives::Address::repeat_byte(0x02);
454        let metadata = vec![SubBlockMetadata {
455            version: SubBlockVersion::V1,
456            validator: validator_key,
457            fee_recipient,
458            signature: Bytes::from_static(&[0; 64]),
459        }];
460
461        // Create system tx with metadata
462        let block_number = 1u64;
463        let mut input = BytesMut::new();
464        metadata.encode(&mut input);
465        input.extend_from_slice(&U256::from(block_number).to_be_bytes::<32>());
466
467        let system_tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(
468            TxLegacy {
469                chain_id: Some(reth_chainspec::EthChainSpec::chain(&*chainspec).id()),
470                nonce: 0,
471                gas_price: 0,
472                gas_limit: 0,
473                to: TxKind::Call(alloy_primitives::Address::ZERO),
474                value: U256::ZERO,
475                input: input.freeze().into(),
476            },
477            TEMPO_SYSTEM_TX_SIGNATURE,
478        ));
479
480        let header = TempoHeader {
481            inner: alloy_consensus::Header {
482                number: block_number,
483                timestamp: 1000,
484                gas_limit: 30_000_000,
485                parent_beacon_block_root: Some(B256::ZERO),
486                ..Default::default()
487            },
488            general_gas_limit: 10_000_000,
489            timestamp_millis_part: 500,
490            shared_gas_limit: 3_000_000,
491            ..Default::default()
492        };
493
494        let body = BlockBody {
495            transactions: vec![system_tx],
496            ommers: vec![],
497            withdrawals: None,
498        };
499
500        let block = Block { header, body };
501        let sealed_block = SealedBlock::seal_slow(block);
502
503        let result = evm_config.context_for_block(&sealed_block);
504        assert!(result.is_ok());
505
506        let context = result.unwrap();
507
508        // Verify context fields
509        assert_eq!(context.general_gas_limit, 10_000_000);
510        assert_eq!(context.shared_gas_limit, 3_000_000);
511        assert!(context.validator_set.is_none());
512
513        // Verify subblock_fee_recipients was extracted from metadata
514        let partial_key = PartialValidatorKey::from_slice(&validator_key[..15]);
515        assert_eq!(
516            context.subblock_fee_recipients.get(&partial_key),
517            Some(&fee_recipient)
518        );
519    }
520
521    #[test]
522    fn test_context_for_block_t4_without_metadata_has_empty_fee_recipients() {
523        use tempo_chainspec::spec::DEV;
524
525        let chainspec = DEV.clone();
526        let evm_config = TempoEvmConfig::new(chainspec);
527
528        let header = TempoHeader {
529            inner: alloy_consensus::Header {
530                number: 1,
531                timestamp: 1000,
532                gas_limit: 30_000_000,
533                parent_beacon_block_root: Some(B256::ZERO),
534                ..Default::default()
535            },
536            general_gas_limit: 10_000_000,
537            timestamp_millis_part: 500,
538            shared_gas_limit: 3_000_000,
539            ..Default::default()
540        };
541
542        let body = BlockBody {
543            transactions: vec![],
544            ommers: vec![],
545            withdrawals: None,
546        };
547
548        let block = Block { header, body };
549        let sealed_block = SealedBlock::seal_slow(block);
550
551        let context = evm_config.context_for_block(&sealed_block).unwrap();
552        assert!(context.subblock_fee_recipients.is_empty());
553    }
554
555    #[test]
556    fn test_context_for_next_block() {
557        let evm_config = TempoEvmConfig::new(test_chainspec());
558
559        let parent_header = TempoHeader {
560            inner: alloy_consensus::Header {
561                number: 99,
562                timestamp: 900,
563                gas_limit: 30_000_000,
564                ..Default::default()
565            },
566            general_gas_limit: 10_000_000,
567            timestamp_millis_part: 0,
568            shared_gas_limit: 0,
569            ..Default::default()
570        };
571        let parent = SealedHeader::seal_slow(parent_header);
572
573        let fee_recipient = Address::repeat_byte(0x02);
574        let mut subblock_fee_recipients = HashMap::new();
575        let partial_key = PartialValidatorKey::from_slice(&[0x01; 15]);
576        subblock_fee_recipients.insert(partial_key, fee_recipient);
577
578        let attributes = TempoNextBlockEnvAttributes {
579            inner: NextBlockEnvAttributes {
580                timestamp: 1000,
581                suggested_fee_recipient: alloy_primitives::Address::repeat_byte(0x03),
582                prev_randao: B256::repeat_byte(0x04),
583                gas_limit: 30_000_000,
584                parent_beacon_block_root: Some(B256::repeat_byte(0x05)),
585                withdrawals: None,
586                extra_data: Default::default(),
587                slot_number: None,
588            },
589            general_gas_limit: 12_000_000,
590            shared_gas_limit: 4_000_000,
591            timestamp_millis_part: 999,
592            consensus_context: None,
593            subblock_fee_recipients: subblock_fee_recipients.clone(),
594        };
595
596        let result = evm_config.context_for_next_block(&parent, attributes);
597        assert!(result.is_ok());
598
599        let context = result.unwrap();
600
601        // Verify context fields from attributes
602        assert_eq!(context.general_gas_limit, 12_000_000);
603        assert_eq!(context.shared_gas_limit, 4_000_000);
604        assert!(context.validator_set.is_none());
605        assert_eq!(context.inner.parent_hash, parent.hash());
606        assert_eq!(
607            context.inner.parent_beacon_block_root,
608            Some(B256::repeat_byte(0x05))
609        );
610
611        // Verify subblock_fee_recipients passed through
612        assert_eq!(
613            context.subblock_fee_recipients.get(&partial_key),
614            Some(&fee_recipient)
615        );
616    }
617}