Skip to main content

tempo_evm/consensus/
mod.rs

1//! Tempo consensus implementation.
2
3mod error;
4
5use alloy_consensus::{BlockHeader, Transaction, transaction::TxHashRef};
6use alloy_evm::block::BlockExecutionResult;
7pub use error::TempoConsensusError;
8use reth_chainspec::EthChainSpec;
9use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
10use reth_consensus_common::validation::{
11    validate_against_parent_4844, validate_against_parent_eip1559_base_fee,
12    validate_against_parent_gas_limit, validate_against_parent_hash_number,
13};
14use reth_ethereum_consensus::EthBeaconConsensus;
15use reth_primitives_traits::{RecoveredBlock, SealedBlock, SealedHeader};
16use std::sync::Arc;
17use tempo_chainspec::{
18    hardfork::TempoHardforks,
19    spec::{SYSTEM_TX_ADDRESSES, SYSTEM_TX_COUNT, TempoChainSpec},
20};
21use tempo_primitives::{
22    Block, BlockBody, TempoHeader, TempoPrimitives, TempoReceipt, TempoTxEnvelope,
23};
24
25/// How far in the future the block timestamp can be.
26///
27/// We are setting this to 0 to not allow any drift of the block time in the future.
28/// We are considering this safe because with the way CL works currently block time would
29/// be consistent and thus an honest proposer should never produce a block that appears
30/// to be in the future even assuming 50-100ms clock drift.
31pub const ALLOWED_FUTURE_BLOCK_TIME_MILLIS: u64 = 0;
32
33/// Maximum extra data size for Tempo blocks.
34pub const TEMPO_MAXIMUM_EXTRA_DATA_SIZE: usize = 10 * 1_024; // 10KiB
35
36/// Tempo consensus implementation.
37#[derive(Debug, Clone)]
38pub struct TempoConsensus {
39    /// Inner Ethereum consensus.
40    inner: EthBeaconConsensus<TempoChainSpec>,
41}
42
43impl TempoConsensus {
44    /// Creates a new [`TempoConsensus`] with the given chain spec.
45    pub fn new(chain_spec: Arc<TempoChainSpec>) -> Self {
46        Self::new_with_bal_hashes(chain_spec, false)
47    }
48
49    /// Creates a new [`TempoConsensus`] with optional pre-Amsterdam BAL hash support.
50    pub fn new_with_bal_hashes(chain_spec: Arc<TempoChainSpec>, allow_bal_hashes: bool) -> Self {
51        Self {
52            inner: EthBeaconConsensus::new(chain_spec)
53                .with_max_extra_data_size(TEMPO_MAXIMUM_EXTRA_DATA_SIZE)
54                .with_allow_bal_hashes(allow_bal_hashes),
55        }
56    }
57
58    /// Validates the given header against common consensus rules and the given millisecond timestamp.
59    fn validate_header_with_timestamp_millis(
60        &self,
61        header: &SealedHeader<TempoHeader>,
62        present_timestamp_millis: u64,
63    ) -> Result<(), ConsensusError> {
64        self.inner.validate_header(header)?;
65
66        // Validate the timestamp milliseconds part
67        if header.timestamp_millis_part >= 1000 {
68            return Err(TempoConsensusError::InvalidTimestampMillisPart {
69                millis_part: header.timestamp_millis_part,
70            }
71            .into());
72        }
73
74        if header.timestamp_millis() > present_timestamp_millis + ALLOWED_FUTURE_BLOCK_TIME_MILLIS {
75            return Err(ConsensusError::TimestampIsInFuture {
76                timestamp: header.timestamp_millis(),
77                present_timestamp: present_timestamp_millis,
78            });
79        }
80
81        let expected_shared = self
82            .inner
83            .chain_spec()
84            .shared_gas_limit_at(header.timestamp(), header.gas_limit());
85        if header.shared_gas_limit != expected_shared {
86            return Err(TempoConsensusError::SharedGasLimitMismatch {
87                expected: expected_shared,
88                actual: header.shared_gas_limit,
89            }
90            .into());
91        }
92
93        // Validate the general (non-payment) gas limit
94        let expected_general_gas_limit = self.inner.chain_spec().general_gas_limit_at(
95            header.timestamp(),
96            header.gas_limit(),
97            header.shared_gas_limit,
98        );
99
100        if header.general_gas_limit != expected_general_gas_limit {
101            return Err(TempoConsensusError::GeneralGasLimitMismatch {
102                expected: expected_general_gas_limit,
103                actual: header.general_gas_limit,
104            }
105            .into());
106        }
107
108        Ok(())
109    }
110}
111
112impl HeaderValidator<TempoHeader> for TempoConsensus {
113    fn validate_header(&self, header: &SealedHeader<TempoHeader>) -> Result<(), ConsensusError> {
114        let current_timestamp_millis = std::time::SystemTime::now()
115            .duration_since(std::time::SystemTime::UNIX_EPOCH)
116            .expect("system time should never be before UNIX EPOCH")
117            .as_millis() as u64;
118        self.validate_header_with_timestamp_millis(header, current_timestamp_millis)
119    }
120
121    fn validate_header_against_parent(
122        &self,
123        header: &SealedHeader<TempoHeader>,
124        parent: &SealedHeader<TempoHeader>,
125    ) -> Result<(), ConsensusError> {
126        validate_against_parent_hash_number(header.header(), parent)?;
127
128        validate_against_parent_gas_limit(header, parent, self.inner.chain_spec())?;
129
130        validate_against_parent_eip1559_base_fee(
131            header.header(),
132            parent.header(),
133            self.inner.chain_spec(),
134        )?;
135
136        if let Some(blob_params) = self
137            .inner
138            .chain_spec()
139            .blob_params_at_timestamp(header.timestamp())
140        {
141            validate_against_parent_4844(header.header(), parent.header(), blob_params)?;
142        }
143
144        if header.timestamp_millis() <= parent.timestamp_millis() {
145            return Err(ConsensusError::TimestampIsInPast {
146                parent_timestamp: parent.timestamp_millis(),
147                timestamp: header.timestamp_millis(),
148            });
149        }
150
151        Ok(())
152    }
153}
154
155impl Consensus<Block> for TempoConsensus {
156    fn validate_body_against_header(
157        &self,
158        body: &BlockBody,
159        header: &SealedHeader<TempoHeader>,
160    ) -> Result<(), ConsensusError> {
161        Consensus::<Block>::validate_body_against_header(&self.inner, body, header)
162    }
163
164    fn validate_block_pre_execution(
165        &self,
166        block: &SealedBlock<Block>,
167    ) -> Result<(), ConsensusError> {
168        let transactions = &block.body().transactions;
169
170        if let Some(tx) = transactions.iter().find(|&tx| {
171            tx.is_system_tx() && !tx.is_valid_system_tx(self.inner.chain_spec().chain().id())
172        }) {
173            return Err(TempoConsensusError::InvalidSystemTransaction {
174                tx_hash: *tx.tx_hash(),
175            }
176            .into());
177        }
178
179        let expected_system_tx_count = if self
180            .inner
181            .chain_spec()
182            .is_t4_active_at_timestamp(block.header().timestamp())
183        {
184            0
185        } else {
186            SYSTEM_TX_COUNT
187        };
188
189        // Get the last END_OF_BLOCK_SYSTEM_TX_COUNT transactions and validate they are end-of-block system txs
190        let end_of_block_system_txs = transactions
191            .get(transactions.len().saturating_sub(expected_system_tx_count)..)
192            .map(|slice| {
193                slice
194                    .iter()
195                    .filter(|tx| tx.is_system_tx())
196                    .collect::<Vec<&TempoTxEnvelope>>()
197            })
198            .unwrap_or_default();
199
200        if end_of_block_system_txs.len() != expected_system_tx_count {
201            return Err(TempoConsensusError::MissingEndOfBlockSystemTxs {
202                expected: expected_system_tx_count,
203                actual: end_of_block_system_txs.len(),
204            }
205            .into());
206        }
207
208        // Validate that the sequence of end-of-block system txs is correct
209        for (tx, expected_to) in end_of_block_system_txs.into_iter().zip(SYSTEM_TX_ADDRESSES) {
210            let actual_to = tx.to().unwrap_or_default();
211            if actual_to != expected_to {
212                return Err(TempoConsensusError::InvalidEndOfBlockSystemTxOrder {
213                    expected: expected_to,
214                    actual: actual_to,
215                }
216                .into());
217            }
218        }
219
220        self.inner.validate_block_pre_execution(block)
221    }
222
223    fn is_transient_error(&self, error: &ConsensusError) -> bool {
224        // Future timestamps can happen briefly when clocks drift between nodes.
225        Consensus::<Block>::is_transient_error(&self.inner, error)
226            || matches!(error, ConsensusError::TimestampIsInFuture { .. })
227    }
228}
229
230impl FullConsensus<TempoPrimitives> for TempoConsensus {
231    fn validate_block_post_execution(
232        &self,
233        block: &RecoveredBlock<Block>,
234        result: &BlockExecutionResult<TempoReceipt>,
235        receipt_root_bloom: Option<ReceiptRootBloom>,
236        block_access_list_hash: Option<alloy_primitives::B256>,
237    ) -> Result<(), ConsensusError> {
238        FullConsensus::<TempoPrimitives>::validate_block_post_execution(
239            &self.inner,
240            block,
241            result,
242            receipt_root_bloom,
243            block_access_list_hash,
244        )
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use alloy_consensus::{
252        Header, Signed, TxLegacy, constants::EMPTY_ROOT_HASH, proofs::calculate_transaction_root,
253        transaction::TxHashRef,
254    };
255    use alloy_genesis::Genesis;
256    use alloy_primitives::{Address, B256, Signature, TxKind, U256};
257    use reth_primitives_traits::SealedHeader;
258    use std::time::{SystemTime, UNIX_EPOCH};
259    use tempo_chainspec::{
260        hardfork::TempoHardfork,
261        spec::{DEV, MODERATO, TempoChainSpec},
262    };
263
264    fn current_timestamp_millis() -> u64 {
265        SystemTime::now()
266            .duration_since(UNIX_EPOCH)
267            .unwrap()
268            .as_millis() as u64
269    }
270
271    #[derive(Default)]
272    struct TestHeaderBuilder {
273        gas_limit: u64,
274        timestamp: u64,
275        timestamp_millis_part: u64,
276        number: u64,
277        parent_hash: B256,
278        shared_gas_limit: Option<u64>,
279        general_gas_limit: Option<u64>,
280        base_fee: Option<u64>,
281        gas_used: u64,
282    }
283
284    impl TestHeaderBuilder {
285        fn gas_limit(mut self, gas_limit: u64) -> Self {
286            self.gas_limit = gas_limit;
287            self
288        }
289
290        fn timestamp_millis(mut self, timestamp: u64) -> Self {
291            self.timestamp = timestamp / 1000;
292            self.timestamp_millis_part = timestamp % 1000;
293            self
294        }
295
296        fn timestamp(mut self, timestamp: u64) -> Self {
297            self.timestamp = timestamp;
298            self
299        }
300
301        fn timestamp_millis_part(mut self, millis: u64) -> Self {
302            self.timestamp_millis_part = millis;
303            self
304        }
305
306        fn number(mut self, number: u64) -> Self {
307            self.number = number;
308            self
309        }
310
311        fn parent_hash(mut self, hash: B256) -> Self {
312            self.parent_hash = hash;
313            self
314        }
315
316        fn shared_gas_limit(mut self, limit: u64) -> Self {
317            self.shared_gas_limit = Some(limit);
318            self
319        }
320
321        fn general_gas_limit(mut self, limit: u64) -> Self {
322            self.general_gas_limit = Some(limit);
323            self
324        }
325
326        fn base_fee(mut self, fee: u64) -> Self {
327            self.base_fee = Some(fee);
328            self
329        }
330
331        fn gas_used(mut self, gas_used: u64) -> Self {
332            self.gas_used = gas_used;
333            self
334        }
335
336        fn build(self) -> TempoHeader {
337            let shared_gas_limit = self.shared_gas_limit.unwrap_or(0);
338            // Default to T1 fixed general gas limit
339            let general_gas_limit = self
340                .general_gas_limit
341                .unwrap_or(tempo_chainspec::spec::TEMPO_T1_GENERAL_GAS_LIMIT);
342
343            TempoHeader {
344                inner: Header {
345                    gas_limit: self.gas_limit,
346                    gas_used: self.gas_used,
347                    timestamp: self.timestamp,
348                    number: self.number,
349                    parent_hash: self.parent_hash,
350                    base_fee_per_gas: Some(
351                        self.base_fee
352                            .unwrap_or(tempo_chainspec::spec::TEMPO_T0_BASE_FEE),
353                    ),
354                    withdrawals_root: Some(EMPTY_ROOT_HASH),
355                    blob_gas_used: Some(0),
356                    excess_blob_gas: Some(0),
357                    parent_beacon_block_root: Some(B256::ZERO),
358                    requests_hash: Some(B256::ZERO),
359                    ..Default::default()
360                },
361                shared_gas_limit,
362                general_gas_limit,
363                timestamp_millis_part: self.timestamp_millis_part,
364                ..Default::default()
365            }
366        }
367    }
368
369    fn create_valid_block(header: TempoHeader, transactions: Vec<TempoTxEnvelope>) -> Block {
370        let transactions_root = calculate_transaction_root(&transactions);
371        let mut header = header;
372        header.inner.transactions_root = transactions_root;
373
374        Block {
375            header,
376            body: BlockBody {
377                transactions,
378                withdrawals: Some(Default::default()),
379                ..Default::default()
380            },
381        }
382    }
383
384    fn create_system_tx(chain_id: u64, to: Address) -> TempoTxEnvelope {
385        let tx = TxLegacy {
386            chain_id: Some(chain_id),
387            nonce: 0,
388            gas_price: 0,
389            gas_limit: 0,
390            to: TxKind::Call(to),
391            value: U256::ZERO,
392            input: Default::default(),
393        };
394        let signature = Signature::new(U256::ZERO, U256::ZERO, false);
395        TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, signature))
396    }
397
398    fn create_tx(chain_id: u64) -> TempoTxEnvelope {
399        let tx = TxLegacy {
400            chain_id: Some(chain_id),
401            nonce: 1,
402            gas_price: 1_000_000_000,
403            gas_limit: 21000,
404            to: TxKind::Call(Address::repeat_byte(0x42)),
405            value: U256::from(100),
406            input: Default::default(),
407        };
408        TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, Signature::test_signature()))
409    }
410
411    #[test]
412    fn test_validate_header() {
413        let consensus = TempoConsensus::new(MODERATO.clone());
414        let timestamp = current_timestamp_millis();
415        let header = TestHeaderBuilder::default()
416            .gas_limit(30_000_000)
417            .timestamp_millis(timestamp)
418            .shared_gas_limit(MODERATO.shared_gas_limit_at(timestamp / 1000, 30_000_000))
419            .build();
420        let sealed = SealedHeader::seal_slow(header);
421
422        assert!(consensus.validate_header(&sealed).is_ok());
423    }
424
425    #[test]
426    fn test_validate_header_shared_gas_mismatch() {
427        let consensus = TempoConsensus::new(MODERATO.clone());
428        let header = TestHeaderBuilder::default()
429            .gas_limit(30_000_000)
430            .timestamp_millis(current_timestamp_millis())
431            .shared_gas_limit(999)
432            .build();
433        let sealed = SealedHeader::seal_slow(header);
434
435        let result = consensus.validate_header(&sealed);
436        let err = result.unwrap_err();
437        assert!(
438            err.downcast_other_ref::<TempoConsensusError>()
439                .is_some_and(|e| matches!(e, TempoConsensusError::SharedGasLimitMismatch { .. })),
440            "Expected SharedGasLimitMismatch, got: {err:?}"
441        );
442    }
443
444    #[test]
445    fn test_validate_header_general_gas_mismatch_pre_t1() {
446        // Pre-T1 chainspec uses the divisor-based calculation
447        let consensus = TempoConsensus::new(create_pre_t1_chainspec());
448        let gas_limit = 500_000_000u64;
449        let shared_gas_limit = gas_limit / 10;
450        // Pre-T1: expected = (gas_limit - shared_gas_limit) / 2
451        let header = TestHeaderBuilder::default()
452            .gas_limit(gas_limit)
453            .timestamp_millis(current_timestamp_millis())
454            .general_gas_limit(999)
455            .shared_gas_limit(shared_gas_limit)
456            .build();
457        let sealed = SealedHeader::seal_slow(header);
458
459        let result = consensus.validate_header(&sealed);
460        let err = result.unwrap_err();
461        assert!(
462            err.downcast_other_ref::<TempoConsensusError>()
463                .is_some_and(|e| matches!(e, TempoConsensusError::GeneralGasLimitMismatch { .. })),
464            "Expected GeneralGasLimitMismatch, got: {err:?}",
465        );
466
467        // Now verify the correct pre-T1 value works
468        let expected_general_gas_limit = (gas_limit - shared_gas_limit) / 2;
469        let header = TestHeaderBuilder::default()
470            .gas_limit(gas_limit)
471            .timestamp_millis(current_timestamp_millis())
472            .general_gas_limit(expected_general_gas_limit)
473            .shared_gas_limit(shared_gas_limit)
474            .build();
475        let sealed = SealedHeader::seal_slow(header);
476        assert!(consensus.validate_header(&sealed).is_ok());
477    }
478
479    /// Creates a chainspec with only T0 active (no T1).
480    fn create_pre_t1_chainspec() -> Arc<TempoChainSpec> {
481        let genesis_json = r#"{
482            "config": {
483                "chainId": 99998,
484                "homesteadBlock": 0,
485                "daoForkSupport": false,
486                "eip150Block": 0,
487                "eip155Block": 0,
488                "eip158Block": 0,
489                "byzantiumBlock": 0,
490                "constantinopleBlock": 0,
491                "petersburgBlock": 0,
492                "istanbulBlock": 0,
493                "berlinBlock": 0,
494                "londonBlock": 0,
495                "mergeNetsplitBlock": 0,
496                "shanghaiTime": 0,
497                "cancunTime": 0,
498                "pragueTime": 0,
499                "osakaTime": 0,
500                "terminalTotalDifficulty": 0,
501                "terminalTotalDifficultyPassed": true,
502                "epochLength": 21600,
503                "t0Time": 0
504            },
505            "nonce": "0x42",
506            "timestamp": "0x0",
507            "extraData": "0x",
508            "gasLimit": "0x1dcd6500",
509            "difficulty": "0x0",
510            "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
511            "coinbase": "0x0000000000000000000000000000000000000000",
512            "alloc": {}
513        }"#;
514        let genesis: Genesis = serde_json::from_str(genesis_json).unwrap();
515        Arc::new(TempoChainSpec::from_genesis(genesis))
516    }
517
518    /// Creates a chainspec with T1 active at timestamp 0.
519    fn create_t1_chainspec() -> Arc<TempoChainSpec> {
520        let genesis_json = r#"{
521            "config": {
522                "chainId": 99999,
523                "homesteadBlock": 0,
524                "daoForkSupport": false,
525                "eip150Block": 0,
526                "eip155Block": 0,
527                "eip158Block": 0,
528                "byzantiumBlock": 0,
529                "constantinopleBlock": 0,
530                "petersburgBlock": 0,
531                "istanbulBlock": 0,
532                "berlinBlock": 0,
533                "londonBlock": 0,
534                "mergeNetsplitBlock": 0,
535                "shanghaiTime": 0,
536                "cancunTime": 0,
537                "pragueTime": 0,
538                "osakaTime": 0,
539                "terminalTotalDifficulty": 0,
540                "terminalTotalDifficultyPassed": true,
541                "epochLength": 21600,
542                "t0Time": 0,
543                "t1Time": 0
544            },
545            "nonce": "0x42",
546            "timestamp": "0x0",
547            "extraData": "0x",
548            "gasLimit": "0x1dcd6500",
549            "difficulty": "0x0",
550            "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
551            "coinbase": "0x0000000000000000000000000000000000000000",
552            "alloc": {}
553        }"#;
554        let genesis: Genesis = serde_json::from_str(genesis_json).unwrap();
555        Arc::new(TempoChainSpec::from_genesis(genesis))
556    }
557
558    /// Creates a chainspec with T7 active at timestamp 10.
559    fn create_t7_chainspec() -> Arc<TempoChainSpec> {
560        let genesis_json = r#"{
561            "config": {
562                "chainId": 100000,
563                "homesteadBlock": 0,
564                "daoForkSupport": false,
565                "eip150Block": 0,
566                "eip155Block": 0,
567                "eip158Block": 0,
568                "byzantiumBlock": 0,
569                "constantinopleBlock": 0,
570                "petersburgBlock": 0,
571                "istanbulBlock": 0,
572                "berlinBlock": 0,
573                "londonBlock": 0,
574                "mergeNetsplitBlock": 0,
575                "shanghaiTime": 0,
576                "cancunTime": 0,
577                "pragueTime": 0,
578                "osakaTime": 0,
579                "terminalTotalDifficulty": 0,
580                "terminalTotalDifficultyPassed": true,
581                "epochLength": 21600,
582                "t0Time": 0,
583                "t1Time": 0,
584                "t7Time": 10
585            },
586            "nonce": "0x42",
587            "timestamp": "0x0",
588            "extraData": "0x",
589            "gasLimit": "0x1dcd6500",
590            "difficulty": "0x0",
591            "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
592            "coinbase": "0x0000000000000000000000000000000000000000",
593            "alloc": {}
594        }"#;
595        let genesis: Genesis = serde_json::from_str(genesis_json).unwrap();
596        Arc::new(TempoChainSpec::from_genesis(genesis))
597    }
598
599    #[test]
600    fn test_validate_header_general_gas_limit_t1() {
601        // Create a chainspec with T1 active at timestamp 0
602        let chainspec = create_t1_chainspec();
603        let consensus = TempoConsensus::new(chainspec);
604        let gas_limit = 500_000_000u64;
605
606        // T1+: general gas limit must be fixed at 30M
607        // Test with wrong value
608        let header = TestHeaderBuilder::default()
609            .gas_limit(gas_limit)
610            .timestamp_millis(current_timestamp_millis())
611            .general_gas_limit(999)
612            .shared_gas_limit(50_000_000)
613            .build();
614        let sealed = SealedHeader::seal_slow(header);
615
616        let result = consensus.validate_header(&sealed);
617        let err = result.unwrap_err();
618        assert!(
619            err.downcast_other_ref::<TempoConsensusError>()
620                .is_some_and(|e| matches!(e, TempoConsensusError::GeneralGasLimitMismatch { .. })),
621            "Expected GeneralGasLimitMismatch, got: {err:?}",
622        );
623
624        // Now verify the correct T1 value works (fixed 30M)
625        let header = TestHeaderBuilder::default()
626            .gas_limit(gas_limit)
627            .timestamp_millis(current_timestamp_millis())
628            .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
629            .shared_gas_limit(50_000_000)
630            .build();
631        let sealed = SealedHeader::seal_slow(header);
632        consensus.validate_header(&sealed).expect("should be valid");
633    }
634
635    #[test]
636    fn test_validate_header_timestamp_milli_gte_1000() {
637        let consensus = TempoConsensus::new(MODERATO.clone());
638
639        let current_timestamp_millis = 1000000999;
640
641        // Test timestamp equal to 1000
642        let header = TestHeaderBuilder::default()
643            .gas_limit(30_000_000)
644            .timestamp_millis(current_timestamp_millis)
645            .timestamp_millis_part(1000)
646            .build();
647        let sealed = SealedHeader::seal_slow(header);
648
649        let result =
650            consensus.validate_header_with_timestamp_millis(&sealed, current_timestamp_millis);
651        let err = result.unwrap_err();
652        assert!(
653            err.downcast_other_ref::<TempoConsensusError>()
654                .is_some_and(|e| matches!(
655                    e,
656                    TempoConsensusError::InvalidTimestampMillisPart { millis_part: 1000 }
657                )),
658            "Expected InvalidTimestampMillisPart, got: {err:?}"
659        );
660
661        // Test timestamp > 1000
662        let header = TestHeaderBuilder::default()
663            .gas_limit(30_000_000)
664            .timestamp_millis(current_timestamp_millis)
665            .timestamp_millis_part(1001)
666            .build();
667        let sealed = SealedHeader::seal_slow(header);
668        let result =
669            consensus.validate_header_with_timestamp_millis(&sealed, current_timestamp_millis);
670        let err = result.unwrap_err();
671        assert!(
672            err.downcast_other_ref::<TempoConsensusError>()
673                .is_some_and(|e| matches!(
674                    e,
675                    TempoConsensusError::InvalidTimestampMillisPart { millis_part: 1001 }
676                )),
677            "Expected InvalidTimestampMillisPart, got: {err:?}"
678        );
679    }
680
681    #[test]
682    fn test_validate_header_against_parent() {
683        use tempo_chainspec::spec::TEMPO_T1_BASE_FEE;
684
685        let consensus = TempoConsensus::new(MODERATO.clone());
686        let parent_ts = current_timestamp_millis() - 1;
687        let parent = TestHeaderBuilder::default()
688            .gas_limit(30_000_000)
689            .timestamp(parent_ts)
690            .number(1)
691            .timestamp_millis_part(500)
692            .base_fee(TEMPO_T1_BASE_FEE)
693            .build();
694        let parent_sealed = SealedHeader::seal_slow(parent);
695
696        let child = TestHeaderBuilder::default()
697            .gas_limit(30_000_000)
698            .timestamp(parent_ts + 1)
699            .timestamp_millis_part(600)
700            .number(2)
701            .base_fee(TEMPO_T1_BASE_FEE)
702            .parent_hash(parent_sealed.hash())
703            .build();
704        let child_sealed = SealedHeader::seal_slow(child);
705
706        let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
707        assert!(result.is_ok());
708    }
709
710    #[test]
711    fn test_validate_header_against_parent_timestamp_not_increasing() {
712        use tempo_chainspec::spec::TEMPO_T1_BASE_FEE;
713
714        let consensus = TempoConsensus::new(MODERATO.clone());
715        let parent_ts = current_timestamp_millis();
716        let parent = TestHeaderBuilder::default()
717            .gas_limit(30_000_000)
718            .timestamp(parent_ts)
719            .timestamp_millis_part(500)
720            .base_fee(TEMPO_T1_BASE_FEE)
721            .build();
722        let parent_sealed = SealedHeader::seal_slow(parent);
723
724        let child = TestHeaderBuilder::default()
725            .gas_limit(30_000_000)
726            .timestamp(parent_ts)
727            .timestamp_millis_part(400)
728            .number(1)
729            .base_fee(TEMPO_T1_BASE_FEE)
730            .parent_hash(parent_sealed.hash())
731            .build();
732        let child_sealed = SealedHeader::seal_slow(child);
733
734        let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
735        assert!(matches!(
736            result,
737            Err(ConsensusError::TimestampIsInPast { .. })
738        ));
739    }
740
741    #[test]
742    fn test_validate_header_against_parent_t1() {
743        use tempo_chainspec::spec::TEMPO_T1_BASE_FEE;
744
745        let chainspec = create_t1_chainspec();
746        let consensus = TempoConsensus::new(chainspec);
747
748        let parent_ts = current_timestamp_millis() - 1;
749        let parent = TestHeaderBuilder::default()
750            .gas_limit(500_000_000)
751            .timestamp(parent_ts)
752            .number(1)
753            .timestamp_millis_part(500)
754            .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
755            .base_fee(TEMPO_T1_BASE_FEE)
756            .build();
757        let parent_sealed = SealedHeader::seal_slow(parent);
758
759        let child = TestHeaderBuilder::default()
760            .gas_limit(500_000_000)
761            .timestamp(parent_ts + 1)
762            .timestamp_millis_part(600)
763            .number(2)
764            .parent_hash(parent_sealed.hash())
765            .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
766            .base_fee(TEMPO_T1_BASE_FEE)
767            .build();
768        let child_sealed = SealedHeader::seal_slow(child);
769
770        let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
771        assert!(result.is_ok(), "T1 validation failed: {result:?}");
772    }
773
774    #[test]
775    fn test_validate_header_against_parent_t1_wrong_base_fee() {
776        use tempo_chainspec::spec::{TEMPO_T0_BASE_FEE, TEMPO_T1_BASE_FEE};
777
778        let chainspec = create_t1_chainspec();
779        let consensus = TempoConsensus::new(chainspec);
780
781        let parent_ts = current_timestamp_millis() - 1;
782        let parent = TestHeaderBuilder::default()
783            .gas_limit(500_000_000)
784            .timestamp(parent_ts)
785            .number(1)
786            .timestamp_millis_part(500)
787            .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
788            .base_fee(TEMPO_T1_BASE_FEE)
789            .build();
790        let parent_sealed = SealedHeader::seal_slow(parent);
791
792        // Child uses pre-T1 base fee (wrong for T1 chainspec)
793        let child = TestHeaderBuilder::default()
794            .gas_limit(500_000_000)
795            .timestamp(parent_ts + 1)
796            .timestamp_millis_part(600)
797            .number(2)
798            .parent_hash(parent_sealed.hash())
799            .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
800            .base_fee(TEMPO_T0_BASE_FEE)
801            .build();
802        let child_sealed = SealedHeader::seal_slow(child);
803
804        let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
805        assert!(
806            matches!(result, Err(ConsensusError::BaseFeeDiff(_))),
807            "Expected BaseFeeDiff error, got: {result:?}"
808        );
809    }
810
811    #[test]
812    fn test_validate_header_against_parent_t7_dynamic_base_fee() {
813        use tempo_chainspec::spec::{TEMPO_T7_BASE_FEE_CAP, TEMPO_T7_BASE_FEE_FLOOR};
814
815        let chainspec = create_t7_chainspec();
816        let consensus = TempoConsensus::new(chainspec);
817
818        let parent = TestHeaderBuilder::default()
819            .gas_limit(500_000_000)
820            .timestamp(10)
821            .number(1)
822            .timestamp_millis_part(500)
823            .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
824            .base_fee(TEMPO_T7_BASE_FEE_CAP)
825            .gas_used(0)
826            .build();
827        let parent_sealed = SealedHeader::seal_slow(parent);
828
829        let child = TestHeaderBuilder::default()
830            .gas_limit(500_000_000)
831            .timestamp(11)
832            .timestamp_millis_part(600)
833            .number(2)
834            .parent_hash(parent_sealed.hash())
835            .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
836            .base_fee(TEMPO_T7_BASE_FEE_CAP * 7 / 8)
837            .build();
838        let child_sealed = SealedHeader::seal_slow(child);
839
840        assert!(
841            consensus
842                .validate_header_against_parent(&child_sealed, &parent_sealed)
843                .is_ok()
844        );
845
846        let bad_child = TestHeaderBuilder::default()
847            .gas_limit(500_000_000)
848            .timestamp(11)
849            .timestamp_millis_part(600)
850            .number(2)
851            .parent_hash(parent_sealed.hash())
852            .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
853            .base_fee(TEMPO_T7_BASE_FEE_CAP)
854            .build();
855        let bad_child_sealed = SealedHeader::seal_slow(bad_child);
856        let result = consensus.validate_header_against_parent(&bad_child_sealed, &parent_sealed);
857        assert!(
858            matches!(result, Err(ConsensusError::BaseFeeDiff(_))),
859            "Expected BaseFeeDiff error, got: {result:?}"
860        );
861
862        let parent = TestHeaderBuilder::default()
863            .gas_limit(500_000_000)
864            .timestamp(10)
865            .number(1)
866            .timestamp_millis_part(500)
867            .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
868            .base_fee(TEMPO_T7_BASE_FEE_FLOOR)
869            .gas_used(0)
870            .build();
871        let parent_sealed = SealedHeader::seal_slow(parent);
872        let child = TestHeaderBuilder::default()
873            .gas_limit(500_000_000)
874            .timestamp(11)
875            .timestamp_millis_part(600)
876            .number(2)
877            .parent_hash(parent_sealed.hash())
878            .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
879            .base_fee(TEMPO_T7_BASE_FEE_FLOOR)
880            .build();
881        let child_sealed = SealedHeader::seal_slow(child);
882
883        assert!(
884            consensus
885                .validate_header_against_parent(&child_sealed, &parent_sealed)
886                .is_ok()
887        );
888    }
889
890    #[test]
891    fn test_validate_body_against_header() {
892        let consensus = TempoConsensus::new(MODERATO.clone());
893        let header = TestHeaderBuilder::default()
894            .gas_limit(30_000_000)
895            .timestamp(current_timestamp_millis())
896            .build();
897        let sealed = SealedHeader::seal_slow(header);
898        let body = BlockBody {
899            withdrawals: Some(Default::default()),
900            ..Default::default()
901        };
902
903        assert!(
904            consensus
905                .validate_body_against_header(&body, &sealed)
906                .is_ok()
907        );
908    }
909
910    #[test]
911    fn test_validate_block_pre_execution() {
912        let consensus = TempoConsensus::new(MODERATO.clone());
913        let chain_id = MODERATO.chain().id();
914
915        let system_tx = create_system_tx(chain_id, SYSTEM_TX_ADDRESSES[0]);
916        let user_tx = create_tx(chain_id);
917
918        let header = TestHeaderBuilder::default()
919            .gas_limit(30_000_000)
920            .timestamp(current_timestamp_millis())
921            .build();
922        let block = create_valid_block(header, vec![user_tx, system_tx]);
923        let sealed = reth_primitives_traits::SealedBlock::seal_slow(block);
924
925        assert!(consensus.validate_block_pre_execution(&sealed).is_ok());
926    }
927
928    #[test]
929    fn test_validate_block_pre_execution_invalid_system_tx() {
930        let consensus = TempoConsensus::new(MODERATO.clone());
931        let chain_id = MODERATO.chain().id();
932
933        let tx = TxLegacy {
934            chain_id: Some(chain_id),
935            nonce: 0,
936            gas_price: 1_000_000_000,
937            gas_limit: 21000,
938            to: TxKind::Call(Address::ZERO),
939            value: U256::ZERO,
940            input: Default::default(),
941        };
942        let signature = Signature::new(U256::ZERO, U256::ZERO, false);
943        let invalid_system_tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, signature));
944        let tx_hash = *invalid_system_tx.tx_hash();
945
946        let header = TestHeaderBuilder::default()
947            .gas_limit(30_000_000)
948            .timestamp(current_timestamp_millis())
949            .build();
950        let block = create_valid_block(header, vec![invalid_system_tx]);
951        let sealed = SealedBlock::seal_slow(block);
952
953        let result = consensus.validate_block_pre_execution(&sealed);
954        let err = result.unwrap_err();
955        assert!(
956            err.downcast_other_ref::<TempoConsensusError>()
957                .is_some_and(
958                    |e| matches!(e, TempoConsensusError::InvalidSystemTransaction { tx_hash: h } if *h == tx_hash)
959                ),
960            "Expected InvalidSystemTransaction, got: {err:?}"
961        );
962    }
963
964    #[test]
965    fn test_validate_block_pre_execution_pre_t4_missing_system_tx() {
966        let consensus = TempoConsensus::new(MODERATO.clone());
967        let chain_id = MODERATO.chain().id();
968
969        let user_tx = create_tx(chain_id);
970
971        use tempo_chainspec::constants::moderato::MODERATO_T4_TIMESTAMP;
972
973        let header = TestHeaderBuilder::default()
974            .gas_limit(30_000_000)
975            .timestamp(MODERATO_T4_TIMESTAMP - 1)
976            .build();
977        let block = create_valid_block(header, vec![user_tx]);
978        let sealed = SealedBlock::seal_slow(block);
979
980        let result = consensus.validate_block_pre_execution(&sealed);
981        let err = result.unwrap_err();
982        assert!(
983            err.downcast_other_ref::<TempoConsensusError>()
984                .is_some_and(|e| matches!(
985                    e,
986                    TempoConsensusError::MissingEndOfBlockSystemTxs { .. }
987                )),
988            "Expected MissingEndOfBlockSystemTxs, got: {err:?}"
989        );
990    }
991
992    #[test]
993    fn test_validate_block_pre_execution_t4_allows_missing_system_tx() {
994        let consensus = TempoConsensus::new(DEV.clone());
995        let chain_id = DEV.chain().id();
996
997        let user_tx = create_tx(chain_id);
998
999        let header = TestHeaderBuilder::default()
1000            .gas_limit(30_000_000)
1001            .timestamp(0)
1002            .build();
1003        let block = create_valid_block(header, vec![user_tx]);
1004        let sealed = SealedBlock::seal_slow(block);
1005
1006        assert!(consensus.validate_block_pre_execution(&sealed).is_ok());
1007    }
1008
1009    #[test]
1010    fn test_validate_body_against_header_bad_tx_root() {
1011        let consensus = TempoConsensus::new(MODERATO.clone());
1012        let header = TestHeaderBuilder::default()
1013            .gas_limit(30_000_000)
1014            .timestamp(current_timestamp_millis())
1015            .build();
1016        let sealed = SealedHeader::seal_slow(header);
1017
1018        let chain_id = MODERATO.chain().id();
1019        let user_tx = create_tx(chain_id);
1020        let body = BlockBody {
1021            transactions: vec![user_tx],
1022            withdrawals: Some(Default::default()),
1023            ..Default::default()
1024        };
1025
1026        let result = consensus.validate_body_against_header(&body, &sealed);
1027        assert!(
1028            matches!(result, Err(ConsensusError::BodyTransactionRootDiff(_))),
1029            "Expected BodyTransactionRootDiff error, got: {result:?}"
1030        );
1031    }
1032
1033    #[test]
1034    fn test_validate_block_post_execution_bad_receipts() {
1035        let consensus = TempoConsensus::new(MODERATO.clone());
1036        let chain_id = MODERATO.chain().id();
1037
1038        let system_tx = create_system_tx(chain_id, SYSTEM_TX_ADDRESSES[0]);
1039        let user_tx = create_tx(chain_id);
1040
1041        let header = TestHeaderBuilder::default()
1042            .gas_limit(30_000_000)
1043            .timestamp(current_timestamp_millis())
1044            .build();
1045        let block = create_valid_block(header, vec![user_tx, system_tx]);
1046        let recovered = RecoveredBlock::new_unhashed(block, vec![Address::ZERO, Address::ZERO]);
1047
1048        let receipt = TempoReceipt {
1049            tx_type: tempo_primitives::TempoTxType::Legacy,
1050            success: true,
1051            cumulative_gas_used: 0,
1052            logs: vec![],
1053        };
1054        let result = BlockExecutionResult {
1055            receipts: vec![receipt],
1056            requests: Default::default(),
1057            gas_used: 0,
1058            blob_gas_used: 0,
1059        };
1060
1061        let err = consensus
1062            .validate_block_post_execution(&recovered, &result, None, None)
1063            .unwrap_err();
1064        assert!(
1065            matches!(err, ConsensusError::BodyReceiptRootDiff(_)),
1066            "Expected BodyReceiptRootDiff error, got: {err:?}"
1067        );
1068    }
1069
1070    #[test]
1071    fn test_validate_header_timestamp_exactly_at_boundary() {
1072        let consensus = TempoConsensus::new(MODERATO.clone());
1073        let boundary_timestamp = current_timestamp_millis() + ALLOWED_FUTURE_BLOCK_TIME_MILLIS;
1074        let header = TestHeaderBuilder::default()
1075            .gas_limit(30_000_000)
1076            .timestamp_millis(boundary_timestamp)
1077            .shared_gas_limit(MODERATO.shared_gas_limit_at(boundary_timestamp / 1000, 30_000_000))
1078            .build();
1079        let sealed = SealedHeader::seal_slow(header);
1080
1081        let result = consensus.validate_header(&sealed);
1082        assert!(
1083            result.is_ok(),
1084            "Timestamp exactly at boundary should be accepted, got: {result:?}"
1085        );
1086    }
1087
1088    #[test]
1089    fn test_timestamp_in_future_is_transient_error() {
1090        let consensus = TempoConsensus::new(MODERATO.clone());
1091        let err = ConsensusError::TimestampIsInFuture {
1092            timestamp: 2,
1093            present_timestamp: 1,
1094        };
1095
1096        assert!(Consensus::<Block>::is_transient_error(&consensus, &err));
1097
1098        let err = ConsensusError::TimestampIsInPast {
1099            parent_timestamp: 2,
1100            timestamp: 1,
1101        };
1102
1103        assert!(!Consensus::<Block>::is_transient_error(&consensus, &err));
1104    }
1105
1106    #[test]
1107    fn test_validate_block_pre_execution_system_tx_out_of_order() {
1108        let consensus = TempoConsensus::new(MODERATO.clone());
1109        let chain_id = MODERATO.chain().id();
1110
1111        let wrong_addr = Address::repeat_byte(0xFF);
1112        let system_tx = create_system_tx(chain_id, wrong_addr);
1113
1114        use tempo_chainspec::constants::moderato::MODERATO_T4_TIMESTAMP;
1115
1116        let header = TestHeaderBuilder::default()
1117            .gas_limit(30_000_000)
1118            .timestamp(MODERATO_T4_TIMESTAMP - 1)
1119            .build();
1120        let block = create_valid_block(header, vec![system_tx]);
1121        let sealed = SealedBlock::seal_slow(block);
1122
1123        let result = consensus.validate_block_pre_execution(&sealed);
1124        let err = result.unwrap_err();
1125        assert!(
1126            err.downcast_other_ref::<TempoConsensusError>()
1127                .is_some_and(|e| matches!(
1128                    e,
1129                    TempoConsensusError::InvalidEndOfBlockSystemTxOrder { .. }
1130                )),
1131            "Expected InvalidEndOfBlockSystemTxOrder, got: {err:?}"
1132        );
1133    }
1134}