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