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#[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 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 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 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 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 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 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 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 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 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 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 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 (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 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 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 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 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 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 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 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 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 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 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 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 provider: Provider,
622 consensus: Arc<dyn FullConsensus<E::Primitives>>,
624 payload_validator:
626 Arc<dyn PayloadValidator<T, Block = <E::Primitives as NodePrimitives>::Block>>,
627 evm_config: E,
629 disallow: AddressSet,
631 validation_window: u64,
633 cached_state: RwLock<(B256, CachedReads)>,
638 task_spawner: Runtime,
640 metrics: ValidationMetrics,
642}
643
644fn 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
676fn hash_disallow_list(disallow: &AddressSet) -> String {
681 let mut sorted: Vec<_> = disallow.iter().collect();
682 sorted.sort_unstable(); 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#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
700pub struct ValidationApiConfig {
701 pub disallow: AddressSet,
703 pub validation_window: u64,
705}
706
707impl ValidationApiConfig {
708 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#[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#[derive(Metrics)]
793#[metrics(scope = "builder.validation")]
794pub(crate) struct ValidationMetrics {
795 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])); 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 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}