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