Skip to main content

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