Skip to main content

ethrex_common/types/
block.rs

1use super::{
2    BASE_FEE_MAX_CHANGE_DENOMINATOR, ChainConfig, Fork, ForkBlobSchedule,
3    GAS_LIMIT_ADJUSTMENT_FACTOR, GAS_LIMIT_MINIMUM, INITIAL_BASE_FEE,
4};
5use crate::{
6    Address, H256, U256,
7    constants::{
8        BLOB_BASE_COST, DEFAULT_OMMERS_HASH, EMPTY_WITHDRAWALS_HASH, GAS_PER_BLOB,
9        MIN_BASE_FEE_PER_BLOB_GAS,
10    },
11    types::{Receipt, Transaction},
12};
13use bytes::Bytes;
14use ethereum_types::Bloom;
15use ethrex_crypto::{Crypto, CryptoError, NativeCrypto};
16use ethrex_rlp::{
17    decode::RLPDecode,
18    encode::RLPEncode,
19    error::RLPDecodeError,
20    structs::{Decoder, Encoder},
21};
22use ethrex_trie::Trie;
23#[cfg(feature = "rayon")]
24use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
25use rkyv::{Archive, Deserialize as RDeserialize, Serialize as RSerialize};
26use serde::{Deserialize, Serialize};
27
28use std::cmp::{Ordering, max};
29
30pub type BlockNumber = u64;
31pub type BlockHash = H256;
32
33#[cfg(all(feature = "zisk", target_arch = "riscv64"))]
34use super::unsync_cell::OnceCell;
35#[cfg(not(all(feature = "zisk", target_arch = "riscv64")))]
36use once_cell::sync::OnceCell;
37
38#[derive(
39    PartialEq, Eq, Debug, Clone, Deserialize, Serialize, Default, RSerialize, RDeserialize, Archive,
40)]
41pub struct Block {
42    pub header: BlockHeader,
43    pub body: BlockBody,
44}
45
46impl Block {
47    pub fn new(header: BlockHeader, body: BlockBody) -> Block {
48        Block { header, body }
49    }
50
51    pub fn hash(&self) -> BlockHash {
52        self.header.hash()
53    }
54}
55
56impl RLPEncode for Block {
57    fn encode(&self, buf: &mut dyn bytes::BufMut) {
58        Encoder::new(buf)
59            .encode_field(&self.header)
60            .encode_field(&self.body.transactions)
61            .encode_field(&self.body.ommers)
62            .encode_optional_field(&self.body.withdrawals)
63            .finish();
64    }
65}
66
67impl RLPDecode for Block {
68    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
69        let decoder = Decoder::new(rlp)?;
70        let (header, decoder) = decoder.decode_field("header")?;
71        let (transactions, decoder) = decoder.decode_field("transactions")?;
72        let (ommers, decoder) = decoder.decode_field("ommers")?;
73        let (withdrawals, decoder) = decoder.decode_optional_field();
74        let remaining = decoder.finish()?;
75        let body = BlockBody {
76            transactions,
77            ommers,
78            withdrawals,
79        };
80        let block = Block::new(header, body);
81        Ok((block, remaining))
82    }
83}
84
85/// Header part of a block on the chain.
86#[derive(Clone, Debug, Serialize, Default, Deserialize, RSerialize, RDeserialize, Archive, Eq)]
87#[serde(rename_all = "camelCase")]
88pub struct BlockHeader {
89    #[serde(skip)]
90    #[rkyv(with=rkyv::with::Skip)]
91    pub hash: OnceCell<BlockHash>,
92    #[rkyv(with=crate::rkyv_utils::H256Wrapper)]
93    pub parent_hash: H256,
94    #[serde(rename = "sha3Uncles")]
95    #[rkyv(with=crate::rkyv_utils::H256Wrapper)]
96    pub ommers_hash: H256, // ommer = uncle
97    #[rkyv(with=crate::rkyv_utils::H160Wrapper)]
98    #[serde(rename = "miner")]
99    pub coinbase: Address,
100    #[rkyv(with=crate::rkyv_utils::H256Wrapper)]
101    pub state_root: H256,
102    #[rkyv(with=crate::rkyv_utils::H256Wrapper)]
103    pub transactions_root: H256,
104    #[rkyv(with=crate::rkyv_utils::H256Wrapper)]
105    pub receipts_root: H256,
106    #[rkyv(with=crate::rkyv_utils::BloomWrapper)]
107    pub logs_bloom: Bloom,
108    #[serde(default)]
109    #[rkyv(with=crate::rkyv_utils::U256Wrapper)]
110    pub difficulty: U256,
111    #[serde(with = "crate::serde_utils::u64::hex_str")]
112    pub number: BlockNumber,
113    #[serde(with = "crate::serde_utils::u64::hex_str")]
114    pub gas_limit: u64,
115    #[serde(with = "crate::serde_utils::u64::hex_str")]
116    pub gas_used: u64,
117    #[serde(with = "crate::serde_utils::u64::hex_str")]
118    pub timestamp: u64,
119    #[serde(with = "crate::serde_utils::bytes")]
120    #[rkyv(with= crate::rkyv_utils::BytesWrapper)]
121    pub extra_data: Bytes,
122    #[serde(rename = "mixHash")]
123    #[rkyv(with=crate::rkyv_utils::H256Wrapper)]
124    pub prev_randao: H256,
125    #[serde(with = "crate::serde_utils::u64::hex_str_padding")]
126    pub nonce: u64,
127    #[serde(default, with = "crate::serde_utils::u64::hex_str_opt")]
128    pub base_fee_per_gas: Option<u64>,
129    #[rkyv(with=crate::rkyv_utils::OptionH256Wrapper)]
130    pub withdrawals_root: Option<H256>,
131    #[serde(
132        skip_serializing_if = "Option::is_none",
133        with = "crate::serde_utils::u64::hex_str_opt",
134        default = "Option::default"
135    )]
136    pub blob_gas_used: Option<u64>,
137    #[serde(
138        skip_serializing_if = "Option::is_none",
139        with = "crate::serde_utils::u64::hex_str_opt",
140        default = "Option::default"
141    )]
142    pub excess_blob_gas: Option<u64>,
143    #[rkyv(with=crate::rkyv_utils::OptionH256Wrapper)]
144    pub parent_beacon_block_root: Option<H256>,
145    #[serde(skip_serializing_if = "Option::is_none", default = "Option::default")]
146    #[rkyv(with=crate::rkyv_utils::OptionH256Wrapper)]
147    pub requests_hash: Option<H256>,
148    // Amsterdam fork fields (EIP-7928)
149    #[serde(skip_serializing_if = "Option::is_none", default = "Option::default")]
150    #[rkyv(with=crate::rkyv_utils::OptionH256Wrapper)]
151    pub block_access_list_hash: Option<H256>,
152    #[serde(
153        skip_serializing_if = "Option::is_none",
154        with = "crate::serde_utils::u64::hex_str_opt",
155        default = "Option::default"
156    )]
157    pub slot_number: Option<u64>,
158    // LStar fork fields (EIP-8079)
159    #[serde(
160        skip_serializing_if = "Option::is_none",
161        with = "crate::serde_utils::u64::hex_str_opt",
162        default = "Option::default"
163    )]
164    pub burned_fees: Option<u64>,
165}
166
167// Needs a explicit impl due to the hash OnceLock.
168impl PartialEq for BlockHeader {
169    fn eq(&self, other: &Self) -> bool {
170        let BlockHeader {
171            hash: _,
172            parent_hash,
173            ommers_hash,
174            coinbase,
175            state_root,
176            transactions_root,
177            receipts_root,
178            logs_bloom,
179            difficulty,
180            number,
181            gas_limit,
182            gas_used,
183            timestamp,
184            extra_data,
185            prev_randao,
186            nonce,
187            base_fee_per_gas,
188            withdrawals_root,
189            blob_gas_used,
190            excess_blob_gas,
191            parent_beacon_block_root,
192            requests_hash,
193            block_access_list_hash,
194            slot_number,
195            burned_fees,
196        } = self;
197
198        parent_hash == &other.parent_hash
199            && number == &other.number
200            && timestamp == &other.timestamp
201            && nonce == &other.nonce
202            && gas_used == &other.gas_used
203            && gas_limit == &other.gas_limit
204            && base_fee_per_gas == &other.base_fee_per_gas
205            && blob_gas_used == &other.blob_gas_used
206            && excess_blob_gas == &other.excess_blob_gas
207            && parent_beacon_block_root == &other.parent_beacon_block_root
208            && prev_randao == &other.prev_randao
209            && coinbase == &other.coinbase
210            && state_root == &other.state_root
211            && transactions_root == &other.transactions_root
212            && receipts_root == &other.receipts_root
213            && withdrawals_root == &other.withdrawals_root
214            && difficulty == &other.difficulty
215            && ommers_hash == &other.ommers_hash
216            && requests_hash == &other.requests_hash
217            && block_access_list_hash == &other.block_access_list_hash
218            && slot_number == &other.slot_number
219            && burned_fees == &other.burned_fees
220            && logs_bloom == &other.logs_bloom
221            && extra_data == &other.extra_data
222    }
223}
224
225impl RLPEncode for BlockHeader {
226    fn encode(&self, buf: &mut dyn bytes::BufMut) {
227        Encoder::new(buf)
228            .encode_field(&self.parent_hash)
229            .encode_field(&self.ommers_hash)
230            .encode_field(&self.coinbase)
231            .encode_field(&self.state_root)
232            .encode_field(&self.transactions_root)
233            .encode_field(&self.receipts_root)
234            .encode_field(&self.logs_bloom)
235            .encode_field(&self.difficulty)
236            .encode_field(&self.number)
237            .encode_field(&self.gas_limit)
238            .encode_field(&self.gas_used)
239            .encode_field(&self.timestamp)
240            .encode_field(&self.extra_data)
241            .encode_field(&self.prev_randao)
242            .encode_field(&self.nonce.to_be_bytes())
243            .encode_optional_field(&self.base_fee_per_gas)
244            .encode_optional_field(&self.withdrawals_root)
245            .encode_optional_field(&self.blob_gas_used)
246            .encode_optional_field(&self.excess_blob_gas)
247            .encode_optional_field(&self.parent_beacon_block_root)
248            .encode_optional_field(&self.requests_hash)
249            .encode_optional_field(&self.block_access_list_hash)
250            .encode_optional_field(&self.slot_number)
251            .encode_optional_field(&self.burned_fees)
252            .finish();
253    }
254}
255
256impl RLPDecode for BlockHeader {
257    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
258        let decoder = Decoder::new(rlp)?;
259        let (parent_hash, decoder) = decoder.decode_field("parent_hash")?;
260        let (ommers_hash, decoder) = decoder.decode_field("ommers_hash")?;
261        let (coinbase, decoder) = decoder.decode_field("coinbase")?;
262        let (state_root, decoder) = decoder.decode_field("state_root")?;
263        let (transactions_root, decoder) = decoder.decode_field("transactions_root")?;
264        let (receipts_root, decoder) = decoder.decode_field("receipts_root")?;
265        let (logs_bloom, decoder) = decoder.decode_field("logs_bloom")?;
266        let (difficulty, decoder) = decoder.decode_field("difficulty")?;
267        let (number, decoder) = decoder.decode_field("number")?;
268        let (gas_limit, decoder) = decoder.decode_field("gas_limit")?;
269        let (gas_used, decoder) = decoder.decode_field("gas_used")?;
270        let (timestamp, decoder) = decoder.decode_field("timestamp")?;
271        let (extra_data, decoder) = decoder.decode_field("extra_data")?;
272        let (prev_randao, decoder) = decoder.decode_field("prev_randao")?;
273        let (nonce, decoder) = decoder.decode_field("nonce")?;
274        let nonce = u64::from_be_bytes(nonce);
275        let (base_fee_per_gas, decoder) = decoder.decode_optional_field();
276        let (withdrawals_root, decoder) = decoder.decode_optional_field();
277        let (blob_gas_used, decoder) = decoder.decode_optional_field();
278        let (excess_blob_gas, decoder) = decoder.decode_optional_field();
279        let (parent_beacon_block_root, decoder) = decoder.decode_optional_field();
280        let (requests_hash, decoder) = decoder.decode_optional_field();
281        let (block_access_list_hash, decoder) = decoder.decode_optional_field();
282        let (slot_number, decoder) = decoder.decode_optional_field();
283        let (burned_fees, decoder) = decoder.decode_optional_field();
284
285        Ok((
286            BlockHeader {
287                hash: OnceCell::new(),
288                parent_hash,
289                ommers_hash,
290                coinbase,
291                state_root,
292                transactions_root,
293                receipts_root,
294                logs_bloom,
295                difficulty,
296                number,
297                gas_limit,
298                gas_used,
299                timestamp,
300                extra_data,
301                prev_randao,
302                nonce,
303                base_fee_per_gas,
304                withdrawals_root,
305                blob_gas_used,
306                excess_blob_gas,
307                parent_beacon_block_root,
308                requests_hash,
309                block_access_list_hash,
310                slot_number,
311                burned_fees,
312            },
313            decoder.finish()?,
314        ))
315    }
316}
317
318// The body of a block on the chain
319#[derive(
320    Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default, RSerialize, RDeserialize, Archive,
321)]
322pub struct BlockBody {
323    pub transactions: Vec<Transaction>,
324    // TODO: ommers list is always empty, so we can remove it
325    #[serde(rename = "uncles")]
326    pub ommers: Vec<BlockHeader>,
327    pub withdrawals: Option<Vec<Withdrawal>>,
328}
329
330impl BlockBody {
331    pub const fn empty() -> Self {
332        Self {
333            transactions: Vec::new(),
334            ommers: Vec::new(),
335            withdrawals: Some(Vec::new()),
336        }
337    }
338
339    pub fn get_transactions_with_sender(
340        &self,
341        crypto: &dyn Crypto,
342    ) -> Result<Vec<(&Transaction, Address)>, CryptoError> {
343        // Recovering addresses is computationally expensive.
344        // Computing them in parallel greatly reduces execution time.
345        // Without rayon, use sequential iteration
346        #[cfg(feature = "rayon")]
347        return self
348            .transactions
349            .par_iter()
350            .map(|tx| Ok((tx, tx.sender(crypto)?)))
351            .collect::<Result<Vec<(&Transaction, Address)>, CryptoError>>();
352
353        #[cfg(not(feature = "rayon"))]
354        self.transactions
355            .iter()
356            .map(|tx| Ok((tx, tx.sender(crypto)?)))
357            .collect::<Result<Vec<(&Transaction, Address)>, CryptoError>>()
358    }
359}
360
361pub fn compute_transactions_root(transactions: &[Transaction], crypto: &dyn Crypto) -> H256 {
362    let iter = transactions.iter().enumerate().map(|(idx, tx)| {
363        // Key: RLP(tx_index)
364        // Value: tx_type || RLP(tx)  if tx_type != 0
365        //                   RLP(tx)  else
366        (idx.encode_to_vec(), tx.encode_canonical_to_vec())
367    });
368    Trie::compute_hash_from_unsorted_iter(iter, crypto)
369}
370
371pub fn compute_receipts_root(receipts: &[Receipt], crypto: &dyn Crypto) -> H256 {
372    let iter = receipts
373        .iter()
374        .enumerate()
375        .map(|(idx, receipt)| (idx.encode_to_vec(), receipt.encode_inner_with_bloom(crypto)));
376    Trie::compute_hash_from_unsorted_iter(iter, crypto)
377}
378
379/// Computes the receipts root and the aggregate header `logs_bloom` in a single pass,
380/// hashing each receipt's bloom only once (it feeds both the receipts trie and the
381/// OR-ed header bloom). Validation paths need both, so this avoids the duplicate
382/// `bloom_from_logs` keccak work — relevant in the zkVM guest where it is cycle-counted.
383pub fn compute_receipts_root_and_logs_bloom(
384    receipts: &[Receipt],
385    crypto: &dyn Crypto,
386) -> (H256, Bloom) {
387    let mut logs_bloom = Bloom::zero();
388    let iter = receipts.iter().enumerate().map(|(idx, receipt)| {
389        let bloom = crate::types::bloom_from_logs(&receipt.logs, crypto);
390        logs_bloom |= bloom;
391        (
392            idx.encode_to_vec(),
393            receipt.encode_inner_with_precomputed_bloom(bloom),
394        )
395    });
396    let receipts_root = Trie::compute_hash_from_unsorted_iter(iter, crypto);
397    (receipts_root, logs_bloom)
398}
399
400// See [EIP-4895](https://eips.ethereum.org/EIPS/eip-4895)
401pub fn compute_withdrawals_root(withdrawals: &[Withdrawal], crypto: &dyn Crypto) -> H256 {
402    let iter = withdrawals
403        .iter()
404        .enumerate()
405        .map(|(idx, withdrawal)| (idx.encode_to_vec(), withdrawal.encode_to_vec()));
406    Trie::compute_hash_from_unsorted_iter(iter, crypto)
407}
408
409impl RLPEncode for BlockBody {
410    fn encode(&self, buf: &mut dyn bytes::BufMut) {
411        Encoder::new(buf)
412            .encode_field(&self.transactions)
413            .encode_field(&self.ommers)
414            .encode_optional_field(&self.withdrawals)
415            .finish();
416    }
417}
418
419impl RLPDecode for BlockBody {
420    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
421        let decoder = Decoder::new(rlp)?;
422        let (transactions, decoder) = decoder.decode_field("transactions")?;
423        let (ommers, decoder) = decoder.decode_field("ommers")?;
424        let (withdrawals, decoder) = decoder.decode_optional_field();
425        Ok((
426            BlockBody {
427                transactions,
428                ommers,
429                withdrawals,
430            },
431            decoder.finish()?,
432        ))
433    }
434}
435
436impl BlockHeader {
437    pub fn compute_block_hash(&self, crypto: &dyn Crypto) -> H256 {
438        let mut buf = vec![];
439        self.encode(&mut buf);
440        H256(crypto.keccak256(&buf))
441    }
442
443    pub fn hash(&self) -> H256 {
444        *self
445            .hash
446            .get_or_init(|| self.compute_block_hash(&NativeCrypto))
447    }
448
449    /// Returns `self` with `burned_fees` set and the hash cache cleared.
450    ///
451    /// After `execute_blocks`, the header's `OnceCell` may already hold a stale
452    /// hash that was computed before `burned_fees` was known (e.g. during
453    /// `initialize_block_header_hashes`).  Taking ownership and clearing the
454    /// cache guarantees that the next call to `hash()` reflects the injected
455    /// value — without any extra `once_cell` dependency at the call site.
456    pub fn into_with_burned_fees(mut self, burned_fees: Option<u64>) -> Self {
457        self.burned_fees = burned_fees;
458        let _ = self.hash.take(); // clear stale cached hash
459        self
460    }
461}
462
463#[derive(
464    Clone, Debug, PartialEq, Eq, Deserialize, Serialize, RSerialize, RDeserialize, Archive,
465)]
466#[serde(rename_all = "camelCase")]
467pub struct Withdrawal {
468    #[serde(with = "crate::serde_utils::u64::hex_str")]
469    pub index: u64,
470    #[serde(with = "crate::serde_utils::u64::hex_str")]
471    pub validator_index: u64,
472    #[rkyv(with=crate::rkyv_utils::H160Wrapper)]
473    pub address: Address,
474    #[serde(with = "crate::serde_utils::u64::hex_str")]
475    pub amount: u64,
476}
477
478impl RLPEncode for Withdrawal {
479    fn encode(&self, buf: &mut dyn bytes::BufMut) {
480        Encoder::new(buf)
481            .encode_field(&self.index)
482            .encode_field(&self.validator_index)
483            .encode_field(&self.address)
484            .encode_field(&self.amount)
485            .finish();
486    }
487}
488
489impl RLPDecode for Withdrawal {
490    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
491        let decoder = Decoder::new(rlp)?;
492        let (index, decoder) = decoder.decode_field("index")?;
493        let (validator_index, decoder) = decoder.decode_field("validator_index")?;
494        let (address, decoder) = decoder.decode_field("address")?;
495        let (amount, decoder) = decoder.decode_field("amount")?;
496        Ok((
497            Withdrawal {
498                index,
499                validator_index,
500                address,
501                amount,
502            },
503            decoder.finish()?,
504        ))
505    }
506}
507
508// Checks that the gas_limit fits the gas bounds set by its parent block
509fn check_gas_limit(gas_limit: u64, parent_gas_limit: u64) -> bool {
510    let max_adjustment_delta = parent_gas_limit / GAS_LIMIT_ADJUSTMENT_FACTOR;
511
512    gas_limit < parent_gas_limit + max_adjustment_delta
513        && gas_limit > parent_gas_limit - max_adjustment_delta
514        && gas_limit >= GAS_LIMIT_MINIMUM
515}
516
517/// Calculates the base fee per blob gas for the current block based on
518/// it's parent excess blob gas and the update fraction, which depends on the fork.
519pub fn calculate_base_fee_per_blob_gas(parent_excess_blob_gas: u64, update_fraction: u64) -> U256 {
520    if update_fraction == 0 {
521        return U256::zero();
522    }
523
524    fake_exponential(
525        U256::from(MIN_BASE_FEE_PER_BLOB_GAS),
526        U256::from(parent_excess_blob_gas),
527        update_fraction,
528    )
529    .unwrap_or_default()
530}
531
532/// Approximates factor * e ** (numerator / denominator) using Taylor expansion
533/// https://eips.ethereum.org/EIPS/eip-4844#helpers
534/// 400_000_000 numerator is the limit for this operation to work with U256,
535/// it will overflow with a larger numerator
536pub fn fake_exponential(
537    factor: U256,
538    numerator: U256,
539    denominator: u64,
540) -> Result<U256, FakeExponentialError> {
541    if denominator == 0 {
542        return Err(FakeExponentialError::DenominatorIsZero);
543    }
544
545    if numerator.is_zero() {
546        return Ok(factor);
547    }
548
549    let mut output: U256 = U256::zero();
550    let denominator_u256: U256 = denominator.into();
551
552    // Initial multiplication: factor * denominator
553    let mut numerator_accum = factor
554        .checked_mul(denominator_u256)
555        .ok_or(FakeExponentialError::CheckedMul)?;
556
557    let mut denominator_by_i = denominator_u256;
558
559    #[expect(
560        clippy::arithmetic_side_effects,
561        reason = "division can't overflow since denominator is not 0"
562    )]
563    {
564        while !numerator_accum.is_zero() {
565            // Safe addition to output
566            output = output
567                .checked_add(numerator_accum)
568                .ok_or(FakeExponentialError::CheckedAdd)?;
569
570            // Safe multiplication and division within loop
571            numerator_accum = numerator_accum
572                .checked_mul(numerator)
573                .ok_or(FakeExponentialError::CheckedMul)?
574                / denominator_by_i;
575
576            // denominator comes from a u64 value, will never overflow before other variables.
577            denominator_by_i += denominator_u256;
578        }
579
580        output
581            .checked_div(denominator.into())
582            .ok_or(FakeExponentialError::CheckedDiv)
583    }
584}
585
586#[derive(Debug, thiserror::Error, Serialize, Clone, PartialEq, Deserialize, Eq)]
587pub enum FakeExponentialError {
588    #[error("FakeExponentialError: Denominator cannot be zero.")]
589    DenominatorIsZero,
590    #[error("FakeExponentialError: Checked div failed is None.")]
591    CheckedDiv,
592    #[error("FakeExponentialError: Checked mul failed is None.")]
593    CheckedMul,
594    #[error("FakeExponentialError: Checked add failed is None.")]
595    CheckedAdd,
596}
597
598// Calculates the base fee for the current block based on its gas_limit and parent's gas and fee
599// Returns None if the block gas limit is not valid in relation to its parent's gas limit
600pub fn calculate_base_fee_per_gas(
601    block_gas_limit: u64,
602    parent_gas_limit: u64,
603    parent_gas_used: u64,
604    parent_base_fee_per_gas: u64,
605    elasticity_multiplier: u64,
606) -> Option<u64> {
607    // Check gas limit, if the check passes we can also rest assured that none of the
608    // following divisions will have zero as a divider
609    if !check_gas_limit(block_gas_limit, parent_gas_limit) {
610        return None;
611    }
612
613    let parent_gas_target = parent_gas_limit / elasticity_multiplier;
614
615    match parent_gas_used.cmp(&parent_gas_target) {
616        Ordering::Equal => Some(parent_base_fee_per_gas),
617        Ordering::Greater => {
618            let gas_used_delta = parent_gas_used - parent_gas_target;
619
620            let parent_fee_gas_delta =
621                u128::from(parent_base_fee_per_gas) * u128::from(gas_used_delta);
622            let target_fee_gas_delta = parent_fee_gas_delta / u128::from(parent_gas_target);
623
624            let base_fee_per_gas_delta =
625                max(target_fee_gas_delta / BASE_FEE_MAX_CHANGE_DENOMINATOR, 1);
626
627            (u128::from(parent_base_fee_per_gas) + base_fee_per_gas_delta)
628                .try_into()
629                .ok()
630        }
631        Ordering::Less => {
632            let gas_used_delta = parent_gas_target - parent_gas_used;
633
634            let parent_fee_gas_delta =
635                u128::from(parent_base_fee_per_gas) * u128::from(gas_used_delta);
636            let target_fee_gas_delta = parent_fee_gas_delta / u128::from(parent_gas_target);
637
638            let base_fee_per_gas_delta = target_fee_gas_delta / BASE_FEE_MAX_CHANGE_DENOMINATOR;
639
640            (u128::from(parent_base_fee_per_gas) - base_fee_per_gas_delta)
641                .try_into()
642                .ok()
643        }
644    }
645}
646
647#[derive(Debug, thiserror::Error)]
648pub enum InvalidBlockHeaderError {
649    #[error("Gas used is greater than gas limit")]
650    GasUsedGreaterThanGasLimit,
651    #[error("Gas limit changed more than allowed from the parent")]
652    GasLimitTooFarFromParent,
653    #[error("Base fee per gas is incorrect")]
654    BaseFeePerGasIncorrect,
655    #[error("Timestamp is not greater than parent timestamp")]
656    TimestampNotGreaterThanParent,
657    #[error("Block number is not one greater than parent number")]
658    BlockNumberNotOneGreater,
659    #[error("Extra data is too long")]
660    ExtraDataTooLong,
661    #[error("Difficulty is not zero")]
662    DifficultyNotZero,
663    #[error("Nonce is not zero")]
664    NonceNotZero,
665    #[error("Ommers hash is not the default")]
666    OmmersHashNotDefault,
667    #[error("Parent hash is incorrect")]
668    ParentHashIncorrect,
669    // Cancun fork errors
670    #[error("Excess blob gas is not present")]
671    ExcessBlobGasNotPresent,
672    #[error("Blob gas used is not present")]
673    BlobGasUsedNotPresent,
674    #[error("Excess blob gas is incorrect")]
675    ExcessBlobGasIncorrect,
676    #[error("Parent beacon block root is not present")]
677    ParentBeaconBlockRootNotPresent,
678    #[error("Requests hash is not present")]
679    RequestsHashNotPresent,
680    // Other fork errors
681    #[error("Excess blob gas is present")]
682    ExcessBlobGasPresent,
683    #[error("Blob gas used is present")]
684    BlobGasUsedPresent,
685    #[error("Parent beacon block root is present")]
686    ParentBeaconBlockRootPresent,
687    #[error("Requests hash is present")]
688    RequestsHashPresent,
689    #[error("Block access list hash is not present")]
690    BlockAccessListHashNotPresent,
691    #[error("Block access list hash is present")]
692    BlockAccessListHashPresent,
693    #[error("Slot number is not present")]
694    SlotNumberNotPresent,
695    #[error("Slot number is present")]
696    SlotNumberPresent,
697}
698
699#[derive(Debug, thiserror::Error)]
700pub enum InvalidBlockBodyError {
701    #[error("Withdrawals root does not match")]
702    WithdrawalsRootNotMatch,
703    #[error("Transactions root does not match")]
704    TransactionsRootNotMatch,
705    #[error("Ommers is not empty")]
706    OmmersIsNotEmpty,
707}
708
709/// Validates that the header fields are correct in reference to the parent_header
710pub fn validate_block_header(
711    header: &BlockHeader,
712    parent_header: &BlockHeader,
713    elasticity_multiplier: u64,
714) -> Result<(), InvalidBlockHeaderError> {
715    if header.gas_used > header.gas_limit {
716        return Err(InvalidBlockHeaderError::GasUsedGreaterThanGasLimit);
717    }
718
719    let expected_base_fee_per_gas = if let Some(base_fee) = calculate_base_fee_per_gas(
720        header.gas_limit,
721        parent_header.gas_limit,
722        parent_header.gas_used,
723        parent_header.base_fee_per_gas.unwrap_or(INITIAL_BASE_FEE),
724        elasticity_multiplier,
725    ) {
726        base_fee
727    } else {
728        return Err(InvalidBlockHeaderError::GasLimitTooFarFromParent);
729    };
730
731    if expected_base_fee_per_gas != header.base_fee_per_gas.unwrap_or(INITIAL_BASE_FEE) {
732        return Err(InvalidBlockHeaderError::BaseFeePerGasIncorrect);
733    }
734
735    if header.timestamp <= parent_header.timestamp {
736        return Err(InvalidBlockHeaderError::TimestampNotGreaterThanParent);
737    }
738
739    if header.number != parent_header.number + 1 {
740        return Err(InvalidBlockHeaderError::BlockNumberNotOneGreater);
741    }
742
743    if header.extra_data.len() > 32 {
744        return Err(InvalidBlockHeaderError::ExtraDataTooLong);
745    }
746
747    if !header.difficulty.is_zero() {
748        return Err(InvalidBlockHeaderError::DifficultyNotZero);
749    }
750
751    if header.nonce != 0 {
752        return Err(InvalidBlockHeaderError::NonceNotZero);
753    }
754
755    if header.ommers_hash != *DEFAULT_OMMERS_HASH {
756        return Err(InvalidBlockHeaderError::OmmersHashNotDefault);
757    }
758
759    if header.parent_hash != parent_header.hash() {
760        return Err(InvalidBlockHeaderError::ParentHashIncorrect);
761    }
762
763    Ok(())
764}
765
766/// Validates that the body matches with the header
767pub fn validate_block_body(
768    block_header: &BlockHeader,
769    block_body: &BlockBody,
770    crypto: &dyn Crypto,
771) -> Result<(), InvalidBlockBodyError> {
772    // Validates that:
773    //  - Transactions root and withdrawals root matches with the header
774    //  - Ommers is empty -> https://eips.ethereum.org/EIPS/eip-3675
775    let computed_tx_root = compute_transactions_root(&block_body.transactions, crypto);
776
777    if block_header.transactions_root != computed_tx_root {
778        return Err(InvalidBlockBodyError::TransactionsRootNotMatch);
779    }
780
781    if !block_body.ommers.is_empty() {
782        return Err(InvalidBlockBodyError::OmmersIsNotEmpty);
783    }
784
785    match (block_header.withdrawals_root, &block_body.withdrawals) {
786        (Some(withdrawals_root), Some(withdrawals)) => {
787            let computed_withdrawals_root = compute_withdrawals_root(withdrawals, crypto);
788            if withdrawals_root != computed_withdrawals_root {
789                return Err(InvalidBlockBodyError::WithdrawalsRootNotMatch);
790            }
791        }
792        // Post-Shanghai, the withdrawals field must be present in the body even when
793        // empty. A body that omits it is malformed, regardless of the header's
794        // withdrawals_root — geth ("missing withdrawals in block body"), reth,
795        // nethermind, erigon and besu all reject this shape.
796        (None, None) => {}
797        _ => return Err(InvalidBlockBodyError::WithdrawalsRootNotMatch),
798    }
799
800    Ok(())
801}
802
803/// Normalizes a legacy post-Shanghai body shape produced by older ethrex versions,
804/// which omitted the `withdrawals` field while the header committed to the
805/// empty-withdrawals root. `validate_block_body` rejects that shape (reference
806/// clients treat it as malformed on the wire), but blocks already sitting in a
807/// node's store or in batch blobs published to L1 are immutable history. When the
808/// header's withdrawals_root is exactly the empty-list root, `Some(vec![])` is the
809/// provably-equivalent canonical body; any other root leaves the body untouched, and
810/// validation rejects it exactly as before.
811///
812/// Only call this on bodies read from trusted local history (store re-execution,
813/// blob reconstruction) — never on bodies received from the network.
814pub fn normalize_legacy_withdrawals(header: &BlockHeader, body: &mut BlockBody) {
815    if body.withdrawals.is_none() && header.withdrawals_root == Some(*EMPTY_WITHDRAWALS_HASH) {
816        tracing::debug!(
817            block = header.number,
818            "Normalizing legacy omitted-withdrawals block body from trusted history"
819        );
820        body.withdrawals = Some(Vec::new());
821    }
822}
823
824/// Validates that only the required field are present for a Prague block
825/// Also validates excess_blob_gas value against parent's header
826pub fn validate_prague_header_fields(
827    header: &BlockHeader,
828    parent_header: &BlockHeader,
829    chain_config: &ChainConfig,
830) -> Result<(), InvalidBlockHeaderError> {
831    if header.excess_blob_gas.is_none() {
832        return Err(InvalidBlockHeaderError::ExcessBlobGasNotPresent);
833    }
834    if header.blob_gas_used.is_none() {
835        return Err(InvalidBlockHeaderError::BlobGasUsedNotPresent);
836    }
837    validate_excess_blob_gas(header, parent_header, chain_config)?;
838
839    if header.parent_beacon_block_root.is_none() {
840        return Err(InvalidBlockHeaderError::ParentBeaconBlockRootNotPresent);
841    }
842    if header.requests_hash.is_none() {
843        return Err(InvalidBlockHeaderError::RequestsHashNotPresent);
844    }
845    if chain_config.is_amsterdam_activated(header.timestamp) {
846        // EIP-7928: BAL hash and EIP-7843: slot_number are both mandatory
847        // trailing header fields on Amsterdam. A header omitting either is
848        // schema-invalid and must be rejected to match conformant clients.
849        if header.block_access_list_hash.is_none() {
850            return Err(InvalidBlockHeaderError::BlockAccessListHashNotPresent);
851        }
852        if header.slot_number.is_none() {
853            return Err(InvalidBlockHeaderError::SlotNumberNotPresent);
854        }
855    } else {
856        if header.block_access_list_hash.is_some() {
857            return Err(InvalidBlockHeaderError::BlockAccessListHashPresent);
858        }
859        if header.slot_number.is_some() {
860            return Err(InvalidBlockHeaderError::SlotNumberPresent);
861        }
862    }
863    Ok(())
864}
865
866/// Validates that only the required field are present for a Cancun block
867/// Also validates excess_blob_gas value against parent's header
868pub fn validate_cancun_header_fields(
869    header: &BlockHeader,
870    parent_header: &BlockHeader,
871    chain_config: &ChainConfig,
872) -> Result<(), InvalidBlockHeaderError> {
873    if header.excess_blob_gas.is_none() {
874        return Err(InvalidBlockHeaderError::ExcessBlobGasNotPresent);
875    }
876    if header.blob_gas_used.is_none() {
877        return Err(InvalidBlockHeaderError::BlobGasUsedNotPresent);
878    }
879    validate_excess_blob_gas(header, parent_header, chain_config)?;
880    if header.parent_beacon_block_root.is_none() {
881        return Err(InvalidBlockHeaderError::ParentBeaconBlockRootNotPresent);
882    }
883    if header.requests_hash.is_some() {
884        return Err(InvalidBlockHeaderError::RequestsHashPresent);
885    }
886    if header.block_access_list_hash.is_some() {
887        return Err(InvalidBlockHeaderError::BlockAccessListHashPresent);
888    }
889    if header.slot_number.is_some() {
890        return Err(InvalidBlockHeaderError::SlotNumberPresent);
891    }
892    Ok(())
893}
894
895/// Validates that only the required field are present for a pre Cancun block
896/// Also validates excess_blob_gas value against parent's header
897pub fn validate_pre_cancun_header_fields(
898    header: &BlockHeader,
899) -> Result<(), InvalidBlockHeaderError> {
900    if header.excess_blob_gas.is_some() {
901        return Err(InvalidBlockHeaderError::ExcessBlobGasPresent);
902    }
903    if header.blob_gas_used.is_some() {
904        return Err(InvalidBlockHeaderError::BlobGasUsedPresent);
905    }
906    if header.parent_beacon_block_root.is_some() {
907        return Err(InvalidBlockHeaderError::ParentBeaconBlockRootPresent);
908    }
909    if header.requests_hash.is_some() {
910        return Err(InvalidBlockHeaderError::RequestsHashPresent);
911    }
912    if header.block_access_list_hash.is_some() {
913        return Err(InvalidBlockHeaderError::BlockAccessListHashPresent);
914    }
915    if header.slot_number.is_some() {
916        return Err(InvalidBlockHeaderError::SlotNumberPresent);
917    }
918    Ok(())
919}
920
921fn validate_excess_blob_gas(
922    header: &BlockHeader,
923    parent_header: &BlockHeader,
924    chain_config: &ChainConfig,
925) -> Result<(), InvalidBlockHeaderError> {
926    let expected_excess_blob_gas = chain_config
927        .get_fork_blob_schedule(header.timestamp)
928        .map(|schedule| {
929            calc_excess_blob_gas(parent_header, schedule, chain_config.fork(header.timestamp))
930        })
931        .unwrap_or_default();
932    if header
933        .excess_blob_gas
934        .is_none_or(|header_excess_blob_gas| header_excess_blob_gas != expected_excess_blob_gas)
935    {
936        return Err(InvalidBlockHeaderError::ExcessBlobGasIncorrect);
937    }
938    Ok(())
939}
940
941pub fn calc_excess_blob_gas(parent: &BlockHeader, schedule: ForkBlobSchedule, fork: Fork) -> u64 {
942    let parent_blob_gas_used = parent.blob_gas_used.unwrap_or_default();
943    let parent_base_fee_per_gas = parent.base_fee_per_gas.unwrap_or_default();
944    let parent_excess_blob_gas = parent.excess_blob_gas.unwrap_or_default();
945
946    let excess_blob_gas = parent_excess_blob_gas + parent_blob_gas_used;
947    let target_blob_gas_per_block = (schedule.target * GAS_PER_BLOB) as u64;
948    if excess_blob_gas < target_blob_gas_per_block {
949        return 0;
950    }
951
952    if fork >= Fork::Osaka
953        && U256::from(BLOB_BASE_COST) * U256::from(parent_base_fee_per_gas)
954            > (U256::from(GAS_PER_BLOB))
955                * calculate_base_fee_per_blob_gas(
956                    parent_excess_blob_gas,
957                    schedule.base_fee_update_fraction,
958                )
959    {
960        return parent_excess_blob_gas
961            + parent_blob_gas_used * (schedule.max as u64 - schedule.target as u64)
962                / schedule.max as u64;
963    }
964
965    excess_blob_gas - target_blob_gas_per_block
966}
967
968#[cfg(test)]
969mod test {
970    use super::*;
971    use crate::constants::EMPTY_KECCAK_HASH;
972    use crate::types::{BLOB_BASE_FEE_UPDATE_FRACTION, ELASTICITY_MULTIPLIER, INITIAL_BASE_FEE};
973    use ethereum_types::H160;
974    use hex_literal::hex;
975    use std::str::FromStr;
976
977    #[test]
978    fn test_compute_withdrawals_root() {
979        // Source: https://github.com/ethereum/tests/blob/9760400e667eba241265016b02644ef62ab55de2/BlockchainTests/EIPTests/bc4895-withdrawals/amountIs0.json
980        // "withdrawals" : [
981        //             {
982        //                 "address" : "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b",
983        //                 "amount" : "0x00",
984        //                 "index" : "0x00",
985        //                 "validatorIndex" : "0x00"
986        //             }
987        //         ]
988        // "withdrawalsRoot" : "0x48a703da164234812273ea083e4ec3d09d028300cd325b46a6a75402e5a7ab95"
989        let withdrawals = vec![Withdrawal {
990            index: 0x00,
991            validator_index: 0x00,
992            address: H160::from_slice(&hex!("c94f5374fce5edbc8e2a8697c15331677e6ebf0b")),
993            amount: 0x00_u64,
994        }];
995        let expected_root = H256::from_slice(&hex!(
996            "48a703da164234812273ea083e4ec3d09d028300cd325b46a6a75402e5a7ab95"
997        ));
998        let root = compute_withdrawals_root(&withdrawals, &ethrex_crypto::NativeCrypto);
999        assert_eq!(root, expected_root);
1000    }
1001
1002    #[test]
1003    fn test_validate_block_header() {
1004        let parent_block = BlockHeader {
1005            parent_hash: H256::from_str(
1006                "0x0000000000000000000000000000000000000000000000000000000000000000",
1007            )
1008            .unwrap(),
1009            ommers_hash: H256::from_str(
1010                "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
1011            )
1012            .unwrap(),
1013            coinbase: Address::zero(),
1014            state_root: H256::from_str(
1015                "0x590245a249decc317041b8dc7141cec0559c533efb82221e4e0a30a6456acf8b",
1016            )
1017            .unwrap(),
1018            transactions_root: H256::from_str(
1019                "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
1020            )
1021            .unwrap(),
1022            receipts_root: H256::from_str(
1023                "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
1024            )
1025            .unwrap(),
1026            logs_bloom: Bloom::from([0; 256]),
1027            difficulty: U256::zero(),
1028            number: 0,
1029            gas_limit: 0x016345785d8a0000,
1030            gas_used: 0,
1031            timestamp: 0,
1032            extra_data: Bytes::new(),
1033            prev_randao: H256::zero(),
1034            nonce: 0x0000000000000000,
1035            base_fee_per_gas: Some(0x07),
1036            withdrawals_root: Some(
1037                H256::from_str(
1038                    "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
1039                )
1040                .unwrap(),
1041            ),
1042            blob_gas_used: Some(0x00),
1043            excess_blob_gas: Some(0x00),
1044            parent_beacon_block_root: Some(H256::zero()),
1045            requests_hash: Some(*EMPTY_KECCAK_HASH),
1046            ..Default::default()
1047        };
1048        let block = BlockHeader {
1049            parent_hash: H256::from_str(
1050                "0x48e29e7357408113a4166e04e9f1aeff0680daa2b97ba93df6512a73ddf7a154",
1051            )
1052            .unwrap(),
1053            ommers_hash: H256::from_str(
1054                "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
1055            )
1056            .unwrap(),
1057            coinbase: Address::from_str("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba").unwrap(),
1058            state_root: H256::from_str(
1059                "0x9de6f95cb4ff4ef22a73705d6ba38c4b927c7bca9887ef5d24a734bb863218d9",
1060            )
1061            .unwrap(),
1062            transactions_root: H256::from_str(
1063                "0x578602b2b7e3a3291c3eefca3a08bc13c0d194f9845a39b6f3bcf843d9fed79d",
1064            )
1065            .unwrap(),
1066            receipts_root: H256::from_str(
1067                "0x035d56bac3f47246c5eed0e6642ca40dc262f9144b582f058bc23ded72aa72fa",
1068            )
1069            .unwrap(),
1070            logs_bloom: Bloom::from([0; 256]),
1071            difficulty: U256::zero(),
1072            number: 1,
1073            gas_limit: 0x016345785d8a0000,
1074            gas_used: 0xa8de,
1075            timestamp: 0x03e8,
1076            extra_data: Bytes::new(),
1077            prev_randao: H256::zero(),
1078            nonce: 0x0000000000000000,
1079            base_fee_per_gas: Some(0x07),
1080            withdrawals_root: Some(
1081                H256::from_str(
1082                    "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
1083                )
1084                .unwrap(),
1085            ),
1086            blob_gas_used: Some(0x00),
1087            excess_blob_gas: Some(0x00),
1088            parent_beacon_block_root: Some(H256::zero()),
1089            requests_hash: Some(*EMPTY_KECCAK_HASH),
1090            ..Default::default()
1091        };
1092        assert!(validate_block_header(&block, &parent_block, ELASTICITY_MULTIPLIER).is_ok());
1093        assert_eq!(parent_block.encode_to_vec().len(), parent_block.length());
1094        assert_eq!(block.encode_to_vec().len(), block.length());
1095    }
1096
1097    #[test]
1098    fn test_compute_transactions_root() {
1099        let encoded_transactions = [
1100            "0x01f8d68330182404842daf517a830186a08080b880c1597f3c842558e64df52c3e0f0973067577c030c0c6578dbb2eef63155a21106fd4426057527f296b2ecdfabc81e34ffc82e89dec20f6b7c41fa1969d3c3bc44262c86f08b5b76077527fb7ece918787c50c878052c30a8b1d4abc07331e6d14b8ded52bbc58a6e9992b76097527f0110937c38cc13b914f201fc09dc6f7a80c001a09930cb92b4a27dce971c697a8c47fa34c98d076abc7b36e1239d6abcfc7c8403a041b35118447fe77c38c0b3a92a2dd3ecba4a9e4b35cc6534cd787f56c0cf2e21",
1101            "0xf86e81fa843127403882f61894db8d964741c53e55df9c2d4e9414c6c96482874e870aa87bee538000808360306ca03aa421df67a101c45ff9cb06ce28f518a5d8d8dbb76a79361280071909650a27a05a447ff053c4ae601cfe81859b58d5603f2d0a73481c50f348089032feb0b073",
1102            "0x02f8ef83301824048413f157f8842daf517a830186a094000000000000000000000000000000000000000080b8807a0a600060a0553db8600060c855c77fb29ecd7661d8aefe101a0db652a728af0fded622ff55d019b545d03a7532932a60ad52604260cd5360bf60ce53609460cf53603e60d05360f560d153bc596000609e55600060c6556000601f556000609155535660556057536055605853606e60595360e7605a5360d0605b5360eb60c080a03acb03b1fc20507bc66210f7e18ff5af65038fb22c626ae488ad9513d9b6debca05d38459e9d2a221eb345b0c2761b719b313d062ff1ea3d10cf5b8762c44385a6",
1103            "0x01f8ea8330182402842daf517a830186a094000000000000000000000000000000000000000080b880bdb30d976000604e557145600060a155d67fe7e473caf6e33cba341136268fc1189ba07837ef8a266570289ff53afc43436260c7527f333dfe837f4838f6053e5e46e4151aeec28f356ec39a2db9769f36ec92e3e3f660e7527f0b261608674300d4621eff679096a6ed786591aca69f2b22a3ea6949621daade610107527f3cc080a01f3f906540fb56b0576c51b3ffa86df213fd1f407378c9441cfdd9d5f3c1df3da035691b16c053b68ec74683ae020293cbc6a47ac773dc8defb96cb680c576e5a3",
1104        ];
1105        let transactions: Vec<Transaction> = encoded_transactions
1106            .iter()
1107            .map(|hex| {
1108                Transaction::decode_canonical(&hex::decode(hex.trim_start_matches("0x")).unwrap())
1109                    .unwrap()
1110            })
1111            .collect();
1112        let transactions_root =
1113            compute_transactions_root(&transactions, &ethrex_crypto::NativeCrypto);
1114        let expected_root = H256::from_slice(
1115            &hex::decode("adf0387d2303fe80aeca23bf6828c979b44d8a8fe4a1ba1d3511bc1567ca80de")
1116                .unwrap(),
1117        );
1118        assert_eq!(transactions_root, expected_root);
1119    }
1120
1121    #[test]
1122    // The values for this test were taken from sepolia testnet block number 6029872
1123    // Where a silent overflow within base fee calculations led to the wrong expected base fee
1124    fn test_calculate_base_fee_per_gas_big_numbers() {
1125        let expected_base_fee = Some(1317727380375);
1126        let block_gas_limit = 30000000;
1127        let parent_gas_limit = 30000000;
1128        let parent_gas_used = 1981764;
1129        let parent_base_fee_per_gas = 1478077008012;
1130        let calc_base_fee = calculate_base_fee_per_gas(
1131            block_gas_limit,
1132            parent_gas_limit,
1133            parent_gas_used,
1134            parent_base_fee_per_gas,
1135            ELASTICITY_MULTIPLIER,
1136        );
1137        assert_eq!(calc_base_fee, expected_base_fee)
1138    }
1139
1140    #[test]
1141    fn test_calc_blob_fee_post_osaka_bpo1() {
1142        let parent = BlockHeader {
1143            excess_blob_gas: Some(5149252),
1144            blob_gas_used: Some(1310720),
1145            base_fee_per_gas: Some(30),
1146            ..Default::default()
1147        };
1148        let schedule = ForkBlobSchedule {
1149            target: 9,
1150            max: 14,
1151            base_fee_update_fraction: 8832827,
1152        };
1153        let fork = Fork::Osaka;
1154
1155        let res = calc_excess_blob_gas(&parent, schedule, fork);
1156        assert_eq!(res, 5617366)
1157    }
1158
1159    #[test]
1160    fn test_calc_blob_fee_post_osaka_bpo3() {
1161        let parent = BlockHeader {
1162            excess_blob_gas: Some(19251039),
1163            blob_gas_used: Some(2490368),
1164            base_fee_per_gas: Some(50),
1165            ..Default::default()
1166        };
1167        let schedule = ForkBlobSchedule {
1168            target: 21,
1169            max: 32,
1170            base_fee_update_fraction: 20609697,
1171        };
1172        let fork = Fork::Osaka;
1173        let res = calc_excess_blob_gas(&parent, schedule, fork);
1174        assert_eq!(res, 20107103)
1175    }
1176
1177    #[test]
1178    fn test_calc_blob_fee_post_osaka_bpo1_ef() {
1179        let parent = BlockHeader {
1180            excess_blob_gas: Some(0x360000),
1181            blob_gas_used: Some(0),
1182            base_fee_per_gas: Some(0x11),
1183            ..Default::default()
1184        };
1185        let schedule = ForkBlobSchedule {
1186            target: 9,
1187            max: 14,
1188            base_fee_update_fraction: 0x86c73b,
1189        };
1190        let fork = Fork::Osaka;
1191
1192        let res = calc_excess_blob_gas(&parent, schedule, fork);
1193        assert_eq!(res, 3538944)
1194    }
1195
1196    #[test]
1197    fn test_calc_excess_blob_gas_at_amsterdam_inherits_bpo1_target() {
1198        // Amsterdam carries no blob params of its own, so a chain that scheduled BPO1
1199        // and never scheduled BPO2 keeps BPO1's target of 10 and max of 15 across the
1200        // Amsterdam boundary. A parent that spent 12 blobs is above that target, so the
1201        // EIP-7918 reserve-price branch raises the excess by `used * (max - target) / max`.
1202        // Resolving the schedule from an unscheduled BPO2 (target 14) instead would put
1203        // the parent below target and wrongly return 0.
1204        let parent = BlockHeader {
1205            excess_blob_gas: Some(0),
1206            blob_gas_used: Some(12 * GAS_PER_BLOB as u64),
1207            base_fee_per_gas: Some(INITIAL_BASE_FEE),
1208            ..Default::default()
1209        };
1210        let config = ChainConfig {
1211            osaka_time: Some(0),
1212            bpo1_time: Some(0),
1213            amsterdam_time: Some(100),
1214            ..Default::default()
1215        };
1216
1217        let schedule = config
1218            .get_fork_blob_schedule(100)
1219            .expect("Amsterdam must resolve a blob schedule");
1220        assert_eq!(schedule.target, 10);
1221        assert_eq!(schedule.max, 15);
1222
1223        let res = calc_excess_blob_gas(&parent, schedule, config.fork(100));
1224        assert_eq!(res, 4 * GAS_PER_BLOB as u64);
1225    }
1226
1227    #[test]
1228    fn test_fake_exponential_overflow() {
1229        // With u64 this overflows
1230        assert!(fake_exponential(57532635.into(), 3145728.into(), 3338477).is_ok());
1231    }
1232
1233    #[test]
1234    fn test_fake_exponential_bounds_overflow() {
1235        // Making sure the limit we state in the documentation of 400_000_000 works
1236        let thing = fake_exponential(
1237            MIN_BASE_FEE_PER_BLOB_GAS.into(),
1238            400_000_000.into(),
1239            BLOB_BASE_FEE_UPDATE_FRACTION,
1240        );
1241        // With u64 this overflows
1242        assert!(thing.is_ok());
1243    }
1244
1245    #[test]
1246    fn burned_fees_header_roundtrip_and_hash() {
1247        // For a correct round-trip, all preceding optional fields must be Some so the
1248        // positional/trailing RLP decoder can attribute burned_fees correctly.
1249        // This mirrors a real LStar-era block header.
1250        let h = BlockHeader {
1251            base_fee_per_gas: Some(7),
1252            withdrawals_root: Some(H256::zero()),
1253            blob_gas_used: Some(0),
1254            excess_blob_gas: Some(0),
1255            parent_beacon_block_root: Some(H256::zero()),
1256            requests_hash: Some(H256::zero()),
1257            block_access_list_hash: Some(H256::zero()),
1258            slot_number: Some(1),
1259            burned_fees: Some(12345),
1260            ..Default::default()
1261        };
1262        let encoded = h.encode_to_vec();
1263        let decoded = BlockHeader::decode(&encoded).expect("decode");
1264        assert_eq!(decoded.burned_fees, Some(12345));
1265        // None round-trips as None (pre-LStar).
1266        let h_none = BlockHeader {
1267            burned_fees: None,
1268            ..Default::default()
1269        };
1270        assert_eq!(
1271            BlockHeader::decode(&h_none.encode_to_vec())
1272                .unwrap()
1273                .burned_fees,
1274            None
1275        );
1276        // burned_fees participates in the block hash: same base state, only burned_fees differs.
1277        let h_with = BlockHeader {
1278            base_fee_per_gas: Some(7),
1279            withdrawals_root: Some(H256::zero()),
1280            blob_gas_used: Some(0),
1281            excess_blob_gas: Some(0),
1282            parent_beacon_block_root: Some(H256::zero()),
1283            requests_hash: Some(H256::zero()),
1284            block_access_list_hash: Some(H256::zero()),
1285            slot_number: Some(1),
1286            burned_fees: Some(12345),
1287            ..Default::default()
1288        };
1289        let h_without = BlockHeader {
1290            base_fee_per_gas: Some(7),
1291            withdrawals_root: Some(H256::zero()),
1292            blob_gas_used: Some(0),
1293            excess_blob_gas: Some(0),
1294            parent_beacon_block_root: Some(H256::zero()),
1295            requests_hash: Some(H256::zero()),
1296            block_access_list_hash: Some(H256::zero()),
1297            slot_number: Some(1),
1298            burned_fees: None,
1299            ..Default::default()
1300        };
1301        assert_ne!(h_with.hash(), h_without.hash());
1302    }
1303
1304    /// Verify that `into_with_burned_fees` clears the OnceCell so the resulting
1305    /// hash reflects the injected value and not any previously cached hash.
1306    ///
1307    /// This is the TDD anchor for the Task-4 verify-flow reorder: after
1308    /// `execute_blocks` caches the block hash (burned_fees=None), we must be
1309    /// able to inject the recomputed `burned_fees` and still get the correct hash.
1310    #[test]
1311    fn into_with_burned_fees_clears_stale_cache() {
1312        use crate::NativeCrypto;
1313
1314        // Build a default header (burned_fees = None).
1315        let header_none = BlockHeader::default();
1316
1317        // Force the hash cache to populate with burned_fees=None value.
1318        let hash_stale = header_none.hash();
1319
1320        // Build a reference header with burned_fees=Some(42) from scratch (fresh OnceCell).
1321        let header_some_fresh = BlockHeader {
1322            burned_fees: Some(42),
1323            ..BlockHeader::default()
1324        };
1325        let hash_expected = header_some_fresh.compute_block_hash(&NativeCrypto);
1326
1327        // The two hashes must differ — burned_fees is RLP-encoded in the header.
1328        assert_ne!(
1329            hash_stale, hash_expected,
1330            "burned_fees=None and burned_fees=Some(42) must produce different block hashes"
1331        );
1332
1333        // Now inject Some(42) via into_with_burned_fees, starting from the stale-cached header.
1334        let fresh_header = header_none.into_with_burned_fees(Some(42));
1335        let hash_fresh = fresh_header.hash();
1336
1337        // Must match the reference hash, NOT the stale one.
1338        assert_eq!(
1339            hash_fresh, hash_expected,
1340            "into_with_burned_fees must yield a hash identical to building a fresh header with the same burned_fees"
1341        );
1342        assert_ne!(
1343            hash_fresh, hash_stale,
1344            "OnceCell must have been cleared: fresh hash must differ from the stale cached hash"
1345        );
1346    }
1347
1348    /// Verify the Amsterdam (pre-LStar) no-op: injecting None into a header
1349    /// whose hash was cached with burned_fees=None yields the same hash.
1350    ///
1351    /// This is the primary correctness requirement for Task 4: the reordered
1352    /// verify-flow must not regress the current Amsterdam path.
1353    #[test]
1354    fn into_with_burned_fees_none_is_idempotent() {
1355        let header = BlockHeader::default();
1356        let hash_before = header.hash(); // populate cache with burned_fees=None
1357
1358        // Inject None (Amsterdam: burned_fees is None both before and after execution).
1359        let header_after = header.into_with_burned_fees(None);
1360        let hash_after = header_after.hash();
1361
1362        assert_eq!(
1363            hash_before, hash_after,
1364            "injecting None must not change the block hash (Amsterdam no-op)"
1365        );
1366    }
1367
1368    #[test]
1369    fn test_validate_block_body_rejects_missing_withdrawals_field() {
1370        use crate::constants::EMPTY_WITHDRAWALS_HASH;
1371
1372        let crypto = NativeCrypto;
1373        let header = BlockHeader {
1374            transactions_root: compute_transactions_root(&[], &crypto),
1375            withdrawals_root: Some(*EMPTY_WITHDRAWALS_HASH),
1376            ..Default::default()
1377        };
1378
1379        // A post-Shanghai body that OMITS the withdrawals field is malformed and
1380        // must be rejected even when the header commits to the empty-withdrawals
1381        // root (geth: "missing withdrawals in block body"; reth, nethermind,
1382        // erigon and besu reject this shape as well).
1383        let body_missing = BlockBody {
1384            withdrawals: None,
1385            ..BlockBody::empty()
1386        };
1387        assert!(matches!(
1388            validate_block_body(&header, &body_missing, &crypto),
1389            Err(InvalidBlockBodyError::WithdrawalsRootNotMatch)
1390        ));
1391
1392        // An explicit empty withdrawals list is valid for the same header.
1393        assert!(validate_block_body(&header, &BlockBody::empty(), &crypto).is_ok());
1394
1395        // A withdrawals field without a header root (pre-Shanghai shape) is rejected.
1396        let pre_shanghai_header = BlockHeader {
1397            transactions_root: compute_transactions_root(&[], &crypto),
1398            ..Default::default()
1399        };
1400        assert!(matches!(
1401            validate_block_body(&pre_shanghai_header, &BlockBody::empty(), &crypto),
1402            Err(InvalidBlockBodyError::WithdrawalsRootNotMatch)
1403        ));
1404        // Neither field present is valid.
1405        let body_no_withdrawals = BlockBody {
1406            withdrawals: None,
1407            ..BlockBody::empty()
1408        };
1409        assert!(validate_block_body(&pre_shanghai_header, &body_no_withdrawals, &crypto).is_ok());
1410    }
1411
1412    #[test]
1413    fn test_normalize_legacy_withdrawals() {
1414        use crate::constants::EMPTY_WITHDRAWALS_HASH;
1415
1416        let crypto = NativeCrypto;
1417
1418        // Legacy shape from trusted history: omitted field + empty-withdrawals
1419        // header root normalizes to the canonical Some(vec![]), and the
1420        // normalized body passes validation.
1421        let header = BlockHeader {
1422            transactions_root: compute_transactions_root(&[], &crypto),
1423            withdrawals_root: Some(*EMPTY_WITHDRAWALS_HASH),
1424            ..Default::default()
1425        };
1426        let mut body = BlockBody {
1427            withdrawals: None,
1428            ..BlockBody::empty()
1429        };
1430        normalize_legacy_withdrawals(&header, &mut body);
1431        assert_eq!(body.withdrawals, Some(Vec::new()));
1432        assert!(validate_block_body(&header, &body, &crypto).is_ok());
1433
1434        // An omitted field under a NON-empty withdrawals root is not the legacy
1435        // shape: the body stays untouched and validation still rejects it.
1436        let bad_header = BlockHeader {
1437            transactions_root: compute_transactions_root(&[], &crypto),
1438            withdrawals_root: Some(H256::repeat_byte(0xAB)),
1439            ..Default::default()
1440        };
1441        let mut bad_body = BlockBody {
1442            withdrawals: None,
1443            ..BlockBody::empty()
1444        };
1445        normalize_legacy_withdrawals(&bad_header, &mut bad_body);
1446        assert_eq!(bad_body.withdrawals, None);
1447        assert!(validate_block_body(&bad_header, &bad_body, &crypto).is_err());
1448
1449        // Pre-Shanghai shape (no header root, no field) stays untouched.
1450        let pre_shanghai_header = BlockHeader {
1451            transactions_root: compute_transactions_root(&[], &crypto),
1452            ..Default::default()
1453        };
1454        let mut pre_shanghai_body = BlockBody {
1455            withdrawals: None,
1456            ..BlockBody::empty()
1457        };
1458        normalize_legacy_withdrawals(&pre_shanghai_header, &mut pre_shanghai_body);
1459        assert_eq!(pre_shanghai_body.withdrawals, None);
1460
1461        // An already-canonical body is left as-is.
1462        let mut canonical = BlockBody::empty();
1463        let before = canonical.withdrawals.clone();
1464        normalize_legacy_withdrawals(&header, &mut canonical);
1465        assert_eq!(canonical.withdrawals, before);
1466    }
1467}