Skip to main content

reth_rpc/
validation.rs

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