reth_rpc/
validation.rs

1use alloy_consensus::{
2    BlobTransactionValidationError, BlockHeader, EnvKzgSettings, Transaction, TxReceipt,
3};
4use alloy_eips::{eip4844::kzg_to_versioned_hash, eip7685::RequestsOrHash};
5use alloy_rpc_types_beacon::relay::{
6    BidTrace, BuilderBlockValidationRequest, BuilderBlockValidationRequestV2,
7    BuilderBlockValidationRequestV3, BuilderBlockValidationRequestV4,
8};
9use alloy_rpc_types_engine::{
10    BlobsBundleV1, CancunPayloadFields, ExecutionPayload, ExecutionPayloadSidecar, PayloadError,
11    PraguePayloadFields,
12};
13use async_trait::async_trait;
14use jsonrpsee::core::RpcResult;
15use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
16use reth_consensus::{Consensus, FullConsensus, PostExecutionInput};
17use reth_engine_primitives::PayloadValidator;
18use reth_errors::{BlockExecutionError, ConsensusError, ProviderError};
19use reth_evm::execute::{BlockExecutorProvider, Executor};
20use reth_primitives::{GotExpected, NodePrimitives, SealedBlockWithSenders, SealedHeader};
21use reth_primitives_traits::{constants::GAS_LIMIT_BOUND_DIVISOR, Block as _, BlockBody};
22use reth_provider::{
23    BlockExecutionInput, BlockExecutionOutput, BlockReaderIdExt, StateProviderFactory,
24};
25use reth_revm::{cached::CachedReads, database::StateProviderDatabase};
26use reth_rpc_api::BlockSubmissionValidationApiServer;
27use reth_rpc_server_types::result::internal_rpc_err;
28use reth_tasks::TaskSpawner;
29use revm_primitives::{Address, B256, U256};
30use serde::{Deserialize, Serialize};
31use std::{collections::HashSet, sync::Arc};
32use tokio::sync::{oneshot, RwLock};
33
34/// The type that implements the `validation` rpc namespace trait
35#[derive(Clone, Debug, derive_more::Deref)]
36pub struct ValidationApi<Provider, E: BlockExecutorProvider> {
37    #[deref]
38    inner: Arc<ValidationApiInner<Provider, E>>,
39}
40
41impl<Provider, E> ValidationApi<Provider, E>
42where
43    E: BlockExecutorProvider,
44{
45    /// Create a new instance of the [`ValidationApi`]
46    pub fn new(
47        provider: Provider,
48        consensus: Arc<dyn FullConsensus<E::Primitives>>,
49        executor_provider: E,
50        config: ValidationApiConfig,
51        task_spawner: Box<dyn TaskSpawner>,
52        payload_validator: Arc<
53            dyn PayloadValidator<Block = <E::Primitives as NodePrimitives>::Block>,
54        >,
55    ) -> Self {
56        let ValidationApiConfig { disallow } = config;
57
58        let inner = Arc::new(ValidationApiInner {
59            provider,
60            consensus,
61            payload_validator,
62            executor_provider,
63            disallow,
64            cached_state: Default::default(),
65            task_spawner,
66        });
67
68        Self { inner }
69    }
70
71    /// Returns the cached reads for the given head hash.
72    async fn cached_reads(&self, head: B256) -> CachedReads {
73        let cache = self.inner.cached_state.read().await;
74        if cache.0 == head {
75            cache.1.clone()
76        } else {
77            Default::default()
78        }
79    }
80
81    /// Updates the cached state for the given head hash.
82    async fn update_cached_reads(&self, head: B256, cached_state: CachedReads) {
83        let mut cache = self.inner.cached_state.write().await;
84        if cache.0 == head {
85            cache.1.extend(cached_state);
86        } else {
87            *cache = (head, cached_state)
88        }
89    }
90}
91
92impl<Provider, E> ValidationApi<Provider, E>
93where
94    Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
95        + ChainSpecProvider<ChainSpec: EthereumHardforks>
96        + StateProviderFactory
97        + 'static,
98    E: BlockExecutorProvider,
99{
100    /// Validates the given block and a [`BidTrace`] against it.
101    pub async fn validate_message_against_block(
102        &self,
103        block: SealedBlockWithSenders<<E::Primitives as NodePrimitives>::Block>,
104        message: BidTrace,
105        registered_gas_limit: u64,
106    ) -> Result<(), ValidationApiError> {
107        self.validate_message_against_header(&block.header, &message)?;
108
109        self.consensus.validate_header_with_total_difficulty(&block.header, U256::MAX)?;
110        self.consensus.validate_header(&block.header)?;
111        self.consensus.validate_block_pre_execution(&block)?;
112
113        if !self.disallow.is_empty() {
114            if self.disallow.contains(&block.beneficiary()) {
115                return Err(ValidationApiError::Blacklist(block.beneficiary()))
116            }
117            if self.disallow.contains(&message.proposer_fee_recipient) {
118                return Err(ValidationApiError::Blacklist(message.proposer_fee_recipient))
119            }
120            for (sender, tx) in block.senders.iter().zip(block.transactions()) {
121                if self.disallow.contains(sender) {
122                    return Err(ValidationApiError::Blacklist(*sender))
123                }
124                if let Some(to) = tx.to() {
125                    if self.disallow.contains(&to) {
126                        return Err(ValidationApiError::Blacklist(to))
127                    }
128                }
129            }
130        }
131
132        let latest_header =
133            self.provider.latest_header()?.ok_or_else(|| ValidationApiError::MissingLatestBlock)?;
134
135        if latest_header.hash() != block.header.parent_hash() {
136            return Err(ConsensusError::ParentHashMismatch(
137                GotExpected { got: block.header.parent_hash(), expected: latest_header.hash() }
138                    .into(),
139            )
140            .into())
141        }
142        self.consensus.validate_header_against_parent(&block.header, &latest_header)?;
143        self.validate_gas_limit(registered_gas_limit, &latest_header, &block.header)?;
144
145        let latest_header_hash = latest_header.hash();
146        let state_provider = self.provider.state_by_block_hash(latest_header_hash)?;
147
148        let mut request_cache = self.cached_reads(latest_header_hash).await;
149
150        let cached_db = request_cache.as_db_mut(StateProviderDatabase::new(&state_provider));
151        let executor = self.executor_provider.executor(cached_db);
152
153        let block = block.unseal();
154        let mut accessed_blacklisted = None;
155        let output = executor.execute_with_state_closure(
156            BlockExecutionInput::new(&block, U256::MAX),
157            |state| {
158                if !self.disallow.is_empty() {
159                    for account in state.cache.accounts.keys() {
160                        if self.disallow.contains(account) {
161                            accessed_blacklisted = Some(*account);
162                        }
163                    }
164                }
165            },
166        )?;
167
168        // update the cached reads
169        self.update_cached_reads(latest_header_hash, request_cache).await;
170
171        if let Some(account) = accessed_blacklisted {
172            return Err(ValidationApiError::Blacklist(account))
173        }
174
175        self.consensus.validate_block_post_execution(
176            &block,
177            PostExecutionInput::new(&output.receipts, &output.requests),
178        )?;
179
180        self.ensure_payment(&block, &output, &message)?;
181
182        let state_root =
183            state_provider.state_root(state_provider.hashed_post_state(&output.state))?;
184
185        if state_root != block.header().state_root() {
186            return Err(ConsensusError::BodyStateRootDiff(
187                GotExpected { got: state_root, expected: block.header().state_root() }.into(),
188            )
189            .into())
190        }
191
192        Ok(())
193    }
194
195    /// Ensures that fields of [`BidTrace`] match the fields of the [`SealedHeader`].
196    fn validate_message_against_header(
197        &self,
198        header: &SealedHeader<<E::Primitives as NodePrimitives>::BlockHeader>,
199        message: &BidTrace,
200    ) -> Result<(), ValidationApiError> {
201        if header.hash() != message.block_hash {
202            Err(ValidationApiError::BlockHashMismatch(GotExpected {
203                got: message.block_hash,
204                expected: header.hash(),
205            }))
206        } else if header.parent_hash() != message.parent_hash {
207            Err(ValidationApiError::ParentHashMismatch(GotExpected {
208                got: message.parent_hash,
209                expected: header.parent_hash(),
210            }))
211        } else if header.gas_limit() != message.gas_limit {
212            Err(ValidationApiError::GasLimitMismatch(GotExpected {
213                got: message.gas_limit,
214                expected: header.gas_limit(),
215            }))
216        } else if header.gas_used() != message.gas_used {
217            return Err(ValidationApiError::GasUsedMismatch(GotExpected {
218                got: message.gas_used,
219                expected: header.gas_used(),
220            }))
221        } else {
222            Ok(())
223        }
224    }
225
226    /// Ensures that the chosen gas limit is the closest possible value for the validator's
227    /// registered gas limit.
228    ///
229    /// Ref: <https://github.com/flashbots/builder/blob/a742641e24df68bc2fc476199b012b0abce40ffe/core/blockchain.go#L2474-L2477>
230    fn validate_gas_limit(
231        &self,
232        registered_gas_limit: u64,
233        parent_header: &SealedHeader<<E::Primitives as NodePrimitives>::BlockHeader>,
234        header: &SealedHeader<<E::Primitives as NodePrimitives>::BlockHeader>,
235    ) -> Result<(), ValidationApiError> {
236        let max_gas_limit =
237            parent_header.gas_limit() + parent_header.gas_limit() / GAS_LIMIT_BOUND_DIVISOR - 1;
238        let min_gas_limit =
239            parent_header.gas_limit() - parent_header.gas_limit() / GAS_LIMIT_BOUND_DIVISOR + 1;
240
241        let best_gas_limit =
242            std::cmp::max(min_gas_limit, std::cmp::min(max_gas_limit, registered_gas_limit));
243
244        if best_gas_limit != header.gas_limit() {
245            return Err(ValidationApiError::GasLimitMismatch(GotExpected {
246                got: header.gas_limit(),
247                expected: best_gas_limit,
248            }))
249        }
250
251        Ok(())
252    }
253
254    /// Ensures that the proposer has received [`BidTrace::value`] for this block.
255    ///
256    /// Firstly attempts to verify the payment by checking the state changes, otherwise falls back
257    /// to checking the latest block transaction.
258    fn ensure_payment(
259        &self,
260        block: &<E::Primitives as NodePrimitives>::Block,
261        output: &BlockExecutionOutput<<E::Primitives as NodePrimitives>::Receipt>,
262        message: &BidTrace,
263    ) -> Result<(), ValidationApiError> {
264        let (mut balance_before, balance_after) = if let Some(acc) =
265            output.state.state.get(&message.proposer_fee_recipient)
266        {
267            let balance_before = acc.original_info.as_ref().map(|i| i.balance).unwrap_or_default();
268            let balance_after = acc.info.as_ref().map(|i| i.balance).unwrap_or_default();
269
270            (balance_before, balance_after)
271        } else {
272            // account might have balance but considering it zero is fine as long as we know
273            // that balance have not changed
274            (U256::ZERO, U256::ZERO)
275        };
276
277        if let Some(withdrawals) = block.body().withdrawals() {
278            for withdrawal in withdrawals {
279                if withdrawal.address == message.proposer_fee_recipient {
280                    balance_before += withdrawal.amount_wei();
281                }
282            }
283        }
284
285        if balance_after >= balance_before + message.value {
286            return Ok(())
287        }
288
289        let (receipt, tx) = output
290            .receipts
291            .last()
292            .zip(block.body().transactions().last())
293            .ok_or(ValidationApiError::ProposerPayment)?;
294
295        if !receipt.status() {
296            return Err(ValidationApiError::ProposerPayment)
297        }
298
299        if tx.to() != Some(message.proposer_fee_recipient) {
300            return Err(ValidationApiError::ProposerPayment)
301        }
302
303        if tx.value() != message.value {
304            return Err(ValidationApiError::ProposerPayment)
305        }
306
307        if !tx.input().is_empty() {
308            return Err(ValidationApiError::ProposerPayment)
309        }
310
311        if let Some(block_base_fee) = block.header().base_fee_per_gas() {
312            if tx.effective_tip_per_gas(block_base_fee).unwrap_or_default() != 0 {
313                return Err(ValidationApiError::ProposerPayment)
314            }
315        }
316
317        Ok(())
318    }
319
320    /// Validates the given [`BlobsBundleV1`] and returns versioned hashes for blobs.
321    pub fn validate_blobs_bundle(
322        &self,
323        mut blobs_bundle: BlobsBundleV1,
324    ) -> Result<Vec<B256>, ValidationApiError> {
325        if blobs_bundle.commitments.len() != blobs_bundle.proofs.len() ||
326            blobs_bundle.commitments.len() != blobs_bundle.blobs.len()
327        {
328            return Err(ValidationApiError::InvalidBlobsBundle)
329        }
330
331        let versioned_hashes = blobs_bundle
332            .commitments
333            .iter()
334            .map(|c| kzg_to_versioned_hash(c.as_slice()))
335            .collect::<Vec<_>>();
336
337        let sidecar = blobs_bundle.pop_sidecar(blobs_bundle.blobs.len());
338
339        sidecar.validate(&versioned_hashes, EnvKzgSettings::default().get())?;
340
341        Ok(versioned_hashes)
342    }
343
344    /// Core logic for validating the builder submission v3
345    async fn validate_builder_submission_v3(
346        &self,
347        request: BuilderBlockValidationRequestV3,
348    ) -> Result<(), ValidationApiError> {
349        let block = self
350            .payload_validator
351            .ensure_well_formed_payload(
352                ExecutionPayload::V3(request.request.execution_payload),
353                ExecutionPayloadSidecar::v3(CancunPayloadFields {
354                    parent_beacon_block_root: request.parent_beacon_block_root,
355                    versioned_hashes: self.validate_blobs_bundle(request.request.blobs_bundle)?,
356                }),
357            )?
358            .try_seal_with_senders()
359            .map_err(|_| ValidationApiError::InvalidTransactionSignature)?;
360
361        self.validate_message_against_block(
362            block,
363            request.request.message,
364            request.registered_gas_limit,
365        )
366        .await
367    }
368
369    /// Core logic for validating the builder submission v4
370    async fn validate_builder_submission_v4(
371        &self,
372        request: BuilderBlockValidationRequestV4,
373    ) -> Result<(), ValidationApiError> {
374        let block = self
375            .payload_validator
376            .ensure_well_formed_payload(
377                ExecutionPayload::V3(request.request.execution_payload),
378                ExecutionPayloadSidecar::v4(
379                    CancunPayloadFields {
380                        parent_beacon_block_root: request.parent_beacon_block_root,
381                        versioned_hashes: self
382                            .validate_blobs_bundle(request.request.blobs_bundle)?,
383                    },
384                    PraguePayloadFields {
385                        requests: RequestsOrHash::Requests(
386                            request.request.execution_requests.into(),
387                        ),
388                        target_blobs_per_block: request.request.target_blobs_per_block,
389                    },
390                ),
391            )?
392            .try_seal_with_senders()
393            .map_err(|_| ValidationApiError::InvalidTransactionSignature)?;
394
395        self.validate_message_against_block(
396            block,
397            request.request.message,
398            request.registered_gas_limit,
399        )
400        .await
401    }
402}
403
404#[async_trait]
405impl<Provider, E> BlockSubmissionValidationApiServer for ValidationApi<Provider, E>
406where
407    Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
408        + ChainSpecProvider<ChainSpec: EthereumHardforks>
409        + StateProviderFactory
410        + Clone
411        + 'static,
412    E: BlockExecutorProvider,
413{
414    async fn validate_builder_submission_v1(
415        &self,
416        _request: BuilderBlockValidationRequest,
417    ) -> RpcResult<()> {
418        Err(internal_rpc_err("unimplemented"))
419    }
420
421    async fn validate_builder_submission_v2(
422        &self,
423        _request: BuilderBlockValidationRequestV2,
424    ) -> RpcResult<()> {
425        Err(internal_rpc_err("unimplemented"))
426    }
427
428    /// Validates a block submitted to the relay
429    async fn validate_builder_submission_v3(
430        &self,
431        request: BuilderBlockValidationRequestV3,
432    ) -> RpcResult<()> {
433        let this = self.clone();
434        let (tx, rx) = oneshot::channel();
435
436        self.task_spawner.spawn_blocking(Box::pin(async move {
437            let result = Self::validate_builder_submission_v3(&this, request)
438                .await
439                .map_err(|err| internal_rpc_err(err.to_string()));
440            let _ = tx.send(result);
441        }));
442
443        rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
444    }
445
446    /// Validates a block submitted to the relay
447    async fn validate_builder_submission_v4(
448        &self,
449        request: BuilderBlockValidationRequestV4,
450    ) -> RpcResult<()> {
451        let this = self.clone();
452        let (tx, rx) = oneshot::channel();
453
454        self.task_spawner.spawn_blocking(Box::pin(async move {
455            let result = Self::validate_builder_submission_v4(&this, request)
456                .await
457                .map_err(|err| internal_rpc_err(err.to_string()));
458            let _ = tx.send(result);
459        }));
460
461        rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
462    }
463}
464
465#[derive(Debug)]
466pub struct ValidationApiInner<Provider, E: BlockExecutorProvider> {
467    /// The provider that can interact with the chain.
468    provider: Provider,
469    /// Consensus implementation.
470    consensus: Arc<dyn FullConsensus<E::Primitives>>,
471    /// Execution payload validator.
472    payload_validator: Arc<dyn PayloadValidator<Block = <E::Primitives as NodePrimitives>::Block>>,
473    /// Block executor factory.
474    executor_provider: E,
475    /// Set of disallowed addresses
476    disallow: HashSet<Address>,
477    /// Cached state reads to avoid redundant disk I/O across multiple validation attempts
478    /// targeting the same state. Stores a tuple of (`block_hash`, `cached_reads`) for the
479    /// latest head block state. Uses async `RwLock` to safely handle concurrent validation
480    /// requests.
481    cached_state: RwLock<(B256, CachedReads)>,
482    /// Task spawner for blocking operations
483    task_spawner: Box<dyn TaskSpawner>,
484}
485
486/// Configuration for validation API.
487#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
488pub struct ValidationApiConfig {
489    /// Disallowed addresses.
490    pub disallow: HashSet<Address>,
491}
492
493/// Errors thrown by the validation API.
494#[derive(Debug, thiserror::Error)]
495pub enum ValidationApiError {
496    #[error("block gas limit mismatch: {_0}")]
497    GasLimitMismatch(GotExpected<u64>),
498    #[error("block gas used mismatch: {_0}")]
499    GasUsedMismatch(GotExpected<u64>),
500    #[error("block parent hash mismatch: {_0}")]
501    ParentHashMismatch(GotExpected<B256>),
502    #[error("block hash mismatch: {_0}")]
503    BlockHashMismatch(GotExpected<B256>),
504    #[error("missing latest block in database")]
505    MissingLatestBlock,
506    #[error("could not verify proposer payment")]
507    ProposerPayment,
508    #[error("invalid blobs bundle")]
509    InvalidBlobsBundle,
510    /// When the transaction signature is invalid
511    #[error("invalid transaction signature")]
512    InvalidTransactionSignature,
513    #[error("block accesses blacklisted address: {_0}")]
514    Blacklist(Address),
515    #[error(transparent)]
516    Blob(#[from] BlobTransactionValidationError),
517    #[error(transparent)]
518    Consensus(#[from] ConsensusError),
519    #[error(transparent)]
520    Provider(#[from] ProviderError),
521    #[error(transparent)]
522    Execution(#[from] BlockExecutionError),
523    #[error(transparent)]
524    Payload(#[from] PayloadError),
525}