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