Skip to main content

reth_consensus/
lib.rs

1//! Consensus protocol functions
2//!
3//! # Trait hierarchy
4//!
5//! Consensus validation is split across three traits, each adding a layer:
6//!
7//! - [`HeaderValidator`] — validates a header in isolation and against its parent. Used early in
8//!   the validation pipeline before block execution.
9//!
10//! - [`Consensus`] — extends `HeaderValidator` with block body validation. Checks that the body
11//!   matches the header (tx root, ommer hash, withdrawals) and runs pre-execution checks. Used
12//!   before a block is executed.
13//!
14//! - [`FullConsensus`] — extends `Consensus` with post-execution validation. Checks execution
15//!   results against the header (gas used, receipt root, logs bloom). Used after block execution to
16//!   verify the outcome.
17//!
18//! In the engine, these are applied in order during payload validation (`engine_newPayload`).
19//! Payload attribute validation for block building (`engine_forkchoiceUpdated`) is handled
20//! separately at the engine API layer and does not use these traits.
21
22#![doc(
23    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
24    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
25    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
26)]
27#![cfg_attr(not(test), warn(unused_crate_dependencies))]
28#![cfg_attr(docsrs, feature(doc_cfg))]
29#![cfg_attr(not(feature = "std"), no_std)]
30
31extern crate alloc;
32
33use alloc::{
34    boxed::Box,
35    fmt::Debug,
36    string::{String, ToString},
37    sync::Arc,
38    vec::Vec,
39};
40use alloy_consensus::Header;
41use alloy_eip7928::BlockAccessListGasError;
42use alloy_primitives::{BlockHash, BlockNumber, Bloom, B256};
43use core::{error::Error, fmt::Display};
44
45/// Pre-computed receipt root and logs bloom.
46///
47/// When provided to [`FullConsensus::validate_block_post_execution`], this allows skipping
48/// the receipt root computation and using the pre-computed values instead.
49pub type ReceiptRootBloom = (B256, Bloom);
50
51/// Pre-computed transaction root.
52///
53/// When provided to [`Consensus::validate_block_pre_execution_with_tx_root`], this allows
54/// skipping transaction trie reconstruction from the block body.
55pub type TransactionRoot = B256;
56use reth_execution_types::BlockExecutionResult;
57use reth_primitives_traits::{
58    constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK, MINIMUM_GAS_LIMIT},
59    transaction::error::InvalidTransactionError,
60    Block, GotExpected, GotExpectedBoxed, NodePrimitives, RecoveredBlock, SealedBlock,
61    SealedHeader,
62};
63
64/// A consensus implementation that does nothing.
65pub mod noop;
66
67#[cfg(any(test, feature = "test-utils"))]
68/// test helpers for mocking consensus
69pub mod test_utils;
70
71/// [`Consensus`] implementation which knows full node primitives and is able to validation block's
72/// execution outcome.
73#[auto_impl::auto_impl(&, Arc)]
74pub trait FullConsensus<N: NodePrimitives>: Consensus<N::Block> {
75    /// Validate a block considering world state, i.e. things that can not be checked before
76    /// execution.
77    ///
78    /// See the Yellow Paper sections 4.3.2 "Holistic Validity".
79    ///
80    /// If `receipt_root_bloom` is provided, the implementation should use the pre-computed
81    /// receipt root and logs bloom instead of computing them from the receipts.
82    ///
83    /// Note: validating blocks does not include other validations of the Consensus
84    fn validate_block_post_execution(
85        &self,
86        block: &RecoveredBlock<N::Block>,
87        result: &BlockExecutionResult<N::Receipt>,
88        receipt_root_bloom: Option<ReceiptRootBloom>,
89        block_access_list_hash: Option<B256>,
90    ) -> Result<(), ConsensusError>;
91}
92
93/// Consensus is a protocol that chooses canonical chain.
94#[auto_impl::auto_impl(&, Arc)]
95pub trait Consensus<B: Block>: HeaderValidator<B::Header> {
96    /// Ensures that body field values match the header.
97    fn validate_body_against_header(
98        &self,
99        body: &B::Body,
100        header: &SealedHeader<B::Header>,
101    ) -> Result<(), ConsensusError>;
102
103    /// Validate a block disregarding world state, i.e. things that can be checked before sender
104    /// recovery and execution.
105    ///
106    /// See the Yellow Paper sections 4.4.2 "Holistic Validity", 4.4.4 "Block Header Validity".
107    /// Note: Ommer Validation (previously section 11.1) has been deprecated since the Paris hard
108    /// fork transition to proof of stake.
109    ///
110    /// **This should not be called for the genesis block**.
111    ///
112    /// Note: validating blocks does not include other validations of the Consensus
113    fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError>;
114
115    /// Returns `true` if the given consensus error is transient and may resolve on its own.
116    ///
117    /// On fast chains, clock skew between nodes can cause a valid block's timestamp to
118    /// appear briefly in the future. Caching such blocks as permanently invalid would
119    /// prevent them from being re-validated once the local clock catches up.
120    ///
121    /// Transient errors will not cause the block hash to be cached as permanently invalid,
122    /// allowing the block to be re-validated later.
123    fn is_transient_error(&self, _error: &ConsensusError) -> bool {
124        false
125    }
126
127    /// Validate a block disregarding world state using an optional pre-computed transaction root.
128    ///
129    /// If `transaction_root` is provided, the implementation should use the pre-computed
130    /// transaction root instead of recomputing it from the block body. The value must have been
131    /// derived from `block.body().calculate_tx_root()`.
132    ///
133    /// By default this falls back to [`Self::validate_block_pre_execution`].
134    fn validate_block_pre_execution_with_tx_root(
135        &self,
136        block: &SealedBlock<B>,
137        transaction_root: Option<TransactionRoot>,
138    ) -> Result<(), ConsensusError> {
139        let _ = transaction_root;
140        self.validate_block_pre_execution(block)
141    }
142}
143
144/// `HeaderValidator` is a protocol that validates headers and their relationships.
145#[auto_impl::auto_impl(&, Arc)]
146pub trait HeaderValidator<H = Header>: Debug + Send + Sync {
147    /// Validate if header is correct and follows consensus specification.
148    ///
149    /// This is called on standalone header to check if all hashes are correct.
150    fn validate_header(&self, header: &SealedHeader<H>) -> Result<(), ConsensusError>;
151
152    /// Validate that the header information regarding parent are correct.
153    /// This checks the block number, timestamp, basefee and gas limit increment.
154    ///
155    /// This is called before properties that are not in the header itself (like total difficulty)
156    /// have been computed.
157    ///
158    /// **This should not be called for the genesis block**.
159    ///
160    /// Note: Validating header against its parent does not include other `HeaderValidator`
161    /// validations.
162    fn validate_header_against_parent(
163        &self,
164        header: &SealedHeader<H>,
165        parent: &SealedHeader<H>,
166    ) -> Result<(), ConsensusError>;
167
168    /// Validates the given headers
169    ///
170    /// This ensures that the first header is valid on its own and all subsequent headers are valid
171    /// on its own and valid against its parent.
172    ///
173    /// Note: this expects that the headers are in natural order (ascending block number)
174    fn validate_header_range(
175        &self,
176        headers: &[SealedHeader<H>],
177    ) -> Result<(), HeaderConsensusError<H>>
178    where
179        H: Clone,
180    {
181        if let Some((initial_header, remaining_headers)) = headers.split_first() {
182            self.validate_header(initial_header)
183                .map_err(|e| HeaderConsensusError(e, initial_header.clone()))?;
184            let mut parent = initial_header;
185            for child in remaining_headers {
186                self.validate_header(child).map_err(|e| HeaderConsensusError(e, child.clone()))?;
187                self.validate_header_against_parent(child, parent)
188                    .map_err(|e| HeaderConsensusError(e, child.clone()))?;
189                parent = child;
190            }
191        }
192        Ok(())
193    }
194}
195
196/// Consensus Errors
197#[derive(Debug, Clone, thiserror::Error)]
198pub enum ConsensusError {
199    /// Error when the gas used in the header exceeds the gas limit.
200    #[error("block used gas ({gas_used}) is greater than gas limit ({gas_limit})")]
201    HeaderGasUsedExceedsGasLimit {
202        /// The gas used in the block header.
203        gas_used: u64,
204        /// The gas limit in the block header.
205        gas_limit: u64,
206    },
207    /// Error when the gas limit is more than the maximum allowed.
208    #[error(
209        "header gas limit ({gas_limit}) exceed the maximum allowed gas limit ({MAXIMUM_GAS_LIMIT_BLOCK})"
210    )]
211    HeaderGasLimitExceedsMax {
212        /// The gas limit in the block header.
213        gas_limit: u64,
214    },
215
216    /// Error when block gas used doesn't match expected value
217    #[error("block gas used mismatch: {gas}; gas spent by each transaction: {gas_spent_by_tx:?}")]
218    BlockGasUsed {
219        /// The gas diff.
220        gas: GotExpected<u64>,
221        /// Gas spent by each transaction
222        gas_spent_by_tx: Vec<(u64, u64)>,
223    },
224
225    /// Error when the hash of block ommer is different from the expected hash.
226    #[error("mismatched block ommer hash: {0}")]
227    BodyOmmersHashDiff(GotExpectedBoxed<B256>),
228
229    /// Error when the state root in the block is different from the expected state root.
230    #[error("mismatched block state root: {0}")]
231    BodyStateRootDiff(GotExpectedBoxed<B256>),
232
233    /// Error when the transaction root in the block is different from the expected transaction
234    /// root.
235    #[error("mismatched block transaction root: {0}")]
236    BodyTransactionRootDiff(GotExpectedBoxed<B256>),
237
238    /// Error when the receipt root in the block is different from the expected receipt root.
239    #[error("receipt root mismatch: {0}")]
240    BodyReceiptRootDiff(GotExpectedBoxed<B256>),
241
242    /// Error when header bloom filter is different from the expected bloom filter.
243    #[error("header bloom filter mismatch: {0}")]
244    BodyBloomLogDiff(GotExpectedBoxed<Bloom>),
245
246    /// Error when the withdrawals root in the block is different from the expected withdrawals
247    /// root.
248    #[error("mismatched block withdrawals root: {0}")]
249    BodyWithdrawalsRootDiff(GotExpectedBoxed<B256>),
250
251    /// Error when the requests hash in the block is different from the expected requests
252    /// hash.
253    #[error("mismatched block requests hash: {0}")]
254    BodyRequestsHashDiff(GotExpectedBoxed<B256>),
255
256    /// Error when a block with a specific hash and number is already known.
257    #[error("block with [hash={hash}, number={number}] is already known")]
258    BlockKnown {
259        /// The hash of the known block.
260        hash: BlockHash,
261        /// The block number of the known block.
262        number: BlockNumber,
263    },
264
265    /// Error when the parent hash of a block is not known.
266    #[error("block parent [hash={hash}] is not known")]
267    ParentUnknown {
268        /// The hash of the unknown parent block.
269        hash: BlockHash,
270    },
271
272    /// Error when the block number does not match the parent block number.
273    #[error(
274        "block number {block_number} does not match parent block number {parent_block_number}"
275    )]
276    ParentBlockNumberMismatch {
277        /// The parent block number.
278        parent_block_number: BlockNumber,
279        /// The block number.
280        block_number: BlockNumber,
281    },
282
283    /// Error when the parent hash does not match the expected parent hash.
284    #[error("mismatched parent hash: {0}")]
285    ParentHashMismatch(GotExpectedBoxed<B256>),
286
287    /// Error when the block timestamp is in the future compared to our clock time.
288    #[error(
289        "block timestamp {timestamp} is in the future compared to our clock time {present_timestamp}"
290    )]
291    TimestampIsInFuture {
292        /// The block's timestamp.
293        timestamp: u64,
294        /// The current timestamp.
295        present_timestamp: u64,
296    },
297
298    /// Error when the base fee is missing.
299    #[error("base fee missing")]
300    BaseFeeMissing,
301
302    /// Error when there is a transaction signer recovery error.
303    #[error("transaction signer recovery error")]
304    TransactionSignerRecoveryError,
305
306    /// Error when the extra data length exceeds the maximum allowed.
307    #[error("extra data {len} exceeds max length")]
308    ExtraDataExceedsMax {
309        /// The length of the extra data.
310        len: usize,
311    },
312
313    /// Error when the difficulty after a merge is not zero.
314    #[error("difficulty after merge is not zero")]
315    TheMergeDifficultyIsNotZero,
316
317    /// Error when the nonce after a merge is not zero.
318    #[error("nonce after merge is not zero")]
319    TheMergeNonceIsNotZero,
320
321    /// Error when the ommer root after a merge is not empty.
322    #[error("ommer root after merge is not empty")]
323    TheMergeOmmerRootIsNotEmpty,
324
325    /// Error when the withdrawals root is missing.
326    #[error("missing withdrawals root")]
327    WithdrawalsRootMissing,
328
329    /// Error when the requests hash is missing.
330    #[error("missing requests hash")]
331    RequestsHashMissing,
332
333    /// Error when an unexpected withdrawals root is encountered.
334    #[error("unexpected withdrawals root")]
335    WithdrawalsRootUnexpected,
336
337    /// Error when an unexpected requests hash is encountered.
338    #[error("unexpected requests hash")]
339    RequestsHashUnexpected,
340
341    /// Error when withdrawals are missing.
342    #[error("missing withdrawals")]
343    BodyWithdrawalsMissing,
344
345    /// Error when requests are missing.
346    #[error("missing requests")]
347    BodyRequestsMissing,
348
349    /// Error when blob gas used is missing.
350    #[error("missing blob gas used")]
351    BlobGasUsedMissing,
352
353    /// Error when unexpected blob gas used is encountered.
354    #[error("unexpected blob gas used")]
355    BlobGasUsedUnexpected,
356
357    /// Error when excess blob gas is missing.
358    #[error("missing excess blob gas")]
359    ExcessBlobGasMissing,
360
361    /// Error when unexpected excess blob gas is encountered.
362    #[error("unexpected excess blob gas")]
363    ExcessBlobGasUnexpected,
364
365    /// Error when the parent beacon block root is missing.
366    #[error("missing parent beacon block root")]
367    ParentBeaconBlockRootMissing,
368
369    /// Error when an unexpected parent beacon block root is encountered.
370    #[error("unexpected parent beacon block root")]
371    ParentBeaconBlockRootUnexpected,
372
373    /// Error when blob gas used exceeds the maximum allowed.
374    #[error("blob gas used {blob_gas_used} exceeds maximum allowance {max_blob_gas_per_block}")]
375    BlobGasUsedExceedsMaxBlobGasPerBlock {
376        /// The actual blob gas used.
377        blob_gas_used: u64,
378        /// The maximum allowed blob gas per block.
379        max_blob_gas_per_block: u64,
380    },
381
382    /// Error when blob gas used is not a multiple of blob gas per blob.
383    #[error(
384        "blob gas used {blob_gas_used} is not a multiple of blob gas per blob {blob_gas_per_blob}"
385    )]
386    BlobGasUsedNotMultipleOfBlobGasPerBlob {
387        /// The actual blob gas used.
388        blob_gas_used: u64,
389        /// The blob gas per blob.
390        blob_gas_per_blob: u64,
391    },
392
393    /// Error when the blob gas used in the header does not match the expected blob gas used.
394    #[error("blob gas used mismatch: {0}")]
395    BlobGasUsedDiff(GotExpected<u64>),
396
397    /// Error for a transaction that violates consensus.
398    #[error(transparent)]
399    InvalidTransaction(InvalidTransactionError),
400
401    /// Error when the block's base fee is different from the expected base fee.
402    #[error("block base fee mismatch: {0}")]
403    BaseFeeDiff(GotExpected<u64>),
404
405    /// Error when there is an invalid excess blob gas.
406    #[error(
407        "invalid excess blob gas: {diff}; \
408            parent excess blob gas: {parent_excess_blob_gas}, \
409            parent blob gas used: {parent_blob_gas_used}"
410    )]
411    ExcessBlobGasDiff {
412        /// The excess blob gas diff.
413        diff: GotExpected<u64>,
414        /// The parent excess blob gas.
415        parent_excess_blob_gas: u64,
416        /// The parent blob gas used.
417        parent_blob_gas_used: u64,
418    },
419
420    /// Error when the child gas limit exceeds the maximum allowed increase.
421    #[error("child gas_limit {child_gas_limit} exceeds the max allowed increase ({parent_gas_limit}/{GAS_LIMIT_BOUND_DIVISOR})")]
422    GasLimitInvalidIncrease {
423        /// The parent gas limit.
424        parent_gas_limit: u64,
425        /// The child gas limit.
426        child_gas_limit: u64,
427    },
428
429    /// Error indicating that the child gas limit is below the minimum allowed limit.
430    ///
431    /// This error occurs when the child gas limit is less than the specified minimum gas limit.
432    #[error(
433        "child gas limit {child_gas_limit} is below the minimum allowed limit ({MINIMUM_GAS_LIMIT})"
434    )]
435    GasLimitInvalidMinimum {
436        /// The child gas limit.
437        child_gas_limit: u64,
438    },
439
440    /// Error indicating that the block gas limit is above the allowed maximum.
441    ///
442    /// This error occurs when the gas limit is more than the specified maximum gas limit.
443    #[error("child gas limit {block_gas_limit} is above the maximum allowed limit ({MAXIMUM_GAS_LIMIT_BLOCK})")]
444    GasLimitInvalidBlockMaximum {
445        /// block gas limit.
446        block_gas_limit: u64,
447    },
448
449    /// Error when the child gas limit exceeds the maximum allowed decrease.
450    #[error("child gas_limit {child_gas_limit} is below the max allowed decrease ({parent_gas_limit}/{GAS_LIMIT_BOUND_DIVISOR})")]
451    GasLimitInvalidDecrease {
452        /// The parent gas limit.
453        parent_gas_limit: u64,
454        /// The child gas limit.
455        child_gas_limit: u64,
456    },
457
458    /// Error when the block timestamp is in the past compared to the parent timestamp.
459    #[error(
460        "block timestamp {timestamp} is in the past compared to the parent timestamp {parent_timestamp}"
461    )]
462    TimestampIsInPast {
463        /// The parent block's timestamp.
464        parent_timestamp: u64,
465        /// The block's timestamp.
466        timestamp: u64,
467    },
468    /// Error when the block is too large.
469    #[error("block is too large: {rlp_length} > {max_rlp_length}")]
470    BlockTooLarge {
471        /// The actual RLP length of the block.
472        rlp_length: usize,
473        /// The maximum allowed RLP length.
474        max_rlp_length: usize,
475    },
476    /// EIP-7825: Transaction gas limit exceeds maximum allowed
477    #[error(transparent)]
478    TransactionGasLimitTooHigh(Box<TxGasLimitTooHighErr>),
479    /// Error when an unexpected block access list cost is encountered.
480    #[error(transparent)]
481    BlockAccessListCostMoreThanGasLimit(Box<BlockAccessListGasError>),
482    /// Error when the block access list hash doesn't match the expected value.
483    #[error("block access list hash mismatch: {0}")]
484    BlockAccessListHashMismatch(GotExpectedBoxed<B256>),
485    /// Any additional consensus error, for example L2-specific errors.
486    #[error(transparent)]
487    Other(#[from] Arc<dyn Error + Send + Sync>),
488}
489
490impl ConsensusError {
491    /// Returns a new [`ConsensusError::Other`] instance with the given error.
492    pub fn other<E>(error: E) -> Self
493    where
494        E: Error + Send + Sync + 'static,
495    {
496        Self::Other(Arc::new(error))
497    }
498
499    /// Returns a new [`ConsensusError::Other`] instance with the given message.
500    pub fn msg(msg: impl Display) -> Self {
501        Self::other(MessageError(msg.to_string()))
502    }
503
504    /// Returns `true` if the error is a state root error.
505    pub const fn is_state_root_error(&self) -> bool {
506        matches!(self, Self::BodyStateRootDiff(_))
507    }
508
509    /// Returns the arbitrary error if it is [`ConsensusError::Other`].
510    pub fn as_other(&self) -> Option<&(dyn Error + Send + Sync + 'static)> {
511        match self {
512            Self::Other(err) => Some(err.as_ref()),
513            _ => None,
514        }
515    }
516
517    /// Returns a reference to the [`ConsensusError::Other`] value if it is of that type.
518    /// Returns `None` otherwise.
519    pub fn downcast_other_ref<T: Error + 'static>(&self) -> Option<&T> {
520        let other = self.as_other()?;
521        other.downcast_ref()
522    }
523
524    /// Returns `true` if this type is a [`ConsensusError::Other`] of that error type.
525    pub fn is_other<T: Error + 'static>(&self) -> bool {
526        self.as_other().map(|err| err.is::<T>()).unwrap_or(false)
527    }
528}
529
530impl From<InvalidTransactionError> for ConsensusError {
531    fn from(value: InvalidTransactionError) -> Self {
532        Self::InvalidTransaction(value)
533    }
534}
535
536impl From<TxGasLimitTooHighErr> for ConsensusError {
537    fn from(value: TxGasLimitTooHighErr) -> Self {
538        Self::TransactionGasLimitTooHigh(Box::new(value))
539    }
540}
541
542impl From<BlockAccessListGasError> for ConsensusError {
543    fn from(value: BlockAccessListGasError) -> Self {
544        Self::BlockAccessListCostMoreThanGasLimit(Box::new(value))
545    }
546}
547
548/// `HeaderConsensusError` combines a `ConsensusError` with the `SealedHeader` it relates to.
549#[derive(thiserror::Error, Debug)]
550#[error("Consensus error: {0}, Invalid header: {1:?}")]
551pub struct HeaderConsensusError<H>(ConsensusError, SealedHeader<H>);
552
553/// EIP-7825: Transaction gas limit exceeds maximum allowed
554#[derive(thiserror::Error, Debug, Eq, PartialEq, Clone)]
555#[error("transaction gas limit ({gas_limit}) is greater than the cap ({max_allowed})")]
556pub struct TxGasLimitTooHighErr {
557    /// Hash of the transaction that violates the rule
558    pub tx_hash: B256,
559    /// The gas limit of the transaction
560    pub gas_limit: u64,
561    /// The maximum allowed gas limit
562    pub max_allowed: u64,
563}
564
565#[derive(Debug, thiserror::Error)]
566#[error("{0}")]
567struct MessageError(String);
568
569#[cfg(test)]
570mod tests {
571    use super::*;
572
573    #[derive(thiserror::Error, Debug)]
574    #[error("Custom L2 consensus error")]
575    struct CustomL2Error;
576
577    #[test]
578    fn test_other_error_conversion() {
579        let consensus_err = ConsensusError::other(CustomL2Error);
580        assert!(matches!(consensus_err, ConsensusError::Other(_)));
581    }
582
583    #[test]
584    fn test_other_error_display() {
585        let consensus_err = ConsensusError::other(CustomL2Error);
586        let error_message = format!("{}", consensus_err);
587        assert_eq!(error_message, "Custom L2 consensus error");
588    }
589
590    #[test]
591    fn test_other_error_downcast() {
592        let consensus_err = ConsensusError::other(CustomL2Error);
593
594        assert!(consensus_err.is_other::<CustomL2Error>());
595        assert!(consensus_err.downcast_other_ref::<CustomL2Error>().is_some());
596    }
597
598    #[test]
599    fn test_other_msg() {
600        let consensus_err = ConsensusError::msg("consensus message");
601
602        assert_eq!(consensus_err.to_string(), "consensus message");
603        assert!(consensus_err.downcast_other_ref::<MessageError>().is_some());
604    }
605}