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, revm::primitives::Address};
8use reth_chainspec::EthChainSpec;
9use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator};
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::{hardfork::TempoHardforks, spec::TempoChainSpec};
18use tempo_contracts::precompiles::{
19    STABLECOIN_EXCHANGE_ADDRESS, TIP_FEE_MANAGER_ADDRESS, TIP20_REWARDS_REGISTRY_ADDRESS,
20};
21use tempo_primitives::{
22    Block, BlockBody, TempoHeader, TempoPrimitives, TempoReceipt, TempoTxEnvelope,
23};
24
25// End-of-block system transactions (required)
26const END_OF_BLOCK_SYSTEM_TX_COUNT: usize = 3;
27const END_OF_BLOCK_SYSTEM_TX_ADDRESSES: [Address; END_OF_BLOCK_SYSTEM_TX_COUNT] = [
28    TIP_FEE_MANAGER_ADDRESS,
29    STABLECOIN_EXCHANGE_ADDRESS,
30    Address::ZERO,
31];
32
33/// How far in the future the block timestamp can be.
34pub const ALLOWED_FUTURE_BLOCK_TIME_SECONDS: u64 = 3;
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 {
47            inner: EthBeaconConsensus::new(chain_spec)
48                .with_max_extra_data_size(TEMPO_MAXIMUM_EXTRA_DATA_SIZE),
49        }
50    }
51}
52
53impl HeaderValidator<TempoHeader> for TempoConsensus {
54    fn validate_header(&self, header: &SealedHeader<TempoHeader>) -> Result<(), ConsensusError> {
55        self.inner.validate_header(header)?;
56
57        let present_timestamp = std::time::SystemTime::now()
58            .duration_since(std::time::SystemTime::UNIX_EPOCH)
59            .expect("system time should never be before UNIX EPOCH")
60            .as_secs();
61
62        if header.timestamp() > present_timestamp + ALLOWED_FUTURE_BLOCK_TIME_SECONDS {
63            return Err(ConsensusError::TimestampIsInFuture {
64                timestamp: header.timestamp(),
65                present_timestamp,
66            });
67        }
68
69        if header.shared_gas_limit != header.gas_limit() / TEMPO_SHARED_GAS_DIVISOR {
70            return Err(ConsensusError::Other(
71                "Shared gas limit does not match header gas limit".to_string(),
72            ));
73        }
74
75        // Validate the non-payment gas limit
76        if header.general_gas_limit
77            != (header.gas_limit() - header.shared_gas_limit) / TEMPO_GENERAL_GAS_DIVISOR
78        {
79            return Err(ConsensusError::Other(
80                "Non-payment gas limit does not match header gas limit".to_string(),
81            ));
82        }
83
84        // Validate the timestamp milliseconds part
85        if header.timestamp_millis_part >= 1000 {
86            return Err(ConsensusError::Other(
87                "Timestamp milliseconds part must be less than 1000".to_string(),
88            ));
89        }
90
91        Ok(())
92    }
93
94    fn validate_header_against_parent(
95        &self,
96        header: &SealedHeader<TempoHeader>,
97        parent: &SealedHeader<TempoHeader>,
98    ) -> Result<(), ConsensusError> {
99        validate_against_parent_hash_number(header.header(), parent)?;
100
101        validate_against_parent_gas_limit(header, parent, self.inner.chain_spec())?;
102
103        validate_against_parent_eip1559_base_fee(
104            header.header(),
105            parent.header(),
106            self.inner.chain_spec(),
107        )?;
108
109        if let Some(blob_params) = self
110            .inner
111            .chain_spec()
112            .blob_params_at_timestamp(header.timestamp())
113        {
114            validate_against_parent_4844(header.header(), parent.header(), blob_params)?;
115        }
116
117        if header.timestamp_millis() <= parent.timestamp_millis() {
118            return Err(ConsensusError::TimestampIsInPast {
119                parent_timestamp: parent.timestamp_millis(),
120                timestamp: header.timestamp_millis(),
121            });
122        }
123
124        Ok(())
125    }
126}
127
128impl Consensus<Block> for TempoConsensus {
129    type Error = ConsensusError;
130
131    fn validate_body_against_header(
132        &self,
133        body: &BlockBody,
134        header: &SealedHeader<TempoHeader>,
135    ) -> Result<(), Self::Error> {
136        Consensus::<Block>::validate_body_against_header(&self.inner, body, header)
137    }
138
139    fn validate_block_pre_execution(&self, block: &SealedBlock<Block>) -> Result<(), Self::Error> {
140        let transactions = &block.body().transactions;
141
142        if let Some(tx) = transactions.iter().find(|&tx| {
143            tx.is_system_tx() && !tx.is_valid_system_tx(self.inner.chain_spec().chain().id())
144        }) {
145            return Err(ConsensusError::Other(format!(
146                "Invalid system transaction: {}",
147                tx.tx_hash()
148            )));
149        }
150
151        // If the moderator hardfork is not active, validate that the TIP20 Rewards Registry system
152        // tx is the first transaction in the block. If the block timestamp is post moderato, skip
153        // this step
154        if !self
155            .inner
156            .chain_spec()
157            .is_moderato_active_at_timestamp(block.timestamp())
158        {
159            // Check for optional rewards registry system transaction at the start
160            if let Some(first_tx) = transactions.first()
161                && first_tx.is_system_tx()
162                && first_tx.to().unwrap_or_default() != TIP20_REWARDS_REGISTRY_ADDRESS
163            {
164                return Err(ConsensusError::Other(
165                    "First transaction must be rewards registry if it's a system tx".to_string(),
166                ));
167            }
168        }
169
170        // Get the last END_OF_BLOCK_SYSTEM_TX_COUNT transactions and validate they are end-of-block system txs
171        let end_of_block_system_txs = transactions
172            .get(
173                transactions
174                    .len()
175                    .saturating_sub(END_OF_BLOCK_SYSTEM_TX_COUNT)..,
176            )
177            .map(|slice| {
178                slice
179                    .iter()
180                    .filter(|tx| tx.is_system_tx())
181                    .collect::<Vec<&TempoTxEnvelope>>()
182            })
183            .unwrap_or_default();
184
185        if end_of_block_system_txs.len() != END_OF_BLOCK_SYSTEM_TX_COUNT {
186            return Err(ConsensusError::Other(
187                "Block must contain end-of-block system txs".to_string(),
188            ));
189        }
190
191        // Validate that the sequence of end-of-block system txs is correct
192        for (tx, expected_to) in end_of_block_system_txs
193            .into_iter()
194            .zip(END_OF_BLOCK_SYSTEM_TX_ADDRESSES)
195        {
196            if tx.to().unwrap_or_default() != expected_to {
197                return Err(ConsensusError::Other(
198                    "Invalid end-of-block system tx order".to_string(),
199                ));
200            }
201        }
202
203        self.inner.validate_block_pre_execution(block)
204    }
205}
206
207impl FullConsensus<TempoPrimitives> for TempoConsensus {
208    fn validate_block_post_execution(
209        &self,
210        block: &RecoveredBlock<Block>,
211        result: &BlockExecutionResult<TempoReceipt>,
212    ) -> Result<(), ConsensusError> {
213        FullConsensus::<TempoPrimitives>::validate_block_post_execution(&self.inner, block, result)
214    }
215}
216
217/// Divisor for calculating non-payment gas limit.
218pub const TEMPO_GENERAL_GAS_DIVISOR: u64 = 2;
219
220/// Divisor for calculating shared gas limit.
221pub const TEMPO_SHARED_GAS_DIVISOR: u64 = 10;
222
223/// Maximum extra data size for Tempo blocks.
224pub const TEMPO_MAXIMUM_EXTRA_DATA_SIZE: usize = 10 * 1_024; // 10KiB