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>(
195 block: &SealedBlock<B>,
196 chain_spec: &ChainSpec,
197) -> Result<(), ConsensusError>
198where
199 B: Block,
200 ChainSpec: EthereumHardforks,
201{
202 let ommers_hash = block.body().calculate_ommers_root();
204 if Some(block.ommers_hash()) != ommers_hash {
205 return Err(ConsensusError::BodyOmmersHashDiff(
206 GotExpected {
207 got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
208 expected: block.ommers_hash(),
209 }
210 .into(),
211 ))
212 }
213
214 if chain_spec.is_shanghai_active_at_timestamp(block.timestamp()) {
216 validate_shanghai_withdrawals(block)?;
217 }
218
219 if chain_spec.is_cancun_active_at_timestamp(block.timestamp()) {
220 validate_cancun_gas(block)?;
221 }
222
223 if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) &&
224 block.rlp_length() > MAX_RLP_BLOCK_SIZE
225 {
226 return Err(ConsensusError::BlockTooLarge {
227 rlp_length: block.rlp_length(),
228 max_rlp_length: MAX_RLP_BLOCK_SIZE,
229 })
230 }
231
232 Ok(())
233}
234
235pub fn validate_4844_header_standalone<H: BlockHeader>(
242 header: &H,
243 blob_params: BlobParams,
244) -> Result<(), ConsensusError> {
245 let blob_gas_used = header.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
246
247 if header.parent_beacon_block_root().is_none() {
248 return Err(ConsensusError::ParentBeaconBlockRootMissing)
249 }
250
251 if !blob_gas_used.is_multiple_of(DATA_GAS_PER_BLOB) {
252 return Err(ConsensusError::BlobGasUsedNotMultipleOfBlobGasPerBlob {
253 blob_gas_used,
254 blob_gas_per_blob: DATA_GAS_PER_BLOB,
255 })
256 }
257
258 if blob_gas_used > blob_params.max_blob_gas_per_block() {
259 return Err(ConsensusError::BlobGasUsedExceedsMaxBlobGasPerBlock {
260 blob_gas_used,
261 max_blob_gas_per_block: blob_params.max_blob_gas_per_block(),
262 })
263 }
264
265 Ok(())
266}
267
268#[inline]
273pub fn validate_header_extra_data<H: BlockHeader>(
274 header: &H,
275 max_size: usize,
276) -> Result<(), ConsensusError> {
277 let extra_data_len = header.extra_data().len();
278 if extra_data_len > max_size {
279 Err(ConsensusError::ExtraDataExceedsMax { len: extra_data_len })
280 } else {
281 Ok(())
282 }
283}
284
285#[inline]
290pub fn validate_against_parent_hash_number<H: BlockHeader>(
291 header: &H,
292 parent: &SealedHeader<H>,
293) -> Result<(), ConsensusError> {
294 if parent.hash() != header.parent_hash() {
295 return Err(ConsensusError::ParentHashMismatch(
296 GotExpected { got: header.parent_hash(), expected: parent.hash() }.into(),
297 ))
298 }
299
300 let Some(parent_number) = parent.number().checked_add(1) else {
301 return Err(ConsensusError::ParentBlockNumberMismatch {
303 parent_block_number: parent.number(),
304 block_number: u64::MAX,
305 })
306 };
307
308 if parent_number != header.number() {
310 return Err(ConsensusError::ParentBlockNumberMismatch {
311 parent_block_number: parent.number(),
312 block_number: header.number(),
313 })
314 }
315
316 Ok(())
317}
318
319#[inline]
321pub fn validate_against_parent_eip1559_base_fee<ChainSpec: EthChainSpec + EthereumHardforks>(
322 header: &ChainSpec::Header,
323 parent: &ChainSpec::Header,
324 chain_spec: &ChainSpec,
325) -> Result<(), ConsensusError> {
326 if chain_spec.is_london_active_at_block(header.number()) {
327 let base_fee = header.base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)?;
328
329 let expected_base_fee = if chain_spec
330 .ethereum_fork_activation(EthereumHardfork::London)
331 .transitions_at_block(header.number())
332 {
333 alloy_eips::eip1559::INITIAL_BASE_FEE
334 } else {
335 chain_spec
336 .next_block_base_fee(parent, header.timestamp())
337 .ok_or(ConsensusError::BaseFeeMissing)?
338 };
339 if expected_base_fee != base_fee {
340 return Err(ConsensusError::BaseFeeDiff(GotExpected {
341 expected: expected_base_fee,
342 got: base_fee,
343 }))
344 }
345 }
346
347 Ok(())
348}
349
350#[inline]
352pub fn validate_against_parent_timestamp<H: BlockHeader>(
353 header: &H,
354 parent: &H,
355) -> Result<(), ConsensusError> {
356 if header.timestamp() <= parent.timestamp() {
357 return Err(ConsensusError::TimestampIsInPast {
358 parent_timestamp: parent.timestamp(),
359 timestamp: header.timestamp(),
360 })
361 }
362 Ok(())
363}
364
365#[inline]
370pub fn validate_against_parent_gas_limit<
371 H: BlockHeader,
372 ChainSpec: EthChainSpec + EthereumHardforks,
373>(
374 header: &SealedHeader<H>,
375 parent: &SealedHeader<H>,
376 chain_spec: &ChainSpec,
377) -> Result<(), ConsensusError> {
378 let parent_gas_limit = if !chain_spec.is_london_active_at_block(parent.number()) &&
380 chain_spec.is_london_active_at_block(header.number())
381 {
382 parent.gas_limit() *
383 chain_spec.base_fee_params_at_timestamp(header.timestamp()).elasticity_multiplier
384 as u64
385 } else {
386 parent.gas_limit()
387 };
388
389 if header.gas_limit() > parent_gas_limit {
391 if header.gas_limit() - parent_gas_limit >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
392 return Err(ConsensusError::GasLimitInvalidIncrease {
393 parent_gas_limit,
394 child_gas_limit: header.gas_limit(),
395 })
396 }
397 }
398 else if parent_gas_limit - header.gas_limit() >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
400 return Err(ConsensusError::GasLimitInvalidDecrease {
401 parent_gas_limit,
402 child_gas_limit: header.gas_limit(),
403 })
404 }
405 else if header.gas_limit() < MINIMUM_GAS_LIMIT {
407 return Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: header.gas_limit() })
408 }
409
410 Ok(())
411}
412
413pub fn validate_against_parent_4844<H: BlockHeader>(
418 header: &H,
419 parent: &H,
420 blob_params: BlobParams,
421) -> Result<(), ConsensusError> {
422 let parent_blob_gas_used = parent.blob_gas_used().unwrap_or(0);
429 let parent_excess_blob_gas = parent.excess_blob_gas().unwrap_or(0);
430
431 if header.blob_gas_used().is_none() {
432 return Err(ConsensusError::BlobGasUsedMissing)
433 }
434 let excess_blob_gas = header.excess_blob_gas().ok_or(ConsensusError::ExcessBlobGasMissing)?;
435
436 let parent_base_fee_per_gas = parent.base_fee_per_gas().unwrap_or(0);
437 let expected_excess_blob_gas = blob_params.next_block_excess_blob_gas_osaka(
438 parent_excess_blob_gas,
439 parent_blob_gas_used,
440 parent_base_fee_per_gas,
441 );
442 if expected_excess_blob_gas != excess_blob_gas {
443 return Err(ConsensusError::ExcessBlobGasDiff {
444 diff: GotExpected { got: excess_blob_gas, expected: expected_excess_blob_gas },
445 parent_excess_blob_gas,
446 parent_blob_gas_used,
447 })
448 }
449
450 Ok(())
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use alloy_consensus::{BlockBody, Header, TxEip4844};
457 use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip4895::Withdrawals};
458 use alloy_primitives::{Address, Bytes, Signature, U256};
459 use rand::Rng;
460 use reth_chainspec::ChainSpecBuilder;
461 use reth_ethereum_primitives::{Transaction, TransactionSigned};
462 use reth_primitives_traits::proofs;
463
464 fn mock_blob_tx(nonce: u64, num_blobs: usize) -> TransactionSigned {
465 let mut rng = rand::rng();
466 let request = Transaction::Eip4844(TxEip4844 {
467 chain_id: 1u64,
468 nonce,
469 max_fee_per_gas: 0x28f000fff,
470 max_priority_fee_per_gas: 0x28f000fff,
471 max_fee_per_blob_gas: 0x7,
472 gas_limit: 10,
473 to: Address::default(),
474 value: U256::from(3_u64),
475 input: Bytes::from(vec![1, 2]),
476 access_list: Default::default(),
477 blob_versioned_hashes: std::iter::repeat_with(|| rng.random())
478 .take(num_blobs)
479 .collect(),
480 });
481
482 let signature = Signature::new(U256::default(), U256::default(), true);
483
484 TransactionSigned::new_unhashed(request, signature)
485 }
486
487 #[test]
488 fn cancun_block_incorrect_blob_gas_used() {
489 let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
490
491 let transaction = mock_blob_tx(1, 10);
493
494 let header = Header {
495 base_fee_per_gas: Some(1337),
496 withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
497 blob_gas_used: Some(1),
498 transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
499 &transaction,
500 )),
501 ..Default::default()
502 };
503 let body = BlockBody {
504 transactions: vec![transaction],
505 ommers: vec![],
506 withdrawals: Some(Withdrawals::default()),
507 };
508
509 let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
510
511 let expected_blob_gas_used = 10 * DATA_GAS_PER_BLOB;
513
514 assert!(matches!(
516 validate_block_pre_execution(&block, &chain_spec).unwrap_err(),
517 ConsensusError::BlobGasUsedDiff(diff)
518 if diff.got == 1 && diff.expected == expected_blob_gas_used
519 ));
520 }
521
522 #[test]
523 fn validate_header_extra_data_with_custom_limit() {
524 let header_32 = Header { extra_data: Bytes::from(vec![0; 32]), ..Default::default() };
526 assert!(validate_header_extra_data(&header_32, 32).is_ok());
527
528 let header_33 = Header { extra_data: Bytes::from(vec![0; 33]), ..Default::default() };
530 assert!(matches!(
531 validate_header_extra_data(&header_33, 32).unwrap_err(),
532 ConsensusError::ExtraDataExceedsMax { len } if len == 33
533 ));
534
535 assert!(validate_header_extra_data(&header_33, 64).is_ok());
537 }
538
539 #[test]
540 fn precomputed_tx_root_correct_passes() {
541 let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
542
543 let transaction = mock_blob_tx(1, 1);
544 let tx_root = proofs::calculate_transaction_root(std::slice::from_ref(&transaction));
545
546 let header = Header {
547 base_fee_per_gas: Some(1337),
548 withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
549 transactions_root: tx_root,
550 blob_gas_used: Some(DATA_GAS_PER_BLOB),
551 excess_blob_gas: Some(0),
552 ..Default::default()
553 };
554 let body = BlockBody {
555 transactions: vec![transaction],
556 ommers: vec![],
557 withdrawals: Some(Withdrawals::default()),
558 };
559
560 let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
561
562 assert!(
564 validate_block_pre_execution_with_tx_root(&block, &chain_spec, Some(tx_root)).is_ok()
565 );
566 assert!(validate_block_pre_execution_with_tx_root(&block, &chain_spec, None).is_ok());
567 }
568
569 #[test]
570 fn precomputed_tx_root_wrong_fails() {
571 let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
572
573 let transaction = mock_blob_tx(1, 1);
574 let tx_root = proofs::calculate_transaction_root(std::slice::from_ref(&transaction));
575
576 let header = Header {
577 base_fee_per_gas: Some(1337),
578 withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
579 transactions_root: tx_root,
580 blob_gas_used: Some(DATA_GAS_PER_BLOB),
581 excess_blob_gas: Some(0),
582 ..Default::default()
583 };
584 let body = BlockBody {
585 transactions: vec![transaction],
586 ommers: vec![],
587 withdrawals: Some(Withdrawals::default()),
588 };
589
590 let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
591
592 let wrong_root = B256::repeat_byte(0xff);
593 assert!(matches!(
594 validate_block_pre_execution_with_tx_root(&block, &chain_spec, Some(wrong_root))
595 .unwrap_err(),
596 ConsensusError::BodyTransactionRootDiff(diff)
597 if diff.0.got == wrong_root && diff.0.expected == tx_root
598 ));
599 }
600}