Skip to main content

tempo_consensus/
lib.rs

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