1#![doc(
4 html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5 html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6 issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
7)]
8#![cfg_attr(not(test), warn(unused_crate_dependencies))]
9#![cfg_attr(docsrs, feature(doc_cfg))]
10#![cfg_attr(not(feature = "std"), no_std)]
11
12extern crate alloc;
13
14use alloc::{fmt::Debug, sync::Arc};
15use alloy_consensus::{constants::MAXIMUM_EXTRA_DATA_SIZE, EMPTY_OMMER_ROOT_HASH};
16use alloy_eips::eip7840::BlobParams;
17use alloy_primitives::B256;
18use reth_chainspec::{EthChainSpec, EthereumHardforks};
19use reth_consensus::{
20 Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom, TransactionRoot,
21};
22use reth_consensus_common::validation::{
23 validate_4844_header_standalone, validate_against_parent_4844,
24 validate_against_parent_eip1559_base_fee, validate_against_parent_gas_limit,
25 validate_against_parent_hash_number, validate_against_parent_timestamp,
26 validate_block_pre_execution, validate_block_pre_execution_with_tx_root,
27 validate_body_against_header, validate_header_base_fee, validate_header_extra_data,
28 validate_header_gas,
29};
30use reth_execution_types::BlockExecutionResult;
31use reth_primitives_traits::{
32 Block, BlockHeader, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader,
33};
34
35mod validation;
36pub use validation::{
37 compare_receipts_root_and_logs_bloom, validate_block_post_execution, verify_receipts,
38};
39
40#[derive(Debug, Clone)]
44pub struct EthBeaconConsensus<ChainSpec> {
45 chain_spec: Arc<ChainSpec>,
47 max_extra_data_size: usize,
49 skip_gas_limit_ramp_check: bool,
51 skip_blob_gas_used_check: bool,
53 skip_requests_hash_check: bool,
55 allow_bal_hashes: bool,
57}
58
59impl<ChainSpec: EthChainSpec + EthereumHardforks> EthBeaconConsensus<ChainSpec> {
60 pub const fn new(chain_spec: Arc<ChainSpec>) -> Self {
62 Self {
63 chain_spec,
64 max_extra_data_size: MAXIMUM_EXTRA_DATA_SIZE,
65 skip_gas_limit_ramp_check: false,
66 skip_blob_gas_used_check: false,
67 skip_requests_hash_check: false,
68 allow_bal_hashes: false,
69 }
70 }
71
72 pub const fn max_extra_data_size(&self) -> usize {
74 self.max_extra_data_size
75 }
76
77 pub const fn with_max_extra_data_size(mut self, size: usize) -> Self {
79 self.max_extra_data_size = size;
80 self
81 }
82
83 pub const fn with_skip_gas_limit_ramp_check(mut self, skip: bool) -> Self {
85 self.skip_gas_limit_ramp_check = skip;
86 self
87 }
88
89 pub const fn with_skip_blob_gas_used_check(mut self, skip: bool) -> Self {
91 self.skip_blob_gas_used_check = skip;
92 self
93 }
94
95 pub const fn with_skip_requests_hash_check(mut self, skip: bool) -> Self {
97 self.skip_requests_hash_check = skip;
98 self
99 }
100
101 pub const fn with_allow_bal_hashes(mut self, allow: bool) -> Self {
103 self.allow_bal_hashes = allow;
104 self
105 }
106
107 pub const fn chain_spec(&self) -> &Arc<ChainSpec> {
109 &self.chain_spec
110 }
111}
112
113impl<ChainSpec, N> FullConsensus<N> for EthBeaconConsensus<ChainSpec>
114where
115 ChainSpec: Send + Sync + EthChainSpec<Header = N::BlockHeader> + EthereumHardforks + Debug,
116 N: NodePrimitives,
117{
118 fn validate_block_post_execution(
119 &self,
120 block: &RecoveredBlock<N::Block>,
121 result: &BlockExecutionResult<N::Receipt>,
122 receipt_root_bloom: Option<ReceiptRootBloom>,
123 block_access_list_hash: Option<B256>,
124 ) -> Result<(), ConsensusError> {
125 let res = validation::validate_block_post_execution_with_bal_hashes(
126 block,
127 &self.chain_spec,
128 result,
129 receipt_root_bloom,
130 block_access_list_hash,
131 self.allow_bal_hashes,
132 );
133
134 if self.skip_requests_hash_check &&
135 let Err(ConsensusError::BodyRequestsHashDiff(_)) = &res
136 {
137 return Ok(());
138 }
139
140 res
141 }
142}
143
144impl<B, ChainSpec> Consensus<B> for EthBeaconConsensus<ChainSpec>
145where
146 B: Block,
147 ChainSpec: EthChainSpec<Header = B::Header> + EthereumHardforks + Debug + Send + Sync,
148{
149 fn validate_body_against_header(
150 &self,
151 body: &B::Body,
152 header: &SealedHeader<B::Header>,
153 ) -> Result<(), ConsensusError> {
154 validate_body_against_header(body, header.header())
155 }
156
157 fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError> {
158 validate_block_pre_execution(block, &self.chain_spec)
159 }
160
161 fn validate_block_pre_execution_with_tx_root(
162 &self,
163 block: &SealedBlock<B>,
164 transaction_root: Option<TransactionRoot>,
165 ) -> Result<(), ConsensusError> {
166 validate_block_pre_execution_with_tx_root(block, &self.chain_spec, transaction_root)
167 }
168}
169
170impl<H, ChainSpec> HeaderValidator<H> for EthBeaconConsensus<ChainSpec>
171where
172 H: BlockHeader,
173 ChainSpec: EthChainSpec<Header = H> + EthereumHardforks + Debug + Send + Sync,
174{
175 fn validate_header(&self, header: &SealedHeader<H>) -> Result<(), ConsensusError> {
176 let header = header.header();
177 let is_post_merge = self.chain_spec.is_paris_active_at_block(header.number());
178
179 if is_post_merge {
180 if !header.difficulty().is_zero() {
181 return Err(ConsensusError::TheMergeDifficultyIsNotZero);
182 }
183
184 if !header.nonce().is_some_and(|nonce| nonce.is_zero()) {
185 return Err(ConsensusError::TheMergeNonceIsNotZero);
186 }
187
188 if header.ommers_hash() != EMPTY_OMMER_ROOT_HASH {
189 return Err(ConsensusError::TheMergeOmmerRootIsNotEmpty);
190 }
191 } else {
192 #[cfg(feature = "std")]
193 {
194 let present_timestamp = std::time::SystemTime::now()
195 .duration_since(std::time::SystemTime::UNIX_EPOCH)
196 .unwrap()
197 .as_secs();
198
199 if header.timestamp() >
200 present_timestamp + alloy_eips::merge::ALLOWED_FUTURE_BLOCK_TIME_SECONDS
201 {
202 return Err(ConsensusError::TimestampIsInFuture {
203 timestamp: header.timestamp(),
204 present_timestamp,
205 });
206 }
207 }
208 }
209 validate_header_extra_data(header, self.max_extra_data_size)?;
210 validate_header_gas(header)?;
211 validate_header_base_fee(header, &self.chain_spec)?;
212
213 if self.chain_spec.is_shanghai_active_at_timestamp(header.timestamp()) &&
215 header.withdrawals_root().is_none()
216 {
217 return Err(ConsensusError::WithdrawalsRootMissing)
218 } else if !self.chain_spec.is_shanghai_active_at_timestamp(header.timestamp()) &&
219 header.withdrawals_root().is_some()
220 {
221 return Err(ConsensusError::WithdrawalsRootUnexpected)
222 }
223
224 if self.chain_spec.is_cancun_active_at_timestamp(header.timestamp()) {
226 if !self.skip_blob_gas_used_check {
227 validate_4844_header_standalone(
228 header,
229 self.chain_spec
230 .blob_params_at_timestamp(header.timestamp())
231 .unwrap_or_else(BlobParams::cancun),
232 )?;
233 }
234 } else if header.blob_gas_used().is_some() {
235 return Err(ConsensusError::BlobGasUsedUnexpected)
236 } else if header.excess_blob_gas().is_some() {
237 return Err(ConsensusError::ExcessBlobGasUnexpected)
238 } else if header.parent_beacon_block_root().is_some() {
239 return Err(ConsensusError::ParentBeaconBlockRootUnexpected)
240 }
241
242 if self.chain_spec.is_prague_active_at_timestamp(header.timestamp()) {
243 if header.requests_hash().is_none() {
244 return Err(ConsensusError::RequestsHashMissing)
245 }
246 } else if header.requests_hash().is_some() {
247 return Err(ConsensusError::RequestsHashUnexpected)
248 }
249
250 if self.chain_spec.is_amsterdam_active_at_timestamp(header.timestamp()) {
251 if header.block_access_list_hash().is_none() {
252 return Err(ConsensusError::BlockAccessListHashMissing)
253 }
254 if header.slot_number().is_none() {
255 return Err(ConsensusError::SlotNumberMissing)
256 }
257 } else {
258 if header.block_access_list_hash().is_some() && !self.allow_bal_hashes {
259 return Err(ConsensusError::BlockAccessListHashUnexpected)
260 }
261 if header.slot_number().is_some() {
262 return Err(ConsensusError::SlotNumberUnexpected)
263 }
264 }
265
266 Ok(())
267 }
268
269 fn validate_header_against_parent(
270 &self,
271 header: &SealedHeader<H>,
272 parent: &SealedHeader<H>,
273 ) -> Result<(), ConsensusError> {
274 validate_against_parent_hash_number(header.header(), parent)?;
275
276 validate_against_parent_timestamp(header.header(), parent.header())?;
277
278 if !self.skip_gas_limit_ramp_check {
279 validate_against_parent_gas_limit(header, parent, &self.chain_spec)?;
280 }
281
282 validate_against_parent_eip1559_base_fee(
283 header.header(),
284 parent.header(),
285 &self.chain_spec,
286 )?;
287
288 if let Some(blob_params) = self.chain_spec.blob_params_at_timestamp(header.timestamp()) {
290 validate_against_parent_4844(header.header(), parent.header(), blob_params)?;
291 }
292
293 Ok(())
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use alloy_consensus::Header;
301 use alloy_eips::eip7685::EMPTY_REQUESTS_HASH;
302 use alloy_primitives::B256;
303 use reth_chainspec::{ChainSpec, ChainSpecBuilder};
304 use reth_consensus_common::validation::validate_against_parent_gas_limit;
305 use reth_ethereum_primitives::{Block as EthBlock, EthPrimitives, Receipt};
306 use reth_primitives_traits::{
307 constants::{GAS_LIMIT_BOUND_DIVISOR, MINIMUM_GAS_LIMIT},
308 proofs,
309 };
310
311 fn header_with_gas_limit(gas_limit: u64) -> SealedHeader {
312 let header = reth_primitives_traits::Header { gas_limit, ..Default::default() };
313 SealedHeader::new(header, B256::ZERO)
314 }
315
316 fn valid_prague_header() -> reth_primitives_traits::Header {
317 reth_primitives_traits::Header {
318 base_fee_per_gas: Some(1337),
319 withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
320 blob_gas_used: Some(0),
321 excess_blob_gas: Some(0),
322 parent_beacon_block_root: Some(B256::ZERO),
323 requests_hash: Some(EMPTY_REQUESTS_HASH),
324 ..Default::default()
325 }
326 }
327
328 fn prague_recovered_block_with_bal_hash(hash: B256) -> RecoveredBlock<EthBlock> {
329 let mut header = valid_prague_header();
330 header.block_access_list_hash = Some(hash);
331 RecoveredBlock::new_unhashed(EthBlock { header, body: Default::default() }, Vec::new())
332 }
333
334 #[test]
335 fn test_valid_gas_limit_increase() {
336 let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
337 let child = header_with_gas_limit(parent.gas_limit + 5);
338
339 assert!(validate_against_parent_gas_limit(
340 &child,
341 &parent,
342 &ChainSpec::<Header>::default()
343 )
344 .is_ok());
345 }
346
347 #[test]
348 fn test_gas_limit_below_minimum() {
349 let parent = header_with_gas_limit(MINIMUM_GAS_LIMIT);
350 let child = header_with_gas_limit(MINIMUM_GAS_LIMIT - 1);
351
352 assert!(matches!(
353 validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
354 ConsensusError::GasLimitInvalidMinimum { child_gas_limit }
355 if child_gas_limit == child.gas_limit
356 ));
357 }
358
359 #[test]
360 fn test_invalid_gas_limit_increase_exceeding_limit() {
361 let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
362 let child = header_with_gas_limit(
363 parent.gas_limit + parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR + 1,
364 );
365
366 assert!(matches!(
367 validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
368 ConsensusError::GasLimitInvalidIncrease { parent_gas_limit, child_gas_limit }
369 if parent_gas_limit == parent.gas_limit && child_gas_limit == child.gas_limit
370 ));
371 }
372
373 #[test]
374 fn test_valid_gas_limit_decrease_within_limit() {
375 let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
376 let child = header_with_gas_limit(parent.gas_limit - 5);
377
378 assert!(validate_against_parent_gas_limit(
379 &child,
380 &parent,
381 &ChainSpec::<Header>::default()
382 )
383 .is_ok());
384 }
385
386 #[test]
387 fn test_invalid_gas_limit_decrease_exceeding_limit() {
388 let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
389 let child = header_with_gas_limit(
390 parent.gas_limit - parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR - 1,
391 );
392
393 assert!(matches!(
394 validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
395 ConsensusError::GasLimitInvalidDecrease { parent_gas_limit, child_gas_limit }
396 if parent_gas_limit == parent.gas_limit && child_gas_limit == child.gas_limit
397 ));
398 }
399
400 #[test]
401 fn shanghai_block_zero_withdrawals() {
402 let chain_spec = Arc::new(ChainSpecBuilder::mainnet().shanghai_activated().build());
405
406 let header = reth_primitives_traits::Header {
407 base_fee_per_gas: Some(1337),
408 withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
409 ..Default::default()
410 };
411
412 assert!(EthBeaconConsensus::new(chain_spec)
413 .validate_header(&SealedHeader::seal_slow(header,))
414 .is_ok());
415 }
416
417 #[test]
418 fn prague_header_rejects_block_access_list_hash_before_amsterdam() {
419 let chain_spec = Arc::new(ChainSpecBuilder::mainnet().prague_activated().build());
420 let mut header = valid_prague_header();
421 header.block_access_list_hash = Some(B256::ZERO);
422
423 assert!(matches!(
424 EthBeaconConsensus::new(chain_spec)
425 .validate_header(&SealedHeader::seal_slow(header,))
426 .unwrap_err(),
427 ConsensusError::BlockAccessListHashUnexpected
428 ));
429 }
430
431 #[test]
432 fn prague_header_allows_block_access_list_hash_before_amsterdam() {
433 let chain_spec = Arc::new(ChainSpecBuilder::mainnet().prague_activated().build());
434 let mut header = valid_prague_header();
435 header.block_access_list_hash = Some(B256::ZERO);
436
437 assert!(EthBeaconConsensus::new(chain_spec)
438 .with_allow_bal_hashes(true)
439 .validate_header(&SealedHeader::seal_slow(header,))
440 .is_ok());
441 }
442
443 #[test]
444 fn prague_header_rejects_slot_number_before_amsterdam() {
445 let chain_spec = Arc::new(ChainSpecBuilder::mainnet().prague_activated().build());
446 let mut header = valid_prague_header();
447 header.slot_number = Some(0);
448
449 assert!(matches!(
450 EthBeaconConsensus::new(chain_spec)
451 .validate_header(&SealedHeader::seal_slow(header,))
452 .unwrap_err(),
453 ConsensusError::SlotNumberUnexpected
454 ));
455 }
456
457 #[test]
458 fn prague_header_rejects_slot_number_with_allowed_bal_hashes_before_amsterdam() {
459 let chain_spec = Arc::new(ChainSpecBuilder::mainnet().prague_activated().build());
460 let mut header = valid_prague_header();
461 header.block_access_list_hash = Some(B256::ZERO);
462 header.slot_number = Some(0);
463
464 assert!(matches!(
465 EthBeaconConsensus::new(chain_spec)
466 .with_allow_bal_hashes(true)
467 .validate_header(&SealedHeader::seal_slow(header,))
468 .unwrap_err(),
469 ConsensusError::SlotNumberUnexpected
470 ));
471 }
472
473 #[test]
474 fn prague_post_execution_allows_block_access_list_hash_before_amsterdam() {
475 let chain_spec = Arc::new(ChainSpecBuilder::mainnet().prague_activated().build());
476 let expected_hash = B256::repeat_byte(0x42);
477 let block = prague_recovered_block_with_bal_hash(expected_hash);
478 let result = BlockExecutionResult::<Receipt>::default();
479 let consensus = EthBeaconConsensus::new(chain_spec).with_allow_bal_hashes(true);
480
481 assert!(FullConsensus::<EthPrimitives>::validate_block_post_execution(
482 &consensus,
483 &block,
484 &result,
485 None,
486 Some(expected_hash),
487 )
488 .is_ok());
489
490 assert!(FullConsensus::<EthPrimitives>::validate_block_post_execution(
491 &consensus, &block, &result, None, None,
492 )
493 .is_ok());
494
495 assert!(matches!(
496 FullConsensus::<EthPrimitives>::validate_block_post_execution(
497 &consensus,
498 &block,
499 &result,
500 None,
501 Some(B256::repeat_byte(0x24)),
502 )
503 .unwrap_err(),
504 ConsensusError::BlockAccessListHashMismatch(_)
505 ));
506 }
507
508 #[test]
509 fn amsterdam_header_requires_block_access_list_hash() {
510 let chain_spec = Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
511 let mut header = valid_prague_header();
512 header.slot_number = Some(0);
513
514 assert!(matches!(
515 EthBeaconConsensus::new(chain_spec)
516 .validate_header(&SealedHeader::seal_slow(header,))
517 .unwrap_err(),
518 ConsensusError::BlockAccessListHashMissing
519 ));
520 }
521
522 #[test]
523 fn amsterdam_header_requires_slot_number() {
524 let chain_spec = Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
525 let mut header = valid_prague_header();
526 header.block_access_list_hash = Some(B256::ZERO);
527
528 assert!(matches!(
529 EthBeaconConsensus::new(chain_spec)
530 .validate_header(&SealedHeader::seal_slow(header,))
531 .unwrap_err(),
532 ConsensusError::SlotNumberMissing
533 ));
534 }
535
536 #[test]
537 fn amsterdam_header_accepts_block_access_list_hash_and_slot_number() {
538 let chain_spec = Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
539 let mut header = valid_prague_header();
540 header.block_access_list_hash = Some(B256::ZERO);
541 header.slot_number = Some(0);
542
543 assert!(EthBeaconConsensus::new(chain_spec)
544 .validate_header(&SealedHeader::seal_slow(header,))
545 .is_ok());
546 }
547
548 #[test]
549 fn amsterdam_post_execution_requires_computed_block_access_list_hash() {
550 let chain_spec = Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
551 let block = prague_recovered_block_with_bal_hash(B256::ZERO);
552 let result = BlockExecutionResult::<Receipt>::default();
553 let consensus = EthBeaconConsensus::new(chain_spec);
554
555 assert!(matches!(
556 FullConsensus::<EthPrimitives>::validate_block_post_execution(
557 &consensus, &block, &result, None, None,
558 )
559 .unwrap_err(),
560 ConsensusError::BlockAccessListHashMissing
561 ));
562 }
563}