tempo_evm/
block.rs

1use crate::{TempoBlockExecutionCtx, evm::TempoEvm};
2use alloy_consensus::{Transaction, transaction::TxHashRef};
3use alloy_evm::{
4    Database, Evm,
5    block::{
6        BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockValidationError,
7        ExecutableTx, OnStateHook,
8    },
9    eth::{
10        EthBlockExecutor,
11        receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx},
12    },
13};
14use alloy_primitives::{Address, B256, Bytes, U256};
15use alloy_rlp::Decodable;
16use alloy_sol_types::SolCall;
17use commonware_codec::DecodeExt;
18use commonware_cryptography::{
19    Verifier,
20    ed25519::{PublicKey, Signature},
21};
22use reth_revm::{Inspector, State, context::result::ResultAndState};
23use revm::{
24    DatabaseCommit,
25    context::ContextTr,
26    state::{Account, Bytecode},
27};
28use std::collections::{HashMap, HashSet};
29use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardforks};
30use tempo_precompiles::{
31    ACCOUNT_KEYCHAIN_ADDRESS, STABLECOIN_EXCHANGE_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
32    TIP20_REWARDS_REGISTRY_ADDRESS, stablecoin_exchange::IStablecoinExchange,
33    tip_fee_manager::IFeeManager, tip20_rewards_registry::ITIP20RewardsRegistry,
34};
35use tempo_primitives::{
36    SubBlock, SubBlockMetadata, TempoReceipt, TempoTxEnvelope, subblock::PartialValidatorKey,
37};
38use tempo_revm::{TempoHaltReason, evm::TempoContext};
39use tracing::trace;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42enum BlockSection {
43    /// Start of block system transactions (rewards registry).
44    StartOfBlock { seen_tip20_rewards_registry: bool },
45    /// Basic section of the block. Includes arbitrary transactions chosen by the proposer.
46    ///
47    /// Must use at most `non_shared_gas_left` gas.
48    NonShared,
49    /// Subblock authored by the given validator.
50    SubBlock { proposer: PartialValidatorKey },
51    /// Gas incentive transaction.
52    GasIncentive,
53    /// End of block system transactions.
54    System {
55        seen_fee_manager: bool,
56        seen_stablecoin_dex: bool,
57        seen_subblocks_signatures: bool,
58    },
59}
60
61/// Builder for [`TempoReceipt`].
62#[derive(Debug, Clone, Copy, Default)]
63#[non_exhaustive]
64pub(crate) struct TempoReceiptBuilder;
65
66impl ReceiptBuilder for TempoReceiptBuilder {
67    type Transaction = TempoTxEnvelope;
68    type Receipt = TempoReceipt;
69
70    fn build_receipt<E: Evm>(
71        &self,
72        ctx: ReceiptBuilderCtx<'_, Self::Transaction, E>,
73    ) -> Self::Receipt {
74        let ReceiptBuilderCtx {
75            tx,
76            result,
77            cumulative_gas_used,
78            ..
79        } = ctx;
80        TempoReceipt {
81            tx_type: tx.tx_type(),
82            // Success flag was added in `EIP-658: Embedding transaction status code in
83            // receipts`.
84            success: result.is_success(),
85            cumulative_gas_used,
86            logs: result.into_logs(),
87        }
88    }
89}
90
91/// Block executor for Tempo. Wraps an inner [`EthBlockExecutor`].
92pub(crate) struct TempoBlockExecutor<'a, DB: Database, I> {
93    pub(crate) inner: EthBlockExecutor<
94        'a,
95        TempoEvm<&'a mut State<DB>, I>,
96        &'a TempoChainSpec,
97        TempoReceiptBuilder,
98    >,
99
100    section: BlockSection,
101    seen_subblocks: Vec<(PartialValidatorKey, Vec<TempoTxEnvelope>)>,
102    validator_set: Option<Vec<B256>>,
103    shared_gas_limit: u64,
104    subblock_fee_recipients: HashMap<PartialValidatorKey, Address>,
105
106    non_shared_gas_left: u64,
107    non_payment_gas_left: u64,
108    incentive_gas_used: u64,
109}
110
111impl<'a, DB, I> TempoBlockExecutor<'a, DB, I>
112where
113    DB: Database,
114    I: Inspector<TempoContext<&'a mut State<DB>>>,
115{
116    pub(crate) fn new(
117        evm: TempoEvm<&'a mut State<DB>, I>,
118        ctx: TempoBlockExecutionCtx<'a>,
119        chain_spec: &'a TempoChainSpec,
120    ) -> Self {
121        Self {
122            incentive_gas_used: 0,
123            validator_set: ctx.validator_set,
124            non_payment_gas_left: ctx.general_gas_limit,
125            non_shared_gas_left: evm.block().gas_limit - ctx.shared_gas_limit,
126            shared_gas_limit: ctx.shared_gas_limit,
127            inner: EthBlockExecutor::new(
128                evm,
129                ctx.inner,
130                chain_spec,
131                TempoReceiptBuilder::default(),
132            ),
133            section: BlockSection::StartOfBlock {
134                seen_tip20_rewards_registry: false,
135            },
136            seen_subblocks: Vec::new(),
137            subblock_fee_recipients: ctx.subblock_fee_recipients,
138        }
139    }
140
141    /// Validates a system transaction.
142    fn validate_system_tx(
143        &self,
144        tx: &TempoTxEnvelope,
145    ) -> Result<BlockSection, BlockValidationError> {
146        let block = self.evm().block();
147        let block_timestamp = block.timestamp;
148        let block_number = block.number.to_be_bytes_vec();
149        let to = tx.to().unwrap_or_default();
150
151        if !self
152            .inner
153            .spec
154            .is_moderato_active_at_timestamp(block_timestamp.to::<u64>())
155        {
156            // Handle start-of-block system transaction (rewards registry)
157            // Only enforce this restriction when we haven't seen the rewards registry yet
158            if let BlockSection::StartOfBlock {
159                seen_tip20_rewards_registry: false,
160            } = self.section
161            {
162                if to != TIP20_REWARDS_REGISTRY_ADDRESS {
163                    return Err(BlockValidationError::msg(
164                        "only rewards registry system transaction allowed at start of block",
165                    ));
166                }
167
168                let finalize_streams_input = ITIP20RewardsRegistry::finalizeStreamsCall {}
169                    .abi_encode()
170                    .into_iter()
171                    .chain(block_number)
172                    .collect::<Bytes>();
173
174                if *tx.input() != finalize_streams_input {
175                    return Err(BlockValidationError::msg(
176                        "invalid TIP20 rewards registry system transaction",
177                    ));
178                }
179
180                return Ok(BlockSection::StartOfBlock {
181                    seen_tip20_rewards_registry: true,
182                });
183            }
184        }
185
186        // Handle end-of-block system transactions (fee manager, DEX, subblocks signatures)
187        let (mut seen_fee_manager, mut seen_stablecoin_dex, mut seen_subblocks_signatures) =
188            match self.section {
189                BlockSection::System {
190                    seen_fee_manager,
191                    seen_stablecoin_dex,
192                    seen_subblocks_signatures,
193                } => (
194                    seen_fee_manager,
195                    seen_stablecoin_dex,
196                    seen_subblocks_signatures,
197                ),
198                _ => (false, false, false),
199            };
200
201        if to == TIP_FEE_MANAGER_ADDRESS {
202            if seen_fee_manager {
203                return Err(BlockValidationError::msg(
204                    "duplicate fee manager system transaction",
205                ));
206            }
207
208            let fee_input = IFeeManager::executeBlockCall
209                .abi_encode()
210                .into_iter()
211                .chain(block_number)
212                .collect::<Bytes>();
213
214            if *tx.input() != fee_input {
215                return Err(BlockValidationError::msg(
216                    "invalid fee manager system transaction",
217                ));
218            }
219
220            seen_fee_manager = true;
221        } else if to == STABLECOIN_EXCHANGE_ADDRESS {
222            if seen_stablecoin_dex {
223                return Err(BlockValidationError::msg(
224                    "duplicate stablecoin DEX system transaction",
225                ));
226            }
227
228            let dex_input = IStablecoinExchange::executeBlockCall {}
229                .abi_encode()
230                .into_iter()
231                .chain(block_number)
232                .collect::<Bytes>();
233
234            if *tx.input() != dex_input {
235                return Err(BlockValidationError::msg(
236                    "invalid stablecoin DEX system transaction",
237                ));
238            }
239
240            seen_stablecoin_dex = true;
241        } else if to.is_zero() {
242            if seen_subblocks_signatures {
243                return Err(BlockValidationError::msg(
244                    "duplicate subblocks metadata system transaction",
245                ));
246            }
247
248            if tx.input().len() < U256::BYTES
249                || tx.input()[tx.input().len() - U256::BYTES..] != block_number
250            {
251                return Err(BlockValidationError::msg(
252                    "invalid subblocks metadata system transaction",
253                ));
254            }
255
256            let mut buf = &tx.input()[..tx.input().len() - U256::BYTES];
257            let Ok(metadata) = Vec::<SubBlockMetadata>::decode(&mut buf) else {
258                return Err(BlockValidationError::msg(
259                    "invalid subblocks metadata system transaction",
260                ));
261            };
262
263            if !buf.is_empty() {
264                return Err(BlockValidationError::msg(
265                    "invalid subblocks metadata system transaction",
266                ));
267            }
268
269            self.validate_shared_gas(&metadata)?;
270
271            seen_subblocks_signatures = true;
272        } else {
273            return Err(BlockValidationError::msg("invalid system transaction"));
274        }
275
276        Ok(BlockSection::System {
277            seen_fee_manager,
278            seen_stablecoin_dex,
279            seen_subblocks_signatures,
280        })
281    }
282
283    fn validate_shared_gas(
284        &self,
285        metadata: &[SubBlockMetadata],
286    ) -> Result<(), BlockValidationError> {
287        // Skip incentive gas validation if validator set context is not available.
288        let Some(validator_set) = &self.validator_set else {
289            return Ok(());
290        };
291        let gas_per_subblock = self.shared_gas_limit / validator_set.len() as u64;
292
293        let mut incentive_gas = 0;
294        let mut seen = HashSet::new();
295        let mut next_non_empty = 0;
296        for metadata in metadata {
297            if !validator_set.contains(&metadata.validator) {
298                return Err(BlockValidationError::msg("invalid subblock validator"));
299            }
300
301            if !seen.insert(metadata.validator) {
302                return Err(BlockValidationError::msg(
303                    "only one subblock per validator is allowed",
304                ));
305            }
306
307            let transactions = if let Some((validator, txs)) =
308                self.seen_subblocks.get(next_non_empty)
309                && validator.matches(metadata.validator)
310            {
311                next_non_empty += 1;
312                txs.clone()
313            } else {
314                Vec::new()
315            };
316
317            let reserved_gas = transactions.iter().map(|tx| tx.gas_limit()).sum::<u64>();
318
319            let signature_hash = SubBlock {
320                version: metadata.version,
321                fee_recipient: metadata.fee_recipient,
322                parent_hash: self.inner.ctx.parent_hash,
323                transactions: transactions.clone(),
324            }
325            .signature_hash();
326
327            let Ok(validator) = PublicKey::decode(&mut metadata.validator.as_ref()) else {
328                return Err(BlockValidationError::msg("invalid subblock validator"));
329            };
330
331            let Ok(signature) = Signature::decode(&mut metadata.signature.as_ref()) else {
332                return Err(BlockValidationError::msg(
333                    "invalid subblock signature encoding",
334                ));
335            };
336
337            if !validator.verify(None, signature_hash.as_slice(), &signature) {
338                return Err(BlockValidationError::msg("invalid subblock signature"));
339            }
340
341            if reserved_gas > gas_per_subblock {
342                return Err(BlockValidationError::msg(
343                    "subblock gas used exceeds gas per subblock",
344                ));
345            }
346
347            incentive_gas += gas_per_subblock - reserved_gas;
348        }
349
350        if next_non_empty != self.seen_subblocks.len() {
351            return Err(BlockValidationError::msg(
352                "failed to map all non-empty subblocks to metadata",
353            ));
354        }
355
356        if incentive_gas < self.incentive_gas_used {
357            return Err(BlockValidationError::msg("incentive gas limit exceeded"));
358        }
359
360        Ok(())
361    }
362
363    fn validate_tx(
364        &self,
365        tx: &TempoTxEnvelope,
366        gas_used: u64,
367    ) -> Result<BlockSection, BlockValidationError> {
368        let block = self.evm().block();
369        let block_timestamp = block.timestamp.to::<u64>();
370        let post_moderato = self
371            .inner
372            .spec
373            .is_moderato_active_at_timestamp(block_timestamp);
374
375        // Start with processing of transaction kinds that require specific sections.
376        if tx.is_system_tx() {
377            self.validate_system_tx(tx)
378        } else if let Some(tx_proposer) = tx.subblock_proposer() {
379            match self.section {
380                BlockSection::StartOfBlock {
381                    seen_tip20_rewards_registry,
382                } if !post_moderato && !seen_tip20_rewards_registry => {
383                    Err(BlockValidationError::msg(
384                        "TIP20 rewards registry system transaction was not seen",
385                    ))
386                }
387                BlockSection::GasIncentive | BlockSection::System { .. } => {
388                    Err(BlockValidationError::msg("subblock section already passed"))
389                }
390                BlockSection::StartOfBlock { .. } | BlockSection::NonShared => {
391                    Ok(BlockSection::SubBlock {
392                        proposer: tx_proposer,
393                    })
394                }
395                BlockSection::SubBlock { proposer } => {
396                    if proposer == tx_proposer
397                        || !self.seen_subblocks.iter().any(|(p, _)| *p == tx_proposer)
398                    {
399                        Ok(BlockSection::SubBlock {
400                            proposer: tx_proposer,
401                        })
402                    } else {
403                        Err(BlockValidationError::msg(
404                            "proposer's subblock already processed",
405                        ))
406                    }
407                }
408            }
409        } else {
410            match self.section {
411                BlockSection::StartOfBlock {
412                    seen_tip20_rewards_registry,
413                } if !post_moderato && !seen_tip20_rewards_registry => {
414                    Err(BlockValidationError::msg(
415                        "TIP20 rewards registry system transaction was not seen",
416                    ))
417                }
418                BlockSection::StartOfBlock { .. } | BlockSection::NonShared => {
419                    if gas_used > self.non_shared_gas_left
420                        || (!tx.is_payment() && gas_used > self.non_payment_gas_left)
421                    {
422                        // Assume that this transaction wants to make use of gas incentive section
423                        //
424                        // This would only be possible if no non-empty subblocks were included.
425                        Ok(BlockSection::GasIncentive)
426                    } else {
427                        Ok(BlockSection::NonShared)
428                    }
429                }
430                BlockSection::SubBlock { .. } => {
431                    // If we were just processing a subblock, assume that this transaction wants to make
432                    // use of gas incentive section, thus concluding subblocks execution.
433                    Ok(BlockSection::GasIncentive)
434                }
435                BlockSection::GasIncentive => Ok(BlockSection::GasIncentive),
436                BlockSection::System { .. } => {
437                    trace!(target: "tempo::block", tx_hash = ?*tx.tx_hash(), "Rejecting: regular transaction after system transaction");
438                    Err(BlockValidationError::msg(
439                        "regular transaction can't follow system transaction",
440                    ))
441                }
442            }
443        }
444    }
445}
446
447impl<'a, DB, I> BlockExecutor for TempoBlockExecutor<'a, DB, I>
448where
449    DB: Database,
450    I: Inspector<TempoContext<&'a mut State<DB>>>,
451{
452    type Transaction = TempoTxEnvelope;
453    type Receipt = TempoReceipt;
454    type Evm = TempoEvm<&'a mut State<DB>, I>;
455
456    fn apply_pre_execution_changes(&mut self) -> Result<(), alloy_evm::block::BlockExecutionError> {
457        self.inner.apply_pre_execution_changes()?;
458
459        // Initialize keychain precompile if allegretto is active
460        let block_timestamp = self.evm().block().timestamp.to::<u64>();
461        if self
462            .inner
463            .spec
464            .is_allegretto_active_at_timestamp(block_timestamp)
465        {
466            let evm = self.evm_mut();
467            let db = evm.ctx_mut().db_mut();
468
469            // Load the keychain account from the cache
470            let acc = db
471                .load_cache_account(ACCOUNT_KEYCHAIN_ADDRESS)
472                .map_err(BlockExecutionError::other)?;
473
474            // Get existing account info or create default
475            let mut acc_info = acc.account_info().unwrap_or_default();
476
477            // Only initialize if the account has no code
478            if acc_info.is_empty_code_hash() {
479                // Set the keychain code
480                let code = Bytecode::new_legacy(Bytes::from_static(&[0xef]));
481                acc_info.code_hash = code.hash_slow();
482                acc_info.code = Some(code);
483
484                // Convert to revm account and mark as touched
485                let mut revm_acc: Account = acc_info.into();
486                revm_acc.mark_touch();
487
488                // Commit the account to the database to ensure it persists
489                // even if no transactions are executed in this block
490                db.commit(HashMap::from_iter([(ACCOUNT_KEYCHAIN_ADDRESS, revm_acc)]));
491            }
492        }
493
494        Ok(())
495    }
496
497    fn execute_transaction_without_commit(
498        &mut self,
499        tx: impl ExecutableTx<Self>,
500    ) -> Result<ResultAndState<TempoHaltReason>, BlockExecutionError> {
501        let beneficiary = self.evm_mut().ctx_mut().block.beneficiary;
502        // If we are dealing with a subblock transaction, configure the fee recipient context.
503        if self.evm().ctx().cfg.spec.is_allegretto()
504            && let Some(validator) = tx.tx().subblock_proposer()
505        {
506            let fee_recipient = *self
507                .subblock_fee_recipients
508                .get(&validator)
509                .ok_or(BlockExecutionError::msg("invalid subblock transaction"))?;
510
511            self.evm_mut().ctx_mut().block.beneficiary = fee_recipient;
512        }
513        let result = self.inner.execute_transaction_without_commit(tx);
514
515        self.evm_mut().ctx_mut().block.beneficiary = beneficiary;
516
517        result
518    }
519
520    fn commit_transaction(
521        &mut self,
522        output: ResultAndState<TempoHaltReason>,
523        tx: impl ExecutableTx<Self>,
524    ) -> Result<u64, BlockExecutionError> {
525        let next_section = self.validate_tx(tx.tx(), output.result.gas_used())?;
526
527        let gas_used = self.inner.commit_transaction(output, &tx)?;
528
529        // TODO: remove once revm supports emitting logs for reverted transactions
530        //
531        // <https://github.com/tempoxyz/tempo/pull/729>
532        let logs = self.inner.evm.take_revert_logs();
533        if !logs.is_empty() {
534            self.inner
535                .receipts
536                .last_mut()
537                .expect("receipt was just pushed")
538                .logs
539                .extend(logs);
540        }
541
542        self.section = next_section;
543
544        match self.section {
545            BlockSection::StartOfBlock { .. } => {
546                // no gas spending for start-of-block system transactions
547            }
548            BlockSection::NonShared => {
549                self.non_shared_gas_left -= gas_used;
550                if !tx.tx().is_payment() {
551                    self.non_payment_gas_left -= gas_used;
552                }
553            }
554            BlockSection::SubBlock { proposer } => {
555                // record subblock transactions to verify later
556                let last_subblock = if let Some(last) = self
557                    .seen_subblocks
558                    .last_mut()
559                    .filter(|(p, _)| *p == proposer)
560                {
561                    last
562                } else {
563                    self.seen_subblocks.push((proposer, Vec::new()));
564                    self.seen_subblocks.last_mut().unwrap()
565                };
566
567                last_subblock.1.push(tx.tx().clone());
568            }
569            BlockSection::GasIncentive => {
570                self.incentive_gas_used += gas_used;
571            }
572            BlockSection::System { .. } => {
573                // no gas spending for end-of-block system transactions
574            }
575        }
576
577        Ok(gas_used)
578    }
579
580    fn finish(
581        self,
582    ) -> Result<(Self::Evm, BlockExecutionResult<Self::Receipt>), BlockExecutionError> {
583        // Check that we ended in the System section with all end-of-block system txs seen
584        if self.section
585            != (BlockSection::System {
586                seen_fee_manager: true,
587                seen_stablecoin_dex: true,
588                seen_subblocks_signatures: true,
589            })
590        {
591            return Err(
592                BlockValidationError::msg("end-of-block system transactions not seen").into(),
593            );
594        }
595        self.inner.finish()
596    }
597
598    fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
599        self.inner.set_state_hook(hook)
600    }
601
602    fn evm_mut(&mut self) -> &mut Self::Evm {
603        self.inner.evm_mut()
604    }
605
606    fn evm(&self) -> &Self::Evm {
607        self.inner.evm()
608    }
609}