1#![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
25const 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
33pub const ALLOWED_FUTURE_BLOCK_TIME_SECONDS: u64 = 3;
35
36#[derive(Debug, Clone)]
38pub struct TempoConsensus {
39 inner: EthBeaconConsensus<TempoChainSpec>,
41}
42
43impl TempoConsensus {
44 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 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 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 !self
155 .inner
156 .chain_spec()
157 .is_moderato_active_at_timestamp(block.timestamp())
158 {
159 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 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 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
217pub const TEMPO_GENERAL_GAS_DIVISOR: u64 = 2;
219
220pub const TEMPO_SHARED_GAS_DIVISOR: u64 = 10;
222
223pub const TEMPO_MAXIMUM_EXTRA_DATA_SIZE: usize = 10 * 1_024;