Skip to main content

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