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 core::fmt;
18use jsonrpsee::core::RpcResult;
19use jsonrpsee_types::error::ErrorObject;
20use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
21use reth_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::{
26 cached::CachedReads, database::StateProviderDatabase, execute::Executor, ConfigureEvm,
27 SenderRecoveryCache,
28};
29use reth_execution_types::BlockExecutionOutput;
30use reth_metrics::{
31 metrics,
32 metrics::{gauge, Gauge},
33 Metrics,
34};
35use reth_payload_primitives::{NewPayloadError, PayloadTypes};
36use reth_primitives_traits::{
37 block::error::SealedBlockRecoveryError, BlockBody, GotExpected, NodePrimitives, RecoveredBlock,
38 SealedBlock, SealedHeaderFor,
39};
40use reth_rpc_api::BlockSubmissionValidationApiServer;
41use reth_rpc_server_types::result::{internal_rpc_err, invalid_params_rpc_err};
42use reth_storage_api::{
43 BlockReaderIdExt, HashedPostStateProvider, StateProvider, StateProviderFactory,
44};
45use reth_tasks::Runtime;
46use serde::{Deserialize, Serialize};
47use sha2::{Digest, Sha256};
48use std::sync::Arc;
49use tokio::sync::{oneshot, RwLock};
50use tracing::warn;
51
52mod blob_cache;
53
54#[derive(Clone, Debug, derive_more::Deref)]
56pub struct ValidationApi<Provider, E: ConfigureEvm, T: PayloadTypes> {
57 #[deref]
58 inner: Arc<ValidationApiInner<Provider, E, T>>,
59}
60
61impl<Provider, E, T> ValidationApi<Provider, E, T>
62where
63 E: ConfigureEvm,
64 T: PayloadTypes,
65{
66 pub fn new(
71 provider: Provider,
72 consensus: Arc<dyn FullConsensus<E::Primitives>>,
73 evm_config: E,
74 config: ValidationApiConfig,
75 task_spawner: Runtime,
76 payload_validator: Arc<
77 dyn PayloadValidator<T, Block = <E::Primitives as NodePrimitives>::Block>,
78 >,
79 sender_recovery_cache: Option<SenderRecoveryCache>,
80 ) -> Self {
81 let ValidationApiConfig { disallow, validation_window } = config;
82
83 let inner = Arc::new(ValidationApiInner {
84 provider,
85 consensus,
86 payload_validator,
87 evm_config,
88 disallow,
89 validation_window,
90 cached_state: Default::default(),
91 validated_blobs: Default::default(),
92 task_spawner,
93 sender_recovery_cache,
94 metrics: Default::default(),
95 });
96
97 inner.metrics.disallow_size.set(inner.disallow.len() as f64);
98
99 let disallow_hash = hash_disallow_list(&inner.disallow);
100 let hash_gauge = gauge!("builder_validation_disallow_hash", "hash" => disallow_hash);
101 hash_gauge.set(1.0);
102
103 Self { inner }
104 }
105
106 async fn cached_reads(&self, head: B256) -> CachedReads {
108 let cache = self.inner.cached_state.read().await;
109 if cache.0 == head {
110 cache.1.clone()
111 } else {
112 Default::default()
113 }
114 }
115
116 async fn update_cached_reads(&self, head: B256, cached_state: CachedReads) {
118 let mut cache = self.inner.cached_state.write().await;
119 if cache.0 == head {
120 cache.1.extend(cached_state);
121 } else {
122 *cache = (head, cached_state)
123 }
124 }
125
126 async fn spawn_validation<F>(&self, validation: F) -> RpcResult<()>
131 where
132 F: Future<Output = Result<(), ValidationApiError>> + Send + 'static,
133 {
134 let (mut tx, rx) = oneshot::channel();
135
136 self.task_spawner.spawn_blocking_task(async move {
137 let result = tokio::select! {
138 biased;
139 _ = tx.closed() => return,
140 result = validation => result.map_err(ErrorObject::from),
141 };
142 let _ = tx.send(result);
143 });
144
145 rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
146 }
147}
148
149impl<Provider, E, T> ValidationApi<Provider, E, T>
150where
151 Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
152 + ChainSpecProvider<ChainSpec: EthereumHardforks>
153 + StateProviderFactory
154 + 'static,
155 E: ConfigureEvm + 'static,
156 T: PayloadTypes<ExecutionData = ExecutionData>,
157{
158 pub async fn validate_message_against_block(
160 &self,
161 block: RecoveredBlock<<E::Primitives as NodePrimitives>::Block>,
162 message: BidTrace,
163 registered_gas_limit: u64,
164 decoded_bal: Option<DecodedBal>,
165 ) -> Result<(), ValidationApiError> {
166 self.validate_message_against_header(block.sealed_header(), &message)?;
167
168 self.consensus.validate_header(block.sealed_header())?;
169 self.consensus.validate_block_pre_execution(block.sealed_block())?;
170
171 if !self.disallow.is_empty() {
172 if self.disallow.contains(&block.beneficiary()) {
173 return Err(ValidationApiError::Blacklist(block.beneficiary()))
174 }
175 if self.disallow.contains(&message.proposer_fee_recipient) {
176 return Err(ValidationApiError::Blacklist(message.proposer_fee_recipient))
177 }
178 for (sender, tx) in block.senders_iter().zip(block.body().transactions()) {
179 if self.disallow.contains(sender) {
180 return Err(ValidationApiError::Blacklist(*sender))
181 }
182 if let Some(to) = tx.to() &&
183 self.disallow.contains(&to)
184 {
185 return Err(ValidationApiError::Blacklist(to))
186 }
187 }
188 }
189
190 let latest_header =
191 self.provider.latest_header()?.ok_or_else(|| ValidationApiError::MissingLatestBlock)?;
192
193 let parent_header = if block.parent_hash() == latest_header.hash() {
194 latest_header
195 } else {
196 let parent_header = self
198 .provider
199 .sealed_header_by_hash(block.parent_hash())?
200 .ok_or_else(|| ValidationApiError::MissingParentBlock)?;
201
202 if latest_header.number().saturating_sub(parent_header.number()) >
203 self.validation_window
204 {
205 return Err(ValidationApiError::BlockTooOld)
206 }
207 parent_header
208 };
209
210 self.consensus.validate_header_against_parent(block.sealed_header(), &parent_header)?;
211 parent_header.validate_gas_limit(registered_gas_limit, block.gas_limit()).map_err(
212 |err| {
213 ValidationApiError::GasLimitMismatch(GotExpected {
214 got: err.got,
215 expected: err.expected,
216 })
217 },
218 )?;
219
220 if let Some(decoded_bal) = decoded_bal {
222 decoded_bal
223 .as_bal()
224 .validate_gas_limit(block.gas_limit())
225 .map_err(ConsensusError::from)?;
226 }
227
228 let parent_header_hash = parent_header.hash();
229 let state_provider = self.provider.state_by_block_hash(parent_header_hash)?;
230
231 let mut request_cache = self.cached_reads(parent_header_hash).await;
232
233 let (output, block_access_list_hash) = {
234 let cached_db = request_cache
235 .as_db_mut(StateProviderDatabase::new((&state_provider).into_evm_state_provider()));
236 let mut executor = self.evm_config.batch_executor(cached_db);
237
238 let result = executor.execute_one(&block)?;
239
240 let block_access_list_hash =
245 executor.take_bal().map(|bal| compute_block_access_list_hash(&bal));
246
247 let state = executor.into_state();
248 if !self.disallow.is_empty() {
249 for account in request_cache.accounts.keys().chain(state.state.keys()) {
252 if self.disallow.contains(account) {
253 return Err(ValidationApiError::Blacklist(*account))
254 }
255 }
256 }
257
258 (BlockExecutionOutput { state, 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.account_state(&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
585impl<Provider, E, T> BlockSubmissionValidationApiServer for ValidationApi<Provider, E, T>
586where
587 Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
588 + ChainSpecProvider<ChainSpec: EthereumHardforks>
589 + StateProviderFactory
590 + Clone
591 + 'static,
592 E: ConfigureEvm + 'static,
593 T: PayloadTypes<ExecutionData = ExecutionData>,
594{
595 async fn validate_builder_submission_v1(
596 &self,
597 _request: BuilderBlockValidationRequest,
598 ) -> RpcResult<()> {
599 warn!(target: "rpc::flashbots", "Method `flashbots_validateBuilderSubmissionV1` is not supported");
600 Err(internal_rpc_err("unimplemented"))
601 }
602
603 async fn validate_builder_submission_v2(
604 &self,
605 _request: BuilderBlockValidationRequestV2,
606 ) -> RpcResult<()> {
607 warn!(target: "rpc::flashbots", "Method `flashbots_validateBuilderSubmissionV2` is not supported");
608 Err(internal_rpc_err("unimplemented"))
609 }
610
611 async fn validate_builder_submission_v3(
613 &self,
614 request: BuilderBlockValidationRequestV3,
615 ) -> RpcResult<()> {
616 let this = self.clone();
617 let validation = async move { Self::validate_builder_submission_v3(&this, request).await };
618 self.spawn_validation(validation).await
619 }
620
621 async fn validate_builder_submission_v4(
623 &self,
624 request: BuilderBlockValidationRequestV4,
625 ) -> RpcResult<()> {
626 let this = self.clone();
627 let validation = async move { Self::validate_builder_submission_v4(&this, request).await };
628 self.spawn_validation(validation).await
629 }
630
631 async fn validate_builder_submission_v5(
633 &self,
634 request: BuilderBlockValidationRequestV5,
635 ) -> RpcResult<()> {
636 let this = self.clone();
637 let validation = async move { Self::validate_builder_submission_v5(&this, request).await };
638 self.spawn_validation(validation).await
639 }
640
641 async fn validate_builder_submission_v6(
643 &self,
644 request: BuilderBlockValidationRequestV6,
645 ) -> RpcResult<()> {
646 let this = self.clone();
647 let validation = async move { Self::validate_builder_submission_v6(&this, request).await };
648 self.spawn_validation(validation).await
649 }
650}
651
652pub struct ValidationApiInner<Provider, E: ConfigureEvm, T: PayloadTypes> {
653 provider: Provider,
655 consensus: Arc<dyn FullConsensus<E::Primitives>>,
657 payload_validator:
659 Arc<dyn PayloadValidator<T, Block = <E::Primitives as NodePrimitives>::Block>>,
660 evm_config: E,
662 disallow: AddressSet,
664 validation_window: u64,
666 cached_state: RwLock<(B256, CachedReads)>,
671 validated_blobs: BlobValidationCache,
673 task_spawner: Runtime,
675 sender_recovery_cache: Option<SenderRecoveryCache>,
678 metrics: ValidationMetrics,
680}
681
682fn validate_message_against_payload(
684 message: &BidTrace,
685 payload: &ExecutionPayload,
686) -> Result<(), ValidationApiError> {
687 let payload = payload.as_v1();
688
689 if payload.block_hash != message.block_hash {
690 Err(ValidationApiError::BlockHashMismatch(GotExpected {
691 got: message.block_hash,
692 expected: payload.block_hash,
693 }))
694 } else if payload.parent_hash != message.parent_hash {
695 Err(ValidationApiError::ParentHashMismatch(GotExpected {
696 got: message.parent_hash,
697 expected: payload.parent_hash,
698 }))
699 } else if payload.gas_limit != message.gas_limit {
700 Err(ValidationApiError::GasLimitMismatch(GotExpected {
701 got: message.gas_limit,
702 expected: payload.gas_limit,
703 }))
704 } else if payload.gas_used != message.gas_used {
705 Err(ValidationApiError::GasUsedMismatch(GotExpected {
706 got: message.gas_used,
707 expected: payload.gas_used,
708 }))
709 } else {
710 Ok(())
711 }
712}
713
714fn hash_disallow_list(disallow: &AddressSet) -> String {
719 let mut sorted: Vec<_> = disallow.iter().collect();
720 sorted.sort_unstable(); let mut hasher = Sha256::new();
723 for addr in sorted {
724 hasher.update(addr.as_slice());
725 }
726
727 format!("{:x}", hasher.finalize())
728}
729
730impl<Provider, E: ConfigureEvm, T: PayloadTypes> fmt::Debug for ValidationApiInner<Provider, E, T> {
731 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732 f.debug_struct("ValidationApiInner").finish_non_exhaustive()
733 }
734}
735
736#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
738pub struct ValidationApiConfig {
739 pub disallow: AddressSet,
741 pub validation_window: u64,
743}
744
745impl ValidationApiConfig {
746 pub const DEFAULT_VALIDATION_WINDOW: u64 = 3;
748}
749
750impl Default for ValidationApiConfig {
751 fn default() -> Self {
752 Self { disallow: Default::default(), validation_window: Self::DEFAULT_VALIDATION_WINDOW }
753 }
754}
755
756#[derive(Debug, thiserror::Error)]
758pub enum ValidationApiError {
759 #[error("block gas limit mismatch: {_0}")]
760 GasLimitMismatch(GotExpected<u64>),
761 #[error("block gas used mismatch: {_0}")]
762 GasUsedMismatch(GotExpected<u64>),
763 #[error("block parent hash mismatch: {_0}")]
764 ParentHashMismatch(GotExpected<B256>),
765 #[error("block hash mismatch: {_0}")]
766 BlockHashMismatch(GotExpected<B256>),
767 #[error("missing latest block in database")]
768 MissingLatestBlock,
769 #[error("parent block not found")]
770 MissingParentBlock,
771 #[error("block is too old, outside validation window")]
772 BlockTooOld,
773 #[error("could not verify proposer payment")]
774 ProposerPayment,
775 #[error("invalid blobs bundle")]
776 InvalidBlobsBundle,
777 #[error("invalid block access list: {_0}")]
778 InvalidBlockAccessList(alloy_rlp::Error),
779 #[error("block accesses blacklisted address: {_0}")]
780 Blacklist(Address),
781 #[error(transparent)]
782 Blob(#[from] BlobTransactionValidationError),
783 #[error(transparent)]
784 Consensus(#[from] ConsensusError),
785 #[error(transparent)]
786 Provider(#[from] ProviderError),
787 #[error(transparent)]
788 Execution(#[from] BlockExecutionError),
789 #[error(transparent)]
790 Payload(#[from] NewPayloadError),
791}
792
793impl From<ValidationApiError> for ErrorObject<'static> {
794 fn from(error: ValidationApiError) -> Self {
795 match error {
796 ValidationApiError::GasLimitMismatch(_) |
797 ValidationApiError::GasUsedMismatch(_) |
798 ValidationApiError::ParentHashMismatch(_) |
799 ValidationApiError::BlockHashMismatch(_) |
800 ValidationApiError::Blacklist(_) |
801 ValidationApiError::ProposerPayment |
802 ValidationApiError::InvalidBlobsBundle |
803 ValidationApiError::InvalidBlockAccessList(_) |
804 ValidationApiError::Blob(_) |
805 ValidationApiError::Consensus(_) => invalid_params_rpc_err(error.to_string()),
806
807 ValidationApiError::MissingLatestBlock |
808 ValidationApiError::MissingParentBlock |
809 ValidationApiError::BlockTooOld |
810 ValidationApiError::Provider(_) => internal_rpc_err(error.to_string()),
811 ValidationApiError::Execution(err) => match err {
812 error @ BlockExecutionError::Validation(_) => {
813 invalid_params_rpc_err(error.to_string())
814 }
815 error @ BlockExecutionError::Internal(_) => internal_rpc_err(error.to_string()),
816 },
817 ValidationApiError::Payload(err) => match err {
818 error @ NewPayloadError::Eth(_) => invalid_params_rpc_err(error.to_string()),
819 error @ NewPayloadError::Other(_) => internal_rpc_err(error.to_string()),
820 },
821 }
822 }
823}
824
825#[derive(Metrics)]
827#[metrics(scope = "builder.validation")]
828pub(crate) struct ValidationMetrics {
829 pub(crate) disallow_size: Gauge,
831}
832
833#[cfg(test)]
834mod tests {
835 use super::{
836 hash_disallow_list, validate_message_against_payload, AddressSet, ValidationApi,
837 ValidationApiConfig, ValidationApiError,
838 };
839 use alloy_consensus::{BlockHeader, Header};
840 use alloy_primitives::{Address, B256, U256};
841 use alloy_rpc_types_beacon::relay::BidTrace;
842 use alloy_rpc_types_engine::{ExecutionData, ExecutionPayload, ExecutionPayloadV1};
843 use evm2::evm::AccountInfo;
844 use reth_consensus::noop::NoopConsensus;
845 use reth_engine_primitives::PayloadValidator;
846 use reth_errors::ConsensusError;
847 use reth_ethereum_engine_primitives::EthPayloadTypes;
848 use reth_ethereum_primitives::Block;
849 use reth_evm_ethereum::EthEvmConfig;
850 use reth_execution_types::BlockExecutionOutput;
851 use reth_payload_primitives::NewPayloadError;
852 use reth_primitives_traits::{RecoveredBlock, SealedBlock, SealedHeader};
853 use reth_provider::test_utils::MockEthProvider;
854 use reth_tasks::Runtime;
855 use reth_trie_common::bundle_state::{AccountStatus, BundleAccount, BundleState};
856 use std::sync::Arc;
857
858 fn test_execution_payload() -> ExecutionPayload {
859 ExecutionPayload::V1(ExecutionPayloadV1 {
860 parent_hash: B256::repeat_byte(0x11),
861 fee_recipient: Address::ZERO,
862 state_root: B256::ZERO,
863 receipts_root: B256::ZERO,
864 logs_bloom: Default::default(),
865 prev_randao: B256::ZERO,
866 block_number: 1,
867 gas_limit: 30_000_000,
868 gas_used: 15_000_000,
869 timestamp: 1,
870 extra_data: Default::default(),
871 base_fee_per_gas: Default::default(),
872 block_hash: B256::repeat_byte(0x22),
873 transactions: Default::default(),
874 })
875 }
876
877 fn matching_bid_trace(payload: &ExecutionPayload) -> BidTrace {
878 let payload = payload.as_v1();
879 BidTrace {
880 parent_hash: payload.parent_hash,
881 block_hash: payload.block_hash,
882 gas_limit: payload.gas_limit,
883 gas_used: payload.gas_used,
884 ..Default::default()
885 }
886 }
887
888 #[test]
889 fn test_validate_message_against_payload_block_hash_mismatch() {
890 let payload = test_execution_payload();
891 let mut message = matching_bid_trace(&payload);
892 message.block_hash = B256::repeat_byte(0x33);
893
894 let err = validate_message_against_payload(&message, &payload).unwrap_err();
895 let ValidationApiError::BlockHashMismatch(mismatch) = err else {
896 panic!("unexpected error: {err}")
897 };
898 assert_eq!(mismatch.got, message.block_hash);
899 assert_eq!(mismatch.expected, payload.block_hash());
900 }
901
902 #[test]
903 fn test_validate_message_against_payload_parent_hash_mismatch() {
904 let payload = test_execution_payload();
905 let mut message = matching_bid_trace(&payload);
906 message.parent_hash = B256::repeat_byte(0x33);
907
908 let err = validate_message_against_payload(&message, &payload).unwrap_err();
909 let ValidationApiError::ParentHashMismatch(mismatch) = err else {
910 panic!("unexpected error: {err}")
911 };
912 assert_eq!(mismatch.got, message.parent_hash);
913 assert_eq!(mismatch.expected, payload.parent_hash());
914 }
915
916 #[test]
917 fn test_validate_message_against_payload_gas_limit_mismatch() {
918 let payload = test_execution_payload();
919 let mut message = matching_bid_trace(&payload);
920 message.gas_limit += 1;
921
922 let err = validate_message_against_payload(&message, &payload).unwrap_err();
923 let ValidationApiError::GasLimitMismatch(mismatch) = err else {
924 panic!("unexpected error: {err}")
925 };
926 assert_eq!(mismatch.got, message.gas_limit);
927 assert_eq!(mismatch.expected, payload.gas_limit());
928 }
929
930 #[test]
931 fn test_validate_message_against_payload_gas_used_mismatch() {
932 let payload = test_execution_payload();
933 let mut message = matching_bid_trace(&payload);
934 message.gas_used += 1;
935
936 let err = validate_message_against_payload(&message, &payload).unwrap_err();
937 let ValidationApiError::GasUsedMismatch(mismatch) = err else {
938 panic!("unexpected error: {err}")
939 };
940 assert_eq!(mismatch.got, message.gas_used);
941 assert_eq!(mismatch.expected, payload.as_v1().gas_used);
942 }
943
944 #[test]
945 fn test_hash_disallow_list_deterministic() {
946 let mut addresses = AddressSet::default();
947 addresses.insert(Address::repeat_byte(1u8));
948 addresses.insert(Address::repeat_byte(2u8));
949
950 let hash1 = hash_disallow_list(&addresses);
951 let hash2 = hash_disallow_list(&addresses);
952
953 assert_eq!(hash1, hash2);
954 }
955
956 #[test]
957 fn test_hash_disallow_list_different_content() {
958 let mut addresses1 = AddressSet::default();
959 addresses1.insert(Address::repeat_byte(1u8));
960
961 let mut addresses2 = AddressSet::default();
962 addresses2.insert(Address::repeat_byte(2u8));
963
964 let hash1 = hash_disallow_list(&addresses1);
965 let hash2 = hash_disallow_list(&addresses2);
966
967 assert_ne!(hash1, hash2);
968 }
969
970 #[test]
971 fn test_hash_disallow_list_order_independent() {
972 let mut addresses1 = AddressSet::default();
973 addresses1.insert(Address::repeat_byte(1u8));
974 addresses1.insert(Address::repeat_byte(2u8));
975
976 let mut addresses2 = AddressSet::default();
977 addresses2.insert(Address::repeat_byte(2u8)); addresses2.insert(Address::repeat_byte(1u8));
979
980 let hash1 = hash_disallow_list(&addresses1);
981 let hash2 = hash_disallow_list(&addresses2);
982
983 assert_eq!(hash1, hash2);
984 }
985
986 #[test]
987 fn test_disallow_list_hash_rbuilder_parity() {
989 let json = r#"["0x05E0b5B40B7b66098C2161A5EE11C5740A3A7C45","0x01e2919679362dFBC9ee1644Ba9C6da6D6245BB1","0x03893a7c7463AE47D46bc7f091665f1893656003","0x04DBA1194ee10112fE6C3207C0687DEf0e78baCf"]"#;
990 let blocklist: Vec<Address> = serde_json::from_str(json).unwrap();
991 let blocklist: AddressSet = blocklist.into_iter().collect();
992 let expected_hash = "ee14e9d115e182f61871a5a385ab2f32ecf434f3b17bdbacc71044810d89e608";
993 let hash = hash_disallow_list(&blocklist);
994 assert_eq!(expected_hash, hash);
995 }
996
997 #[derive(Debug)]
1000 struct UnusedPayloadValidator;
1001
1002 impl PayloadValidator<EthPayloadTypes> for UnusedPayloadValidator {
1003 type Block = Block;
1004
1005 fn convert_payload_to_block(
1006 &self,
1007 _payload: ExecutionData,
1008 ) -> Result<SealedBlock<Self::Block>, NewPayloadError> {
1009 unimplemented!()
1010 }
1011 }
1012
1013 #[derive(Debug)]
1014 struct SpecializedPayloadValidator;
1015
1016 impl PayloadValidator<EthPayloadTypes> for SpecializedPayloadValidator {
1017 type Block = Block;
1018
1019 fn convert_payload_to_block(
1020 &self,
1021 _payload: ExecutionData,
1022 ) -> Result<SealedBlock<Self::Block>, NewPayloadError> {
1023 panic!("the specialized recovery method must be used without a cache")
1024 }
1025
1026 fn ensure_well_formed_payload(
1027 &self,
1028 _payload: ExecutionData,
1029 ) -> Result<RecoveredBlock<Self::Block>, NewPayloadError> {
1030 Ok(SealedBlock::seal_slow(Block::default()).try_recover().unwrap())
1031 }
1032 }
1033
1034 #[test]
1035 fn recover_payload_without_cache_uses_validator_override() {
1036 let api = ValidationApi::new(
1037 MockEthProvider::default(),
1038 NoopConsensus::arc(),
1039 EthEvmConfig::mainnet(),
1040 ValidationApiConfig::default(),
1041 Runtime::test(),
1042 Arc::new(SpecializedPayloadValidator),
1043 None,
1044 );
1045
1046 api.recover_payload(ExecutionData {
1047 payload: test_execution_payload(),
1048 sidecar: Default::default(),
1049 })
1050 .unwrap();
1051 }
1052
1053 fn test_validation_api(
1054 provider: MockEthProvider,
1055 ) -> ValidationApi<MockEthProvider, EthEvmConfig, EthPayloadTypes> {
1056 ValidationApi::new(
1057 provider,
1058 NoopConsensus::arc(),
1059 EthEvmConfig::mainnet(),
1060 ValidationApiConfig::default(),
1061 Runtime::test(),
1062 Arc::new(UnusedPayloadValidator),
1063 None,
1064 )
1065 }
1066
1067 fn payment_free_submission() -> (MockEthProvider, RecoveredBlock<Block>, BidTrace) {
1070 let provider = MockEthProvider::default();
1071
1072 let parent = Header { gas_limit: 30_000_000, ..Default::default() };
1073 let parent = SealedHeader::seal_slow(parent);
1074 provider.add_block(
1075 parent.hash(),
1076 Block { header: parent.clone_header(), body: Default::default() },
1077 );
1078
1079 let header = Header {
1080 parent_hash: parent.hash(),
1081 number: parent.number() + 1,
1082 gas_limit: parent.gas_limit(),
1083 timestamp: parent.timestamp() + 12,
1084 ..Default::default()
1085 };
1086 let block = SealedBlock::seal_slow(Block { header, body: Default::default() })
1087 .try_recover()
1088 .unwrap();
1089 provider.state_roots.lock().push(block.state_root());
1090
1091 let message = BidTrace {
1092 parent_hash: block.parent_hash(),
1093 block_hash: block.hash(),
1094 gas_limit: block.gas_limit(),
1095 gas_used: block.gas_used(),
1096 proposer_fee_recipient: Address::repeat_byte(0x42),
1097 value: U256::from(1_000_000_000_000_000_000u64),
1098 ..Default::default()
1099 };
1100
1101 (provider, block, message)
1102 }
1103
1104 #[tokio::test]
1105 async fn execution_invalid_submission_is_invalid_params() {
1106 let (provider, parent_block, mut message) = payment_free_submission();
1107 let transaction = reth_ethereum_primitives::TransactionSigned::new_unhashed(
1108 reth_ethereum_primitives::Transaction::Legacy(alloy_consensus::TxLegacy {
1109 gas_limit: 20_999,
1110 to: alloy_primitives::TxKind::Call(Address::with_last_byte(1)),
1111 ..Default::default()
1112 }),
1113 alloy_primitives::Signature::test_signature(),
1114 );
1115 let block = RecoveredBlock::new_unhashed(
1116 Block {
1117 header: parent_block.clone_header(),
1118 body: alloy_consensus::BlockBody {
1119 transactions: vec![transaction],
1120 ..Default::default()
1121 },
1122 },
1123 vec![Address::ZERO],
1124 );
1125 message.block_hash = block.hash();
1126 let error = test_validation_api(provider)
1127 .validate_message_against_block(block, message, parent_block.gas_limit(), None)
1128 .await
1129 .unwrap_err();
1130 assert!(
1131 matches!(
1132 error,
1133 ValidationApiError::Execution(reth_errors::BlockExecutionError::Validation(_))
1134 ),
1135 "{error}"
1136 );
1137 let error = jsonrpsee::types::ErrorObjectOwned::from(error);
1138 assert_eq!(error.code(), jsonrpsee::types::error::INVALID_PARAMS_CODE);
1139 }
1140
1141 #[tokio::test]
1142 async fn consensus_invalid_submission_is_invalid_params() {
1143 let (provider, block, mut message) = payment_free_submission();
1144 *provider.state_roots.lock() = vec![B256::repeat_byte(0xab)];
1147 message.value = U256::ZERO;
1150 let registered_gas_limit = block.gas_limit();
1151
1152 let error = test_validation_api(provider)
1153 .validate_message_against_block(block, message, registered_gas_limit, None)
1154 .await
1155 .unwrap_err();
1156 assert!(
1157 matches!(error, ValidationApiError::Consensus(ConsensusError::BodyStateRootDiff(_))),
1158 "{error}"
1159 );
1160 let error = jsonrpsee::types::ErrorObjectOwned::from(error);
1161 assert_eq!(error.code(), jsonrpsee::types::error::INVALID_PARAMS_CODE);
1162 }
1163
1164 #[test]
1165 fn node_fault_is_internal_error() {
1166 let error =
1167 jsonrpsee::types::ErrorObjectOwned::from(ValidationApiError::MissingParentBlock);
1168 assert_eq!(error.code(), jsonrpsee::types::error::INTERNAL_ERROR_CODE);
1169 }
1170
1171 #[tokio::test]
1172 async fn test_payment_check_runs_for_a_nonzero_bid() {
1173 let (provider, block, message) = payment_free_submission();
1174 let registered_gas_limit = block.gas_limit();
1175
1176 let err = test_validation_api(provider)
1177 .validate_message_against_block(block, message, registered_gas_limit, None)
1178 .await
1179 .unwrap_err();
1180
1181 assert!(matches!(err, ValidationApiError::ProposerPayment), "unexpected error: {err}");
1182 }
1183
1184 #[tokio::test]
1185 async fn test_zero_value_bid_skips_the_payment_check() {
1186 let (provider, block, mut message) = payment_free_submission();
1187 let registered_gas_limit = block.gas_limit();
1188 message.value = U256::ZERO;
1189
1190 test_validation_api(provider)
1191 .validate_message_against_block(block, message, registered_gas_limit, None)
1192 .await
1193 .unwrap();
1194 }
1195
1196 #[test]
1201 fn test_zero_value_bid_is_accepted_when_the_fee_recipient_spends() {
1202 let (provider, block, mut message) = payment_free_submission();
1203 message.value = U256::ZERO;
1204
1205 let output = BlockExecutionOutput {
1206 result: Default::default(),
1207 state: fee_recipient_spent(message.proposer_fee_recipient),
1208 };
1209
1210 test_validation_api(provider)
1211 .ensure_payment(block.sealed_block(), &output, &message)
1212 .unwrap();
1213 }
1214
1215 fn fee_recipient_spent(address: Address) -> BundleState {
1217 let balance =
1218 |wei: u64| Some(AccountInfo { balance: U256::from(wei), ..Default::default() });
1219
1220 let mut state = BundleState::default();
1221 state.state.insert(
1222 address,
1223 BundleAccount {
1224 original_info: balance(1_000),
1225 info: balance(999),
1226 storage: Default::default(),
1227 status: AccountStatus::Changed,
1228 },
1229 );
1230 state
1231 }
1232}