Skip to main content

ethrex_common/types/
genesis.rs

1use bytes::Bytes;
2use ethereum_types::{Address, Bloom, H256, U256};
3use ethrex_crypto::{NativeCrypto, keccak::keccak_hash};
4use ethrex_rlp::encode::RLPEncode;
5use ethrex_trie::Trie;
6use rkyv::{Archive, Deserialize as RDeserialize, Serialize as RSerialize};
7use serde::{Deserialize, Serialize};
8use std::{
9    collections::{BTreeMap, HashMap},
10    io::{BufReader, Error},
11    path::Path,
12};
13use tracing::warn;
14
15use super::{
16    AccountState, Block, BlockBody, BlockHeader, BlockNumber, INITIAL_BASE_FEE,
17    compute_receipts_root, compute_transactions_root, compute_withdrawals_root,
18};
19use crate::{
20    constants::{DEFAULT_OMMERS_HASH, DEFAULT_REQUESTS_HASH, EMPTY_BLOCK_ACCESS_LIST_HASH},
21    rkyv_utils,
22};
23
24#[allow(unused)]
25#[derive(Debug, Deserialize, Serialize, Clone, Default)]
26#[serde(rename_all = "camelCase")]
27pub struct Genesis {
28    /// Chain configuration
29    pub config: ChainConfig,
30    /// The initial state of the accounts in the genesis block.
31    /// This is a BTreeMap because: https://github.com/lambdaclass/ethrex/issues/2070
32    pub alloc: BTreeMap<Address, GenesisAccount>,
33    /// Genesis header values
34    pub coinbase: Address,
35    pub difficulty: U256,
36    #[serde(default, with = "crate::serde_utils::bytes")]
37    pub extra_data: Bytes,
38    #[serde(with = "crate::serde_utils::u64::hex_str")]
39    pub gas_limit: u64,
40    #[serde(with = "crate::serde_utils::u64::hex_str")]
41    pub nonce: u64,
42    #[serde(alias = "mixHash", alias = "mixhash")]
43    pub mix_hash: H256,
44    #[serde(deserialize_with = "crate::serde_utils::u64::deser_hex_or_dec_str")]
45    #[serde(serialize_with = "crate::serde_utils::u256::serialize_number")]
46    pub timestamp: u64,
47    #[serde(default, with = "crate::serde_utils::u64::hex_str_opt")]
48    pub base_fee_per_gas: Option<u64>,
49    #[serde(default, with = "crate::serde_utils::u64::hex_str_opt")]
50    pub blob_gas_used: Option<u64>,
51    #[serde(default, with = "crate::serde_utils::u64::hex_str_opt")]
52    pub excess_blob_gas: Option<u64>,
53    pub requests_hash: Option<H256>,
54    // Amsterdam fork fields (EIP-7928)
55    pub block_access_list_hash: Option<H256>,
56    #[serde(default, with = "crate::serde_utils::u64::hex_str_opt")]
57    pub slot_number: Option<u64>,
58}
59
60#[derive(Debug, thiserror::Error)]
61pub enum GenesisError {
62    #[error("Failed to decode genesis file: {0}")]
63    Decode(#[from] serde_json::Error),
64    #[error("Fork not supported. Only post-merge networks are supported.")]
65    InvalidFork(),
66    #[error("Failed to open genesis file: {0}")]
67    File(#[from] Error),
68}
69
70impl TryFrom<&Path> for Genesis {
71    type Error = GenesisError;
72
73    fn try_from(genesis_file_path: &Path) -> Result<Self, Self::Error> {
74        let genesis_file = std::fs::File::open(genesis_file_path)?;
75        let genesis_reader = BufReader::new(genesis_file);
76        let genesis: Genesis = serde_json::from_reader(genesis_reader)?;
77
78        // ethrex only supports post-merge (PoS) networks. PoW execution is not planned.
79        if is_unsupported_pow_genesis(&genesis) {
80            warn!("Genesis has no merge configuration; ethrex only supports post-merge networks.");
81        }
82
83        if genesis.config.bpo3_time.is_some() && genesis.config.blob_schedule.bpo3.is_none()
84            || genesis.config.bpo4_time.is_some() && genesis.config.blob_schedule.bpo4.is_none()
85            || genesis.config.bpo5_time.is_some() && genesis.config.blob_schedule.bpo5.is_none()
86        {
87            warn!("BPO time set but no BPO BlobSchedule found in ChainConfig")
88        }
89
90        Ok(genesis)
91    }
92}
93
94/// Returns true for a genesis that describes a pre-merge PoW chain with no
95/// merge configured. A real post-merge genesis either configures the merge
96/// (terminal total difficulty or merge-netsplit block) or starts post-merge
97/// (genesis difficulty 0). The previous heuristic warned whenever the
98/// post-merge forks were scheduled at non-zero timestamps, which false-fired
99/// on every mainnet-style genesis.
100///
101/// Note: `terminal_total_difficulty = Some(0)` is the sentinel for "PoS active
102/// from genesis" and counts as merge-configured here, as does the
103/// `terminal_total_difficulty_passed` flag (the post-merge signal used by the
104/// sync manager).
105fn is_unsupported_pow_genesis(genesis: &Genesis) -> bool {
106    let merge_configured = genesis.config.terminal_total_difficulty.is_some()
107        || genesis.config.merge_netsplit_block.is_some()
108        || genesis.config.terminal_total_difficulty_passed;
109    let post_merge_at_genesis = genesis.difficulty.is_zero();
110    !merge_configured && !post_merge_at_genesis
111}
112
113#[allow(unused)]
114#[derive(
115    Clone,
116    Copy,
117    Debug,
118    Serialize,
119    Deserialize,
120    PartialEq,
121    RSerialize,
122    RDeserialize,
123    Archive,
124    Default,
125)]
126#[serde(rename_all = "camelCase")]
127pub struct ForkBlobSchedule {
128    pub base_fee_update_fraction: u64,
129    pub max: u32,
130    pub target: u32,
131}
132
133#[allow(unused)]
134#[derive(
135    Clone, Copy, Debug, Serialize, Deserialize, PartialEq, RSerialize, RDeserialize, Archive,
136)]
137#[serde(rename_all = "camelCase")]
138pub struct BlobSchedule {
139    #[serde(default = "default_cancun_schedule")]
140    pub cancun: ForkBlobSchedule,
141    #[serde(default = "default_prague_schedule")]
142    pub prague: ForkBlobSchedule,
143    #[serde(default = "default_osaka_schedule")]
144    pub osaka: ForkBlobSchedule,
145    #[serde(default = "default_bpo1_schedule")]
146    pub bpo1: ForkBlobSchedule,
147    #[serde(default = "default_bpo2_schedule")]
148    pub bpo2: ForkBlobSchedule,
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub bpo3: Option<ForkBlobSchedule>,
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub bpo4: Option<ForkBlobSchedule>,
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub bpo5: Option<ForkBlobSchedule>,
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub amsterdam: Option<ForkBlobSchedule>,
157}
158
159impl Default for BlobSchedule {
160    fn default() -> Self {
161        BlobSchedule {
162            cancun: default_cancun_schedule(),
163            prague: default_prague_schedule(),
164            osaka: default_osaka_schedule(),
165            bpo1: default_bpo1_schedule(),
166            bpo2: default_bpo2_schedule(),
167            bpo3: None,
168            bpo4: None,
169            bpo5: None,
170            amsterdam: None,
171        }
172    }
173}
174
175fn default_cancun_schedule() -> ForkBlobSchedule {
176    ForkBlobSchedule {
177        target: 3,
178        max: 6,
179        base_fee_update_fraction: 3338477,
180    }
181}
182
183fn default_prague_schedule() -> ForkBlobSchedule {
184    ForkBlobSchedule {
185        target: 6,
186        max: 9,
187        base_fee_update_fraction: 5007716,
188    }
189}
190
191fn default_osaka_schedule() -> ForkBlobSchedule {
192    ForkBlobSchedule {
193        target: 6,
194        max: 9,
195        base_fee_update_fraction: 5007716,
196    }
197}
198
199fn default_bpo1_schedule() -> ForkBlobSchedule {
200    ForkBlobSchedule {
201        target: 10,
202        max: 15,
203        base_fee_update_fraction: 8346193,
204    }
205}
206
207fn default_bpo2_schedule() -> ForkBlobSchedule {
208    ForkBlobSchedule {
209        target: 14,
210        max: 21,
211        base_fee_update_fraction: 11684671,
212    }
213}
214/// Blockchain settings defined per block
215#[allow(unused)]
216#[derive(
217    Clone,
218    Copy,
219    Debug,
220    Serialize,
221    Deserialize,
222    Default,
223    PartialEq,
224    RSerialize,
225    RDeserialize,
226    Archive,
227)]
228#[serde(rename_all = "camelCase")]
229pub struct ChainConfig {
230    /// Current chain identifier
231    pub chain_id: u64,
232
233    /// Block numbers for the block where each fork was activated
234    /// (None = no fork, 0 = fork is already active)
235    pub homestead_block: Option<u64>,
236
237    pub dao_fork_block: Option<u64>,
238    /// Whether the node supports or opposes the DAO hard-fork
239    #[serde(default)]
240    pub dao_fork_support: bool,
241
242    pub eip150_block: Option<u64>,
243    pub eip155_block: Option<u64>,
244    pub eip158_block: Option<u64>,
245
246    pub byzantium_block: Option<u64>,
247    pub constantinople_block: Option<u64>,
248    pub petersburg_block: Option<u64>,
249    pub istanbul_block: Option<u64>,
250    pub muir_glacier_block: Option<u64>,
251    pub berlin_block: Option<u64>,
252    pub london_block: Option<u64>,
253    pub arrow_glacier_block: Option<u64>,
254    pub gray_glacier_block: Option<u64>,
255    pub merge_netsplit_block: Option<u64>,
256
257    /// Timestamp at which each fork was activated
258    /// (None = no fork, 0 = fork is already active)
259    pub shanghai_time: Option<u64>,
260    pub cancun_time: Option<u64>,
261    pub prague_time: Option<u64>,
262    pub verkle_time: Option<u64>,
263    pub osaka_time: Option<u64>,
264
265    pub bpo1_time: Option<u64>,
266    pub bpo2_time: Option<u64>,
267    pub bpo3_time: Option<u64>,
268    pub bpo4_time: Option<u64>,
269    pub bpo5_time: Option<u64>,
270    pub amsterdam_time: Option<u64>,
271    /// `hezeTime`/`heze_time`, and `bogotaTime`/`bogota_time` in genesis JSON
272    /// to track upstream rename of the post-Glamsterdam fork
273    /// (Bogotá → Hegotá → Hezé). The ethereum-genesis-generator (6.0.5)
274    /// currently emits `bogotaTime` regardless of which name kurtosis args
275    /// use. Internal name stays `hegota_time` until upstream picks a final
276    /// name; this is purely a parsing alias.
277    #[serde(
278        alias = "hezeTime",
279        alias = "heze_time",
280        alias = "bogotaTime",
281        alias = "bogota_time"
282    )]
283    pub hegota_time: Option<u64>,
284
285    /// Amount of total difficulty reached by the network that triggers the consensus upgrade.
286    #[serde(default, with = "crate::serde_utils::u128::hex_str_opt")]
287    pub terminal_total_difficulty: Option<u128>,
288    /// Network has already passed the terminal total difficult
289    #[serde(default)]
290    pub terminal_total_difficulty_passed: bool,
291    #[serde(default)]
292    pub blob_schedule: BlobSchedule,
293    #[rkyv(with = rkyv_utils::H160Wrapper)]
294    // Deposits system contract address
295    pub deposit_contract_address: Address,
296
297    #[serde(default)]
298    pub enable_verkle_at_genesis: bool,
299}
300
301lazy_static::lazy_static! {
302    pub static ref NETWORK_NAMES: HashMap<u64, &'static str> = {
303        HashMap::from([
304            (1, "mainnet"),
305            (11155111, "sepolia"),
306            (560048, "hoodi"),
307            (9, "L1 local devnet"),
308            (65536999, "L2 local devnet"),
309        ])
310    };
311}
312
313#[repr(u8)]
314#[derive(Debug, PartialEq, Eq, PartialOrd, Default, Hash, Clone, Copy, Serialize, Deserialize)]
315pub enum Fork {
316    Frontier = 0,
317    FrontierThawing = 1,
318    Homestead = 2,
319    DaoFork = 3,
320    Tangerine = 4,
321    SpuriousDragon = 5,
322    Byzantium = 6,
323    Constantinople = 7,
324    Petersburg = 8,
325    Istanbul = 9,
326    MuirGlacier = 10,
327    Berlin = 11,
328    London = 12,
329    ArrowGlacier = 13,
330    GrayGlacier = 14,
331    Paris = 15,
332    Shanghai = 16,
333    #[default]
334    Cancun = 17,
335    Prague = 18,
336    Osaka = 19,
337    BPO1 = 20,
338    BPO2 = 21,
339    BPO3 = 22,
340    BPO4 = 23,
341    BPO5 = 24,
342    Amsterdam = 25,
343    Hegota = 26,
344}
345
346impl From<Fork> for &str {
347    fn from(fork: Fork) -> Self {
348        match fork {
349            Fork::Frontier => "Frontier",
350            Fork::FrontierThawing => "FrontierThawing",
351            Fork::Homestead => "Homestead",
352            Fork::DaoFork => "DaoFork",
353            Fork::Tangerine => "Tangerine",
354            Fork::SpuriousDragon => "SpuriousDragon",
355            Fork::Byzantium => "Byzantium",
356            Fork::Constantinople => "Constantinople",
357            Fork::Petersburg => "Petersburg",
358            Fork::Istanbul => "Istanbul",
359            Fork::MuirGlacier => "MuirGlacier",
360            Fork::Berlin => "Berlin",
361            Fork::London => "London",
362            Fork::ArrowGlacier => "ArrowGlacier",
363            Fork::GrayGlacier => "GrayGlacier",
364            Fork::Paris => "Paris",
365            Fork::Shanghai => "Shanghai",
366            Fork::Cancun => "Cancun",
367            Fork::Prague => "Prague",
368            Fork::Osaka => "Osaka",
369            Fork::BPO1 => "BPO1",
370            Fork::BPO2 => "BPO2",
371            Fork::BPO3 => "BPO3",
372            Fork::BPO4 => "BPO4",
373            Fork::BPO5 => "BPO5",
374            Fork::Amsterdam => "Amsterdam",
375            Fork::Hegota => "Hegota",
376        }
377    }
378}
379
380impl ChainConfig {
381    pub fn is_hegota_activated(&self, block_timestamp: u64) -> bool {
382        self.hegota_time.is_some_and(|time| time <= block_timestamp)
383    }
384
385    pub fn is_amsterdam_activated(&self, block_timestamp: u64) -> bool {
386        self.amsterdam_time
387            .is_some_and(|time| time <= block_timestamp)
388    }
389
390    pub fn is_bpo5_activated(&self, block_timestamp: u64) -> bool {
391        self.bpo5_time.is_some_and(|time| time <= block_timestamp)
392    }
393
394    pub fn is_bpo4_activated(&self, block_timestamp: u64) -> bool {
395        self.bpo4_time.is_some_and(|time| time <= block_timestamp)
396    }
397
398    pub fn is_bpo3_activated(&self, block_timestamp: u64) -> bool {
399        self.bpo3_time.is_some_and(|time| time <= block_timestamp)
400    }
401
402    pub fn is_bpo2_activated(&self, block_timestamp: u64) -> bool {
403        self.bpo2_time.is_some_and(|time| time <= block_timestamp)
404    }
405
406    pub fn is_bpo1_activated(&self, block_timestamp: u64) -> bool {
407        self.bpo1_time.is_some_and(|time| time <= block_timestamp)
408    }
409
410    pub fn is_osaka_activated(&self, block_timestamp: u64) -> bool {
411        self.osaka_time.is_some_and(|time| time <= block_timestamp)
412    }
413
414    pub fn is_prague_activated(&self, block_timestamp: u64) -> bool {
415        self.prague_time.is_some_and(|time| time <= block_timestamp)
416    }
417
418    pub fn is_shanghai_activated(&self, block_timestamp: u64) -> bool {
419        self.shanghai_time
420            .is_some_and(|time| time <= block_timestamp)
421    }
422
423    pub fn is_cancun_activated(&self, block_timestamp: u64) -> bool {
424        self.cancun_time.is_some_and(|time| time <= block_timestamp)
425    }
426
427    pub fn is_istanbul_activated(&self, block_number: BlockNumber) -> bool {
428        self.istanbul_block.is_some_and(|num| num <= block_number)
429    }
430
431    pub fn is_london_activated(&self, block_number: BlockNumber) -> bool {
432        self.london_block.is_some_and(|num| num <= block_number)
433    }
434
435    pub fn is_eip155_activated(&self, block_number: BlockNumber) -> bool {
436        self.eip155_block.is_some_and(|num| num <= block_number)
437    }
438
439    pub fn display_config(&self) -> String {
440        let network = NETWORK_NAMES.get(&self.chain_id).unwrap_or(&"unknown");
441        let mut output = format!("Chain ID: {} ({})\n\n", self.chain_id, network);
442
443        let post_merge_forks = [
444            ("Shanghai", self.shanghai_time),
445            ("Cancun", self.cancun_time),
446            ("Prague", self.prague_time),
447            ("Verkle", self.verkle_time),
448            ("Osaka", self.osaka_time),
449            ("Amsterdam", self.amsterdam_time),
450            ("Hegota", self.hegota_time),
451        ];
452
453        let active_forks: Vec<_> = post_merge_forks
454            .iter()
455            .filter_map(|(name, t)| t.map(|time| format!("- {}: @{:<10}", name, time)))
456            .collect();
457
458        if !active_forks.is_empty() {
459            output.push_str("Network is post-merge\n\n");
460            output.push_str("Post-Merge hard forks (timestamp based):\n");
461            output.push_str(&active_forks.join("\n"));
462        } else {
463            output.push_str("Network is at Paris\n\n");
464        }
465
466        output
467    }
468
469    pub fn get_fork(&self, block_timestamp: u64) -> Fork {
470        if self.is_hegota_activated(block_timestamp) {
471            Fork::Hegota
472        } else if self.is_amsterdam_activated(block_timestamp) {
473            Fork::Amsterdam
474        } else if self.is_bpo5_activated(block_timestamp) {
475            Fork::BPO5
476        } else if self.is_bpo4_activated(block_timestamp) {
477            Fork::BPO4
478        } else if self.is_bpo3_activated(block_timestamp) {
479            Fork::BPO3
480        } else if self.is_bpo2_activated(block_timestamp) {
481            Fork::BPO2
482        } else if self.is_bpo1_activated(block_timestamp) {
483            Fork::BPO1
484        } else if self.is_osaka_activated(block_timestamp) {
485            Fork::Osaka
486        } else if self.is_prague_activated(block_timestamp) {
487            Fork::Prague
488        } else if self.is_cancun_activated(block_timestamp) {
489            Fork::Cancun
490        } else if self.is_shanghai_activated(block_timestamp) {
491            Fork::Shanghai
492        } else {
493            Fork::Paris
494        }
495    }
496
497    pub fn get_fork_blob_schedule(&self, block_timestamp: u64) -> Option<ForkBlobSchedule> {
498        // Hegotá inherits Amsterdam's blob schedule unless an explicit Hegotá
499        // entry is added to BlobSchedule in a future change.
500        if self.is_hegota_activated(block_timestamp)
501            && let Some(schedule) = self.blob_schedule.amsterdam
502        {
503            return Some(schedule);
504        }
505        // Amsterdam (and BPO3-5) don't independently define blob params in Hive;
506        // they inherit from the highest activated BPO fork. If the fork-specific
507        // entry is None, fall through to find the right BPO schedule.
508        if self.is_amsterdam_activated(block_timestamp)
509            && let Some(schedule) = self.blob_schedule.amsterdam
510        {
511            return Some(schedule);
512        }
513        // Fall through to BPO chain
514        if self.is_bpo5_activated(block_timestamp)
515            && let Some(schedule) = self.blob_schedule.bpo5
516        {
517            return Some(schedule);
518        }
519        if self.is_bpo4_activated(block_timestamp)
520            && let Some(schedule) = self.blob_schedule.bpo4
521        {
522            return Some(schedule);
523        }
524        if self.is_bpo3_activated(block_timestamp)
525            && let Some(schedule) = self.blob_schedule.bpo3
526        {
527            return Some(schedule);
528        }
529        // Amsterdam implies BPO2 blob params when no explicit schedule is set.
530        if self.is_bpo2_activated(block_timestamp) || self.is_amsterdam_activated(block_timestamp) {
531            Some(self.blob_schedule.bpo2)
532        } else if self.is_bpo1_activated(block_timestamp) {
533            Some(self.blob_schedule.bpo1)
534        } else if self.is_osaka_activated(block_timestamp) {
535            Some(self.blob_schedule.osaka)
536        } else if self.is_prague_activated(block_timestamp) {
537            Some(self.blob_schedule.prague)
538        } else if self.is_cancun_activated(block_timestamp) {
539            Some(self.blob_schedule.cancun)
540        } else {
541            None
542        }
543    }
544
545    pub fn fork(&self, block_timestamp: u64) -> Fork {
546        self.get_fork(block_timestamp)
547    }
548
549    pub fn next_fork(&self, block_timestamp: u64) -> Option<Fork> {
550        // Pick the scheduled fork with the smallest activation timestamp strictly
551        // greater than `block_timestamp`. Iterating all timestamp-based forks avoids
552        // bugs when intermediate forks (e.g. BPOs) are skipped in a network's schedule.
553        //
554        // NOTE: every timestamp-based fork MUST appear here in chronological order.
555        // Omitting a fork will silently cause `next_fork` to skip it; ties are
556        // broken by array position, so the order also encodes the canonical
557        // schedule independent of the `Fork` enum's discriminants.
558        [
559            Fork::Shanghai,
560            Fork::Cancun,
561            Fork::Prague,
562            Fork::Osaka,
563            Fork::BPO1,
564            Fork::BPO2,
565            Fork::BPO3,
566            Fork::BPO4,
567            Fork::BPO5,
568            Fork::Amsterdam,
569            Fork::Hegota,
570        ]
571        .into_iter()
572        .enumerate()
573        .filter_map(|(pos, fork)| {
574            self.get_activation_timestamp_for_fork(fork)
575                .filter(|&t| t > block_timestamp)
576                .map(|t| (fork, t, pos))
577        })
578        .min_by(|a, b| a.1.cmp(&b.1).then_with(|| a.2.cmp(&b.2)))
579        .map(|(fork, _, _)| fork)
580    }
581
582    pub fn get_last_scheduled_fork(&self) -> Fork {
583        if self.hegota_time.is_some() {
584            Fork::Hegota
585        } else if self.amsterdam_time.is_some() {
586            Fork::Amsterdam
587        } else if self.bpo5_time.is_some() {
588            Fork::BPO5
589        } else if self.bpo4_time.is_some() {
590            Fork::BPO4
591        } else if self.bpo3_time.is_some() {
592            Fork::BPO3
593        } else if self.bpo2_time.is_some() {
594            Fork::BPO2
595        } else if self.bpo1_time.is_some() {
596            Fork::BPO1
597        } else if self.osaka_time.is_some() {
598            Fork::Osaka
599        } else if self.prague_time.is_some() {
600            Fork::Prague
601        } else if self.cancun_time.is_some() {
602            Fork::Cancun
603        } else {
604            Fork::Paris
605        }
606    }
607
608    pub fn get_activation_timestamp_for_fork(&self, fork: Fork) -> Option<u64> {
609        match fork {
610            Fork::Cancun => self.cancun_time,
611            Fork::Prague => self.prague_time,
612            Fork::Osaka => self.osaka_time,
613            Fork::BPO1 => self.bpo1_time,
614            Fork::BPO2 => self.bpo2_time,
615            Fork::BPO3 => self.bpo3_time,
616            Fork::BPO4 => self.bpo4_time,
617            Fork::BPO5 => self.bpo5_time,
618            Fork::Amsterdam => self.amsterdam_time,
619            Fork::Hegota => self.hegota_time,
620            Fork::Homestead => self.homestead_block,
621            Fork::DaoFork => self.dao_fork_block,
622            Fork::Byzantium => self.byzantium_block,
623            Fork::Constantinople => self.constantinople_block,
624            Fork::Petersburg => self.petersburg_block,
625            Fork::Istanbul => self.istanbul_block,
626            Fork::MuirGlacier => self.muir_glacier_block,
627            Fork::Berlin => self.berlin_block,
628            Fork::London => self.london_block,
629            Fork::ArrowGlacier => self.arrow_glacier_block,
630            Fork::GrayGlacier => self.gray_glacier_block,
631            Fork::Paris => self.merge_netsplit_block,
632            Fork::Shanghai => self.shanghai_time,
633            _ => None,
634        }
635    }
636
637    pub fn get_blob_schedule_for_fork(&self, fork: Fork) -> Option<ForkBlobSchedule> {
638        match fork {
639            Fork::Cancun => Some(self.blob_schedule.cancun),
640            Fork::Prague => Some(self.blob_schedule.prague),
641            Fork::Osaka => Some(self.blob_schedule.osaka),
642            Fork::BPO1 => Some(self.blob_schedule.bpo1),
643            Fork::BPO2 => Some(self.blob_schedule.bpo2),
644            Fork::BPO3 => self.blob_schedule.bpo3,
645            Fork::BPO4 => self.blob_schedule.bpo4,
646            Fork::BPO5 => self.blob_schedule.bpo5,
647            Fork::Amsterdam => self.blob_schedule.amsterdam,
648            // Hegotá inherits Amsterdam's blob schedule unless an explicit
649            // Hegotá entry is added to BlobSchedule in a future change.
650            Fork::Hegota => self.blob_schedule.amsterdam,
651            _ => None,
652        }
653    }
654
655    pub fn gather_forks(&self, genesis_header: BlockHeader) -> (Vec<u64>, Vec<u64>) {
656        let mut block_number_based_forks: Vec<u64> = vec![
657            self.homestead_block,
658            if self.dao_fork_support {
659                self.dao_fork_block
660            } else {
661                None
662            },
663            self.eip150_block,
664            self.eip155_block,
665            self.eip158_block,
666            self.byzantium_block,
667            self.constantinople_block,
668            self.petersburg_block,
669            self.istanbul_block,
670            self.muir_glacier_block,
671            self.berlin_block,
672            self.london_block,
673            self.arrow_glacier_block,
674            self.gray_glacier_block,
675            self.merge_netsplit_block,
676        ]
677        .into_iter()
678        .flatten()
679        .collect();
680
681        // Remove repeated values
682        block_number_based_forks.sort();
683        block_number_based_forks.dedup();
684
685        let mut timestamp_based_forks: Vec<u64> = vec![
686            self.shanghai_time,
687            self.cancun_time,
688            self.prague_time,
689            self.osaka_time,
690            self.bpo1_time,
691            self.bpo2_time,
692            self.bpo3_time,
693            self.bpo4_time,
694            self.bpo5_time,
695            self.amsterdam_time,
696            self.hegota_time,
697            self.verkle_time,
698        ]
699        .into_iter()
700        .flatten()
701        .collect();
702
703        // Remove repeated values
704        timestamp_based_forks.sort();
705        timestamp_based_forks.dedup();
706
707        // Filter forks before genesis
708        block_number_based_forks.retain(|block_number| *block_number != 0);
709        timestamp_based_forks.retain(|block_timestamp| *block_timestamp > genesis_header.timestamp);
710
711        (block_number_based_forks, timestamp_based_forks)
712    }
713}
714
715#[allow(unused)]
716#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
717pub struct GenesisAccount {
718    #[serde(default, with = "crate::serde_utils::bytes")]
719    pub code: Bytes,
720    #[serde(default)]
721    pub storage: BTreeMap<U256, U256>,
722    #[serde(deserialize_with = "crate::serde_utils::u256::deser_hex_or_dec_str")]
723    pub balance: U256,
724    #[serde(default, with = "crate::serde_utils::u64::hex_str")]
725    pub nonce: u64,
726}
727
728impl Genesis {
729    pub fn get_block(&self) -> Block {
730        Block::new(self.get_block_header(), self.get_block_body())
731    }
732
733    fn get_block_header(&self) -> BlockHeader {
734        let mut blob_gas_used: Option<u64> = None;
735        let mut excess_blob_gas: Option<u64> = None;
736
737        if let Some(cancun_time) = self.config.cancun_time
738            && cancun_time <= self.timestamp
739        {
740            blob_gas_used = Some(self.blob_gas_used.unwrap_or(0));
741            excess_blob_gas = Some(self.excess_blob_gas.unwrap_or(0));
742        }
743        let base_fee_per_gas = self.base_fee_per_gas.or_else(|| {
744            self.config
745                .is_london_activated(0)
746                .then_some(INITIAL_BASE_FEE)
747        });
748
749        let withdrawals_root = self
750            .config
751            .is_shanghai_activated(self.timestamp)
752            .then_some(compute_withdrawals_root(&[], &NativeCrypto));
753
754        let parent_beacon_block_root = self
755            .config
756            .is_cancun_activated(self.timestamp)
757            .then_some(H256::zero());
758
759        let requests_hash = self
760            .config
761            .is_prague_activated(self.timestamp)
762            .then_some(self.requests_hash.unwrap_or(*DEFAULT_REQUESTS_HASH));
763
764        let block_access_list_hash = self
765            .config
766            .is_amsterdam_activated(self.timestamp)
767            .then_some(
768                self.block_access_list_hash
769                    .unwrap_or(*EMPTY_BLOCK_ACCESS_LIST_HASH),
770            );
771
772        let slot_number = self
773            .config
774            .is_amsterdam_activated(self.timestamp)
775            .then_some(self.slot_number.unwrap_or(0));
776
777        BlockHeader {
778            parent_hash: H256::zero(),
779            ommers_hash: *DEFAULT_OMMERS_HASH,
780            coinbase: self.coinbase,
781            state_root: self.compute_state_root(),
782            transactions_root: compute_transactions_root(&[], &NativeCrypto),
783            receipts_root: compute_receipts_root(&[], &NativeCrypto),
784            logs_bloom: Bloom::zero(),
785            difficulty: self.difficulty,
786            number: 0,
787            gas_limit: self.gas_limit,
788            gas_used: 0,
789            timestamp: self.timestamp,
790            extra_data: self.extra_data.clone(),
791            prev_randao: self.mix_hash,
792            nonce: self.nonce,
793            base_fee_per_gas,
794            withdrawals_root,
795            blob_gas_used,
796            excess_blob_gas,
797            parent_beacon_block_root,
798            requests_hash,
799            block_access_list_hash,
800            slot_number,
801            ..Default::default()
802        }
803    }
804
805    fn get_block_body(&self) -> BlockBody {
806        BlockBody {
807            transactions: vec![],
808            ommers: vec![],
809            withdrawals: Some(vec![]),
810        }
811    }
812
813    pub fn compute_state_root(&self) -> H256 {
814        let iter = self.alloc.iter().map(|(addr, account)| {
815            (
816                keccak_hash(addr).to_vec(),
817                AccountState::from(account).encode_to_vec(),
818            )
819        });
820        Trie::compute_hash_from_unsorted_iter(iter, &NativeCrypto)
821    }
822}
823#[cfg(test)]
824mod tests {
825    use std::str::FromStr;
826    use std::{fs::File, io::BufReader};
827
828    use ethereum_types::H160;
829
830    use crate::types::INITIAL_BASE_FEE;
831
832    use super::*;
833
834    #[test]
835    fn terminal_total_difficulty_accepts_number_or_hex_string() {
836        // geth/reth-style genesis files encode terminalTotalDifficulty as a
837        // bare decimal number that exceeds u64::MAX; ethrex must accept it as
838        // well as the 0x-hex-string form.
839        let dca = r#""depositContractAddress":"0x00000000219ab540356cbb839cbe05303d7705fa""#;
840
841        let from_number: ChainConfig = serde_json::from_str(&format!(
842            r#"{{"chainId":1,"terminalTotalDifficulty":58750000000000000000000,{dca}}}"#
843        ))
844        .expect("number-encoded TTD should parse");
845        // f64 cast is lossy above u64::MAX; assert the known, stable
846        // approximation so a regression to Some(0) can't silently pass.
847        assert_eq!(
848            from_number.terminal_total_difficulty,
849            Some(58749999999999996329984u128)
850        );
851
852        let from_hex: ChainConfig = serde_json::from_str(&format!(
853            r#"{{"chainId":1,"terminalTotalDifficulty":"0xc70d808a128d7380000",{dca}}}"#
854        ))
855        .expect("hex-string TTD should parse");
856        assert_eq!(
857            from_hex.terminal_total_difficulty,
858            Some(58750000000000000000000u128)
859        );
860
861        let small: ChainConfig = serde_json::from_str(&format!(
862            r#"{{"chainId":1,"terminalTotalDifficulty":17000000000000000,{dca}}}"#
863        ))
864        .expect("small number TTD should parse");
865        assert_eq!(small.terminal_total_difficulty, Some(17000000000000000u128));
866
867        // A negative bare number must error, not silently saturate to Some(0).
868        let negative = serde_json::from_str::<ChainConfig>(&format!(
869            r#"{{"chainId":1,"terminalTotalDifficulty":-1,{dca}}}"#
870        ));
871        let err = negative.expect_err("negative TTD must be rejected");
872        assert!(
873            err.to_string().contains("finite, non-negative"),
874            "error should name the sign/finiteness cause, got: {err}"
875        );
876    }
877
878    #[test]
879    fn pow_genesis_detection() {
880        // Default genesis: difficulty 0 (post-merge at genesis) -> supported.
881        let mut g = Genesis::default();
882        assert!(!is_unsupported_pow_genesis(&g));
883
884        // PoW genesis: non-zero difficulty, no merge configured -> unsupported.
885        g.difficulty = U256::from(0x4000_0000u64);
886        assert!(is_unsupported_pow_genesis(&g));
887
888        // Mainnet-style: non-zero difficulty but TTD configured -> supported.
889        g.config.terminal_total_difficulty = Some(58750000000000000000000);
890        assert!(!is_unsupported_pow_genesis(&g));
891
892        // Merge-netsplit block configured (no TTD) -> supported.
893        g.config.terminal_total_difficulty = None;
894        g.config.merge_netsplit_block = Some(15537394);
895        assert!(!is_unsupported_pow_genesis(&g));
896
897        // TTD = Some(0) sentinel (PoS from genesis), non-zero difficulty -> supported.
898        g.config.merge_netsplit_block = None;
899        g.config.terminal_total_difficulty = Some(0);
900        assert!(!is_unsupported_pow_genesis(&g));
901
902        // terminal_total_difficulty_passed with no TTD value (post-merge snapshot
903        // that dropped the redundant field), non-zero difficulty -> supported.
904        g.config.terminal_total_difficulty = None;
905        g.config.terminal_total_difficulty_passed = true;
906        assert!(!is_unsupported_pow_genesis(&g));
907    }
908
909    #[test]
910    fn deserialize_genesis_file() {
911        // Deserialize genesis file
912        let file = File::open("../../fixtures/genesis/kurtosis.json")
913            .expect("Failed to open genesis file");
914        let reader = BufReader::new(file);
915        let genesis: Genesis =
916            serde_json::from_reader(reader).expect("Failed to deserialize genesis file");
917        // Check Genesis fields
918        // Chain config
919        let expected_chain_config = ChainConfig {
920            chain_id: 3151908_u64,
921            homestead_block: Some(0),
922            eip150_block: Some(0),
923            eip155_block: Some(0),
924            eip158_block: Some(0),
925            byzantium_block: Some(0),
926            constantinople_block: Some(0),
927            petersburg_block: Some(0),
928            istanbul_block: Some(0),
929            berlin_block: Some(0),
930            london_block: Some(0),
931            merge_netsplit_block: Some(0),
932            shanghai_time: Some(0),
933            cancun_time: Some(0),
934            prague_time: Some(1718232101),
935            terminal_total_difficulty: Some(0),
936            terminal_total_difficulty_passed: true,
937            deposit_contract_address: H160::from_str("0x4242424242424242424242424242424242424242")
938                .unwrap(),
939            // Note this BlobSchedule config is not the default
940            blob_schedule: BlobSchedule {
941                cancun: ForkBlobSchedule {
942                    target: 2,
943                    max: 3,
944                    base_fee_update_fraction: 6676954,
945                },
946                prague: ForkBlobSchedule {
947                    target: 3,
948                    max: 4,
949                    base_fee_update_fraction: 13353908,
950                },
951                ..Default::default()
952            },
953            ..Default::default()
954        };
955        assert_eq!(&genesis.config, &expected_chain_config);
956        // Genesis header fields
957        assert_eq!(genesis.coinbase, Address::from([0; 20]));
958        assert_eq!(genesis.difficulty, U256::from(1));
959        assert!(genesis.extra_data.is_empty());
960        assert_eq!(genesis.gas_limit, 0x17d7840);
961        assert_eq!(genesis.nonce, 0x1234);
962        assert_eq!(genesis.mix_hash, H256::from([0; 32]));
963        assert_eq!(genesis.timestamp, 1718040081);
964        // Check alloc field
965        // We will only check a couple of the hashmap's values as it is quite large
966        let addr_a = Address::from_str("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02").unwrap();
967        assert!(genesis.alloc.contains_key(&addr_a));
968        let expected_account_a = GenesisAccount {
969        code: Bytes::from(hex::decode("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500").unwrap()),
970        balance: 0.into(),
971        nonce: 1,
972        storage: Default::default(),
973    };
974        assert_eq!(genesis.alloc[&addr_a], expected_account_a);
975        // Check some storage values from another account
976        let addr_b = Address::from_str("0x4242424242424242424242424242424242424242").unwrap();
977        assert!(genesis.alloc.contains_key(&addr_b));
978        let addr_b_storage = &genesis.alloc[&addr_b].storage;
979        assert_eq!(
980            addr_b_storage.get(
981                &U256::from_str(
982                    "0x0000000000000000000000000000000000000000000000000000000000000022"
983                )
984                .unwrap()
985            ),
986            Some(
987                &U256::from_str(
988                    "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"
989                )
990                .unwrap()
991            )
992        );
993        assert_eq!(
994            addr_b_storage.get(
995                &U256::from_str(
996                    "0x0000000000000000000000000000000000000000000000000000000000000038"
997                )
998                .unwrap()
999            ),
1000            Some(
1001                &U256::from_str(
1002                    "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7"
1003                )
1004                .unwrap()
1005            )
1006        );
1007    }
1008
1009    #[test]
1010    fn genesis_block() {
1011        // Deserialize genesis file
1012        let file = File::open("../../fixtures/genesis/kurtosis.json")
1013            .expect("Failed to open genesis file");
1014        let reader = BufReader::new(file);
1015        let genesis: Genesis =
1016            serde_json::from_reader(reader).expect("Failed to deserialize genesis file");
1017        let genesis_block = genesis.get_block();
1018        let header = genesis_block.header;
1019        let body = genesis_block.body;
1020        assert_eq!(header.parent_hash, H256::from([0; 32]));
1021        assert_eq!(header.ommers_hash, *DEFAULT_OMMERS_HASH);
1022        assert_eq!(header.coinbase, Address::default());
1023        assert_eq!(
1024            header.state_root,
1025            H256::from_str("0x2dab6a1d6d638955507777aecea699e6728825524facbd446bd4e86d44fa5ecd")
1026                .unwrap()
1027        );
1028        assert_eq!(
1029            header.transactions_root,
1030            compute_transactions_root(&[], &NativeCrypto)
1031        );
1032        assert_eq!(
1033            header.receipts_root,
1034            compute_receipts_root(&[], &NativeCrypto)
1035        );
1036        assert_eq!(header.logs_bloom, Bloom::default());
1037        assert_eq!(header.difficulty, U256::from(1));
1038        assert_eq!(header.gas_limit, 25_000_000);
1039        assert_eq!(header.gas_used, 0);
1040        assert_eq!(header.timestamp, 1_718_040_081);
1041        assert_eq!(header.extra_data, Bytes::default());
1042        assert_eq!(header.prev_randao, H256::from([0; 32]));
1043        assert_eq!(header.nonce, 4660);
1044        assert_eq!(
1045            header.base_fee_per_gas.unwrap_or(INITIAL_BASE_FEE),
1046            INITIAL_BASE_FEE
1047        );
1048        assert_eq!(
1049            header.withdrawals_root,
1050            Some(compute_withdrawals_root(&[], &NativeCrypto))
1051        );
1052        assert_eq!(header.blob_gas_used, Some(0));
1053        assert_eq!(header.excess_blob_gas, Some(0));
1054        assert_eq!(header.parent_beacon_block_root, Some(H256::zero()));
1055        assert!(body.transactions.is_empty());
1056        assert!(body.ommers.is_empty());
1057        assert!(body.withdrawals.is_some_and(|w| w.is_empty()));
1058    }
1059
1060    #[test]
1061    // Parses genesis received by kurtosis and checks that the hash matches the next block's parent hash
1062    fn read_and_compute_kurtosis_hash() {
1063        let file = File::open("../../fixtures/genesis/kurtosis.json")
1064            .expect("Failed to open genesis file");
1065        let reader = BufReader::new(file);
1066        let genesis: Genesis =
1067            serde_json::from_reader(reader).expect("Failed to deserialize genesis file");
1068        let genesis_block_hash = genesis.get_block().hash();
1069        assert_eq!(
1070            genesis_block_hash,
1071            H256::from_str("0xcb5306dd861d0f2c1f9952fbfbc75a46d0b6ce4f37bea370c3471fe8410bf40b")
1072                .unwrap()
1073        )
1074    }
1075
1076    #[test]
1077    fn parse_hive_genesis_file() {
1078        let file =
1079            File::open("../../fixtures/genesis/hive.json").expect("Failed to open genesis file");
1080        let reader = BufReader::new(file);
1081        let _genesis: Genesis =
1082            serde_json::from_reader(reader).expect("Failed to deserialize genesis file");
1083    }
1084
1085    #[test]
1086    fn read_and_compute_hive_hash() {
1087        let file =
1088            File::open("../../fixtures/genesis/hive.json").expect("Failed to open genesis file");
1089        let reader = BufReader::new(file);
1090        let genesis: Genesis =
1091            serde_json::from_reader(reader).expect("Failed to deserialize genesis file");
1092        let computed_block_hash = genesis.get_block().hash();
1093        let genesis_block_hash =
1094            H256::from_str("0x30f516e34fc173bb5fc4daddcc7532c4aca10b702c7228f3c806b4df2646fb7e")
1095                .unwrap();
1096        assert_eq!(genesis_block_hash, computed_block_hash)
1097    }
1098
1099    #[test]
1100    fn deserialize_chain_config_blob_schedule() {
1101        let json = r#"
1102
1103            {
1104                "chainId": 123,
1105                "blobSchedule": {
1106                  "cancun": {
1107                    "target": 1,
1108                    "max": 2,
1109                    "baseFeeUpdateFraction": 10000
1110                  },
1111                  "prague": {
1112                    "target": 3,
1113                    "max": 4,
1114                    "baseFeeUpdateFraction": 20000
1115                  }
1116                },
1117                "depositContractAddress": "0x4242424242424242424242424242424242424242"
1118            }
1119            "#;
1120
1121        let config: ChainConfig =
1122            serde_json::from_str(json).expect("Failed to deserialize ChainConfig");
1123        let expected_chain_config = ChainConfig {
1124            chain_id: 123,
1125            blob_schedule: BlobSchedule {
1126                cancun: ForkBlobSchedule {
1127                    target: 1,
1128                    max: 2,
1129                    base_fee_update_fraction: 10000,
1130                },
1131                prague: ForkBlobSchedule {
1132                    target: 3,
1133                    max: 4,
1134                    base_fee_update_fraction: 20000,
1135                },
1136                ..Default::default()
1137            },
1138            deposit_contract_address: H160::from_str("0x4242424242424242424242424242424242424242")
1139                .unwrap(),
1140            ..Default::default()
1141        };
1142        assert_eq!(&config, &expected_chain_config);
1143    }
1144
1145    #[test]
1146    fn deserialize_chain_config_missing_entire_blob_schedule() {
1147        let json = r#"
1148            {
1149                "chainId": 123,
1150                "depositContractAddress": "0x4242424242424242424242424242424242424242"
1151            }
1152            "#;
1153
1154        let config: ChainConfig =
1155            serde_json::from_str(json).expect("Failed to deserialize ChainConfig");
1156        let expected_chain_config = ChainConfig {
1157            chain_id: 123,
1158            blob_schedule: BlobSchedule {
1159                cancun: ForkBlobSchedule {
1160                    target: 3,
1161                    max: 6,
1162                    base_fee_update_fraction: 3338477,
1163                },
1164                prague: ForkBlobSchedule {
1165                    target: 6,
1166                    max: 9,
1167                    base_fee_update_fraction: 5007716,
1168                },
1169                ..Default::default()
1170            },
1171            deposit_contract_address: H160::from_str("0x4242424242424242424242424242424242424242")
1172                .unwrap(),
1173            ..Default::default()
1174        };
1175        assert_eq!(&config, &expected_chain_config);
1176    }
1177
1178    #[test]
1179    fn deserialize_chain_config_missing_cancun_blob_schedule() {
1180        let json = r#"
1181            {
1182                "chainId": 123,
1183                "blobSchedule": {
1184                    "prague": {
1185                      "target": 3,
1186                      "max": 4,
1187                      "baseFeeUpdateFraction": 20000
1188                    }
1189                },
1190                "depositContractAddress": "0x4242424242424242424242424242424242424242"
1191            }
1192            "#;
1193
1194        let config: ChainConfig =
1195            serde_json::from_str(json).expect("Failed to deserialize ChainConfig");
1196        let expected_chain_config = ChainConfig {
1197            chain_id: 123,
1198            blob_schedule: BlobSchedule {
1199                cancun: ForkBlobSchedule {
1200                    target: 3,
1201                    max: 6,
1202                    base_fee_update_fraction: 3338477,
1203                },
1204                prague: ForkBlobSchedule {
1205                    target: 3,
1206                    max: 4,
1207                    base_fee_update_fraction: 20000,
1208                },
1209                ..Default::default()
1210            },
1211            deposit_contract_address: H160::from_str("0x4242424242424242424242424242424242424242")
1212                .unwrap(),
1213            ..Default::default()
1214        };
1215        assert_eq!(&config, &expected_chain_config);
1216    }
1217
1218    #[test]
1219    fn deserialize_chain_config_missing_prague_blob_schedule() {
1220        let json = r#"
1221            {
1222                "chainId": 123,
1223                "blobSchedule": {
1224                  "cancun": {
1225                    "target": 1,
1226                    "max": 2,
1227                    "baseFeeUpdateFraction": 10000
1228                  }
1229                },
1230                "depositContractAddress": "0x4242424242424242424242424242424242424242"
1231            }
1232            "#;
1233
1234        let config: ChainConfig =
1235            serde_json::from_str(json).expect("Failed to deserialize ChainConfig");
1236        let expected_chain_config = ChainConfig {
1237            chain_id: 123,
1238            blob_schedule: BlobSchedule {
1239                cancun: ForkBlobSchedule {
1240                    target: 1,
1241                    max: 2,
1242                    base_fee_update_fraction: 10000,
1243                },
1244                prague: ForkBlobSchedule {
1245                    target: 6,
1246                    max: 9,
1247                    base_fee_update_fraction: 5007716,
1248                },
1249                ..Default::default()
1250            },
1251            deposit_contract_address: H160::from_str("0x4242424242424242424242424242424242424242")
1252                .unwrap(),
1253            ..Default::default()
1254        };
1255        assert_eq!(&config, &expected_chain_config);
1256    }
1257
1258    #[test]
1259    fn deserialize_chain_config_missing_deposit_contract_address() {
1260        let json = r#"
1261            {
1262                "chainId": 123
1263            }
1264            "#;
1265
1266        let result: Result<ChainConfig, _> = serde_json::from_str(json);
1267
1268        assert!(result.is_err());
1269
1270        let error_message = result.unwrap_err().to_string();
1271        assert!(error_message.contains("missing field `depositContractAddress`"),);
1272    }
1273
1274    #[test]
1275    fn test_hegota_after_amsterdam() {
1276        // Discriminant ordering: Hegota strictly follows Amsterdam.
1277        assert!(Fork::Hegota > Fork::Amsterdam);
1278        assert_eq!(Fork::Hegota as u8, 26);
1279        assert_eq!(Fork::Amsterdam as u8, 25);
1280
1281        // String conversion.
1282        let hegota_str: &str = Fork::Hegota.into();
1283        assert_eq!(hegota_str, "Hegota");
1284
1285        // Activation predicate boundary behavior.
1286        let mut config = ChainConfig {
1287            chain_id: 1,
1288            deposit_contract_address: Address::default(),
1289            ..Default::default()
1290        };
1291        config.hegota_time = Some(1000);
1292        assert!(!config.is_hegota_activated(999));
1293        assert!(config.is_hegota_activated(1000));
1294        assert!(config.is_hegota_activated(1001));
1295
1296        // None means inactive.
1297        config.hegota_time = None;
1298        assert!(!config.is_hegota_activated(0));
1299        assert!(!config.is_hegota_activated(u64::MAX));
1300
1301        // get_fork returns Hegota when both Amsterdam and Hegota are active.
1302        let mut config = ChainConfig {
1303            chain_id: 1,
1304            deposit_contract_address: Address::default(),
1305            ..Default::default()
1306        };
1307        config.amsterdam_time = Some(500);
1308        config.hegota_time = Some(1000);
1309        assert_eq!(config.get_fork(499), Fork::Paris);
1310        assert_eq!(config.get_fork(500), Fork::Amsterdam);
1311        assert_eq!(config.get_fork(999), Fork::Amsterdam);
1312        assert_eq!(config.get_fork(1000), Fork::Hegota);
1313        assert_eq!(config.get_fork(2000), Fork::Hegota);
1314
1315        // next_fork transitions correctly.
1316        assert_eq!(config.next_fork(500), Some(Fork::Hegota));
1317        assert_eq!(config.next_fork(1000), None);
1318
1319        // get_last_scheduled_fork picks Hegota when scheduled.
1320        assert_eq!(config.get_last_scheduled_fork(), Fork::Hegota);
1321
1322        // get_activation_timestamp_for_fork returns the right value.
1323        assert_eq!(
1324            config.get_activation_timestamp_for_fork(Fork::Hegota),
1325            Some(1000)
1326        );
1327    }
1328
1329    #[test]
1330    fn chain_config_accepts_heze_and_bogota_aliases_for_hegota_time() {
1331        for key in [
1332            "hegotaTime",
1333            "hezeTime",
1334            "heze_time",
1335            "bogotaTime",
1336            "bogota_time",
1337        ] {
1338            let json = format!(
1339                r#"{{
1340                    "chainId": 1,
1341                    "depositContractAddress": "0x0000000000000000000000000000000000000000",
1342                    "{key}": 1700000000
1343                }}"#
1344            );
1345            let cfg: ChainConfig = serde_json::from_str(&json)
1346                .unwrap_or_else(|e| panic!("alias {key} must deserialize: {e}"));
1347            assert_eq!(cfg.hegota_time, Some(1_700_000_000), "alias {key}");
1348        }
1349    }
1350
1351    #[test]
1352    fn next_fork_skips_unscheduled_intermediate_forks() {
1353        // bal-devnet-7 layout: every post-merge fork up to Osaka at t=0, Amsterdam
1354        // scheduled later, no BPOs scheduled. `next_fork` must return Amsterdam
1355        // even though the BPO chain between Osaka and Amsterdam is empty.
1356        let config = ChainConfig {
1357            shanghai_time: Some(0),
1358            cancun_time: Some(0),
1359            prague_time: Some(0),
1360            osaka_time: Some(0),
1361            amsterdam_time: Some(1_779_098_127),
1362            ..Default::default()
1363        };
1364        assert_eq!(config.next_fork(0), Some(Fork::Amsterdam));
1365        assert_eq!(config.next_fork(1_779_098_126), Some(Fork::Amsterdam));
1366        assert_eq!(config.next_fork(1_779_098_127), None);
1367    }
1368
1369    #[test]
1370    fn next_fork_picks_earliest_scheduled() {
1371        // Contiguous schedule: at Cancun, next should be Prague (not Osaka).
1372        let config = ChainConfig {
1373            shanghai_time: Some(0),
1374            cancun_time: Some(100),
1375            prague_time: Some(200),
1376            osaka_time: Some(300),
1377            ..Default::default()
1378        };
1379        assert_eq!(config.next_fork(150), Some(Fork::Prague));
1380        assert_eq!(config.next_fork(250), Some(Fork::Osaka));
1381        assert_eq!(config.next_fork(300), None);
1382    }
1383
1384    #[test]
1385    fn next_fork_returns_none_when_at_last_scheduled() {
1386        let config = ChainConfig {
1387            shanghai_time: Some(0),
1388            cancun_time: Some(0),
1389            ..Default::default()
1390        };
1391        assert_eq!(config.next_fork(0), None);
1392    }
1393}