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 alloy_primitives::B256;
18use reth_chainspec::{EthChainSpec, EthereumHardforks};
19use reth_consensus::{
20    Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom, TransactionRoot,
21};
22use reth_consensus_common::validation::{
23    validate_4844_header_standalone, validate_against_parent_4844,
24    validate_against_parent_eip1559_base_fee, validate_against_parent_gas_limit,
25    validate_against_parent_hash_number, validate_against_parent_timestamp,
26    validate_block_pre_execution, validate_block_pre_execution_with_tx_root,
27    validate_body_against_header, validate_header_base_fee, validate_header_extra_data,
28    validate_header_gas,
29};
30use reth_execution_types::BlockExecutionResult;
31use reth_primitives_traits::{
32    Block, BlockHeader, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader,
33};
34
35mod validation;
36pub use validation::validate_block_post_execution;
37
38/// Ethereum beacon consensus
39///
40/// This consensus engine does basic checks as outlined in the execution specs.
41#[derive(Debug, Clone)]
42pub struct EthBeaconConsensus<ChainSpec> {
43    /// Configuration
44    chain_spec: Arc<ChainSpec>,
45    /// Maximum allowed extra data size in bytes
46    max_extra_data_size: usize,
47    /// When true, skips the gas limit change validation between parent and child blocks.
48    skip_gas_limit_ramp_check: bool,
49    /// When true, skips the blob gas used check in header validation.
50    skip_blob_gas_used_check: bool,
51    /// When true, skips the requests hash check in post-execution validation.
52    skip_requests_hash_check: bool,
53    /// When true, allows BAL hashes before Amsterdam activation.
54    allow_bal_hashes: bool,
55}
56
57impl<ChainSpec: EthChainSpec + EthereumHardforks> EthBeaconConsensus<ChainSpec> {
58    /// Create a new instance of [`EthBeaconConsensus`]
59    pub const fn new(chain_spec: Arc<ChainSpec>) -> Self {
60        Self {
61            chain_spec,
62            max_extra_data_size: MAXIMUM_EXTRA_DATA_SIZE,
63            skip_gas_limit_ramp_check: false,
64            skip_blob_gas_used_check: false,
65            skip_requests_hash_check: false,
66            allow_bal_hashes: false,
67        }
68    }
69
70    /// Returns the maximum allowed extra data size.
71    pub const fn max_extra_data_size(&self) -> usize {
72        self.max_extra_data_size
73    }
74
75    /// Sets the maximum allowed extra data size and returns the updated instance.
76    pub const fn with_max_extra_data_size(mut self, size: usize) -> Self {
77        self.max_extra_data_size = size;
78        self
79    }
80
81    /// Disables the gas limit change validation between parent and child blocks.
82    pub const fn with_skip_gas_limit_ramp_check(mut self, skip: bool) -> Self {
83        self.skip_gas_limit_ramp_check = skip;
84        self
85    }
86
87    /// Disables the blob gas used check in header validation.
88    pub const fn with_skip_blob_gas_used_check(mut self, skip: bool) -> Self {
89        self.skip_blob_gas_used_check = skip;
90        self
91    }
92
93    /// Disables the requests hash check in post-execution validation.
94    pub const fn with_skip_requests_hash_check(mut self, skip: bool) -> Self {
95        self.skip_requests_hash_check = skip;
96        self
97    }
98
99    /// Allows BAL hashes before Amsterdam activation.
100    pub const fn with_allow_bal_hashes(mut self, allow: bool) -> Self {
101        self.allow_bal_hashes = allow;
102        self
103    }
104
105    /// Returns the chain spec associated with this consensus engine.
106    pub const fn chain_spec(&self) -> &Arc<ChainSpec> {
107        &self.chain_spec
108    }
109}
110
111impl<ChainSpec, N> FullConsensus<N> for EthBeaconConsensus<ChainSpec>
112where
113    ChainSpec: Send + Sync + EthChainSpec<Header = N::BlockHeader> + EthereumHardforks + Debug,
114    N: NodePrimitives,
115{
116    fn validate_block_post_execution(
117        &self,
118        block: &RecoveredBlock<N::Block>,
119        result: &BlockExecutionResult<N::Receipt>,
120        receipt_root_bloom: Option<ReceiptRootBloom>,
121        block_access_list_hash: Option<B256>,
122    ) -> Result<(), ConsensusError> {
123        let res = validation::validate_block_post_execution_with_bal_hashes(
124            block,
125            &self.chain_spec,
126            result,
127            receipt_root_bloom,
128            block_access_list_hash,
129            self.allow_bal_hashes,
130        );
131
132        if self.skip_requests_hash_check &&
133            let Err(ConsensusError::BodyRequestsHashDiff(_)) = &res
134        {
135            return Ok(());
136        }
137
138        res
139    }
140}
141
142impl<B, ChainSpec> Consensus<B> for EthBeaconConsensus<ChainSpec>
143where
144    B: Block,
145    ChainSpec: EthChainSpec<Header = B::Header> + EthereumHardforks + Debug + Send + Sync,
146{
147    fn validate_body_against_header(
148        &self,
149        body: &B::Body,
150        header: &SealedHeader<B::Header>,
151    ) -> Result<(), ConsensusError> {
152        validate_body_against_header(body, header.header())
153    }
154
155    fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError> {
156        validate_block_pre_execution(block, &self.chain_spec)
157    }
158
159    fn validate_block_pre_execution_with_tx_root(
160        &self,
161        block: &SealedBlock<B>,
162        transaction_root: Option<TransactionRoot>,
163    ) -> Result<(), ConsensusError> {
164        validate_block_pre_execution_with_tx_root(block, &self.chain_spec, transaction_root)
165    }
166}
167
168impl<H, ChainSpec> HeaderValidator<H> for EthBeaconConsensus<ChainSpec>
169where
170    H: BlockHeader,
171    ChainSpec: EthChainSpec<Header = H> + EthereumHardforks + Debug + Send + Sync,
172{
173    fn validate_header(&self, header: &SealedHeader<H>) -> Result<(), ConsensusError> {
174        let header = header.header();
175        let is_post_merge = self.chain_spec.is_paris_active_at_block(header.number());
176
177        if is_post_merge {
178            if !header.difficulty().is_zero() {
179                return Err(ConsensusError::TheMergeDifficultyIsNotZero);
180            }
181
182            if !header.nonce().is_some_and(|nonce| nonce.is_zero()) {
183                return Err(ConsensusError::TheMergeNonceIsNotZero);
184            }
185
186            if header.ommers_hash() != EMPTY_OMMER_ROOT_HASH {
187                return Err(ConsensusError::TheMergeOmmerRootIsNotEmpty);
188            }
189        } else {
190            #[cfg(feature = "std")]
191            {
192                let present_timestamp = std::time::SystemTime::now()
193                    .duration_since(std::time::SystemTime::UNIX_EPOCH)
194                    .unwrap()
195                    .as_secs();
196
197                if header.timestamp() >
198                    present_timestamp + alloy_eips::merge::ALLOWED_FUTURE_BLOCK_TIME_SECONDS
199                {
200                    return Err(ConsensusError::TimestampIsInFuture {
201                        timestamp: header.timestamp(),
202                        present_timestamp,
203                    });
204                }
205            }
206        }
207        validate_header_extra_data(header, self.max_extra_data_size)?;
208        validate_header_gas(header)?;
209        validate_header_base_fee(header, &self.chain_spec)?;
210
211        // EIP-4895: Beacon chain push withdrawals as operations
212        if self.chain_spec.is_shanghai_active_at_timestamp(header.timestamp()) &&
213            header.withdrawals_root().is_none()
214        {
215            return Err(ConsensusError::WithdrawalsRootMissing)
216        } else if !self.chain_spec.is_shanghai_active_at_timestamp(header.timestamp()) &&
217            header.withdrawals_root().is_some()
218        {
219            return Err(ConsensusError::WithdrawalsRootUnexpected)
220        }
221
222        // Ensures that EIP-4844 fields are valid once cancun is active.
223        if self.chain_spec.is_cancun_active_at_timestamp(header.timestamp()) {
224            if !self.skip_blob_gas_used_check {
225                validate_4844_header_standalone(
226                    header,
227                    self.chain_spec
228                        .blob_params_at_timestamp(header.timestamp())
229                        .unwrap_or_else(BlobParams::cancun),
230                )?;
231            }
232        } else if header.blob_gas_used().is_some() {
233            return Err(ConsensusError::BlobGasUsedUnexpected)
234        } else if header.excess_blob_gas().is_some() {
235            return Err(ConsensusError::ExcessBlobGasUnexpected)
236        } else if header.parent_beacon_block_root().is_some() {
237            return Err(ConsensusError::ParentBeaconBlockRootUnexpected)
238        }
239
240        if self.chain_spec.is_prague_active_at_timestamp(header.timestamp()) {
241            if header.requests_hash().is_none() {
242                return Err(ConsensusError::RequestsHashMissing)
243            }
244        } else if header.requests_hash().is_some() {
245            return Err(ConsensusError::RequestsHashUnexpected)
246        }
247
248        if self.chain_spec.is_amsterdam_active_at_timestamp(header.timestamp()) {
249            if header.block_access_list_hash().is_none() {
250                return Err(ConsensusError::BlockAccessListHashMissing)
251            }
252            if header.slot_number().is_none() {
253                return Err(ConsensusError::SlotNumberMissing)
254            }
255        } else {
256            if header.block_access_list_hash().is_some() && !self.allow_bal_hashes {
257                return Err(ConsensusError::BlockAccessListHashUnexpected)
258            }
259            if header.slot_number().is_some() {
260                return Err(ConsensusError::SlotNumberUnexpected)
261            }
262        }
263
264        Ok(())
265    }
266
267    fn validate_header_against_parent(
268        &self,
269        header: &SealedHeader<H>,
270        parent: &SealedHeader<H>,
271    ) -> Result<(), ConsensusError> {
272        validate_against_parent_hash_number(header.header(), parent)?;
273
274        validate_against_parent_timestamp(header.header(), parent.header())?;
275
276        if !self.skip_gas_limit_ramp_check {
277            validate_against_parent_gas_limit(header, parent, &self.chain_spec)?;
278        }
279
280        validate_against_parent_eip1559_base_fee(
281            header.header(),
282            parent.header(),
283            &self.chain_spec,
284        )?;
285
286        // ensure that the blob gas fields for this block
287        if let Some(blob_params) = self.chain_spec.blob_params_at_timestamp(header.timestamp()) {
288            validate_against_parent_4844(header.header(), parent.header(), blob_params)?;
289        }
290
291        Ok(())
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use alloy_consensus::Header;
299    use alloy_eips::eip7685::EMPTY_REQUESTS_HASH;
300    use alloy_primitives::B256;
301    use reth_chainspec::{ChainSpec, ChainSpecBuilder};
302    use reth_consensus_common::validation::validate_against_parent_gas_limit;
303    use reth_ethereum_primitives::{Block as EthBlock, EthPrimitives, Receipt};
304    use reth_primitives_traits::{
305        constants::{GAS_LIMIT_BOUND_DIVISOR, MINIMUM_GAS_LIMIT},
306        proofs,
307    };
308
309    fn header_with_gas_limit(gas_limit: u64) -> SealedHeader {
310        let header = reth_primitives_traits::Header { gas_limit, ..Default::default() };
311        SealedHeader::new(header, B256::ZERO)
312    }
313
314    fn valid_prague_header() -> reth_primitives_traits::Header {
315        reth_primitives_traits::Header {
316            base_fee_per_gas: Some(1337),
317            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
318            blob_gas_used: Some(0),
319            excess_blob_gas: Some(0),
320            parent_beacon_block_root: Some(B256::ZERO),
321            requests_hash: Some(EMPTY_REQUESTS_HASH),
322            ..Default::default()
323        }
324    }
325
326    fn prague_recovered_block_with_bal_hash(hash: B256) -> RecoveredBlock<EthBlock> {
327        let mut header = valid_prague_header();
328        header.block_access_list_hash = Some(hash);
329        RecoveredBlock::new_unhashed(EthBlock { header, body: Default::default() }, Vec::new())
330    }
331
332    #[test]
333    fn test_valid_gas_limit_increase() {
334        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
335        let child = header_with_gas_limit(parent.gas_limit + 5);
336
337        assert!(validate_against_parent_gas_limit(
338            &child,
339            &parent,
340            &ChainSpec::<Header>::default()
341        )
342        .is_ok());
343    }
344
345    #[test]
346    fn test_gas_limit_below_minimum() {
347        let parent = header_with_gas_limit(MINIMUM_GAS_LIMIT);
348        let child = header_with_gas_limit(MINIMUM_GAS_LIMIT - 1);
349
350        assert!(matches!(
351            validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
352            ConsensusError::GasLimitInvalidMinimum { child_gas_limit }
353                if child_gas_limit == child.gas_limit
354        ));
355    }
356
357    #[test]
358    fn test_invalid_gas_limit_increase_exceeding_limit() {
359        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
360        let child = header_with_gas_limit(
361            parent.gas_limit + parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR + 1,
362        );
363
364        assert!(matches!(
365            validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
366            ConsensusError::GasLimitInvalidIncrease { parent_gas_limit, child_gas_limit }
367                if parent_gas_limit == parent.gas_limit && child_gas_limit == child.gas_limit
368        ));
369    }
370
371    #[test]
372    fn test_valid_gas_limit_decrease_within_limit() {
373        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
374        let child = header_with_gas_limit(parent.gas_limit - 5);
375
376        assert!(validate_against_parent_gas_limit(
377            &child,
378            &parent,
379            &ChainSpec::<Header>::default()
380        )
381        .is_ok());
382    }
383
384    #[test]
385    fn test_invalid_gas_limit_decrease_exceeding_limit() {
386        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
387        let child = header_with_gas_limit(
388            parent.gas_limit - parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR - 1,
389        );
390
391        assert!(matches!(
392            validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
393            ConsensusError::GasLimitInvalidDecrease { parent_gas_limit, child_gas_limit }
394                if parent_gas_limit == parent.gas_limit && child_gas_limit == child.gas_limit
395        ));
396    }
397
398    #[test]
399    fn shanghai_block_zero_withdrawals() {
400        // ensures that if shanghai is activated, and we include a block with a withdrawals root,
401        // that the header is valid
402        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().shanghai_activated().build());
403
404        let header = reth_primitives_traits::Header {
405            base_fee_per_gas: Some(1337),
406            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
407            ..Default::default()
408        };
409
410        assert!(EthBeaconConsensus::new(chain_spec)
411            .validate_header(&SealedHeader::seal_slow(header,))
412            .is_ok());
413    }
414
415    #[test]
416    fn prague_header_rejects_block_access_list_hash_before_amsterdam() {
417        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().prague_activated().build());
418        let mut header = valid_prague_header();
419        header.block_access_list_hash = Some(B256::ZERO);
420
421        assert!(matches!(
422            EthBeaconConsensus::new(chain_spec)
423                .validate_header(&SealedHeader::seal_slow(header,))
424                .unwrap_err(),
425            ConsensusError::BlockAccessListHashUnexpected
426        ));
427    }
428
429    #[test]
430    fn prague_header_allows_block_access_list_hash_before_amsterdam() {
431        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().prague_activated().build());
432        let mut header = valid_prague_header();
433        header.block_access_list_hash = Some(B256::ZERO);
434
435        assert!(EthBeaconConsensus::new(chain_spec)
436            .with_allow_bal_hashes(true)
437            .validate_header(&SealedHeader::seal_slow(header,))
438            .is_ok());
439    }
440
441    #[test]
442    fn prague_header_rejects_slot_number_before_amsterdam() {
443        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().prague_activated().build());
444        let mut header = valid_prague_header();
445        header.slot_number = Some(0);
446
447        assert!(matches!(
448            EthBeaconConsensus::new(chain_spec)
449                .validate_header(&SealedHeader::seal_slow(header,))
450                .unwrap_err(),
451            ConsensusError::SlotNumberUnexpected
452        ));
453    }
454
455    #[test]
456    fn prague_header_rejects_slot_number_with_allowed_bal_hashes_before_amsterdam() {
457        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().prague_activated().build());
458        let mut header = valid_prague_header();
459        header.block_access_list_hash = Some(B256::ZERO);
460        header.slot_number = Some(0);
461
462        assert!(matches!(
463            EthBeaconConsensus::new(chain_spec)
464                .with_allow_bal_hashes(true)
465                .validate_header(&SealedHeader::seal_slow(header,))
466                .unwrap_err(),
467            ConsensusError::SlotNumberUnexpected
468        ));
469    }
470
471    #[test]
472    fn prague_post_execution_allows_block_access_list_hash_before_amsterdam() {
473        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().prague_activated().build());
474        let expected_hash = B256::repeat_byte(0x42);
475        let block = prague_recovered_block_with_bal_hash(expected_hash);
476        let result = BlockExecutionResult::<Receipt>::default();
477        let consensus = EthBeaconConsensus::new(chain_spec).with_allow_bal_hashes(true);
478
479        assert!(FullConsensus::<EthPrimitives>::validate_block_post_execution(
480            &consensus,
481            &block,
482            &result,
483            None,
484            Some(expected_hash),
485        )
486        .is_ok());
487
488        assert!(FullConsensus::<EthPrimitives>::validate_block_post_execution(
489            &consensus, &block, &result, None, None,
490        )
491        .is_ok());
492
493        assert!(matches!(
494            FullConsensus::<EthPrimitives>::validate_block_post_execution(
495                &consensus,
496                &block,
497                &result,
498                None,
499                Some(B256::repeat_byte(0x24)),
500            )
501            .unwrap_err(),
502            ConsensusError::BlockAccessListHashMismatch(_)
503        ));
504    }
505
506    #[test]
507    fn amsterdam_header_requires_block_access_list_hash() {
508        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
509        let mut header = valid_prague_header();
510        header.slot_number = Some(0);
511
512        assert!(matches!(
513            EthBeaconConsensus::new(chain_spec)
514                .validate_header(&SealedHeader::seal_slow(header,))
515                .unwrap_err(),
516            ConsensusError::BlockAccessListHashMissing
517        ));
518    }
519
520    #[test]
521    fn amsterdam_header_requires_slot_number() {
522        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
523        let mut header = valid_prague_header();
524        header.block_access_list_hash = Some(B256::ZERO);
525
526        assert!(matches!(
527            EthBeaconConsensus::new(chain_spec)
528                .validate_header(&SealedHeader::seal_slow(header,))
529                .unwrap_err(),
530            ConsensusError::SlotNumberMissing
531        ));
532    }
533
534    #[test]
535    fn amsterdam_header_accepts_block_access_list_hash_and_slot_number() {
536        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
537        let mut header = valid_prague_header();
538        header.block_access_list_hash = Some(B256::ZERO);
539        header.slot_number = Some(0);
540
541        assert!(EthBeaconConsensus::new(chain_spec)
542            .validate_header(&SealedHeader::seal_slow(header,))
543            .is_ok());
544    }
545}