1use alloy_consensus::{BlockHeader as _, Transaction, EMPTY_OMMER_ROOT_HASH};
4use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip7840::BlobParams};
5use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks};
6use reth_consensus::{ConsensusError, TxGasLimitTooHighErr};
7use reth_primitives_traits::{
8 constants::{
9 GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK, MAX_TX_GAS_LIMIT_OSAKA, MINIMUM_GAS_LIMIT,
10 },
11 transaction::TxHashRef,
12 Block, BlockBody, BlockHeader, GotExpected, SealedBlock, SealedHeader,
13};
14
15pub const MAX_RLP_BLOCK_SIZE: usize = 8_388_608;
21
22#[inline]
24pub fn validate_header_gas<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
25 if header.gas_used() > header.gas_limit() {
26 return Err(ConsensusError::HeaderGasUsedExceedsGasLimit {
27 gas_used: header.gas_used(),
28 gas_limit: header.gas_limit(),
29 })
30 }
31 if header.gas_limit() > MAXIMUM_GAS_LIMIT_BLOCK {
33 return Err(ConsensusError::HeaderGasLimitExceedsMax { gas_limit: header.gas_limit() })
34 }
35 Ok(())
36}
37
38#[inline]
40pub fn validate_header_base_fee<H: BlockHeader, ChainSpec: EthereumHardforks>(
41 header: &H,
42 chain_spec: &ChainSpec,
43) -> Result<(), ConsensusError> {
44 if chain_spec.is_london_active_at_block(header.number()) && header.base_fee_per_gas().is_none()
45 {
46 return Err(ConsensusError::BaseFeeMissing)
47 }
48 Ok(())
49}
50
51#[inline]
57pub fn validate_shanghai_withdrawals<B: Block>(
58 block: &SealedBlock<B>,
59) -> Result<(), ConsensusError> {
60 let withdrawals = block.body().withdrawals().ok_or(ConsensusError::BodyWithdrawalsMissing)?;
61 let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root(withdrawals);
62 let header_withdrawals_root =
63 block.withdrawals_root().ok_or(ConsensusError::WithdrawalsRootMissing)?;
64 if withdrawals_root != *header_withdrawals_root {
65 return Err(ConsensusError::BodyWithdrawalsRootDiff(
66 GotExpected { got: withdrawals_root, expected: header_withdrawals_root }.into(),
67 ));
68 }
69 Ok(())
70}
71
72#[inline]
78pub fn validate_cancun_gas<B: Block>(block: &SealedBlock<B>) -> Result<(), ConsensusError> {
79 let header_blob_gas_used = block.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
82 let total_blob_gas = block.body().blob_gas_used();
83 if total_blob_gas != header_blob_gas_used {
84 return Err(ConsensusError::BlobGasUsedDiff(GotExpected {
85 got: header_blob_gas_used,
86 expected: total_blob_gas,
87 }));
88 }
89 Ok(())
90}
91
92pub fn validate_body_against_header<B, H>(body: &B, header: &H) -> Result<(), ConsensusError>
99where
100 B: BlockBody,
101 H: BlockHeader,
102{
103 let ommers_hash = body.calculate_ommers_root();
104 if Some(header.ommers_hash()) != ommers_hash {
105 return Err(ConsensusError::BodyOmmersHashDiff(
106 GotExpected {
107 got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
108 expected: header.ommers_hash(),
109 }
110 .into(),
111 ))
112 }
113
114 let tx_root = body.calculate_tx_root();
115 if header.transactions_root() != tx_root {
116 return Err(ConsensusError::BodyTransactionRootDiff(
117 GotExpected { got: tx_root, expected: header.transactions_root() }.into(),
118 ))
119 }
120
121 match (header.withdrawals_root(), body.calculate_withdrawals_root()) {
122 (Some(header_withdrawals_root), Some(withdrawals_root)) => {
123 if withdrawals_root != header_withdrawals_root {
124 return Err(ConsensusError::BodyWithdrawalsRootDiff(
125 GotExpected { got: withdrawals_root, expected: header_withdrawals_root }.into(),
126 ))
127 }
128 }
129 (None, None) => {
130 }
132 _ => return Err(ConsensusError::WithdrawalsRootUnexpected),
133 }
134
135 Ok(())
136}
137
138pub fn validate_block_pre_execution<B, ChainSpec>(
144 block: &SealedBlock<B>,
145 chain_spec: &ChainSpec,
146) -> Result<(), ConsensusError>
147where
148 B: Block,
149 ChainSpec: EthereumHardforks,
150{
151 post_merge_hardfork_fields(block, chain_spec)?;
152
153 if let Err(error) = block.ensure_transaction_root_valid() {
155 return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
156 }
157 if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) {
159 for tx in block.body().transactions() {
160 if tx.gas_limit() > MAX_TX_GAS_LIMIT_OSAKA {
161 return Err(TxGasLimitTooHighErr {
162 tx_hash: *tx.tx_hash(),
163 gas_limit: tx.gas_limit(),
164 max_allowed: MAX_TX_GAS_LIMIT_OSAKA,
165 }
166 .into());
167 }
168 }
169 }
170
171 Ok(())
172}
173
174pub fn post_merge_hardfork_fields<B, ChainSpec>(
183 block: &SealedBlock<B>,
184 chain_spec: &ChainSpec,
185) -> Result<(), ConsensusError>
186where
187 B: Block,
188 ChainSpec: EthereumHardforks,
189{
190 let ommers_hash = block.body().calculate_ommers_root();
192 if Some(block.ommers_hash()) != ommers_hash {
193 return Err(ConsensusError::BodyOmmersHashDiff(
194 GotExpected {
195 got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
196 expected: block.ommers_hash(),
197 }
198 .into(),
199 ))
200 }
201
202 if chain_spec.is_shanghai_active_at_timestamp(block.timestamp()) {
204 validate_shanghai_withdrawals(block)?;
205 }
206
207 if chain_spec.is_cancun_active_at_timestamp(block.timestamp()) {
208 validate_cancun_gas(block)?;
209 }
210
211 if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) &&
212 block.rlp_length() > MAX_RLP_BLOCK_SIZE
213 {
214 return Err(ConsensusError::BlockTooLarge {
215 rlp_length: block.rlp_length(),
216 max_rlp_length: MAX_RLP_BLOCK_SIZE,
217 })
218 }
219
220 Ok(())
221}
222
223pub fn validate_4844_header_standalone<H: BlockHeader>(
230 header: &H,
231 blob_params: BlobParams,
232) -> Result<(), ConsensusError> {
233 let blob_gas_used = header.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
234
235 if header.parent_beacon_block_root().is_none() {
236 return Err(ConsensusError::ParentBeaconBlockRootMissing)
237 }
238
239 if !blob_gas_used.is_multiple_of(DATA_GAS_PER_BLOB) {
240 return Err(ConsensusError::BlobGasUsedNotMultipleOfBlobGasPerBlob {
241 blob_gas_used,
242 blob_gas_per_blob: DATA_GAS_PER_BLOB,
243 })
244 }
245
246 if blob_gas_used > blob_params.max_blob_gas_per_block() {
247 return Err(ConsensusError::BlobGasUsedExceedsMaxBlobGasPerBlock {
248 blob_gas_used,
249 max_blob_gas_per_block: blob_params.max_blob_gas_per_block(),
250 })
251 }
252
253 Ok(())
254}
255
256#[inline]
261pub fn validate_header_extra_data<H: BlockHeader>(
262 header: &H,
263 max_size: usize,
264) -> Result<(), ConsensusError> {
265 let extra_data_len = header.extra_data().len();
266 if extra_data_len > max_size {
267 Err(ConsensusError::ExtraDataExceedsMax { len: extra_data_len })
268 } else {
269 Ok(())
270 }
271}
272
273#[inline]
278pub fn validate_against_parent_hash_number<H: BlockHeader>(
279 header: &H,
280 parent: &SealedHeader<H>,
281) -> Result<(), ConsensusError> {
282 if parent.hash() != header.parent_hash() {
283 return Err(ConsensusError::ParentHashMismatch(
284 GotExpected { got: header.parent_hash(), expected: parent.hash() }.into(),
285 ))
286 }
287
288 let Some(parent_number) = parent.number().checked_add(1) else {
289 return Err(ConsensusError::ParentBlockNumberMismatch {
291 parent_block_number: parent.number(),
292 block_number: u64::MAX,
293 })
294 };
295
296 if parent_number != header.number() {
298 return Err(ConsensusError::ParentBlockNumberMismatch {
299 parent_block_number: parent.number(),
300 block_number: header.number(),
301 })
302 }
303
304 Ok(())
305}
306
307#[inline]
309pub fn validate_against_parent_eip1559_base_fee<ChainSpec: EthChainSpec + EthereumHardforks>(
310 header: &ChainSpec::Header,
311 parent: &ChainSpec::Header,
312 chain_spec: &ChainSpec,
313) -> Result<(), ConsensusError> {
314 if chain_spec.is_london_active_at_block(header.number()) {
315 let base_fee = header.base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)?;
316
317 let expected_base_fee = if chain_spec
318 .ethereum_fork_activation(EthereumHardfork::London)
319 .transitions_at_block(header.number())
320 {
321 alloy_eips::eip1559::INITIAL_BASE_FEE
322 } else {
323 chain_spec
324 .next_block_base_fee(parent, header.timestamp())
325 .ok_or(ConsensusError::BaseFeeMissing)?
326 };
327 if expected_base_fee != base_fee {
328 return Err(ConsensusError::BaseFeeDiff(GotExpected {
329 expected: expected_base_fee,
330 got: base_fee,
331 }))
332 }
333 }
334
335 Ok(())
336}
337
338#[inline]
340pub fn validate_against_parent_timestamp<H: BlockHeader>(
341 header: &H,
342 parent: &H,
343) -> Result<(), ConsensusError> {
344 if header.timestamp() <= parent.timestamp() {
345 return Err(ConsensusError::TimestampIsInPast {
346 parent_timestamp: parent.timestamp(),
347 timestamp: header.timestamp(),
348 })
349 }
350 Ok(())
351}
352
353#[inline]
358pub fn validate_against_parent_gas_limit<
359 H: BlockHeader,
360 ChainSpec: EthChainSpec + EthereumHardforks,
361>(
362 header: &SealedHeader<H>,
363 parent: &SealedHeader<H>,
364 chain_spec: &ChainSpec,
365) -> Result<(), ConsensusError> {
366 let parent_gas_limit = if !chain_spec.is_london_active_at_block(parent.number()) &&
368 chain_spec.is_london_active_at_block(header.number())
369 {
370 parent.gas_limit() *
371 chain_spec.base_fee_params_at_timestamp(header.timestamp()).elasticity_multiplier
372 as u64
373 } else {
374 parent.gas_limit()
375 };
376
377 if header.gas_limit() > parent_gas_limit {
379 if header.gas_limit() - parent_gas_limit >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
380 return Err(ConsensusError::GasLimitInvalidIncrease {
381 parent_gas_limit,
382 child_gas_limit: header.gas_limit(),
383 })
384 }
385 }
386 else if parent_gas_limit - header.gas_limit() >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
388 return Err(ConsensusError::GasLimitInvalidDecrease {
389 parent_gas_limit,
390 child_gas_limit: header.gas_limit(),
391 })
392 }
393 else if header.gas_limit() < MINIMUM_GAS_LIMIT {
395 return Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: header.gas_limit() })
396 }
397
398 Ok(())
399}
400
401pub fn validate_against_parent_4844<H: BlockHeader>(
406 header: &H,
407 parent: &H,
408 blob_params: BlobParams,
409) -> Result<(), ConsensusError> {
410 let parent_blob_gas_used = parent.blob_gas_used().unwrap_or(0);
417 let parent_excess_blob_gas = parent.excess_blob_gas().unwrap_or(0);
418
419 if header.blob_gas_used().is_none() {
420 return Err(ConsensusError::BlobGasUsedMissing)
421 }
422 let excess_blob_gas = header.excess_blob_gas().ok_or(ConsensusError::ExcessBlobGasMissing)?;
423
424 let parent_base_fee_per_gas = parent.base_fee_per_gas().unwrap_or(0);
425 let expected_excess_blob_gas = blob_params.next_block_excess_blob_gas_osaka(
426 parent_excess_blob_gas,
427 parent_blob_gas_used,
428 parent_base_fee_per_gas,
429 );
430 if expected_excess_blob_gas != excess_blob_gas {
431 return Err(ConsensusError::ExcessBlobGasDiff {
432 diff: GotExpected { got: excess_blob_gas, expected: expected_excess_blob_gas },
433 parent_excess_blob_gas,
434 parent_blob_gas_used,
435 })
436 }
437
438 Ok(())
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use alloy_consensus::{BlockBody, Header, TxEip4844};
445 use alloy_eips::eip4895::Withdrawals;
446 use alloy_primitives::{Address, Bytes, Signature, U256};
447 use rand::Rng;
448 use reth_chainspec::ChainSpecBuilder;
449 use reth_ethereum_primitives::{Transaction, TransactionSigned};
450 use reth_primitives_traits::proofs;
451
452 fn mock_blob_tx(nonce: u64, num_blobs: usize) -> TransactionSigned {
453 let mut rng = rand::rng();
454 let request = Transaction::Eip4844(TxEip4844 {
455 chain_id: 1u64,
456 nonce,
457 max_fee_per_gas: 0x28f000fff,
458 max_priority_fee_per_gas: 0x28f000fff,
459 max_fee_per_blob_gas: 0x7,
460 gas_limit: 10,
461 to: Address::default(),
462 value: U256::from(3_u64),
463 input: Bytes::from(vec![1, 2]),
464 access_list: Default::default(),
465 blob_versioned_hashes: std::iter::repeat_with(|| rng.random())
466 .take(num_blobs)
467 .collect(),
468 });
469
470 let signature = Signature::new(U256::default(), U256::default(), true);
471
472 TransactionSigned::new_unhashed(request, signature)
473 }
474
475 #[test]
476 fn cancun_block_incorrect_blob_gas_used() {
477 let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
478
479 let transaction = mock_blob_tx(1, 10);
481
482 let header = Header {
483 base_fee_per_gas: Some(1337),
484 withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
485 blob_gas_used: Some(1),
486 transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
487 &transaction,
488 )),
489 ..Default::default()
490 };
491 let body = BlockBody {
492 transactions: vec![transaction],
493 ommers: vec![],
494 withdrawals: Some(Withdrawals::default()),
495 };
496
497 let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
498
499 let expected_blob_gas_used = 10 * DATA_GAS_PER_BLOB;
501
502 assert_eq!(
504 validate_block_pre_execution(&block, &chain_spec),
505 Err(ConsensusError::BlobGasUsedDiff(GotExpected {
506 got: 1,
507 expected: expected_blob_gas_used
508 }))
509 );
510 }
511
512 #[test]
513 fn validate_header_extra_data_with_custom_limit() {
514 let header_32 = Header { extra_data: Bytes::from(vec![0; 32]), ..Default::default() };
516 assert!(validate_header_extra_data(&header_32, 32).is_ok());
517
518 let header_33 = Header { extra_data: Bytes::from(vec![0; 33]), ..Default::default() };
520 assert_eq!(
521 validate_header_extra_data(&header_33, 32),
522 Err(ConsensusError::ExtraDataExceedsMax { len: 33 })
523 );
524
525 assert!(validate_header_extra_data(&header_33, 64).is_ok());
527 }
528}