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::TempoReceiptBuilder;
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, BlockExecutorFor},
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::{block::TempoBlockExecutor, evm::TempoEvm};
40use reth_evm_ethereum::EthEvmConfig;
41use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardforks};
42use tempo_revm::{evm::TempoContext, gas_params::tempo_gas_params};
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 mainnet EVM config.
81    pub fn mainnet() -> Self {
82        Self::new(Arc::new(TempoChainSpec::mainnet()))
83    }
84}
85
86impl BlockExecutorFactory for TempoEvmConfig {
87    type EvmFactory = TempoEvmFactory;
88    type ExecutionCtx<'a> = TempoBlockExecutionCtx<'a>;
89    type Transaction = TempoTxEnvelope;
90    type Receipt = TempoReceipt;
91
92    fn evm_factory(&self) -> &Self::EvmFactory {
93        self.inner.executor_factory.evm_factory()
94    }
95
96    fn create_executor<'a, DB, I>(
97        &'a self,
98        evm: TempoEvm<DB, I>,
99        ctx: Self::ExecutionCtx<'a>,
100    ) -> impl BlockExecutorFor<'a, Self, DB, I>
101    where
102        DB: StateDB + 'a,
103        I: Inspector<TempoContext<DB>> + 'a,
104    {
105        TempoBlockExecutor::new(evm, ctx, self.chain_spec())
106    }
107}
108
109impl ConfigureEvm for TempoEvmConfig {
110    type Primitives = TempoPrimitives;
111    type Error = TempoEvmError;
112    type NextBlockEnvCtx = TempoNextBlockEnvAttributes;
113    type BlockExecutorFactory = Self;
114    type BlockAssembler = TempoBlockAssembler;
115
116    fn block_executor_factory(&self) -> &Self::BlockExecutorFactory {
117        self
118    }
119
120    fn block_assembler(&self) -> &Self::BlockAssembler {
121        &self.block_assembler
122    }
123
124    fn evm_env(&self, header: &TempoHeader) -> Result<EvmEnvFor<Self>, Self::Error> {
125        let EvmEnv { cfg_env, block_env } = EvmEnv::for_eth_block(
126            header,
127            self.chain_spec(),
128            self.chain_spec().chain().id(),
129            self.chain_spec()
130                .blob_params_at_timestamp(header.timestamp()),
131        );
132
133        let spec = self.chain_spec().tempo_hardfork_at(header.timestamp());
134
135        // Apply TIP-1000 gas params for T1 hardfork.
136        let mut cfg_env = cfg_env.with_spec_and_gas_params(spec, tempo_gas_params(spec));
137        cfg_env.tx_gas_limit_cap = spec.tx_gas_limit_cap();
138
139        Ok(EvmEnv {
140            cfg_env,
141            block_env: TempoBlockEnv {
142                inner: block_env,
143                timestamp_millis_part: header.timestamp_millis_part,
144            },
145        })
146    }
147
148    fn next_evm_env(
149        &self,
150        parent: &TempoHeader,
151        attributes: &Self::NextBlockEnvCtx,
152    ) -> Result<EvmEnvFor<Self>, Self::Error> {
153        let EvmEnv { cfg_env, block_env } = EvmEnv::for_eth_next_block(
154            parent,
155            NextEvmEnvAttributes {
156                timestamp: attributes.timestamp,
157                suggested_fee_recipient: attributes.suggested_fee_recipient,
158                prev_randao: attributes.prev_randao,
159                gas_limit: attributes.gas_limit,
160            },
161            self.chain_spec()
162                .next_block_base_fee(parent, attributes.timestamp)
163                .unwrap_or_default(),
164            self.chain_spec(),
165            self.chain_spec().chain().id(),
166            self.chain_spec()
167                .blob_params_at_timestamp(attributes.timestamp),
168        );
169
170        let spec = self.chain_spec().tempo_hardfork_at(attributes.timestamp);
171
172        // Apply TIP-1000 gas params for T1 hardfork.
173        let mut cfg_env = cfg_env.with_spec_and_gas_params(spec, tempo_gas_params(spec));
174        cfg_env.tx_gas_limit_cap = spec.tx_gas_limit_cap();
175
176        Ok(EvmEnv {
177            cfg_env,
178            block_env: TempoBlockEnv {
179                inner: block_env,
180                timestamp_millis_part: attributes.timestamp_millis_part,
181            },
182        })
183    }
184
185    fn context_for_block<'a>(
186        &self,
187        block: &'a SealedBlock<Block>,
188    ) -> Result<TempoBlockExecutionCtx<'a>, Self::Error> {
189        // Decode validator -> fee_recipient mapping from the subblock metadata system transaction.
190        let subblock_fee_recipients = block
191            .body()
192            .transactions
193            .iter()
194            .rev()
195            .filter(|tx| (*tx).to() == Some(Address::ZERO))
196            .find_map(|tx| Vec::<SubBlockMetadata>::decode(&mut tx.input().as_ref()).ok())
197            .ok_or(TempoEvmError::NoSubblockMetadataFound)?
198            .into_iter()
199            .map(|metadata| {
200                (
201                    PartialValidatorKey::from_slice(&metadata.validator[..15]),
202                    metadata.fee_recipient,
203                )
204            })
205            .collect();
206
207        Ok(TempoBlockExecutionCtx {
208            inner: EthBlockExecutionCtx {
209                parent_hash: block.header().parent_hash(),
210                parent_beacon_block_root: block.header().parent_beacon_block_root(),
211                // no ommers in tempo
212                ommers: &[],
213                withdrawals: block
214                    .body()
215                    .withdrawals
216                    .as_ref()
217                    .map(|w| Cow::Borrowed(w.as_slice())),
218                extra_data: block.extra_data().clone(),
219                tx_count_hint: Some(block.body().transactions.len()),
220            },
221            general_gas_limit: block.header().general_gas_limit,
222            shared_gas_limit: block.header().gas_limit()
223                / tempo_consensus::TEMPO_SHARED_GAS_DIVISOR,
224            // Not available when we only have a block body.
225            validator_set: None,
226            subblock_fee_recipients,
227        })
228    }
229
230    fn context_for_next_block(
231        &self,
232        parent: &SealedHeader<TempoHeader>,
233        attributes: Self::NextBlockEnvCtx,
234    ) -> Result<TempoBlockExecutionCtx<'_>, Self::Error> {
235        Ok(TempoBlockExecutionCtx {
236            inner: EthBlockExecutionCtx {
237                parent_hash: parent.hash(),
238                parent_beacon_block_root: attributes.parent_beacon_block_root,
239                ommers: &[],
240                withdrawals: attributes
241                    .inner
242                    .withdrawals
243                    .map(|w| Cow::Owned(w.into_inner())),
244                extra_data: attributes.inner.extra_data,
245                tx_count_hint: None,
246            },
247            general_gas_limit: attributes.general_gas_limit,
248            shared_gas_limit: attributes.inner.gas_limit
249                / tempo_consensus::TEMPO_SHARED_GAS_DIVISOR,
250            // Fine to not validate during block building.
251            validator_set: None,
252            subblock_fee_recipients: attributes.subblock_fee_recipients,
253        })
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::test_utils::test_chainspec;
261    use alloy_consensus::{BlockHeader, Signed, TxLegacy};
262    use alloy_primitives::{B256, Bytes, Signature, TxKind, U256};
263    use alloy_rlp::{Encodable, bytes::BytesMut};
264    use reth_evm::{ConfigureEvm, NextBlockEnvAttributes};
265    use std::collections::HashMap;
266    use tempo_chainspec::hardfork::TempoHardfork;
267    use tempo_primitives::{
268        BlockBody, SubBlockMetadata, subblock::SubBlockVersion,
269        transaction::envelope::TEMPO_SYSTEM_TX_SIGNATURE,
270    };
271
272    #[test]
273    fn test_evm_config_can_query_tempo_hardforks() {
274        let evm_config = TempoEvmConfig::new(test_chainspec());
275        let activation = evm_config
276            .chain_spec()
277            .tempo_fork_activation(TempoHardfork::Genesis);
278        assert_eq!(activation, reth_chainspec::ForkCondition::Timestamp(0));
279    }
280
281    #[test]
282    fn test_evm_env() {
283        let evm_config = TempoEvmConfig::new(test_chainspec());
284
285        let header = TempoHeader {
286            inner: alloy_consensus::Header {
287                number: 100,
288                timestamp: 1000,
289                gas_limit: 30_000_000,
290                base_fee_per_gas: Some(1000),
291                beneficiary: alloy_primitives::Address::repeat_byte(0x01),
292                ..Default::default()
293            },
294            general_gas_limit: 10_000_000,
295            timestamp_millis_part: 500,
296            shared_gas_limit: 3_000_000,
297        };
298
299        let result = evm_config.evm_env(&header);
300        assert!(result.is_ok());
301
302        let evm_env = result.unwrap();
303
304        // Verify block env fields
305        assert_eq!(evm_env.block_env.inner.number, U256::from(header.number()));
306        assert_eq!(
307            evm_env.block_env.inner.timestamp,
308            U256::from(header.timestamp())
309        );
310        assert_eq!(evm_env.block_env.inner.gas_limit, header.gas_limit());
311        assert_eq!(evm_env.block_env.inner.beneficiary, header.beneficiary());
312
313        // Verify Tempo-specific field
314        assert_eq!(evm_env.block_env.timestamp_millis_part, 500);
315    }
316
317    /// Test that evm_env sets 30M gas limit cap for T1 hardfork as per [TIP-1000].
318    ///
319    /// [TIP-1000]: <https://docs.tempo.xyz/protocol/tips/tip-1000>
320    #[test]
321    fn test_evm_env_t1_gas_cap() {
322        use tempo_chainspec::spec::DEV;
323
324        // DEV chainspec has T1 activated at timestamp 0
325        let chainspec = DEV.clone();
326        let evm_config = TempoEvmConfig::new(chainspec.clone());
327
328        let header = TempoHeader {
329            inner: alloy_consensus::Header {
330                number: 100,
331                timestamp: 1000, // After T1 activation
332                gas_limit: 30_000_000,
333                base_fee_per_gas: Some(1000),
334                ..Default::default()
335            },
336            general_gas_limit: 10_000_000,
337            timestamp_millis_part: 0,
338            shared_gas_limit: 3_000_000,
339        };
340
341        // Verify we're in T1
342        assert!(chainspec.tempo_hardfork_at(header.timestamp()).is_t1());
343
344        let evm_env = evm_config.evm_env(&header).unwrap();
345
346        // Verify TIP-1000 gas limit cap is set
347        assert_eq!(
348            evm_env.cfg_env.tx_gas_limit_cap,
349            Some(tempo_chainspec::spec::TEMPO_T1_TX_GAS_LIMIT_CAP),
350            "TIP-1000 requires 30M gas limit cap for T1 hardfork"
351        );
352    }
353
354    #[test]
355    fn test_next_evm_env() {
356        let evm_config = TempoEvmConfig::new(test_chainspec());
357
358        let parent = TempoHeader {
359            inner: alloy_consensus::Header {
360                number: 99,
361                timestamp: 900,
362                gas_limit: 30_000_000,
363                base_fee_per_gas: Some(1000),
364                ..Default::default()
365            },
366            general_gas_limit: 10_000_000,
367            timestamp_millis_part: 0,
368            shared_gas_limit: 3_000_000,
369        };
370
371        let attributes = TempoNextBlockEnvAttributes {
372            inner: NextBlockEnvAttributes {
373                timestamp: 1000,
374                suggested_fee_recipient: alloy_primitives::Address::repeat_byte(0x02),
375                prev_randao: B256::repeat_byte(0x03),
376                gas_limit: 30_000_000,
377                parent_beacon_block_root: Some(B256::ZERO),
378                withdrawals: None,
379                extra_data: Default::default(),
380            },
381            general_gas_limit: 10_000_000,
382            shared_gas_limit: 3_000_000,
383            timestamp_millis_part: 750,
384            subblock_fee_recipients: HashMap::new(),
385        };
386
387        let result = evm_config.next_evm_env(&parent, &attributes);
388        assert!(result.is_ok());
389
390        let evm_env = result.unwrap();
391
392        // Verify block env uses attributes
393        // parent + 1
394        assert_eq!(evm_env.block_env.inner.number, U256::from(100));
395        assert_eq!(evm_env.block_env.inner.timestamp, U256::from(1000));
396        assert_eq!(
397            evm_env.block_env.inner.beneficiary,
398            Address::repeat_byte(0x02)
399        );
400        assert_eq!(evm_env.block_env.inner.gas_limit, 30_000_000);
401
402        // Verify Tempo-specific field
403        assert_eq!(evm_env.block_env.timestamp_millis_part, 750);
404    }
405
406    #[test]
407    fn test_context_for_block() {
408        let chainspec = test_chainspec();
409        let evm_config = TempoEvmConfig::new(chainspec.clone());
410
411        // Create subblock metadata
412        let validator_key = B256::repeat_byte(0x01);
413        let fee_recipient = alloy_primitives::Address::repeat_byte(0x02);
414        let metadata = vec![SubBlockMetadata {
415            version: SubBlockVersion::V1,
416            validator: validator_key,
417            fee_recipient,
418            signature: Bytes::from_static(&[0; 64]),
419        }];
420
421        // Create system tx with metadata
422        let block_number = 1u64;
423        let mut input = BytesMut::new();
424        metadata.encode(&mut input);
425        input.extend_from_slice(&U256::from(block_number).to_be_bytes::<32>());
426
427        let system_tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(
428            TxLegacy {
429                chain_id: Some(reth_chainspec::EthChainSpec::chain(&*chainspec).id()),
430                nonce: 0,
431                gas_price: 0,
432                gas_limit: 0,
433                to: TxKind::Call(alloy_primitives::Address::ZERO),
434                value: U256::ZERO,
435                input: input.freeze().into(),
436            },
437            TEMPO_SYSTEM_TX_SIGNATURE,
438        ));
439
440        let header = TempoHeader {
441            inner: alloy_consensus::Header {
442                number: block_number,
443                timestamp: 1000,
444                gas_limit: 30_000_000,
445                parent_beacon_block_root: Some(B256::ZERO),
446                ..Default::default()
447            },
448            general_gas_limit: 10_000_000,
449            timestamp_millis_part: 500,
450            shared_gas_limit: 3_000_000,
451        };
452
453        let body = BlockBody {
454            transactions: vec![system_tx],
455            ommers: vec![],
456            withdrawals: None,
457        };
458
459        let block = Block { header, body };
460        let sealed_block = SealedBlock::seal_slow(block);
461
462        let result = evm_config.context_for_block(&sealed_block);
463        assert!(result.is_ok());
464
465        let context = result.unwrap();
466
467        // Verify context fields
468        assert_eq!(context.general_gas_limit, 10_000_000);
469        assert_eq!(context.shared_gas_limit, 3_000_000);
470        assert!(context.validator_set.is_none());
471
472        // Verify subblock_fee_recipients was extracted from metadata
473        let partial_key = PartialValidatorKey::from_slice(&validator_key[..15]);
474        assert_eq!(
475            context.subblock_fee_recipients.get(&partial_key),
476            Some(&fee_recipient)
477        );
478    }
479
480    #[test]
481    fn test_context_for_block_no_subblock_metadata() {
482        let evm_config = TempoEvmConfig::new(test_chainspec());
483
484        // Create a block without subblock metadata system tx
485        let regular_tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(
486            TxLegacy {
487                chain_id: Some(1),
488                nonce: 0,
489                gas_price: 1,
490                gas_limit: 21000,
491                to: TxKind::Call(alloy_primitives::Address::repeat_byte(0x01)),
492                value: U256::ZERO,
493                input: Bytes::new(),
494            },
495            Signature::test_signature(),
496        ));
497
498        let header = TempoHeader {
499            inner: alloy_consensus::Header {
500                number: 1,
501                timestamp: 1000,
502                gas_limit: 30_000_000,
503                ..Default::default()
504            },
505            general_gas_limit: 10_000_000,
506            timestamp_millis_part: 500,
507            shared_gas_limit: 3_000_000,
508        };
509
510        let body = BlockBody {
511            transactions: vec![regular_tx],
512            ommers: vec![],
513            withdrawals: None,
514        };
515
516        let block = Block { header, body };
517        let sealed_block = SealedBlock::seal_slow(block);
518
519        let result = evm_config.context_for_block(&sealed_block);
520
521        // Should fail because no subblock metadata tx was found
522        assert!(result.is_err());
523        assert!(matches!(
524            result.unwrap_err(),
525            TempoEvmError::NoSubblockMetadataFound
526        ));
527    }
528
529    #[test]
530    fn test_context_for_next_block() {
531        let evm_config = TempoEvmConfig::new(test_chainspec());
532
533        let parent_header = TempoHeader {
534            inner: alloy_consensus::Header {
535                number: 99,
536                timestamp: 900,
537                gas_limit: 30_000_000,
538                ..Default::default()
539            },
540            general_gas_limit: 10_000_000,
541            timestamp_millis_part: 0,
542            shared_gas_limit: 3_000_000,
543        };
544        let parent = SealedHeader::seal_slow(parent_header);
545
546        let fee_recipient = Address::repeat_byte(0x02);
547        let mut subblock_fee_recipients = HashMap::new();
548        let partial_key = PartialValidatorKey::from_slice(&[0x01; 15]);
549        subblock_fee_recipients.insert(partial_key, fee_recipient);
550
551        let attributes = TempoNextBlockEnvAttributes {
552            inner: NextBlockEnvAttributes {
553                timestamp: 1000,
554                suggested_fee_recipient: alloy_primitives::Address::repeat_byte(0x03),
555                prev_randao: B256::repeat_byte(0x04),
556                gas_limit: 30_000_000,
557                parent_beacon_block_root: Some(B256::repeat_byte(0x05)),
558                withdrawals: None,
559                extra_data: Default::default(),
560            },
561            general_gas_limit: 12_000_000,
562            shared_gas_limit: 4_000_000,
563            timestamp_millis_part: 999,
564            subblock_fee_recipients: subblock_fee_recipients.clone(),
565        };
566
567        let result = evm_config.context_for_next_block(&parent, attributes);
568        assert!(result.is_ok());
569
570        let context = result.unwrap();
571
572        // Verify context fields from attributes
573        assert_eq!(context.general_gas_limit, 12_000_000);
574        assert_eq!(context.shared_gas_limit, 3_000_000);
575        assert!(context.validator_set.is_none());
576        assert_eq!(context.inner.parent_hash, parent.hash());
577        assert_eq!(
578            context.inner.parent_beacon_block_root,
579            Some(B256::repeat_byte(0x05))
580        );
581
582        // Verify subblock_fee_recipients passed through
583        assert_eq!(
584            context.subblock_fee_recipients.get(&partial_key),
585            Some(&fee_recipient)
586        );
587    }
588}