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_primitives::Address;
9use alloy_rlp::Decodable;
10pub use assemble::TempoBlockAssembler;
11mod block;
12pub use block::{TempoBlockExecutor, TempoReceiptBuilder, TempoTxResult};
13mod context;
14pub use context::{TempoBlockExecutionCtx, TempoNextBlockEnvAttributes};
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, 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).to() == Some(Address::ZERO))
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().gas_limit()
257                / tempo_consensus::TEMPO_SHARED_GAS_DIVISOR,
258            // Not available when we only have a block body.
259            validator_set: None,
260            consensus_context: block.header().consensus_context,
261            subblock_fee_recipients,
262        })
263    }
264
265    fn context_for_next_block(
266        &self,
267        parent: &SealedHeader<TempoHeader>,
268        attributes: Self::NextBlockEnvCtx,
269    ) -> Result<TempoBlockExecutionCtx<'_>, Self::Error> {
270        Ok(TempoBlockExecutionCtx {
271            inner: EthBlockExecutionCtx {
272                parent_hash: parent.hash(),
273                parent_beacon_block_root: attributes.parent_beacon_block_root,
274                slot_number: attributes.slot_number,
275                ommers: &[],
276                withdrawals: attributes
277                    .inner
278                    .withdrawals
279                    .map(|w| Cow::Owned(w.into_inner())),
280                extra_data: attributes.inner.extra_data,
281                tx_count_hint: None,
282            },
283            general_gas_limit: attributes.general_gas_limit,
284            shared_gas_limit: attributes.inner.gas_limit
285                / tempo_consensus::TEMPO_SHARED_GAS_DIVISOR,
286            // Fine to not validate during block building.
287            validator_set: None,
288            consensus_context: attributes.consensus_context,
289            subblock_fee_recipients: attributes.subblock_fee_recipients,
290        })
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::test_utils::test_chainspec;
298    use alloy_consensus::{BlockHeader, Signed, TxLegacy};
299    use alloy_primitives::{B256, Bytes, TxKind, U256};
300    use alloy_rlp::{Encodable, bytes::BytesMut};
301    use reth_evm::{ConfigureEvm, NextBlockEnvAttributes};
302    use std::collections::HashMap;
303    use tempo_chainspec::hardfork::TempoHardfork;
304    use tempo_primitives::{
305        BlockBody, SubBlockMetadata, subblock::SubBlockVersion,
306        transaction::envelope::TEMPO_SYSTEM_TX_SIGNATURE,
307    };
308
309    #[test]
310    fn test_evm_config_can_query_tempo_hardforks() {
311        let evm_config = TempoEvmConfig::new(test_chainspec());
312        let activation = evm_config
313            .chain_spec()
314            .tempo_fork_activation(TempoHardfork::Genesis);
315        assert_eq!(activation, reth_chainspec::ForkCondition::Timestamp(0));
316    }
317
318    #[test]
319    fn test_evm_env() {
320        let evm_config = TempoEvmConfig::new(test_chainspec());
321
322        let header = TempoHeader {
323            inner: alloy_consensus::Header {
324                number: 100,
325                timestamp: 1000,
326                gas_limit: 30_000_000,
327                base_fee_per_gas: Some(1000),
328                beneficiary: alloy_primitives::Address::repeat_byte(0x01),
329                ..Default::default()
330            },
331            general_gas_limit: 10_000_000,
332            timestamp_millis_part: 500,
333            shared_gas_limit: 3_000_000,
334            ..Default::default()
335        };
336
337        let result = evm_config.evm_env(&header);
338        assert!(result.is_ok());
339
340        let evm_env = result.unwrap();
341
342        // Verify block env fields
343        assert_eq!(evm_env.block_env.inner.number, U256::from(header.number()));
344        assert_eq!(
345            evm_env.block_env.inner.timestamp,
346            U256::from(header.timestamp())
347        );
348        assert_eq!(evm_env.block_env.inner.gas_limit, header.gas_limit());
349        assert_eq!(evm_env.block_env.inner.beneficiary, header.beneficiary());
350
351        // Verify Tempo-specific field
352        assert_eq!(evm_env.block_env.timestamp_millis_part, 500);
353    }
354
355    /// Test that evm_env sets 30M gas limit cap for T1 hardfork as per [TIP-1000].
356    ///
357    /// [TIP-1000]: <https://docs.tempo.xyz/protocol/tips/tip-1000>
358    #[test]
359    fn test_evm_env_t1_gas_cap() {
360        use tempo_chainspec::spec::DEV;
361
362        // DEV chainspec has T1 activated at timestamp 0
363        let chainspec = DEV.clone();
364        let evm_config = TempoEvmConfig::new(chainspec.clone());
365
366        let header = TempoHeader {
367            inner: alloy_consensus::Header {
368                number: 100,
369                timestamp: 1000, // After T1 activation
370                gas_limit: 30_000_000,
371                base_fee_per_gas: Some(1000),
372                ..Default::default()
373            },
374            general_gas_limit: 10_000_000,
375            timestamp_millis_part: 0,
376            shared_gas_limit: 3_000_000,
377            ..Default::default()
378        };
379
380        // Verify we're in T1
381        assert!(chainspec.tempo_hardfork_at(header.timestamp()).is_t1());
382
383        let evm_env = evm_config.evm_env(&header).unwrap();
384
385        // Verify TIP-1000 gas limit cap is set
386        assert_eq!(
387            evm_env.cfg_env.tx_gas_limit_cap,
388            Some(tempo_chainspec::spec::TEMPO_T1_TX_GAS_LIMIT_CAP),
389            "TIP-1000 requires 30M gas limit cap for T1 hardfork"
390        );
391    }
392
393    #[test]
394    fn test_next_evm_env() {
395        let evm_config = TempoEvmConfig::new(test_chainspec());
396
397        let parent = TempoHeader {
398            inner: alloy_consensus::Header {
399                number: 99,
400                timestamp: 900,
401                gas_limit: 30_000_000,
402                base_fee_per_gas: Some(1000),
403                ..Default::default()
404            },
405            general_gas_limit: 10_000_000,
406            timestamp_millis_part: 0,
407            shared_gas_limit: 3_000_000,
408            ..Default::default()
409        };
410
411        let attributes = TempoNextBlockEnvAttributes {
412            inner: NextBlockEnvAttributes {
413                timestamp: 1000,
414                suggested_fee_recipient: alloy_primitives::Address::repeat_byte(0x02),
415                prev_randao: B256::repeat_byte(0x03),
416                gas_limit: 30_000_000,
417                parent_beacon_block_root: Some(B256::ZERO),
418                withdrawals: None,
419                extra_data: Default::default(),
420                slot_number: None,
421            },
422            general_gas_limit: 10_000_000,
423            shared_gas_limit: 3_000_000,
424            timestamp_millis_part: 750,
425            consensus_context: None,
426            subblock_fee_recipients: HashMap::new(),
427        };
428
429        let result = evm_config.next_evm_env(&parent, &attributes);
430        assert!(result.is_ok());
431
432        let evm_env = result.unwrap();
433
434        // Verify block env uses attributes
435        // parent + 1
436        assert_eq!(evm_env.block_env.inner.number, U256::from(100));
437        assert_eq!(evm_env.block_env.inner.timestamp, U256::from(1000));
438        assert_eq!(
439            evm_env.block_env.inner.beneficiary,
440            Address::repeat_byte(0x02)
441        );
442        assert_eq!(evm_env.block_env.inner.gas_limit, 30_000_000);
443
444        // Verify Tempo-specific field
445        assert_eq!(evm_env.block_env.timestamp_millis_part, 750);
446    }
447
448    #[test]
449    fn test_context_for_block() {
450        let chainspec = test_chainspec();
451        let evm_config = TempoEvmConfig::new(chainspec.clone());
452
453        // Create subblock metadata
454        let validator_key = B256::repeat_byte(0x01);
455        let fee_recipient = alloy_primitives::Address::repeat_byte(0x02);
456        let metadata = vec![SubBlockMetadata {
457            version: SubBlockVersion::V1,
458            validator: validator_key,
459            fee_recipient,
460            signature: Bytes::from_static(&[0; 64]),
461        }];
462
463        // Create system tx with metadata
464        let block_number = 1u64;
465        let mut input = BytesMut::new();
466        metadata.encode(&mut input);
467        input.extend_from_slice(&U256::from(block_number).to_be_bytes::<32>());
468
469        let system_tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(
470            TxLegacy {
471                chain_id: Some(reth_chainspec::EthChainSpec::chain(&*chainspec).id()),
472                nonce: 0,
473                gas_price: 0,
474                gas_limit: 0,
475                to: TxKind::Call(alloy_primitives::Address::ZERO),
476                value: U256::ZERO,
477                input: input.freeze().into(),
478            },
479            TEMPO_SYSTEM_TX_SIGNATURE,
480        ));
481
482        let header = TempoHeader {
483            inner: alloy_consensus::Header {
484                number: block_number,
485                timestamp: 1000,
486                gas_limit: 30_000_000,
487                parent_beacon_block_root: Some(B256::ZERO),
488                ..Default::default()
489            },
490            general_gas_limit: 10_000_000,
491            timestamp_millis_part: 500,
492            shared_gas_limit: 3_000_000,
493            ..Default::default()
494        };
495
496        let body = BlockBody {
497            transactions: vec![system_tx],
498            ommers: vec![],
499            withdrawals: None,
500        };
501
502        let block = Block { header, body };
503        let sealed_block = SealedBlock::seal_slow(block);
504
505        let result = evm_config.context_for_block(&sealed_block);
506        assert!(result.is_ok());
507
508        let context = result.unwrap();
509
510        // Verify context fields
511        assert_eq!(context.general_gas_limit, 10_000_000);
512        assert_eq!(context.shared_gas_limit, 3_000_000);
513        assert!(context.validator_set.is_none());
514
515        // Verify subblock_fee_recipients was extracted from metadata
516        let partial_key = PartialValidatorKey::from_slice(&validator_key[..15]);
517        assert_eq!(
518            context.subblock_fee_recipients.get(&partial_key),
519            Some(&fee_recipient)
520        );
521    }
522
523    #[test]
524    fn test_context_for_block_t4_without_metadata_has_empty_fee_recipients() {
525        use tempo_chainspec::spec::DEV;
526
527        let chainspec = DEV.clone();
528        let evm_config = TempoEvmConfig::new(chainspec);
529
530        let header = TempoHeader {
531            inner: alloy_consensus::Header {
532                number: 1,
533                timestamp: 1000,
534                gas_limit: 30_000_000,
535                parent_beacon_block_root: Some(B256::ZERO),
536                ..Default::default()
537            },
538            general_gas_limit: 10_000_000,
539            timestamp_millis_part: 500,
540            shared_gas_limit: 3_000_000,
541            ..Default::default()
542        };
543
544        let body = BlockBody {
545            transactions: vec![],
546            ommers: vec![],
547            withdrawals: None,
548        };
549
550        let block = Block { header, body };
551        let sealed_block = SealedBlock::seal_slow(block);
552
553        let context = evm_config.context_for_block(&sealed_block).unwrap();
554        assert!(context.subblock_fee_recipients.is_empty());
555    }
556
557    #[test]
558    fn test_context_for_next_block() {
559        let evm_config = TempoEvmConfig::new(test_chainspec());
560
561        let parent_header = TempoHeader {
562            inner: alloy_consensus::Header {
563                number: 99,
564                timestamp: 900,
565                gas_limit: 30_000_000,
566                ..Default::default()
567            },
568            general_gas_limit: 10_000_000,
569            timestamp_millis_part: 0,
570            shared_gas_limit: 3_000_000,
571            ..Default::default()
572        };
573        let parent = SealedHeader::seal_slow(parent_header);
574
575        let fee_recipient = Address::repeat_byte(0x02);
576        let mut subblock_fee_recipients = HashMap::new();
577        let partial_key = PartialValidatorKey::from_slice(&[0x01; 15]);
578        subblock_fee_recipients.insert(partial_key, fee_recipient);
579
580        let attributes = TempoNextBlockEnvAttributes {
581            inner: NextBlockEnvAttributes {
582                timestamp: 1000,
583                suggested_fee_recipient: alloy_primitives::Address::repeat_byte(0x03),
584                prev_randao: B256::repeat_byte(0x04),
585                gas_limit: 30_000_000,
586                parent_beacon_block_root: Some(B256::repeat_byte(0x05)),
587                withdrawals: None,
588                extra_data: Default::default(),
589                slot_number: None,
590            },
591            general_gas_limit: 12_000_000,
592            shared_gas_limit: 4_000_000,
593            timestamp_millis_part: 999,
594            consensus_context: None,
595            subblock_fee_recipients: subblock_fee_recipients.clone(),
596        };
597
598        let result = evm_config.context_for_next_block(&parent, attributes);
599        assert!(result.is_ok());
600
601        let context = result.unwrap();
602
603        // Verify context fields from attributes
604        assert_eq!(context.general_gas_limit, 12_000_000);
605        assert_eq!(context.shared_gas_limit, 3_000_000);
606        assert!(context.validator_set.is_none());
607        assert_eq!(context.inner.parent_hash, parent.hash());
608        assert_eq!(
609            context.inner.parent_beacon_block_root,
610            Some(B256::repeat_byte(0x05))
611        );
612
613        // Verify subblock_fee_recipients passed through
614        assert_eq!(
615            context.subblock_fee_recipients.get(&partial_key),
616            Some(&fee_recipient)
617        );
618    }
619}