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