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, OnStateHook, 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    context_interface::JournalTr,
26    state::{Account, Bytecode, EvmState},
27};
28use std::collections::{HashMap, HashSet};
29use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardforks};
30use tempo_contracts::precompiles::VALIDATOR_CONFIG_V2_ADDRESS;
31use tempo_primitives::{
32    SubBlock, SubBlockMetadata, TempoReceipt, TempoTxEnvelope, TempoTxType,
33    subblock::PartialValidatorKey,
34};
35use tempo_revm::{TempoHaltReason, evm::TempoContext};
36use tracing::trace;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub(crate) enum BlockSection {
40    /// Start of block system transactions.
41    StartOfBlock,
42    /// Basic section of the block. Includes arbitrary transactions chosen by the proposer.
43    ///
44    /// Must use at most `non_shared_gas_left` gas.
45    NonShared,
46    /// Subblock authored by the given validator.
47    SubBlock { proposer: PartialValidatorKey },
48    /// Gas incentive transaction.
49    GasIncentive,
50    /// End of block system transactions.
51    System { seen_subblocks_signatures: bool },
52}
53
54/// Builder for [`TempoReceipt`].
55#[derive(Debug, Clone, Copy, Default)]
56#[non_exhaustive]
57pub struct TempoReceiptBuilder;
58
59impl ReceiptBuilder for TempoReceiptBuilder {
60    type Transaction = TempoTxEnvelope;
61    type Receipt = TempoReceipt;
62
63    fn build_receipt<E: Evm>(&self, ctx: ReceiptBuilderCtx<'_, TempoTxType, E>) -> Self::Receipt {
64        let ReceiptBuilderCtx {
65            tx_type,
66            result,
67            cumulative_gas_used,
68            ..
69        } = ctx;
70        TempoReceipt {
71            tx_type,
72            // Success flag was added in `EIP-658: Embedding transaction status code in
73            // receipts`.
74            success: result.is_success(),
75            cumulative_gas_used,
76            logs: result.into_logs(),
77        }
78    }
79}
80
81/// The result of executing a Tempo transaction.
82///
83/// This is an extension of [`EthTxResult`] with context necessary for committing a Tempo transaction.
84#[derive(Debug)]
85pub(crate) struct TempoTxResult<H> {
86    /// Inner transaction execution result.
87    inner: EthTxResult<H, TempoTxType>,
88    /// Next section of the block.
89    next_section: BlockSection,
90    /// Whether the transaction is a payment transaction.
91    is_payment: bool,
92    /// Full transaction that is being committed.
93    ///
94    /// This is only populated for subblock transactions for which we need to store
95    /// the full transaction encoding for later validation of subblock hash.
96    tx: Option<TempoTxEnvelope>,
97}
98
99impl<H> TxResult for TempoTxResult<H> {
100    type HaltReason = H;
101
102    fn result(&self) -> &ResultAndState<Self::HaltReason> {
103        self.inner.result()
104    }
105}
106
107/// Block executor for Tempo.
108///
109/// Wraps an inner [`EthBlockExecutor`] and layers Tempo-specific block execution
110/// logic on top: section-based transaction ordering ([`BlockSection`]), subblock
111/// validation, shared/non-shared gas accounting, and gas incentive tracking.
112pub(crate) struct TempoBlockExecutor<'a, DB: Database, I> {
113    pub(crate) inner:
114        EthBlockExecutor<'a, TempoEvm<DB, I>, &'a TempoChainSpec, TempoReceiptBuilder>,
115
116    section: BlockSection,
117    seen_subblocks: Vec<(PartialValidatorKey, Vec<TempoTxEnvelope>)>,
118    validator_set: Option<Vec<B256>>,
119    shared_gas_limit: u64,
120    subblock_fee_recipients: HashMap<PartialValidatorKey, Address>,
121
122    non_shared_gas_left: u64,
123    non_payment_gas_left: u64,
124    incentive_gas_used: u64,
125}
126
127impl<'a, DB, I> TempoBlockExecutor<'a, DB, I>
128where
129    DB: StateDB,
130    I: Inspector<TempoContext<DB>>,
131{
132    pub(crate) fn new(
133        evm: TempoEvm<DB, I>,
134        ctx: TempoBlockExecutionCtx<'a>,
135        chain_spec: &'a TempoChainSpec,
136    ) -> Self {
137        Self {
138            incentive_gas_used: 0,
139            validator_set: ctx.validator_set,
140            non_payment_gas_left: ctx.general_gas_limit,
141            non_shared_gas_left: evm.block().gas_limit - ctx.shared_gas_limit,
142            shared_gas_limit: ctx.shared_gas_limit,
143            inner: EthBlockExecutor::new(
144                evm,
145                ctx.inner,
146                chain_spec,
147                TempoReceiptBuilder::default(),
148            ),
149            section: BlockSection::StartOfBlock,
150            seen_subblocks: Vec::new(),
151            subblock_fee_recipients: ctx.subblock_fee_recipients,
152        }
153    }
154
155    /// Validates a system transaction.
156    pub(crate) fn validate_system_tx(
157        &self,
158        tx: &TempoTxEnvelope,
159    ) -> Result<BlockSection, BlockValidationError> {
160        let block = self.evm().block();
161        let block_number = block.number.to_be_bytes_vec();
162        let to = tx.to().unwrap_or_default();
163
164        // Handle end-of-block system transactions (subblocks signatures only)
165        let mut seen_subblocks_signatures = match self.section {
166            BlockSection::System {
167                seen_subblocks_signatures,
168            } => seen_subblocks_signatures,
169            _ => false,
170        };
171
172        if to.is_zero() {
173            if seen_subblocks_signatures {
174                return Err(BlockValidationError::msg(
175                    "duplicate subblocks metadata system transaction",
176                ));
177            }
178
179            if tx.input().len() < U256::BYTES
180                || tx.input()[tx.input().len() - U256::BYTES..] != block_number
181            {
182                return Err(BlockValidationError::msg(
183                    "invalid subblocks metadata system transaction",
184                ));
185            }
186
187            let mut buf = &tx.input()[..tx.input().len() - U256::BYTES];
188            let Ok(metadata) = Vec::<SubBlockMetadata>::decode(&mut buf) else {
189                return Err(BlockValidationError::msg(
190                    "invalid subblocks metadata system transaction",
191                ));
192            };
193
194            if !buf.is_empty() {
195                return Err(BlockValidationError::msg(
196                    "invalid subblocks metadata system transaction",
197                ));
198            }
199
200            self.validate_shared_gas(&metadata)?;
201
202            seen_subblocks_signatures = true;
203        } else {
204            return Err(BlockValidationError::msg("invalid system transaction"));
205        }
206
207        Ok(BlockSection::System {
208            seen_subblocks_signatures,
209        })
210    }
211
212    pub(crate) fn validate_shared_gas(
213        &self,
214        metadata: &[SubBlockMetadata],
215    ) -> Result<(), BlockValidationError> {
216        // Skip incentive gas validation if validator set context is not available.
217        let Some(validator_set) = &self.validator_set else {
218            return Ok(());
219        };
220        let gas_per_subblock = self
221            .shared_gas_limit
222            .checked_div(validator_set.len() as u64)
223            .expect("validator set must not be empty");
224
225        let mut incentive_gas = 0;
226        let mut seen = HashSet::new();
227        let mut next_non_empty = 0;
228        for metadata in metadata {
229            if !validator_set.contains(&metadata.validator) {
230                return Err(BlockValidationError::msg("invalid subblock validator"));
231            }
232
233            if !seen.insert(metadata.validator) {
234                return Err(BlockValidationError::msg(
235                    "only one subblock per validator is allowed",
236                ));
237            }
238
239            let transactions = if let Some((validator, txs)) =
240                self.seen_subblocks.get(next_non_empty)
241                && validator.matches(metadata.validator)
242            {
243                next_non_empty += 1;
244                txs.clone()
245            } else {
246                Vec::new()
247            };
248
249            let reserved_gas = transactions.iter().map(|tx| tx.gas_limit()).sum::<u64>();
250
251            let signature_hash = SubBlock {
252                version: metadata.version,
253                fee_recipient: metadata.fee_recipient,
254                parent_hash: self.inner.ctx.parent_hash,
255                transactions: transactions.clone(),
256            }
257            .signature_hash();
258
259            let Ok(validator) = PublicKey::decode(&mut metadata.validator.as_ref()) else {
260                return Err(BlockValidationError::msg("invalid subblock validator"));
261            };
262
263            let Ok(signature) = Signature::decode(&mut metadata.signature.as_ref()) else {
264                return Err(BlockValidationError::msg(
265                    "invalid subblock signature encoding",
266                ));
267            };
268
269            // TODO: Add namespace?
270            if !validator.verify(&[], signature_hash.as_slice(), &signature) {
271                return Err(BlockValidationError::msg("invalid subblock signature"));
272            }
273
274            if reserved_gas > gas_per_subblock {
275                return Err(BlockValidationError::msg(
276                    "subblock gas used exceeds gas per subblock",
277                ));
278            }
279
280            incentive_gas += gas_per_subblock - reserved_gas;
281        }
282
283        if next_non_empty != self.seen_subblocks.len() {
284            return Err(BlockValidationError::msg(
285                "failed to map all non-empty subblocks to metadata",
286            ));
287        }
288
289        if incentive_gas < self.incentive_gas_used {
290            return Err(BlockValidationError::msg("incentive gas limit exceeded"));
291        }
292
293        Ok(())
294    }
295
296    pub(crate) fn validate_tx(
297        &self,
298        tx: &TempoTxEnvelope,
299        gas_used: u64,
300    ) -> Result<BlockSection, BlockValidationError> {
301        // Start with processing of transaction kinds that require specific sections.
302        if tx.is_system_tx() {
303            self.validate_system_tx(tx)
304        } else if let Some(tx_proposer) = tx.subblock_proposer() {
305            match self.section {
306                BlockSection::GasIncentive | BlockSection::System { .. } => {
307                    Err(BlockValidationError::msg("subblock section already passed"))
308                }
309                BlockSection::StartOfBlock | BlockSection::NonShared => {
310                    Ok(BlockSection::SubBlock {
311                        proposer: tx_proposer,
312                    })
313                }
314                BlockSection::SubBlock { proposer } => {
315                    if proposer == tx_proposer
316                        || !self.seen_subblocks.iter().any(|(p, _)| *p == tx_proposer)
317                    {
318                        Ok(BlockSection::SubBlock {
319                            proposer: tx_proposer,
320                        })
321                    } else {
322                        Err(BlockValidationError::msg(
323                            "proposer's subblock already processed",
324                        ))
325                    }
326                }
327            }
328        } else {
329            match self.section {
330                BlockSection::StartOfBlock | BlockSection::NonShared => {
331                    if gas_used > self.non_shared_gas_left
332                        || (!tx.is_payment_v1() && gas_used > self.non_payment_gas_left)
333                    {
334                        // Assume that this transaction wants to make use of gas incentive section
335                        //
336                        // This would only be possible if no non-empty subblocks were included.
337                        Ok(BlockSection::GasIncentive)
338                    } else {
339                        Ok(BlockSection::NonShared)
340                    }
341                }
342                BlockSection::SubBlock { .. } => {
343                    // If we were just processing a subblock, assume that this transaction wants to make
344                    // use of gas incentive section, thus concluding subblocks execution.
345                    Ok(BlockSection::GasIncentive)
346                }
347                BlockSection::GasIncentive => Ok(BlockSection::GasIncentive),
348                BlockSection::System { .. } => {
349                    trace!(target: "tempo::block", tx_hash = ?*tx.tx_hash(), "Rejecting: regular transaction after system transaction");
350                    Err(BlockValidationError::msg(
351                        "regular transaction can't follow system transaction",
352                    ))
353                }
354            }
355        }
356    }
357}
358
359impl<'a, DB, I> BlockExecutor for TempoBlockExecutor<'a, DB, I>
360where
361    DB: StateDB,
362    I: Inspector<TempoContext<DB>>,
363{
364    type Transaction = TempoTxEnvelope;
365    type Receipt = TempoReceipt;
366    type Evm = TempoEvm<DB, I>;
367    type Result = TempoTxResult<TempoHaltReason>;
368
369    fn apply_pre_execution_changes(&mut self) -> Result<(), alloy_evm::block::BlockExecutionError> {
370        self.inner.apply_pre_execution_changes()?;
371
372        // Deploy 0xEF marker bytecode to ValidatorConfigV2 when T2 activates.
373        let timestamp = self.evm().block().timestamp.to::<u64>();
374        if self.inner.spec.is_t2_active_at_timestamp(timestamp) {
375            let db = self.evm_mut().ctx_mut().journaled_state.db_mut();
376            let mut info = db
377                .basic(VALIDATOR_CONFIG_V2_ADDRESS)
378                .map_err(BlockExecutionError::other)?
379                .unwrap_or_default();
380            if info.is_empty_code_hash() {
381                let code = Bytecode::new_legacy([0xef].into());
382                info.code_hash = code.hash_slow();
383                info.code = Some(code);
384                let mut account: Account = info.into();
385                account.mark_touch();
386                db.commit(EvmState::from_iter([(
387                    VALIDATOR_CONFIG_V2_ADDRESS,
388                    account,
389                )]));
390            }
391        }
392
393        Ok(())
394    }
395
396    fn receipts(&self) -> &[Self::Receipt] {
397        self.inner.receipts()
398    }
399
400    fn execute_transaction_without_commit(
401        &mut self,
402        tx: impl ExecutableTx<Self>,
403    ) -> Result<Self::Result, BlockExecutionError> {
404        let (tx_env, recovered) = tx.into_parts();
405
406        let beneficiary = self.evm_mut().ctx_mut().block.beneficiary;
407        // If we are dealing with a subblock transaction, configure the fee recipient context.
408        if let Some(validator) = recovered.tx().subblock_proposer() {
409            let fee_recipient = *self
410                .subblock_fee_recipients
411                .get(&validator)
412                .ok_or(BlockExecutionError::msg("invalid subblock transaction"))?;
413
414            self.evm_mut().ctx_mut().block.beneficiary = fee_recipient;
415        }
416        let result = self
417            .inner
418            .execute_transaction_without_commit((tx_env, &recovered));
419
420        self.evm_mut().ctx_mut().block.beneficiary = beneficiary;
421
422        let inner = result?;
423
424        let next_section = self.validate_tx(recovered.tx(), inner.result.result.gas_used())?;
425        Ok(TempoTxResult {
426            inner,
427            next_section,
428            is_payment: recovered.tx().is_payment_v1(),
429            tx: matches!(next_section, BlockSection::SubBlock { .. })
430                .then(|| recovered.tx().clone()),
431        })
432    }
433
434    fn commit_transaction(&mut self, output: Self::Result) -> Result<u64, BlockExecutionError> {
435        let TempoTxResult {
436            inner,
437            next_section,
438            is_payment,
439            tx,
440        } = output;
441
442        let gas_used = self.inner.commit_transaction(inner)?;
443
444        // TODO: remove once revm supports emitting logs for reverted transactions
445        //
446        // <https://github.com/tempoxyz/tempo/pull/729>
447        let logs = self.inner.evm.take_revert_logs();
448        if !logs.is_empty() {
449            self.inner
450                .receipts
451                .last_mut()
452                .expect("receipt was just pushed")
453                .logs
454                .extend(logs);
455        }
456
457        self.section = next_section;
458
459        match self.section {
460            BlockSection::StartOfBlock => {
461                // no gas spending for start-of-block system transactions
462            }
463            BlockSection::NonShared => {
464                self.non_shared_gas_left -= gas_used;
465                if !is_payment {
466                    self.non_payment_gas_left -= gas_used;
467                }
468            }
469            BlockSection::SubBlock { proposer } => {
470                let last_subblock = if let Some(last) = self
471                    .seen_subblocks
472                    .last_mut()
473                    .filter(|(p, _)| *p == proposer)
474                {
475                    last
476                } else {
477                    self.seen_subblocks.push((proposer, Vec::new()));
478                    self.seen_subblocks.last_mut().unwrap()
479                };
480
481                last_subblock.1.push(tx.ok_or_else(|| {
482                    BlockExecutionError::msg("missing tx for subblock transaction")
483                })?);
484            }
485            BlockSection::GasIncentive => {
486                self.incentive_gas_used += gas_used;
487            }
488            BlockSection::System { .. } => {
489                // no gas spending for end-of-block system transactions
490            }
491        }
492
493        Ok(gas_used)
494    }
495
496    fn finish(
497        self,
498    ) -> Result<(Self::Evm, BlockExecutionResult<Self::Receipt>), BlockExecutionError> {
499        self.inner.finish()
500    }
501
502    fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
503        self.inner.set_state_hook(hook)
504    }
505
506    fn evm_mut(&mut self) -> &mut Self::Evm {
507        self.inner.evm_mut()
508    }
509
510    fn evm(&self) -> &Self::Evm {
511        self.inner.evm()
512    }
513}
514
515// Test-only methods to set internal state without exposing fields as pub(crate)
516#[cfg(test)]
517impl<'a, DB, I> TempoBlockExecutor<'a, DB, I>
518where
519    DB: Database,
520    I: Inspector<TempoContext<DB>>,
521{
522    /// Set the block section for testing section transition logic.
523    pub(crate) fn set_section_for_test(&mut self, section: BlockSection) {
524        self.section = section;
525    }
526
527    /// Add a seen subblock for testing shared gas validation.
528    pub(crate) fn add_seen_subblock_for_test(
529        &mut self,
530        proposer: PartialValidatorKey,
531        txs: Vec<TempoTxEnvelope>,
532    ) {
533        self.seen_subblocks.push((proposer, txs));
534    }
535
536    /// Set incentive gas used for testing gas limit validation.
537    pub(crate) fn set_incentive_gas_used_for_test(&mut self, gas: u64) {
538        self.incentive_gas_used = gas;
539    }
540
541    /// Get the current section for assertions.
542    pub(crate) fn section(&self) -> BlockSection {
543        self.section
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550    use crate::test_utils::{TestExecutorBuilder, test_chainspec, test_evm};
551    use alloy_consensus::{Signed, TxLegacy};
552    use alloy_evm::{block::BlockExecutor, eth::receipt_builder::ReceiptBuilder};
553    use alloy_primitives::{Bytes, Log, Signature, TxKind, bytes::BytesMut};
554    use alloy_rlp::Encodable;
555    use commonware_cryptography::{Signer, ed25519::PrivateKey};
556    use reth_chainspec::EthChainSpec;
557    use reth_revm::State;
558    use revm::{
559        context::result::{ExecutionResult, ResultGas},
560        database::EmptyDB,
561    };
562    use tempo_primitives::{
563        SubBlockMetadata, TempoSignature, TempoTransaction, TempoTxType,
564        subblock::{SubBlockVersion, TEMPO_SUBBLOCK_NONCE_KEY_PREFIX},
565        transaction::{Call, envelope::TEMPO_SYSTEM_TX_SIGNATURE},
566    };
567    use tempo_revm::TempoHaltReason;
568
569    fn create_legacy_tx() -> TempoTxEnvelope {
570        let tx = TxLegacy {
571            chain_id: Some(1),
572            nonce: 0,
573            gas_price: 1,
574            gas_limit: 21000,
575            to: TxKind::Call(Address::ZERO),
576            value: U256::ZERO,
577            input: Bytes::new(),
578        };
579        TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, Signature::test_signature()))
580    }
581
582    #[test]
583    fn test_build_receipt() {
584        let builder = TempoReceiptBuilder;
585        let tx = create_legacy_tx();
586        let evm = test_evm(EmptyDB::default());
587
588        let logs = vec![Log::new_unchecked(
589            Address::ZERO,
590            vec![B256::ZERO],
591            Bytes::new(),
592        )];
593        let result: ExecutionResult<TempoHaltReason> = ExecutionResult::Success {
594            reason: revm::context::result::SuccessReason::Return,
595            gas: ResultGas::default().with_limit(21000).with_spent(21000),
596            logs,
597            output: revm::context::result::Output::Call(Bytes::new()),
598        };
599
600        let cumulative_gas_used = 21000;
601
602        let receipt = builder.build_receipt(ReceiptBuilderCtx {
603            tx_type: tx.tx_type(),
604            evm: &evm,
605            result,
606            state: &Default::default(),
607            cumulative_gas_used,
608        });
609
610        assert_eq!(receipt.tx_type, TempoTxType::Legacy);
611        assert!(receipt.success);
612        assert_eq!(receipt.cumulative_gas_used, 21000);
613        assert_eq!(receipt.logs.len(), 1);
614        assert_eq!(receipt.logs[0].address, Address::ZERO);
615    }
616
617    #[test]
618    fn test_validate_system_tx() {
619        let chainspec = test_chainspec();
620        let mut db = State::builder().with_bundle_update().build();
621        let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
622
623        let signer = PrivateKey::from_seed(0);
624        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
625        let input = create_system_tx_input(metadata, 1);
626        let system_tx = create_system_tx(chainspec.chain().id(), input);
627
628        let result = executor.validate_system_tx(&system_tx);
629        assert!(
630            result.is_ok(),
631            "validate_system_tx failed: {:?}",
632            result.err()
633        );
634        assert_eq!(
635            result.unwrap(),
636            BlockSection::System {
637                seen_subblocks_signatures: true
638            }
639        );
640    }
641
642    fn create_system_tx_input(metadata: Vec<SubBlockMetadata>, block_number: u64) -> Bytes {
643        let mut input = BytesMut::new();
644        metadata.encode(&mut input);
645        input.extend_from_slice(&U256::from(block_number).to_be_bytes::<32>());
646        input.freeze().into()
647    }
648
649    fn create_system_tx(chain_id: u64, input: Bytes) -> TempoTxEnvelope {
650        TempoTxEnvelope::Legacy(Signed::new_unhashed(
651            TxLegacy {
652                chain_id: Some(chain_id),
653                nonce: 0,
654                gas_price: 0,
655                gas_limit: 0,
656                to: TxKind::Call(Address::ZERO),
657                value: U256::ZERO,
658                input,
659            },
660            TEMPO_SYSTEM_TX_SIGNATURE,
661        ))
662    }
663
664    fn create_valid_subblock_metadata(parent_hash: B256, signer: &PrivateKey) -> SubBlockMetadata {
665        let validator_key = B256::from_slice(&signer.public_key());
666        let subblock = tempo_primitives::SubBlock {
667            version: SubBlockVersion::V1,
668            parent_hash,
669            fee_recipient: Address::ZERO,
670            transactions: vec![],
671        };
672        let signature_hash = subblock.signature_hash();
673        let signature = signer.sign(&[], signature_hash.as_slice());
674
675        SubBlockMetadata {
676            version: SubBlockVersion::V1,
677            validator: validator_key,
678            fee_recipient: Address::ZERO,
679            signature: Bytes::copy_from_slice(signature.as_ref()),
680        }
681    }
682
683    #[test]
684    fn test_validate_system_tx_duplicate_subblocks_system_tx() {
685        let chainspec = test_chainspec();
686        let mut db = State::builder().with_bundle_update().build();
687        let executor = TestExecutorBuilder::default()
688            .with_section(BlockSection::System {
689                seen_subblocks_signatures: true,
690            })
691            .build(&mut db, &chainspec);
692
693        let signer = PrivateKey::from_seed(0);
694        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
695        let input = create_system_tx_input(metadata, 1);
696        let system_tx = create_system_tx(chainspec.chain().id(), input);
697
698        let result = executor.validate_system_tx(&system_tx);
699        assert!(result.is_err());
700        assert_eq!(
701            result.unwrap_err().to_string(),
702            "duplicate subblocks metadata system transaction"
703        );
704    }
705
706    #[test]
707    fn test_validate_system_tx_invalid_sublocks_metadata() {
708        let chainspec = test_chainspec();
709        let mut db = State::builder().with_bundle_update().build();
710        let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
711
712        let mut input = BytesMut::new();
713        input.extend_from_slice(&[0xff, 0xff, 0xff]); // Invalid RLP
714        input.extend_from_slice(&U256::from(1u64).to_be_bytes::<32>());
715        let system_tx = create_system_tx(chainspec.chain().id(), input.freeze().into());
716
717        let result = executor.validate_system_tx(&system_tx);
718        assert!(result.is_err());
719        assert_eq!(
720            result.unwrap_err().to_string(),
721            "invalid subblocks metadata system transaction"
722        );
723    }
724
725    #[test]
726    fn test_validate_system_tx_invalid_system_tx() {
727        let chainspec = test_chainspec();
728        let mut db = State::builder().with_bundle_update().build();
729        let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
730
731        // Create system tx with non-zero `to` address
732        let system_tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(
733            TxLegacy {
734                chain_id: Some(chainspec.chain().id()),
735                nonce: 0,
736                gas_price: 0,
737                gas_limit: 0,
738                to: TxKind::Call(Address::repeat_byte(0x01)), // Non-zero address
739                value: U256::ZERO,
740                input: Bytes::new(),
741            },
742            TEMPO_SYSTEM_TX_SIGNATURE,
743        ));
744
745        let result = executor.validate_system_tx(&system_tx);
746        assert!(result.is_err());
747        assert_eq!(
748            result.unwrap_err().to_string(),
749            "invalid system transaction"
750        );
751    }
752
753    #[test]
754    fn test_validate_shared_gas() {
755        let chainspec = test_chainspec();
756        let mut db = State::builder().with_bundle_update().build();
757        let signer = PrivateKey::from_seed(0);
758        let validator_key = B256::from_slice(&signer.public_key());
759        let executor = TestExecutorBuilder::default()
760            .with_validator_set(vec![validator_key])
761            .build(&mut db, &chainspec);
762
763        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
764        let result = executor.validate_shared_gas(&metadata);
765        assert!(result.is_ok());
766    }
767
768    #[test]
769    fn test_validate_shared_gas_set_does_not_contain_validator() {
770        let chainspec = test_chainspec();
771        let mut db = State::builder().with_bundle_update().build();
772        let signer = PrivateKey::from_seed(0);
773        let different_validator = B256::repeat_byte(0x42); // Not the signer's key
774        let executor = TestExecutorBuilder::default()
775            .with_validator_set(vec![different_validator])
776            .build(&mut db, &chainspec);
777
778        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
779        let result = executor.validate_shared_gas(&metadata);
780        assert!(result.is_err());
781        assert_eq!(
782            result.unwrap_err().to_string(),
783            "invalid subblock validator"
784        );
785    }
786
787    #[test]
788    fn test_validate_shared_gas_more_than_one_subblock_per_validator() {
789        let chainspec = test_chainspec();
790        let mut db = State::builder().with_bundle_update().build();
791        let signer = PrivateKey::from_seed(0);
792        let validator_key = B256::from_slice(&signer.public_key());
793        let executor = TestExecutorBuilder::default()
794            .with_validator_set(vec![validator_key])
795            .build(&mut db, &chainspec);
796
797        // Same validator appears twice
798        let m = create_valid_subblock_metadata(B256::ZERO, &signer);
799        let metadata = vec![m.clone(), m];
800
801        let result = executor.validate_shared_gas(&metadata);
802        assert!(result.is_err());
803        assert_eq!(
804            result.unwrap_err().to_string(),
805            "only one subblock per validator is allowed"
806        );
807    }
808
809    #[test]
810    fn test_validate_shared_gas_invalid_signature_encoding() {
811        let chainspec = test_chainspec();
812        let mut db = State::builder().with_bundle_update().build();
813        let signer = PrivateKey::from_seed(0);
814        let validator_key = B256::from_slice(&signer.public_key());
815        let executor = TestExecutorBuilder::default()
816            .with_validator_set(vec![validator_key])
817            .build(&mut db, &chainspec);
818
819        // Create metadata with invalid signature encoding
820        let metadata = vec![SubBlockMetadata {
821            version: SubBlockVersion::V1,
822            validator: validator_key,
823            fee_recipient: Address::ZERO,
824            signature: Bytes::from_static(&[0x01, 0x02, 0x03]),
825        }];
826
827        let result = executor.validate_shared_gas(&metadata);
828        assert!(result.is_err());
829        assert_eq!(
830            result.unwrap_err().to_string(),
831            "invalid subblock signature encoding"
832        );
833    }
834
835    #[test]
836    fn test_validate_shared_gas_invalid_signature() {
837        let chainspec = test_chainspec();
838        let mut db = State::builder().with_bundle_update().build();
839        let signer = PrivateKey::from_seed(0);
840        let validator_key = B256::from_slice(&signer.public_key());
841        let executor = TestExecutorBuilder::default()
842            .with_validator_set(vec![validator_key])
843            .build(&mut db, &chainspec);
844
845        // Create metadata with wrong signature
846        let wrong_signer = PrivateKey::from_seed(1);
847        let subblock = tempo_primitives::SubBlock {
848            version: SubBlockVersion::V1,
849            parent_hash: B256::ZERO,
850            fee_recipient: Address::ZERO,
851            transactions: vec![],
852        };
853        let signature_hash = subblock.signature_hash();
854        let wrong_signature = wrong_signer.sign(&[], signature_hash.as_slice());
855
856        let metadata = vec![SubBlockMetadata {
857            version: SubBlockVersion::V1,
858            validator: validator_key, // Correct validator
859            fee_recipient: Address::ZERO,
860            signature: Bytes::copy_from_slice(wrong_signature.as_ref()), // Wrong signature
861        }];
862
863        let result = executor.validate_shared_gas(&metadata);
864        assert!(result.is_err());
865        assert_eq!(
866            result.unwrap_err().to_string(),
867            "invalid subblock signature"
868        );
869    }
870
871    #[test]
872    fn test_validate_shared_gas_gas_used_exceeds_gas_per_subblock() {
873        let chainspec = test_chainspec();
874        let mut db = State::builder().with_bundle_update().build();
875        let signer = PrivateKey::from_seed(0);
876        let validator_key = B256::from_slice(&signer.public_key());
877        let tx = create_legacy_tx();
878        let proposer = PartialValidatorKey::from_slice(&validator_key[..15]);
879
880        // Create subblock with transactions included
881        let subblock = tempo_primitives::SubBlock {
882            version: SubBlockVersion::V1,
883            parent_hash: B256::ZERO,
884            fee_recipient: Address::ZERO,
885            transactions: vec![tx.clone()],
886        };
887
888        let executor = TestExecutorBuilder::default()
889            .with_validator_set(vec![validator_key])
890            .with_shared_gas_limit(100) // Low shared gas limit
891            .with_seen_subblock(proposer, vec![tx])
892            .build(&mut db, &chainspec);
893        let signature_hash = subblock.signature_hash();
894        let signature = signer.sign(&[], signature_hash.as_slice());
895
896        let metadata = vec![SubBlockMetadata {
897            version: SubBlockVersion::V1,
898            validator: validator_key,
899            fee_recipient: Address::ZERO,
900            signature: Bytes::copy_from_slice(signature.as_ref()),
901        }];
902
903        let result = executor.validate_shared_gas(&metadata);
904        assert!(result.is_err());
905        assert_eq!(
906            result.unwrap_err().to_string(),
907            "subblock gas used exceeds gas per subblock"
908        );
909    }
910
911    #[test]
912    fn test_validate_shared_gas_unexpected_subblock_len() {
913        let chainspec = test_chainspec();
914        let mut db = State::builder().with_bundle_update().build();
915        let signer = PrivateKey::from_seed(0);
916        let validator_key = B256::from_slice(&signer.public_key());
917
918        // Add a seen subblock from a different validator that won't match metadata
919        let different_key = B256::repeat_byte(0x99);
920        let different_proposer = PartialValidatorKey::from_slice(&different_key[..15]);
921
922        let executor = TestExecutorBuilder::default()
923            .with_validator_set(vec![validator_key])
924            .with_seen_subblock(different_proposer, vec![])
925            .build(&mut db, &chainspec);
926
927        // Metadata has validator_key but seen_subblocks has different_key
928        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
929
930        let result = executor.validate_shared_gas(&metadata);
931        assert!(result.is_err());
932        assert_eq!(
933            result.unwrap_err().to_string(),
934            "failed to map all non-empty subblocks to metadata"
935        );
936    }
937
938    #[test]
939    fn test_validate_shared_gas_limit_exceeded() {
940        let chainspec = test_chainspec();
941        let mut db = State::builder().with_bundle_update().build();
942        let signer = PrivateKey::from_seed(0);
943        let validator_key = B256::from_slice(&signer.public_key());
944
945        // Set incentive_gas_used higher than available incentive gas
946        let executor = TestExecutorBuilder::default()
947            .with_validator_set(vec![validator_key])
948            .with_incentive_gas_used(100_000_000)
949            .build(&mut db, &chainspec);
950
951        let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
952
953        let result = executor.validate_shared_gas(&metadata);
954        assert!(result.is_err());
955        assert_eq!(
956            result.unwrap_err().to_string(),
957            "incentive gas limit exceeded"
958        );
959    }
960
961    #[test]
962    fn test_validate_tx() {
963        let chainspec = test_chainspec();
964        let mut db = State::builder().with_bundle_update().build();
965        let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
966
967        // Test regular transaction in StartOfBlock section goes to NonShared
968        let tx = create_legacy_tx();
969        let result = executor.validate_tx(&tx, 21000);
970        assert!(result.is_ok());
971        assert_eq!(result.unwrap(), BlockSection::NonShared);
972    }
973
974    fn create_subblock_tx(proposer: &PartialValidatorKey) -> TempoTxEnvelope {
975        let mut nonce_bytes = [0u8; 32];
976        nonce_bytes[0] = TEMPO_SUBBLOCK_NONCE_KEY_PREFIX;
977        nonce_bytes[1..16].copy_from_slice(proposer.as_slice());
978
979        let tx = TempoTransaction {
980            chain_id: 1,
981            calls: vec![Call {
982                to: Address::ZERO.into(),
983                input: Default::default(),
984                value: Default::default(),
985            }],
986            gas_limit: 21000,
987            nonce_key: U256::from_be_bytes(nonce_bytes),
988            max_fee_per_gas: 1,
989            max_priority_fee_per_gas: 1,
990            ..Default::default()
991        };
992
993        let signature = TempoSignature::from(Signature::test_signature());
994        TempoTxEnvelope::AA(tx.into_signed(signature))
995    }
996
997    #[test]
998    fn test_validate_tx_subblock_section_already_passed() {
999        let chainspec = test_chainspec();
1000        let mut db = State::builder().with_bundle_update().build();
1001        let signer = PrivateKey::from_seed(0);
1002        let validator_key = B256::from_slice(&signer.public_key());
1003        let proposer = PartialValidatorKey::from_slice(&validator_key[..15]);
1004
1005        // Test with GasIncentive section
1006        let executor = TestExecutorBuilder::default()
1007            .with_section(BlockSection::GasIncentive)
1008            .build(&mut db, &chainspec);
1009
1010        let subblock_tx = create_subblock_tx(&proposer);
1011        let result = executor.validate_tx(&subblock_tx, 21000);
1012        assert!(result.is_err());
1013        assert_eq!(
1014            result.unwrap_err().to_string(),
1015            "subblock section already passed"
1016        );
1017
1018        // Also test with System section
1019        let mut db2 = State::builder().with_bundle_update().build();
1020        let executor2 = TestExecutorBuilder::default()
1021            .with_section(BlockSection::System {
1022                seen_subblocks_signatures: false,
1023            })
1024            .build(&mut db2, &chainspec);
1025
1026        let result = executor2.validate_tx(&subblock_tx, 21000);
1027        assert!(result.is_err());
1028        assert_eq!(
1029            result.unwrap_err().to_string(),
1030            "subblock section already passed"
1031        );
1032    }
1033
1034    #[test]
1035    fn test_validate_tx_proposer_subblock_already_processed() {
1036        let chainspec = test_chainspec();
1037        let mut db = State::builder().with_bundle_update().build();
1038        let signer1 = PrivateKey::from_seed(0);
1039        let validator_key1 = B256::from_slice(&signer1.public_key());
1040        let proposer1 = PartialValidatorKey::from_slice(&validator_key1[..15]);
1041
1042        let signer2 = PrivateKey::from_seed(1);
1043        let validator_key2 = B256::from_slice(&signer2.public_key());
1044        let proposer2 = PartialValidatorKey::from_slice(&validator_key2[..15]);
1045
1046        // Set section to SubBlock with a different proposer, and mark proposer1 as already seen
1047        let executor = TestExecutorBuilder::default()
1048            .with_section(BlockSection::SubBlock {
1049                proposer: proposer2,
1050            })
1051            .with_seen_subblock(proposer1, vec![])
1052            .build(&mut db, &chainspec);
1053
1054        // Try to submit a tx for proposer1 (already processed)
1055        let subblock_tx = create_subblock_tx(&proposer1);
1056        let result = executor.validate_tx(&subblock_tx, 21000);
1057        assert!(result.is_err());
1058        assert_eq!(
1059            result.unwrap_err().to_string(),
1060            "proposer's subblock already processed"
1061        );
1062    }
1063
1064    #[test]
1065    fn test_validate_tx_regular_tx_follow_system_tx() {
1066        let chainspec = test_chainspec();
1067        let mut db = State::builder().with_bundle_update().build();
1068
1069        // Set section to System
1070        let executor = TestExecutorBuilder::default()
1071            .with_section(BlockSection::System {
1072                seen_subblocks_signatures: false,
1073            })
1074            .build(&mut db, &chainspec);
1075
1076        // Try to validate a regular tx
1077        let tx = create_legacy_tx();
1078        let result = executor.validate_tx(&tx, 21000);
1079        assert!(result.is_err());
1080        assert_eq!(
1081            result.unwrap_err().to_string(),
1082            "regular transaction can't follow system transaction"
1083        );
1084    }
1085
1086    #[test]
1087    fn test_commit_transaction() {
1088        let chainspec = test_chainspec();
1089        let mut db = State::builder().with_bundle_update().build();
1090        let mut executor = TestExecutorBuilder::default()
1091            .with_general_gas_limit(30_000_000)
1092            .with_parent_beacon_block_root(B256::ZERO)
1093            .build(&mut db, &chainspec);
1094
1095        // Apply pre-execution changes first
1096        executor.apply_pre_execution_changes().unwrap();
1097
1098        let tx = create_legacy_tx();
1099        let output = TempoTxResult {
1100            inner: EthTxResult {
1101                result: ResultAndState {
1102                    result: revm::context::result::ExecutionResult::Success {
1103                        reason: revm::context::result::SuccessReason::Return,
1104                        gas: ResultGas::default().with_limit(21000).with_spent(21000),
1105                        logs: vec![],
1106                        output: revm::context::result::Output::Call(Bytes::new()),
1107                    },
1108                    state: Default::default(),
1109                },
1110                blob_gas_used: 0,
1111                tx_type: tx.tx_type(),
1112            },
1113            next_section: BlockSection::NonShared,
1114            is_payment: false,
1115            tx: None,
1116        };
1117
1118        let gas_used = executor.commit_transaction(output).unwrap();
1119
1120        assert_eq!(gas_used, 21000);
1121        assert_eq!(executor.section(), BlockSection::NonShared);
1122    }
1123
1124    #[test]
1125    fn test_finish() {
1126        let chainspec = test_chainspec();
1127        let mut db = State::builder().with_bundle_update().build();
1128        let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
1129
1130        let result = executor.finish();
1131        assert!(result.is_ok());
1132    }
1133
1134    #[test]
1135    fn test_apply_pre_execution_deploys_validator_v2_code() {
1136        use std::sync::Arc;
1137        use tempo_chainspec::spec::DEV;
1138
1139        // Dev chainspec has t2Time: 0, so T2 is active at any timestamp.
1140        let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1141        let mut db = State::builder().with_bundle_update().build();
1142        let mut executor = TestExecutorBuilder::default()
1143            .with_parent_beacon_block_root(B256::ZERO)
1144            .build(&mut db, &chainspec);
1145
1146        executor.apply_pre_execution_changes().unwrap();
1147
1148        let acc = db.load_cache_account(VALIDATOR_CONFIG_V2_ADDRESS).unwrap();
1149        let info = acc.account_info().unwrap();
1150        assert!(!info.is_empty_code_hash());
1151    }
1152}