Skip to main content

tempo_evm/
block.rs

1use crate::{TempoBlockExecutionCtx, evm::TempoEvm};
2use alloy_consensus::{Transaction, transaction::TxHashRef};
3use alloy_evm::{
4    Database, Evm, RecoveredTx,
5    block::{
6        BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockValidationError,
7        ExecutableTx, GasOutput, TxResult,
8    },
9    eth::{
10        EthBlockExecutor, EthTxResult,
11        receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx},
12    },
13};
14use alloy_primitives::{Address, B256, U256};
15use alloy_rlp::Decodable;
16use commonware_codec::DecodeExt;
17use commonware_cryptography::{
18    Verifier,
19    ed25519::{PublicKey, Signature},
20};
21use reth_evm::block::StateDB;
22use reth_revm::{
23    Inspector,
24    context::result::ResultAndState,
25    state::{Account, Bytecode, EvmState},
26};
27use std::collections::{HashMap, HashSet};
28use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardforks};
29use tempo_contracts::precompiles::{
30    ADDRESS_REGISTRY_ADDRESS, RECEIVE_POLICY_GUARD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS,
31    STORAGE_CREDITS_ADDRESS, TIP20_CHANNEL_RESERVE_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
32};
33use tempo_primitives::{
34    SubBlock, SubBlockMetadata, TempoReceipt, TempoTxEnvelope, TempoTxType,
35    subblock::PartialValidatorKey,
36};
37use tempo_revm::{TempoHaltReason, evm::TempoContext};
38use tracing::trace;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub(crate) enum BlockSection {
42    /// Start of block system transactions.
43    StartOfBlock,
44    /// Basic section of the block. Includes arbitrary transactions chosen by the proposer.
45    ///
46    /// Must use at most `non_shared_gas_left` gas.
47    NonShared,
48    /// Subblock authored by the given validator.
49    SubBlock { proposer: PartialValidatorKey },
50    /// Gas incentive transaction.
51    GasIncentive,
52    /// End of block system transactions.
53    System { seen_subblocks_signatures: bool },
54}
55
56/// Builder for [`TempoReceipt`].
57#[derive(Debug, Clone, Copy, Default)]
58#[non_exhaustive]
59pub struct TempoReceiptBuilder;
60
61impl ReceiptBuilder for TempoReceiptBuilder {
62    type Transaction = TempoTxEnvelope;
63    type Receipt = TempoReceipt;
64
65    fn build_receipt<E: Evm>(&self, ctx: ReceiptBuilderCtx<'_, TempoTxType, E>) -> Self::Receipt {
66        let ReceiptBuilderCtx {
67            tx_type,
68            result,
69            cumulative_gas_used,
70            ..
71        } = ctx;
72        TempoReceipt {
73            tx_type,
74            // Success flag was added in `EIP-658: Embedding transaction status code in
75            // receipts`.
76            success: result.is_success(),
77            cumulative_gas_used,
78            logs: result.into_logs(),
79        }
80    }
81}
82
83/// The result of executing a Tempo transaction.
84///
85/// This is an extension of [`EthTxResult`] with context necessary for committing a Tempo transaction.
86#[derive(Debug)]
87pub struct TempoTxResult {
88    /// Inner transaction execution result.
89    inner: EthTxResult<TempoHaltReason, TempoTxType>,
90    /// Next section of the block.
91    next_section: BlockSection,
92    /// Whether the transaction is a payment transaction.
93    is_payment: bool,
94    /// Full transaction that is being committed.
95    ///
96    /// This is only populated for subblock transactions for which we need to store
97    /// the full transaction encoding for later validation of subblock hash.
98    tx: Option<TempoTxEnvelope>,
99    /// Block gas consumed by this transaction. The block `gas_used` field will be incremented by this value.
100    block_gas_used: u64,
101    /// Validator-credited fee (in the validator's fee token) reported by `collectFeePostTx`.
102    ///
103    /// Used by the payload builder to score blocks by actual proposer revenue. The value is the
104    /// post-feeAMM amount, regardless of route shape — absorbs any number of pool haircuts.
105    validator_fee: U256,
106}
107
108impl TempoTxResult {
109    /// Returns the block gas consumed by this transaction.
110    pub fn block_gas_used(&self) -> u64 {
111        self.block_gas_used
112    }
113
114    /// Returns the state gas consumed by this transaction.
115    pub fn state_gas_used(&self) -> u64 {
116        self.inner.result.result.gas().state_gas_spent_final()
117    }
118
119    /// Returns the validator-credited fee amount (post-feeAMM haircut) for this transaction.
120    pub fn validator_fee(&self) -> U256 {
121        self.validator_fee
122    }
123}
124
125impl TxResult for TempoTxResult {
126    type HaltReason = TempoHaltReason;
127
128    fn result(&self) -> &ResultAndState<Self::HaltReason> {
129        self.inner.result()
130    }
131
132    fn into_result(self) -> ResultAndState<Self::HaltReason> {
133        self.inner.into_result()
134    }
135}
136
137/// Block executor for Tempo.
138///
139/// Wraps an inner [`EthBlockExecutor`] and layers Tempo-specific block execution
140/// logic on top: section-based transaction ordering (`BlockSection`), subblock
141/// validation, shared/non-shared gas accounting, and gas incentive tracking.
142pub struct TempoBlockExecutor<'a, DB: Database, I> {
143    pub(crate) inner:
144        EthBlockExecutor<'a, TempoEvm<DB, I>, &'a TempoChainSpec, TempoReceiptBuilder>,
145
146    section: BlockSection,
147    seen_subblocks: Vec<(PartialValidatorKey, Vec<TempoTxEnvelope>)>,
148    validator_set: Option<Vec<B256>>,
149    shared_gas_limit: u64,
150    subblock_fee_recipients: HashMap<PartialValidatorKey, Address>,
151
152    non_shared_gas_left: u64,
153    non_payment_gas_left: u64,
154    incentive_gas_used: u64,
155}
156
157impl<'a, DB, I> TempoBlockExecutor<'a, DB, I>
158where
159    DB: StateDB,
160    I: Inspector<TempoContext<DB>>,
161{
162    pub(crate) fn new(
163        evm: TempoEvm<DB, I>,
164        ctx: TempoBlockExecutionCtx<'a>,
165        chain_spec: &'a TempoChainSpec,
166    ) -> Self {
167        Self {
168            incentive_gas_used: 0,
169            validator_set: ctx.validator_set,
170            non_payment_gas_left: ctx.general_gas_limit,
171            non_shared_gas_left: evm.block().gas_limit.saturating_sub(ctx.shared_gas_limit),
172            shared_gas_limit: ctx.shared_gas_limit,
173            inner: EthBlockExecutor::new(
174                evm,
175                ctx.inner,
176                chain_spec,
177                TempoReceiptBuilder::default(),
178            ),
179            section: BlockSection::StartOfBlock,
180            seen_subblocks: Vec::new(),
181            subblock_fee_recipients: ctx.subblock_fee_recipients,
182        }
183    }
184
185    /// Deploys `0xEF` marker bytecode to a precompile address if it doesn't already have code.
186    ///
187    /// This also dispatches the state change to the system caller's state hook so that the
188    /// sparse trie task is aware of the change.
189    fn deploy_precompile_at_boundary(
190        &mut self,
191        address: Address,
192    ) -> Result<(), BlockExecutionError> {
193        let info = self
194            .inner
195            .evm
196            .db_mut()
197            .basic(address)
198            .map_err(BlockExecutionError::other)?
199            .unwrap_or_default();
200        if info.is_empty_code_hash() {
201            let mut account = Account::from(info);
202            let code = Bytecode::new_legacy([0xef].into());
203            account.info.code_hash = code.hash_slow();
204            account.info.code = Some(code);
205            account.mark_touch();
206            let state = EvmState::from_iter([(address, account)]);
207            self.inner.evm.db_mut().commit(state);
208        }
209        Ok(())
210    }
211
212    /// Validates a system transaction.
213    pub(crate) fn validate_system_tx(
214        &self,
215        tx: &TempoTxEnvelope,
216    ) -> Result<BlockSection, BlockValidationError> {
217        let block = self.evm().block();
218        let block_number = block.number.to_be_bytes_vec();
219        let to = tx.to().unwrap_or_default();
220
221        // Handle end-of-block system transactions (subblocks signatures only)
222        let mut seen_subblocks_signatures = match self.section {
223            BlockSection::System {
224                seen_subblocks_signatures,
225            } => seen_subblocks_signatures,
226            _ => false,
227        };
228
229        if to.is_zero() {
230            if seen_subblocks_signatures {
231                return Err(BlockValidationError::msg(
232                    "duplicate subblocks metadata system transaction",
233                ));
234            }
235
236            if self.evm().cfg.spec.is_t4() {
237                return Err(BlockValidationError::msg("subblocks are disabled in T4+"));
238            }
239
240            if tx.input().len() < U256::BYTES
241                || tx.input()[tx.input().len() - U256::BYTES..] != block_number
242            {
243                return Err(BlockValidationError::msg(
244                    "invalid subblocks metadata system transaction",
245                ));
246            }
247
248            let mut buf = &tx.input()[..tx.input().len() - U256::BYTES];
249            let Ok(metadata) = Vec::<SubBlockMetadata>::decode(&mut buf) else {
250                return Err(BlockValidationError::msg(
251                    "invalid subblocks metadata system transaction",
252                ));
253            };
254
255            if !buf.is_empty() {
256                return Err(BlockValidationError::msg(
257                    "invalid subblocks metadata system transaction",
258                ));
259            }
260
261            self.validate_shared_gas(&metadata)?;
262
263            seen_subblocks_signatures = true;
264        } else {
265            return Err(BlockValidationError::msg("invalid system transaction"));
266        }
267
268        Ok(BlockSection::System {
269            seen_subblocks_signatures,
270        })
271    }
272
273    pub(crate) fn validate_shared_gas(
274        &self,
275        metadata: &[SubBlockMetadata],
276    ) -> Result<(), BlockValidationError> {
277        // Skip incentive gas validation if validator set context is not available.
278        let Some(validator_set) = &self.validator_set else {
279            return Ok(());
280        };
281        let gas_per_subblock = self
282            .shared_gas_limit
283            .checked_div(validator_set.len() as u64)
284            .expect("validator set must not be empty");
285
286        let mut incentive_gas = 0;
287        let mut seen = HashSet::new();
288        let mut next_non_empty = 0;
289        for metadata in metadata {
290            if !validator_set.contains(&metadata.validator) {
291                return Err(BlockValidationError::msg("invalid subblock validator"));
292            }
293
294            if !seen.insert(metadata.validator) {
295                return Err(BlockValidationError::msg(
296                    "only one subblock per validator is allowed",
297                ));
298            }
299
300            let transactions = if let Some((validator, txs)) =
301                self.seen_subblocks.get(next_non_empty)
302                && validator.matches(metadata.validator)
303            {
304                next_non_empty += 1;
305                txs.clone()
306            } else {
307                Vec::new()
308            };
309
310            let reserved_gas = transactions
311                .iter()
312                .map(|tx| {
313                    core::cmp::min(
314                        tx.gas_limit(),
315                        self.inner.evm.cfg.tx_gas_limit_cap.unwrap_or(u64::MAX),
316                    )
317                })
318                .sum::<u64>();
319
320            let signature_hash = SubBlock {
321                version: metadata.version,
322                fee_recipient: metadata.fee_recipient,
323                parent_hash: self.inner.ctx.parent_hash,
324                transactions: transactions.clone(),
325            }
326            .signature_hash();
327
328            let Ok(validator) = PublicKey::decode(&mut metadata.validator.as_ref()) else {
329                return Err(BlockValidationError::msg("invalid subblock validator"));
330            };
331
332            let Ok(signature) = Signature::decode(&mut metadata.signature.as_ref()) else {
333                return Err(BlockValidationError::msg(
334                    "invalid subblock signature encoding",
335                ));
336            };
337
338            // TODO: Add namespace?
339            if !validator.verify(&[], signature_hash.as_slice(), &signature) {
340                return Err(BlockValidationError::msg("invalid subblock signature"));
341            }
342
343            if reserved_gas > gas_per_subblock {
344                return Err(BlockValidationError::msg(
345                    "subblock gas used exceeds gas per subblock",
346                ));
347            }
348
349            incentive_gas += gas_per_subblock - reserved_gas;
350        }
351
352        if next_non_empty != self.seen_subblocks.len() {
353            return Err(BlockValidationError::msg(
354                "failed to map all non-empty subblocks to metadata",
355            ));
356        }
357
358        if incentive_gas < self.incentive_gas_used {
359            return Err(BlockValidationError::msg("incentive gas limit exceeded"));
360        }
361
362        Ok(())
363    }
364
365    /// Pre-validate a transaction before execution.
366    ///
367    /// This is only done for system transaction as they are effectively bypassing
368    /// the regular block gas limit checks and we need to make sure that they
369    /// only perform explicitly allowed actions.
370    pub(crate) fn validate_tx_pre_execution(
371        &self,
372        tx: &TempoTxEnvelope,
373    ) -> Result<Option<BlockSection>, BlockValidationError> {
374        if tx.is_system_tx() {
375            self.validate_system_tx(tx).map(Some)
376        } else {
377            Ok(None)
378        }
379    }
380
381    /// Returns whether `tx` qualifies for the payment lane under the active hardfork.
382    ///
383    /// T5+: TIP-1045 classification ([`is_payment_v2`]).
384    /// Pre-T5: legacy TIP-20 prefix-only check ([`is_payment_v1`]).
385    ///
386    /// [`is_payment_v1`]: TempoTxEnvelope::is_payment_v1
387    /// [`is_payment_v2`]: TempoTxEnvelope::is_payment_v2
388    pub(crate) fn is_payment(&self, tx: &TempoTxEnvelope) -> bool {
389        if self.evm().cfg.spec.is_t5() {
390            tx.is_payment_v2()
391        } else {
392            tx.is_payment_v1()
393        }
394    }
395
396    pub(crate) fn validate_tx(
397        &self,
398        tx: &TempoTxEnvelope,
399        gas_used: u64,
400    ) -> Result<BlockSection, BlockValidationError> {
401        // Start with processing of transaction kinds that require specific sections.
402        if tx.is_system_tx() {
403            self.validate_system_tx(tx)
404        } else if let Some(tx_proposer) = tx.subblock_proposer() {
405            match self.section {
406                BlockSection::GasIncentive | BlockSection::System { .. } => {
407                    Err(BlockValidationError::msg("subblock section already passed"))
408                }
409                BlockSection::StartOfBlock | BlockSection::NonShared => {
410                    Ok(BlockSection::SubBlock {
411                        proposer: tx_proposer,
412                    })
413                }
414                BlockSection::SubBlock { proposer } => {
415                    if proposer == tx_proposer
416                        || !self.seen_subblocks.iter().any(|(p, _)| *p == tx_proposer)
417                    {
418                        Ok(BlockSection::SubBlock {
419                            proposer: tx_proposer,
420                        })
421                    } else {
422                        Err(BlockValidationError::msg(
423                            "proposer's subblock already processed",
424                        ))
425                    }
426                }
427            }
428        } else {
429            match self.section {
430                BlockSection::StartOfBlock | BlockSection::NonShared => {
431                    if gas_used > self.non_shared_gas_left
432                        || (!self.is_payment(tx) && gas_used > self.non_payment_gas_left)
433                    {
434                        // Assume that this transaction wants to make use of gas incentive section
435                        //
436                        // This would only be possible if no non-empty subblocks were included.
437                        Ok(BlockSection::GasIncentive)
438                    } else {
439                        Ok(BlockSection::NonShared)
440                    }
441                }
442                BlockSection::SubBlock { .. } => {
443                    // If we were just processing a subblock, assume that this transaction wants to make
444                    // use of gas incentive section, thus concluding subblocks execution.
445                    Ok(BlockSection::GasIncentive)
446                }
447                BlockSection::GasIncentive => Ok(BlockSection::GasIncentive),
448                BlockSection::System { .. } => {
449                    trace!(target: "tempo::block", tx_hash = ?*tx.tx_hash(), "Rejecting: regular transaction after system transaction");
450                    Err(BlockValidationError::msg(
451                        "regular transaction can't follow system transaction",
452                    ))
453                }
454            }
455        }
456    }
457}
458
459impl<'a, DB, I> BlockExecutor for TempoBlockExecutor<'a, DB, I>
460where
461    DB: StateDB,
462    I: Inspector<TempoContext<DB>>,
463{
464    type Transaction = TempoTxEnvelope;
465    type Receipt = TempoReceipt;
466    type Evm = TempoEvm<DB, I>;
467    type Result = TempoTxResult;
468
469    fn apply_pre_execution_changes(&mut self) -> Result<(), alloy_evm::block::BlockExecutionError> {
470        if self
471            .inner
472            .ctx
473            .withdrawals
474            .as_ref()
475            .is_some_and(|withdrawals| !withdrawals.is_empty())
476        {
477            return Err(BlockValidationError::msg("withdrawals are not permitted").into());
478        }
479
480        self.inner.apply_pre_execution_changes()?;
481
482        // Deploy 0xEF marker bytecode to precompiles at their activation hardforks.
483        let timestamp = self.evm().block().timestamp.to::<u64>();
484        if self.inner.spec.is_t2_active_at_timestamp(timestamp) {
485            self.deploy_precompile_at_boundary(VALIDATOR_CONFIG_V2_ADDRESS)?;
486        }
487        if self.inner.spec.is_t3_active_at_timestamp(timestamp) {
488            self.deploy_precompile_at_boundary(SIGNATURE_VERIFIER_ADDRESS)?;
489            self.deploy_precompile_at_boundary(ADDRESS_REGISTRY_ADDRESS)?;
490        }
491        if self.inner.spec.is_t5_active_at_timestamp(timestamp) {
492            self.deploy_precompile_at_boundary(TIP20_CHANNEL_RESERVE_ADDRESS)?;
493        }
494        if self.inner.spec.is_t6_active_at_timestamp(timestamp) {
495            self.deploy_precompile_at_boundary(RECEIVE_POLICY_GUARD_ADDRESS)?;
496        }
497        if self.inner.spec.is_t7_active_at_timestamp(timestamp) {
498            self.deploy_precompile_at_boundary(STORAGE_CREDITS_ADDRESS)?;
499        }
500
501        Ok(())
502    }
503
504    fn receipts(&self) -> &[Self::Receipt] {
505        self.inner.receipts()
506    }
507
508    fn execute_transaction_without_commit(
509        &mut self,
510        tx: impl ExecutableTx<Self>,
511    ) -> Result<Self::Result, BlockExecutionError> {
512        let (mut tx_env, recovered) = tx.into_parts();
513        // Remove any prewarming-specific context that was added to the tx env.
514        if let Some(tempo_tx_env) = tx_env.tempo_tx_env.as_mut() {
515            tempo_tx_env.expiring_nonce_idx = None;
516        }
517        let next_section = self.validate_tx_pre_execution(recovered.tx())?;
518
519        let beneficiary = self.evm_mut().ctx_mut().block.beneficiary;
520        // If we are dealing with a subblock transaction, configure the fee recipient context.
521        if let Some(validator) = recovered.tx().subblock_proposer() {
522            let fee_recipient = *self
523                .subblock_fee_recipients
524                .get(&validator)
525                .ok_or(BlockExecutionError::msg("invalid subblock transaction"))?;
526
527            self.evm_mut().ctx_mut().block.beneficiary = fee_recipient;
528        }
529        let result = self
530            .inner
531            .execute_transaction_without_commit((tx_env, &recovered));
532
533        self.evm_mut().ctx_mut().block.beneficiary = beneficiary;
534
535        let inner = result?;
536
537        // TIP-1016 enabled: use block_regular_gas_used (excludes state gas) for section
538        // validation, matching block gas limit semantics. TIP-1016 disabled: use tx_gas_used.
539        let block_gas_used = if self.evm().cfg.enable_amsterdam_eip8037 {
540            inner.result.result.gas().block_regular_gas_used()
541        } else {
542            inner.result.result.tx_gas_used()
543        };
544
545        let next_section = if let Some(next_section) = next_section {
546            // If pre-execution validation returned a section to use, just use it.
547            next_section
548        } else {
549            self.validate_tx(recovered.tx(), block_gas_used)?
550        };
551        // Snapshot the per-tx validator-credited fee set by the handler's `reimburse_caller`
552        let validator_fee = self.evm().validator_fee();
553        Ok(TempoTxResult {
554            inner,
555            next_section,
556            is_payment: self.is_payment(recovered.tx()),
557            tx: matches!(next_section, BlockSection::SubBlock { .. })
558                .then(|| recovered.tx().clone()),
559            block_gas_used,
560            validator_fee,
561        })
562    }
563
564    fn commit_transaction(&mut self, output: Self::Result) -> GasOutput {
565        let TempoTxResult {
566            inner,
567            next_section,
568            is_payment,
569            tx,
570            block_gas_used,
571            validator_fee: _,
572        } = output;
573
574        let gas_output = self.inner.commit_transaction(inner);
575
576        self.section = next_section;
577
578        match self.section {
579            BlockSection::StartOfBlock => {
580                // no gas spending for start-of-block system transactions
581            }
582            BlockSection::NonShared => {
583                self.non_shared_gas_left -= block_gas_used;
584                if !is_payment {
585                    self.non_payment_gas_left -= block_gas_used;
586                }
587            }
588            BlockSection::SubBlock { proposer } => {
589                let last_subblock = if let Some(last) = self
590                    .seen_subblocks
591                    .last_mut()
592                    .filter(|(p, _)| *p == proposer)
593                {
594                    last
595                } else {
596                    self.seen_subblocks.push((proposer, Vec::new()));
597                    self.seen_subblocks.last_mut().unwrap()
598                };
599
600                last_subblock
601                    .1
602                    .push(tx.expect("missing tx for subblock transaction"));
603            }
604            BlockSection::GasIncentive => {
605                self.incentive_gas_used += block_gas_used;
606            }
607            BlockSection::System { .. } => {
608                // no gas spending for end-of-block system transactions
609            }
610        }
611
612        gas_output
613    }
614
615    fn finish(
616        self,
617    ) -> Result<(Self::Evm, BlockExecutionResult<Self::Receipt>), BlockExecutionError> {
618        let seen_subblock_signatures = match self.section {
619            BlockSection::System {
620                seen_subblocks_signatures,
621            } => seen_subblocks_signatures,
622            _ => false,
623        };
624
625        // Post T4, if subblocks metadata transaction was not seen, imply empty metadata.
626        if !seen_subblock_signatures && self.evm().cfg.spec.is_t4() {
627            self.validate_shared_gas(&[])?;
628        }
629
630        let amsterdam_eip8037_enabled = self.evm().cfg.enable_amsterdam_eip8037;
631
632        let regular_gas_used = self.inner.block_regular_gas_used;
633        let (evm, mut result) = self.inner.finish()?;
634
635        // TIP-1016 enabled: block header `gas_used` = block_regular_gas_used.
636        // State gas is charged to users (in receipts) but exempted from block
637        // capacity. block_regular_gas_used is accumulated per-tx as
638        // max(total_spent - state_spent, floor) and is independent of refunds.
639        //
640        // TIP-1016 disabled: use the standard gas_used from the inner executor which equals
641        // cumulative_tx_gas_used (total_spent - refunded), matching the original
642        // block header semantics.
643        if amsterdam_eip8037_enabled {
644            result.gas_used = regular_gas_used;
645        }
646
647        Ok((evm, result))
648    }
649
650    fn evm_mut(&mut self) -> &mut Self::Evm {
651        self.inner.evm_mut()
652    }
653
654    fn evm(&self) -> &Self::Evm {
655        self.inner.evm()
656    }
657}
658
659// Test-only methods to set internal state without exposing fields as pub(crate)
660#[cfg(test)]
661impl<'a, DB, I> TempoBlockExecutor<'a, DB, I>
662where
663    DB: Database,
664    I: Inspector<TempoContext<DB>>,
665{
666    /// Set the block section for testing section transition logic.
667    pub(crate) fn set_section_for_test(&mut self, section: BlockSection) {
668        self.section = section;
669    }
670
671    /// Add a seen subblock for testing shared gas validation.
672    pub(crate) fn add_seen_subblock_for_test(
673        &mut self,
674        proposer: PartialValidatorKey,
675        txs: Vec<TempoTxEnvelope>,
676    ) {
677        self.seen_subblocks.push((proposer, txs));
678    }
679
680    /// Set incentive gas used for testing gas limit validation.
681    pub(crate) fn set_incentive_gas_used_for_test(&mut self, gas: u64) {
682        self.incentive_gas_used = gas;
683    }
684
685    /// Get the current section for assertions.
686    pub(crate) fn section(&self) -> BlockSection {
687        self.section
688    }
689}
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694    use crate::test_utils::{TestExecutorBuilder, test_chainspec, test_evm};
695    use alloy_consensus::{Signed, TxLegacy};
696    use alloy_evm::{block::BlockExecutor, eth::receipt_builder::ReceiptBuilder};
697    use alloy_primitives::{Bytes, Log, Signature, TxKind, bytes::BytesMut};
698    use alloy_rlp::Encodable;
699    use commonware_cryptography::{Signer, ed25519::PrivateKey};
700    use reth_chainspec::EthChainSpec;
701    use reth_revm::{State, state::AccountInfo};
702    use revm::{
703        context::result::{ExecutionResult, ResultGas},
704        database::EmptyDB,
705    };
706    use std::sync::{Arc, Mutex};
707    use tempo_chainspec::spec::DEV;
708    use tempo_contracts::precompiles::PATH_USD_ADDRESS;
709    use tempo_primitives::{
710        SubBlockMetadata, TempoSignature, TempoTransaction, TempoTxType,
711        subblock::{SubBlockVersion, TEMPO_SUBBLOCK_NONCE_KEY_PREFIX},
712        transaction::{Call, envelope::TEMPO_SYSTEM_TX_SIGNATURE},
713    };
714    use tempo_revm::TempoHaltReason;
715
716    fn create_legacy_tx() -> TempoTxEnvelope {
717        let tx = TxLegacy {
718            chain_id: Some(1),
719            nonce: 0,
720            gas_price: 1,
721            gas_limit: 21000,
722            to: TxKind::Call(Address::ZERO),
723            value: U256::ZERO,
724            input: Bytes::new(),
725        };
726        TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, Signature::test_signature()))
727    }
728
729    fn create_tip20_empty_calldata_tx() -> TempoTxEnvelope {
730        let tx = TxLegacy {
731            chain_id: Some(1),
732            nonce: 0,
733            gas_price: 1,
734            gas_limit: 21000,
735            to: TxKind::Call(PATH_USD_ADDRESS),
736            value: U256::ZERO,
737            input: Bytes::new(),
738        };
739        TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, Signature::test_signature()))
740    }
741
742    #[test]
743    fn test_build_receipt() {
744        let builder = TempoReceiptBuilder;
745        let tx = create_legacy_tx();
746        let evm = test_evm(EmptyDB::default());
747
748        let logs = vec![Log::new_unchecked(
749            Address::ZERO,
750            vec![B256::ZERO],
751            Bytes::new(),
752        )];
753        let result: ExecutionResult<TempoHaltReason> = ExecutionResult::Success {
754            reason: revm::context::result::SuccessReason::Return,
755            gas: ResultGas::default().with_total_gas_spent(21000),
756            logs,
757            output: revm::context::result::Output::Call(Bytes::new()),
758        };
759
760        let cumulative_gas_used = 21000;
761
762        let receipt = builder.build_receipt(ReceiptBuilderCtx {
763            tx_type: tx.tx_type(),
764            evm: &evm,
765            result,
766            state: &Default::default(),
767            cumulative_gas_used,
768        });
769
770        assert_eq!(receipt.tx_type, TempoTxType::Legacy);
771        assert!(receipt.success);
772        assert_eq!(receipt.cumulative_gas_used, 21000);
773        assert_eq!(receipt.logs.len(), 1);
774        assert_eq!(receipt.logs[0].address, Address::ZERO);
775    }
776
777    #[test]
778    fn test_validate_system_tx() {
779        let chainspec = test_chainspec();
780        let mut db = State::builder().with_bundle_update().build();
781        let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
782
783        let signer = PrivateKey::from_seed(0);
784        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
785        let input = create_system_tx_input(metadata, 1);
786        let system_tx = create_system_tx(chainspec.chain().id(), input);
787
788        let result = executor.validate_system_tx(&system_tx);
789        assert!(
790            result.is_ok(),
791            "validate_system_tx failed: {:?}",
792            result.err()
793        );
794        assert_eq!(
795            result.unwrap(),
796            BlockSection::System {
797                seen_subblocks_signatures: true
798            }
799        );
800    }
801
802    fn create_system_tx_input(metadata: Vec<SubBlockMetadata>, block_number: u64) -> Bytes {
803        let mut input = BytesMut::new();
804        metadata.encode(&mut input);
805        input.extend_from_slice(&U256::from(block_number).to_be_bytes::<32>());
806        input.freeze().into()
807    }
808
809    fn create_system_tx(chain_id: u64, input: Bytes) -> TempoTxEnvelope {
810        TempoTxEnvelope::Legacy(Signed::new_unhashed(
811            TxLegacy {
812                chain_id: Some(chain_id),
813                nonce: 0,
814                gas_price: 0,
815                gas_limit: 0,
816                to: TxKind::Call(Address::ZERO),
817                value: U256::ZERO,
818                input,
819            },
820            TEMPO_SYSTEM_TX_SIGNATURE,
821        ))
822    }
823
824    fn create_valid_subblock_metadata(parent_hash: B256, signer: &PrivateKey) -> SubBlockMetadata {
825        let validator_key = B256::from_slice(&signer.public_key());
826        let subblock = tempo_primitives::SubBlock {
827            version: SubBlockVersion::V1,
828            parent_hash,
829            fee_recipient: Address::ZERO,
830            transactions: vec![],
831        };
832        let signature_hash = subblock.signature_hash();
833        let signature = signer.sign(&[], signature_hash.as_slice());
834
835        SubBlockMetadata {
836            version: SubBlockVersion::V1,
837            validator: validator_key,
838            fee_recipient: Address::ZERO,
839            signature: Bytes::copy_from_slice(signature.as_ref()),
840        }
841    }
842
843    #[test]
844    fn test_validate_system_tx_duplicate_subblocks_system_tx() {
845        let chainspec = test_chainspec();
846        let mut db = State::builder().with_bundle_update().build();
847        let executor = TestExecutorBuilder::default()
848            .with_section(BlockSection::System {
849                seen_subblocks_signatures: true,
850            })
851            .build(&mut db, &chainspec);
852
853        let signer = PrivateKey::from_seed(0);
854        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
855        let input = create_system_tx_input(metadata, 1);
856        let system_tx = create_system_tx(chainspec.chain().id(), input);
857
858        let result = executor.validate_system_tx(&system_tx);
859        assert!(result.is_err());
860        assert_eq!(
861            result.unwrap_err().to_string(),
862            "duplicate subblocks metadata system transaction"
863        );
864    }
865
866    #[test]
867    fn test_validate_system_tx_invalid_sublocks_metadata() {
868        let chainspec = test_chainspec();
869        let mut db = State::builder().with_bundle_update().build();
870        let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
871
872        let mut input = BytesMut::new();
873        input.extend_from_slice(&[0xff, 0xff, 0xff]); // Invalid RLP
874        input.extend_from_slice(&U256::from(1u64).to_be_bytes::<32>());
875        let system_tx = create_system_tx(chainspec.chain().id(), input.freeze().into());
876
877        let result = executor.validate_system_tx(&system_tx);
878        assert!(result.is_err());
879        assert_eq!(
880            result.unwrap_err().to_string(),
881            "invalid subblocks metadata system transaction"
882        );
883    }
884
885    #[test]
886    fn test_validate_system_tx_invalid_system_tx() {
887        let chainspec = test_chainspec();
888        let mut db = State::builder().with_bundle_update().build();
889        let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
890
891        // Create system tx with non-zero `to` address
892        let system_tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(
893            TxLegacy {
894                chain_id: Some(chainspec.chain().id()),
895                nonce: 0,
896                gas_price: 0,
897                gas_limit: 0,
898                to: TxKind::Call(Address::repeat_byte(0x01)), // Non-zero address
899                value: U256::ZERO,
900                input: Bytes::new(),
901            },
902            TEMPO_SYSTEM_TX_SIGNATURE,
903        ));
904
905        let result = executor.validate_system_tx(&system_tx);
906        assert!(result.is_err());
907        assert_eq!(
908            result.unwrap_err().to_string(),
909            "invalid system transaction"
910        );
911    }
912
913    #[test]
914    fn test_validate_system_tx_rejects_metadata_tx_in_t4() {
915        let chainspec = DEV.clone();
916        let mut db = State::builder().with_bundle_update().build();
917        let mut executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
918
919        // TestExecutorBuilder seeds the default runtime spec, so force the T4 path explicitly.
920        executor.inner.evm.cfg.spec = tempo_chainspec::hardfork::TempoHardfork::T4;
921
922        let signer = PrivateKey::from_seed(0);
923        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
924        let input = create_system_tx_input(metadata, 1);
925        let system_tx = create_system_tx(chainspec.chain().id(), input);
926
927        let result = executor.validate_system_tx(&system_tx);
928        assert!(result.is_err());
929        assert_eq!(
930            result.unwrap_err().to_string(),
931            "subblocks are disabled in T4+"
932        );
933    }
934
935    #[test]
936    fn test_validate_shared_gas() {
937        let chainspec = test_chainspec();
938        let mut db = State::builder().with_bundle_update().build();
939        let signer = PrivateKey::from_seed(0);
940        let validator_key = B256::from_slice(&signer.public_key());
941        let executor = TestExecutorBuilder::default()
942            .with_validator_set(vec![validator_key])
943            .build(&mut db, &chainspec);
944
945        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
946        let result = executor.validate_shared_gas(&metadata);
947        assert!(result.is_ok());
948    }
949
950    #[test]
951    fn test_validate_shared_gas_set_does_not_contain_validator() {
952        let chainspec = test_chainspec();
953        let mut db = State::builder().with_bundle_update().build();
954        let signer = PrivateKey::from_seed(0);
955        let different_validator = B256::repeat_byte(0x42); // Not the signer's key
956        let executor = TestExecutorBuilder::default()
957            .with_validator_set(vec![different_validator])
958            .build(&mut db, &chainspec);
959
960        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
961        let result = executor.validate_shared_gas(&metadata);
962        assert!(result.is_err());
963        assert_eq!(
964            result.unwrap_err().to_string(),
965            "invalid subblock validator"
966        );
967    }
968
969    #[test]
970    fn test_validate_shared_gas_more_than_one_subblock_per_validator() {
971        let chainspec = test_chainspec();
972        let mut db = State::builder().with_bundle_update().build();
973        let signer = PrivateKey::from_seed(0);
974        let validator_key = B256::from_slice(&signer.public_key());
975        let executor = TestExecutorBuilder::default()
976            .with_validator_set(vec![validator_key])
977            .build(&mut db, &chainspec);
978
979        // Same validator appears twice
980        let m = create_valid_subblock_metadata(B256::ZERO, &signer);
981        let metadata = vec![m.clone(), m];
982
983        let result = executor.validate_shared_gas(&metadata);
984        assert!(result.is_err());
985        assert_eq!(
986            result.unwrap_err().to_string(),
987            "only one subblock per validator is allowed"
988        );
989    }
990
991    #[test]
992    fn test_validate_shared_gas_invalid_signature_encoding() {
993        let chainspec = test_chainspec();
994        let mut db = State::builder().with_bundle_update().build();
995        let signer = PrivateKey::from_seed(0);
996        let validator_key = B256::from_slice(&signer.public_key());
997        let executor = TestExecutorBuilder::default()
998            .with_validator_set(vec![validator_key])
999            .build(&mut db, &chainspec);
1000
1001        // Create metadata with invalid signature encoding
1002        let metadata = vec![SubBlockMetadata {
1003            version: SubBlockVersion::V1,
1004            validator: validator_key,
1005            fee_recipient: Address::ZERO,
1006            signature: Bytes::from_static(&[0x01, 0x02, 0x03]),
1007        }];
1008
1009        let result = executor.validate_shared_gas(&metadata);
1010        assert!(result.is_err());
1011        assert_eq!(
1012            result.unwrap_err().to_string(),
1013            "invalid subblock signature encoding"
1014        );
1015    }
1016
1017    #[test]
1018    fn test_validate_shared_gas_invalid_signature() {
1019        let chainspec = test_chainspec();
1020        let mut db = State::builder().with_bundle_update().build();
1021        let signer = PrivateKey::from_seed(0);
1022        let validator_key = B256::from_slice(&signer.public_key());
1023        let executor = TestExecutorBuilder::default()
1024            .with_validator_set(vec![validator_key])
1025            .build(&mut db, &chainspec);
1026
1027        // Create metadata with wrong signature
1028        let wrong_signer = PrivateKey::from_seed(1);
1029        let subblock = tempo_primitives::SubBlock {
1030            version: SubBlockVersion::V1,
1031            parent_hash: B256::ZERO,
1032            fee_recipient: Address::ZERO,
1033            transactions: vec![],
1034        };
1035        let signature_hash = subblock.signature_hash();
1036        let wrong_signature = wrong_signer.sign(&[], signature_hash.as_slice());
1037
1038        let metadata = vec![SubBlockMetadata {
1039            version: SubBlockVersion::V1,
1040            validator: validator_key, // Correct validator
1041            fee_recipient: Address::ZERO,
1042            signature: Bytes::copy_from_slice(wrong_signature.as_ref()), // Wrong signature
1043        }];
1044
1045        let result = executor.validate_shared_gas(&metadata);
1046        assert!(result.is_err());
1047        assert_eq!(
1048            result.unwrap_err().to_string(),
1049            "invalid subblock signature"
1050        );
1051    }
1052
1053    #[test]
1054    fn test_validate_shared_gas_gas_used_exceeds_gas_per_subblock() {
1055        let chainspec = test_chainspec();
1056        let mut db = State::builder().with_bundle_update().build();
1057        let signer = PrivateKey::from_seed(0);
1058        let validator_key = B256::from_slice(&signer.public_key());
1059        let tx = create_legacy_tx();
1060        let proposer = PartialValidatorKey::from_slice(&validator_key[..15]);
1061
1062        // Create subblock with transactions included
1063        let subblock = tempo_primitives::SubBlock {
1064            version: SubBlockVersion::V1,
1065            parent_hash: B256::ZERO,
1066            fee_recipient: Address::ZERO,
1067            transactions: vec![tx.clone()],
1068        };
1069
1070        let executor = TestExecutorBuilder::default()
1071            .with_validator_set(vec![validator_key])
1072            .with_shared_gas_limit(100) // Low shared gas limit
1073            .with_seen_subblock(proposer, vec![tx])
1074            .build(&mut db, &chainspec);
1075        let signature_hash = subblock.signature_hash();
1076        let signature = signer.sign(&[], signature_hash.as_slice());
1077
1078        let metadata = vec![SubBlockMetadata {
1079            version: SubBlockVersion::V1,
1080            validator: validator_key,
1081            fee_recipient: Address::ZERO,
1082            signature: Bytes::copy_from_slice(signature.as_ref()),
1083        }];
1084
1085        let result = executor.validate_shared_gas(&metadata);
1086        assert!(result.is_err());
1087        assert_eq!(
1088            result.unwrap_err().to_string(),
1089            "subblock gas used exceeds gas per subblock"
1090        );
1091    }
1092
1093    #[test]
1094    fn test_validate_shared_gas_unexpected_subblock_len() {
1095        let chainspec = test_chainspec();
1096        let mut db = State::builder().with_bundle_update().build();
1097        let signer = PrivateKey::from_seed(0);
1098        let validator_key = B256::from_slice(&signer.public_key());
1099
1100        // Add a seen subblock from a different validator that won't match metadata
1101        let different_key = B256::repeat_byte(0x99);
1102        let different_proposer = PartialValidatorKey::from_slice(&different_key[..15]);
1103
1104        let executor = TestExecutorBuilder::default()
1105            .with_validator_set(vec![validator_key])
1106            .with_seen_subblock(different_proposer, vec![])
1107            .build(&mut db, &chainspec);
1108
1109        // Metadata has validator_key but seen_subblocks has different_key
1110        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
1111
1112        let result = executor.validate_shared_gas(&metadata);
1113        assert!(result.is_err());
1114        assert_eq!(
1115            result.unwrap_err().to_string(),
1116            "failed to map all non-empty subblocks to metadata"
1117        );
1118    }
1119
1120    #[test]
1121    fn test_validate_shared_gas_limit_exceeded() {
1122        let chainspec = test_chainspec();
1123        let mut db = State::builder().with_bundle_update().build();
1124        let signer = PrivateKey::from_seed(0);
1125        let validator_key = B256::from_slice(&signer.public_key());
1126
1127        // Set incentive_gas_used higher than available incentive gas
1128        let executor = TestExecutorBuilder::default()
1129            .with_validator_set(vec![validator_key])
1130            .with_incentive_gas_used(100_000_000)
1131            .build(&mut db, &chainspec);
1132
1133        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
1134
1135        let result = executor.validate_shared_gas(&metadata);
1136        assert!(result.is_err());
1137        assert_eq!(
1138            result.unwrap_err().to_string(),
1139            "incentive gas limit exceeded"
1140        );
1141    }
1142
1143    #[test]
1144    fn test_is_payment_uses_v2_from_t5() {
1145        let tx = create_tip20_empty_calldata_tx();
1146        assert!(
1147            tx.is_payment_v1(),
1148            "pre-T5 prefix check accepts TIP-20 target"
1149        );
1150        assert!(
1151            !tx.is_payment_v2(),
1152            "T5 classifier rejects empty calldata per TIP-1045"
1153        );
1154
1155        let chainspec = test_chainspec();
1156        let mut db = State::builder().with_bundle_update().build();
1157        let pre_t5_executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
1158        assert!(pre_t5_executor.is_payment(&tx));
1159
1160        let chainspec = DEV.clone();
1161        let mut db = State::builder().with_bundle_update().build();
1162        let mut t5_executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
1163        t5_executor.inner.evm.cfg.spec = tempo_chainspec::hardfork::TempoHardfork::T5;
1164        assert!(!t5_executor.is_payment(&tx));
1165    }
1166
1167    #[test]
1168    fn test_validate_tx() {
1169        let chainspec = test_chainspec();
1170        let mut db = State::builder().with_bundle_update().build();
1171        let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
1172
1173        // Test regular transaction in StartOfBlock section goes to NonShared
1174        let tx = create_legacy_tx();
1175        let result = executor.validate_tx(&tx, 21000);
1176        assert!(result.is_ok());
1177        assert_eq!(result.unwrap(), BlockSection::NonShared);
1178    }
1179
1180    fn create_subblock_tx(proposer: &PartialValidatorKey) -> TempoTxEnvelope {
1181        let mut nonce_bytes = [0u8; 32];
1182        nonce_bytes[0] = TEMPO_SUBBLOCK_NONCE_KEY_PREFIX;
1183        nonce_bytes[1..16].copy_from_slice(proposer.as_slice());
1184
1185        let tx = TempoTransaction {
1186            chain_id: 1,
1187            calls: vec![Call {
1188                to: Address::ZERO.into(),
1189                input: Default::default(),
1190                value: Default::default(),
1191            }],
1192            gas_limit: 21000,
1193            nonce_key: U256::from_be_bytes(nonce_bytes),
1194            max_fee_per_gas: 1,
1195            max_priority_fee_per_gas: 1,
1196            ..Default::default()
1197        };
1198
1199        let signature = TempoSignature::from(Signature::test_signature());
1200        TempoTxEnvelope::AA(tx.into_signed(signature))
1201    }
1202
1203    #[test]
1204    fn test_validate_tx_subblock_section_already_passed() {
1205        let chainspec = test_chainspec();
1206        let mut db = State::builder().with_bundle_update().build();
1207        let signer = PrivateKey::from_seed(0);
1208        let validator_key = B256::from_slice(&signer.public_key());
1209        let proposer = PartialValidatorKey::from_slice(&validator_key[..15]);
1210
1211        // Test with GasIncentive section
1212        let executor = TestExecutorBuilder::default()
1213            .with_section(BlockSection::GasIncentive)
1214            .build(&mut db, &chainspec);
1215
1216        let subblock_tx = create_subblock_tx(&proposer);
1217        let result = executor.validate_tx(&subblock_tx, 21000);
1218        assert!(result.is_err());
1219        assert_eq!(
1220            result.unwrap_err().to_string(),
1221            "subblock section already passed"
1222        );
1223
1224        // Also test with System section
1225        let mut db2 = State::builder().with_bundle_update().build();
1226        let executor2 = TestExecutorBuilder::default()
1227            .with_section(BlockSection::System {
1228                seen_subblocks_signatures: false,
1229            })
1230            .build(&mut db2, &chainspec);
1231
1232        let result = executor2.validate_tx(&subblock_tx, 21000);
1233        assert!(result.is_err());
1234        assert_eq!(
1235            result.unwrap_err().to_string(),
1236            "subblock section already passed"
1237        );
1238    }
1239
1240    #[test]
1241    fn test_validate_tx_proposer_subblock_already_processed() {
1242        let chainspec = test_chainspec();
1243        let mut db = State::builder().with_bundle_update().build();
1244        let signer1 = PrivateKey::from_seed(0);
1245        let validator_key1 = B256::from_slice(&signer1.public_key());
1246        let proposer1 = PartialValidatorKey::from_slice(&validator_key1[..15]);
1247
1248        let signer2 = PrivateKey::from_seed(1);
1249        let validator_key2 = B256::from_slice(&signer2.public_key());
1250        let proposer2 = PartialValidatorKey::from_slice(&validator_key2[..15]);
1251
1252        // Set section to SubBlock with a different proposer, and mark proposer1 as already seen
1253        let executor = TestExecutorBuilder::default()
1254            .with_section(BlockSection::SubBlock {
1255                proposer: proposer2,
1256            })
1257            .with_seen_subblock(proposer1, vec![])
1258            .build(&mut db, &chainspec);
1259
1260        // Try to submit a tx for proposer1 (already processed)
1261        let subblock_tx = create_subblock_tx(&proposer1);
1262        let result = executor.validate_tx(&subblock_tx, 21000);
1263        assert!(result.is_err());
1264        assert_eq!(
1265            result.unwrap_err().to_string(),
1266            "proposer's subblock already processed"
1267        );
1268    }
1269
1270    #[test]
1271    fn test_validate_tx_regular_tx_follow_system_tx() {
1272        let chainspec = test_chainspec();
1273        let mut db = State::builder().with_bundle_update().build();
1274
1275        // Set section to System
1276        let executor = TestExecutorBuilder::default()
1277            .with_section(BlockSection::System {
1278                seen_subblocks_signatures: false,
1279            })
1280            .build(&mut db, &chainspec);
1281
1282        // Try to validate a regular tx
1283        let tx = create_legacy_tx();
1284        let result = executor.validate_tx(&tx, 21000);
1285        assert!(result.is_err());
1286        assert_eq!(
1287            result.unwrap_err().to_string(),
1288            "regular transaction can't follow system transaction"
1289        );
1290    }
1291
1292    #[test]
1293    fn test_commit_transaction() {
1294        let chainspec = test_chainspec();
1295        let mut db = State::builder().with_bundle_update().build();
1296        let mut executor = TestExecutorBuilder::default()
1297            .with_general_gas_limit(30_000_000)
1298            .with_parent_beacon_block_root(B256::ZERO)
1299            .build(&mut db, &chainspec);
1300
1301        // Apply pre-execution changes first
1302        executor.apply_pre_execution_changes().unwrap();
1303
1304        let tx = create_legacy_tx();
1305        let output = TempoTxResult {
1306            inner: EthTxResult {
1307                result: ResultAndState {
1308                    result: revm::context::result::ExecutionResult::Success {
1309                        reason: revm::context::result::SuccessReason::Return,
1310                        gas: ResultGas::default().with_total_gas_spent(21000),
1311                        logs: vec![],
1312                        output: revm::context::result::Output::Call(Bytes::new()),
1313                    },
1314                    state: Default::default(),
1315                },
1316                blob_gas_used: 0,
1317                tx_type: tx.tx_type(),
1318            },
1319            next_section: BlockSection::NonShared,
1320            is_payment: false,
1321            tx: None,
1322            block_gas_used: 21000,
1323            validator_fee: U256::ZERO,
1324        };
1325
1326        let gas_output = executor.commit_transaction(output);
1327
1328        assert_eq!(gas_output.tx_gas_used(), 21000);
1329        assert_eq!(executor.section(), BlockSection::NonShared);
1330    }
1331
1332    #[test]
1333    fn test_finish() {
1334        let chainspec = test_chainspec();
1335        let mut db = State::builder().with_bundle_update().build();
1336        let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
1337
1338        let result = executor.finish();
1339        assert!(result.is_ok());
1340    }
1341
1342    #[test]
1343    fn test_finish_t4_without_metadata_passes_when_incentive_gas_is_zero() {
1344        let chainspec = DEV.clone();
1345        let mut db = State::builder().with_bundle_update().build();
1346        let mut executor = TestExecutorBuilder::default()
1347            .with_parent_beacon_block_root(B256::ZERO)
1348            .with_validator_set(vec![B256::repeat_byte(0x01)])
1349            .build(&mut db, &chainspec);
1350
1351        executor.inner.evm.cfg.spec = tempo_chainspec::hardfork::TempoHardfork::T4;
1352        executor.apply_pre_execution_changes().unwrap();
1353
1354        assert!(executor.finish().is_ok());
1355    }
1356
1357    #[test]
1358    fn test_finish_t4_without_metadata_rejects_incentive_gas() {
1359        let chainspec = DEV.clone();
1360        let mut db = State::builder().with_bundle_update().build();
1361        let mut executor = TestExecutorBuilder::default()
1362            .with_parent_beacon_block_root(B256::ZERO)
1363            .with_validator_set(vec![B256::repeat_byte(0x01)])
1364            .with_incentive_gas_used(1)
1365            .build(&mut db, &chainspec);
1366
1367        executor.inner.evm.cfg.spec = tempo_chainspec::hardfork::TempoHardfork::T4;
1368        executor.apply_pre_execution_changes().unwrap();
1369
1370        match executor.finish() {
1371            Err(err) => assert_eq!(err.to_string(), "incentive gas limit exceeded"),
1372            Ok(_) => panic!("finish should fail when T4 block has incentive gas without metadata"),
1373        }
1374    }
1375
1376    #[test]
1377    fn test_commit_transaction_tracks_total_cumulative_gas() {
1378        let chainspec = test_chainspec();
1379        let mut db = State::builder().with_bundle_update().build();
1380        let mut executor = TestExecutorBuilder::default()
1381            .with_general_gas_limit(30_000_000)
1382            .with_parent_beacon_block_root(B256::ZERO)
1383            .build(&mut db, &chainspec);
1384
1385        executor.apply_pre_execution_changes().unwrap();
1386
1387        let tx = create_legacy_tx();
1388        let output = TempoTxResult {
1389            inner: EthTxResult {
1390                result: ResultAndState {
1391                    result: revm::context::result::ExecutionResult::Success {
1392                        reason: revm::context::result::SuccessReason::Return,
1393                        gas: ResultGas::new_with_state_gas(21000, 0, 0, 0),
1394                        logs: vec![],
1395                        output: revm::context::result::Output::Call(Bytes::new()),
1396                    },
1397                    state: Default::default(),
1398                },
1399                blob_gas_used: 0,
1400                tx_type: tx.tx_type(),
1401            },
1402            next_section: BlockSection::NonShared,
1403            is_payment: false,
1404            tx: None,
1405            block_gas_used: 21000,
1406            validator_fee: U256::ZERO,
1407        };
1408
1409        let gas_output = executor.commit_transaction(output);
1410
1411        // With zero storage creation gas, execution gas equals total gas
1412        assert_eq!(gas_output.tx_gas_used(), 21000);
1413    }
1414
1415    #[test]
1416    fn test_cumulative_gas_accumulates_across_transactions() {
1417        let chainspec = test_chainspec();
1418        let mut db = State::builder().with_bundle_update().build();
1419        let mut executor = TestExecutorBuilder::default()
1420            .with_general_gas_limit(30_000_000)
1421            .with_parent_beacon_block_root(B256::ZERO)
1422            .build(&mut db, &chainspec);
1423
1424        executor.apply_pre_execution_changes().unwrap();
1425
1426        // Commit first transaction (21000 gas)
1427        let tx1 = create_legacy_tx();
1428        let output1 = TempoTxResult {
1429            inner: EthTxResult {
1430                result: ResultAndState {
1431                    result: revm::context::result::ExecutionResult::Success {
1432                        reason: revm::context::result::SuccessReason::Return,
1433                        gas: ResultGas::new_with_state_gas(21000, 0, 0, 0),
1434                        logs: vec![],
1435                        output: revm::context::result::Output::Call(Bytes::new()),
1436                    },
1437                    state: Default::default(),
1438                },
1439                blob_gas_used: 0,
1440                tx_type: tx1.tx_type(),
1441            },
1442            next_section: BlockSection::NonShared,
1443            is_payment: false,
1444            tx: None,
1445            block_gas_used: 21000,
1446            validator_fee: U256::ZERO,
1447        };
1448        executor.commit_transaction(output1);
1449
1450        // Commit second transaction (50000 gas)
1451        let tx2 = create_legacy_tx();
1452        let output2 = TempoTxResult {
1453            inner: EthTxResult {
1454                result: ResultAndState {
1455                    result: revm::context::result::ExecutionResult::Success {
1456                        reason: revm::context::result::SuccessReason::Return,
1457                        gas: ResultGas::new_with_state_gas(50000, 0, 0, 0),
1458                        logs: vec![],
1459                        output: revm::context::result::Output::Call(Bytes::new()),
1460                    },
1461                    state: Default::default(),
1462                },
1463                blob_gas_used: 0,
1464                tx_type: tx2.tx_type(),
1465            },
1466            next_section: BlockSection::NonShared,
1467            is_payment: false,
1468            tx: None,
1469            block_gas_used: 50000,
1470            validator_fee: U256::ZERO,
1471        };
1472        executor.commit_transaction(output2);
1473
1474        // Receipts should have cumulative total gas (tracked by inner executor)
1475        let receipts = executor.receipts();
1476        assert_eq!(receipts[0].cumulative_gas_used, 21000);
1477        assert_eq!(receipts[1].cumulative_gas_used, 71000);
1478    }
1479
1480    #[test]
1481    fn test_finish_returns_execution_gas_for_block_header() {
1482        let chainspec = test_chainspec();
1483        let mut db = State::builder().with_bundle_update().build();
1484        let mut executor = TestExecutorBuilder::default()
1485            .with_general_gas_limit(30_000_000)
1486            .with_parent_beacon_block_root(B256::ZERO)
1487            .with_section(BlockSection::NonShared)
1488            .build(&mut db, &chainspec);
1489
1490        executor.apply_pre_execution_changes().unwrap();
1491
1492        // Manually set state to simulate a committed transaction (no state gas)
1493        executor.inner.cumulative_tx_gas_used += 21000;
1494        executor.inner.block_regular_gas_used += 21000;
1495
1496        let (_, result) = executor.finish().unwrap();
1497        // Block header gas_used = block_regular_gas_used
1498        assert_eq!(result.gas_used, 21000);
1499    }
1500
1501    #[test]
1502    fn test_non_shared_gas_uses_execution_gas_only() {
1503        let chainspec = test_chainspec();
1504        let mut db = State::builder().with_bundle_update().build();
1505        let mut executor = TestExecutorBuilder::default()
1506            .with_general_gas_limit(30_000_000)
1507            .with_parent_beacon_block_root(B256::ZERO)
1508            .build(&mut db, &chainspec);
1509
1510        executor.apply_pre_execution_changes().unwrap();
1511
1512        let initial_non_shared = executor.non_shared_gas_left;
1513
1514        let tx = create_legacy_tx();
1515        let output = TempoTxResult {
1516            inner: EthTxResult {
1517                result: ResultAndState {
1518                    result: revm::context::result::ExecutionResult::Success {
1519                        reason: revm::context::result::SuccessReason::Return,
1520                        gas: ResultGas::new_with_state_gas(50_000, 0, 0, 0),
1521                        logs: vec![],
1522                        output: revm::context::result::Output::Call(Bytes::new()),
1523                    },
1524                    state: Default::default(),
1525                },
1526                blob_gas_used: 0,
1527                tx_type: tx.tx_type(),
1528            },
1529            next_section: BlockSection::NonShared,
1530            is_payment: false,
1531            tx: None,
1532            block_gas_used: 50000,
1533            validator_fee: U256::ZERO,
1534        };
1535        executor.commit_transaction(output);
1536
1537        assert_eq!(executor.non_shared_gas_left, initial_non_shared - 50_000);
1538    }
1539
1540    /// T4: payment lane gas accounting must exclude state gas and use
1541    /// block_regular_gas_used semantics (no refunds, no state gas).
1542    #[test]
1543    fn test_t4_non_shared_gas_excludes_state_gas() {
1544        let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1545        let mut db = State::builder().with_bundle_update().build();
1546        let mut executor = TestExecutorBuilder::default()
1547            .with_general_gas_limit(30_000_000)
1548            .with_parent_beacon_block_root(B256::ZERO)
1549            .with_amsterdam_eip8037_enabled(true)
1550            .build(&mut db, &chainspec);
1551
1552        executor.apply_pre_execution_changes().unwrap();
1553
1554        let initial_non_shared = executor.non_shared_gas_left;
1555        let initial_non_payment = executor.non_payment_gas_left;
1556
1557        // tx with total_gas_spent=300k, state_gas=100k
1558        // block_regular_gas_used = max(300k - 100k, 0) = 200k
1559        // tx_gas_used = max(300k - 0_refund, 0) = 300k
1560        let tx = create_legacy_tx();
1561        let output = TempoTxResult {
1562            inner: EthTxResult {
1563                result: ResultAndState {
1564                    result: revm::context::result::ExecutionResult::Success {
1565                        reason: revm::context::result::SuccessReason::Return,
1566                        gas: ResultGas::new_with_state_gas(300_000, 0, 0, 100_000),
1567                        logs: vec![],
1568                        output: revm::context::result::Output::Call(Bytes::new()),
1569                    },
1570                    state: Default::default(),
1571                },
1572                blob_gas_used: 0,
1573                tx_type: tx.tx_type(),
1574            },
1575            next_section: BlockSection::NonShared,
1576            is_payment: false,
1577            tx: None,
1578            block_gas_used: 200_000,
1579            validator_fee: U256::ZERO,
1580        };
1581        executor.commit_transaction(output);
1582
1583        // non_shared_gas_left should decrease by regular gas (200k), not total (300k)
1584        assert_eq!(
1585            executor.non_shared_gas_left,
1586            initial_non_shared - 200_000,
1587            "T4: non_shared_gas_left should exclude state gas"
1588        );
1589        assert_eq!(
1590            executor.non_payment_gas_left,
1591            initial_non_payment - 200_000,
1592            "T4: non_payment_gas_left should exclude state gas"
1593        );
1594    }
1595
1596    /// T4: incentive gas accounting must also exclude state gas.
1597    #[test]
1598    fn test_t4_incentive_gas_excludes_state_gas() {
1599        let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1600        let mut db = State::builder().with_bundle_update().build();
1601        let mut executor = TestExecutorBuilder::default()
1602            .with_general_gas_limit(30_000_000)
1603            .with_parent_beacon_block_root(B256::ZERO)
1604            .with_amsterdam_eip8037_enabled(true)
1605            .build(&mut db, &chainspec);
1606
1607        executor.apply_pre_execution_changes().unwrap();
1608
1609        let tx = create_legacy_tx();
1610        let output = TempoTxResult {
1611            inner: EthTxResult {
1612                result: ResultAndState {
1613                    result: revm::context::result::ExecutionResult::Success {
1614                        reason: revm::context::result::SuccessReason::Return,
1615                        gas: ResultGas::new_with_state_gas(300_000, 0, 0, 100_000),
1616                        logs: vec![],
1617                        output: revm::context::result::Output::Call(Bytes::new()),
1618                    },
1619                    state: Default::default(),
1620                },
1621                blob_gas_used: 0,
1622                tx_type: tx.tx_type(),
1623            },
1624            next_section: BlockSection::GasIncentive,
1625            is_payment: false,
1626            tx: None,
1627            block_gas_used: 200_000,
1628            validator_fee: U256::ZERO,
1629        };
1630        executor.commit_transaction(output);
1631
1632        assert_eq!(
1633            executor.incentive_gas_used, 200_000,
1634            "T4: incentive_gas_used should exclude state gas"
1635        );
1636    }
1637
1638    #[test]
1639    fn test_apply_pre_execution_deploys_validator_v2_code() {
1640        // Dev chainspec has t2Time: 0, so T2 is active at any timestamp.
1641        let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1642        let mut db = State::builder().with_bundle_update().build();
1643        let mut executor = TestExecutorBuilder::default()
1644            .with_parent_beacon_block_root(B256::ZERO)
1645            .build(&mut db, &chainspec);
1646
1647        executor.apply_pre_execution_changes().unwrap();
1648
1649        let acc = db.load_cache_account(VALIDATOR_CONFIG_V2_ADDRESS).unwrap();
1650        let info = acc.account_info().unwrap();
1651        assert!(!info.is_empty_code_hash());
1652    }
1653
1654    #[test]
1655    fn test_apply_pre_execution_deploys_signature_verifier_code() {
1656        // Dev chainspec has t3Time: 0, so T3 is active at any timestamp.
1657        let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1658        let mut db = State::builder().with_bundle_update().build();
1659        let mut executor = TestExecutorBuilder::default()
1660            .with_parent_beacon_block_root(B256::ZERO)
1661            .build(&mut db, &chainspec);
1662
1663        executor.apply_pre_execution_changes().unwrap();
1664
1665        let acc = db.load_cache_account(SIGNATURE_VERIFIER_ADDRESS).unwrap();
1666        let info = acc.account_info().unwrap();
1667        assert!(!info.is_empty_code_hash());
1668    }
1669
1670    #[test]
1671    fn test_apply_pre_execution_deploys_guard_code() {
1672        // Dev chainspec has t6Time: 0, so T6 is active at any timestamp.
1673        let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1674        let mut db = State::builder().with_bundle_update().build();
1675        let mut executor = TestExecutorBuilder::default()
1676            .with_parent_beacon_block_root(B256::ZERO)
1677            .build(&mut db, &chainspec);
1678
1679        executor.apply_pre_execution_changes().unwrap();
1680
1681        let acc = db.load_cache_account(RECEIVE_POLICY_GUARD_ADDRESS).unwrap();
1682        let info = acc.account_info().unwrap();
1683        assert!(!info.is_empty_code_hash());
1684    }
1685
1686    #[test]
1687    fn test_pre_t3_does_not_deploy_signature_verifier_code() {
1688        // Moderato does not have T4 active (no t3Time set), so the code should NOT be deployed.
1689        let chainspec = test_chainspec();
1690        let mut db = State::builder().with_bundle_update().build();
1691        let mut executor = TestExecutorBuilder::default()
1692            .with_parent_beacon_block_root(B256::ZERO)
1693            .build(&mut db, &chainspec);
1694
1695        executor.apply_pre_execution_changes().unwrap();
1696
1697        let acc = db.load_cache_account(SIGNATURE_VERIFIER_ADDRESS).unwrap();
1698        let info = acc.account_info();
1699        assert!(
1700            info.is_none() || info.unwrap().is_empty_code_hash(),
1701            "SignatureVerifier code should not be deployed before T3"
1702        );
1703    }
1704
1705    #[test]
1706    fn test_deploy_precompile_at_boundary_dispatches_state_hook() {
1707        let chainspec = test_chainspec();
1708        let mut db = State::builder().with_bundle_update().build();
1709        let mut executor = TestExecutorBuilder::default()
1710            .with_parent_beacon_block_root(B256::ZERO)
1711            .build(&mut db, &chainspec);
1712
1713        let hook_calls: Arc<Mutex<Vec<EvmState>>> = Arc::new(Mutex::new(Vec::new()));
1714        let hook_calls_clone = hook_calls.clone();
1715        executor
1716            .evm_mut()
1717            .db_mut()
1718            .set_state_hook(Some(Box::new(move |state: EvmState| {
1719                hook_calls_clone.lock().unwrap().push(state);
1720            })));
1721
1722        let addr = Address::with_last_byte(0xff);
1723        executor.deploy_precompile_at_boundary(addr).unwrap();
1724
1725        // Verify code was deployed.
1726        let acc = db.load_cache_account(addr).unwrap();
1727        let info = acc.account_info().unwrap();
1728        assert!(!info.is_empty_code_hash());
1729
1730        // Verify the state hook was called exactly once with the correct address.
1731        let calls = hook_calls.lock().unwrap();
1732        assert_eq!(calls.len(), 1, "state hook should be called exactly once");
1733        assert!(
1734            calls[0].contains_key(&addr),
1735            "state hook should contain the deployed address"
1736        );
1737        assert_eq!(
1738            calls[0][&addr].original_info(),
1739            Default::default(),
1740            "state hook account should preserve original_info"
1741        );
1742    }
1743
1744    #[test]
1745    fn test_deploy_precompile_at_boundary_preserves_existing_original_info() {
1746        use std::sync::{Arc, Mutex};
1747
1748        let chainspec = test_chainspec();
1749        let mut db = State::builder().with_bundle_update().build();
1750        let addr = Address::with_last_byte(0xfe);
1751        let original_info = AccountInfo {
1752            balance: U256::from(42),
1753            nonce: 7,
1754            ..Default::default()
1755        };
1756        db.insert_account(addr, original_info.clone());
1757
1758        let mut executor = TestExecutorBuilder::default()
1759            .with_parent_beacon_block_root(B256::ZERO)
1760            .build(&mut db, &chainspec);
1761
1762        let hook_calls: Arc<Mutex<Vec<EvmState>>> = Arc::new(Mutex::new(Vec::new()));
1763        let hook_calls_clone = hook_calls.clone();
1764        executor
1765            .evm_mut()
1766            .db_mut()
1767            .set_state_hook(Some(Box::new(move |state: EvmState| {
1768                hook_calls_clone.lock().unwrap().push(state);
1769            })));
1770
1771        executor.deploy_precompile_at_boundary(addr).unwrap();
1772
1773        let calls = hook_calls.lock().unwrap();
1774        assert_eq!(calls.len(), 1, "state hook should be called exactly once");
1775        assert_eq!(
1776            calls[0][&addr].original_info(),
1777            original_info,
1778            "state hook account should preserve existing original_info"
1779        );
1780    }
1781
1782    /// TIP-1016 (T4+): block header `gas_used` = `block_regular_gas_used`.
1783    /// Receipts track `tx_gas_used` (what the user pays, including state gas).
1784    /// The difference between receipts total and header gas_used is the state gas
1785    /// exempted from block capacity.
1786    #[test]
1787    fn test_t4_finish_exempts_state_gas_from_header() {
1788        // DEV chainspec has T4 active at timestamp 0.
1789        let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1790        let mut db = State::builder().with_bundle_update().build();
1791        let mut executor = TestExecutorBuilder::default()
1792            .with_parent_beacon_block_root(B256::ZERO)
1793            .with_amsterdam_eip8037_enabled(true)
1794            .build(&mut db, &chainspec);
1795
1796        executor.apply_pre_execution_changes().unwrap();
1797
1798        // Simulate: tx with total=300k, refund=30k, state=40k
1799        // tx_gas_used = max(300k - 30k, floor) = 270k  (receipt gas)
1800        // block_regular_gas_used = max(300k - 40k, floor) = 260k  (capacity gas)
1801        // block_state_gas_used = 40k
1802        let tx_gas_used = 270_000u64;
1803        let regular_gas = 260_000u64;
1804        let state_gas = 40_000u64;
1805
1806        executor.inner.cumulative_tx_gas_used = tx_gas_used;
1807        executor.inner.block_regular_gas_used = regular_gas;
1808        executor.inner.block_state_gas_used = state_gas;
1809
1810        executor.inner.receipts.push(TempoReceipt {
1811            tx_type: TempoTxType::Legacy,
1812            success: true,
1813            cumulative_gas_used: tx_gas_used,
1814            logs: vec![],
1815        });
1816
1817        let (_evm, result) = executor.finish().expect("finish should succeed");
1818
1819        // T4: Block header gas_used must equal block_regular_gas_used
1820        assert_eq!(
1821            result.gas_used, regular_gas,
1822            "T4 header gas_used ({}) must equal block_regular_gas_used ({})",
1823            result.gas_used, regular_gas
1824        );
1825
1826        // Receipt tracks total gas (what user pays, including state gas)
1827        let last_cumulative = result.receipts.last().unwrap().cumulative_gas_used;
1828        assert_eq!(last_cumulative, tx_gas_used);
1829    }
1830
1831    /// Pre-T4: block header `gas_used` must use cumulative_tx_gas_used (post-refund),
1832    /// not block_regular_gas_used (pre-refund). This is a regression test for a bug
1833    /// where `finish()` unconditionally used block_regular_gas_used, causing re-execution
1834    /// of historical blocks to produce a gas mismatch when transactions had SSTORE refunds.
1835    #[test]
1836    fn test_pre_t4_finish_uses_cumulative_gas_with_refunds() {
1837        let chainspec = test_chainspec(); // MODERATO, T4 not active at timestamp 0
1838
1839        let mut db = State::builder().with_bundle_update().build();
1840        let mut executor = TestExecutorBuilder::default()
1841            .with_parent_beacon_block_root(B256::ZERO)
1842            .build(&mut db, &chainspec);
1843
1844        executor.apply_pre_execution_changes().unwrap();
1845
1846        // Simulate: tx with total_spent=276078, refund=2800, state_gas=0 (pre-T4)
1847        // tx_gas_used = 276078 - 2800 = 273278 (post-refund, what goes in receipts)
1848        // block_regular_gas_used = 276078 (pre-refund, no state gas to subtract)
1849        let cumulative = 273_278u64; // post-refund
1850        let regular = 276_078u64; // pre-refund (no state gas subtraction pre-T4)
1851
1852        executor.inner.cumulative_tx_gas_used = cumulative;
1853        executor.inner.block_regular_gas_used = regular;
1854
1855        executor.inner.receipts.push(TempoReceipt {
1856            tx_type: TempoTxType::Legacy,
1857            success: true,
1858            cumulative_gas_used: cumulative,
1859            logs: vec![],
1860        });
1861
1862        let (_evm, result) = executor.finish().expect("finish should succeed");
1863
1864        // Pre-T4: header gas_used must equal cumulative_tx_gas_used (post-refund),
1865        // NOT block_regular_gas_used (pre-refund).
1866        assert_eq!(
1867            result.gas_used, cumulative,
1868            "pre-T4 header gas_used ({}) must equal cumulative_tx_gas_used ({}), \
1869             not block_regular_gas_used ({})",
1870            result.gas_used, cumulative, regular
1871        );
1872    }
1873}