reth_payload_validator/
lib.rs

1//! Payload Validation support.
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_rpc_types::engine::{
12    ExecutionPayload, ExecutionPayloadSidecar, MaybeCancunPayloadFields, PayloadError,
13};
14use reth_chainspec::EthereumHardforks;
15use reth_primitives::{BlockExt, SealedBlock};
16use reth_rpc_types_compat::engine::payload::try_into_block;
17use std::sync::Arc;
18
19/// Execution payload validator.
20#[derive(Clone, Debug)]
21pub struct ExecutionPayloadValidator<ChainSpec> {
22    /// Chain spec to validate against.
23    chain_spec: Arc<ChainSpec>,
24}
25
26impl<ChainSpec> ExecutionPayloadValidator<ChainSpec> {
27    /// Create a new validator.
28    pub const fn new(chain_spec: Arc<ChainSpec>) -> Self {
29        Self { chain_spec }
30    }
31
32    /// Returns the chain spec used by the validator.
33    #[inline]
34    pub const fn chain_spec(&self) -> &Arc<ChainSpec> {
35        &self.chain_spec
36    }
37}
38
39impl<ChainSpec: EthereumHardforks> ExecutionPayloadValidator<ChainSpec> {
40    /// Returns true if the Cancun hardfork is active at the given timestamp.
41    #[inline]
42    fn is_cancun_active_at_timestamp(&self, timestamp: u64) -> bool {
43        self.chain_spec().is_cancun_active_at_timestamp(timestamp)
44    }
45
46    /// Returns true if the Shanghai hardfork is active at the given timestamp.
47    #[inline]
48    fn is_shanghai_active_at_timestamp(&self, timestamp: u64) -> bool {
49        self.chain_spec().is_shanghai_active_at_timestamp(timestamp)
50    }
51
52    /// Returns true if the Prague harkdfork is active at the given timestamp.
53    #[inline]
54    fn is_prague_active_at_timestamp(&self, timestamp: u64) -> bool {
55        self.chain_spec().is_prague_active_at_timestamp(timestamp)
56    }
57
58    /// Cancun specific checks for EIP-4844 blob transactions.
59    ///
60    /// Ensures that the number of blob versioned hashes matches the number hashes included in the
61    /// _separate_ `block_versioned_hashes` of the cancun payload fields.
62    fn ensure_matching_blob_versioned_hashes(
63        &self,
64        sealed_block: &SealedBlock,
65        cancun_fields: &MaybeCancunPayloadFields,
66    ) -> Result<(), PayloadError> {
67        let num_blob_versioned_hashes = sealed_block.blob_versioned_hashes_iter().count();
68        // Additional Cancun checks for blob transactions
69        if let Some(versioned_hashes) = cancun_fields.versioned_hashes() {
70            if num_blob_versioned_hashes != versioned_hashes.len() {
71                // Number of blob versioned hashes does not match
72                return Err(PayloadError::InvalidVersionedHashes)
73            }
74            // we can use `zip` safely here because we already compared their length
75            for (payload_versioned_hash, block_versioned_hash) in
76                versioned_hashes.iter().zip(sealed_block.blob_versioned_hashes_iter())
77            {
78                if payload_versioned_hash != block_versioned_hash {
79                    return Err(PayloadError::InvalidVersionedHashes)
80                }
81            }
82        } else {
83            // No Cancun fields, if block includes any blobs, this is an error
84            if num_blob_versioned_hashes > 0 {
85                return Err(PayloadError::InvalidVersionedHashes)
86            }
87        }
88
89        Ok(())
90    }
91
92    /// Ensures that the given payload does not violate any consensus rules that concern the block's
93    /// layout, like:
94    ///    - missing or invalid base fee
95    ///    - invalid extra data
96    ///    - invalid transactions
97    ///    - incorrect hash
98    ///    - the versioned hashes passed with the payload do not exactly match transaction versioned
99    ///      hashes
100    ///    - the block does not contain blob transactions if it is pre-cancun
101    ///
102    /// The checks are done in the order that conforms with the engine-API specification.
103    ///
104    /// This is intended to be invoked after receiving the payload from the CLI.
105    /// The additional [`MaybeCancunPayloadFields`] are not part of the payload, but are additional fields in the `engine_newPayloadV3` RPC call, See also <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#engine_newpayloadv3>
106    ///
107    /// If the cancun fields are provided this also validates that the versioned hashes in the block
108    /// match the versioned hashes passed in the
109    /// [`CancunPayloadFields`](alloy_rpc_types::engine::CancunPayloadFields), if the cancun payload
110    /// fields are provided. If the payload fields are not provided, but versioned hashes exist
111    /// in the block, this is considered an error: [`PayloadError::InvalidVersionedHashes`].
112    ///
113    /// This validates versioned hashes according to the Engine API Cancun spec:
114    /// <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#specification>
115    pub fn ensure_well_formed_payload(
116        &self,
117        payload: ExecutionPayload,
118        sidecar: ExecutionPayloadSidecar,
119    ) -> Result<SealedBlock, PayloadError> {
120        let expected_hash = payload.block_hash();
121
122        // First parse the block
123        let sealed_block = try_into_block(payload, &sidecar)?.seal_slow();
124
125        // Ensure the hash included in the payload matches the block hash
126        if expected_hash != sealed_block.hash() {
127            return Err(PayloadError::BlockHash {
128                execution: sealed_block.hash(),
129                consensus: expected_hash,
130            })
131        }
132
133        if self.is_cancun_active_at_timestamp(sealed_block.timestamp) {
134            if sealed_block.header.blob_gas_used.is_none() {
135                // cancun active but blob gas used not present
136                return Err(PayloadError::PostCancunBlockWithoutBlobGasUsed)
137            }
138            if sealed_block.header.excess_blob_gas.is_none() {
139                // cancun active but excess blob gas not present
140                return Err(PayloadError::PostCancunBlockWithoutExcessBlobGas)
141            }
142            if sidecar.cancun().is_none() {
143                // cancun active but cancun fields not present
144                return Err(PayloadError::PostCancunWithoutCancunFields)
145            }
146        } else {
147            if sealed_block.has_eip4844_transactions() {
148                // cancun not active but blob transactions present
149                return Err(PayloadError::PreCancunBlockWithBlobTransactions)
150            }
151            if sealed_block.header.blob_gas_used.is_some() {
152                // cancun not active but blob gas used present
153                return Err(PayloadError::PreCancunBlockWithBlobGasUsed)
154            }
155            if sealed_block.header.excess_blob_gas.is_some() {
156                // cancun not active but excess blob gas present
157                return Err(PayloadError::PreCancunBlockWithExcessBlobGas)
158            }
159            if sidecar.cancun().is_some() {
160                // cancun not active but cancun fields present
161                return Err(PayloadError::PreCancunWithCancunFields)
162            }
163        }
164
165        let shanghai_active = self.is_shanghai_active_at_timestamp(sealed_block.timestamp);
166        if !shanghai_active && sealed_block.body.withdrawals.is_some() {
167            // shanghai not active but withdrawals present
168            return Err(PayloadError::PreShanghaiBlockWithWithdrawals)
169        }
170
171        if !self.is_prague_active_at_timestamp(sealed_block.timestamp) &&
172            sealed_block.has_eip7702_transactions()
173        {
174            return Err(PayloadError::PrePragueBlockWithEip7702Transactions)
175        }
176
177        // EIP-4844 checks
178        self.ensure_matching_blob_versioned_hashes(
179            &sealed_block,
180            &sidecar.cancun().cloned().into(),
181        )?;
182
183        Ok(sealed_block)
184    }
185}