reth_ethereum_consensus/
lib.rs

1//! Beacon consensus implementation.
2
3#![doc(
4    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
7)]
8#![cfg_attr(not(test), warn(unused_crate_dependencies))]
9#![cfg_attr(docsrs, feature(doc_cfg))]
10#![cfg_attr(not(feature = "std"), no_std)]
11
12extern crate alloc;
13
14use alloc::{fmt::Debug, sync::Arc};
15use alloy_consensus::{constants::MAXIMUM_EXTRA_DATA_SIZE, EMPTY_OMMER_ROOT_HASH};
16use alloy_eips::eip7840::BlobParams;
17use reth_chainspec::{EthChainSpec, EthereumHardforks};
18use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
19use reth_consensus_common::validation::{
20    validate_4844_header_standalone, validate_against_parent_4844,
21    validate_against_parent_eip1559_base_fee, validate_against_parent_gas_limit,
22    validate_against_parent_hash_number, validate_against_parent_timestamp,
23    validate_block_pre_execution, validate_body_against_header, validate_header_base_fee,
24    validate_header_extra_data, validate_header_gas,
25};
26use reth_execution_types::BlockExecutionResult;
27use reth_primitives_traits::{
28    Block, BlockHeader, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader,
29};
30
31mod validation;
32pub use validation::validate_block_post_execution;
33
34/// Ethereum beacon consensus
35///
36/// This consensus engine does basic checks as outlined in the execution specs.
37#[derive(Debug, Clone)]
38pub struct EthBeaconConsensus<ChainSpec> {
39    /// Configuration
40    chain_spec: Arc<ChainSpec>,
41    /// Maximum allowed extra data size in bytes
42    max_extra_data_size: usize,
43}
44
45impl<ChainSpec: EthChainSpec + EthereumHardforks> EthBeaconConsensus<ChainSpec> {
46    /// Create a new instance of [`EthBeaconConsensus`]
47    pub const fn new(chain_spec: Arc<ChainSpec>) -> Self {
48        Self { chain_spec, max_extra_data_size: MAXIMUM_EXTRA_DATA_SIZE }
49    }
50
51    /// Returns the maximum allowed extra data size.
52    pub const fn max_extra_data_size(&self) -> usize {
53        self.max_extra_data_size
54    }
55
56    /// Sets the maximum allowed extra data size and returns the updated instance.
57    pub const fn with_max_extra_data_size(mut self, size: usize) -> Self {
58        self.max_extra_data_size = size;
59        self
60    }
61
62    /// Returns the chain spec associated with this consensus engine.
63    pub const fn chain_spec(&self) -> &Arc<ChainSpec> {
64        &self.chain_spec
65    }
66}
67
68impl<ChainSpec, N> FullConsensus<N> for EthBeaconConsensus<ChainSpec>
69where
70    ChainSpec: Send + Sync + EthChainSpec<Header = N::BlockHeader> + EthereumHardforks + Debug,
71    N: NodePrimitives,
72{
73    fn validate_block_post_execution(
74        &self,
75        block: &RecoveredBlock<N::Block>,
76        result: &BlockExecutionResult<N::Receipt>,
77        receipt_root_bloom: Option<ReceiptRootBloom>,
78    ) -> Result<(), ConsensusError> {
79        validate_block_post_execution(
80            block,
81            &self.chain_spec,
82            &result.receipts,
83            &result.requests,
84            receipt_root_bloom,
85        )
86    }
87}
88
89impl<B, ChainSpec> Consensus<B> for EthBeaconConsensus<ChainSpec>
90where
91    B: Block,
92    ChainSpec: EthChainSpec<Header = B::Header> + EthereumHardforks + Debug + Send + Sync,
93{
94    fn validate_body_against_header(
95        &self,
96        body: &B::Body,
97        header: &SealedHeader<B::Header>,
98    ) -> Result<(), ConsensusError> {
99        validate_body_against_header(body, header.header())
100    }
101
102    fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError> {
103        validate_block_pre_execution(block, &self.chain_spec)
104    }
105}
106
107impl<H, ChainSpec> HeaderValidator<H> for EthBeaconConsensus<ChainSpec>
108where
109    H: BlockHeader,
110    ChainSpec: EthChainSpec<Header = H> + EthereumHardforks + Debug + Send + Sync,
111{
112    fn validate_header(&self, header: &SealedHeader<H>) -> Result<(), ConsensusError> {
113        let header = header.header();
114        let is_post_merge = self.chain_spec.is_paris_active_at_block(header.number());
115
116        if is_post_merge {
117            if !header.difficulty().is_zero() {
118                return Err(ConsensusError::TheMergeDifficultyIsNotZero);
119            }
120
121            if !header.nonce().is_some_and(|nonce| nonce.is_zero()) {
122                return Err(ConsensusError::TheMergeNonceIsNotZero);
123            }
124
125            if header.ommers_hash() != EMPTY_OMMER_ROOT_HASH {
126                return Err(ConsensusError::TheMergeOmmerRootIsNotEmpty);
127            }
128        } else {
129            #[cfg(feature = "std")]
130            {
131                let present_timestamp = std::time::SystemTime::now()
132                    .duration_since(std::time::SystemTime::UNIX_EPOCH)
133                    .unwrap()
134                    .as_secs();
135
136                if header.timestamp() >
137                    present_timestamp + alloy_eips::merge::ALLOWED_FUTURE_BLOCK_TIME_SECONDS
138                {
139                    return Err(ConsensusError::TimestampIsInFuture {
140                        timestamp: header.timestamp(),
141                        present_timestamp,
142                    });
143                }
144            }
145        }
146        validate_header_extra_data(header, self.max_extra_data_size)?;
147        validate_header_gas(header)?;
148        validate_header_base_fee(header, &self.chain_spec)?;
149
150        // EIP-4895: Beacon chain push withdrawals as operations
151        if self.chain_spec.is_shanghai_active_at_timestamp(header.timestamp()) &&
152            header.withdrawals_root().is_none()
153        {
154            return Err(ConsensusError::WithdrawalsRootMissing)
155        } else if !self.chain_spec.is_shanghai_active_at_timestamp(header.timestamp()) &&
156            header.withdrawals_root().is_some()
157        {
158            return Err(ConsensusError::WithdrawalsRootUnexpected)
159        }
160
161        // Ensures that EIP-4844 fields are valid once cancun is active.
162        if self.chain_spec.is_cancun_active_at_timestamp(header.timestamp()) {
163            validate_4844_header_standalone(
164                header,
165                self.chain_spec
166                    .blob_params_at_timestamp(header.timestamp())
167                    .unwrap_or_else(BlobParams::cancun),
168            )?;
169        } else if header.blob_gas_used().is_some() {
170            return Err(ConsensusError::BlobGasUsedUnexpected)
171        } else if header.excess_blob_gas().is_some() {
172            return Err(ConsensusError::ExcessBlobGasUnexpected)
173        } else if header.parent_beacon_block_root().is_some() {
174            return Err(ConsensusError::ParentBeaconBlockRootUnexpected)
175        }
176
177        if self.chain_spec.is_prague_active_at_timestamp(header.timestamp()) {
178            if header.requests_hash().is_none() {
179                return Err(ConsensusError::RequestsHashMissing)
180            }
181        } else if header.requests_hash().is_some() {
182            return Err(ConsensusError::RequestsHashUnexpected)
183        }
184
185        Ok(())
186    }
187
188    fn validate_header_against_parent(
189        &self,
190        header: &SealedHeader<H>,
191        parent: &SealedHeader<H>,
192    ) -> Result<(), ConsensusError> {
193        validate_against_parent_hash_number(header.header(), parent)?;
194
195        validate_against_parent_timestamp(header.header(), parent.header())?;
196
197        validate_against_parent_gas_limit(header, parent, &self.chain_spec)?;
198
199        validate_against_parent_eip1559_base_fee(
200            header.header(),
201            parent.header(),
202            &self.chain_spec,
203        )?;
204
205        // ensure that the blob gas fields for this block
206        if let Some(blob_params) = self.chain_spec.blob_params_at_timestamp(header.timestamp()) {
207            validate_against_parent_4844(header.header(), parent.header(), blob_params)?;
208        }
209
210        Ok(())
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use alloy_consensus::Header;
218    use alloy_primitives::B256;
219    use reth_chainspec::{ChainSpec, ChainSpecBuilder};
220    use reth_consensus_common::validation::validate_against_parent_gas_limit;
221    use reth_primitives_traits::{
222        constants::{GAS_LIMIT_BOUND_DIVISOR, MINIMUM_GAS_LIMIT},
223        proofs,
224    };
225
226    fn header_with_gas_limit(gas_limit: u64) -> SealedHeader {
227        let header = reth_primitives_traits::Header { gas_limit, ..Default::default() };
228        SealedHeader::new(header, B256::ZERO)
229    }
230
231    #[test]
232    fn test_valid_gas_limit_increase() {
233        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
234        let child = header_with_gas_limit((parent.gas_limit + 5) as u64);
235
236        assert!(validate_against_parent_gas_limit(
237            &child,
238            &parent,
239            &ChainSpec::<Header>::default()
240        )
241        .is_ok());
242    }
243
244    #[test]
245    fn test_gas_limit_below_minimum() {
246        let parent = header_with_gas_limit(MINIMUM_GAS_LIMIT);
247        let child = header_with_gas_limit(MINIMUM_GAS_LIMIT - 1);
248
249        assert!(matches!(
250            validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
251            ConsensusError::GasLimitInvalidMinimum { child_gas_limit }
252                if child_gas_limit == child.gas_limit as u64
253        ));
254    }
255
256    #[test]
257    fn test_invalid_gas_limit_increase_exceeding_limit() {
258        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
259        let child = header_with_gas_limit(
260            parent.gas_limit + parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR + 1,
261        );
262
263        assert!(matches!(
264            validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
265            ConsensusError::GasLimitInvalidIncrease { parent_gas_limit, child_gas_limit }
266                if parent_gas_limit == parent.gas_limit && child_gas_limit == child.gas_limit
267        ));
268    }
269
270    #[test]
271    fn test_valid_gas_limit_decrease_within_limit() {
272        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
273        let child = header_with_gas_limit(parent.gas_limit - 5);
274
275        assert!(validate_against_parent_gas_limit(
276            &child,
277            &parent,
278            &ChainSpec::<Header>::default()
279        )
280        .is_ok());
281    }
282
283    #[test]
284    fn test_invalid_gas_limit_decrease_exceeding_limit() {
285        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
286        let child = header_with_gas_limit(
287            parent.gas_limit - parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR - 1,
288        );
289
290        assert!(matches!(
291            validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
292            ConsensusError::GasLimitInvalidDecrease { parent_gas_limit, child_gas_limit }
293                if parent_gas_limit == parent.gas_limit && child_gas_limit == child.gas_limit
294        ));
295    }
296
297    #[test]
298    fn shanghai_block_zero_withdrawals() {
299        // ensures that if shanghai is activated, and we include a block with a withdrawals root,
300        // that the header is valid
301        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().shanghai_activated().build());
302
303        let header = reth_primitives_traits::Header {
304            base_fee_per_gas: Some(1337),
305            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
306            ..Default::default()
307        };
308
309        assert!(EthBeaconConsensus::new(chain_spec)
310            .validate_header(&SealedHeader::seal_slow(header,))
311            .is_ok());
312    }
313}