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