1use alloy_consensus::{
2 BlobTransactionValidationError, BlockHeader, EnvKzgSettings, Transaction, TxReceipt,
3};
4use alloy_eips::{eip4844::kzg_to_versioned_hash, eip7685::RequestsOrHash};
5use alloy_rpc_types_beacon::relay::{
6 BidTrace, BuilderBlockValidationRequest, BuilderBlockValidationRequestV2,
7 BuilderBlockValidationRequestV3, BuilderBlockValidationRequestV4,
8};
9use alloy_rpc_types_engine::{
10 BlobsBundleV1, CancunPayloadFields, ExecutionData, ExecutionPayload, ExecutionPayloadSidecar,
11 PraguePayloadFields,
12};
13use async_trait::async_trait;
14use core::fmt;
15use jsonrpsee::core::RpcResult;
16use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
17use reth_consensus::{Consensus, FullConsensus};
18use reth_engine_primitives::PayloadValidator;
19use reth_errors::{BlockExecutionError, ConsensusError, ProviderError};
20use reth_evm::execute::{BlockExecutorProvider, Executor};
21use reth_metrics::{metrics, metrics::Gauge, Metrics};
22use reth_node_api::NewPayloadError;
23use reth_primitives_traits::{
24 constants::GAS_LIMIT_BOUND_DIVISOR, BlockBody, GotExpected, NodePrimitives, RecoveredBlock,
25 SealedBlock, SealedHeaderFor,
26};
27use reth_provider::{BlockExecutionOutput, BlockReaderIdExt, StateProviderFactory};
28use reth_revm::{cached::CachedReads, database::StateProviderDatabase};
29use reth_rpc_api::BlockSubmissionValidationApiServer;
30use reth_rpc_server_types::result::internal_rpc_err;
31use reth_tasks::TaskSpawner;
32use revm_primitives::{Address, B256, U256};
33use serde::{Deserialize, Serialize};
34use std::{collections::HashSet, sync::Arc};
35use tokio::sync::{oneshot, RwLock};
36
37#[derive(Clone, Debug, derive_more::Deref)]
39pub struct ValidationApi<Provider, E: BlockExecutorProvider> {
40 #[deref]
41 inner: Arc<ValidationApiInner<Provider, E>>,
42}
43
44impl<Provider, E> ValidationApi<Provider, E>
45where
46 E: BlockExecutorProvider,
47{
48 pub fn new(
50 provider: Provider,
51 consensus: Arc<dyn FullConsensus<E::Primitives, Error = ConsensusError>>,
52 executor_provider: E,
53 config: ValidationApiConfig,
54 task_spawner: Box<dyn TaskSpawner>,
55 payload_validator: Arc<
56 dyn PayloadValidator<
57 Block = <E::Primitives as NodePrimitives>::Block,
58 ExecutionData = ExecutionData,
59 >,
60 >,
61 ) -> Self {
62 let ValidationApiConfig { disallow, validation_window } = config;
63
64 let inner = Arc::new(ValidationApiInner {
65 provider,
66 consensus,
67 payload_validator,
68 executor_provider,
69 disallow,
70 validation_window,
71 cached_state: Default::default(),
72 task_spawner,
73 metrics: Default::default(),
74 });
75
76 inner.metrics.disallow_size.set(inner.disallow.len() as f64);
77 Self { inner }
78 }
79
80 async fn cached_reads(&self, head: B256) -> CachedReads {
82 let cache = self.inner.cached_state.read().await;
83 if cache.0 == head {
84 cache.1.clone()
85 } else {
86 Default::default()
87 }
88 }
89
90 async fn update_cached_reads(&self, head: B256, cached_state: CachedReads) {
92 let mut cache = self.inner.cached_state.write().await;
93 if cache.0 == head {
94 cache.1.extend(cached_state);
95 } else {
96 *cache = (head, cached_state)
97 }
98 }
99}
100
101impl<Provider, E> ValidationApi<Provider, E>
102where
103 Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
104 + ChainSpecProvider<ChainSpec: EthereumHardforks>
105 + StateProviderFactory
106 + 'static,
107 E: BlockExecutorProvider,
108{
109 pub async fn validate_message_against_block(
111 &self,
112 block: RecoveredBlock<<E::Primitives as NodePrimitives>::Block>,
113 message: BidTrace,
114 registered_gas_limit: u64,
115 ) -> Result<(), ValidationApiError> {
116 self.validate_message_against_header(block.sealed_header(), &message)?;
117
118 self.consensus.validate_header_with_total_difficulty(block.sealed_header(), U256::MAX)?;
119 self.consensus.validate_header(block.sealed_header())?;
120 self.consensus.validate_block_pre_execution(block.sealed_block())?;
121
122 if !self.disallow.is_empty() {
123 if self.disallow.contains(&block.beneficiary()) {
124 return Err(ValidationApiError::Blacklist(block.beneficiary()))
125 }
126 if self.disallow.contains(&message.proposer_fee_recipient) {
127 return Err(ValidationApiError::Blacklist(message.proposer_fee_recipient))
128 }
129 for (sender, tx) in block.senders_iter().zip(block.body().transactions()) {
130 if self.disallow.contains(sender) {
131 return Err(ValidationApiError::Blacklist(*sender))
132 }
133 if let Some(to) = tx.to() {
134 if self.disallow.contains(&to) {
135 return Err(ValidationApiError::Blacklist(to))
136 }
137 }
138 }
139 }
140
141 let latest_header =
142 self.provider.latest_header()?.ok_or_else(|| ValidationApiError::MissingLatestBlock)?;
143
144 let parent_header = if block.parent_hash() == latest_header.hash() {
145 latest_header
146 } else {
147 let parent_header = self
149 .provider
150 .sealed_header_by_hash(block.parent_hash())?
151 .ok_or_else(|| ValidationApiError::MissingParentBlock)?;
152
153 if latest_header.number().saturating_sub(parent_header.number()) >
154 self.validation_window
155 {
156 return Err(ValidationApiError::BlockTooOld)
157 }
158 parent_header
159 };
160
161 self.consensus.validate_header_against_parent(block.sealed_header(), &parent_header)?;
162 self.validate_gas_limit(registered_gas_limit, &parent_header, block.sealed_header())?;
163 let parent_header_hash = parent_header.hash();
164 let state_provider = self.provider.state_by_block_hash(parent_header_hash)?;
165
166 let mut request_cache = self.cached_reads(parent_header_hash).await;
167
168 let cached_db = request_cache.as_db_mut(StateProviderDatabase::new(&state_provider));
169 let executor = self.executor_provider.executor(cached_db);
170
171 let mut accessed_blacklisted = None;
172 let output = executor.execute_with_state_closure(&block, |state| {
173 if !self.disallow.is_empty() {
174 for account in state.cache.accounts.keys() {
175 if self.disallow.contains(account) {
176 accessed_blacklisted = Some(*account);
177 }
178 }
179 }
180 })?;
181
182 self.update_cached_reads(parent_header_hash, request_cache).await;
184
185 if let Some(account) = accessed_blacklisted {
186 return Err(ValidationApiError::Blacklist(account))
187 }
188
189 self.consensus.validate_block_post_execution(&block, &output)?;
190
191 self.ensure_payment(&block, &output, &message)?;
192
193 let state_root =
194 state_provider.state_root(state_provider.hashed_post_state(&output.state))?;
195
196 if state_root != block.header().state_root() {
197 return Err(ConsensusError::BodyStateRootDiff(
198 GotExpected { got: state_root, expected: block.header().state_root() }.into(),
199 )
200 .into())
201 }
202
203 Ok(())
204 }
205
206 fn validate_message_against_header(
208 &self,
209 header: &SealedHeaderFor<E::Primitives>,
210 message: &BidTrace,
211 ) -> Result<(), ValidationApiError> {
212 if header.hash() != message.block_hash {
213 Err(ValidationApiError::BlockHashMismatch(GotExpected {
214 got: message.block_hash,
215 expected: header.hash(),
216 }))
217 } else if header.parent_hash() != message.parent_hash {
218 Err(ValidationApiError::ParentHashMismatch(GotExpected {
219 got: message.parent_hash,
220 expected: header.parent_hash(),
221 }))
222 } else if header.gas_limit() != message.gas_limit {
223 Err(ValidationApiError::GasLimitMismatch(GotExpected {
224 got: message.gas_limit,
225 expected: header.gas_limit(),
226 }))
227 } else if header.gas_used() != message.gas_used {
228 return Err(ValidationApiError::GasUsedMismatch(GotExpected {
229 got: message.gas_used,
230 expected: header.gas_used(),
231 }))
232 } else {
233 Ok(())
234 }
235 }
236
237 fn validate_gas_limit(
242 &self,
243 registered_gas_limit: u64,
244 parent_header: &SealedHeaderFor<E::Primitives>,
245 header: &SealedHeaderFor<E::Primitives>,
246 ) -> Result<(), ValidationApiError> {
247 let max_gas_limit =
248 parent_header.gas_limit() + parent_header.gas_limit() / GAS_LIMIT_BOUND_DIVISOR - 1;
249 let min_gas_limit =
250 parent_header.gas_limit() - parent_header.gas_limit() / GAS_LIMIT_BOUND_DIVISOR + 1;
251
252 let best_gas_limit =
253 std::cmp::max(min_gas_limit, std::cmp::min(max_gas_limit, registered_gas_limit));
254
255 if best_gas_limit != header.gas_limit() {
256 return Err(ValidationApiError::GasLimitMismatch(GotExpected {
257 got: header.gas_limit(),
258 expected: best_gas_limit,
259 }))
260 }
261
262 Ok(())
263 }
264
265 fn ensure_payment(
270 &self,
271 block: &SealedBlock<<E::Primitives as NodePrimitives>::Block>,
272 output: &BlockExecutionOutput<<E::Primitives as NodePrimitives>::Receipt>,
273 message: &BidTrace,
274 ) -> Result<(), ValidationApiError> {
275 let (mut balance_before, balance_after) = if let Some(acc) =
276 output.state.state.get(&message.proposer_fee_recipient)
277 {
278 let balance_before = acc.original_info.as_ref().map(|i| i.balance).unwrap_or_default();
279 let balance_after = acc.info.as_ref().map(|i| i.balance).unwrap_or_default();
280
281 (balance_before, balance_after)
282 } else {
283 (U256::ZERO, U256::ZERO)
286 };
287
288 if let Some(withdrawals) = block.body().withdrawals() {
289 for withdrawal in withdrawals {
290 if withdrawal.address == message.proposer_fee_recipient {
291 balance_before += withdrawal.amount_wei();
292 }
293 }
294 }
295
296 if balance_after >= balance_before + message.value {
297 return Ok(())
298 }
299
300 let (receipt, tx) = output
301 .receipts
302 .last()
303 .zip(block.body().transactions().last())
304 .ok_or(ValidationApiError::ProposerPayment)?;
305
306 if !receipt.status() {
307 return Err(ValidationApiError::ProposerPayment)
308 }
309
310 if tx.to() != Some(message.proposer_fee_recipient) {
311 return Err(ValidationApiError::ProposerPayment)
312 }
313
314 if tx.value() != message.value {
315 return Err(ValidationApiError::ProposerPayment)
316 }
317
318 if !tx.input().is_empty() {
319 return Err(ValidationApiError::ProposerPayment)
320 }
321
322 if let Some(block_base_fee) = block.header().base_fee_per_gas() {
323 if tx.effective_tip_per_gas(block_base_fee).unwrap_or_default() != 0 {
324 return Err(ValidationApiError::ProposerPayment)
325 }
326 }
327
328 Ok(())
329 }
330
331 pub fn validate_blobs_bundle(
333 &self,
334 mut blobs_bundle: BlobsBundleV1,
335 ) -> Result<Vec<B256>, ValidationApiError> {
336 if blobs_bundle.commitments.len() != blobs_bundle.proofs.len() ||
337 blobs_bundle.commitments.len() != blobs_bundle.blobs.len()
338 {
339 return Err(ValidationApiError::InvalidBlobsBundle)
340 }
341
342 let versioned_hashes = blobs_bundle
343 .commitments
344 .iter()
345 .map(|c| kzg_to_versioned_hash(c.as_slice()))
346 .collect::<Vec<_>>();
347
348 let sidecar = blobs_bundle.pop_sidecar(blobs_bundle.blobs.len());
349
350 sidecar.validate(&versioned_hashes, EnvKzgSettings::default().get())?;
351
352 Ok(versioned_hashes)
353 }
354
355 async fn validate_builder_submission_v3(
357 &self,
358 request: BuilderBlockValidationRequestV3,
359 ) -> Result<(), ValidationApiError> {
360 let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
361 payload: ExecutionPayload::V3(request.request.execution_payload),
362 sidecar: ExecutionPayloadSidecar::v3(CancunPayloadFields {
363 parent_beacon_block_root: request.parent_beacon_block_root,
364 versioned_hashes: self.validate_blobs_bundle(request.request.blobs_bundle)?,
365 }),
366 })?;
367
368 self.validate_message_against_block(
369 block,
370 request.request.message,
371 request.registered_gas_limit,
372 )
373 .await
374 }
375
376 async fn validate_builder_submission_v4(
378 &self,
379 request: BuilderBlockValidationRequestV4,
380 ) -> Result<(), ValidationApiError> {
381 let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
382 payload: ExecutionPayload::V3(request.request.execution_payload),
383 sidecar: ExecutionPayloadSidecar::v4(
384 CancunPayloadFields {
385 parent_beacon_block_root: request.parent_beacon_block_root,
386 versioned_hashes: self.validate_blobs_bundle(request.request.blobs_bundle)?,
387 },
388 PraguePayloadFields {
389 requests: RequestsOrHash::Requests(
390 request.request.execution_requests.to_requests(),
391 ),
392 },
393 ),
394 })?;
395
396 self.validate_message_against_block(
397 block,
398 request.request.message,
399 request.registered_gas_limit,
400 )
401 .await
402 }
403}
404
405#[async_trait]
406impl<Provider, E> BlockSubmissionValidationApiServer for ValidationApi<Provider, E>
407where
408 Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
409 + ChainSpecProvider<ChainSpec: EthereumHardforks>
410 + StateProviderFactory
411 + Clone
412 + 'static,
413 E: BlockExecutorProvider,
414{
415 async fn validate_builder_submission_v1(
416 &self,
417 _request: BuilderBlockValidationRequest,
418 ) -> RpcResult<()> {
419 Err(internal_rpc_err("unimplemented"))
420 }
421
422 async fn validate_builder_submission_v2(
423 &self,
424 _request: BuilderBlockValidationRequestV2,
425 ) -> RpcResult<()> {
426 Err(internal_rpc_err("unimplemented"))
427 }
428
429 async fn validate_builder_submission_v3(
431 &self,
432 request: BuilderBlockValidationRequestV3,
433 ) -> RpcResult<()> {
434 let this = self.clone();
435 let (tx, rx) = oneshot::channel();
436
437 self.task_spawner.spawn_blocking(Box::pin(async move {
438 let result = Self::validate_builder_submission_v3(&this, request)
439 .await
440 .map_err(|err| internal_rpc_err(err.to_string()));
441 let _ = tx.send(result);
442 }));
443
444 rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
445 }
446
447 async fn validate_builder_submission_v4(
449 &self,
450 request: BuilderBlockValidationRequestV4,
451 ) -> RpcResult<()> {
452 let this = self.clone();
453 let (tx, rx) = oneshot::channel();
454
455 self.task_spawner.spawn_blocking(Box::pin(async move {
456 let result = Self::validate_builder_submission_v4(&this, request)
457 .await
458 .map_err(|err| internal_rpc_err(err.to_string()));
459 let _ = tx.send(result);
460 }));
461
462 rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
463 }
464}
465
466pub struct ValidationApiInner<Provider, E: BlockExecutorProvider> {
467 provider: Provider,
469 consensus: Arc<dyn FullConsensus<E::Primitives, Error = ConsensusError>>,
471 payload_validator: Arc<
473 dyn PayloadValidator<
474 Block = <E::Primitives as NodePrimitives>::Block,
475 ExecutionData = ExecutionData,
476 >,
477 >,
478 executor_provider: E,
480 disallow: HashSet<Address>,
482 validation_window: u64,
484 cached_state: RwLock<(B256, CachedReads)>,
489 task_spawner: Box<dyn TaskSpawner>,
491 metrics: ValidationMetrics,
493}
494
495impl<Provider, E: BlockExecutorProvider> fmt::Debug for ValidationApiInner<Provider, E> {
496 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497 f.debug_struct("ValidationApiInner").finish_non_exhaustive()
498 }
499}
500
501#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
503pub struct ValidationApiConfig {
504 pub disallow: HashSet<Address>,
506 pub validation_window: u64,
508}
509
510impl ValidationApiConfig {
511 pub const DEFAULT_VALIDATION_WINDOW: u64 = 3;
513}
514
515impl Default for ValidationApiConfig {
516 fn default() -> Self {
517 Self { disallow: Default::default(), validation_window: Self::DEFAULT_VALIDATION_WINDOW }
518 }
519}
520
521#[derive(Debug, thiserror::Error)]
523pub enum ValidationApiError {
524 #[error("block gas limit mismatch: {_0}")]
525 GasLimitMismatch(GotExpected<u64>),
526 #[error("block gas used mismatch: {_0}")]
527 GasUsedMismatch(GotExpected<u64>),
528 #[error("block parent hash mismatch: {_0}")]
529 ParentHashMismatch(GotExpected<B256>),
530 #[error("block hash mismatch: {_0}")]
531 BlockHashMismatch(GotExpected<B256>),
532 #[error("missing latest block in database")]
533 MissingLatestBlock,
534 #[error("parent block not found")]
535 MissingParentBlock,
536 #[error("block is too old, outside validation window")]
537 BlockTooOld,
538 #[error("could not verify proposer payment")]
539 ProposerPayment,
540 #[error("invalid blobs bundle")]
541 InvalidBlobsBundle,
542 #[error("invalid transaction signature")]
544 InvalidTransactionSignature,
545 #[error("block accesses blacklisted address: {_0}")]
546 Blacklist(Address),
547 #[error(transparent)]
548 Blob(#[from] BlobTransactionValidationError),
549 #[error(transparent)]
550 Consensus(#[from] ConsensusError),
551 #[error(transparent)]
552 Provider(#[from] ProviderError),
553 #[error(transparent)]
554 Execution(#[from] BlockExecutionError),
555 #[error(transparent)]
556 Payload(#[from] NewPayloadError),
557}
558
559#[derive(Metrics)]
561#[metrics(scope = "builder.validation")]
562pub(crate) struct ValidationMetrics {
563 pub(crate) disallow_size: Gauge,
565}