From 9ee3960393b420cf7d8dc4ef9c6e3dfc95e9ad74 Mon Sep 17 00:00:00 2001 From: Dumitrel Loghin Date: Tue, 3 Mar 2026 13:13:24 +0800 Subject: [PATCH 1/6] feat(chainspec): xlayer-devnet chainspec --- .../genesis/xlayer-devnet-genesis-hash.txt | 1 + .../res/genesis/xlayer-devnet-state-root.txt | 1 + .../chainspec/res/genesis/xlayer-devnet.json | 53 +++ crates/chainspec/src/lib.rs | 47 +++ crates/chainspec/src/parser.rs | 17 +- crates/chainspec/src/xlayer_devnet.rs | 318 ++++++++++++++++++ 6 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 crates/chainspec/res/genesis/xlayer-devnet-genesis-hash.txt create mode 100644 crates/chainspec/res/genesis/xlayer-devnet-state-root.txt create mode 100644 crates/chainspec/res/genesis/xlayer-devnet.json create mode 100644 crates/chainspec/src/xlayer_devnet.rs diff --git a/crates/chainspec/res/genesis/xlayer-devnet-genesis-hash.txt b/crates/chainspec/res/genesis/xlayer-devnet-genesis-hash.txt new file mode 100644 index 00000000..892483cb --- /dev/null +++ b/crates/chainspec/res/genesis/xlayer-devnet-genesis-hash.txt @@ -0,0 +1 @@ +a8e6bc8a65093614612138cccd568b9dd8a7ab79db9097092e639945a437f0c7 diff --git a/crates/chainspec/res/genesis/xlayer-devnet-state-root.txt b/crates/chainspec/res/genesis/xlayer-devnet-state-root.txt new file mode 100644 index 00000000..e1465abf --- /dev/null +++ b/crates/chainspec/res/genesis/xlayer-devnet-state-root.txt @@ -0,0 +1 @@ +5d335834cb1c1c20a1f44f964b16cd409aa5d10891d5c6cf26f1f2c26726efcf 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..199ce276 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 testnet 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..5e226944 --- /dev/null +++ b/crates/chainspec/src/xlayer_devnet.rs @@ -0,0 +1,318 @@ +//! XLayer Devnet chain specification + +use crate::XLAYER_DEVNET_HARDFORKS; +use alloy_chains::Chain; +use alloy_primitives::{b256, 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::sync::Arc; + +/// X Layer Devnet genesis hash +/// +/// Computed from the genesis block header. +/// This value is hardcoded to avoid expensive computation on every startup. +pub(crate) const XLAYER_DEVNET_GENESIS_HASH: B256 = + b256!("a8e6bc8a65093614612138cccd568b9dd8a7ab79db9097092e639945a437f0c7"); + +/// X Layer Devnet genesis state root +/// +/// The Merkle Patricia Trie root of all 1,866,483 accounts in the genesis alloc. +/// This value is hardcoded to avoid expensive computation on every startup. +pub(crate) const XLAYER_DEVNET_STATE_ROOT: B256 = + b256!("5d335834cb1c1c20a1f44f964b16cd409aa5d10891d5c6cf26f1f2c26726efcf"); + +/// 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::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!("01000000fa000000060000000000000000").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!("a3a639b09fea244d577c7e7ed7bcc4eb1adb0c5b54441cd29d9949e417dfa355")) + ); + 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"), + } + } +} From 2508a01654e3046eec4c3ac72f004401144e2c05 Mon Sep 17 00:00:00 2001 From: Dumi Loghin Date: Tue, 3 Mar 2026 15:36:16 +0800 Subject: [PATCH 2/6] read genesis hash from file for devnet chainspec --- crates/chainspec/src/xlayer_devnet.rs | 35 +++++++++++++++++---------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/crates/chainspec/src/xlayer_devnet.rs b/crates/chainspec/src/xlayer_devnet.rs index 5e226944..22f496e2 100644 --- a/crates/chainspec/src/xlayer_devnet.rs +++ b/crates/chainspec/src/xlayer_devnet.rs @@ -2,7 +2,8 @@ use crate::XLAYER_DEVNET_HARDFORKS; use alloy_chains::Chain; -use alloy_primitives::{b256, B256, U256}; +use alloy_primitives::{B256, U256}; + use once_cell::sync::Lazy; use reth_chainspec::{BaseFeeParams, BaseFeeParamsKind, ChainSpec, Hardfork}; use reth_ethereum_forks::EthereumHardfork; @@ -14,16 +15,24 @@ use std::sync::Arc; /// X Layer Devnet genesis hash /// /// Computed from the genesis block header. -/// This value is hardcoded to avoid expensive computation on every startup. -pub(crate) const XLAYER_DEVNET_GENESIS_HASH: B256 = - b256!("a8e6bc8a65093614612138cccd568b9dd8a7ab79db9097092e639945a437f0c7"); +/// Read from the resource file to pick up any updates without manual changes. +pub(crate) static XLAYER_DEVNET_GENESIS_HASH: Lazy = Lazy::new(|| { + include_str!("../res/genesis/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. -/// This value is hardcoded to avoid expensive computation on every startup. -pub(crate) const XLAYER_DEVNET_STATE_ROOT: B256 = - b256!("5d335834cb1c1c20a1f44f964b16cd409aa5d10891d5c6cf26f1f2c26726efcf"); +/// Read from the resource file to pick up any updates without manual changes. +pub(crate) static XLAYER_DEVNET_STATE_ROOT: Lazy = Lazy::new(|| { + include_str!("../res/genesis/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; @@ -47,7 +56,7 @@ pub static XLAYER_DEVNET: Lazy> = Lazy::new(|| { // 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; + 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; @@ -55,7 +64,7 @@ pub static XLAYER_DEVNET: Lazy> = Lazy::new(|| { 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); + let genesis_header = SealedHeader::new(genesis_header, *XLAYER_DEVNET_GENESIS_HASH); OpChainSpec { inner: ChainSpec { @@ -81,7 +90,7 @@ pub static XLAYER_DEVNET: Lazy> = Lazy::new(|| { mod tests { use super::*; use alloy_genesis::Genesis; - use alloy_primitives::hex; + use alloy_primitives::{b256, hex}; use reth_ethereum_forks::EthereumHardfork; use reth_optimism_forks::OpHardfork; @@ -99,12 +108,12 @@ mod tests { #[test] fn test_xlayer_devnet_genesis_hash() { - assert_eq!(XLAYER_DEVNET.genesis_hash(), 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); + assert_eq!(XLAYER_DEVNET.genesis_header().state_root, *XLAYER_DEVNET_STATE_ROOT); } #[test] @@ -262,7 +271,7 @@ mod tests { 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); + assert_eq!(header.state_root, *XLAYER_DEVNET_STATE_ROOT); } #[test] From 0fb7cfe720f1e17c10bec52195e654afe7da0dc1 Mon Sep 17 00:00:00 2001 From: Dumitrel Loghin Date: Wed, 4 Mar 2026 11:19:45 +0800 Subject: [PATCH 3/6] generate constant files at init time and remove them from git --- .gitignore | 3 ++ .../genesis/xlayer-devnet-genesis-hash.txt | 1 - .../res/genesis/xlayer-devnet-state-root.txt | 1 - crates/chainspec/src/xlayer_devnet.rs | 36 +++++++++++++++++-- 4 files changed, 37 insertions(+), 4 deletions(-) delete mode 100644 crates/chainspec/res/genesis/xlayer-devnet-genesis-hash.txt delete mode 100644 crates/chainspec/res/genesis/xlayer-devnet-state-root.txt 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-genesis-hash.txt b/crates/chainspec/res/genesis/xlayer-devnet-genesis-hash.txt deleted file mode 100644 index 892483cb..00000000 --- a/crates/chainspec/res/genesis/xlayer-devnet-genesis-hash.txt +++ /dev/null @@ -1 +0,0 @@ -a8e6bc8a65093614612138cccd568b9dd8a7ab79db9097092e639945a437f0c7 diff --git a/crates/chainspec/res/genesis/xlayer-devnet-state-root.txt b/crates/chainspec/res/genesis/xlayer-devnet-state-root.txt deleted file mode 100644 index e1465abf..00000000 --- a/crates/chainspec/res/genesis/xlayer-devnet-state-root.txt +++ /dev/null @@ -1 +0,0 @@ -5d335834cb1c1c20a1f44f964b16cd409aa5d10891d5c6cf26f1f2c26726efcf diff --git a/crates/chainspec/src/xlayer_devnet.rs b/crates/chainspec/src/xlayer_devnet.rs index 22f496e2..aac2053e 100644 --- a/crates/chainspec/src/xlayer_devnet.rs +++ b/crates/chainspec/src/xlayer_devnet.rs @@ -10,14 +10,31 @@ 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(|| { - include_str!("../res/genesis/xlayer-devnet-genesis-hash.txt") + 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") @@ -27,8 +44,23 @@ pub(crate) static XLAYER_DEVNET_GENESIS_HASH: Lazy = Lazy::new(|| { /// /// 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(|| { - include_str!("../res/genesis/xlayer-devnet-state-root.txt") + 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 = "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; + 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") From d3e9357a491b0c428ff064df9130af67d177abc7 Mon Sep 17 00:00:00 2001 From: Dumitrel Loghin Date: Wed, 4 Mar 2026 11:22:26 +0800 Subject: [PATCH 4/6] improve test coverage --- crates/chainspec/src/xlayer_devnet.rs | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/crates/chainspec/src/xlayer_devnet.rs b/crates/chainspec/src/xlayer_devnet.rs index aac2053e..6ba46de8 100644 --- a/crates/chainspec/src/xlayer_devnet.rs +++ b/crates/chainspec/src/xlayer_devnet.rs @@ -356,4 +356,83 @@ mod tests { _ => 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"); + } } From eb2d27e773e33e1a03f47d2ae58fa54284170a2f Mon Sep 17 00:00:00 2001 From: Dumitrel Loghin Date: Thu, 5 Mar 2026 17:01:13 +0800 Subject: [PATCH 5/6] prefix hex strings with 0x and format --- crates/chainspec/src/xlayer_devnet.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/chainspec/src/xlayer_devnet.rs b/crates/chainspec/src/xlayer_devnet.rs index 6ba46de8..90202d24 100644 --- a/crates/chainspec/src/xlayer_devnet.rs +++ b/crates/chainspec/src/xlayer_devnet.rs @@ -51,7 +51,7 @@ pub(crate) static XLAYER_DEVNET_STATE_ROOT: Lazy = Lazy::new(|| { if !state_root_path.exists() { // Create the file with hash of empty hash - let empty_hash = "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; + let empty_hash = "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; if let Some(parent) = state_root_path.parent() { std::fs::create_dir_all(parent).expect("Failed to create genesis directory"); } @@ -241,7 +241,7 @@ mod tests { assert_eq!(genesis.timestamp, 0x699d723d); assert_eq!( genesis.extra_data.as_ref(), - hex!("01000000fa000000060000000000000000").as_ref() + hex!("0x01000000fa000000060000000000000000").as_ref() ); assert_eq!(genesis.gas_limit, 0xbebc200); assert_eq!(genesis.difficulty, U256::ZERO); @@ -250,7 +250,7 @@ mod tests { assert_eq!(genesis.coinbase.to_string(), "0x4200000000000000000000000000000000000011"); assert_eq!( genesis.parent_hash, - Some(b256!("a3a639b09fea244d577c7e7ed7bcc4eb1adb0c5b54441cd29d9949e417dfa355")) + 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)); @@ -381,8 +381,8 @@ mod tests { #[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"); + 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")); } @@ -392,8 +392,8 @@ mod tests { // 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"); + 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; From 744984fbfa2efd478ac8a3cfbc7f73a4e43f45f1 Mon Sep 17 00:00:00 2001 From: Dumitrel Loghin Date: Thu, 5 Mar 2026 17:02:55 +0800 Subject: [PATCH 6/6] fix testnet-devnet name in lib.rs --- crates/chainspec/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/chainspec/src/lib.rs b/crates/chainspec/src/lib.rs index 199ce276..034a724c 100644 --- a/crates/chainspec/src/lib.rs +++ b/crates/chainspec/src/lib.rs @@ -115,7 +115,7 @@ pub static XLAYER_TESTNET_HARDFORKS: Lazy = Lazy::new(|| { ]) }); -/// X Layer testnet list of hardforks. +/// X Layer devnet list of hardforks. /// /// All time-based hardforks are activated at genesis (timestamp 0). pub static XLAYER_DEVNET_HARDFORKS: Lazy = Lazy::new(|| {