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.number() + 1 != header.number() {
284 return Err(ConsensusError::ParentBlockNumberMismatch {
285 parent_block_number: parent.number(),
286 block_number: header.number(),
287 })
288 }
289
290 if parent.hash() != header.parent_hash() {
291 return Err(ConsensusError::ParentHashMismatch(
292 GotExpected { got: header.parent_hash(), expected: parent.hash() }.into(),
293 ))
294 }
295
296 Ok(())
297}
298
299#[inline]
301pub fn validate_against_parent_eip1559_base_fee<ChainSpec: EthChainSpec + EthereumHardforks>(
302 header: &ChainSpec::Header,
303 parent: &ChainSpec::Header,
304 chain_spec: &ChainSpec,
305) -> Result<(), ConsensusError> {
306 if chain_spec.is_london_active_at_block(header.number()) {
307 let base_fee = header.base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)?;
308
309 let expected_base_fee = if chain_spec
310 .ethereum_fork_activation(EthereumHardfork::London)
311 .transitions_at_block(header.number())
312 {
313 alloy_eips::eip1559::INITIAL_BASE_FEE
314 } else {
315 chain_spec
316 .next_block_base_fee(parent, header.timestamp())
317 .ok_or(ConsensusError::BaseFeeMissing)?
318 };
319 if expected_base_fee != base_fee {
320 return Err(ConsensusError::BaseFeeDiff(GotExpected {
321 expected: expected_base_fee,
322 got: base_fee,
323 }))
324 }
325 }
326
327 Ok(())
328}
329
330#[inline]
332pub fn validate_against_parent_timestamp<H: BlockHeader>(
333 header: &H,
334 parent: &H,
335) -> Result<(), ConsensusError> {
336 if header.timestamp() <= parent.timestamp() {
337 return Err(ConsensusError::TimestampIsInPast {
338 parent_timestamp: parent.timestamp(),
339 timestamp: header.timestamp(),
340 })
341 }
342 Ok(())
343}
344
345#[inline]
350pub fn validate_against_parent_gas_limit<
351 H: BlockHeader,
352 ChainSpec: EthChainSpec + EthereumHardforks,
353>(
354 header: &SealedHeader<H>,
355 parent: &SealedHeader<H>,
356 chain_spec: &ChainSpec,
357) -> Result<(), ConsensusError> {
358 let parent_gas_limit = if !chain_spec.is_london_active_at_block(parent.number()) &&
360 chain_spec.is_london_active_at_block(header.number())
361 {
362 parent.gas_limit() *
363 chain_spec.base_fee_params_at_timestamp(header.timestamp()).elasticity_multiplier
364 as u64
365 } else {
366 parent.gas_limit()
367 };
368
369 if header.gas_limit() > parent_gas_limit {
371 if header.gas_limit() - parent_gas_limit >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
372 return Err(ConsensusError::GasLimitInvalidIncrease {
373 parent_gas_limit,
374 child_gas_limit: header.gas_limit(),
375 })
376 }
377 }
378 else if parent_gas_limit - header.gas_limit() >= parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR {
380 return Err(ConsensusError::GasLimitInvalidDecrease {
381 parent_gas_limit,
382 child_gas_limit: header.gas_limit(),
383 })
384 }
385 else if header.gas_limit() < MINIMUM_GAS_LIMIT {
387 return Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: header.gas_limit() })
388 }
389
390 Ok(())
391}
392
393pub fn validate_against_parent_4844<H: BlockHeader>(
398 header: &H,
399 parent: &H,
400 blob_params: BlobParams,
401) -> Result<(), ConsensusError> {
402 let parent_blob_gas_used = parent.blob_gas_used().unwrap_or(0);
409 let parent_excess_blob_gas = parent.excess_blob_gas().unwrap_or(0);
410
411 if header.blob_gas_used().is_none() {
412 return Err(ConsensusError::BlobGasUsedMissing)
413 }
414 let excess_blob_gas = header.excess_blob_gas().ok_or(ConsensusError::ExcessBlobGasMissing)?;
415
416 let parent_base_fee_per_gas = parent.base_fee_per_gas().unwrap_or(0);
417 let expected_excess_blob_gas = blob_params.next_block_excess_blob_gas_osaka(
418 parent_excess_blob_gas,
419 parent_blob_gas_used,
420 parent_base_fee_per_gas,
421 );
422 if expected_excess_blob_gas != excess_blob_gas {
423 return Err(ConsensusError::ExcessBlobGasDiff {
424 diff: GotExpected { got: excess_blob_gas, expected: expected_excess_blob_gas },
425 parent_excess_blob_gas,
426 parent_blob_gas_used,
427 })
428 }
429
430 Ok(())
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436 use alloy_consensus::{BlockBody, Header, TxEip4844};
437 use alloy_eips::eip4895::Withdrawals;
438 use alloy_primitives::{Address, Bytes, Signature, U256};
439 use rand::Rng;
440 use reth_chainspec::ChainSpecBuilder;
441 use reth_ethereum_primitives::{Transaction, TransactionSigned};
442 use reth_primitives_traits::proofs;
443
444 fn mock_blob_tx(nonce: u64, num_blobs: usize) -> TransactionSigned {
445 let mut rng = rand::rng();
446 let request = Transaction::Eip4844(TxEip4844 {
447 chain_id: 1u64,
448 nonce,
449 max_fee_per_gas: 0x28f000fff,
450 max_priority_fee_per_gas: 0x28f000fff,
451 max_fee_per_blob_gas: 0x7,
452 gas_limit: 10,
453 to: Address::default(),
454 value: U256::from(3_u64),
455 input: Bytes::from(vec![1, 2]),
456 access_list: Default::default(),
457 blob_versioned_hashes: std::iter::repeat_with(|| rng.random())
458 .take(num_blobs)
459 .collect(),
460 });
461
462 let signature = Signature::new(U256::default(), U256::default(), true);
463
464 TransactionSigned::new_unhashed(request, signature)
465 }
466
467 #[test]
468 fn cancun_block_incorrect_blob_gas_used() {
469 let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
470
471 let transaction = mock_blob_tx(1, 10);
473
474 let header = Header {
475 base_fee_per_gas: Some(1337),
476 withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
477 blob_gas_used: Some(1),
478 transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
479 &transaction,
480 )),
481 ..Default::default()
482 };
483 let body = BlockBody {
484 transactions: vec![transaction],
485 ommers: vec![],
486 withdrawals: Some(Withdrawals::default()),
487 };
488
489 let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
490
491 let expected_blob_gas_used = 10 * DATA_GAS_PER_BLOB;
493
494 assert_eq!(
496 validate_block_pre_execution(&block, &chain_spec),
497 Err(ConsensusError::BlobGasUsedDiff(GotExpected {
498 got: 1,
499 expected: expected_blob_gas_used
500 }))
501 );
502 }
503
504 #[test]
505 fn validate_header_extra_data_with_custom_limit() {
506 let header_32 = Header { extra_data: Bytes::from(vec![0; 32]), ..Default::default() };
508 assert!(validate_header_extra_data(&header_32, 32).is_ok());
509
510 let header_33 = Header { extra_data: Bytes::from(vec![0; 33]), ..Default::default() };
512 assert_eq!(
513 validate_header_extra_data(&header_33, 32),
514 Err(ConsensusError::ExtraDataExceedsMax { len: 33 })
515 );
516
517 assert!(validate_header_extra_data(&header_33, 64).is_ok());
519 }
520}