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