pub use alloy_eips::eip1559::BaseFeeParams;
use alloy_evm::eth::spec::EthExecutorSpec;

use crate::{
    constants::{MAINNET_DEPOSIT_CONTRACT, MAINNET_PRUNE_DELETE_LIMIT},
    ethereum::SEPOLIA_PARIS_TTD,
    holesky, hoodi, mainnet,
    mainnet::{MAINNET_PARIS_BLOCK, MAINNET_PARIS_TTD},
    sepolia,
    sepolia::SEPOLIA_PARIS_BLOCK,
    EthChainSpec,
};
use alloc::{
    boxed::Box,
    collections::BTreeMap,
    format,
    string::{String, ToString},
    sync::Arc,
    vec::Vec,
};
use alloy_chains::{Chain, NamedChain};
use alloy_consensus::{
    constants::{
        EMPTY_WITHDRAWALS, HOLESKY_GENESIS_HASH, HOODI_GENESIS_HASH, MAINNET_GENESIS_HASH,
        SEPOLIA_GENESIS_HASH,
    },
    Header,
};
use alloy_eips::{
    eip1559::INITIAL_BASE_FEE, eip7685::EMPTY_REQUESTS_HASH, eip7840::BlobParams,
    eip7892::BlobScheduleBlobParams, eip7928::EMPTY_BLOCK_ACCESS_LIST_HASH,
};
use alloy_genesis::{ChainConfig, Genesis};
use alloy_primitives::{address, b256, Address, BlockNumber, B256, U256};
use alloy_trie::root::state_root_ref_unhashed;
use core::fmt::Debug;
use derive_more::From;
use reth_ethereum_forks::{
    ChainHardforks, DisplayHardforks, EthereumHardfork, EthereumHardforks, ForkCondition,
    ForkFilter, ForkFilterKey, ForkHash, ForkId, Hardfork, Hardforks, Head, DEV_HARDFORKS,
};
use reth_network_peers::{holesky_nodes, hoodi_nodes, mainnet_nodes, sepolia_nodes, NodeRecord};
use reth_primitives_traits::{sync::LazyLock, BlockHeader, SealedHeader};

/// Helper method building a [`Header`] given [`Genesis`] and [`ChainHardforks`].
pub fn make_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Header {
    // If London is activated at genesis, we set the initial base fee as per EIP-1559.
    let base_fee_per_gas = hardforks
        .fork(EthereumHardfork::London)
        .active_at_block(0)
        .then(|| genesis.base_fee_per_gas.map(|fee| fee as u64).unwrap_or(INITIAL_BASE_FEE));

    // If shanghai is activated, initialize the header with an empty withdrawals hash, and
    // empty withdrawals list.
    let withdrawals_root = hardforks
        .fork(EthereumHardfork::Shanghai)
        .active_at_timestamp(genesis.timestamp)
        .then_some(EMPTY_WITHDRAWALS);

    // If Cancun is activated at genesis, we set:
    // * parent beacon block root to 0x0
    // * blob gas used to provided genesis or 0x0
    // * excess blob gas to provided genesis or 0x0
    let (parent_beacon_block_root, blob_gas_used, excess_blob_gas) =
        if hardforks.fork(EthereumHardfork::Cancun).active_at_timestamp(genesis.timestamp) {
            let blob_gas_used = genesis.blob_gas_used.unwrap_or(0);
            let excess_blob_gas = genesis.excess_blob_gas.unwrap_or(0);
            (Some(B256::ZERO), Some(blob_gas_used), Some(excess_blob_gas))
        } else {
            (None, None, None)
        };

    // If Prague is activated at genesis we set requests root to an empty trie root.
    let requests_hash = hardforks
        .fork(EthereumHardfork::Prague)
        .active_at_timestamp(genesis.timestamp)
        .then_some(EMPTY_REQUESTS_HASH);

    // If Amsterdam is activated at genesis we set block access list hash to an empty bal hash
    let block_access_list_hash = hardforks
        .fork(EthereumHardfork::Amsterdam)
        .active_at_timestamp(genesis.timestamp)
        .then_some(EMPTY_BLOCK_ACCESS_LIST_HASH);

    // If Amsterdam is activated at genesis we set slot number to 0
    let slot_number = hardforks
        .fork(EthereumHardfork::Amsterdam)
        .active_at_timestamp(genesis.timestamp)
        .then_some(0);

    Header {
        number: genesis.number.unwrap_or_default(),
        parent_hash: genesis.parent_hash.unwrap_or_default(),
        gas_limit: genesis.gas_limit,
        difficulty: genesis.difficulty,
        nonce: genesis.nonce.into(),
        extra_data: genesis.extra_data.clone(),
        state_root: state_root_ref_unhashed(&genesis.alloc),
        timestamp: genesis.timestamp,
        mix_hash: genesis.mix_hash,
        beneficiary: genesis.coinbase,
        base_fee_per_gas,
        withdrawals_root,
        parent_beacon_block_root,
        blob_gas_used,
        excess_blob_gas,
        requests_hash,
        block_access_list_hash,
        slot_number,
        ..Default::default()
    }
}

/// The Ethereum mainnet spec
pub static MAINNET: LazyLock<Arc<ChainSpec>> = LazyLock::new(|| {
    let genesis = serde_json::from_str(include_str!("../res/genesis/mainnet.json"))
        .expect("Can't deserialize Mainnet genesis json");
    let hardforks = EthereumHardfork::mainnet().into();
    let mut spec = ChainSpec {
        chain: Chain::mainnet(),
        genesis_header: SealedHeader::new(
            make_genesis_header(&genesis, &hardforks),
            MAINNET_GENESIS_HASH,
        ),
        genesis,
        // <https://etherscan.io/block/15537394>
        paris_block_and_final_difficulty: Some((
            MAINNET_PARIS_BLOCK,
            U256::from(58_750_003_716_598_352_816_469u128),
        )),
        hardforks,
        // https://etherscan.io/tx/0xe75fb554e433e03763a1560646ee22dcb74e5274b34c5ad644e7c0f619a7e1d0
        deposit_contract: Some(MAINNET_DEPOSIT_CONTRACT),
        base_fee_params: BaseFeeParamsKind::Constant(BaseFeeParams::ethereum()),
        prune_delete_limit: MAINNET_PRUNE_DELETE_LIMIT,
        blob_params: BlobScheduleBlobParams::default().with_scheduled([
            (mainnet::MAINNET_BPO1_TIMESTAMP, BlobParams::bpo1()),
            (mainnet::MAINNET_BPO2_TIMESTAMP, BlobParams::bpo2()),
        ]),
    };
    spec.genesis.config.dao_fork_support = true;
    spec.into()
});

/// The Sepolia spec
pub static SEPOLIA: LazyLock<Arc<ChainSpec>> = LazyLock::new(|| {
    let genesis = serde_json::from_str(include_str!("../res/genesis/sepolia.json"))
        .expect("Can't deserialize Sepolia genesis json");
    let hardforks = EthereumHardfork::sepolia().into();
    let mut spec = ChainSpec {
        chain: Chain::sepolia(),
        genesis_header: SealedHeader::new(
            make_genesis_header(&genesis, &hardforks),
            SEPOLIA_GENESIS_HASH,
        ),
        genesis,
        // <https://sepolia.etherscan.io/block/1450409>
        paris_block_and_final_difficulty: Some((
            SEPOLIA_PARIS_BLOCK,
            U256::from(17_000_018_015_853_232u128),
        )),
        hardforks,
        // https://sepolia.etherscan.io/tx/0x025ecbf81a2f1220da6285d1701dc89fb5a956b62562ee922e1a9efd73eb4b14
        deposit_contract: Some(DepositContract::new(
            address!("0x7f02c3e3c98b133055b8b348b2ac625669ed295d"),
            1273020,
            b256!("0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5"),
        )),
        base_fee_params: BaseFeeParamsKind::Constant(BaseFeeParams::ethereum()),
        prune_delete_limit: 10000,
        blob_params: BlobScheduleBlobParams::default().with_scheduled([
            (sepolia::SEPOLIA_BPO1_TIMESTAMP, BlobParams::bpo1()),
            (sepolia::SEPOLIA_BPO2_TIMESTAMP, BlobParams::bpo2()),
        ]),
    };
    spec.genesis.config.dao_fork_support = true;
    spec.into()
});

/// The Holesky spec
pub static HOLESKY: LazyLock<Arc<ChainSpec>> = LazyLock::new(|| {
    let genesis = serde_json::from_str(include_str!("../res/genesis/holesky.json"))
        .expect("Can't deserialize Holesky genesis json");
    let hardforks = EthereumHardfork::holesky().into();
    let mut spec = ChainSpec {
        chain: Chain::holesky(),
        genesis_header: SealedHeader::new(
            make_genesis_header(&genesis, &hardforks),
            HOLESKY_GENESIS_HASH,
        ),
        genesis,
        paris_block_and_final_difficulty: Some((0, U256::from(1))),
        hardforks,
        deposit_contract: Some(DepositContract::new(
            address!("0x4242424242424242424242424242424242424242"),
            0,
            b256!("0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5"),
        )),
        base_fee_params: BaseFeeParamsKind::Constant(BaseFeeParams::ethereum()),
        prune_delete_limit: 10000,
        blob_params: BlobScheduleBlobParams::default().with_scheduled([
            (holesky::HOLESKY_BPO1_TIMESTAMP, BlobParams::bpo1()),
            (holesky::HOLESKY_BPO2_TIMESTAMP, BlobParams::bpo2()),
        ]),
    };
    spec.genesis.config.dao_fork_support = true;
    spec.into()
});

/// The Hoodi spec
///
/// Genesis files from: <https://github.com/eth-clients/hoodi>
pub static HOODI: LazyLock<Arc<ChainSpec>> = LazyLock::new(|| {
    let genesis = serde_json::from_str(include_str!("../res/genesis/hoodi.json"))
        .expect("Can't deserialize Hoodi genesis json");
    let hardforks = EthereumHardfork::hoodi().into();
    let mut spec = ChainSpec {
        chain: Chain::hoodi(),
        genesis_header: SealedHeader::new(
            make_genesis_header(&genesis, &hardforks),
            HOODI_GENESIS_HASH,
        ),
        genesis,
        paris_block_and_final_difficulty: Some((0, U256::from(0))),
        hardforks,
        deposit_contract: Some(DepositContract::new(
            address!("0x00000000219ab540356cBB839Cbe05303d7705Fa"),
            0,
            b256!("0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5"),
        )),
        base_fee_params: BaseFeeParamsKind::Constant(BaseFeeParams::ethereum()),
        prune_delete_limit: 10000,
        blob_params: BlobScheduleBlobParams::default().with_scheduled([
            (hoodi::HOODI_BPO1_TIMESTAMP, BlobParams::bpo1()),
            (hoodi::HOODI_BPO2_TIMESTAMP, BlobParams::bpo2()),
        ]),
    };
    spec.genesis.config.dao_fork_support = true;
    spec.into()
});

/// Dev testnet specification
///
/// Includes 20 prefunded accounts with `10_000` ETH each derived from mnemonic "test test test test
/// test test test test test test test junk".
pub static DEV: LazyLock<Arc<ChainSpec>> = LazyLock::new(|| {
    let genesis = serde_json::from_str(include_str!("../res/genesis/dev.json"))
        .expect("Can't deserialize Dev testnet genesis json");
    let hardforks = DEV_HARDFORKS.clone();
    ChainSpec {
        chain: Chain::dev(),
        genesis_header: SealedHeader::seal_slow(make_genesis_header(&genesis, &hardforks)),
        genesis,
        paris_block_and_final_difficulty: Some((0, U256::from(0))),
        hardforks,
        base_fee_params: BaseFeeParamsKind::Constant(BaseFeeParams::ethereum()),
        deposit_contract: None, // TODO: do we even have?
        ..Default::default()
    }
    .into()
});

/// Creates a [`ChainConfig`] from the given chain, hardforks, deposit contract address, and blob
/// schedule.
pub fn create_chain_config(
    chain: Option<Chain>,
    hardforks: &ChainHardforks,
    deposit_contract_address: Option<Address>,
    blob_schedule: BTreeMap<String, BlobParams>,
) -> ChainConfig {
    // Helper to extract block number from a hardfork condition
    let block_num = |fork: EthereumHardfork| hardforks.fork(fork).block_number();

    // Helper to extract timestamp from a hardfork condition
    let timestamp = |fork: EthereumHardfork| -> Option<u64> {
        match hardforks.fork(fork) {
            ForkCondition::Timestamp(t) => Some(t),
            _ => None,
        }
    };

    // Extract TTD from Paris fork
    let (terminal_total_difficulty, terminal_total_difficulty_passed) =
        match hardforks.fork(EthereumHardfork::Paris) {
            ForkCondition::TTD { total_difficulty, .. } => (Some(total_difficulty), true),
            _ => (None, false),
        };

    // Check if DAO fork is supported (it has an activation block)
    let dao_fork_support = hardforks.fork(EthereumHardfork::Dao) != ForkCondition::Never;

    ChainConfig {
        chain_id: chain.map(|c| c.id()).unwrap_or(0),
        homestead_block: block_num(EthereumHardfork::Homestead),
        dao_fork_block: block_num(EthereumHardfork::Dao),
        dao_fork_support,
        eip150_block: block_num(EthereumHardfork::Tangerine),
        eip155_block: block_num(EthereumHardfork::SpuriousDragon),
        eip158_block: block_num(EthereumHardfork::SpuriousDragon),
        byzantium_block: block_num(EthereumHardfork::Byzantium),
        constantinople_block: block_num(EthereumHardfork::Constantinople),
        petersburg_block: block_num(EthereumHardfork::Petersburg),
        istanbul_block: block_num(EthereumHardfork::Istanbul),
        muir_glacier_block: block_num(EthereumHardfork::MuirGlacier),
        berlin_block: block_num(EthereumHardfork::Berlin),
        london_block: block_num(EthereumHardfork::London),
        arrow_glacier_block: block_num(EthereumHardfork::ArrowGlacier),
        gray_glacier_block: block_num(EthereumHardfork::GrayGlacier),
        merge_netsplit_block: None,
        shanghai_time: timestamp(EthereumHardfork::Shanghai),
        cancun_time: timestamp(EthereumHardfork::Cancun),
        prague_time: timestamp(EthereumHardfork::Prague),
        osaka_time: timestamp(EthereumHardfork::Osaka),
        amsterdam_time: timestamp(EthereumHardfork::Amsterdam),
        bpo1_time: timestamp(EthereumHardfork::Bpo1),
        bpo2_time: timestamp(EthereumHardfork::Bpo2),
        bpo3_time: timestamp(EthereumHardfork::Bpo3),
        bpo4_time: timestamp(EthereumHardfork::Bpo4),
        bpo5_time: timestamp(EthereumHardfork::Bpo5),
        terminal_total_difficulty,
        terminal_total_difficulty_passed,
        deposit_contract_address,
        blob_schedule,
        ..Default::default()
    }
}

/// Returns a [`ChainConfig`] for the current Ethereum mainnet chain.
pub fn mainnet_chain_config() -> ChainConfig {
    let hardforks: ChainHardforks = EthereumHardfork::mainnet().into();
    let blob_schedule = blob_params_to_schedule(&MAINNET.blob_params, &hardforks);
    create_chain_config(
        Some(Chain::mainnet()),
        &hardforks,
        Some(MAINNET_DEPOSIT_CONTRACT.address),
        blob_schedule,
    )
}

/// Converts the given [`BlobScheduleBlobParams`] into blobs schedule.
pub fn blob_params_to_schedule(
    params: &BlobScheduleBlobParams,
    hardforks: &ChainHardforks,
) -> BTreeMap<String, BlobParams> {
    let mut schedule = BTreeMap::new();
    schedule.insert("cancun".to_string(), params.cancun);
    schedule.insert("prague".to_string(), params.prague);
    schedule.insert("osaka".to_string(), params.osaka);

    // Map scheduled entries back to bpo fork names by matching timestamps
    let bpo_forks = EthereumHardfork::bpo_variants();
    for (timestamp, blob_params) in &params.scheduled {
        for bpo_fork in bpo_forks {
            if let ForkCondition::Timestamp(fork_ts) = hardforks.fork(bpo_fork) &&
                fork_ts == *timestamp
            {
                schedule.insert(bpo_fork.name().to_lowercase(), *blob_params);
                break;
            }
        }
    }

    schedule
}

/// A wrapper around [`BaseFeeParams`] that allows for specifying constant or dynamic EIP-1559
/// parameters based on the active [Hardfork].
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BaseFeeParamsKind {
    /// Constant [`BaseFeeParams`]; used for chains that don't have dynamic EIP-1559 parameters
    Constant(BaseFeeParams),
    /// Variable [`BaseFeeParams`]; used for chains that have dynamic EIP-1559 parameters like
    /// Optimism
    Variable(ForkBaseFeeParams),
}

impl Default for BaseFeeParamsKind {
    fn default() -> Self {
        BaseFeeParams::ethereum().into()
    }
}

impl From<BaseFeeParams> for BaseFeeParamsKind {
    fn from(params: BaseFeeParams) -> Self {
        Self::Constant(params)
    }
}

impl From<ForkBaseFeeParams> for BaseFeeParamsKind {
    fn from(params: ForkBaseFeeParams) -> Self {
        Self::Variable(params)
    }
}

/// A type alias to a vector of tuples of [Hardfork] and [`BaseFeeParams`], sorted by [Hardfork]
/// activation order. This is used to specify dynamic EIP-1559 parameters for chains like Optimism.
#[derive(Clone, Debug, PartialEq, Eq, From)]
pub struct ForkBaseFeeParams(Vec<(Box<dyn Hardfork>, BaseFeeParams)>);

impl<H: BlockHeader> core::ops::Deref for ChainSpec<H> {
    type Target = ChainHardforks;

    fn deref(&self) -> &Self::Target {
        &self.hardforks
    }
}

/// An Ethereum chain specification.
///
/// A chain specification describes:
///
/// - Meta-information about the chain (the chain ID)
/// - The genesis block of the chain ([`Genesis`])
/// - What hardforks are activated, and under which conditions
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainSpec<H: BlockHeader = Header> {
    /// The chain ID
    pub chain: Chain,

    /// The genesis block.
    pub genesis: Genesis,

    /// The header corresponding to the genesis block.
    pub genesis_header: SealedHeader<H>,

    /// The block at which [`EthereumHardfork::Paris`] was activated and the final difficulty at
    /// this block.
    pub paris_block_and_final_difficulty: Option<(u64, U256)>,

    /// The active hard forks and their activation conditions
    pub hardforks: ChainHardforks,

    /// The deposit contract deployed for `PoS`
    pub deposit_contract: Option<DepositContract>,

    /// The parameters that configure how a block's base fee is computed
    pub base_fee_params: BaseFeeParamsKind,

    /// The delete limit for pruner, per run.
    pub prune_delete_limit: usize,

    /// The settings passed for blob configurations for specific hardforks.
    pub blob_params: BlobScheduleBlobParams,
}

impl<H: BlockHeader> Default for ChainSpec<H> {
    fn default() -> Self {
        Self {
            chain: Default::default(),
            genesis: Default::default(),
            genesis_header: Default::default(),
            paris_block_and_final_difficulty: Default::default(),
            hardforks: Default::default(),
            deposit_contract: Default::default(),
            base_fee_params: BaseFeeParamsKind::Constant(BaseFeeParams::ethereum()),
            prune_delete_limit: MAINNET_PRUNE_DELETE_LIMIT,
            blob_params: Default::default(),
        }
    }
}

impl ChainSpec {
    /// Converts the given [`Genesis`] into a [`ChainSpec`].
    pub fn from_genesis(genesis: Genesis) -> Self {
        genesis.into()
    }

    /// Build a chainspec using [`ChainSpecBuilder`]
    pub fn builder() -> ChainSpecBuilder {
        ChainSpecBuilder::default()
    }

    /// Map a chain ID to a known chain spec, if available.
    pub fn from_chain_id(chain_id: u64) -> Option<Arc<Self>> {
        match NamedChain::try_from(chain_id).ok()? {
            NamedChain::Mainnet => Some(MAINNET.clone()),
            NamedChain::Sepolia => Some(SEPOLIA.clone()),
            NamedChain::Holesky => Some(HOLESKY.clone()),
            NamedChain::Hoodi => Some(HOODI.clone()),
            NamedChain::Dev => Some(DEV.clone()),
            _ => None,
        }
    }
}

impl<H: BlockHeader> ChainSpec<H> {
    /// Get information about the chain itself
    pub const fn chain(&self) -> Chain {
        self.chain
    }

    /// Returns `true` if this chain contains Ethereum configuration.
    #[inline]
    pub const fn is_ethereum(&self) -> bool {
        self.chain.is_ethereum()
    }

    /// Returns `true` if this chain is Optimism mainnet.
    #[inline]
    pub fn is_optimism_mainnet(&self) -> bool {
        self.chain == Chain::optimism_mainnet()
    }

    /// Returns the known paris block, if it exists.
    #[inline]
    pub fn paris_block(&self) -> Option<u64> {
        self.paris_block_and_final_difficulty.map(|(block, _)| block)
    }

    /// Get the genesis block specification.
    ///
    /// To get the header for the genesis block, use [`Self::genesis_header`] instead.
    pub const fn genesis(&self) -> &Genesis {
        &self.genesis
    }

    /// Get the header for the genesis block.
    pub fn genesis_header(&self) -> &H {
        &self.genesis_header
    }

    /// Get the sealed header for the genesis block.
    pub fn sealed_genesis_header(&self) -> SealedHeader<H> {
        SealedHeader::new(self.genesis_header().clone(), self.genesis_hash())
    }

    /// Get the initial base fee of the genesis block.
    pub fn initial_base_fee(&self) -> Option<u64> {
        // If the base fee is set in the genesis block, we use that instead of the default.
        let genesis_base_fee =
            self.genesis.base_fee_per_gas.map(|fee| fee as u64).unwrap_or(INITIAL_BASE_FEE);

        // If London is activated at genesis, we set the initial base fee as per EIP-1559.
        self.hardforks.fork(EthereumHardfork::London).active_at_block(0).then_some(genesis_base_fee)
    }

    /// Get the [`BaseFeeParams`] for the chain at the given timestamp.
    pub fn base_fee_params_at_timestamp(&self, timestamp: u64) -> BaseFeeParams {
        match self.base_fee_params {
            BaseFeeParamsKind::Constant(bf_params) => bf_params,
            BaseFeeParamsKind::Variable(ForkBaseFeeParams(ref bf_params)) => {
                // Walk through the base fee params configuration in reverse order, and return the
                // first one that corresponds to a hardfork that is active at the
                // given timestamp.
                for (fork, params) in bf_params.iter().rev() {
                    if self.hardforks.is_fork_active_at_timestamp(fork.clone(), timestamp) {
                        return *params
                    }
                }

                bf_params.first().map(|(_, params)| *params).unwrap_or_else(BaseFeeParams::ethereum)
            }
        }
    }

    /// Get the hash of the genesis block.
    pub fn genesis_hash(&self) -> B256 {
        self.genesis_header.hash()
    }

    /// Get the timestamp of the genesis block.
    pub const fn genesis_timestamp(&self) -> u64 {
        self.genesis.timestamp
    }

    /// Returns the final total difficulty if the Paris hardfork is known.
    pub fn get_final_paris_total_difficulty(&self) -> Option<U256> {
        self.paris_block_and_final_difficulty.map(|(_, final_difficulty)| final_difficulty)
    }

    /// Get the fork filter for the given hardfork
    pub fn hardfork_fork_filter<HF: Hardfork + Clone>(&self, fork: HF) -> Option<ForkFilter> {
        match self.hardforks.fork(fork.clone()) {
            ForkCondition::Never => None,
            _ => Some(self.fork_filter(self.satisfy(self.hardforks.fork(fork)))),
        }
    }

    /// Returns the hardfork display helper.
    pub fn display_hardforks(&self) -> DisplayHardforks {
        // Create an iterator with hardfork, condition, and optional blob metadata
        let hardforks_with_meta = self.hardforks.forks_iter().map(|(fork, condition)| {
            // Generate blob metadata for timestamp-based hardforks that have blob params
            let metadata = match condition {
                ForkCondition::Timestamp(timestamp) => {
                    // Try to get blob params for this timestamp
                    // This automatically handles all hardforks with blob support
                    EthChainSpec::blob_params_at_timestamp(self, timestamp).map(|params| {
                        format!(
                            "blob: (target: {}, max: {}, fraction: {})",
                            params.target_blob_count, params.max_blob_count, params.update_fraction
                        )
                    })
                }
                _ => None,
            };
            (fork, condition, metadata)
        });

        DisplayHardforks::with_meta(hardforks_with_meta)
    }

    /// Get the fork id for the given hardfork.
    #[inline]
    pub fn hardfork_fork_id<HF: Hardfork + Clone>(&self, fork: HF) -> Option<ForkId> {
        let condition = self.hardforks.fork(fork);
        match condition {
            ForkCondition::Never => None,
            _ => Some(self.fork_id(&self.satisfy(condition))),
        }
    }

    /// Convenience method to get the fork id for [`EthereumHardfork::Shanghai`] from a given
    /// chainspec.
    #[inline]
    pub fn shanghai_fork_id(&self) -> Option<ForkId> {
        self.hardfork_fork_id(EthereumHardfork::Shanghai)
    }

    /// Convenience method to get the fork id for [`EthereumHardfork::Cancun`] from a given
    /// chainspec.
    #[inline]
    pub fn cancun_fork_id(&self) -> Option<ForkId> {
        self.hardfork_fork_id(EthereumHardfork::Cancun)
    }

    /// Convenience method to get the latest fork id from the chainspec. Panics if chainspec has no
    /// hardforks.
    #[inline]
    pub fn latest_fork_id(&self) -> ForkId {
        self.hardfork_fork_id(self.hardforks.last().unwrap().0).unwrap()
    }

    /// Creates a [`ForkFilter`] for the block described by [Head].
    pub fn fork_filter(&self, head: Head) -> ForkFilter {
        let forks = self.hardforks.forks_iter().filter_map(|(_, condition)| {
            // We filter out TTD-based forks w/o a pre-known block since those do not show up in
            // the fork filter.
            Some(match condition {
                ForkCondition::Block(block) |
                ForkCondition::TTD { fork_block: Some(block), .. } => ForkFilterKey::Block(block),
                ForkCondition::Timestamp(time) => ForkFilterKey::Time(time),
                _ => return None,
            })
        });

        ForkFilter::new(head, self.genesis_hash(), self.genesis_timestamp(), forks)
    }

    /// Compute the [`ForkId`] for the given [`Head`] following eip-6122 spec.
    ///
    /// The fork hash is computed by starting from the genesis hash and iteratively adding
    /// block numbers (for block-based forks) or timestamps (for timestamp-based forks) of
    /// active forks. The `next` field indicates the next fork activation point, or `0` if
    /// all forks are active.
    ///
    /// Block-based forks are processed first, then timestamp-based forks. Multiple hardforks
    /// activated at the same block or timestamp: only the first one is applied.
    ///
    /// See: <https://eips.ethereum.org/EIPS/eip-6122>
    pub fn fork_id(&self, head: &Head) -> ForkId {
        let mut forkhash = ForkHash::from(self.genesis_hash());

        // this tracks the last applied block or timestamp fork. This is necessary for optimism,
        // because for the optimism hardforks both the optimism and the corresponding ethereum
        // hardfork can be configured in `ChainHardforks` if it enables ethereum equivalent
        // functionality (e.g. additional header,body fields) This is set to 0 so that all
        // block based hardforks are skipped in the following loop
        let mut current_applied = 0;

        // handle all block forks before handling timestamp based forks. see: https://eips.ethereum.org/EIPS/eip-6122
        for (_, cond) in self.hardforks.forks_iter() {
            // handle block based forks and the sepolia merge netsplit block edge case (TTD
            // ForkCondition with Some(block))
            if let ForkCondition::Block(block) |
            ForkCondition::TTD { fork_block: Some(block), .. } = cond
            {
                if head.number >= block {
                    // skip duplicated hardforks: hardforks enabled at genesis block
                    if block != current_applied {
                        forkhash += block;
                        current_applied = block;
                    }
                } else {
                    // we can return here because this block fork is not active, so we set the
                    // `next` value
                    return ForkId { hash: forkhash, next: block }
                }
            }
        }

        // timestamp are ALWAYS applied after the merge.
        //
        // this filter ensures that no block-based forks are returned
        for timestamp in self.hardforks.forks_iter().filter_map(|(_, cond)| {
            // ensure we only get timestamp forks activated __after__ the genesis block
            cond.as_timestamp().filter(|time| time > &self.genesis.timestamp)
        }) {
            if head.timestamp >= timestamp {
                // skip duplicated hardfork activated at the same timestamp
                if timestamp != current_applied {
                    forkhash += timestamp;
                    current_applied = timestamp;
                }
            } else {
                // can safely return here because we have already handled all block forks and
                // have handled all active timestamp forks, and set the next value to the
                // timestamp that is known but not active yet
                return ForkId { hash: forkhash, next: timestamp }
            }
        }

        ForkId { hash: forkhash, next: 0 }
    }

    /// An internal helper function that returns a head block that satisfies a given Fork condition.
    ///
    /// Creates a [`Head`] representation for a fork activation point, used by [`Self::fork_id`] to
    /// compute fork IDs. For timestamp-based forks, includes the last block-based fork number
    /// before the merge (if any).
    pub(crate) fn satisfy(&self, cond: ForkCondition) -> Head {
        match cond {
            ForkCondition::Block(number) => Head { number, ..Default::default() },
            ForkCondition::Timestamp(timestamp) => {
                // to satisfy every timestamp ForkCondition, we find the last ForkCondition::Block
                // if one exists, and include its block_num in the returned Head
                Head {
                    timestamp,
                    number: self.last_block_fork_before_merge_or_timestamp().unwrap_or_default(),
                    ..Default::default()
                }
            }
            ForkCondition::TTD { total_difficulty, fork_block, .. } => Head {
                total_difficulty,
                number: fork_block.unwrap_or_default(),
                ..Default::default()
            },
            ForkCondition::Never => unreachable!(),
        }
    }

    /// This internal helper function retrieves the block number of the last block-based fork
    /// that occurs before:
    /// - Any existing Total Terminal Difficulty (TTD) or
    /// - Timestamp-based forks in the current [`ChainSpec`].
    ///
    /// The function operates by examining the configured hard forks in the chain. It iterates
    /// through the fork conditions and identifies the most recent block-based fork that
    /// precedes any TTD or timestamp-based conditions.
    ///
    /// If there are no block-based forks found before these conditions, or if the [`ChainSpec`]
    /// is not configured with a TTD or timestamp fork, this function will return `None`.
    pub(crate) fn last_block_fork_before_merge_or_timestamp(&self) -> Option<u64> {
        let mut hardforks_iter = self.hardforks.forks_iter().peekable();
        while let Some((_, curr_cond)) = hardforks_iter.next() {
            if let Some((_, next_cond)) = hardforks_iter.peek() {
                // Match against the `next_cond` to see if it represents:
                // - A TTD (merge)
                // - A timestamp-based fork
                match next_cond {
                    // If the next fork is TTD and specifies a specific block, return that block
                    // number
                    ForkCondition::TTD { fork_block: Some(block), .. } => return Some(*block),

                    // If the next fork is TTD without a specific block or is timestamp-based,
                    // return the block number of the current condition if it is block-based.
                    ForkCondition::TTD { .. } | ForkCondition::Timestamp(_) => {
                        // Check if `curr_cond` is a block-based fork and return its block number if
                        // true.
                        if let ForkCondition::Block(block_num) = curr_cond {
                            return Some(block_num);
                        }
                    }
                    ForkCondition::Block(_) | ForkCondition::Never => {}
                }
            }
        }
        None
    }

    /// Returns the known bootnode records for the given chain.
    pub fn bootnodes(&self) -> Option<Vec<NodeRecord>> {
        use NamedChain as C;

        match self.chain.try_into().ok()? {
            C::Mainnet => Some(mainnet_nodes()),
            C::Sepolia => Some(sepolia_nodes()),
            C::Holesky => Some(holesky_nodes()),
            C::Hoodi => Some(hoodi_nodes()),
            _ => None,
        }
    }

    /// Convert header to another type.
    pub fn map_header<NewH: BlockHeader>(self, f: impl FnOnce(H) -> NewH) -> ChainSpec<NewH> {
        let Self {
            chain,
            genesis,
            genesis_header,
            paris_block_and_final_difficulty,
            hardforks,
            deposit_contract,
            base_fee_params,
            prune_delete_limit,
            blob_params,
        } = self;
        ChainSpec {
            chain,
            genesis,
            genesis_header: SealedHeader::new_unhashed(f(genesis_header.into_header())),
            paris_block_and_final_difficulty,
            hardforks,
            deposit_contract,
            base_fee_params,
            prune_delete_limit,
            blob_params,
        }
    }
}

impl From<Genesis> for ChainSpec {
    fn from(genesis: Genesis) -> Self {
        // Block-based hardforks
        let hardfork_opts = [
            (EthereumHardfork::Frontier.boxed(), Some(0)),
            (EthereumHardfork::Homestead.boxed(), genesis.config.homestead_block),
            (EthereumHardfork::Dao.boxed(), genesis.config.dao_fork_block),
            (EthereumHardfork::Tangerine.boxed(), genesis.config.eip150_block),
            (EthereumHardfork::SpuriousDragon.boxed(), genesis.config.eip155_block),
            (EthereumHardfork::Byzantium.boxed(), genesis.config.byzantium_block),
            (EthereumHardfork::Constantinople.boxed(), genesis.config.constantinople_block),
            (EthereumHardfork::Petersburg.boxed(), genesis.config.petersburg_block),
            (EthereumHardfork::Istanbul.boxed(), genesis.config.istanbul_block),
            (EthereumHardfork::MuirGlacier.boxed(), genesis.config.muir_glacier_block),
            (EthereumHardfork::Berlin.boxed(), genesis.config.berlin_block),
            (EthereumHardfork::London.boxed(), genesis.config.london_block),
            (EthereumHardfork::ArrowGlacier.boxed(), genesis.config.arrow_glacier_block),
            (EthereumHardfork::GrayGlacier.boxed(), genesis.config.gray_glacier_block),
        ];
        let mut hardforks = hardfork_opts
            .into_iter()
            .filter_map(|(hardfork, opt)| opt.map(|block| (hardfork, ForkCondition::Block(block))))
            .collect::<Vec<_>>();

        // We expect no new networks to be configured with the merge, so we ignore the TTD field
        // and merge netsplit block from external genesis files. All existing networks that have
        // merged should have a static ChainSpec already (namely mainnet and sepolia).
        let paris_block_and_final_difficulty = if let Some(ttd) =
            genesis.config.terminal_total_difficulty
        {
            hardforks.push((
                EthereumHardfork::Paris.boxed(),
                ForkCondition::TTD {
                    // NOTE: this will not work properly if the merge is not activated at
                    // genesis, and there is no merge netsplit block
                    activation_block_number: genesis
                        .config
                        .merge_netsplit_block
                        .or_else(|| {
                            // due to this limitation we can't determine the merge block,
                            // this is the case for perfnet testing for example
                            // at the time of this fix, only two networks transitioned: MAINNET +
                            // SEPOLIA and this parsing from genesis is used for shadowforking, so
                            // we can reasonably assume that if the TTD and the chainid matches
                            // those networks we use the activation
                            // blocks of those networks
                            match genesis.config.chain_id {
                                1 if ttd == MAINNET_PARIS_TTD => return Some(MAINNET_PARIS_BLOCK),
                                11155111 if ttd == SEPOLIA_PARIS_TTD => {
                                    return Some(SEPOLIA_PARIS_BLOCK)
                                }
                                _ => {}
                            };
                            None
                        })
                        .unwrap_or_default(),
                    total_difficulty: ttd,
                    fork_block: genesis.config.merge_netsplit_block,
                },
            ));

            genesis.config.merge_netsplit_block.map(|block| (block, ttd))
        } else {
            None
        };

        // Time-based hardforks
        let time_hardfork_opts = [
            (EthereumHardfork::Shanghai.boxed(), genesis.config.shanghai_time),
            (EthereumHardfork::Cancun.boxed(), genesis.config.cancun_time),
            (EthereumHardfork::Prague.boxed(), genesis.config.prague_time),
            (EthereumHardfork::Osaka.boxed(), genesis.config.osaka_time),
            (EthereumHardfork::Bpo1.boxed(), genesis.config.bpo1_time),
            (EthereumHardfork::Bpo2.boxed(), genesis.config.bpo2_time),
            (EthereumHardfork::Bpo3.boxed(), genesis.config.bpo3_time),
            (EthereumHardfork::Bpo4.boxed(), genesis.config.bpo4_time),
            (EthereumHardfork::Bpo5.boxed(), genesis.config.bpo5_time),
            (EthereumHardfork::Amsterdam.boxed(), genesis.config.amsterdam_time),
        ];

        let mut time_hardforks = time_hardfork_opts
            .into_iter()
            .filter_map(|(hardfork, opt)| {
                opt.map(|time| (hardfork, ForkCondition::Timestamp(time)))
            })
            .collect::<Vec<_>>();

        hardforks.append(&mut time_hardforks);

        // Ordered Hardforks
        let mainnet_hardforks: ChainHardforks = EthereumHardfork::mainnet().into();
        let mainnet_order = mainnet_hardforks.forks_iter();

        let mut ordered_hardforks = Vec::with_capacity(hardforks.len());
        for (hardfork, _) in mainnet_order {
            if let Some(pos) = hardforks.iter().position(|(e, _)| **e == *hardfork) {
                ordered_hardforks.push(hardforks.remove(pos));
            }
        }

        // append the remaining unknown hardforks to ensure we don't filter any out
        ordered_hardforks.append(&mut hardforks);

        // Extract blob parameters directly from blob_schedule
        let blob_params = genesis.config.blob_schedule_blob_params();

        // NOTE: in full node, we prune all receipts except the deposit contract's. We do not
        // have the deployment block in the genesis file, so we use block zero. We use the same
        // deposit topic as the mainnet contract if we have the deposit contract address in the
        // genesis json.
        let deposit_contract = genesis.config.deposit_contract_address.map(|address| {
            DepositContract { address, block: 0, topic: MAINNET_DEPOSIT_CONTRACT.topic }
        });

        let hardforks = ChainHardforks::new(ordered_hardforks);

        Self {
            chain: genesis.config.chain_id.into(),
            genesis_header: SealedHeader::new_unhashed(make_genesis_header(&genesis, &hardforks)),
            genesis,
            hardforks,
            paris_block_and_final_difficulty,
            deposit_contract,
            blob_params,
            ..Default::default()
        }
    }
}

impl<H: BlockHeader> Hardforks for ChainSpec<H> {
    fn fork<HF: Hardfork>(&self, fork: HF) -> ForkCondition {
        self.hardforks.fork(fork)
    }

    fn forks_iter(&self) -> impl Iterator<Item = (&dyn Hardfork, ForkCondition)> {
        self.hardforks.forks_iter()
    }

    fn fork_id(&self, head: &Head) -> ForkId {
        self.fork_id(head)
    }

    fn latest_fork_id(&self) -> ForkId {
        self.latest_fork_id()
    }

    fn fork_filter(&self, head: Head) -> ForkFilter {
        self.fork_filter(head)
    }
}

impl<H: BlockHeader> EthereumHardforks for ChainSpec<H> {
    fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition {
        self.fork(fork)
    }
}

/// A trait for reading the current chainspec.
#[auto_impl::auto_impl(&, Arc)]
pub trait ChainSpecProvider: Debug + Send {
    /// The chain spec type.
    type ChainSpec: EthChainSpec + 'static;

    /// Get an [`Arc`] to the chainspec.
    fn chain_spec(&self) -> Arc<Self::ChainSpec>;
}

/// A helper to build custom chain specs
#[derive(Debug, Default, Clone)]
pub struct ChainSpecBuilder {
    chain: Option<Chain>,
    genesis: Option<Genesis>,
    hardforks: ChainHardforks,
}

impl ChainSpecBuilder {
    /// Construct a new builder from the mainnet chain spec.
    pub fn mainnet() -> Self {
        Self {
            chain: Some(MAINNET.chain),
            genesis: Some(MAINNET.genesis.clone()),
            hardforks: MAINNET.hardforks.clone(),
        }
    }
}

impl ChainSpecBuilder {
    /// Set the chain ID
    pub const fn chain(mut self, chain: Chain) -> Self {
        self.chain = Some(chain);
        self
    }

    /// Resets any existing hardforks from the builder.
    pub fn reset(mut self) -> Self {
        self.hardforks = ChainHardforks::default();
        self
    }

    /// Set the genesis block.
    pub fn genesis(mut self, genesis: Genesis) -> Self {
        self.genesis = Some(genesis);
        self
    }

    /// Add the given fork with the given activation condition to the spec.
    pub fn with_fork<H: Hardfork>(mut self, fork: H, condition: ForkCondition) -> Self {
        self.hardforks.insert(fork, condition);
        self
    }

    /// Add the given chain hardforks to the spec.
    pub fn with_forks(mut self, forks: ChainHardforks) -> Self {
        self.hardforks = forks;
        self
    }

    /// Remove the given fork from the spec.
    pub fn without_fork<H: Hardfork>(mut self, fork: H) -> Self {
        self.hardforks.remove(&fork);
        self
    }

    /// Enable the Paris hardfork at the given TTD.
    ///
    /// Does not set the merge netsplit block.
    pub fn paris_at_ttd(self, ttd: U256, activation_block_number: BlockNumber) -> Self {
        self.with_fork(
            EthereumHardfork::Paris,
            ForkCondition::TTD { activation_block_number, total_difficulty: ttd, fork_block: None },
        )
    }

    /// Enable Frontier at genesis.
    pub fn frontier_activated(mut self) -> Self {
        self.hardforks.insert(EthereumHardfork::Frontier, ForkCondition::Block(0));
        self
    }

    /// Enable Dao at genesis.
    pub fn dao_activated(mut self) -> Self {
        self = self.frontier_activated();
        self.hardforks.insert(EthereumHardfork::Dao, ForkCondition::Block(0));
        self
    }

    /// Enable Homestead at genesis.
    pub fn homestead_activated(mut self) -> Self {
        self = self.dao_activated();
        self.hardforks.insert(EthereumHardfork::Homestead, ForkCondition::Block(0));
        self
    }

    /// Enable Tangerine at genesis.
    pub fn tangerine_whistle_activated(mut self) -> Self {
        self = self.homestead_activated();
        self.hardforks.insert(EthereumHardfork::Tangerine, ForkCondition::Block(0));
        self
    }

    /// Enable Spurious Dragon at genesis.
    pub fn spurious_dragon_activated(mut self) -> Self {
        self = self.tangerine_whistle_activated();
        self.hardforks.insert(EthereumHardfork::SpuriousDragon, ForkCondition::Block(0));
        self
    }

    /// Enable Byzantium at genesis.
    pub fn byzantium_activated(mut self) -> Self {
        self = self.spurious_dragon_activated();
        self.hardforks.insert(EthereumHardfork::Byzantium, ForkCondition::Block(0));
        self
    }

    /// Enable Constantinople at genesis.
    pub fn constantinople_activated(mut self) -> Self {
        self = self.byzantium_activated();
        self.hardforks.insert(EthereumHardfork::Constantinople, ForkCondition::Block(0));
        self
    }

    /// Enable Petersburg at genesis.
    pub fn petersburg_activated(mut self) -> Self {
        self = self.constantinople_activated();
        self.hardforks.insert(EthereumHardfork::Petersburg, ForkCondition::Block(0));
        self
    }

    /// Enable Istanbul at genesis.
    pub fn istanbul_activated(mut self) -> Self {
        self = self.petersburg_activated();
        self.hardforks.insert(EthereumHardfork::Istanbul, ForkCondition::Block(0));
        self
    }

    /// Enable Muir Glacier at genesis.
    pub fn muirglacier_activated(mut self) -> Self {
        self = self.istanbul_activated();
        self.hardforks.insert(EthereumHardfork::MuirGlacier, ForkCondition::Block(0));
        self
    }

    /// Enable Berlin at genesis.
    pub fn berlin_activated(mut self) -> Self {
        self = self.muirglacier_activated();
        self.hardforks.insert(EthereumHardfork::Berlin, ForkCondition::Block(0));
        self
    }

    /// Enable London at genesis.
    pub fn london_activated(mut self) -> Self {
        self = self.berlin_activated();
        self.hardforks.insert(EthereumHardfork::London, ForkCondition::Block(0));
        self
    }

    /// Enable Arrow Glacier at genesis.
    pub fn arrowglacier_activated(mut self) -> Self {
        self = self.london_activated();
        self.hardforks.insert(EthereumHardfork::ArrowGlacier, ForkCondition::Block(0));
        self
    }

    /// Enable Gray Glacier at genesis.
    pub fn grayglacier_activated(mut self) -> Self {
        self = self.arrowglacier_activated();
        self.hardforks.insert(EthereumHardfork::GrayGlacier, ForkCondition::Block(0));
        self
    }

    /// Enable Paris at genesis.
    pub fn paris_activated(mut self) -> Self {
        self = self.grayglacier_activated();
        self.hardforks.insert(
            EthereumHardfork::Paris,
            ForkCondition::TTD {
                activation_block_number: 0,
                total_difficulty: U256::ZERO,
                fork_block: None,
            },
        );
        self
    }

    /// Enable Shanghai at genesis.
    pub fn shanghai_activated(mut self) -> Self {
        self = self.paris_activated();
        self.hardforks.insert(EthereumHardfork::Shanghai, ForkCondition::Timestamp(0));
        self
    }

    /// Enable Cancun at genesis.
    pub fn cancun_activated(mut self) -> Self {
        self = self.shanghai_activated();
        self.hardforks.insert(EthereumHardfork::Cancun, ForkCondition::Timestamp(0));
        self
    }

    /// Enable Prague at genesis.
    pub fn prague_activated(mut self) -> Self {
        self = self.cancun_activated();
        self.hardforks.insert(EthereumHardfork::Prague, ForkCondition::Timestamp(0));
        self
    }

    /// Enable Prague at the given timestamp.
    pub fn with_prague_at(mut self, timestamp: u64) -> Self {
        self.hardforks.insert(EthereumHardfork::Prague, ForkCondition::Timestamp(timestamp));
        self
    }

    /// Enable Osaka at genesis.
    pub fn osaka_activated(mut self) -> Self {
        self = self.prague_activated();
        self.hardforks.insert(EthereumHardfork::Osaka, ForkCondition::Timestamp(0));
        self
    }

    /// Enable Osaka at the given timestamp.
    pub fn with_osaka_at(mut self, timestamp: u64) -> Self {
        self.hardforks.insert(EthereumHardfork::Osaka, ForkCondition::Timestamp(timestamp));
        self
    }

    /// Enable Amsterdam at genesis.
    pub fn amsterdam_activated(mut self) -> Self {
        self = self.osaka_activated();
        self.hardforks.insert(EthereumHardfork::Amsterdam, ForkCondition::Timestamp(0));
        self
    }

    /// Enable Amsterdam at the given timestamp.
    pub fn with_amsterdam_at(mut self, timestamp: u64) -> Self {
        self.hardforks.insert(EthereumHardfork::Amsterdam, ForkCondition::Timestamp(timestamp));
        self
    }

    /// Build the resulting [`ChainSpec`].
    ///
    /// # Panics
    ///
    /// This function panics if the chain ID and genesis is not set ([`Self::chain`] and
    /// [`Self::genesis`])
    pub fn build(self) -> ChainSpec {
        let paris_block_and_final_difficulty = {
            self.hardforks.get(EthereumHardfork::Paris).and_then(|cond| {
                if let ForkCondition::TTD { total_difficulty, activation_block_number, .. } = cond {
                    Some((activation_block_number, total_difficulty))
                } else {
                    None
                }
            })
        };
        let genesis = self.genesis.expect("The genesis is required");
        ChainSpec {
            chain: self.chain.expect("The chain is required"),
            genesis_header: SealedHeader::new_unhashed(make_genesis_header(
                &genesis,
                &self.hardforks,
            )),
            genesis,
            hardforks: self.hardforks,
            paris_block_and_final_difficulty,
            deposit_contract: None,
            ..Default::default()
        }
    }
}

impl From<&Arc<ChainSpec>> for ChainSpecBuilder {
    fn from(value: &Arc<ChainSpec>) -> Self {
        Self {
            chain: Some(value.chain),
            genesis: Some(value.genesis.clone()),
            hardforks: value.hardforks.clone(),
        }
    }
}

impl<H: BlockHeader> EthExecutorSpec for ChainSpec<H> {
    fn deposit_contract_address(&self) -> Option<Address> {
        self.deposit_contract.map(|deposit_contract| deposit_contract.address)
    }
}

/// `PoS` deposit contract details.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DepositContract {
    /// Deposit Contract Address
    pub address: Address,
    /// Deployment Block
    pub block: BlockNumber,
    /// `DepositEvent` event signature
    pub topic: B256,
}

impl DepositContract {
    /// Creates a new [`DepositContract`].
    pub const fn new(address: Address, block: BlockNumber, topic: B256) -> Self {
        Self { address, block, topic }
    }
}

/// Verifies [`ChainSpec`] configuration against expected data in given cases.
#[cfg(any(test, feature = "test-utils"))]
pub fn test_fork_ids(spec: &ChainSpec, cases: &[(Head, ForkId)]) {
    for (block, expected_id) in cases {
        let computed_id = spec.fork_id(block);
        assert_eq!(
            expected_id, &computed_id,
            "Expected fork ID {:?}, computed fork ID {:?} at block {}",
            expected_id, computed_id, block.number
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy_chains::Chain;
    use alloy_consensus::constants::ETH_TO_WEI;
    use alloy_eips::{eip4844::BLOB_TX_MIN_BLOB_GASPRICE, eip7840::BlobParams};
    use alloy_evm::block::calc::{base_block_reward, block_reward};
    use alloy_genesis::{ChainConfig, GenesisAccount};
    use alloy_primitives::{b256, hex};
    use alloy_trie::{TrieAccount, EMPTY_ROOT_HASH};
    use core::ops::Deref;
    use reth_ethereum_forks::{ForkCondition, ForkHash, ForkId, Head};
    use std::{collections::HashMap, str::FromStr};

    fn test_hardfork_fork_ids(spec: &ChainSpec, cases: &[(EthereumHardfork, ForkId)]) {
        for (hardfork, expected_id) in cases {
            if let Some(computed_id) = spec.hardfork_fork_id(*hardfork) {
                assert_eq!(
                    expected_id, &computed_id,
                    "Expected fork ID {expected_id:?}, computed fork ID {computed_id:?} for hardfork {hardfork}"
                );
                if matches!(hardfork, EthereumHardfork::Shanghai) {
                    if let Some(shanghai_id) = spec.shanghai_fork_id() {
                        assert_eq!(
                            expected_id, &shanghai_id,
                            "Expected fork ID {expected_id:?}, computed fork ID {computed_id:?} for Shanghai hardfork"
                        );
                    } else {
                        panic!("Expected ForkCondition to return Some for Hardfork::Shanghai");
                    }
                }
            }
        }
    }

    #[test]
    fn test_hardfork_list_display_mainnet() {
        assert_eq!(
            MAINNET.display_hardforks().to_string(),
            "Pre-merge hard forks (block based):
- Frontier                         @0
- Homestead                        @1150000
- Dao                              @1920000
- Tangerine                        @2463000
- SpuriousDragon                   @2675000
- Byzantium                        @4370000
- Constantinople                   @7280000
- Petersburg                       @7280000
- Istanbul                         @9069000
- MuirGlacier                      @9200000
- Berlin                           @12244000
- London                           @12965000
- ArrowGlacier                     @13773000
- GrayGlacier                      @15050000
Merge hard forks:
- Paris                            @58750000000000000000000 (network is known to be merged)
Post-merge hard forks (timestamp based):
- Shanghai                         @1681338455
- Cancun                           @1710338135          blob: (target: 3, max: 6, fraction: 3338477)
- Prague                           @1746612311          blob: (target: 6, max: 9, fraction: 5007716)
- Osaka                            @1764798551          blob: (target: 6, max: 9, fraction: 5007716)
- Bpo1                             @1765290071          blob: (target: 10, max: 15, fraction: 8346193)
- Bpo2                             @1767747671          blob: (target: 14, max: 21, fraction: 11684671)"
        );
    }

    #[test]
    fn test_hardfork_list_ignores_disabled_forks() {
        let spec = ChainSpec::builder()
            .chain(Chain::mainnet())
            .genesis(Genesis::default())
            .with_fork(EthereumHardfork::Frontier, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Shanghai, ForkCondition::Never)
            .build();
        assert_eq!(
            spec.display_hardforks().to_string(),
            "Pre-merge hard forks (block based):
- Frontier                         @0"
        );
    }

    // Tests that we skip any fork blocks in block #0 (the genesis ruleset)
    #[test]
    fn ignores_genesis_fork_blocks() {
        let spec = ChainSpec::builder()
            .chain(Chain::mainnet())
            .genesis(Genesis::default())
            .with_fork(EthereumHardfork::Frontier, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Homestead, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Tangerine, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::SpuriousDragon, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Byzantium, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Constantinople, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Istanbul, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::MuirGlacier, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Berlin, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::London, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::ArrowGlacier, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::GrayGlacier, ForkCondition::Block(0))
            .build();

        assert_eq!(spec.deref().len(), 12, "12 forks should be active.");
        assert_eq!(
            spec.fork_id(&Head { number: 1, ..Default::default() }),
            ForkId { hash: ForkHash::from(spec.genesis_hash()), next: 0 },
            "the fork ID should be the genesis hash; forks at genesis are ignored for fork filters"
        );
    }

    #[test]
    fn ignores_duplicate_fork_blocks() {
        let empty_genesis = Genesis::default();
        let unique_spec = ChainSpec::builder()
            .chain(Chain::mainnet())
            .genesis(empty_genesis.clone())
            .with_fork(EthereumHardfork::Frontier, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Homestead, ForkCondition::Block(1))
            .build();

        let duplicate_spec = ChainSpec::builder()
            .chain(Chain::mainnet())
            .genesis(empty_genesis)
            .with_fork(EthereumHardfork::Frontier, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Homestead, ForkCondition::Block(1))
            .with_fork(EthereumHardfork::Tangerine, ForkCondition::Block(1))
            .build();

        assert_eq!(
            unique_spec.fork_id(&Head { number: 2, ..Default::default() }),
            duplicate_spec.fork_id(&Head { number: 2, ..Default::default() }),
            "duplicate fork blocks should be deduplicated for fork filters"
        );
    }

    #[test]
    fn test_chainspec_satisfy() {
        let empty_genesis = Genesis::default();
        // happy path test case
        let happy_path_case = ChainSpec::builder()
            .chain(Chain::mainnet())
            .genesis(empty_genesis.clone())
            .with_fork(EthereumHardfork::Frontier, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Homestead, ForkCondition::Block(73))
            .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(11313123))
            .build();
        let happy_path_head = happy_path_case.satisfy(ForkCondition::Timestamp(11313123));
        let happy_path_expected = Head { number: 73, timestamp: 11313123, ..Default::default() };
        assert_eq!(
            happy_path_head, happy_path_expected,
            "expected satisfy() to return {happy_path_expected:#?}, but got {happy_path_head:#?} "
        );
        // multiple timestamp test case (i.e Shanghai -> Cancun)
        let multiple_timestamp_fork_case = ChainSpec::builder()
            .chain(Chain::mainnet())
            .genesis(empty_genesis.clone())
            .with_fork(EthereumHardfork::Frontier, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Homestead, ForkCondition::Block(73))
            .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(11313123))
            .with_fork(EthereumHardfork::Cancun, ForkCondition::Timestamp(11313398))
            .build();
        let multi_timestamp_head =
            multiple_timestamp_fork_case.satisfy(ForkCondition::Timestamp(11313398));
        let mult_timestamp_expected =
            Head { number: 73, timestamp: 11313398, ..Default::default() };
        assert_eq!(
            multi_timestamp_head, mult_timestamp_expected,
            "expected satisfy() to return {mult_timestamp_expected:#?}, but got {multi_timestamp_head:#?} "
        );
        // no ForkCondition::Block test case
        let no_block_fork_case = ChainSpec::builder()
            .chain(Chain::mainnet())
            .genesis(empty_genesis.clone())
            .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(11313123))
            .build();
        let no_block_fork_head = no_block_fork_case.satisfy(ForkCondition::Timestamp(11313123));
        let no_block_fork_expected = Head { number: 0, timestamp: 11313123, ..Default::default() };
        assert_eq!(
            no_block_fork_head, no_block_fork_expected,
            "expected satisfy() to return {no_block_fork_expected:#?}, but got {no_block_fork_head:#?} ",
        );
        // spec w/ ForkCondition::TTD with block_num test case (Sepolia merge netsplit edge case)
        let fork_cond_ttd_blocknum_case = ChainSpec::builder()
            .chain(Chain::mainnet())
            .genesis(empty_genesis.clone())
            .with_fork(EthereumHardfork::Frontier, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Homestead, ForkCondition::Block(73))
            .with_fork(
                EthereumHardfork::Paris,
                ForkCondition::TTD {
                    activation_block_number: 101,
                    fork_block: Some(101),
                    total_difficulty: U256::from(10_790_000),
                },
            )
            .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(11313123))
            .build();
        let fork_cond_ttd_blocknum_head =
            fork_cond_ttd_blocknum_case.satisfy(ForkCondition::Timestamp(11313123));
        let fork_cond_ttd_blocknum_expected =
            Head { number: 101, timestamp: 11313123, ..Default::default() };
        assert_eq!(
            fork_cond_ttd_blocknum_head, fork_cond_ttd_blocknum_expected,
            "expected satisfy() to return {fork_cond_ttd_blocknum_expected:#?}, but got {fork_cond_ttd_blocknum_head:#?} ",
        );

        // spec w/ only ForkCondition::Block - test the match arm for ForkCondition::Block to ensure
        // no regressions, for these ForkConditions(Block/TTD) - a separate chain spec definition is
        // technically unnecessary - but we include it here for thoroughness
        let fork_cond_block_only_case = ChainSpec::builder()
            .chain(Chain::mainnet())
            .genesis(empty_genesis)
            .with_fork(EthereumHardfork::Frontier, ForkCondition::Block(0))
            .with_fork(EthereumHardfork::Homestead, ForkCondition::Block(73))
            .build();
        let fork_cond_block_only_head = fork_cond_block_only_case.satisfy(ForkCondition::Block(73));
        let fork_cond_block_only_expected = Head { number: 73, ..Default::default() };
        assert_eq!(
            fork_cond_block_only_head, fork_cond_block_only_expected,
            "expected satisfy() to return {fork_cond_block_only_expected:#?}, but got {fork_cond_block_only_head:#?} ",
        );
        // Fork::ConditionTTD test case without a new chain spec to demonstrate ChainSpec::satisfy
        // is independent of ChainSpec for this(these - including ForkCondition::Block) match arm(s)
        let fork_cond_ttd_no_new_spec = fork_cond_block_only_case.satisfy(ForkCondition::TTD {
            activation_block_number: 101,
            fork_block: None,
            total_difficulty: U256::from(10_790_000),
        });
        let fork_cond_ttd_no_new_spec_expected =
            Head { total_difficulty: U256::from(10_790_000), ..Default::default() };
        assert_eq!(
            fork_cond_ttd_no_new_spec, fork_cond_ttd_no_new_spec_expected,
            "expected satisfy() to return {fork_cond_ttd_no_new_spec_expected:#?}, but got {fork_cond_ttd_no_new_spec:#?} ",
        );
    }

    #[test]
    fn mainnet_hardfork_fork_ids() {
        test_hardfork_fork_ids(
            &MAINNET,
            &[
                (
                    EthereumHardfork::Frontier,
                    ForkId { hash: ForkHash(hex!("0xfc64ec04")), next: 1150000 },
                ),
                (
                    EthereumHardfork::Homestead,
                    ForkId { hash: ForkHash(hex!("0x97c2c34c")), next: 1920000 },
                ),
                (
                    EthereumHardfork::Dao,
                    ForkId { hash: ForkHash(hex!("0x91d1f948")), next: 2463000 },
                ),
                (
                    EthereumHardfork::Tangerine,
                    ForkId { hash: ForkHash(hex!("0x7a64da13")), next: 2675000 },
                ),
                (
                    EthereumHardfork::SpuriousDragon,
                    ForkId { hash: ForkHash(hex!("0x3edd5b10")), next: 4370000 },
                ),
                (
                    EthereumHardfork::Byzantium,
                    ForkId { hash: ForkHash(hex!("0xa00bc324")), next: 7280000 },
                ),
                (
                    EthereumHardfork::Constantinople,
                    ForkId { hash: ForkHash(hex!("0x668db0af")), next: 9069000 },
                ),
                (
                    EthereumHardfork::Petersburg,
                    ForkId { hash: ForkHash(hex!("0x668db0af")), next: 9069000 },
                ),
                (
                    EthereumHardfork::Istanbul,
                    ForkId { hash: ForkHash(hex!("0x879d6e30")), next: 9200000 },
                ),
                (
                    EthereumHardfork::MuirGlacier,
                    ForkId { hash: ForkHash(hex!("0xe029e991")), next: 12244000 },
                ),
                (
                    EthereumHardfork::Berlin,
                    ForkId { hash: ForkHash(hex!("0x0eb440f6")), next: 12965000 },
                ),
                (
                    EthereumHardfork::London,
                    ForkId { hash: ForkHash(hex!("0xb715077d")), next: 13773000 },
                ),
                (
                    EthereumHardfork::ArrowGlacier,
                    ForkId { hash: ForkHash(hex!("0x20c327fc")), next: 15050000 },
                ),
                (
                    EthereumHardfork::GrayGlacier,
                    ForkId { hash: ForkHash(hex!("0xf0afd0e3")), next: 1681338455 },
                ),
                (
                    EthereumHardfork::Shanghai,
                    ForkId { hash: ForkHash(hex!("0xdce96c2d")), next: 1710338135 },
                ),
                (
                    EthereumHardfork::Cancun,
                    ForkId { hash: ForkHash(hex!("0x9f3d2254")), next: 1746612311 },
                ),
                (
                    EthereumHardfork::Prague,
                    ForkId {
                        hash: ForkHash(hex!("0xc376cf8b")),
                        next: mainnet::MAINNET_OSAKA_TIMESTAMP,
                    },
                ),
            ],
        );
    }

    #[test]
    fn sepolia_hardfork_fork_ids() {
        test_hardfork_fork_ids(
            &SEPOLIA,
            &[
                (
                    EthereumHardfork::Frontier,
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    EthereumHardfork::Homestead,
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    EthereumHardfork::Tangerine,
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    EthereumHardfork::SpuriousDragon,
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    EthereumHardfork::Byzantium,
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    EthereumHardfork::Constantinople,
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    EthereumHardfork::Petersburg,
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    EthereumHardfork::Istanbul,
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    EthereumHardfork::Berlin,
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    EthereumHardfork::London,
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    EthereumHardfork::Paris,
                    ForkId { hash: ForkHash(hex!("0xb96cbd13")), next: 1677557088 },
                ),
                (
                    EthereumHardfork::Shanghai,
                    ForkId { hash: ForkHash(hex!("0xf7f9bc08")), next: 1706655072 },
                ),
                (
                    EthereumHardfork::Cancun,
                    ForkId { hash: ForkHash(hex!("0x88cf81d9")), next: 1741159776 },
                ),
                (
                    EthereumHardfork::Prague,
                    ForkId {
                        hash: ForkHash(hex!("0xed88b5fd")),
                        next: sepolia::SEPOLIA_OSAKA_TIMESTAMP,
                    },
                ),
            ],
        );
    }

    #[test]
    fn mainnet_fork_ids() {
        test_fork_ids(
            &MAINNET,
            &[
                (
                    Head { number: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xfc64ec04")), next: 1150000 },
                ),
                (
                    Head { number: 1150000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x97c2c34c")), next: 1920000 },
                ),
                (
                    Head { number: 1920000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x91d1f948")), next: 2463000 },
                ),
                (
                    Head { number: 2463000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x7a64da13")), next: 2675000 },
                ),
                (
                    Head { number: 2675000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x3edd5b10")), next: 4370000 },
                ),
                (
                    Head { number: 4370000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xa00bc324")), next: 7280000 },
                ),
                (
                    Head { number: 7280000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x668db0af")), next: 9069000 },
                ),
                (
                    Head { number: 9069000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x879d6e30")), next: 9200000 },
                ),
                (
                    Head { number: 9200000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xe029e991")), next: 12244000 },
                ),
                (
                    Head { number: 12244000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x0eb440f6")), next: 12965000 },
                ),
                (
                    Head { number: 12965000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xb715077d")), next: 13773000 },
                ),
                (
                    Head { number: 13773000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x20c327fc")), next: 15050000 },
                ),
                (
                    Head { number: 15050000, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xf0afd0e3")), next: 1681338455 },
                ),
                // First Shanghai block
                (
                    Head { number: 20000000, timestamp: 1681338455, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xdce96c2d")), next: 1710338135 },
                ),
                // First Cancun block
                (
                    Head { number: 20000001, timestamp: 1710338135, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x9f3d2254")), next: 1746612311 },
                ),
                // First Prague block
                (
                    Head { number: 20000004, timestamp: 1746612311, ..Default::default() },
                    ForkId {
                        hash: ForkHash(hex!("0xc376cf8b")),
                        next: mainnet::MAINNET_OSAKA_TIMESTAMP,
                    },
                ),
                // Osaka block
                (
                    Head {
                        number: 20000004,
                        timestamp: mainnet::MAINNET_OSAKA_TIMESTAMP,
                        ..Default::default()
                    },
                    ForkId {
                        hash: ForkHash(hex!("0x5167e2a6")),
                        next: mainnet::MAINNET_BPO1_TIMESTAMP,
                    },
                ),
            ],
        );
    }

    #[test]
    fn hoodi_fork_ids() {
        test_fork_ids(
            &HOODI,
            &[
                (
                    Head { number: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xbef71d30")), next: 1742999832 },
                ),
                // First Prague block
                (
                    Head { number: 0, timestamp: 1742999833, ..Default::default() },
                    ForkId {
                        hash: ForkHash(hex!("0x0929e24e")),
                        next: hoodi::HOODI_OSAKA_TIMESTAMP,
                    },
                ),
                // First Osaka block
                (
                    Head {
                        number: 0,
                        timestamp: hoodi::HOODI_OSAKA_TIMESTAMP,
                        ..Default::default()
                    },
                    ForkId {
                        hash: ForkHash(hex!("0xe7e0e7ff")),
                        next: hoodi::HOODI_BPO1_TIMESTAMP,
                    },
                ),
            ],
        )
    }

    #[test]
    fn holesky_fork_ids() {
        test_fork_ids(
            &HOLESKY,
            &[
                (
                    Head { number: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xc61a6098")), next: 1696000704 },
                ),
                // First MergeNetsplit block
                (
                    Head { number: 123, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xc61a6098")), next: 1696000704 },
                ),
                // Last MergeNetsplit block
                (
                    Head { number: 123, timestamp: 1696000703, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xc61a6098")), next: 1696000704 },
                ),
                // First Shanghai block
                (
                    Head { number: 123, timestamp: 1696000704, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xfd4f016b")), next: 1707305664 },
                ),
                // Last Shanghai block
                (
                    Head { number: 123, timestamp: 1707305663, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xfd4f016b")), next: 1707305664 },
                ),
                // First Cancun block
                (
                    Head { number: 123, timestamp: 1707305664, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x9b192ad0")), next: 1740434112 },
                ),
                // Last Cancun block
                (
                    Head { number: 123, timestamp: 1740434111, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x9b192ad0")), next: 1740434112 },
                ),
                // First Prague block
                (
                    Head { number: 123, timestamp: 1740434112, ..Default::default() },
                    ForkId {
                        hash: ForkHash(hex!("0xdfbd9bed")),
                        next: holesky::HOLESKY_OSAKA_TIMESTAMP,
                    },
                ),
                // First Osaka block
                (
                    Head {
                        number: 123,
                        timestamp: holesky::HOLESKY_OSAKA_TIMESTAMP,
                        ..Default::default()
                    },
                    ForkId {
                        hash: ForkHash(hex!("0x783def52")),
                        next: holesky::HOLESKY_BPO1_TIMESTAMP,
                    },
                ),
            ],
        )
    }

    #[test]
    fn sepolia_fork_ids() {
        test_fork_ids(
            &SEPOLIA,
            &[
                (
                    Head { number: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    Head { number: 1735370, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 },
                ),
                (
                    Head { number: 1735371, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xb96cbd13")), next: 1677557088 },
                ),
                (
                    Head { number: 1735372, timestamp: 1677557087, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xb96cbd13")), next: 1677557088 },
                ),
                // First Shanghai block
                (
                    Head { number: 1735373, timestamp: 1677557088, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xf7f9bc08")), next: 1706655072 },
                ),
                // Last Shanghai block
                (
                    Head { number: 1735374, timestamp: 1706655071, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xf7f9bc08")), next: 1706655072 },
                ),
                // First Cancun block
                (
                    Head { number: 1735375, timestamp: 1706655072, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x88cf81d9")), next: 1741159776 },
                ),
                // Last Cancun block
                (
                    Head { number: 1735376, timestamp: 1741159775, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x88cf81d9")), next: 1741159776 },
                ),
                // First Prague block
                (
                    Head { number: 1735377, timestamp: 1741159776, ..Default::default() },
                    ForkId {
                        hash: ForkHash(hex!("0xed88b5fd")),
                        next: sepolia::SEPOLIA_OSAKA_TIMESTAMP,
                    },
                ),
                // First Osaka block
                (
                    Head {
                        number: 1735377,
                        timestamp: sepolia::SEPOLIA_OSAKA_TIMESTAMP,
                        ..Default::default()
                    },
                    ForkId {
                        hash: ForkHash(hex!("0xe2ae4999")),
                        next: sepolia::SEPOLIA_BPO1_TIMESTAMP,
                    },
                ),
            ],
        );
    }

    #[test]
    fn dev_fork_ids() {
        test_fork_ids(
            &DEV,
            &[(
                Head { number: 0, ..Default::default() },
                ForkId { hash: ForkHash(hex!("0x0b1a4ef7")), next: 0 },
            )],
        )
    }

    /// Checks that time-based forks work
    ///
    /// This is based off of the test vectors here: <https://github.com/ethereum/go-ethereum/blob/5c8cc10d1e05c23ff1108022f4150749e73c0ca1/core/forkid/forkid_test.go#L155-L188>
    #[test]
    fn timestamped_forks() {
        let mainnet_with_timestamps = ChainSpecBuilder::mainnet().build();
        test_fork_ids(
            &mainnet_with_timestamps,
            &[
                (
                    Head { number: 0, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xfc64ec04")), next: 1150000 },
                ), // Unsynced
                (
                    Head { number: 1149999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xfc64ec04")), next: 1150000 },
                ), // Last Frontier block
                (
                    Head { number: 1150000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x97c2c34c")), next: 1920000 },
                ), // First Homestead block
                (
                    Head { number: 1919999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x97c2c34c")), next: 1920000 },
                ), // Last Homestead block
                (
                    Head { number: 1920000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x91d1f948")), next: 2463000 },
                ), // First DAO block
                (
                    Head { number: 2462999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x91d1f948")), next: 2463000 },
                ), // Last DAO block
                (
                    Head { number: 2463000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x7a64da13")), next: 2675000 },
                ), // First Tangerine block
                (
                    Head { number: 2674999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x7a64da13")), next: 2675000 },
                ), // Last Tangerine block
                (
                    Head { number: 2675000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x3edd5b10")), next: 4370000 },
                ), // First Spurious block
                (
                    Head { number: 4369999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x3edd5b10")), next: 4370000 },
                ), // Last Spurious block
                (
                    Head { number: 4370000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xa00bc324")), next: 7280000 },
                ), // First Byzantium block
                (
                    Head { number: 7279999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xa00bc324")), next: 7280000 },
                ), // Last Byzantium block
                (
                    Head { number: 7280000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x668db0af")), next: 9069000 },
                ), // First and last Constantinople, first Petersburg block
                (
                    Head { number: 9068999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x668db0af")), next: 9069000 },
                ), // Last Petersburg block
                (
                    Head { number: 9069000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x879d6e30")), next: 9200000 },
                ), // First Istanbul and first Muir Glacier block
                (
                    Head { number: 9199999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x879d6e30")), next: 9200000 },
                ), // Last Istanbul and first Muir Glacier block
                (
                    Head { number: 9200000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xe029e991")), next: 12244000 },
                ), // First Muir Glacier block
                (
                    Head { number: 12243999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xe029e991")), next: 12244000 },
                ), // Last Muir Glacier block
                (
                    Head { number: 12244000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x0eb440f6")), next: 12965000 },
                ), // First Berlin block
                (
                    Head { number: 12964999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x0eb440f6")), next: 12965000 },
                ), // Last Berlin block
                (
                    Head { number: 12965000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xb715077d")), next: 13773000 },
                ), // First London block
                (
                    Head { number: 13772999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xb715077d")), next: 13773000 },
                ), // Last London block
                (
                    Head { number: 13773000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x20c327fc")), next: 15050000 },
                ), // First Arrow Glacier block
                (
                    Head { number: 15049999, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x20c327fc")), next: 15050000 },
                ), // Last Arrow Glacier block
                (
                    Head { number: 15050000, timestamp: 0, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xf0afd0e3")), next: 1681338455 },
                ), // First Gray Glacier block
                (
                    Head { number: 19999999, timestamp: 1667999999, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xf0afd0e3")), next: 1681338455 },
                ), // Last Gray Glacier block
                (
                    Head { number: 20000000, timestamp: 1681338455, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xdce96c2d")), next: 1710338135 },
                ), // Last Shanghai block
                (
                    Head { number: 20000001, timestamp: 1710338134, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0xdce96c2d")), next: 1710338135 },
                ), // First Cancun block
                (
                    Head { number: 20000002, timestamp: 1710338135, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x9f3d2254")), next: 1746612311 },
                ), // Last Cancun block
                (
                    Head { number: 20000003, timestamp: 1746612310, ..Default::default() },
                    ForkId { hash: ForkHash(hex!("0x9f3d2254")), next: 1746612311 },
                ), // First Prague block
                (
                    Head { number: 20000004, timestamp: 1746612311, ..Default::default() },
                    ForkId {
                        hash: ForkHash(hex!("0xc376cf8b")),
                        next: mainnet::MAINNET_OSAKA_TIMESTAMP,
                    },
                ),
                // Osaka block
                (
                    Head {
                        number: 20000004,
                        timestamp: mainnet::MAINNET_OSAKA_TIMESTAMP,
                        ..Default::default()
                    },
                    ForkId {
                        hash: ForkHash(hex!("0x5167e2a6")),
                        next: mainnet::MAINNET_BPO1_TIMESTAMP,
                    },
                ),
            ],
        );
    }

    /// Constructs a [`ChainSpec`] with the given [`ChainSpecBuilder`], shanghai, and cancun fork
    /// timestamps.
    fn construct_chainspec(
        builder: ChainSpecBuilder,
        shanghai_time: u64,
        cancun_time: u64,
    ) -> ChainSpec {
        builder
            .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(shanghai_time))
            .with_fork(EthereumHardfork::Cancun, ForkCondition::Timestamp(cancun_time))
            .build()
    }

    /// Tests that time-based forks which are active at genesis are not included in forkid hash.
    ///
    /// This is based off of the test vectors here:
    /// <https://github.com/ethereum/go-ethereum/blob/2e02c1ffd9dffd1ec9e43c6b66f6c9bd1e556a0b/core/forkid/forkid_test.go#L390-L440>
    #[test]
    fn test_timestamp_fork_in_genesis() {
        let timestamp = 1690475657u64;
        let default_spec_builder = ChainSpecBuilder::default()
            .chain(Chain::from_id(1337))
            .genesis(Genesis::default().with_timestamp(timestamp))
            .paris_activated();

        // test format: (chain spec, expected next value) - the forkhash will be determined by the
        // genesis hash of the constructed chainspec
        let tests = [
            (
                construct_chainspec(default_spec_builder.clone(), timestamp - 1, timestamp + 1),
                timestamp + 1,
            ),
            (
                construct_chainspec(default_spec_builder.clone(), timestamp, timestamp + 1),
                timestamp + 1,
            ),
            (
                construct_chainspec(default_spec_builder, timestamp + 1, timestamp + 2),
                timestamp + 1,
            ),
        ];

        for (spec, expected_timestamp) in tests {
            let got_forkid = spec.fork_id(&Head { number: 0, timestamp: 0, ..Default::default() });
            // This is slightly different from the geth test because we use the shanghai timestamp
            // to determine whether or not to include a withdrawals root in the genesis header.
            // This makes the genesis hash different, and as a result makes the ChainSpec fork hash
            // different.
            let genesis_hash = spec.genesis_hash();
            let expected_forkid =
                ForkId { hash: ForkHash::from(genesis_hash), next: expected_timestamp };
            assert_eq!(got_forkid, expected_forkid);
        }
    }

    /// Checks that the fork is not active at a terminal ttd block.
    #[test]
    fn check_terminal_ttd() {
        let chainspec = ChainSpecBuilder::mainnet().build();

        // Check that Paris is not active on terminal PoW block #15537393.
        let terminal_block_ttd = U256::from(58750003716598352816469_u128);
        let terminal_block_difficulty = U256::from(11055787484078698_u128);
        assert!(!chainspec
            .fork(EthereumHardfork::Paris)
            .active_at_ttd(terminal_block_ttd, terminal_block_difficulty));

        // Check that Paris is active on first PoS block #15537394.
        let first_pos_block_ttd = U256::from(58750003716598352816469_u128);
        let first_pos_difficulty = U256::ZERO;
        assert!(chainspec
            .fork(EthereumHardfork::Paris)
            .active_at_ttd(first_pos_block_ttd, first_pos_difficulty));
    }

    #[test]
    fn geth_genesis_with_shanghai() {
        let geth_genesis = r#"
        {
          "config": {
            "chainId": 1337,
            "homesteadBlock": 0,
            "eip150Block": 0,
            "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "eip155Block": 0,
            "eip158Block": 0,
            "byzantiumBlock": 0,
            "constantinopleBlock": 0,
            "petersburgBlock": 0,
            "istanbulBlock": 0,
            "muirGlacierBlock": 0,
            "berlinBlock": 0,
            "londonBlock": 0,
            "arrowGlacierBlock": 0,
            "grayGlacierBlock": 0,
            "shanghaiTime": 0,
            "cancunTime": 1,
            "terminalTotalDifficulty": 0,
            "terminalTotalDifficultyPassed": true,
            "ethash": {}
          },
          "nonce": "0x0",
          "timestamp": "0x0",
          "extraData": "0x",
          "gasLimit": "0x4c4b40",
          "difficulty": "0x1",
          "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
          "coinbase": "0x0000000000000000000000000000000000000000",
          "alloc": {
            "658bdf435d810c91414ec09147daa6db62406379": {
              "balance": "0x487a9a304539440000"
            },
            "aa00000000000000000000000000000000000000": {
              "code": "0x6042",
              "storage": {
                "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000000",
                "0x0100000000000000000000000000000000000000000000000000000000000000": "0x0100000000000000000000000000000000000000000000000000000000000000",
                "0x0200000000000000000000000000000000000000000000000000000000000000": "0x0200000000000000000000000000000000000000000000000000000000000000",
                "0x0300000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000303"
              },
              "balance": "0x1",
              "nonce": "0x1"
            },
            "bb00000000000000000000000000000000000000": {
              "code": "0x600154600354",
              "storage": {
                "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000000",
                "0x0100000000000000000000000000000000000000000000000000000000000000": "0x0100000000000000000000000000000000000000000000000000000000000000",
                "0x0200000000000000000000000000000000000000000000000000000000000000": "0x0200000000000000000000000000000000000000000000000000000000000000",
                "0x0300000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000303"
              },
              "balance": "0x2",
              "nonce": "0x1"
            }
          },
          "number": "0x0",
          "gasUsed": "0x0",
          "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
          "baseFeePerGas": "0x3b9aca00"
        }
        "#;

        let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap();
        let chainspec = ChainSpec::from(genesis);

        // assert a bunch of hardforks that should be set
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::Homestead).unwrap(),
            ForkCondition::Block(0)
        );
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::Tangerine).unwrap(),
            ForkCondition::Block(0)
        );
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::SpuriousDragon).unwrap(),
            ForkCondition::Block(0)
        );
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::Byzantium).unwrap(),
            ForkCondition::Block(0)
        );
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::Constantinople).unwrap(),
            ForkCondition::Block(0)
        );
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::Petersburg).unwrap(),
            ForkCondition::Block(0)
        );
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::Istanbul).unwrap(),
            ForkCondition::Block(0)
        );
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::MuirGlacier).unwrap(),
            ForkCondition::Block(0)
        );
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::Berlin).unwrap(),
            ForkCondition::Block(0)
        );
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::London).unwrap(),
            ForkCondition::Block(0)
        );
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::ArrowGlacier).unwrap(),
            ForkCondition::Block(0)
        );
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::GrayGlacier).unwrap(),
            ForkCondition::Block(0)
        );

        // including time based hardforks
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::Shanghai).unwrap(),
            ForkCondition::Timestamp(0)
        );

        // including time based hardforks
        assert_eq!(
            chainspec.hardforks.get(EthereumHardfork::Cancun).unwrap(),
            ForkCondition::Timestamp(1)
        );

        // alloc key -> expected rlp mapping
        let key_rlp = vec![
            (
                hex!("0x658bdf435d810c91414ec09147daa6db62406379"),
                &hex!(
                    "0xf84d8089487a9a304539440000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
                )[..],
            ),
            (
                hex!("0xaa00000000000000000000000000000000000000"),
                &hex!(
                    "0xf8440101a08afc95b7d18a226944b9c2070b6bda1c3a36afcc3730429d47579c94b9fe5850a0ce92c756baff35fa740c3557c1a971fd24d2d35b7c8e067880d50cd86bb0bc99"
                )[..],
            ),
            (
                hex!("0xbb00000000000000000000000000000000000000"),
                &hex!(
                    "0xf8440102a08afc95b7d18a226944b9c2070b6bda1c3a36afcc3730429d47579c94b9fe5850a0e25a53cbb501cec2976b393719c63d832423dd70a458731a0b64e4847bbca7d2"
                )[..],
            ),
        ];

        for (key, expected_rlp) in key_rlp {
            let account = chainspec.genesis.alloc.get(&key).expect("account should exist");
            assert_eq!(&alloy_rlp::encode(TrieAccount::from(account.clone())), expected_rlp);
        }

        let expected_state_root: B256 =
            hex!("0x078dc6061b1d8eaa8493384b59c9c65ceb917201221d08b80c4de6770b6ec7e7").into();
        assert_eq!(chainspec.genesis_header().state_root, expected_state_root);

        assert_eq!(chainspec.genesis_header().withdrawals_root, Some(EMPTY_ROOT_HASH));

        let expected_hash: B256 =
            hex!("0x1fc027d65f820d3eef441ebeec139ebe09e471cf98516dce7b5643ccb27f418c").into();
        let hash = chainspec.genesis_hash();
        assert_eq!(hash, expected_hash);
    }

    #[test]
    fn hive_geth_json() {
        let hive_json = r#"
        {
            "nonce": "0x0000000000000042",
            "difficulty": "0x2123456",
            "mixHash": "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234",
            "coinbase": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "timestamp": "0x123456",
            "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "extraData": "0xfafbfcfd",
            "gasLimit": "0x2fefd8",
            "alloc": {
                "dbdbdb2cbd23b783741e8d7fcf51e459b497e4a6": {
                    "balance": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
                },
                "e6716f9544a56c530d868e4bfbacb172315bdead": {
                    "balance": "0x11",
                    "code": "0x12"
                },
                "b9c015918bdaba24b4ff057a92a3873d6eb201be": {
                    "balance": "0x21",
                    "storage": {
                        "0x0000000000000000000000000000000000000000000000000000000000000001": "0x22"
                    }
                },
                "1a26338f0d905e295fccb71fa9ea849ffa12aaf4": {
                    "balance": "0x31",
                    "nonce": "0x32"
                },
                "0000000000000000000000000000000000000001": {
                    "balance": "0x41"
                },
                "0000000000000000000000000000000000000002": {
                    "balance": "0x51"
                },
                "0000000000000000000000000000000000000003": {
                    "balance": "0x61"
                },
                "0000000000000000000000000000000000000004": {
                    "balance": "0x71"
                }
            },
            "config": {
                "ethash": {},
                "chainId": 10,
                "homesteadBlock": 0,
                "eip150Block": 0,
                "eip155Block": 0,
                "eip158Block": 0,
                "byzantiumBlock": 0,
                "constantinopleBlock": 0,
                "petersburgBlock": 0,
                "istanbulBlock": 0
            }
        }
        "#;

        let genesis = serde_json::from_str::<Genesis>(hive_json).unwrap();
        let chainspec: ChainSpec = genesis.into();
        assert_eq!(chainspec.chain, Chain::from_named(NamedChain::Optimism));
        let expected_state_root: B256 =
            hex!("0x9a6049ac535e3dc7436c189eaa81c73f35abd7f282ab67c32944ff0301d63360").into();
        assert_eq!(chainspec.genesis_header().state_root, expected_state_root);
        let hard_forks = vec![
            EthereumHardfork::Byzantium,
            EthereumHardfork::Homestead,
            EthereumHardfork::Istanbul,
            EthereumHardfork::Petersburg,
            EthereumHardfork::Constantinople,
        ];
        for fork in hard_forks {
            assert_eq!(chainspec.hardforks.get(fork).unwrap(), ForkCondition::Block(0));
        }

        let expected_hash: B256 =
            hex!("0x5ae31c6522bd5856129f66be3d582b842e4e9faaa87f21cce547128339a9db3c").into();
        let hash = chainspec.genesis_header().hash_slow();
        assert_eq!(hash, expected_hash);
    }

    #[test]
    fn test_hive_paris_block_genesis_json() {
        // this tests that we can handle `parisBlock` in the genesis json and can use it to output
        // a correct forkid
        let hive_paris = r#"
        {
          "config": {
            "ethash": {},
            "chainId": 3503995874084926,
            "homesteadBlock": 0,
            "eip150Block": 6,
            "eip155Block": 12,
            "eip158Block": 12,
            "byzantiumBlock": 18,
            "constantinopleBlock": 24,
            "petersburgBlock": 30,
            "istanbulBlock": 36,
            "muirGlacierBlock": 42,
            "berlinBlock": 48,
            "londonBlock": 54,
            "arrowGlacierBlock": 60,
            "grayGlacierBlock": 66,
            "mergeNetsplitBlock": 72,
            "terminalTotalDifficulty": 9454784,
            "shanghaiTime": 780,
            "cancunTime": 840
          },
          "nonce": "0x0",
          "timestamp": "0x0",
          "extraData": "0x68697665636861696e",
          "gasLimit": "0x23f3e20",
          "difficulty": "0x20000",
          "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
          "coinbase": "0x0000000000000000000000000000000000000000",
          "alloc": {
            "000f3df6d732807ef1319fb7b8bb8522d0beac02": {
              "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500",
              "balance": "0x2a"
            },
            "0c2c51a0990aee1d73c1228de158688341557508": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "14e46043e63d0e3cdcf2530519f4cfaf35058cb2": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "16c57edf7fa9d9525378b0b81bf8a3ced0620c1c": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "1f4924b14f34e24159387c0a4cdbaa32f3ddb0cf": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "1f5bde34b4afc686f136c7a3cb6ec376f7357759": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "2d389075be5be9f2246ad654ce152cf05990b209": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "3ae75c08b4c907eb63a8960c45b86e1e9ab6123c": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "4340ee1b812acb40a1eb561c019c327b243b92df": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "4a0f1452281bcec5bd90c3dce6162a5995bfe9df": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "4dde844b71bcdf95512fb4dc94e84fb67b512ed8": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "5f552da00dfb4d3749d9e62dcee3c918855a86a0": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "654aa64f5fbefb84c270ec74211b81ca8c44a72e": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "717f8aa2b982bee0e29f573d31df288663e1ce16": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "7435ed30a8b4aeb0877cef0c6e8cffe834eb865f": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "83c7e323d189f18725ac510004fdc2941f8c4a78": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "84e75c28348fb86acea1a93a39426d7d60f4cc46": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "8bebc8ba651aee624937e7d897853ac30c95a067": {
              "storage": {
                "0x0000000000000000000000000000000000000000000000000000000000000001": "0x0000000000000000000000000000000000000000000000000000000000000001",
                "0x0000000000000000000000000000000000000000000000000000000000000002": "0x0000000000000000000000000000000000000000000000000000000000000002",
                "0x0000000000000000000000000000000000000000000000000000000000000003": "0x0000000000000000000000000000000000000000000000000000000000000003"
              },
              "balance": "0x1",
              "nonce": "0x1"
            },
            "c7b99a164efd027a93f147376cc7da7c67c6bbe0": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "d803681e487e6ac18053afc5a6cd813c86ec3e4d": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "e7d13f7aa2a838d24c59b40186a0aca1e21cffcc": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            },
            "eda8645ba6948855e3b3cd596bbb07596d59c603": {
              "balance": "0xc097ce7bc90715b34b9f1000000000"
            }
          },
          "number": "0x0",
          "gasUsed": "0x0",
          "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
          "baseFeePerGas": null,
          "excessBlobGas": null,
          "blobGasUsed": null
        }
        "#;

        // check that it deserializes properly
        let genesis: Genesis = serde_json::from_str(hive_paris).unwrap();
        let chainspec = ChainSpec::from(genesis);

        // make sure we are at ForkHash("bc0c2605") with Head post-cancun
        let expected_forkid = ForkId { hash: ForkHash(hex!("0xbc0c2605")), next: 0 };
        let got_forkid =
            chainspec.fork_id(&Head { number: 73, timestamp: 840, ..Default::default() });

        // check that they're the same
        assert_eq!(got_forkid, expected_forkid);
        // Check that paris block and final difficulty are set correctly
        assert_eq!(chainspec.paris_block_and_final_difficulty, Some((72, U256::from(9454784))));
    }

    #[test]
    fn test_parse_genesis_json() {
        let s = r#"{"config":{"ethash":{},"chainId":1337,"homesteadBlock":0,"eip150Block":0,"eip155Block":0,"eip158Block":0,"byzantiumBlock":0,"constantinopleBlock":0,"petersburgBlock":0,"istanbulBlock":0,"berlinBlock":0,"londonBlock":0,"terminalTotalDifficulty":0,"terminalTotalDifficultyPassed":true,"shanghaiTime":0},"nonce":"0x0","timestamp":"0x0","extraData":"0x","gasLimit":"0x4c4b40","difficulty":"0x1","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","coinbase":"0x0000000000000000000000000000000000000000","alloc":{"658bdf435d810c91414ec09147daa6db62406379":{"balance":"0x487a9a304539440000"},"aa00000000000000000000000000000000000000":{"code":"0x6042","storage":{"0x0000000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000000","0x0100000000000000000000000000000000000000000000000000000000000000":"0x0100000000000000000000000000000000000000000000000000000000000000","0x0200000000000000000000000000000000000000000000000000000000000000":"0x0200000000000000000000000000000000000000000000000000000000000000","0x0300000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000303"},"balance":"0x1","nonce":"0x1"},"bb00000000000000000000000000000000000000":{"code":"0x600154600354","storage":{"0x0000000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000000","0x0100000000000000000000000000000000000000000000000000000000000000":"0x0100000000000000000000000000000000000000000000000000000000000000","0x0200000000000000000000000000000000000000000000000000000000000000":"0x0200000000000000000000000000000000000000000000000000000000000000","0x0300000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000303"},"balance":"0x2","nonce":"0x1"}},"number":"0x0","gasUsed":"0x0","parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","baseFeePerGas":"0x1337"}"#;
        let genesis: Genesis = serde_json::from_str(s).unwrap();
        let acc = genesis
            .alloc
            .get(&"0xaa00000000000000000000000000000000000000".parse::<Address>().unwrap())
            .unwrap();
        assert_eq!(acc.balance, U256::from(1));
        assert_eq!(genesis.base_fee_per_gas, Some(0x1337));
    }

    #[test]
    fn test_parse_cancun_genesis_json() {
        let s = r#"{"config":{"ethash":{},"chainId":1337,"homesteadBlock":0,"eip150Block":0,"eip155Block":0,"eip158Block":0,"byzantiumBlock":0,"constantinopleBlock":0,"petersburgBlock":0,"istanbulBlock":0,"berlinBlock":0,"londonBlock":0,"terminalTotalDifficulty":0,"terminalTotalDifficultyPassed":true,"shanghaiTime":0,"cancunTime":4661},"nonce":"0x0","timestamp":"0x0","extraData":"0x","gasLimit":"0x4c4b40","difficulty":"0x1","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","coinbase":"0x0000000000000000000000000000000000000000","alloc":{"658bdf435d810c91414ec09147daa6db62406379":{"balance":"0x487a9a304539440000"},"aa00000000000000000000000000000000000000":{"code":"0x6042","storage":{"0x0000000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000000","0x0100000000000000000000000000000000000000000000000000000000000000":"0x0100000000000000000000000000000000000000000000000000000000000000","0x0200000000000000000000000000000000000000000000000000000000000000":"0x0200000000000000000000000000000000000000000000000000000000000000","0x0300000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000303"},"balance":"0x1","nonce":"0x1"},"bb00000000000000000000000000000000000000":{"code":"0x600154600354","storage":{"0x0000000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000000","0x0100000000000000000000000000000000000000000000000000000000000000":"0x0100000000000000000000000000000000000000000000000000000000000000","0x0200000000000000000000000000000000000000000000000000000000000000":"0x0200000000000000000000000000000000000000000000000000000000000000","0x0300000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000303"},"balance":"0x2","nonce":"0x1"}},"number":"0x0","gasUsed":"0x0","parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","baseFeePerGas":"0x3b9aca00"}"#;
        let genesis: Genesis = serde_json::from_str(s).unwrap();
        let acc = genesis
            .alloc
            .get(&"0xaa00000000000000000000000000000000000000".parse::<Address>().unwrap())
            .unwrap();
        assert_eq!(acc.balance, U256::from(1));
        // assert that the cancun time was picked up
        assert_eq!(genesis.config.cancun_time, Some(4661));
    }

    #[test]
    fn test_parse_prague_genesis_all_formats() {
        let s = r#"{"config":{"ethash":{},"chainId":1337,"homesteadBlock":0,"eip150Block":0,"eip155Block":0,"eip158Block":0,"byzantiumBlock":0,"constantinopleBlock":0,"petersburgBlock":0,"istanbulBlock":0,"berlinBlock":0,"londonBlock":0,"terminalTotalDifficulty":0,"terminalTotalDifficultyPassed":true,"shanghaiTime":0,"cancunTime":4661, "pragueTime": 4662},"nonce":"0x0","timestamp":"0x0","extraData":"0x","gasLimit":"0x4c4b40","difficulty":"0x1","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","coinbase":"0x0000000000000000000000000000000000000000","alloc":{"658bdf435d810c91414ec09147daa6db62406379":{"balance":"0x487a9a304539440000"},"aa00000000000000000000000000000000000000":{"code":"0x6042","storage":{"0x0000000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000000","0x0100000000000000000000000000000000000000000000000000000000000000":"0x0100000000000000000000000000000000000000000000000000000000000000","0x0200000000000000000000000000000000000000000000000000000000000000":"0x0200000000000000000000000000000000000000000000000000000000000000","0x0300000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000303"},"balance":"0x1","nonce":"0x1"},"bb00000000000000000000000000000000000000":{"code":"0x600154600354","storage":{"0x0000000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000000","0x0100000000000000000000000000000000000000000000000000000000000000":"0x0100000000000000000000000000000000000000000000000000000000000000","0x0200000000000000000000000000000000000000000000000000000000000000":"0x0200000000000000000000000000000000000000000000000000000000000000","0x0300000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000303"},"balance":"0x2","nonce":"0x1"}},"number":"0x0","gasUsed":"0x0","parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","baseFeePerGas":"0x3b9aca00"}"#;
        let genesis: Genesis = serde_json::from_str(s).unwrap();

        // assert that the alloc was picked up
        let acc = genesis
            .alloc
            .get(&"0xaa00000000000000000000000000000000000000".parse::<Address>().unwrap())
            .unwrap();
        assert_eq!(acc.balance, U256::from(1));
        // assert that the cancun time was picked up
        assert_eq!(genesis.config.cancun_time, Some(4661));
        // assert that the prague time was picked up
        assert_eq!(genesis.config.prague_time, Some(4662));
    }

    #[test]
    fn test_parse_cancun_genesis_all_formats() {
        let s = r#"{"config":{"ethash":{},"chainId":1337,"homesteadBlock":0,"eip150Block":0,"eip155Block":0,"eip158Block":0,"byzantiumBlock":0,"constantinopleBlock":0,"petersburgBlock":0,"istanbulBlock":0,"berlinBlock":0,"londonBlock":0,"terminalTotalDifficulty":0,"terminalTotalDifficultyPassed":true,"shanghaiTime":0,"cancunTime":4661},"nonce":"0x0","timestamp":"0x0","extraData":"0x","gasLimit":"0x4c4b40","difficulty":"0x1","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","coinbase":"0x0000000000000000000000000000000000000000","alloc":{"658bdf435d810c91414ec09147daa6db62406379":{"balance":"0x487a9a304539440000"},"aa00000000000000000000000000000000000000":{"code":"0x6042","storage":{"0x0000000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000000","0x0100000000000000000000000000000000000000000000000000000000000000":"0x0100000000000000000000000000000000000000000000000000000000000000","0x0200000000000000000000000000000000000000000000000000000000000000":"0x0200000000000000000000000000000000000000000000000000000000000000","0x0300000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000303"},"balance":"0x1","nonce":"0x1"},"bb00000000000000000000000000000000000000":{"code":"0x600154600354","storage":{"0x0000000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000000","0x0100000000000000000000000000000000000000000000000000000000000000":"0x0100000000000000000000000000000000000000000000000000000000000000","0x0200000000000000000000000000000000000000000000000000000000000000":"0x0200000000000000000000000000000000000000000000000000000000000000","0x0300000000000000000000000000000000000000000000000000000000000000":"0x0000000000000000000000000000000000000000000000000000000000000303"},"balance":"0x2","nonce":"0x1"}},"number":"0x0","gasUsed":"0x0","parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","baseFeePerGas":"0x3b9aca00"}"#;
        let genesis: Genesis = serde_json::from_str(s).unwrap();

        // assert that the alloc was picked up
        let acc = genesis
            .alloc
            .get(&"0xaa00000000000000000000000000000000000000".parse::<Address>().unwrap())
            .unwrap();
        assert_eq!(acc.balance, U256::from(1));
        // assert that the cancun time was picked up
        assert_eq!(genesis.config.cancun_time, Some(4661));
    }

    #[test]
    fn test_paris_block_and_total_difficulty() {
        let genesis = Genesis { gas_limit: 0x2fefd8u64, ..Default::default() };
        let paris_chainspec = ChainSpecBuilder::default()
            .chain(Chain::from_id(1337))
            .genesis(genesis)
            .paris_activated()
            .build();
        assert_eq!(paris_chainspec.paris_block_and_final_difficulty, Some((0, U256::ZERO)));
    }

    #[test]
    fn test_default_cancun_header_forkhash() {
        // set the gas limit from the hive test genesis according to the hash
        let genesis = Genesis { gas_limit: 0x2fefd8u64, ..Default::default() };
        let default_chainspec = ChainSpecBuilder::default()
            .chain(Chain::from_id(1337))
            .genesis(genesis)
            .cancun_activated()
            .build();
        let mut header = default_chainspec.genesis_header().clone();

        // set the state root to the same as in the hive test the hash was pulled from
        header.state_root =
            B256::from_str("0x62e2595e017f0ca23e08d17221010721a71c3ae932f4ea3cb12117786bb392d4")
                .unwrap();

        // shanghai is activated so we should have a withdrawals root
        assert_eq!(header.withdrawals_root, Some(EMPTY_WITHDRAWALS));

        // cancun is activated so we should have a zero parent beacon block root, zero blob gas
        // used, and zero excess blob gas
        assert_eq!(header.parent_beacon_block_root, Some(B256::ZERO));
        assert_eq!(header.blob_gas_used, Some(0));
        assert_eq!(header.excess_blob_gas, Some(0));

        // check the genesis hash
        let genesis_hash = header.hash_slow();
        let expected_hash =
            b256!("0x16bb7c59613a5bad3f7c04a852fd056545ade2483968d9a25a1abb05af0c4d37");
        assert_eq!(genesis_hash, expected_hash);

        // check that the forkhash is correct
        let expected_forkhash = ForkHash(hex!("0x8062457a"));
        assert_eq!(ForkHash::from(genesis_hash), expected_forkhash);
    }

    #[test]
    fn holesky_paris_activated_at_genesis() {
        assert!(HOLESKY
            .fork(EthereumHardfork::Paris)
            .active_at_ttd(HOLESKY.genesis.difficulty, HOLESKY.genesis.difficulty));
    }

    #[test]
    fn test_genesis_format_deserialization() {
        // custom genesis with chain config
        let config = ChainConfig {
            chain_id: 2600,
            homestead_block: Some(0),
            eip150_block: Some(0),
            eip155_block: Some(0),
            eip158_block: Some(0),
            byzantium_block: Some(0),
            constantinople_block: Some(0),
            petersburg_block: Some(0),
            istanbul_block: Some(0),
            berlin_block: Some(0),
            london_block: Some(0),
            shanghai_time: Some(0),
            terminal_total_difficulty: Some(U256::ZERO),
            terminal_total_difficulty_passed: true,
            ..Default::default()
        };
        // genesis
        let genesis = Genesis {
            config,
            nonce: 0,
            timestamp: 1698688670,
            gas_limit: 5000,
            difficulty: U256::ZERO,
            mix_hash: B256::ZERO,
            coinbase: Address::ZERO,
            ..Default::default()
        };

        // seed accounts after genesis struct created
        let address = hex!("0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").into();
        let account = GenesisAccount::default().with_balance(U256::from(33));
        let genesis = genesis.extend_accounts(HashMap::from([(address, account)]));

        // ensure genesis is deserialized correctly
        let serialized_genesis = serde_json::to_string(&genesis).unwrap();
        let deserialized_genesis: Genesis = serde_json::from_str(&serialized_genesis).unwrap();

        assert_eq!(genesis, deserialized_genesis);
    }

    #[test]
    fn check_fork_id_chainspec_with_fork_condition_never() {
        let spec: ChainSpec = ChainSpec {
            chain: Chain::mainnet(),
            genesis: Genesis::default(),
            hardforks: ChainHardforks::new(vec![(
                EthereumHardfork::Frontier.boxed(),
                ForkCondition::Never,
            )]),
            paris_block_and_final_difficulty: None,
            deposit_contract: None,
            ..Default::default()
        };

        assert_eq!(spec.hardfork_fork_id(EthereumHardfork::Frontier), None);
    }

    #[test]
    fn check_fork_filter_chainspec_with_fork_condition_never() {
        let spec: ChainSpec = ChainSpec {
            chain: Chain::mainnet(),
            genesis: Genesis::default(),
            hardforks: ChainHardforks::new(vec![(
                EthereumHardfork::Shanghai.boxed(),
                ForkCondition::Never,
            )]),
            paris_block_and_final_difficulty: None,
            deposit_contract: None,
            ..Default::default()
        };

        assert_eq!(spec.hardfork_fork_filter(EthereumHardfork::Shanghai), None);
    }

    #[test]
    fn latest_eth_mainnet_fork_id() {
        // BPO2
        assert_eq!(ForkId { hash: ForkHash(hex!("0x07c9462e")), next: 0 }, MAINNET.latest_fork_id())
    }

    #[test]
    fn latest_hoodi_mainnet_fork_id() {
        // BPO2
        assert_eq!(ForkId { hash: ForkHash(hex!("0x23aa1351")), next: 0 }, HOODI.latest_fork_id())
    }

    #[test]
    fn latest_holesky_mainnet_fork_id() {
        // BPO2
        assert_eq!(ForkId { hash: ForkHash(hex!("0x9bc6cb31")), next: 0 }, HOLESKY.latest_fork_id())
    }

    #[test]
    fn latest_sepolia_mainnet_fork_id() {
        // BPO2
        assert_eq!(ForkId { hash: ForkHash(hex!("0x268956b6")), next: 0 }, SEPOLIA.latest_fork_id())
    }

    #[test]
    fn test_fork_order_ethereum_mainnet() {
        let genesis = Genesis {
            config: ChainConfig {
                chain_id: 0,
                homestead_block: Some(0),
                dao_fork_block: Some(0),
                dao_fork_support: false,
                eip150_block: Some(0),
                eip155_block: Some(0),
                eip158_block: Some(0),
                byzantium_block: Some(0),
                constantinople_block: Some(0),
                petersburg_block: Some(0),
                istanbul_block: Some(0),
                muir_glacier_block: Some(0),
                berlin_block: Some(0),
                london_block: Some(0),
                arrow_glacier_block: Some(0),
                gray_glacier_block: Some(0),
                merge_netsplit_block: Some(0),
                shanghai_time: Some(0),
                cancun_time: Some(0),
                terminal_total_difficulty: Some(U256::ZERO),
                ..Default::default()
            },
            ..Default::default()
        };

        let chain_spec: ChainSpec = genesis.into();

        let hardforks: Vec<_> = chain_spec.hardforks.forks_iter().map(|(h, _)| h).collect();
        let expected_hardforks = vec![
            EthereumHardfork::Frontier.boxed(),
            EthereumHardfork::Homestead.boxed(),
            EthereumHardfork::Dao.boxed(),
            EthereumHardfork::Tangerine.boxed(),
            EthereumHardfork::SpuriousDragon.boxed(),
            EthereumHardfork::Byzantium.boxed(),
            EthereumHardfork::Constantinople.boxed(),
            EthereumHardfork::Petersburg.boxed(),
            EthereumHardfork::Istanbul.boxed(),
            EthereumHardfork::MuirGlacier.boxed(),
            EthereumHardfork::Berlin.boxed(),
            EthereumHardfork::London.boxed(),
            EthereumHardfork::ArrowGlacier.boxed(),
            EthereumHardfork::GrayGlacier.boxed(),
            EthereumHardfork::Paris.boxed(),
            EthereumHardfork::Shanghai.boxed(),
            EthereumHardfork::Cancun.boxed(),
        ];

        assert!(expected_hardforks
            .iter()
            .zip(hardforks.iter())
            .all(|(expected, actual)| &**expected == *actual));
        assert_eq!(expected_hardforks.len(), hardforks.len());
    }

    #[test]
    fn test_calc_base_block_reward() {
        // ((block number, td), reward)
        let cases = [
            // Pre-byzantium
            ((0, U256::ZERO), Some(ETH_TO_WEI * 5)),
            // Byzantium
            ((4370000, U256::ZERO), Some(ETH_TO_WEI * 3)),
            // Petersburg
            ((7280000, U256::ZERO), Some(ETH_TO_WEI * 2)),
            // Merge
            ((15537394, U256::from(58_750_000_000_000_000_000_000_u128)), None),
        ];

        for ((block_number, _td), expected_reward) in cases {
            assert_eq!(base_block_reward(&*MAINNET, block_number), expected_reward);
        }
    }

    #[test]
    fn test_calc_full_block_reward() {
        let base_reward = ETH_TO_WEI;
        let one_thirty_twoth_reward = base_reward >> 5;

        // (num_ommers, reward)
        let cases = [
            (0, base_reward),
            (1, base_reward + one_thirty_twoth_reward),
            (2, base_reward + one_thirty_twoth_reward * 2),
        ];

        for (num_ommers, expected_reward) in cases {
            assert_eq!(block_reward(base_reward, num_ommers), expected_reward);
        }
    }

    #[test]
    fn blob_params_from_genesis() {
        let s = r#"{
            "blobSchedule": {
                "cancun":{
                    "baseFeeUpdateFraction":3338477,
                    "max":6,
                    "target":3
                },
                "prague":{
                    "baseFeeUpdateFraction":3338477,
                    "max":6,
                    "target":3
                }
            }
        }"#;
        let config: ChainConfig = serde_json::from_str(s).unwrap();
        let hardfork_params = config.blob_schedule_blob_params();
        let expected = BlobScheduleBlobParams {
            cancun: BlobParams {
                target_blob_count: 3,
                max_blob_count: 6,
                update_fraction: 3338477,
                min_blob_fee: BLOB_TX_MIN_BLOB_GASPRICE,
                max_blobs_per_tx: 6,
                blob_base_cost: 0,
            },
            prague: BlobParams {
                target_blob_count: 3,
                max_blob_count: 6,
                update_fraction: 3338477,
                min_blob_fee: BLOB_TX_MIN_BLOB_GASPRICE,
                max_blobs_per_tx: 6,
                blob_base_cost: 0,
            },
            ..Default::default()
        };
        assert_eq!(hardfork_params, expected);
    }

    #[test]
    fn parse_perf_net_genesis() {
        let s = r#"{
    "config": {
        "chainId": 1,
        "homesteadBlock": 1150000,
        "daoForkBlock": 1920000,
        "daoForkSupport": true,
        "eip150Block": 2463000,
        "eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0",
        "eip155Block": 2675000,
        "eip158Block": 2675000,
        "byzantiumBlock": 4370000,
        "constantinopleBlock": 7280000,
        "petersburgBlock": 7280000,
        "istanbulBlock": 9069000,
        "muirGlacierBlock": 9200000,
        "berlinBlock": 12244000,
        "londonBlock": 12965000,
        "arrowGlacierBlock": 13773000,
        "grayGlacierBlock": 15050000,
        "terminalTotalDifficulty": 58750000000000000000000,
        "terminalTotalDifficultyPassed": true,
        "shanghaiTime": 1681338455,
        "cancunTime": 1710338135,
        "pragueTime": 1746612311,
        "ethash": {},
        "depositContractAddress": "0x00000000219ab540356cBB839Cbe05303d7705Fa",
        "blobSchedule": {
            "cancun": {
                "target": 3,
                "max": 6,
                "baseFeeUpdateFraction": 3338477
            },
            "prague": {
                "target": 6,
                "max": 9,
                "baseFeeUpdateFraction": 5007716
            }
        }
    },
    "nonce": "0x42",
    "timestamp": "0x0",
    "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa",
    "gasLimit": "0x1388",
    "difficulty": "0x400000000",
    "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "coinbase": "0x0000000000000000000000000000000000000000",
    "number": "0x0",
    "gasUsed": "0x0",
    "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "baseFeePerGas": null
}"#;

        let genesis = serde_json::from_str::<Genesis>(s).unwrap();
        let chainspec = ChainSpec::from_genesis(genesis);
        let activation = chainspec.hardforks.fork(EthereumHardfork::Paris);
        assert_eq!(
            activation,
            ForkCondition::TTD {
                activation_block_number: MAINNET_PARIS_BLOCK,
                total_difficulty: MAINNET_PARIS_TTD,
                fork_block: None,
            }
        )
    }
}
