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