Skip to main content

reth_rpc/
validation.rs

1use alloy_consensus::{
2    BlobTransactionValidationError, BlockHeader, EnvKzgSettings, Transaction, TxReceipt,
3};
4use alloy_eips::{eip7685::RequestsOrHash, eip7928::bal::DecodedBal};
5use alloy_primitives::map::AddressSet;
6use alloy_rpc_types_beacon::relay::{
7    BidTrace, BuilderBlockValidationRequest, BuilderBlockValidationRequestV2,
8    BuilderBlockValidationRequestV3, BuilderBlockValidationRequestV4,
9    BuilderBlockValidationRequestV5, BuilderBlockValidationRequestV6,
10};
11use alloy_rpc_types_engine::{
12    BlobsBundleV1, BlobsBundleV2, CancunPayloadFields, ExecutionData, ExecutionPayload,
13    ExecutionPayloadSidecar, PraguePayloadFields,
14};
15use async_trait::async_trait;
16use core::fmt;
17use jsonrpsee::core::RpcResult;
18use jsonrpsee_types::error::ErrorObject;
19use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
20use reth_consensus::{Consensus, FullConsensus};
21use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
22use reth_engine_primitives::PayloadValidator;
23use reth_errors::{BlockExecutionError, ConsensusError, ProviderError};
24use reth_evm::{execute::Executor, ConfigureEvm};
25use reth_execution_types::BlockExecutionOutput;
26use reth_metrics::{
27    metrics,
28    metrics::{gauge, Gauge},
29    Metrics,
30};
31use reth_node_api::{NewPayloadError, PayloadTypes};
32use reth_primitives_traits::{
33    constants::GAS_LIMIT_BOUND_DIVISOR, BlockBody, GotExpected, NodePrimitives, RecoveredBlock,
34    SealedBlock, SealedHeaderFor,
35};
36use reth_revm::{cached::CachedReads, database::StateProviderDatabase};
37use reth_rpc_api::BlockSubmissionValidationApiServer;
38use reth_rpc_server_types::result::{internal_rpc_err, invalid_params_rpc_err};
39use reth_storage_api::{BlockReaderIdExt, StateProviderFactory};
40use reth_tasks::Runtime;
41use revm_primitives::{Address, B256, U256};
42use serde::{Deserialize, Serialize};
43use sha2::{Digest, Sha256};
44use std::sync::Arc;
45use tokio::sync::{oneshot, RwLock};
46use tracing::warn;
47
48/// The type that implements the `validation` rpc namespace trait
49#[derive(Clone, Debug, derive_more::Deref)]
50pub struct ValidationApi<Provider, E: ConfigureEvm, T: PayloadTypes> {
51    #[deref]
52    inner: Arc<ValidationApiInner<Provider, E, T>>,
53}
54
55impl<Provider, E, T> ValidationApi<Provider, E, T>
56where
57    E: ConfigureEvm,
58    T: PayloadTypes,
59{
60    /// Create a new instance of the [`ValidationApi`]
61    pub fn new(
62        provider: Provider,
63        consensus: Arc<dyn FullConsensus<E::Primitives>>,
64        evm_config: E,
65        config: ValidationApiConfig,
66        task_spawner: Runtime,
67        payload_validator: Arc<
68            dyn PayloadValidator<T, Block = <E::Primitives as NodePrimitives>::Block>,
69        >,
70    ) -> Self {
71        let ValidationApiConfig { disallow, validation_window } = config;
72
73        let inner = Arc::new(ValidationApiInner {
74            provider,
75            consensus,
76            payload_validator,
77            evm_config,
78            disallow,
79            validation_window,
80            cached_state: Default::default(),
81            task_spawner,
82            metrics: Default::default(),
83        });
84
85        inner.metrics.disallow_size.set(inner.disallow.len() as f64);
86
87        let disallow_hash = hash_disallow_list(&inner.disallow);
88        let hash_gauge = gauge!("builder_validation_disallow_hash", "hash" => disallow_hash);
89        hash_gauge.set(1.0);
90
91        Self { inner }
92    }
93
94    /// Returns the cached reads for the given head hash.
95    async fn cached_reads(&self, head: B256) -> CachedReads {
96        let cache = self.inner.cached_state.read().await;
97        if cache.0 == head {
98            cache.1.clone()
99        } else {
100            Default::default()
101        }
102    }
103
104    /// Updates the cached state for the given head hash.
105    async fn update_cached_reads(&self, head: B256, cached_state: CachedReads) {
106        let mut cache = self.inner.cached_state.write().await;
107        if cache.0 == head {
108            cache.1.extend(cached_state);
109        } else {
110            *cache = (head, cached_state)
111        }
112    }
113}
114
115impl<Provider, E, T> ValidationApi<Provider, E, T>
116where
117    Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
118        + ChainSpecProvider<ChainSpec: EthereumHardforks>
119        + StateProviderFactory
120        + 'static,
121    E: ConfigureEvm + 'static,
122    T: PayloadTypes<ExecutionData = ExecutionData>,
123{
124    /// Validates the given block and a [`BidTrace`] against it.
125    pub async fn validate_message_against_block(
126        &self,
127        block: RecoveredBlock<<E::Primitives as NodePrimitives>::Block>,
128        message: BidTrace,
129        registered_gas_limit: u64,
130        _decoded_bal: Option<DecodedBal>,
131    ) -> Result<(), ValidationApiError> {
132        self.validate_message_against_header(block.sealed_header(), &message)?;
133
134        self.consensus.validate_header(block.sealed_header())?;
135        self.consensus.validate_block_pre_execution(block.sealed_block())?;
136
137        if !self.disallow.is_empty() {
138            if self.disallow.contains(&block.beneficiary()) {
139                return Err(ValidationApiError::Blacklist(block.beneficiary()))
140            }
141            if self.disallow.contains(&message.proposer_fee_recipient) {
142                return Err(ValidationApiError::Blacklist(message.proposer_fee_recipient))
143            }
144            for (sender, tx) in block.senders_iter().zip(block.body().transactions()) {
145                if self.disallow.contains(sender) {
146                    return Err(ValidationApiError::Blacklist(*sender))
147                }
148                if let Some(to) = tx.to() &&
149                    self.disallow.contains(&to)
150                {
151                    return Err(ValidationApiError::Blacklist(to))
152                }
153            }
154        }
155
156        let latest_header =
157            self.provider.latest_header()?.ok_or_else(|| ValidationApiError::MissingLatestBlock)?;
158
159        let parent_header = if block.parent_hash() == latest_header.hash() {
160            latest_header
161        } else {
162            // parent is not the latest header so we need to fetch it and ensure it's not too old
163            let parent_header = self
164                .provider
165                .sealed_header_by_hash(block.parent_hash())?
166                .ok_or_else(|| ValidationApiError::MissingParentBlock)?;
167
168            if latest_header.number().saturating_sub(parent_header.number()) >
169                self.validation_window
170            {
171                return Err(ValidationApiError::BlockTooOld)
172            }
173            parent_header
174        };
175
176        self.consensus.validate_header_against_parent(block.sealed_header(), &parent_header)?;
177        self.validate_gas_limit(registered_gas_limit, &parent_header, block.sealed_header())?;
178        let parent_header_hash = parent_header.hash();
179        let state_provider = self.provider.state_by_block_hash(parent_header_hash)?;
180
181        let mut request_cache = self.cached_reads(parent_header_hash).await;
182
183        let cached_db = request_cache.as_db_mut(StateProviderDatabase::new(&state_provider));
184        let executor = self.evm_config.batch_executor(cached_db);
185
186        let mut accessed_blacklisted = None;
187        let output = executor.execute_with_state_closure(&block, |state| {
188            if !self.disallow.is_empty() {
189                // Check whether the submission interacted with any blacklisted account by scanning
190                // the `State`'s cache that records everything read from database during execution.
191                for account in state.cache.accounts.keys() {
192                    if self.disallow.contains(account) {
193                        accessed_blacklisted = Some(*account);
194                    }
195                }
196            }
197        })?;
198
199        if let Some(account) = accessed_blacklisted {
200            return Err(ValidationApiError::Blacklist(account))
201        }
202
203        // update the cached reads
204        self.update_cached_reads(parent_header_hash, request_cache).await;
205
206        self.consensus.validate_block_post_execution(&block, &output, None, None)?;
207
208        self.ensure_payment(&block, &output, &message)?;
209
210        let state_root =
211            state_provider.state_root(state_provider.hashed_post_state(&output.state))?;
212
213        if state_root != block.header().state_root() {
214            return Err(ConsensusError::BodyStateRootDiff(
215                GotExpected { got: state_root, expected: block.header().state_root() }.into(),
216            )
217            .into())
218        }
219
220        Ok(())
221    }
222
223    /// Ensures that fields of [`BidTrace`] match the fields of the [`SealedHeaderFor`].
224    fn validate_message_against_header(
225        &self,
226        header: &SealedHeaderFor<E::Primitives>,
227        message: &BidTrace,
228    ) -> Result<(), ValidationApiError> {
229        if header.hash() != message.block_hash {
230            Err(ValidationApiError::BlockHashMismatch(GotExpected {
231                got: message.block_hash,
232                expected: header.hash(),
233            }))
234        } else if header.parent_hash() != message.parent_hash {
235            Err(ValidationApiError::ParentHashMismatch(GotExpected {
236                got: message.parent_hash,
237                expected: header.parent_hash(),
238            }))
239        } else if header.gas_limit() != message.gas_limit {
240            Err(ValidationApiError::GasLimitMismatch(GotExpected {
241                got: message.gas_limit,
242                expected: header.gas_limit(),
243            }))
244        } else if header.gas_used() != message.gas_used {
245            Err(ValidationApiError::GasUsedMismatch(GotExpected {
246                got: message.gas_used,
247                expected: header.gas_used(),
248            }))
249        } else {
250            Ok(())
251        }
252    }
253
254    /// Ensures that the chosen gas limit is the closest possible value for the validator's
255    /// registered gas limit.
256    ///
257    /// Ref: <https://github.com/flashbots/builder/blob/a742641e24df68bc2fc476199b012b0abce40ffe/core/blockchain.go#L2474-L2477>
258    fn validate_gas_limit(
259        &self,
260        registered_gas_limit: u64,
261        parent_header: &SealedHeaderFor<E::Primitives>,
262        header: &SealedHeaderFor<E::Primitives>,
263    ) -> Result<(), ValidationApiError> {
264        let max_gas_limit =
265            parent_header.gas_limit() + parent_header.gas_limit() / GAS_LIMIT_BOUND_DIVISOR - 1;
266        let min_gas_limit =
267            parent_header.gas_limit() - parent_header.gas_limit() / GAS_LIMIT_BOUND_DIVISOR + 1;
268
269        let best_gas_limit =
270            std::cmp::max(min_gas_limit, std::cmp::min(max_gas_limit, registered_gas_limit));
271
272        if best_gas_limit != header.gas_limit() {
273            return Err(ValidationApiError::GasLimitMismatch(GotExpected {
274                got: header.gas_limit(),
275                expected: best_gas_limit,
276            }))
277        }
278
279        Ok(())
280    }
281
282    /// Ensures that the proposer has received [`BidTrace::value`] for this block.
283    ///
284    /// Firstly attempts to verify the payment by checking the state changes, otherwise falls back
285    /// to checking the latest block transaction.
286    fn ensure_payment(
287        &self,
288        block: &SealedBlock<<E::Primitives as NodePrimitives>::Block>,
289        output: &BlockExecutionOutput<<E::Primitives as NodePrimitives>::Receipt>,
290        message: &BidTrace,
291    ) -> Result<(), ValidationApiError> {
292        let (mut balance_before, balance_after) = if let Some(acc) =
293            output.state.state.get(&message.proposer_fee_recipient)
294        {
295            let balance_before = acc.original_info.as_ref().map(|i| i.balance).unwrap_or_default();
296            let balance_after = acc.info.as_ref().map(|i| i.balance).unwrap_or_default();
297
298            (balance_before, balance_after)
299        } else {
300            // account might have balance but considering it zero is fine as long as we know
301            // that balance have not changed
302            (U256::ZERO, U256::ZERO)
303        };
304
305        if let Some(withdrawals) = block.body().withdrawals() {
306            for withdrawal in withdrawals {
307                if withdrawal.address == message.proposer_fee_recipient {
308                    balance_before += withdrawal.amount_wei();
309                }
310            }
311        }
312
313        if balance_after >= balance_before.saturating_add(message.value) {
314            return Ok(())
315        }
316
317        let (receipt, tx) = output
318            .receipts
319            .last()
320            .zip(block.body().transactions().last())
321            .ok_or(ValidationApiError::ProposerPayment)?;
322
323        if !receipt.status() {
324            return Err(ValidationApiError::ProposerPayment)
325        }
326
327        if tx.to() != Some(message.proposer_fee_recipient) {
328            return Err(ValidationApiError::ProposerPayment)
329        }
330
331        if tx.value() != message.value {
332            return Err(ValidationApiError::ProposerPayment)
333        }
334
335        if !tx.input().is_empty() {
336            return Err(ValidationApiError::ProposerPayment)
337        }
338
339        if let Some(block_base_fee) = block.header().base_fee_per_gas() &&
340            tx.effective_tip_per_gas(block_base_fee).unwrap_or_default() != 0
341        {
342            return Err(ValidationApiError::ProposerPayment)
343        }
344
345        Ok(())
346    }
347
348    /// Validates the given [`BlobsBundleV1`] and returns versioned hashes for blobs.
349    pub fn validate_blobs_bundle(
350        &self,
351        blobs_bundle: BlobsBundleV1,
352    ) -> Result<Vec<B256>, ValidationApiError> {
353        let versioned_hashes = blobs_bundle.versioned_hashes();
354        let sidecar =
355            blobs_bundle.try_into_sidecar().map_err(|_| ValidationApiError::InvalidBlobsBundle)?;
356
357        sidecar.validate(&versioned_hashes, EnvKzgSettings::default().get())?;
358        Ok(versioned_hashes)
359    }
360
361    /// Validates the given [`BlobsBundleV2`] and returns versioned hashes for blobs.
362    pub fn validate_blobs_bundle_v2(
363        &self,
364        blobs_bundle: BlobsBundleV2,
365    ) -> Result<Vec<B256>, ValidationApiError> {
366        let versioned_hashes = blobs_bundle.versioned_hashes();
367        let sidecar =
368            blobs_bundle.try_into_sidecar().map_err(|_| ValidationApiError::InvalidBlobsBundle)?;
369
370        sidecar.validate(&versioned_hashes, EnvKzgSettings::default().get())?;
371        Ok(versioned_hashes)
372    }
373
374    /// Core logic for validating the builder submission v3
375    async fn validate_builder_submission_v3(
376        &self,
377        request: BuilderBlockValidationRequestV3,
378    ) -> Result<(), ValidationApiError> {
379        let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
380            payload: ExecutionPayload::V3(request.request.execution_payload),
381            sidecar: ExecutionPayloadSidecar::v3(CancunPayloadFields {
382                parent_beacon_block_root: request.parent_beacon_block_root,
383                versioned_hashes: self.validate_blobs_bundle(request.request.blobs_bundle)?,
384            }),
385        })?;
386
387        self.validate_message_against_block(
388            block,
389            request.request.message,
390            request.registered_gas_limit,
391            None,
392        )
393        .await
394    }
395
396    /// Core logic for validating the builder submission v4
397    async fn validate_builder_submission_v4(
398        &self,
399        request: BuilderBlockValidationRequestV4,
400    ) -> Result<(), ValidationApiError> {
401        let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
402            payload: ExecutionPayload::V3(request.request.execution_payload),
403            sidecar: ExecutionPayloadSidecar::v4(
404                CancunPayloadFields {
405                    parent_beacon_block_root: request.parent_beacon_block_root,
406                    versioned_hashes: self.validate_blobs_bundle(request.request.blobs_bundle)?,
407                },
408                PraguePayloadFields {
409                    requests: RequestsOrHash::Requests(
410                        request.request.execution_requests.to_requests(),
411                    ),
412                },
413            ),
414        })?;
415
416        self.validate_message_against_block(
417            block,
418            request.request.message,
419            request.registered_gas_limit,
420            None,
421        )
422        .await
423    }
424
425    /// Core logic for validating the builder submission v5
426    async fn validate_builder_submission_v5(
427        &self,
428        request: BuilderBlockValidationRequestV5,
429    ) -> Result<(), ValidationApiError> {
430        let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
431            payload: ExecutionPayload::V3(request.request.execution_payload),
432            sidecar: ExecutionPayloadSidecar::v4(
433                CancunPayloadFields {
434                    parent_beacon_block_root: request.parent_beacon_block_root,
435                    versioned_hashes: self
436                        .validate_blobs_bundle_v2(request.request.blobs_bundle)?,
437                },
438                PraguePayloadFields {
439                    requests: RequestsOrHash::Requests(
440                        request.request.execution_requests.to_requests(),
441                    ),
442                },
443            ),
444        })?;
445
446        // Check block size as per EIP-7934 (only applies when Osaka hardfork is active)
447        let chain_spec = self.provider.chain_spec();
448        if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) &&
449            block.rlp_length() > MAX_RLP_BLOCK_SIZE
450        {
451            return Err(ValidationApiError::Consensus(ConsensusError::BlockTooLarge {
452                rlp_length: block.rlp_length(),
453                max_rlp_length: MAX_RLP_BLOCK_SIZE,
454            }));
455        }
456
457        self.validate_message_against_block(
458            block,
459            request.request.message,
460            request.registered_gas_limit,
461            None,
462        )
463        .await
464    }
465
466    /// Core logic for validating the builder submission v6
467    async fn validate_builder_submission_v6(
468        &self,
469        request: BuilderBlockValidationRequestV6,
470    ) -> Result<(), ValidationApiError> {
471        let decoded_bal =
472            DecodedBal::from_rlp_bytes(request.request.execution_payload.block_access_list.clone())
473                .map_err(ValidationApiError::InvalidBlockAccessList)?;
474
475        let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
476            payload: ExecutionPayload::V4(request.request.execution_payload),
477            sidecar: ExecutionPayloadSidecar::v4(
478                CancunPayloadFields {
479                    parent_beacon_block_root: request.parent_beacon_block_root,
480                    versioned_hashes: self
481                        .validate_blobs_bundle_v2(request.request.blobs_bundle)?,
482                },
483                PraguePayloadFields {
484                    requests: RequestsOrHash::Requests(
485                        request.request.execution_requests.to_requests(),
486                    ),
487                },
488            ),
489        })?;
490
491        let chain_spec = self.provider.chain_spec();
492        if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) &&
493            block.rlp_length() > MAX_RLP_BLOCK_SIZE
494        {
495            return Err(ValidationApiError::Consensus(ConsensusError::BlockTooLarge {
496                rlp_length: block.rlp_length(),
497                max_rlp_length: MAX_RLP_BLOCK_SIZE,
498            }));
499        }
500
501        self.validate_message_against_block(
502            block,
503            request.request.message,
504            request.registered_gas_limit,
505            Some(decoded_bal),
506        )
507        .await
508    }
509}
510
511#[async_trait]
512impl<Provider, E, T> BlockSubmissionValidationApiServer for ValidationApi<Provider, E, T>
513where
514    Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
515        + ChainSpecProvider<ChainSpec: EthereumHardforks>
516        + StateProviderFactory
517        + Clone
518        + 'static,
519    E: ConfigureEvm + 'static,
520    T: PayloadTypes<ExecutionData = ExecutionData>,
521{
522    async fn validate_builder_submission_v1(
523        &self,
524        _request: BuilderBlockValidationRequest,
525    ) -> RpcResult<()> {
526        warn!(target: "rpc::flashbots", "Method `flashbots_validateBuilderSubmissionV1` is not supported");
527        Err(internal_rpc_err("unimplemented"))
528    }
529
530    async fn validate_builder_submission_v2(
531        &self,
532        _request: BuilderBlockValidationRequestV2,
533    ) -> RpcResult<()> {
534        warn!(target: "rpc::flashbots", "Method `flashbots_validateBuilderSubmissionV2` is not supported");
535        Err(internal_rpc_err("unimplemented"))
536    }
537
538    /// Validates a block submitted to the relay
539    async fn validate_builder_submission_v3(
540        &self,
541        request: BuilderBlockValidationRequestV3,
542    ) -> RpcResult<()> {
543        let this = self.clone();
544        let (tx, rx) = oneshot::channel();
545
546        self.task_spawner.spawn_blocking_task(async move {
547            let result = Self::validate_builder_submission_v3(&this, request)
548                .await
549                .map_err(ErrorObject::from);
550            let _ = tx.send(result);
551        });
552
553        rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
554    }
555
556    /// Validates a block submitted to the relay
557    async fn validate_builder_submission_v4(
558        &self,
559        request: BuilderBlockValidationRequestV4,
560    ) -> RpcResult<()> {
561        let this = self.clone();
562        let (tx, rx) = oneshot::channel();
563
564        self.task_spawner.spawn_blocking_task(async move {
565            let result = Self::validate_builder_submission_v4(&this, request)
566                .await
567                .map_err(ErrorObject::from);
568            let _ = tx.send(result);
569        });
570
571        rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
572    }
573
574    /// Validates a block submitted to the relay
575    async fn validate_builder_submission_v5(
576        &self,
577        request: BuilderBlockValidationRequestV5,
578    ) -> RpcResult<()> {
579        let this = self.clone();
580        let (tx, rx) = oneshot::channel();
581
582        self.task_spawner.spawn_blocking_task(async move {
583            let result = Self::validate_builder_submission_v5(&this, request)
584                .await
585                .map_err(ErrorObject::from);
586            let _ = tx.send(result);
587        });
588
589        rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
590    }
591
592    /// Validates a block submitted to the relay
593    async fn validate_builder_submission_v6(
594        &self,
595        request: BuilderBlockValidationRequestV6,
596    ) -> RpcResult<()> {
597        let this = self.clone();
598        let (tx, rx) = oneshot::channel();
599
600        self.task_spawner.spawn_blocking_task(async move {
601            let result = Self::validate_builder_submission_v6(&this, request)
602                .await
603                .map_err(ErrorObject::from);
604            let _ = tx.send(result);
605        });
606
607        rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
608    }
609}
610
611pub struct ValidationApiInner<Provider, E: ConfigureEvm, T: PayloadTypes> {
612    /// The provider that can interact with the chain.
613    provider: Provider,
614    /// Consensus implementation.
615    consensus: Arc<dyn FullConsensus<E::Primitives>>,
616    /// Execution payload validator.
617    payload_validator:
618        Arc<dyn PayloadValidator<T, Block = <E::Primitives as NodePrimitives>::Block>>,
619    /// Block executor factory.
620    evm_config: E,
621    /// Set of disallowed addresses
622    disallow: AddressSet,
623    /// The maximum block distance - parent to latest - allowed for validation
624    validation_window: u64,
625    /// Cached state reads to avoid redundant disk I/O across multiple validation attempts
626    /// targeting the same state. Stores a tuple of (`block_hash`, `cached_reads`) for the
627    /// latest head block state. Uses async `RwLock` to safely handle concurrent validation
628    /// requests.
629    cached_state: RwLock<(B256, CachedReads)>,
630    /// Task spawner for blocking operations
631    task_spawner: Runtime,
632    /// Validation metrics
633    metrics: ValidationMetrics,
634}
635
636/// Calculates a deterministic hash of the blocklist for change detection.
637///
638/// This function sorts addresses to ensure deterministic output regardless of
639/// insertion order, then computes a SHA256 hash of the concatenated addresses.
640fn hash_disallow_list(disallow: &AddressSet) -> String {
641    let mut sorted: Vec<_> = disallow.iter().collect();
642    sorted.sort_unstable(); // sort for deterministic hashing
643
644    let mut hasher = Sha256::new();
645    for addr in sorted {
646        hasher.update(addr.as_slice());
647    }
648
649    format!("{:x}", hasher.finalize())
650}
651
652impl<Provider, E: ConfigureEvm, T: PayloadTypes> fmt::Debug for ValidationApiInner<Provider, E, T> {
653    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
654        f.debug_struct("ValidationApiInner").finish_non_exhaustive()
655    }
656}
657
658/// Configuration for validation API.
659#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
660pub struct ValidationApiConfig {
661    /// Disallowed addresses.
662    pub disallow: AddressSet,
663    /// The maximum block distance - parent to latest - allowed for validation
664    pub validation_window: u64,
665}
666
667impl ValidationApiConfig {
668    /// Default validation blocks window of 3 blocks
669    pub const DEFAULT_VALIDATION_WINDOW: u64 = 3;
670}
671
672impl Default for ValidationApiConfig {
673    fn default() -> Self {
674        Self { disallow: Default::default(), validation_window: Self::DEFAULT_VALIDATION_WINDOW }
675    }
676}
677
678/// Errors thrown by the validation API.
679#[derive(Debug, thiserror::Error)]
680pub enum ValidationApiError {
681    #[error("block gas limit mismatch: {_0}")]
682    GasLimitMismatch(GotExpected<u64>),
683    #[error("block gas used mismatch: {_0}")]
684    GasUsedMismatch(GotExpected<u64>),
685    #[error("block parent hash mismatch: {_0}")]
686    ParentHashMismatch(GotExpected<B256>),
687    #[error("block hash mismatch: {_0}")]
688    BlockHashMismatch(GotExpected<B256>),
689    #[error("missing latest block in database")]
690    MissingLatestBlock,
691    #[error("parent block not found")]
692    MissingParentBlock,
693    #[error("block is too old, outside validation window")]
694    BlockTooOld,
695    #[error("could not verify proposer payment")]
696    ProposerPayment,
697    #[error("invalid blobs bundle")]
698    InvalidBlobsBundle,
699    #[error("invalid block access list: {_0}")]
700    InvalidBlockAccessList(alloy_rlp::Error),
701    #[error("block accesses blacklisted address: {_0}")]
702    Blacklist(Address),
703    #[error(transparent)]
704    Blob(#[from] BlobTransactionValidationError),
705    #[error(transparent)]
706    Consensus(#[from] ConsensusError),
707    #[error(transparent)]
708    Provider(#[from] ProviderError),
709    #[error(transparent)]
710    Execution(#[from] BlockExecutionError),
711    #[error(transparent)]
712    Payload(#[from] NewPayloadError),
713}
714
715impl From<ValidationApiError> for ErrorObject<'static> {
716    fn from(error: ValidationApiError) -> Self {
717        match error {
718            ValidationApiError::GasLimitMismatch(_) |
719            ValidationApiError::GasUsedMismatch(_) |
720            ValidationApiError::ParentHashMismatch(_) |
721            ValidationApiError::BlockHashMismatch(_) |
722            ValidationApiError::Blacklist(_) |
723            ValidationApiError::ProposerPayment |
724            ValidationApiError::InvalidBlobsBundle |
725            ValidationApiError::InvalidBlockAccessList(_) |
726            ValidationApiError::Blob(_) => invalid_params_rpc_err(error.to_string()),
727
728            ValidationApiError::MissingLatestBlock |
729            ValidationApiError::MissingParentBlock |
730            ValidationApiError::BlockTooOld |
731            ValidationApiError::Consensus(_) |
732            ValidationApiError::Provider(_) => internal_rpc_err(error.to_string()),
733            ValidationApiError::Execution(err) => match err {
734                error @ BlockExecutionError::Validation(_) => {
735                    invalid_params_rpc_err(error.to_string())
736                }
737                error @ BlockExecutionError::Internal(_) => internal_rpc_err(error.to_string()),
738            },
739            ValidationApiError::Payload(err) => match err {
740                error @ NewPayloadError::Eth(_) => invalid_params_rpc_err(error.to_string()),
741                error @ NewPayloadError::Other(_) => internal_rpc_err(error.to_string()),
742            },
743        }
744    }
745}
746
747/// Metrics for the validation endpoint.
748#[derive(Metrics)]
749#[metrics(scope = "builder.validation")]
750pub(crate) struct ValidationMetrics {
751    /// The number of entries configured in the builder validation disallow list.
752    pub(crate) disallow_size: Gauge,
753}
754
755#[cfg(test)]
756mod tests {
757    use super::{hash_disallow_list, AddressSet};
758    use revm_primitives::Address;
759
760    #[test]
761    fn test_hash_disallow_list_deterministic() {
762        let mut addresses = AddressSet::default();
763        addresses.insert(Address::from([1u8; 20]));
764        addresses.insert(Address::from([2u8; 20]));
765
766        let hash1 = hash_disallow_list(&addresses);
767        let hash2 = hash_disallow_list(&addresses);
768
769        assert_eq!(hash1, hash2);
770    }
771
772    #[test]
773    fn test_hash_disallow_list_different_content() {
774        let mut addresses1 = AddressSet::default();
775        addresses1.insert(Address::from([1u8; 20]));
776
777        let mut addresses2 = AddressSet::default();
778        addresses2.insert(Address::from([2u8; 20]));
779
780        let hash1 = hash_disallow_list(&addresses1);
781        let hash2 = hash_disallow_list(&addresses2);
782
783        assert_ne!(hash1, hash2);
784    }
785
786    #[test]
787    fn test_hash_disallow_list_order_independent() {
788        let mut addresses1 = AddressSet::default();
789        addresses1.insert(Address::from([1u8; 20]));
790        addresses1.insert(Address::from([2u8; 20]));
791
792        let mut addresses2 = AddressSet::default();
793        addresses2.insert(Address::from([2u8; 20])); // Different insertion order
794        addresses2.insert(Address::from([1u8; 20]));
795
796        let hash1 = hash_disallow_list(&addresses1);
797        let hash2 = hash_disallow_list(&addresses2);
798
799        assert_eq!(hash1, hash2);
800    }
801
802    #[test]
803    //ensures parity with rbuilder hashing https://github.com/flashbots/rbuilder/blob/962c8444cdd490a216beda22c7eec164db9fc3ac/crates/rbuilder/src/live_builder/block_list_provider.rs#L248
804    fn test_disallow_list_hash_rbuilder_parity() {
805        let json = r#"["0x05E0b5B40B7b66098C2161A5EE11C5740A3A7C45","0x01e2919679362dFBC9ee1644Ba9C6da6D6245BB1","0x03893a7c7463AE47D46bc7f091665f1893656003","0x04DBA1194ee10112fE6C3207C0687DEf0e78baCf"]"#;
806        let blocklist: Vec<Address> = serde_json::from_str(json).unwrap();
807        let blocklist: AddressSet = blocklist.into_iter().collect();
808        let expected_hash = "ee14e9d115e182f61871a5a385ab2f32ecf434f3b17bdbacc71044810d89e608";
809        let hash = hash_disallow_list(&blocklist);
810        assert_eq!(expected_hash, hash);
811    }
812}