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/SeismicSystems/seismic-reth/issues/"
7)]
8#![cfg_attr(not(test), warn(unused_crate_dependencies))]
9#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
10
11use alloy_consensus::{BlockHeader, EMPTY_OMMER_ROOT_HASH};
12use alloy_eips::merge::ALLOWED_FUTURE_BLOCK_TIME_SECONDS;
13use alloy_primitives::U256;
14use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks};
15use reth_consensus::{
16    Consensus, ConsensusError, FullConsensus, HeaderValidator, PostExecutionInput,
17};
18use reth_consensus_common::validation::{
19    validate_4844_header_standalone, validate_against_parent_4844,
20    validate_against_parent_eip1559_base_fee, validate_against_parent_hash_number,
21    validate_against_parent_timestamp, validate_block_pre_execution, validate_body_against_header,
22    validate_header_base_fee, validate_header_extradata, validate_header_gas,
23};
24use reth_primitives::{BlockWithSenders, NodePrimitives, Receipt, SealedBlock, SealedHeader};
25use reth_primitives_traits::{constants::MINIMUM_GAS_LIMIT, BlockBody};
26use std::{fmt::Debug, sync::Arc, time::SystemTime};
27
28/// The bound divisor of the gas limit, used in update calculations.
29pub const GAS_LIMIT_BOUND_DIVISOR: u64 = 1024;
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}
42
43impl<ChainSpec: EthChainSpec + EthereumHardforks> EthBeaconConsensus<ChainSpec> {
44    /// Create a new instance of [`EthBeaconConsensus`]
45    pub const fn new(chain_spec: Arc<ChainSpec>) -> Self {
46        Self { chain_spec }
47    }
48
49    /// Checks the gas limit for consistency between parent and self headers.
50    ///
51    /// The maximum allowable difference between self and parent gas limits is determined by the
52    /// parent's gas limit divided by the [`GAS_LIMIT_BOUND_DIVISOR`].
53    fn validate_against_parent_gas_limit<H: BlockHeader>(
54        &self,
55        header: &SealedHeader<H>,
56        parent: &SealedHeader<H>,
57    ) -> Result<(), ConsensusError> {
58        // Determine the parent gas limit, considering elasticity multiplier on the London fork.
59        let parent_gas_limit =
60            if self.chain_spec.fork(EthereumHardfork::London).transitions_at_block(header.number())
61            {
62                parent.gas_limit() *
63                    self.chain_spec
64                        .base_fee_params_at_timestamp(header.timestamp())
65                        .elasticity_multiplier as u64
66            } else {
67                parent.gas_limit()
68            };
69
70        // Check for an increase in gas limit beyond the allowed threshold.
71        if header.gas_limit() > parent_gas_limit {
72            if header.gas_limit() - parent_gas_limit >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
73                return Err(ConsensusError::GasLimitInvalidIncrease {
74                    parent_gas_limit,
75                    child_gas_limit: header.gas_limit(),
76                })
77            }
78        }
79        // Check for a decrease in gas limit beyond the allowed threshold.
80        else if parent_gas_limit - header.gas_limit() >=
81            parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR
82        {
83            return Err(ConsensusError::GasLimitInvalidDecrease {
84                parent_gas_limit,
85                child_gas_limit: header.gas_limit(),
86            })
87        }
88        // Check if the self gas limit is below the minimum required limit.
89        else if header.gas_limit() < MINIMUM_GAS_LIMIT {
90            return Err(ConsensusError::GasLimitInvalidMinimum {
91                child_gas_limit: header.gas_limit(),
92            })
93        }
94
95        Ok(())
96    }
97}
98
99impl<ChainSpec, N> FullConsensus<N> for EthBeaconConsensus<ChainSpec>
100where
101    ChainSpec: Send + Sync + EthChainSpec + EthereumHardforks + Debug,
102    N: NodePrimitives<Receipt = Receipt>,
103{
104    fn validate_block_post_execution(
105        &self,
106        block: &BlockWithSenders<N::Block>,
107        input: PostExecutionInput<'_>,
108    ) -> Result<(), ConsensusError> {
109        validate_block_post_execution(block, &self.chain_spec, input.receipts, input.requests)
110    }
111}
112
113impl<H, B, ChainSpec: Send + Sync + EthChainSpec + EthereumHardforks + Debug> Consensus<H, B>
114    for EthBeaconConsensus<ChainSpec>
115where
116    H: BlockHeader,
117    B: BlockBody,
118{
119    fn validate_body_against_header(
120        &self,
121        body: &B,
122        header: &SealedHeader<H>,
123    ) -> Result<(), ConsensusError> {
124        validate_body_against_header(body, header.header())
125    }
126
127    fn validate_block_pre_execution(
128        &self,
129        block: &SealedBlock<H, B>,
130    ) -> Result<(), ConsensusError> {
131        validate_block_pre_execution(block, &self.chain_spec)
132    }
133}
134
135impl<H, ChainSpec: Send + Sync + EthChainSpec + EthereumHardforks + Debug> HeaderValidator<H>
136    for EthBeaconConsensus<ChainSpec>
137where
138    H: BlockHeader,
139{
140    fn validate_header(&self, header: &SealedHeader<H>) -> Result<(), ConsensusError> {
141        validate_header_gas(header.header())?;
142        validate_header_base_fee(header.header(), &self.chain_spec)?;
143
144        // EIP-4895: Beacon chain push withdrawals as operations
145        if self.chain_spec.is_shanghai_active_at_timestamp(header.timestamp()) &&
146            header.withdrawals_root().is_none()
147        {
148            return Err(ConsensusError::WithdrawalsRootMissing)
149        } else if !self.chain_spec.is_shanghai_active_at_timestamp(header.timestamp()) &&
150            header.withdrawals_root().is_some()
151        {
152            return Err(ConsensusError::WithdrawalsRootUnexpected)
153        }
154
155        // Ensures that EIP-4844 fields are valid once cancun is active.
156        if self.chain_spec.is_cancun_active_at_timestamp(header.timestamp()) {
157            validate_4844_header_standalone(header.header())?;
158        } else if header.blob_gas_used().is_some() {
159            return Err(ConsensusError::BlobGasUsedUnexpected)
160        } else if header.excess_blob_gas().is_some() {
161            return Err(ConsensusError::ExcessBlobGasUnexpected)
162        } else if header.parent_beacon_block_root().is_some() {
163            return Err(ConsensusError::ParentBeaconBlockRootUnexpected)
164        }
165
166        if self.chain_spec.is_prague_active_at_timestamp(header.timestamp()) {
167            if header.requests_hash().is_none() {
168                return Err(ConsensusError::RequestsHashMissing)
169            }
170        } else if header.requests_hash().is_some() {
171            return Err(ConsensusError::RequestsHashUnexpected)
172        }
173
174        Ok(())
175    }
176
177    fn validate_header_against_parent(
178        &self,
179        header: &SealedHeader<H>,
180        parent: &SealedHeader<H>,
181    ) -> Result<(), ConsensusError> {
182        validate_against_parent_hash_number(header.header(), parent)?;
183
184        validate_against_parent_timestamp(header.header(), parent.header())?;
185
186        // TODO Check difficulty increment between parent and self
187        // Ace age did increment it by some formula that we need to follow.
188        self.validate_against_parent_gas_limit(header, parent)?;
189
190        validate_against_parent_eip1559_base_fee(
191            header.header(),
192            parent.header(),
193            &self.chain_spec,
194        )?;
195
196        // ensure that the blob gas fields for this block
197        if self.chain_spec.is_cancun_active_at_timestamp(header.timestamp()) {
198            validate_against_parent_4844(header.header(), parent.header())?;
199        }
200
201        Ok(())
202    }
203
204    fn validate_header_with_total_difficulty(
205        &self,
206        header: &H,
207        total_difficulty: U256,
208    ) -> Result<(), ConsensusError> {
209        let is_post_merge = self
210            .chain_spec
211            .fork(EthereumHardfork::Paris)
212            .active_at_ttd(total_difficulty, header.difficulty());
213
214        if is_post_merge {
215            // TODO: add `is_zero_difficulty` to `alloy_consensus::BlockHeader` trait
216            if !header.difficulty().is_zero() {
217                return Err(ConsensusError::TheMergeDifficultyIsNotZero)
218            }
219
220            // TODO: helper fn in `alloy_consensus::BlockHeader` trait
221            if !header.nonce().is_some_and(|nonce| nonce.is_zero()) {
222                return Err(ConsensusError::TheMergeNonceIsNotZero)
223            }
224
225            if header.ommers_hash() != EMPTY_OMMER_ROOT_HASH {
226                return Err(ConsensusError::TheMergeOmmerRootIsNotEmpty)
227            }
228
229            // Post-merge, the consensus layer is expected to perform checks such that the block
230            // timestamp is a function of the slot. This is different from pre-merge, where blocks
231            // are only allowed to be in the future (compared to the system's clock) by a certain
232            // threshold.
233            //
234            // Block validation with respect to the parent should ensure that the block timestamp
235            // is greater than its parent timestamp.
236
237            // validate header extradata for all networks post merge
238            validate_header_extradata(header)?;
239
240            // mixHash is used instead of difficulty inside EVM
241            // https://eips.ethereum.org/EIPS/eip-4399#using-mixhash-field-instead-of-difficulty
242        } else {
243            // TODO Consensus checks for old blocks:
244            //  * difficulty, mix_hash & nonce aka PoW stuff
245            // low priority as syncing is done in reverse order
246
247            // Check if timestamp is in the future. Clock can drift but this can be consensus issue.
248            let present_timestamp =
249                SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
250
251            // TODO: move this to `alloy_consensus::BlockHeader`
252            if header.timestamp() > present_timestamp + ALLOWED_FUTURE_BLOCK_TIME_SECONDS {
253                return Err(ConsensusError::TimestampIsInFuture {
254                    timestamp: header.timestamp(),
255                    present_timestamp,
256                })
257            }
258
259            validate_header_extradata(header)?;
260        }
261
262        Ok(())
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use alloy_primitives::B256;
270    use reth_chainspec::{ChainSpec, ChainSpecBuilder};
271    use reth_primitives::proofs;
272
273    fn header_with_gas_limit(gas_limit: u64) -> SealedHeader {
274        let header = reth_primitives::Header { gas_limit, ..Default::default() };
275        SealedHeader::new(header, B256::ZERO)
276    }
277
278    #[test]
279    fn test_valid_gas_limit_increase() {
280        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
281        let child = header_with_gas_limit((parent.gas_limit + 5) as u64);
282
283        assert_eq!(
284            EthBeaconConsensus::new(Arc::new(ChainSpec::default()))
285                .validate_against_parent_gas_limit(&child, &parent),
286            Ok(())
287        );
288    }
289
290    #[test]
291    fn test_gas_limit_below_minimum() {
292        let parent = header_with_gas_limit(MINIMUM_GAS_LIMIT);
293        let child = header_with_gas_limit(MINIMUM_GAS_LIMIT - 1);
294
295        assert_eq!(
296            EthBeaconConsensus::new(Arc::new(ChainSpec::default()))
297                .validate_against_parent_gas_limit(&child, &parent),
298            Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: child.gas_limit as u64 })
299        );
300    }
301
302    #[test]
303    fn test_invalid_gas_limit_increase_exceeding_limit() {
304        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
305        let child = header_with_gas_limit(
306            parent.gas_limit + parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR + 1,
307        );
308
309        assert_eq!(
310            EthBeaconConsensus::new(Arc::new(ChainSpec::default()))
311                .validate_against_parent_gas_limit(&child, &parent),
312            Err(ConsensusError::GasLimitInvalidIncrease {
313                parent_gas_limit: parent.gas_limit,
314                child_gas_limit: child.gas_limit,
315            })
316        );
317    }
318
319    #[test]
320    fn test_valid_gas_limit_decrease_within_limit() {
321        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
322        let child = header_with_gas_limit(parent.gas_limit - 5);
323
324        assert_eq!(
325            EthBeaconConsensus::new(Arc::new(ChainSpec::default()))
326                .validate_against_parent_gas_limit(&child, &parent),
327            Ok(())
328        );
329    }
330
331    #[test]
332    fn test_invalid_gas_limit_decrease_exceeding_limit() {
333        let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
334        let child = header_with_gas_limit(
335            parent.gas_limit - parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR - 1,
336        );
337
338        assert_eq!(
339            EthBeaconConsensus::new(Arc::new(ChainSpec::default()))
340                .validate_against_parent_gas_limit(&child, &parent),
341            Err(ConsensusError::GasLimitInvalidDecrease {
342                parent_gas_limit: parent.gas_limit,
343                child_gas_limit: child.gas_limit,
344            })
345        );
346    }
347
348    #[test]
349    fn shanghai_block_zero_withdrawals() {
350        // ensures that if shanghai is activated, and we include a block with a withdrawals root,
351        // that the header is valid
352        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().shanghai_activated().build());
353
354        let header = reth_primitives::Header {
355            base_fee_per_gas: Some(1337),
356            withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
357            ..Default::default()
358        };
359
360        assert_eq!(
361            EthBeaconConsensus::new(chain_spec).validate_header(&SealedHeader::seal(header,)),
362            Ok(())
363        );
364    }
365}