1use self::blob_cache::BlobValidationCache;
2use alloy_consensus::{
3 BlobTransactionValidationError, BlockHeader, EnvKzgSettings, Transaction, TxReceipt,
4};
5use alloy_eip7928::{bal::DecodedBal, compute_block_access_list_hash};
6use alloy_eips::eip7685::RequestsOrHash;
7use alloy_primitives::{map::AddressSet, Address, B256, U256};
8use alloy_rpc_types_beacon::relay::{
9 BidTrace, BuilderBlockValidationRequest, BuilderBlockValidationRequestV2,
10 BuilderBlockValidationRequestV3, BuilderBlockValidationRequestV4,
11 BuilderBlockValidationRequestV5, BuilderBlockValidationRequestV6,
12};
13use alloy_rpc_types_engine::{
14 BlobsBundleV1, BlobsBundleV2, CancunPayloadFields, ExecutionData, ExecutionPayload,
15 ExecutionPayloadSidecar, PraguePayloadFields,
16};
17use async_trait::async_trait;
18use core::fmt;
19use jsonrpsee::core::RpcResult;
20use jsonrpsee_types::error::ErrorObject;
21use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
22use reth_consensus::{Consensus, FullConsensus};
23use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
24use reth_engine_primitives::PayloadValidator;
25use reth_errors::{BlockExecutionError, ConsensusError, ProviderError};
26use reth_evm::{execute::Executor, ConfigureEvm, SenderRecoveryCache};
27use reth_execution_types::BlockExecutionOutput;
28use reth_metrics::{
29 metrics,
30 metrics::{gauge, Gauge},
31 Metrics,
32};
33use reth_node_api::{NewPayloadError, PayloadTypes};
34use reth_primitives_traits::{
35 block::error::SealedBlockRecoveryError, BlockBody, GotExpected, NodePrimitives, RecoveredBlock,
36 SealedBlock, SealedHeaderFor,
37};
38use reth_revm::{cached::CachedReads, database::StateProviderDatabase};
39use reth_rpc_api::BlockSubmissionValidationApiServer;
40use reth_rpc_server_types::result::{internal_rpc_err, invalid_params_rpc_err};
41use reth_storage_api::{
42 BlockReaderIdExt, HashedPostStateProvider, StateProvider, StateProviderFactory,
43};
44use reth_tasks::Runtime;
45use serde::{Deserialize, Serialize};
46use sha2::{Digest, Sha256};
47use std::sync::Arc;
48use tokio::sync::{oneshot, RwLock};
49use tracing::warn;
50
51mod blob_cache;
52
53#[derive(Clone, Debug, derive_more::Deref)]
55pub struct ValidationApi<Provider, E: ConfigureEvm, T: PayloadTypes> {
56 #[deref]
57 inner: Arc<ValidationApiInner<Provider, E, T>>,
58}
59
60impl<Provider, E, T> ValidationApi<Provider, E, T>
61where
62 E: ConfigureEvm,
63 T: PayloadTypes,
64{
65 pub fn new(
70 provider: Provider,
71 consensus: Arc<dyn FullConsensus<E::Primitives>>,
72 evm_config: E,
73 config: ValidationApiConfig,
74 task_spawner: Runtime,
75 payload_validator: Arc<
76 dyn PayloadValidator<T, Block = <E::Primitives as NodePrimitives>::Block>,
77 >,
78 sender_recovery_cache: Option<SenderRecoveryCache>,
79 ) -> Self {
80 let ValidationApiConfig { disallow, validation_window } = config;
81
82 let inner = Arc::new(ValidationApiInner {
83 provider,
84 consensus,
85 payload_validator,
86 evm_config,
87 disallow,
88 validation_window,
89 cached_state: Default::default(),
90 validated_blobs: Default::default(),
91 task_spawner,
92 sender_recovery_cache,
93 metrics: Default::default(),
94 });
95
96 inner.metrics.disallow_size.set(inner.disallow.len() as f64);
97
98 let disallow_hash = hash_disallow_list(&inner.disallow);
99 let hash_gauge = gauge!("builder_validation_disallow_hash", "hash" => disallow_hash);
100 hash_gauge.set(1.0);
101
102 Self { inner }
103 }
104
105 async fn cached_reads(&self, head: B256) -> CachedReads {
107 let cache = self.inner.cached_state.read().await;
108 if cache.0 == head {
109 cache.1.clone()
110 } else {
111 Default::default()
112 }
113 }
114
115 async fn update_cached_reads(&self, head: B256, cached_state: CachedReads) {
117 let mut cache = self.inner.cached_state.write().await;
118 if cache.0 == head {
119 cache.1.extend(cached_state);
120 } else {
121 *cache = (head, cached_state)
122 }
123 }
124
125 async fn spawn_validation<F>(&self, validation: F) -> RpcResult<()>
130 where
131 F: Future<Output = Result<(), ValidationApiError>> + Send + 'static,
132 {
133 let (mut tx, rx) = oneshot::channel();
134
135 self.task_spawner.spawn_blocking_task(async move {
136 let result = tokio::select! {
137 biased;
138 _ = tx.closed() => return,
139 result = validation => result.map_err(ErrorObject::from),
140 };
141 let _ = tx.send(result);
142 });
143
144 rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
145 }
146}
147
148impl<Provider, E, T> ValidationApi<Provider, E, T>
149where
150 Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
151 + ChainSpecProvider<ChainSpec: EthereumHardforks>
152 + StateProviderFactory
153 + 'static,
154 E: ConfigureEvm + 'static,
155 T: PayloadTypes<ExecutionData = ExecutionData>,
156{
157 pub async fn validate_message_against_block(
159 &self,
160 block: RecoveredBlock<<E::Primitives as NodePrimitives>::Block>,
161 message: BidTrace,
162 registered_gas_limit: u64,
163 decoded_bal: Option<DecodedBal>,
164 ) -> Result<(), ValidationApiError> {
165 self.validate_message_against_header(block.sealed_header(), &message)?;
166
167 self.consensus.validate_header(block.sealed_header())?;
168 self.consensus.validate_block_pre_execution(block.sealed_block())?;
169
170 if !self.disallow.is_empty() {
171 if self.disallow.contains(&block.beneficiary()) {
172 return Err(ValidationApiError::Blacklist(block.beneficiary()))
173 }
174 if self.disallow.contains(&message.proposer_fee_recipient) {
175 return Err(ValidationApiError::Blacklist(message.proposer_fee_recipient))
176 }
177 for (sender, tx) in block.senders_iter().zip(block.body().transactions()) {
178 if self.disallow.contains(sender) {
179 return Err(ValidationApiError::Blacklist(*sender))
180 }
181 if let Some(to) = tx.to() &&
182 self.disallow.contains(&to)
183 {
184 return Err(ValidationApiError::Blacklist(to))
185 }
186 }
187 }
188
189 let latest_header =
190 self.provider.latest_header()?.ok_or_else(|| ValidationApiError::MissingLatestBlock)?;
191
192 let parent_header = if block.parent_hash() == latest_header.hash() {
193 latest_header
194 } else {
195 let parent_header = self
197 .provider
198 .sealed_header_by_hash(block.parent_hash())?
199 .ok_or_else(|| ValidationApiError::MissingParentBlock)?;
200
201 if latest_header.number().saturating_sub(parent_header.number()) >
202 self.validation_window
203 {
204 return Err(ValidationApiError::BlockTooOld)
205 }
206 parent_header
207 };
208
209 self.consensus.validate_header_against_parent(block.sealed_header(), &parent_header)?;
210 parent_header.validate_gas_limit(registered_gas_limit, block.gas_limit()).map_err(
211 |err| {
212 ValidationApiError::GasLimitMismatch(GotExpected {
213 got: err.got,
214 expected: err.expected,
215 })
216 },
217 )?;
218
219 if let Some(decoded_bal) = decoded_bal {
221 decoded_bal
222 .as_bal()
223 .validate_gas_limit(block.gas_limit())
224 .map_err(ConsensusError::from)?;
225 }
226
227 let parent_header_hash = parent_header.hash();
228 let state_provider = self.provider.state_by_block_hash(parent_header_hash)?;
229
230 let mut request_cache = self.cached_reads(parent_header_hash).await;
231
232 let (output, block_access_list_hash) = {
233 let cached_db = request_cache
234 .as_db_mut(StateProviderDatabase::new((&state_provider).into_evm_state_provider()));
235 let mut executor = self.evm_config.batch_executor(cached_db);
236
237 let result = executor.execute_one(&block)?;
238
239 let block_access_list_hash =
244 executor.take_bal().map(|bal| compute_block_access_list_hash(&bal));
245
246 let mut state = executor.into_state();
247 if !self.disallow.is_empty() {
248 for account in state.cache.accounts.keys() {
252 if self.disallow.contains(account) {
253 return Err(ValidationApiError::Blacklist(*account))
254 }
255 }
256 }
257
258 (BlockExecutionOutput { state: state.take_bundle(), result }, block_access_list_hash)
259 };
260
261 self.update_cached_reads(parent_header_hash, request_cache).await;
263
264 self.consensus.validate_block_post_execution(
265 &block,
266 &output,
267 None,
268 block_access_list_hash,
269 )?;
270
271 self.ensure_payment(&block, &output, &message)?;
272
273 let hashed_state = state_provider.hashed_post_state(&output.state)?;
274 let state_root = state_provider.state_root(hashed_state)?;
275
276 if state_root != block.header().state_root() {
277 return Err(ConsensusError::BodyStateRootDiff(
278 GotExpected { got: state_root, expected: block.header().state_root() }.into(),
279 )
280 .into())
281 }
282
283 Ok(())
284 }
285
286 fn validate_message_against_header(
288 &self,
289 header: &SealedHeaderFor<E::Primitives>,
290 message: &BidTrace,
291 ) -> Result<(), ValidationApiError> {
292 if header.hash() != message.block_hash {
293 Err(ValidationApiError::BlockHashMismatch(GotExpected {
294 got: message.block_hash,
295 expected: header.hash(),
296 }))
297 } else if header.parent_hash() != message.parent_hash {
298 Err(ValidationApiError::ParentHashMismatch(GotExpected {
299 got: message.parent_hash,
300 expected: header.parent_hash(),
301 }))
302 } else if header.gas_limit() != message.gas_limit {
303 Err(ValidationApiError::GasLimitMismatch(GotExpected {
304 got: message.gas_limit,
305 expected: header.gas_limit(),
306 }))
307 } else if header.gas_used() != message.gas_used {
308 Err(ValidationApiError::GasUsedMismatch(GotExpected {
309 got: message.gas_used,
310 expected: header.gas_used(),
311 }))
312 } else {
313 Ok(())
314 }
315 }
316
317 fn ensure_payment(
322 &self,
323 block: &SealedBlock<<E::Primitives as NodePrimitives>::Block>,
324 output: &BlockExecutionOutput<<E::Primitives as NodePrimitives>::Receipt>,
325 message: &BidTrace,
326 ) -> Result<(), ValidationApiError> {
327 if message.value.is_zero() {
329 return Ok(())
330 }
331
332 let (mut balance_before, balance_after) = if let Some(acc) =
333 output.state.account(&message.proposer_fee_recipient)
334 {
335 let balance_before = acc.original_info.as_ref().map(|i| i.balance).unwrap_or_default();
336 let balance_after = acc.info.as_ref().map(|i| i.balance).unwrap_or_default();
337
338 (balance_before, balance_after)
339 } else {
340 (U256::ZERO, U256::ZERO)
343 };
344
345 if let Some(withdrawals) = block.body().withdrawals() {
346 for withdrawal in withdrawals {
347 if withdrawal.address == message.proposer_fee_recipient {
348 balance_before += withdrawal.amount_wei();
349 }
350 }
351 }
352
353 if balance_after >= balance_before.saturating_add(message.value) {
354 return Ok(())
355 }
356
357 let (receipt, tx) = output
358 .receipts
359 .last()
360 .zip(block.body().transactions().last())
361 .ok_or(ValidationApiError::ProposerPayment)?;
362
363 if !receipt.status() {
364 return Err(ValidationApiError::ProposerPayment)
365 }
366
367 if tx.to() != Some(message.proposer_fee_recipient) {
368 return Err(ValidationApiError::ProposerPayment)
369 }
370
371 if tx.value() != message.value {
372 return Err(ValidationApiError::ProposerPayment)
373 }
374
375 if !tx.input().is_empty() {
376 return Err(ValidationApiError::ProposerPayment)
377 }
378
379 if let Some(block_base_fee) = block.header().base_fee_per_gas() &&
380 tx.effective_tip_per_gas(block_base_fee).unwrap_or_default() != 0
381 {
382 return Err(ValidationApiError::ProposerPayment)
383 }
384
385 Ok(())
386 }
387
388 pub fn validate_blobs_bundle(
390 &self,
391 blobs_bundle: BlobsBundleV1,
392 ) -> Result<Vec<B256>, ValidationApiError> {
393 let versioned_hashes = blobs_bundle.versioned_hashes();
394 let sidecar =
395 blobs_bundle.try_into_sidecar().map_err(|_| ValidationApiError::InvalidBlobsBundle)?;
396
397 sidecar.validate(&versioned_hashes, EnvKzgSettings::default().get())?;
398 Ok(versioned_hashes)
399 }
400
401 pub fn validate_blobs_bundle_v2(
407 &self,
408 blobs_bundle: BlobsBundleV2,
409 ) -> Result<Vec<B256>, ValidationApiError> {
410 self.validated_blobs.validate(blobs_bundle)
411 }
412
413 fn recover_payload(
425 &self,
426 payload: ExecutionData,
427 ) -> Result<RecoveredBlock<<E::Primitives as NodePrimitives>::Block>, ValidationApiError> {
428 let Some(cache) = &self.sender_recovery_cache else {
429 return self.payload_validator.ensure_well_formed_payload(payload).map_err(Into::into)
430 };
431
432 let block = self.payload_validator.convert_payload_to_block(payload)?;
433 let recovered = match cache.recover_signers(block.body().transactions()) {
434 Ok(senders) => Ok(RecoveredBlock::new_sealed(block, senders)),
435 Err(_) => Err(SealedBlockRecoveryError::new(block)),
436 };
437 recovered.map_err(|err| NewPayloadError::Other(err.into()).into())
438 }
439
440 async fn validate_builder_submission_v3(
442 &self,
443 request: BuilderBlockValidationRequestV3,
444 ) -> Result<(), ValidationApiError> {
445 let block = self.recover_payload(ExecutionData {
446 payload: ExecutionPayload::V3(request.request.execution_payload),
447 sidecar: ExecutionPayloadSidecar::v3(CancunPayloadFields {
448 parent_beacon_block_root: request.parent_beacon_block_root,
449 versioned_hashes: self.validate_blobs_bundle(request.request.blobs_bundle)?,
450 }),
451 })?;
452
453 self.validate_message_against_block(
454 block,
455 request.request.message,
456 request.registered_gas_limit,
457 None,
458 )
459 .await
460 }
461
462 async fn validate_builder_submission_v4(
464 &self,
465 request: BuilderBlockValidationRequestV4,
466 ) -> Result<(), ValidationApiError> {
467 let block = self.recover_payload(ExecutionData {
468 payload: ExecutionPayload::V3(request.request.execution_payload),
469 sidecar: ExecutionPayloadSidecar::v4(
470 CancunPayloadFields {
471 parent_beacon_block_root: request.parent_beacon_block_root,
472 versioned_hashes: self.validate_blobs_bundle(request.request.blobs_bundle)?,
473 },
474 PraguePayloadFields {
475 requests: RequestsOrHash::Requests(
476 request.request.execution_requests.to_requests(),
477 ),
478 },
479 ),
480 })?;
481
482 self.validate_message_against_block(
483 block,
484 request.request.message,
485 request.registered_gas_limit,
486 None,
487 )
488 .await
489 }
490
491 async fn validate_builder_submission_v5(
493 &self,
494 request: BuilderBlockValidationRequestV5,
495 ) -> Result<(), ValidationApiError> {
496 let payload = ExecutionPayload::V3(request.request.execution_payload);
497 validate_message_against_payload(&request.request.message, &payload)?;
498
499 let block = self.recover_payload(ExecutionData {
500 payload,
501 sidecar: ExecutionPayloadSidecar::v4(
502 CancunPayloadFields {
503 parent_beacon_block_root: request.parent_beacon_block_root,
504 versioned_hashes: self
505 .validate_blobs_bundle_v2(request.request.blobs_bundle)?,
506 },
507 PraguePayloadFields {
508 requests: RequestsOrHash::Requests(
509 request.request.execution_requests.to_requests(),
510 ),
511 },
512 ),
513 })?;
514
515 let chain_spec = self.provider.chain_spec();
517 if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) {
518 let rlp_length = block.rlp_length();
519 if rlp_length > MAX_RLP_BLOCK_SIZE {
520 return Err(ValidationApiError::Consensus(ConsensusError::BlockTooLarge {
521 rlp_length,
522 max_rlp_length: MAX_RLP_BLOCK_SIZE,
523 }));
524 }
525 }
526
527 self.validate_message_against_block(
528 block,
529 request.request.message,
530 request.registered_gas_limit,
531 None,
532 )
533 .await
534 }
535
536 async fn validate_builder_submission_v6(
538 &self,
539 request: BuilderBlockValidationRequestV6,
540 ) -> Result<(), ValidationApiError> {
541 let payload = ExecutionPayload::V4(request.request.execution_payload);
542 validate_message_against_payload(&request.request.message, &payload)?;
543
544 let decoded_bal =
545 DecodedBal::from_rlp_bytes(payload.as_v4().unwrap().block_access_list.clone())
546 .map_err(ValidationApiError::InvalidBlockAccessList)?;
547
548 let block = self.recover_payload(ExecutionData {
549 payload,
550 sidecar: ExecutionPayloadSidecar::v4(
551 CancunPayloadFields {
552 parent_beacon_block_root: request.parent_beacon_block_root,
553 versioned_hashes: self
554 .validate_blobs_bundle_v2(request.request.blobs_bundle)?,
555 },
556 PraguePayloadFields {
557 requests: RequestsOrHash::Requests(
558 request.request.execution_requests.to_requests(),
559 ),
560 },
561 ),
562 })?;
563
564 let chain_spec = self.provider.chain_spec();
565 if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) {
566 let rlp_length = block.rlp_length();
567 if rlp_length > MAX_RLP_BLOCK_SIZE {
568 return Err(ValidationApiError::Consensus(ConsensusError::BlockTooLarge {
569 rlp_length,
570 max_rlp_length: MAX_RLP_BLOCK_SIZE,
571 }));
572 }
573 }
574
575 self.validate_message_against_block(
576 block,
577 request.request.message,
578 request.registered_gas_limit,
579 Some(decoded_bal),
580 )
581 .await
582 }
583}
584
585#[async_trait]
586impl<Provider, E, T> BlockSubmissionValidationApiServer for ValidationApi<Provider, E, T>
587where
588 Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
589 + ChainSpecProvider<ChainSpec: EthereumHardforks>
590 + StateProviderFactory
591 + Clone
592 + 'static,
593 E: ConfigureEvm + 'static,
594 T: PayloadTypes<ExecutionData = ExecutionData>,
595{
596 async fn validate_builder_submission_v1(
597 &self,
598 _request: BuilderBlockValidationRequest,
599 ) -> RpcResult<()> {
600 warn!(target: "rpc::flashbots", "Method `flashbots_validateBuilderSubmissionV1` is not supported");
601 Err(internal_rpc_err("unimplemented"))
602 }
603
604 async fn validate_builder_submission_v2(
605 &self,
606 _request: BuilderBlockValidationRequestV2,
607 ) -> RpcResult<()> {
608 warn!(target: "rpc::flashbots", "Method `flashbots_validateBuilderSubmissionV2` is not supported");
609 Err(internal_rpc_err("unimplemented"))
610 }
611
612 async fn validate_builder_submission_v3(
614 &self,
615 request: BuilderBlockValidationRequestV3,
616 ) -> RpcResult<()> {
617 let this = self.clone();
618 let validation = async move { Self::validate_builder_submission_v3(&this, request).await };
619 self.spawn_validation(validation).await
620 }
621
622 async fn validate_builder_submission_v4(
624 &self,
625 request: BuilderBlockValidationRequestV4,
626 ) -> RpcResult<()> {
627 let this = self.clone();
628 let validation = async move { Self::validate_builder_submission_v4(&this, request).await };
629 self.spawn_validation(validation).await
630 }
631
632 async fn validate_builder_submission_v5(
634 &self,
635 request: BuilderBlockValidationRequestV5,
636 ) -> RpcResult<()> {
637 let this = self.clone();
638 let validation = async move { Self::validate_builder_submission_v5(&this, request).await };
639 self.spawn_validation(validation).await
640 }
641
642 async fn validate_builder_submission_v6(
644 &self,
645 request: BuilderBlockValidationRequestV6,
646 ) -> RpcResult<()> {
647 let this = self.clone();
648 let validation = async move { Self::validate_builder_submission_v6(&this, request).await };
649 self.spawn_validation(validation).await
650 }
651}
652
653pub struct ValidationApiInner<Provider, E: ConfigureEvm, T: PayloadTypes> {
654 provider: Provider,
656 consensus: Arc<dyn FullConsensus<E::Primitives>>,
658 payload_validator:
660 Arc<dyn PayloadValidator<T, Block = <E::Primitives as NodePrimitives>::Block>>,
661 evm_config: E,
663 disallow: AddressSet,
665 validation_window: u64,
667 cached_state: RwLock<(B256, CachedReads)>,
672 validated_blobs: BlobValidationCache,
674 task_spawner: Runtime,
676 sender_recovery_cache: Option<SenderRecoveryCache>,
679 metrics: ValidationMetrics,
681}
682
683fn validate_message_against_payload(
685 message: &BidTrace,
686 payload: &ExecutionPayload,
687) -> Result<(), ValidationApiError> {
688 let payload = payload.as_v1();
689
690 if payload.block_hash != message.block_hash {
691 Err(ValidationApiError::BlockHashMismatch(GotExpected {
692 got: message.block_hash,
693 expected: payload.block_hash,
694 }))
695 } else if payload.parent_hash != message.parent_hash {
696 Err(ValidationApiError::ParentHashMismatch(GotExpected {
697 got: message.parent_hash,
698 expected: payload.parent_hash,
699 }))
700 } else if payload.gas_limit != message.gas_limit {
701 Err(ValidationApiError::GasLimitMismatch(GotExpected {
702 got: message.gas_limit,
703 expected: payload.gas_limit,
704 }))
705 } else if payload.gas_used != message.gas_used {
706 Err(ValidationApiError::GasUsedMismatch(GotExpected {
707 got: message.gas_used,
708 expected: payload.gas_used,
709 }))
710 } else {
711 Ok(())
712 }
713}
714
715fn hash_disallow_list(disallow: &AddressSet) -> String {
720 let mut sorted: Vec<_> = disallow.iter().collect();
721 sorted.sort_unstable(); let mut hasher = Sha256::new();
724 for addr in sorted {
725 hasher.update(addr.as_slice());
726 }
727
728 format!("{:x}", hasher.finalize())
729}
730
731impl<Provider, E: ConfigureEvm, T: PayloadTypes> fmt::Debug for ValidationApiInner<Provider, E, T> {
732 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
733 f.debug_struct("ValidationApiInner").finish_non_exhaustive()
734 }
735}
736
737#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
739pub struct ValidationApiConfig {
740 pub disallow: AddressSet,
742 pub validation_window: u64,
744}
745
746impl ValidationApiConfig {
747 pub const DEFAULT_VALIDATION_WINDOW: u64 = 3;
749}
750
751impl Default for ValidationApiConfig {
752 fn default() -> Self {
753 Self { disallow: Default::default(), validation_window: Self::DEFAULT_VALIDATION_WINDOW }
754 }
755}
756
757#[derive(Debug, thiserror::Error)]
759pub enum ValidationApiError {
760 #[error("block gas limit mismatch: {_0}")]
761 GasLimitMismatch(GotExpected<u64>),
762 #[error("block gas used mismatch: {_0}")]
763 GasUsedMismatch(GotExpected<u64>),
764 #[error("block parent hash mismatch: {_0}")]
765 ParentHashMismatch(GotExpected<B256>),
766 #[error("block hash mismatch: {_0}")]
767 BlockHashMismatch(GotExpected<B256>),
768 #[error("missing latest block in database")]
769 MissingLatestBlock,
770 #[error("parent block not found")]
771 MissingParentBlock,
772 #[error("block is too old, outside validation window")]
773 BlockTooOld,
774 #[error("could not verify proposer payment")]
775 ProposerPayment,
776 #[error("invalid blobs bundle")]
777 InvalidBlobsBundle,
778 #[error("invalid block access list: {_0}")]
779 InvalidBlockAccessList(alloy_rlp::Error),
780 #[error("block accesses blacklisted address: {_0}")]
781 Blacklist(Address),
782 #[error(transparent)]
783 Blob(#[from] BlobTransactionValidationError),
784 #[error(transparent)]
785 Consensus(#[from] ConsensusError),
786 #[error(transparent)]
787 Provider(#[from] ProviderError),
788 #[error(transparent)]
789 Execution(#[from] BlockExecutionError),
790 #[error(transparent)]
791 Payload(#[from] NewPayloadError),
792}
793
794impl From<ValidationApiError> for ErrorObject<'static> {
795 fn from(error: ValidationApiError) -> Self {
796 match error {
797 ValidationApiError::GasLimitMismatch(_) |
798 ValidationApiError::GasUsedMismatch(_) |
799 ValidationApiError::ParentHashMismatch(_) |
800 ValidationApiError::BlockHashMismatch(_) |
801 ValidationApiError::Blacklist(_) |
802 ValidationApiError::ProposerPayment |
803 ValidationApiError::InvalidBlobsBundle |
804 ValidationApiError::InvalidBlockAccessList(_) |
805 ValidationApiError::Blob(_) => invalid_params_rpc_err(error.to_string()),
806
807 ValidationApiError::Consensus(
808 error @ (ConsensusError::BlockAccessListCostMoreThanGasLimit(_) |
809 ConsensusError::BlockAccessListHashMismatch(_)),
810 ) => invalid_params_rpc_err(error.to_string()),
811 ValidationApiError::MissingLatestBlock |
812 ValidationApiError::MissingParentBlock |
813 ValidationApiError::BlockTooOld |
814 ValidationApiError::Consensus(_) |
815 ValidationApiError::Provider(_) => internal_rpc_err(error.to_string()),
816 ValidationApiError::Execution(err) => match err {
817 error @ BlockExecutionError::Validation(_) => {
818 invalid_params_rpc_err(error.to_string())
819 }
820 error @ BlockExecutionError::Internal(_) => internal_rpc_err(error.to_string()),
821 },
822 ValidationApiError::Payload(err) => match err {
823 error @ NewPayloadError::Eth(_) => invalid_params_rpc_err(error.to_string()),
824 error @ NewPayloadError::Other(_) => internal_rpc_err(error.to_string()),
825 },
826 }
827 }
828}
829
830#[derive(Metrics)]
832#[metrics(scope = "builder.validation")]
833pub(crate) struct ValidationMetrics {
834 pub(crate) disallow_size: Gauge,
836}
837
838#[cfg(test)]
839mod tests {
840 use super::{
841 hash_disallow_list, validate_message_against_payload, AddressSet, ValidationApi,
842 ValidationApiConfig, ValidationApiError,
843 };
844 use alloy_consensus::{BlockHeader, Header};
845 use alloy_primitives::{Address, B256, U256};
846 use alloy_rpc_types_beacon::relay::BidTrace;
847 use alloy_rpc_types_engine::{ExecutionData, ExecutionPayload, ExecutionPayloadV1};
848 use reth_consensus::noop::NoopConsensus;
849 use reth_engine_primitives::PayloadValidator;
850 use reth_ethereum_engine_primitives::EthPayloadTypes;
851 use reth_ethereum_primitives::Block;
852 use reth_evm_ethereum::EthEvmConfig;
853 use reth_execution_types::BlockExecutionOutput;
854 use reth_node_api::NewPayloadError;
855 use reth_primitives_traits::{RecoveredBlock, SealedBlock, SealedHeader};
856 use reth_provider::test_utils::MockEthProvider;
857 use reth_revm::db::{states::bundle_state::BundleState, AccountStatus, BundleAccount};
858 use reth_tasks::Runtime;
859 use revm::state::AccountInfo;
860 use std::sync::Arc;
861
862 fn test_execution_payload() -> ExecutionPayload {
863 ExecutionPayload::V1(ExecutionPayloadV1 {
864 parent_hash: B256::repeat_byte(0x11),
865 fee_recipient: Address::ZERO,
866 state_root: B256::ZERO,
867 receipts_root: B256::ZERO,
868 logs_bloom: Default::default(),
869 prev_randao: B256::ZERO,
870 block_number: 1,
871 gas_limit: 30_000_000,
872 gas_used: 15_000_000,
873 timestamp: 1,
874 extra_data: Default::default(),
875 base_fee_per_gas: Default::default(),
876 block_hash: B256::repeat_byte(0x22),
877 transactions: Default::default(),
878 })
879 }
880
881 fn matching_bid_trace(payload: &ExecutionPayload) -> BidTrace {
882 let payload = payload.as_v1();
883 BidTrace {
884 parent_hash: payload.parent_hash,
885 block_hash: payload.block_hash,
886 gas_limit: payload.gas_limit,
887 gas_used: payload.gas_used,
888 ..Default::default()
889 }
890 }
891
892 #[test]
893 fn test_validate_message_against_payload_block_hash_mismatch() {
894 let payload = test_execution_payload();
895 let mut message = matching_bid_trace(&payload);
896 message.block_hash = B256::repeat_byte(0x33);
897
898 let err = validate_message_against_payload(&message, &payload).unwrap_err();
899 let ValidationApiError::BlockHashMismatch(mismatch) = err else {
900 panic!("unexpected error: {err}")
901 };
902 assert_eq!(mismatch.got, message.block_hash);
903 assert_eq!(mismatch.expected, payload.block_hash());
904 }
905
906 #[test]
907 fn test_validate_message_against_payload_parent_hash_mismatch() {
908 let payload = test_execution_payload();
909 let mut message = matching_bid_trace(&payload);
910 message.parent_hash = B256::repeat_byte(0x33);
911
912 let err = validate_message_against_payload(&message, &payload).unwrap_err();
913 let ValidationApiError::ParentHashMismatch(mismatch) = err else {
914 panic!("unexpected error: {err}")
915 };
916 assert_eq!(mismatch.got, message.parent_hash);
917 assert_eq!(mismatch.expected, payload.parent_hash());
918 }
919
920 #[test]
921 fn test_validate_message_against_payload_gas_limit_mismatch() {
922 let payload = test_execution_payload();
923 let mut message = matching_bid_trace(&payload);
924 message.gas_limit += 1;
925
926 let err = validate_message_against_payload(&message, &payload).unwrap_err();
927 let ValidationApiError::GasLimitMismatch(mismatch) = err else {
928 panic!("unexpected error: {err}")
929 };
930 assert_eq!(mismatch.got, message.gas_limit);
931 assert_eq!(mismatch.expected, payload.gas_limit());
932 }
933
934 #[test]
935 fn test_validate_message_against_payload_gas_used_mismatch() {
936 let payload = test_execution_payload();
937 let mut message = matching_bid_trace(&payload);
938 message.gas_used += 1;
939
940 let err = validate_message_against_payload(&message, &payload).unwrap_err();
941 let ValidationApiError::GasUsedMismatch(mismatch) = err else {
942 panic!("unexpected error: {err}")
943 };
944 assert_eq!(mismatch.got, message.gas_used);
945 assert_eq!(mismatch.expected, payload.as_v1().gas_used);
946 }
947
948 #[test]
949 fn test_hash_disallow_list_deterministic() {
950 let mut addresses = AddressSet::default();
951 addresses.insert(Address::from([1u8; 20]));
952 addresses.insert(Address::from([2u8; 20]));
953
954 let hash1 = hash_disallow_list(&addresses);
955 let hash2 = hash_disallow_list(&addresses);
956
957 assert_eq!(hash1, hash2);
958 }
959
960 #[test]
961 fn test_hash_disallow_list_different_content() {
962 let mut addresses1 = AddressSet::default();
963 addresses1.insert(Address::from([1u8; 20]));
964
965 let mut addresses2 = AddressSet::default();
966 addresses2.insert(Address::from([2u8; 20]));
967
968 let hash1 = hash_disallow_list(&addresses1);
969 let hash2 = hash_disallow_list(&addresses2);
970
971 assert_ne!(hash1, hash2);
972 }
973
974 #[test]
975 fn test_hash_disallow_list_order_independent() {
976 let mut addresses1 = AddressSet::default();
977 addresses1.insert(Address::from([1u8; 20]));
978 addresses1.insert(Address::from([2u8; 20]));
979
980 let mut addresses2 = AddressSet::default();
981 addresses2.insert(Address::from([2u8; 20])); addresses2.insert(Address::from([1u8; 20]));
983
984 let hash1 = hash_disallow_list(&addresses1);
985 let hash2 = hash_disallow_list(&addresses2);
986
987 assert_eq!(hash1, hash2);
988 }
989
990 #[test]
991 fn test_disallow_list_hash_rbuilder_parity() {
993 let json = r#"["0x05E0b5B40B7b66098C2161A5EE11C5740A3A7C45","0x01e2919679362dFBC9ee1644Ba9C6da6D6245BB1","0x03893a7c7463AE47D46bc7f091665f1893656003","0x04DBA1194ee10112fE6C3207C0687DEf0e78baCf"]"#;
994 let blocklist: Vec<Address> = serde_json::from_str(json).unwrap();
995 let blocklist: AddressSet = blocklist.into_iter().collect();
996 let expected_hash = "ee14e9d115e182f61871a5a385ab2f32ecf434f3b17bdbacc71044810d89e608";
997 let hash = hash_disallow_list(&blocklist);
998 assert_eq!(expected_hash, hash);
999 }
1000
1001 #[derive(Debug)]
1004 struct UnusedPayloadValidator;
1005
1006 impl PayloadValidator<EthPayloadTypes> for UnusedPayloadValidator {
1007 type Block = Block;
1008
1009 fn convert_payload_to_block(
1010 &self,
1011 _payload: ExecutionData,
1012 ) -> Result<SealedBlock<Self::Block>, NewPayloadError> {
1013 unimplemented!()
1014 }
1015 }
1016
1017 #[derive(Debug)]
1018 struct SpecializedPayloadValidator;
1019
1020 impl PayloadValidator<EthPayloadTypes> for SpecializedPayloadValidator {
1021 type Block = Block;
1022
1023 fn convert_payload_to_block(
1024 &self,
1025 _payload: ExecutionData,
1026 ) -> Result<SealedBlock<Self::Block>, NewPayloadError> {
1027 panic!("the specialized recovery method must be used without a cache")
1028 }
1029
1030 fn ensure_well_formed_payload(
1031 &self,
1032 _payload: ExecutionData,
1033 ) -> Result<RecoveredBlock<Self::Block>, NewPayloadError> {
1034 Ok(SealedBlock::seal_slow(Block::default()).try_recover().unwrap())
1035 }
1036 }
1037
1038 #[test]
1039 fn recover_payload_without_cache_uses_validator_override() {
1040 let api = ValidationApi::new(
1041 MockEthProvider::default(),
1042 NoopConsensus::arc(),
1043 EthEvmConfig::mainnet(),
1044 ValidationApiConfig::default(),
1045 Runtime::test(),
1046 Arc::new(SpecializedPayloadValidator),
1047 None,
1048 );
1049
1050 api.recover_payload(ExecutionData {
1051 payload: test_execution_payload(),
1052 sidecar: Default::default(),
1053 })
1054 .unwrap();
1055 }
1056
1057 fn test_validation_api(
1058 provider: MockEthProvider,
1059 ) -> ValidationApi<MockEthProvider, EthEvmConfig, EthPayloadTypes> {
1060 ValidationApi::new(
1061 provider,
1062 NoopConsensus::arc(),
1063 EthEvmConfig::mainnet(),
1064 ValidationApiConfig::default(),
1065 Runtime::test(),
1066 Arc::new(UnusedPayloadValidator),
1067 None,
1068 )
1069 }
1070
1071 fn payment_free_submission() -> (MockEthProvider, RecoveredBlock<Block>, BidTrace) {
1074 let provider = MockEthProvider::default();
1075
1076 let parent = Header { gas_limit: 30_000_000, ..Default::default() };
1077 let parent = SealedHeader::seal_slow(parent);
1078 provider.add_block(
1079 parent.hash(),
1080 Block { header: parent.clone_header(), body: Default::default() },
1081 );
1082
1083 let header = Header {
1084 parent_hash: parent.hash(),
1085 number: parent.number() + 1,
1086 gas_limit: parent.gas_limit(),
1087 timestamp: parent.timestamp() + 12,
1088 ..Default::default()
1089 };
1090 let block = SealedBlock::seal_slow(Block { header, body: Default::default() })
1091 .try_recover()
1092 .unwrap();
1093 provider.state_roots.lock().push(block.state_root());
1094
1095 let message = BidTrace {
1096 parent_hash: block.parent_hash(),
1097 block_hash: block.hash(),
1098 gas_limit: block.gas_limit(),
1099 gas_used: block.gas_used(),
1100 proposer_fee_recipient: Address::repeat_byte(0x42),
1101 value: U256::from(1_000_000_000_000_000_000u64),
1102 ..Default::default()
1103 };
1104
1105 (provider, block, message)
1106 }
1107
1108 #[tokio::test]
1109 async fn test_payment_check_runs_for_a_nonzero_bid() {
1110 let (provider, block, message) = payment_free_submission();
1111 let registered_gas_limit = block.gas_limit();
1112
1113 let err = test_validation_api(provider)
1114 .validate_message_against_block(block, message, registered_gas_limit, None)
1115 .await
1116 .unwrap_err();
1117
1118 assert!(matches!(err, ValidationApiError::ProposerPayment), "unexpected error: {err}");
1119 }
1120
1121 #[tokio::test]
1122 async fn test_zero_value_bid_skips_the_payment_check() {
1123 let (provider, block, mut message) = payment_free_submission();
1124 let registered_gas_limit = block.gas_limit();
1125 message.value = U256::ZERO;
1126
1127 test_validation_api(provider)
1128 .validate_message_against_block(block, message, registered_gas_limit, None)
1129 .await
1130 .unwrap();
1131 }
1132
1133 #[test]
1138 fn test_zero_value_bid_is_accepted_when_the_fee_recipient_spends() {
1139 let (provider, block, mut message) = payment_free_submission();
1140 message.value = U256::ZERO;
1141
1142 let output = BlockExecutionOutput {
1143 result: Default::default(),
1144 state: fee_recipient_spent(message.proposer_fee_recipient),
1145 };
1146
1147 test_validation_api(provider)
1148 .ensure_payment(block.sealed_block(), &output, &message)
1149 .unwrap();
1150 }
1151
1152 fn fee_recipient_spent(address: Address) -> BundleState {
1154 let balance =
1155 |wei: u64| Some(AccountInfo { balance: U256::from(wei), ..Default::default() });
1156
1157 let mut state = BundleState::default();
1158 state.state.insert(
1159 address,
1160 BundleAccount {
1161 original_info: balance(1_000),
1162 info: balance(999),
1163 storage: Default::default(),
1164 status: AccountStatus::Changed,
1165 },
1166 );
1167 state
1168 }
1169}