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