1use alloy_consensus::{
4 constants::MAXIMUM_EXTRA_DATA_SIZE, BlockHeader as _, 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;
9use reth_primitives_traits::{
10 constants::MAXIMUM_GAS_LIMIT_BLOCK, Block, BlockBody, BlockHeader, GotExpected, SealedBlock,
11 SealedHeader,
12};
13
14pub const MAX_RLP_BLOCK_SIZE: usize = 8_388_608;
20
21#[inline]
23pub fn validate_header_gas<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
24 if header.gas_used() > header.gas_limit() {
25 return Err(ConsensusError::HeaderGasUsedExceedsGasLimit {
26 gas_used: header.gas_used(),
27 gas_limit: header.gas_limit(),
28 })
29 }
30 if header.gas_limit() > MAXIMUM_GAS_LIMIT_BLOCK {
32 return Err(ConsensusError::HeaderGasLimitExceedsMax { gas_limit: header.gas_limit() })
33 }
34 Ok(())
35}
36
37#[inline]
39pub fn validate_header_base_fee<H: BlockHeader, ChainSpec: EthereumHardforks>(
40 header: &H,
41 chain_spec: &ChainSpec,
42) -> Result<(), ConsensusError> {
43 if chain_spec.is_london_active_at_block(header.number()) && header.base_fee_per_gas().is_none()
44 {
45 return Err(ConsensusError::BaseFeeMissing)
46 }
47 Ok(())
48}
49
50#[inline]
56pub fn validate_shanghai_withdrawals<B: Block>(
57 block: &SealedBlock<B>,
58) -> Result<(), ConsensusError> {
59 let withdrawals = block.body().withdrawals().ok_or(ConsensusError::BodyWithdrawalsMissing)?;
60 let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root(withdrawals);
61 let header_withdrawals_root =
62 block.withdrawals_root().ok_or(ConsensusError::WithdrawalsRootMissing)?;
63 if withdrawals_root != *header_withdrawals_root {
64 return Err(ConsensusError::BodyWithdrawalsRootDiff(
65 GotExpected { got: withdrawals_root, expected: header_withdrawals_root }.into(),
66 ));
67 }
68 Ok(())
69}
70
71#[inline]
77pub fn validate_cancun_gas<B: Block>(block: &SealedBlock<B>) -> Result<(), ConsensusError> {
78 let header_blob_gas_used = block.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
81 let total_blob_gas = block.body().blob_gas_used();
82 if total_blob_gas != header_blob_gas_used {
83 return Err(ConsensusError::BlobGasUsedDiff(GotExpected {
84 got: header_blob_gas_used,
85 expected: total_blob_gas,
86 }));
87 }
88 Ok(())
89}
90
91pub fn validate_body_against_header<B, H>(body: &B, header: &H) -> Result<(), ConsensusError>
98where
99 B: BlockBody,
100 H: BlockHeader,
101{
102 let ommers_hash = body.calculate_ommers_root();
103 if Some(header.ommers_hash()) != ommers_hash {
104 return Err(ConsensusError::BodyOmmersHashDiff(
105 GotExpected {
106 got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
107 expected: header.ommers_hash(),
108 }
109 .into(),
110 ))
111 }
112
113 let tx_root = body.calculate_tx_root();
114 if header.transactions_root() != tx_root {
115 return Err(ConsensusError::BodyTransactionRootDiff(
116 GotExpected { got: tx_root, expected: header.transactions_root() }.into(),
117 ))
118 }
119
120 match (header.withdrawals_root(), body.calculate_withdrawals_root()) {
121 (Some(header_withdrawals_root), Some(withdrawals_root)) => {
122 if withdrawals_root != header_withdrawals_root {
123 return Err(ConsensusError::BodyWithdrawalsRootDiff(
124 GotExpected { got: withdrawals_root, expected: header_withdrawals_root }.into(),
125 ))
126 }
127 }
128 (None, None) => {
129 }
131 _ => return Err(ConsensusError::WithdrawalsRootUnexpected),
132 }
133
134 Ok(())
135}
136
137pub fn validate_block_pre_execution<B, ChainSpec>(
143 block: &SealedBlock<B>,
144 chain_spec: &ChainSpec,
145) -> Result<(), ConsensusError>
146where
147 B: Block,
148 ChainSpec: EthereumHardforks,
149{
150 post_merge_hardfork_fields(block, chain_spec)?;
151
152 if let Err(error) = block.ensure_transaction_root_valid() {
154 return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
155 }
156
157 Ok(())
158}
159
160pub fn post_merge_hardfork_fields<B, ChainSpec>(
169 block: &SealedBlock<B>,
170 chain_spec: &ChainSpec,
171) -> Result<(), ConsensusError>
172where
173 B: Block,
174 ChainSpec: EthereumHardforks,
175{
176 let ommers_hash = block.body().calculate_ommers_root();
178 if Some(block.ommers_hash()) != ommers_hash {
179 return Err(ConsensusError::BodyOmmersHashDiff(
180 GotExpected {
181 got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
182 expected: block.ommers_hash(),
183 }
184 .into(),
185 ))
186 }
187
188 if chain_spec.is_shanghai_active_at_timestamp(block.timestamp()) {
190 validate_shanghai_withdrawals(block)?;
191 }
192
193 if chain_spec.is_cancun_active_at_timestamp(block.timestamp()) {
194 validate_cancun_gas(block)?;
195 }
196
197 if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) &&
198 block.rlp_length() > MAX_RLP_BLOCK_SIZE
199 {
200 return Err(ConsensusError::BlockTooLarge {
201 rlp_length: block.rlp_length(),
202 max_rlp_length: MAX_RLP_BLOCK_SIZE,
203 })
204 }
205
206 Ok(())
207}
208
209pub fn validate_4844_header_standalone<H: BlockHeader>(
220 header: &H,
221 blob_params: BlobParams,
222) -> Result<(), ConsensusError> {
223 let blob_gas_used = header.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
224
225 if header.parent_beacon_block_root().is_none() {
226 return Err(ConsensusError::ParentBeaconBlockRootMissing)
227 }
228
229 if !blob_gas_used.is_multiple_of(DATA_GAS_PER_BLOB) {
230 return Err(ConsensusError::BlobGasUsedNotMultipleOfBlobGasPerBlob {
231 blob_gas_used,
232 blob_gas_per_blob: DATA_GAS_PER_BLOB,
233 })
234 }
235
236 if blob_gas_used > blob_params.max_blob_gas_per_block() {
237 return Err(ConsensusError::BlobGasUsedExceedsMaxBlobGasPerBlock {
238 blob_gas_used,
239 max_blob_gas_per_block: blob_params.max_blob_gas_per_block(),
240 })
241 }
242
243 Ok(())
244}
245
246#[inline]
251pub fn validate_header_extra_data<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
252 let extra_data_len = header.extra_data().len();
253 if extra_data_len > MAXIMUM_EXTRA_DATA_SIZE {
254 Err(ConsensusError::ExtraDataExceedsMax { len: extra_data_len })
255 } else {
256 Ok(())
257 }
258}
259
260#[inline]
265pub fn validate_against_parent_hash_number<H: BlockHeader>(
266 header: &H,
267 parent: &SealedHeader<H>,
268) -> Result<(), ConsensusError> {
269 if parent.number() + 1 != header.number() {
271 return Err(ConsensusError::ParentBlockNumberMismatch {
272 parent_block_number: parent.number(),
273 block_number: header.number(),
274 })
275 }
276
277 if parent.hash() != header.parent_hash() {
278 return Err(ConsensusError::ParentHashMismatch(
279 GotExpected { got: header.parent_hash(), expected: parent.hash() }.into(),
280 ))
281 }
282
283 Ok(())
284}
285
286#[inline]
288pub fn validate_against_parent_eip1559_base_fee<ChainSpec: EthChainSpec + EthereumHardforks>(
289 header: &ChainSpec::Header,
290 parent: &ChainSpec::Header,
291 chain_spec: &ChainSpec,
292) -> Result<(), ConsensusError> {
293 if chain_spec.is_london_active_at_block(header.number()) {
294 let base_fee = header.base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)?;
295
296 let expected_base_fee = if chain_spec
297 .ethereum_fork_activation(EthereumHardfork::London)
298 .transitions_at_block(header.number())
299 {
300 alloy_eips::eip1559::INITIAL_BASE_FEE
301 } else {
302 chain_spec
303 .next_block_base_fee(parent, header.timestamp())
304 .ok_or(ConsensusError::BaseFeeMissing)?
305 };
306 if expected_base_fee != base_fee {
307 return Err(ConsensusError::BaseFeeDiff(GotExpected {
308 expected: expected_base_fee,
309 got: base_fee,
310 }))
311 }
312 }
313
314 Ok(())
315}
316
317#[inline]
319pub fn validate_against_parent_timestamp<H: BlockHeader>(
320 header: &H,
321 parent: &H,
322) -> Result<(), ConsensusError> {
323 if header.timestamp() <= parent.timestamp() {
324 return Err(ConsensusError::TimestampIsInPast {
325 parent_timestamp: parent.timestamp(),
326 timestamp: header.timestamp(),
327 })
328 }
329 Ok(())
330}
331
332pub fn validate_against_parent_4844<H: BlockHeader>(
337 header: &H,
338 parent: &H,
339 blob_params: BlobParams,
340) -> Result<(), ConsensusError> {
341 let parent_blob_gas_used = parent.blob_gas_used().unwrap_or(0);
348 let parent_excess_blob_gas = parent.excess_blob_gas().unwrap_or(0);
349
350 if header.blob_gas_used().is_none() {
351 return Err(ConsensusError::BlobGasUsedMissing)
352 }
353 let excess_blob_gas = header.excess_blob_gas().ok_or(ConsensusError::ExcessBlobGasMissing)?;
354
355 let parent_base_fee_per_gas = parent.base_fee_per_gas().unwrap_or(0);
356 let expected_excess_blob_gas = blob_params.next_block_excess_blob_gas_osaka(
357 parent_excess_blob_gas,
358 parent_blob_gas_used,
359 parent_base_fee_per_gas,
360 );
361 if expected_excess_blob_gas != excess_blob_gas {
362 return Err(ConsensusError::ExcessBlobGasDiff {
363 diff: GotExpected { got: excess_blob_gas, expected: expected_excess_blob_gas },
364 parent_excess_blob_gas,
365 parent_blob_gas_used,
366 })
367 }
368
369 Ok(())
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use alloy_consensus::{BlockBody, Header, TxEip4844};
376 use alloy_eips::eip4895::Withdrawals;
377 use alloy_primitives::{Address, Bytes, Signature, U256};
378 use rand::Rng;
379 use reth_chainspec::ChainSpecBuilder;
380 use reth_ethereum_primitives::{Transaction, TransactionSigned};
381 use reth_primitives_traits::proofs;
382
383 fn mock_blob_tx(nonce: u64, num_blobs: usize) -> TransactionSigned {
384 let mut rng = rand::rng();
385 let request = Transaction::Eip4844(TxEip4844 {
386 chain_id: 1u64,
387 nonce,
388 max_fee_per_gas: 0x28f000fff,
389 max_priority_fee_per_gas: 0x28f000fff,
390 max_fee_per_blob_gas: 0x7,
391 gas_limit: 10,
392 to: Address::default(),
393 value: U256::from(3_u64),
394 input: Bytes::from(vec![1, 2]),
395 access_list: Default::default(),
396 blob_versioned_hashes: std::iter::repeat_with(|| rng.random())
397 .take(num_blobs)
398 .collect(),
399 });
400
401 let signature = Signature::new(U256::default(), U256::default(), true);
402
403 TransactionSigned::new_unhashed(request, signature)
404 }
405
406 #[test]
407 fn cancun_block_incorrect_blob_gas_used() {
408 let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
409
410 let transaction = mock_blob_tx(1, 10);
412
413 let header = Header {
414 base_fee_per_gas: Some(1337),
415 withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
416 blob_gas_used: Some(1),
417 transactions_root: proofs::calculate_transaction_root(std::slice::from_ref(
418 &transaction,
419 )),
420 ..Default::default()
421 };
422 let body = BlockBody {
423 transactions: vec![transaction],
424 ommers: vec![],
425 withdrawals: Some(Withdrawals::default()),
426 };
427
428 let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
429
430 let expected_blob_gas_used = 10 * DATA_GAS_PER_BLOB;
432
433 assert_eq!(
435 validate_block_pre_execution(&block, &chain_spec),
436 Err(ConsensusError::BlobGasUsedDiff(GotExpected {
437 got: 1,
438 expected: expected_blob_gas_used
439 }))
440 );
441 }
442}