diff --git a/.gitignore b/.gitignore index 9890060c..fafd46a6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,8 @@ tests/merged.genesis.json tests/merged.genesis.json.tar.gz tests/genesis-testnet-reth.json +crates/chainspec/res/genesis/xlayer-devnet-genesis-hash.txt +crates/chainspec/res/genesis/xlayer-devnet-state-root.txt + # Local development: cargo config for debugging with local reth .cargo diff --git a/crates/chainspec/res/genesis/xlayer-devnet.json b/crates/chainspec/res/genesis/xlayer-devnet.json new file mode 100644 index 00000000..04438ac7 --- /dev/null +++ b/crates/chainspec/res/genesis/xlayer-devnet.json @@ -0,0 +1,53 @@ +{ + "config": { + "chainId": 195, + "homesteadBlock": 0, + "daoForkSupport": false, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "arrowGlacierBlock": 0, + "grayGlacierBlock": 0, + "mergeNetsplitBlock": 0, + "shanghaiTime": 0, + "cancunTime": 0, + "pragueTime": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": false, + "bedrockBlock": 0, + "canyonTime": 0, + "ecotoneTime": 0, + "fjordTime": 0, + "graniteTime": 0, + "holoceneTime": 0, + "isthmusTime": 0, + "jovianTime": 0, + "legacyXLayerBlock": 18696116, + "optimism": { + "eip1559Denominator": 100000000, + "eip1559DenominatorCanyon": 100000000, + "eip1559Elasticity": 1 + }, + "regolithTime": 0, + "depositContractAddress": "0x0000000000000000000000000000000000000000" + }, + "nonce": "0x0", + "timestamp": "0x699d723d", + "extraData": "0x01000000fa000000060000000000000000", + "gasLimit": "0xbebc200", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x4200000000000000000000000000000000000011", + "baseFeePerGas": "0x5fc01c5", + "excessBlobGas": "0x0", + "blobGasUsed": "0x0", + "number": "0x11d47b4", + "parentHash": "0xa3a639b09fea244d577c7e7ed7bcc4eb1adb0c5b54441cd29d9949e417dfa355" +} \ No newline at end of file diff --git a/crates/chainspec/src/lib.rs b/crates/chainspec/src/lib.rs index 8d0b83e4..034a724c 100644 --- a/crates/chainspec/src/lib.rs +++ b/crates/chainspec/src/lib.rs @@ -3,10 +3,12 @@ //! This crate provides chain specifications for XLayer mainnet and testnet networks. mod parser; +mod xlayer_devnet; mod xlayer_mainnet; mod xlayer_testnet; pub use parser::XLayerChainSpecParser; +pub use xlayer_devnet::XLAYER_DEVNET; pub use xlayer_mainnet::XLAYER_MAINNET; pub use xlayer_testnet::XLAYER_TESTNET; @@ -27,6 +29,10 @@ pub const XLAYER_MAINNET_JOVIAN_TIMESTAMP: u64 = 1764691201; /// 2025-11-28 11:00:00 UTC pub const XLAYER_TESTNET_JOVIAN_TIMESTAMP: u64 = 1764327600; +/// XLayer devnet Jovian hardfork activation timestamp +/// 2025-11-28 11:00:00 UTC +pub const XLAYER_DEVNET_JOVIAN_TIMESTAMP: u64 = 1764327600; + /// X Layer mainnet list of hardforks. /// /// All time-based hardforks are activated at genesis (timestamp 0). @@ -108,3 +114,44 @@ pub static XLAYER_TESTNET_HARDFORKS: Lazy = Lazy::new(|| { (OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(XLAYER_TESTNET_JOVIAN_TIMESTAMP)), ]) }); + +/// X Layer devnet list of hardforks. +/// +/// All time-based hardforks are activated at genesis (timestamp 0). +pub static XLAYER_DEVNET_HARDFORKS: Lazy = Lazy::new(|| { + ChainHardforks::new(vec![ + (EthereumHardfork::Frontier.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::Homestead.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::Tangerine.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::SpuriousDragon.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::Byzantium.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::Constantinople.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::Petersburg.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::Istanbul.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::MuirGlacier.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::Berlin.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::London.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::ArrowGlacier.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::GrayGlacier.boxed(), ForkCondition::Block(0)), + ( + EthereumHardfork::Paris.boxed(), + ForkCondition::TTD { + activation_block_number: 0, + fork_block: Some(0), + total_difficulty: U256::ZERO, + }, + ), + (OpHardfork::Bedrock.boxed(), ForkCondition::Block(0)), + (OpHardfork::Regolith.boxed(), ForkCondition::Timestamp(0)), + (EthereumHardfork::Shanghai.boxed(), ForkCondition::Timestamp(0)), + (OpHardfork::Canyon.boxed(), ForkCondition::Timestamp(0)), + (EthereumHardfork::Cancun.boxed(), ForkCondition::Timestamp(0)), + (OpHardfork::Ecotone.boxed(), ForkCondition::Timestamp(0)), + (OpHardfork::Fjord.boxed(), ForkCondition::Timestamp(0)), + (OpHardfork::Granite.boxed(), ForkCondition::Timestamp(0)), + (OpHardfork::Holocene.boxed(), ForkCondition::Timestamp(0)), + (EthereumHardfork::Prague.boxed(), ForkCondition::Timestamp(0)), + (OpHardfork::Isthmus.boxed(), ForkCondition::Timestamp(0)), + (OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(XLAYER_DEVNET_JOVIAN_TIMESTAMP)), + ]) +}); diff --git a/crates/chainspec/src/parser.rs b/crates/chainspec/src/parser.rs index 97e0265e..0afa6275 100644 --- a/crates/chainspec/src/parser.rs +++ b/crates/chainspec/src/parser.rs @@ -1,6 +1,6 @@ //! XLayer chain specification parser -use crate::{XLAYER_MAINNET, XLAYER_TESTNET}; +use crate::{XLAYER_DEVNET, XLAYER_MAINNET, XLAYER_TESTNET}; use alloy_genesis::Genesis; use reth_cli::chainspec::ChainSpecParser; use reth_optimism_chainspec::{generated_chain_value_parser, OpChainSpec}; @@ -12,6 +12,7 @@ use tracing::debug; /// This parser extends the default OpChainSpecParser to support XLayer chains: /// - xlayer-mainnet (chain id 196) /// - xlayer-testnet (chain id 1952) +/// - xlayer-devnet (chain id 195) /// /// It also supports all standard Optimism chains through delegation to the /// upstream OpChainSpecParser. @@ -34,6 +35,7 @@ impl ChainSpecParser for XLayerChainSpecParser { // XLayer chains "xlayer-mainnet", "xlayer-testnet", + "xlayer-devnet", ]; fn parse(s: &str) -> eyre::Result> { @@ -81,6 +83,13 @@ fn xlayer_chain_value_parser(s: &str) -> eyre::Result> { } Ok(XLAYER_TESTNET.clone()) } + "xlayer-devnet" => { + // Support environment variable override for genesis path + if let Ok(genesis_path) = std::env::var("XLAYER_DEVNET_GENESIS") { + return Ok(Arc::new(parse_genesis(&genesis_path)?.into())); + } + Ok(XLAYER_DEVNET.clone()) + } // For other inputs, try known OP chains first, then parse as genesis _ => { // Try to match known OP chains (optimism, base, etc.) @@ -110,6 +119,12 @@ mod tests { assert_eq!(spec.chain().id(), 1952); } + #[test] + fn test_parse_xlayer_devnet() { + let spec = XLayerChainSpecParser::parse("xlayer-devnet").unwrap(); + assert_eq!(spec.chain().id(), 195); + } + #[test] fn test_parse_optimism() { let spec = XLayerChainSpecParser::parse("optimism").unwrap(); diff --git a/crates/chainspec/src/xlayer_devnet.rs b/crates/chainspec/src/xlayer_devnet.rs new file mode 100644 index 00000000..90202d24 --- /dev/null +++ b/crates/chainspec/src/xlayer_devnet.rs @@ -0,0 +1,438 @@ +//! XLayer Devnet chain specification + +use crate::XLAYER_DEVNET_HARDFORKS; +use alloy_chains::Chain; +use alloy_primitives::{B256, U256}; + +use once_cell::sync::Lazy; +use reth_chainspec::{BaseFeeParams, BaseFeeParamsKind, ChainSpec, Hardfork}; +use reth_ethereum_forks::EthereumHardfork; +use reth_optimism_chainspec::{make_op_genesis_header, OpChainSpec}; +use reth_optimism_forks::OpHardfork; +use reth_primitives_traits::SealedHeader; +use std::path::Path; +use std::sync::Arc; + +/// X Layer Devnet genesis hash +/// +/// Computed from the genesis block header. +/// Read from the resource file to pick up any updates without manual changes. +/// If the file doesn't exist, it will be created with the hash of an empty string. +pub(crate) static XLAYER_DEVNET_GENESIS_HASH: Lazy = Lazy::new(|| { + let genesis_hash_path = + Path::new(env!("CARGO_MANIFEST_DIR")).join("res/genesis/xlayer-devnet-genesis-hash.txt"); + + if !genesis_hash_path.exists() { + // Create the file with hash of empty bytes + + let empty_hash = "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; + if let Some(parent) = genesis_hash_path.parent() { + std::fs::create_dir_all(parent).expect("Failed to create genesis directory"); + } + std::fs::write(&genesis_hash_path, empty_hash) + .expect("Failed to write xlayer-devnet-genesis-hash.txt"); + } + + std::fs::read_to_string(&genesis_hash_path) + .expect("Failed to read xlayer-devnet-genesis-hash.txt") + .trim() + .parse() + .expect("Invalid XLAYER_DEVNET_GENESIS_HASH in xlayer-devnet-genesis-hash.txt") +}); + +/// X Layer Devnet genesis state root +/// +/// The Merkle Patricia Trie root of all 1,866,483 accounts in the genesis alloc. +/// Read from the resource file to pick up any updates without manual changes. +/// If the file doesn't exist, it will be created with the hash of an empty string. +pub(crate) static XLAYER_DEVNET_STATE_ROOT: Lazy = Lazy::new(|| { + let state_root_path = + Path::new(env!("CARGO_MANIFEST_DIR")).join("res/genesis/xlayer-devnet-state-root.txt"); + + if !state_root_path.exists() { + // Create the file with hash of empty hash + let empty_hash = "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; + if let Some(parent) = state_root_path.parent() { + std::fs::create_dir_all(parent).expect("Failed to create genesis directory"); + } + std::fs::write(&state_root_path, empty_hash) + .expect("Failed to write xlayer-devnet-state-root.txt"); + } + + std::fs::read_to_string(&state_root_path) + .expect("Failed to read xlayer-devnet-state-root.txt") + .trim() + .parse() + .expect("Invalid XLAYER_DEVNET_STATE_ROOT in xlayer-devnet-state-root.txt") +}); + +/// X Layer devnet chain id as specified in the published `genesis.json`. +const XLAYER_DEVNET_CHAIN_ID: u64 = 195; + +/// X Layer devnet EIP-1559 parameters. +/// +/// Same as mainnet: see `config.optimism` in `genesis-devnet.json`. +const XLAYER_DEVNET_BASE_FEE_DENOMINATOR: u128 = 100_000_000; +const XLAYER_DEVNET_BASE_FEE_ELASTICITY: u128 = 1; + +/// X Layer devnet base fee params (same for London and Canyon forks). +const XLAYER_DEVNET_BASE_FEE_PARAMS: BaseFeeParams = + BaseFeeParams::new(XLAYER_DEVNET_BASE_FEE_DENOMINATOR, XLAYER_DEVNET_BASE_FEE_ELASTICITY); + +/// The X Layer devnet spec +pub static XLAYER_DEVNET: Lazy> = Lazy::new(|| { + // Minimal genesis contains empty alloc field for fast loading + let genesis = serde_json::from_str(include_str!("../res/genesis/xlayer-devnet.json")) + .expect("Can't deserialize X Layer Devnet genesis json"); + let hardforks = XLAYER_DEVNET_HARDFORKS.clone(); + + // Build genesis header using standard helper, then override state_root with pre-computed value + let mut genesis_header = make_op_genesis_header(&genesis, &hardforks); + genesis_header.state_root = *XLAYER_DEVNET_STATE_ROOT; + // Set block number and parent hash from genesis JSON (not a standard genesis block 0) + if let Some(number) = genesis.number { + genesis_header.number = number; + } + if let Some(parent_hash) = genesis.parent_hash { + genesis_header.parent_hash = parent_hash; + } + let genesis_header = SealedHeader::new(genesis_header, *XLAYER_DEVNET_GENESIS_HASH); + + OpChainSpec { + inner: ChainSpec { + chain: Chain::from_id(XLAYER_DEVNET_CHAIN_ID), + genesis_header, + genesis, + paris_block_and_final_difficulty: Some((0, U256::from(0))), + hardforks, + base_fee_params: BaseFeeParamsKind::Variable( + vec![ + (EthereumHardfork::London.boxed(), XLAYER_DEVNET_BASE_FEE_PARAMS), + (OpHardfork::Canyon.boxed(), XLAYER_DEVNET_BASE_FEE_PARAMS), + ] + .into(), + ), + ..Default::default() + }, + } + .into() +}); + +#[cfg(test)] +mod tests { + use super::*; + use alloy_genesis::Genesis; + use alloy_primitives::{b256, hex}; + use reth_ethereum_forks::EthereumHardfork; + use reth_optimism_forks::OpHardfork; + + const XLAYER_DEVNET_BLOCK_NUMBER: u64 = 18696116; + + fn parse_genesis() -> Genesis { + serde_json::from_str(include_str!("../res/genesis/xlayer-devnet.json")) + .expect("Failed to parse xlayer-devnet.json") + } + + #[test] + fn test_xlayer_devnet_chain_id() { + assert_eq!(XLAYER_DEVNET.chain().id(), 195); + } + + #[test] + fn test_xlayer_devnet_genesis_hash() { + assert_eq!(XLAYER_DEVNET.genesis_hash(), *XLAYER_DEVNET_GENESIS_HASH); + } + + #[test] + fn test_xlayer_devnet_state_root() { + assert_eq!(XLAYER_DEVNET.genesis_header().state_root, *XLAYER_DEVNET_STATE_ROOT); + } + + #[test] + fn test_xlayer_devnet_genesis_block_number() { + assert_eq!(XLAYER_DEVNET.genesis_header().number, XLAYER_DEVNET_BLOCK_NUMBER); + } + + #[test] + fn test_xlayer_devnet_hardforks() { + let spec = &*XLAYER_DEVNET; + assert!(spec.fork(EthereumHardfork::Shanghai).active_at_timestamp(0)); + assert!(spec.fork(EthereumHardfork::Cancun).active_at_timestamp(0)); + assert!(spec.fork(OpHardfork::Bedrock).active_at_block(0)); + assert!(spec.fork(OpHardfork::Isthmus).active_at_timestamp(0)); + } + + #[test] + fn test_xlayer_devnet_base_fee_params() { + assert_eq!( + XLAYER_DEVNET.base_fee_params_at_timestamp(0), + BaseFeeParams::new( + XLAYER_DEVNET_BASE_FEE_DENOMINATOR, + XLAYER_DEVNET_BASE_FEE_ELASTICITY + ) + ); + } + + #[test] + fn test_xlayer_devnet_fast_loading() { + assert_eq!(XLAYER_DEVNET.genesis().alloc.len(), 0); + } + + #[test] + fn test_xlayer_devnet_paris_activated() { + assert_eq!(XLAYER_DEVNET.get_final_paris_total_difficulty(), Some(U256::ZERO)); + } + + #[test] + fn test_xlayer_devnet_canyon_base_fee_unchanged() { + let spec = &*XLAYER_DEVNET; + let london = spec.base_fee_params_at_timestamp(0); + let canyon = spec.base_fee_params_at_timestamp(1); + assert_eq!(london, canyon); + assert_eq!(canyon, XLAYER_DEVNET_BASE_FEE_PARAMS); + } + + #[test] + fn test_xlayer_devnet_genesis_header_fields() { + let header = XLAYER_DEVNET.genesis_header(); + assert_eq!(header.withdrawals_root, Some(alloy_consensus::constants::EMPTY_WITHDRAWALS)); + assert_eq!(header.parent_beacon_block_root, Some(B256::ZERO)); + assert_eq!(header.requests_hash, Some(alloy_eips::eip7685::EMPTY_REQUESTS_HASH)); + } + + #[test] + fn test_xlayer_devnet_all_hardforks_active() { + let spec = &*XLAYER_DEVNET; + let ts = spec.genesis_header().timestamp; + // Ethereum hardforks + assert!(spec.fork(EthereumHardfork::London).active_at_block(0)); + assert!(spec.fork(EthereumHardfork::Shanghai).active_at_timestamp(ts)); + assert!(spec.fork(EthereumHardfork::Cancun).active_at_timestamp(ts)); + assert!(spec.fork(EthereumHardfork::Prague).active_at_timestamp(ts)); + // Optimism hardforks + assert!(spec.fork(OpHardfork::Bedrock).active_at_block(0)); + assert!(spec.fork(OpHardfork::Regolith).active_at_timestamp(ts)); + assert!(spec.fork(OpHardfork::Canyon).active_at_timestamp(ts)); + assert!(spec.fork(OpHardfork::Ecotone).active_at_timestamp(ts)); + assert!(spec.fork(OpHardfork::Fjord).active_at_timestamp(ts)); + assert!(spec.fork(OpHardfork::Granite).active_at_timestamp(ts)); + assert!(spec.fork(OpHardfork::Holocene).active_at_timestamp(ts)); + assert!(spec.fork(OpHardfork::Isthmus).active_at_timestamp(ts)); + assert!(spec.fork(OpHardfork::Jovian).active_at_timestamp(ts)); + } + + #[test] + fn test_xlayer_devnet_constants_match_spec() { + assert_eq!(XLAYER_DEVNET.chain().id(), XLAYER_DEVNET_CHAIN_ID); + assert_eq!( + XLAYER_DEVNET.base_fee_params_at_timestamp(0), + BaseFeeParams::new( + XLAYER_DEVNET_BASE_FEE_DENOMINATOR, + XLAYER_DEVNET_BASE_FEE_ELASTICITY + ) + ); + } + + #[test] + fn test_xlayer_devnet_json_config_consistency() { + let genesis = parse_genesis(); + assert_eq!(genesis.config.chain_id, XLAYER_DEVNET_CHAIN_ID); + assert_eq!(genesis.number, Some(XLAYER_DEVNET_BLOCK_NUMBER)); + assert_eq!(genesis.timestamp, 0x699d723d); + assert_eq!( + genesis.extra_data.as_ref(), + hex!("0x01000000fa000000060000000000000000").as_ref() + ); + assert_eq!(genesis.gas_limit, 0xbebc200); + assert_eq!(genesis.difficulty, U256::ZERO); + assert_eq!(genesis.nonce, 0); + assert_eq!(genesis.mix_hash, B256::ZERO); + assert_eq!(genesis.coinbase.to_string(), "0x4200000000000000000000000000000000000011"); + assert_eq!( + genesis.parent_hash, + Some(b256!("0xa3a639b09fea244d577c7e7ed7bcc4eb1adb0c5b54441cd29d9949e417dfa355")) + ); + assert_eq!(genesis.base_fee_per_gas.map(|fee| fee as u64), Some(0x5fc01c5u64)); + assert_eq!(genesis.excess_blob_gas, Some(0)); + assert_eq!(genesis.blob_gas_used, Some(0)); + } + + #[test] + fn test_xlayer_devnet_json_optimism_config() { + let genesis = parse_genesis(); + let cfg = genesis.config.extra_fields.get("optimism").expect("optimism config must exist"); + assert_eq!( + cfg.get("eip1559Elasticity").and_then(|v| v.as_u64()).unwrap() as u128, + XLAYER_DEVNET_BASE_FEE_ELASTICITY + ); + assert_eq!( + cfg.get("eip1559Denominator").and_then(|v| v.as_u64()).unwrap() as u128, + XLAYER_DEVNET_BASE_FEE_DENOMINATOR + ); + assert_eq!( + cfg.get("eip1559DenominatorCanyon").and_then(|v| v.as_u64()).unwrap() as u128, + XLAYER_DEVNET_BASE_FEE_DENOMINATOR + ); + } + + #[test] + fn test_xlayer_devnet_json_hardforks_warning() { + let genesis = parse_genesis(); + // WARNING: Hardfork times in JSON are overridden by XLAYER_DEVNET_HARDFORKS + assert_eq!( + genesis.config.extra_fields.get("legacyXLayerBlock").and_then(|v| v.as_u64()), + Some(XLAYER_DEVNET_BLOCK_NUMBER) + ); + assert_eq!(genesis.config.shanghai_time, Some(0)); + assert_eq!(genesis.config.cancun_time, Some(0)); + } + + #[test] + fn test_xlayer_devnet_genesis_header_matches_json() { + let header = XLAYER_DEVNET.genesis_header(); + let genesis = parse_genesis(); + // Verify header fields match JSON (except state_root which is hardcoded) + assert_eq!(header.number, genesis.number.unwrap_or_default()); + assert_eq!(header.timestamp, genesis.timestamp); + assert_eq!(header.extra_data, genesis.extra_data); + assert_eq!(header.gas_limit, genesis.gas_limit); + assert_eq!(header.difficulty, genesis.difficulty); + assert_eq!(header.nonce, alloy_primitives::B64::from(genesis.nonce)); + assert_eq!(header.mix_hash, genesis.mix_hash); + assert_eq!(header.beneficiary, genesis.coinbase); + assert_eq!(header.parent_hash, genesis.parent_hash.unwrap_or_default()); + assert_eq!(header.base_fee_per_gas, genesis.base_fee_per_gas.map(|fee| fee as u64)); + // NOTE: state_root is hardcoded, not read from JSON + assert_eq!(header.state_root, *XLAYER_DEVNET_STATE_ROOT); + } + + #[test] + fn test_xlayer_devnet_jovian_activation() { + use crate::XLAYER_DEVNET_JOVIAN_TIMESTAMP; + + let spec = &*XLAYER_DEVNET; + + // Jovian should not be active before activation timestamp + assert!(!spec + .fork(OpHardfork::Jovian) + .active_at_timestamp(XLAYER_DEVNET_JOVIAN_TIMESTAMP - 1)); + + // Jovian should be active at activation timestamp + assert!(spec.fork(OpHardfork::Jovian).active_at_timestamp(XLAYER_DEVNET_JOVIAN_TIMESTAMP)); + + // Jovian should be active after activation timestamp + assert!(spec + .fork(OpHardfork::Jovian) + .active_at_timestamp(XLAYER_DEVNET_JOVIAN_TIMESTAMP + 1)); + } + + #[test] + fn test_xlayer_devnet_jovian_included() { + use crate::XLAYER_DEVNET_HARDFORKS; + let hardforks = &*XLAYER_DEVNET_HARDFORKS; + assert!( + hardforks.get(OpHardfork::Jovian).is_some(), + "XLayer devnet hardforks should include Jovian" + ); + } + + #[test] + fn test_xlayer_devnet_jovian_timestamp_condition() { + use crate::{XLAYER_DEVNET_HARDFORKS, XLAYER_DEVNET_JOVIAN_TIMESTAMP}; + use reth_ethereum_forks::ForkCondition; + + let xlayer_devnet = &*XLAYER_DEVNET_HARDFORKS; + + let jovian_fork = + xlayer_devnet.get(OpHardfork::Jovian).expect("XLayer devnet should have Jovian fork"); + + match jovian_fork { + ForkCondition::Timestamp(ts) => { + assert_eq!( + ts, XLAYER_DEVNET_JOVIAN_TIMESTAMP, + "Jovian fork should use XLAYER_DEVNET_JOVIAN_TIMESTAMP" + ); + } + _ => panic!("Jovian fork should use timestamp condition"), + } + } + + #[test] + fn test_xlayer_devnet_genesis_hash_is_valid_b256() { + let hash = *XLAYER_DEVNET_GENESIS_HASH; + // Verify it's a valid B256 (32 bytes) + assert_eq!(hash.len(), 32); + } + + #[test] + fn test_xlayer_devnet_state_root_is_valid_b256() { + let state_root = *XLAYER_DEVNET_STATE_ROOT; + // Verify it's a valid B256 (32 bytes) + assert_eq!(state_root.len(), 32); + } + + #[test] + fn test_xlayer_devnet_genesis_hash_file_path() { + let genesis_hash_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("res/genesis/xlayer-devnet-genesis-hash.txt"); + // Verify the path is constructed correctly + assert!(genesis_hash_path.to_string_lossy().contains("xlayer-devnet-genesis-hash.txt")); + } + + #[test] + fn test_xlayer_devnet_state_root_file_path() { + let state_root_path = + Path::new(env!("CARGO_MANIFEST_DIR")).join("res/genesis/xlayer-devnet-state-root.txt"); + // Verify the path is constructed correctly + assert!(state_root_path.to_string_lossy().contains("xlayer-devnet-state-root.txt")); + } + + #[test] + fn test_xlayer_devnet_genesis_files_created_or_exist() { + // This test verifies that after initialization, the files either exist or have been created + let genesis_hash_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("res/genesis/xlayer-devnet-genesis-hash.txt"); + let state_root_path = + Path::new(env!("CARGO_MANIFEST_DIR")).join("res/genesis/xlayer-devnet-state-root.txt"); + + // Force initialization by accessing the lazy statics + let _ = *XLAYER_DEVNET_GENESIS_HASH; + let _ = *XLAYER_DEVNET_STATE_ROOT; + + // After initialization, files should exist + assert!(genesis_hash_path.exists(), "Genesis hash file should exist after initialization"); + assert!(state_root_path.exists(), "State root file should exist after initialization"); + } + + #[test] + fn test_xlayer_devnet_genesis_hash_parseable() { + // Verify that the genesis hash can be parsed from the file + let hash = *XLAYER_DEVNET_GENESIS_HASH; + // Should not panic and should be a valid hash + assert_ne!(hash, B256::ZERO); + } + + #[test] + fn test_xlayer_devnet_state_root_parseable() { + // Verify that the state root can be parsed from the file + let state_root = *XLAYER_DEVNET_STATE_ROOT; + // Should not panic and should be a valid hash + assert_ne!(state_root, B256::ZERO); + } + + #[test] + fn test_xlayer_devnet_genesis_hash_consistent() { + // Verify that reading the hash multiple times returns the same value + let hash1 = *XLAYER_DEVNET_GENESIS_HASH; + let hash2 = *XLAYER_DEVNET_GENESIS_HASH; + assert_eq!(hash1, hash2, "Genesis hash should be consistent across multiple reads"); + } + + #[test] + fn test_xlayer_devnet_state_root_consistent() { + // Verify that reading the state root multiple times returns the same value + let root1 = *XLAYER_DEVNET_STATE_ROOT; + let root2 = *XLAYER_DEVNET_STATE_ROOT; + assert_eq!(root1, root2, "State root should be consistent across multiple reads"); + } +}