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