1use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH};
4use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip7840::BlobParams};
5use alloy_primitives::B256;
6use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks};
7use reth_consensus::ConsensusError;
8use reth_primitives_traits::{
9 constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK, MINIMUM_GAS_LIMIT},
10 Block, BlockBody, BlockHeader, GotExpected, SealedBlock, SealedHeader,
11};
12
13pub const MAX_RLP_BLOCK_SIZE: usize = 8_388_608;
19
20#[inline]
22pub fn validate_header_gas<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
23 if header.gas_used() > header.gas_limit() {
24 return Err(ConsensusError::HeaderGasUsedExceedsGasLimit {
25 gas_used: header.gas_used(),
26 gas_limit: header.gas_limit(),
27 })
28 }
29 if header.gas_limit() > MAXIMUM_GAS_LIMIT_BLOCK {
31 return Err(ConsensusError::HeaderGasLimitExceedsMax { gas_limit: header.gas_limit() })
32 }
33 Ok(())
34}
35
36#[inline]
38pub fn validate_header_base_fee<H: BlockHeader, ChainSpec: EthereumHardforks>(
39 header: &H,
40 chain_spec: &ChainSpec,
41) -> Result<(), ConsensusError> {
42 if chain_spec.is_london_active_at_block(header.number()) && header.base_fee_per_gas().is_none()
43 {
44 return Err(ConsensusError::BaseFeeMissing)
45 }
46 Ok(())
47}
48
49#[inline]
55pub fn validate_shanghai_withdrawals<B: Block>(
56 block: &SealedBlock<B>,
57) -> Result<(), ConsensusError> {
58 let withdrawals = block.body().withdrawals().ok_or(ConsensusError::BodyWithdrawalsMissing)?;
59 let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root(withdrawals);
60 let header_withdrawals_root =
61 block.withdrawals_root().ok_or(ConsensusError::WithdrawalsRootMissing)?;
62 if withdrawals_root != *header_withdrawals_root {
63 return Err(ConsensusError::BodyWithdrawalsRootDiff(
64 GotExpected { got: withdrawals_root, expected: header_withdrawals_root }.into(),
65 ));
66 }
67 Ok(())
68}
69
70#[inline]
76pub fn validate_cancun_gas<B: Block>(block: &SealedBlock<B>) -> Result<(), ConsensusError> {
77 let header_blob_gas_used = block.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
80 let total_blob_gas = block.body().blob_gas_used();
81 if total_blob_gas != header_blob_gas_used {
82 return Err(ConsensusError::BlobGasUsedDiff(GotExpected {
83 got: header_blob_gas_used,
84 expected: total_blob_gas,
85 }));
86 }
87 Ok(())
88}
89
90pub fn validate_body_against_header<B, H>(body: &B, header: &H) -> Result<(), ConsensusError>
97where
98 B: BlockBody,
99 H: BlockHeader,
100{
101 let ommers_hash = body.calculate_ommers_root();
102 if Some(header.ommers_hash()) != ommers_hash {
103 return Err(ConsensusError::BodyOmmersHashDiff(
104 GotExpected {
105 got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
106 expected: header.ommers_hash(),
107 }
108 .into(),
109 ))
110 }
111
112 let tx_root = body.calculate_tx_root();
113 if header.transactions_root() != tx_root {
114 return Err(ConsensusError::BodyTransactionRootDiff(
115 GotExpected { got: tx_root, expected: header.transactions_root() }.into(),
116 ))
117 }
118
119 match (header.withdrawals_root(), body.calculate_withdrawals_root()) {
120 (Some(header_withdrawals_root), Some(withdrawals_root)) => {
121 if withdrawals_root != header_withdrawals_root {
122 return Err(ConsensusError::BodyWithdrawalsRootDiff(
123 GotExpected { got: withdrawals_root, expected: header_withdrawals_root }.into(),
124 ))
125 }
126 }
127 (None, None) => {
128 }
130 _ => return Err(ConsensusError::WithdrawalsRootUnexpected),
131 }
132
133 Ok(())
134}
135
136pub fn validate_block_pre_execution<B, ChainSpec>(
142 block: &SealedBlock<B>,
143 chain_spec: &ChainSpec,
144) -> Result<(), ConsensusError>
145where
146 B: Block,
147 ChainSpec: EthChainSpec + EthereumHardforks,
148{
149 validate_block_pre_execution_with_tx_root(block, chain_spec, None)
150}
151
152pub fn validate_block_pre_execution_with_tx_root<B, ChainSpec>(
162 block: &SealedBlock<B>,
163 chain_spec: &ChainSpec,
164 transaction_root: Option<B256>,
165) -> Result<(), ConsensusError>
166where
167 B: Block,
168 ChainSpec: EthChainSpec + EthereumHardforks,
169{
170 post_merge_hardfork_fields(block, chain_spec)?;
171
172 let expected_transaction_root = block.header().transactions_root();
174 let calculated_transaction_root =
175 transaction_root.unwrap_or_else(|| block.body().calculate_tx_root());
176 if calculated_transaction_root != expected_transaction_root {
177 return Err(ConsensusError::BodyTransactionRootDiff(
178 GotExpected { got: calculated_transaction_root, expected: expected_transaction_root }
179 .into(),
180 ))
181 }
182
183 Ok(())
184}
185
186pub fn post_merge_hardfork_fields<B, ChainSpec>(
196 block: &SealedBlock<B>,
197 chain_spec: &ChainSpec,
198) -> Result<(), ConsensusError>
199where
200 B: Block,
201 ChainSpec: EthereumHardforks,
202{
203 let ommers_hash = block.body().calculate_ommers_root();
205 if Some(block.ommers_hash()) != ommers_hash {
206 return Err(ConsensusError::BodyOmmersHashDiff(
207 GotExpected {
208 got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
209 expected: block.ommers_hash(),
210 }
211 .into(),
212 ))
213 }
214
215 if chain_spec.is_shanghai_active_at_timestamp(block.timestamp()) {
217 validate_shanghai_withdrawals(block)?;
218 } else if block.body().withdrawals().is_some() {
219 return Err(ConsensusError::BodyWithdrawalsUnexpected)
222 }
223
224 if chain_spec.is_cancun_active_at_timestamp(block.timestamp()) {
225 validate_cancun_gas(block)?;
226 }
227
228 if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) &&
229 block.rlp_length() > MAX_RLP_BLOCK_SIZE
230 {
231 return Err(ConsensusError::BlockTooLarge {
232 rlp_length: block.rlp_length(),
233 max_rlp_length: MAX_RLP_BLOCK_SIZE,
234 })
235 }
236
237 Ok(())
238}
239
240pub fn validate_4844_header_standalone<H: BlockHeader>(
247 header: &H,
248 blob_params: BlobParams,
249) -> Result<(), ConsensusError> {
250 let blob_gas_used = header.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
251
252 if header.parent_beacon_block_root().is_none() {
253 return Err(ConsensusError::ParentBeaconBlockRootMissing)
254 }
255
256 if !blob_gas_used.is_multiple_of(DATA_GAS_PER_BLOB) {
257 return Err(ConsensusError::BlobGasUsedNotMultipleOfBlobGasPerBlob {
258 blob_gas_used,
259 blob_gas_per_blob: DATA_GAS_PER_BLOB,
260 })
261 }
262
263 if blob_gas_used > blob_params.max_blob_gas_per_block() {
264 return Err(ConsensusError::BlobGasUsedExceedsMaxBlobGasPerBlock {
265 blob_gas_used,
266 max_blob_gas_per_block: blob_params.max_blob_gas_per_block(),
267 })
268 }
269
270 Ok(())
271}
272
273#[inline]
278pub fn validate_header_extra_data<H: BlockHeader>(
279 header: &H,
280 max_size: usize,
281) -> Result<(), ConsensusError> {
282 let extra_data_len = header.extra_data().len();
283 if extra_data_len > max_size {
284 Err(ConsensusError::ExtraDataExceedsMax { len: extra_data_len })
285 } else {
286 Ok(())
287 }
288}
289
290#[inline]
295pub fn validate_against_parent_hash_number<H: BlockHeader>(
296 header: &H,
297 parent: &SealedHeader<H>,
298) -> Result<(), ConsensusError> {
299 if parent.hash() != header.parent_hash() {
300 return Err(ConsensusError::ParentHashMismatch(
301 GotExpected { got: header.parent_hash(), expected: parent.hash() }.into(),
302 ))
303 }
304
305 let Some(parent_number) = parent.number().checked_add(1) else {
306 return Err(ConsensusError::ParentBlockNumberMismatch {
308 parent_block_number: parent.number(),
309 block_number: u64::MAX,
310 })
311 };
312
313 if parent_number != header.number() {
315 return Err(ConsensusError::ParentBlockNumberMismatch {
316 parent_block_number: parent.number(),
317 block_number: header.number(),
318 })
319 }
320
321 Ok(())
322}
323
324#[inline]
326pub fn validate_against_parent_eip1559_base_fee<ChainSpec: EthChainSpec + EthereumHardforks>(
327 header: &ChainSpec::Header,
328 parent: &ChainSpec::Header,
329 chain_spec: &ChainSpec,
330) -> Result<(), ConsensusError> {
331 if chain_spec.is_london_active_at_block(header.number()) {
332 let base_fee = header.base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)?;
333
334 let expected_base_fee = if chain_spec
335 .ethereum_fork_activation(EthereumHardfork::London)
336 .transitions_at_block(header.number())
337 {
338 alloy_eips::eip1559::INITIAL_BASE_FEE
339 } else {
340 chain_spec
341 .next_block_base_fee(parent, header.timestamp())
342 .ok_or(ConsensusError::BaseFeeMissing)?
343 };
344 if expected_base_fee != base_fee {
345 return Err(ConsensusError::BaseFeeDiff(GotExpected {
346 expected: expected_base_fee,
347 got: base_fee,
348 }))
349 }
350 }
351
352 Ok(())
353}
354
355#[inline]
357pub fn validate_against_parent_timestamp<H: BlockHeader>(
358 header: &H,
359 parent: &H,
360) -> Result<(), ConsensusError> {
361 if header.timestamp() <= parent.timestamp() {
362 return Err(ConsensusError::TimestampIsInPast {
363 parent_timestamp: parent.timestamp(),
364 timestamp: header.timestamp(),
365 })
366 }
367 Ok(())
368}
369
370#[inline]
375pub fn validate_against_parent_gas_limit<
376 H: BlockHeader,
377 ChainSpec: EthChainSpec + EthereumHardforks,
378>(
379 header: &SealedHeader<H>,
380 parent: &SealedHeader<H>,
381 chain_spec: &ChainSpec,
382) -> Result<(), ConsensusError> {
383 let parent_gas_limit = if !chain_spec.is_london_active_at_block(parent.number()) &&
385 chain_spec.is_london_active_at_block(header.number())
386 {
387 parent.gas_limit() *
388 chain_spec.base_fee_params_at_timestamp(header.timestamp()).elasticity_multiplier
389 as u64
390 } else {
391 parent.gas_limit()
392 };
393
394 if header.gas_limit() > parent_gas_limit {
396 if header.gas_limit() - parent_gas_limit >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
397 return Err(ConsensusError::GasLimitInvalidIncrease {
398 parent_gas_limit,
399 child_gas_limit: header.gas_limit(),
400 })
401 }
402 }
403 else if parent_gas_limit - header.gas_limit() >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
405 return Err(ConsensusError::GasLimitInvalidDecrease {
406 parent_gas_limit,
407 child_gas_limit: header.gas_limit(),
408 })
409 }
410 if header.gas_limit() < MINIMUM_GAS_LIMIT {
412 return Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: header.gas_limit() })
413 }
414
415 Ok(())
416}
417
418pub fn validate_against_parent_4844<H: BlockHeader>(
423 header: &H,
424 parent: &H,
425 blob_params: BlobParams,
426) -> Result<(), ConsensusError> {
427 let parent_blob_gas_used = parent.blob_gas_used().unwrap_or(0);
434 let parent_excess_blob_gas = parent.excess_blob_gas().unwrap_or(0);
435
436 if header.blob_gas_used().is_none() {
437 return Err(ConsensusError::BlobGasUsedMissing)
438 }
439 let excess_blob_gas = header.excess_blob_gas().ok_or(ConsensusError::ExcessBlobGasMissing)?;
440
441 let parent_base_fee_per_gas = parent.base_fee_per_gas().unwrap_or(0);
442 let expected_excess_blob_gas = blob_params.next_block_excess_blob_gas_osaka(
443 parent_excess_blob_gas,
444 parent_blob_gas_used,
445 parent_base_fee_per_gas,
446 );
447 if expected_excess_blob_gas != excess_blob_gas {
448 return Err(ConsensusError::ExcessBlobGasDiff {
449 diff: GotExpected { got: excess_blob_gas, expected: expected_excess_blob_gas },
450 parent_excess_blob_gas,
451 parent_blob_gas_used,
452 })
453 }
454
455 Ok(())
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use alloy_consensus::{BlockBody, Header, TxEip4844};
462 use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip4895::Withdrawals};
463 use alloy_primitives::{Address, Bytes, Signature, U256};
464 use rand::Rng;
465 use reth_chainspec::ChainSpecBuilder;
466 use reth_ethereum_primitives::{Transaction, TransactionSigned};
467 use reth_primitives_traits::proofs;
468
469 fn mock_blob_tx(nonce: u64, num_blobs: usize) -> TransactionSigned {
470 let mut rng = rand::rng();
471 let request = Transaction::Eip4844(TxEip4844 {
472 chain_id: 1u64,
473 nonce,
474 max_fee_per_gas: 0x28f000fff,
475 max_priority_fee_per_gas: 0x28f000fff,
476 max_fee_per_blob_gas: 0x7,
477 gas_limit: 10,
478 to: Address::default(),
479 value: U256::from(3_u64),
480 input: Bytes::from(vec![1, 2]),
481 access_list: Default::default(),
482 blob_versioned_hashes: std::iter::repeat_with(|| rng.random())
483 .take(num_blobs)
484 .collect(),
485 });
486
487 let signature = Signature::new(U256::default(), U256::default(), true);
488
489 TransactionSigned::new_unhashed(request, signature)
490 }
491
492 #[test]
493 fn cancun_block_incorrect_blob_gas_used() {
494 let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
495
496 let transaction = mock_blob_tx(1, 10);
498
499 let header = Header {
500 base_fee_per_gas: Some(1337),
501 withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
502 blob_gas_used: Some(1),
503 transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
504 &transaction,
505 )),
506 ..Default::default()
507 };
508 let body = BlockBody {
509 transactions: vec![transaction],
510 ommers: vec![],
511 withdrawals: Some(Withdrawals::default()),
512 };
513
514 let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
515
516 let expected_blob_gas_used = 10 * DATA_GAS_PER_BLOB;
518
519 assert!(matches!(
521 validate_block_pre_execution(&block, &chain_spec).unwrap_err(),
522 ConsensusError::BlobGasUsedDiff(diff)
523 if diff.got == 1 && diff.expected == expected_blob_gas_used
524 ));
525 }
526
527 #[test]
528 fn validate_header_extra_data_with_custom_limit() {
529 let header_32 = Header { extra_data: Bytes::from(vec![0; 32]), ..Default::default() };
531 assert!(validate_header_extra_data(&header_32, 32).is_ok());
532
533 let header_33 = Header { extra_data: Bytes::from(vec![0; 33]), ..Default::default() };
535 assert!(matches!(
536 validate_header_extra_data(&header_33, 32).unwrap_err(),
537 ConsensusError::ExtraDataExceedsMax { len } if len == 33
538 ));
539
540 assert!(validate_header_extra_data(&header_33, 64).is_ok());
542 }
543
544 #[test]
545 fn precomputed_tx_root_correct_passes() {
546 let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
547
548 let transaction = mock_blob_tx(1, 1);
549 let tx_root = proofs::calculate_transaction_root(std::slice::from_ref(&transaction));
550
551 let header = Header {
552 base_fee_per_gas: Some(1337),
553 withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
554 transactions_root: tx_root,
555 blob_gas_used: Some(DATA_GAS_PER_BLOB),
556 excess_blob_gas: Some(0),
557 ..Default::default()
558 };
559 let body = BlockBody {
560 transactions: vec![transaction],
561 ommers: vec![],
562 withdrawals: Some(Withdrawals::default()),
563 };
564
565 let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
566
567 assert!(
569 validate_block_pre_execution_with_tx_root(&block, &chain_spec, Some(tx_root)).is_ok()
570 );
571 assert!(validate_block_pre_execution_with_tx_root(&block, &chain_spec, None).is_ok());
572 }
573
574 #[test]
575 fn precomputed_tx_root_wrong_fails() {
576 let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
577
578 let transaction = mock_blob_tx(1, 1);
579 let tx_root = proofs::calculate_transaction_root(std::slice::from_ref(&transaction));
580
581 let header = Header {
582 base_fee_per_gas: Some(1337),
583 withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
584 transactions_root: tx_root,
585 blob_gas_used: Some(DATA_GAS_PER_BLOB),
586 excess_blob_gas: Some(0),
587 ..Default::default()
588 };
589 let body = BlockBody {
590 transactions: vec![transaction],
591 ommers: vec![],
592 withdrawals: Some(Withdrawals::default()),
593 };
594
595 let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
596
597 let wrong_root = B256::repeat_byte(0xff);
598 assert!(matches!(
599 validate_block_pre_execution_with_tx_root(&block, &chain_spec, Some(wrong_root))
600 .unwrap_err(),
601 ConsensusError::BodyTransactionRootDiff(diff)
602 if diff.0.got == wrong_root && diff.0.expected == tx_root
603 ));
604 }
605
606 #[test]
607 fn pre_shanghai_block_with_withdrawals_fails() {
608 let chain_spec = ChainSpecBuilder::mainnet().paris_activated().build();
609
610 let header = Header {
611 transactions_root: proofs::calculate_transaction_root::<TransactionSigned>(&[]),
612 ..Default::default()
613 };
614 let body = BlockBody::<TransactionSigned>::default();
615
616 let block =
617 SealedBlock::seal_slow(alloy_consensus::Block::new(header.clone(), body.clone()));
618 assert!(validate_block_pre_execution(&block, &chain_spec).is_ok());
619
620 let body = BlockBody { withdrawals: Some(Withdrawals::default()), ..body };
621 let block = SealedBlock::seal_slow(alloy_consensus::Block::new(header, body));
622 assert!(matches!(
623 validate_block_pre_execution(&block, &chain_spec).unwrap_err(),
624 ConsensusError::BodyWithdrawalsUnexpected
625 ));
626 }
627}