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