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#[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 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 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 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 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 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 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 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 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 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 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 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 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 (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 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 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 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 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 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 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 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 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 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 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 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 provider: Provider,
636 consensus: Arc<dyn FullConsensus<E::Primitives>>,
638 payload_validator:
640 Arc<dyn PayloadValidator<T, Block = <E::Primitives as NodePrimitives>::Block>>,
641 evm_config: E,
643 disallow: AddressSet,
645 validation_window: u64,
647 cached_state: RwLock<(B256, CachedReads)>,
652 task_spawner: Runtime,
654 metrics: ValidationMetrics,
656}
657
658fn hash_disallow_list(disallow: &AddressSet) -> String {
663 let mut sorted: Vec<_> = disallow.iter().collect();
664 sorted.sort_unstable(); 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#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
682pub struct ValidationApiConfig {
683 pub disallow: AddressSet,
685 pub validation_window: u64,
687}
688
689impl ValidationApiConfig {
690 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#[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#[derive(Metrics)]
775#[metrics(scope = "builder.validation")]
776pub(crate) struct ValidationMetrics {
777 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])); 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 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}