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    pub lstar_time: Option<u64>,
285
286    /// Amount of total difficulty reached by the network that triggers the consensus upgrade.
287    #[serde(default, with = "crate::serde_utils::u128::hex_str_opt")]
288    pub terminal_total_difficulty: Option<u128>,
289    /// Network has already passed the terminal total difficult
290    #[serde(default)]
291    pub terminal_total_difficulty_passed: bool,
292    #[serde(default)]
293    pub blob_schedule: BlobSchedule,
294    #[rkyv(with = rkyv_utils::H160Wrapper)]
295    // Deposits system contract address
296    pub deposit_contract_address: Address,
297
298    #[serde(default)]
299    pub enable_verkle_at_genesis: bool,
300}
301
302lazy_static::lazy_static! {
303    pub static ref NETWORK_NAMES: HashMap<u64, &'static str> = {
304        HashMap::from([
305            (1, "mainnet"),
306            (11155111, "sepolia"),
307            (560048, "hoodi"),
308            (7091047534, "plataberget"),
309            (9, "L1 local devnet"),
310            (65536999, "L2 local devnet"),
311        ])
312    };
313}
314
315#[repr(u8)]
316#[derive(Debug, PartialEq, Eq, PartialOrd, Default, Hash, Clone, Copy, Serialize, Deserialize)]
317pub enum Fork {
318    Frontier = 0,
319    FrontierThawing = 1,
320    Homestead = 2,
321    DaoFork = 3,
322    Tangerine = 4,
323    SpuriousDragon = 5,
324    Byzantium = 6,
325    Constantinople = 7,
326    Petersburg = 8,
327    Istanbul = 9,
328    MuirGlacier = 10,
329    Berlin = 11,
330    London = 12,
331    ArrowGlacier = 13,
332    GrayGlacier = 14,
333    Paris = 15,
334    Shanghai = 16,
335    #[default]
336    Cancun = 17,
337    Prague = 18,
338    Osaka = 19,
339    BPO1 = 20,
340    BPO2 = 21,
341    BPO3 = 22,
342    BPO4 = 23,
343    BPO5 = 24,
344    Amsterdam = 25,
345    Hegota = 26,
346    LStar = 27,
347}
348
349impl From<Fork> for &str {
350    fn from(fork: Fork) -> Self {
351        match fork {
352            Fork::Frontier => "Frontier",
353            Fork::FrontierThawing => "FrontierThawing",
354            Fork::Homestead => "Homestead",
355            Fork::DaoFork => "DaoFork",
356            Fork::Tangerine => "Tangerine",
357            Fork::SpuriousDragon => "SpuriousDragon",
358            Fork::Byzantium => "Byzantium",
359            Fork::Constantinople => "Constantinople",
360            Fork::Petersburg => "Petersburg",
361            Fork::Istanbul => "Istanbul",
362            Fork::MuirGlacier => "MuirGlacier",
363            Fork::Berlin => "Berlin",
364            Fork::London => "London",
365            Fork::ArrowGlacier => "ArrowGlacier",
366            Fork::GrayGlacier => "GrayGlacier",
367            Fork::Paris => "Paris",
368            Fork::Shanghai => "Shanghai",
369            Fork::Cancun => "Cancun",
370            Fork::Prague => "Prague",
371            Fork::Osaka => "Osaka",
372            Fork::BPO1 => "BPO1",
373            Fork::BPO2 => "BPO2",
374            Fork::BPO3 => "BPO3",
375            Fork::BPO4 => "BPO4",
376            Fork::BPO5 => "BPO5",
377            Fork::Amsterdam => "Amsterdam",
378            Fork::Hegota => "Hegota",
379            Fork::LStar => "LStar",
380        }
381    }
382}
383
384impl ChainConfig {
385    pub fn is_hegota_activated(&self, block_timestamp: u64) -> bool {
386        self.hegota_time.is_some_and(|time| time <= block_timestamp)
387    }
388
389    pub fn is_lstar_activated(&self, block_timestamp: u64) -> bool {
390        self.lstar_time.is_some_and(|time| time <= block_timestamp)
391    }
392
393    pub fn is_amsterdam_activated(&self, block_timestamp: u64) -> bool {
394        self.amsterdam_time
395            .is_some_and(|time| time <= block_timestamp)
396    }
397
398    pub fn is_bpo5_activated(&self, block_timestamp: u64) -> bool {
399        self.bpo5_time.is_some_and(|time| time <= block_timestamp)
400    }
401
402    pub fn is_bpo4_activated(&self, block_timestamp: u64) -> bool {
403        self.bpo4_time.is_some_and(|time| time <= block_timestamp)
404    }
405
406    pub fn is_bpo3_activated(&self, block_timestamp: u64) -> bool {
407        self.bpo3_time.is_some_and(|time| time <= block_timestamp)
408    }
409
410    pub fn is_bpo2_activated(&self, block_timestamp: u64) -> bool {
411        self.bpo2_time.is_some_and(|time| time <= block_timestamp)
412    }
413
414    pub fn is_bpo1_activated(&self, block_timestamp: u64) -> bool {
415        self.bpo1_time.is_some_and(|time| time <= block_timestamp)
416    }
417
418    pub fn is_osaka_activated(&self, block_timestamp: u64) -> bool {
419        self.osaka_time.is_some_and(|time| time <= block_timestamp)
420    }
421
422    pub fn is_prague_activated(&self, block_timestamp: u64) -> bool {
423        self.prague_time.is_some_and(|time| time <= block_timestamp)
424    }
425
426    pub fn is_shanghai_activated(&self, block_timestamp: u64) -> bool {
427        self.shanghai_time
428            .is_some_and(|time| time <= block_timestamp)
429    }
430
431    pub fn is_cancun_activated(&self, block_timestamp: u64) -> bool {
432        self.cancun_time.is_some_and(|time| time <= block_timestamp)
433    }
434
435    pub fn is_istanbul_activated(&self, block_number: BlockNumber) -> bool {
436        self.istanbul_block.is_some_and(|num| num <= block_number)
437    }
438
439    pub fn is_london_activated(&self, block_number: BlockNumber) -> bool {
440        self.london_block.is_some_and(|num| num <= block_number)
441    }
442
443    pub fn is_eip155_activated(&self, block_number: BlockNumber) -> bool {
444        self.eip155_block.is_some_and(|num| num <= block_number)
445    }
446
447    pub fn display_config(&self) -> String {
448        let network = NETWORK_NAMES.get(&self.chain_id).unwrap_or(&"unknown");
449        let mut output = format!("Chain ID: {} ({})\n\n", self.chain_id, network);
450
451        let post_merge_forks = [
452            ("Shanghai", self.shanghai_time),
453            ("Cancun", self.cancun_time),
454            ("Prague", self.prague_time),
455            ("Verkle", self.verkle_time),
456            ("Osaka", self.osaka_time),
457            ("Amsterdam", self.amsterdam_time),
458            ("Hegota", self.hegota_time),
459        ];
460
461        let active_forks: Vec<_> = post_merge_forks
462            .iter()
463            .filter_map(|(name, t)| t.map(|time| format!("- {}: @{:<10}", name, time)))
464            .collect();
465
466        if !active_forks.is_empty() {
467            output.push_str("Network is post-merge\n\n");
468            output.push_str("Post-Merge hard forks (timestamp based):\n");
469            output.push_str(&active_forks.join("\n"));
470        } else {
471            output.push_str("Network is at Paris\n\n");
472        }
473
474        output
475    }
476
477    pub fn get_fork(&self, block_timestamp: u64) -> Fork {
478        if self.is_lstar_activated(block_timestamp) {
479            Fork::LStar
480        } else if self.is_hegota_activated(block_timestamp) {
481            Fork::Hegota
482        } else if self.is_amsterdam_activated(block_timestamp) {
483            Fork::Amsterdam
484        } else if self.is_bpo5_activated(block_timestamp) {
485            Fork::BPO5
486        } else if self.is_bpo4_activated(block_timestamp) {
487            Fork::BPO4
488        } else if self.is_bpo3_activated(block_timestamp) {
489            Fork::BPO3
490        } else if self.is_bpo2_activated(block_timestamp) {
491            Fork::BPO2
492        } else if self.is_bpo1_activated(block_timestamp) {
493            Fork::BPO1
494        } else if self.is_osaka_activated(block_timestamp) {
495            Fork::Osaka
496        } else if self.is_prague_activated(block_timestamp) {
497            Fork::Prague
498        } else if self.is_cancun_activated(block_timestamp) {
499            Fork::Cancun
500        } else if self.is_shanghai_activated(block_timestamp) {
501            Fork::Shanghai
502        } else {
503            Fork::Paris
504        }
505    }
506
507    pub fn get_fork_blob_schedule(&self, block_timestamp: u64) -> Option<ForkBlobSchedule> {
508        // EIP-7892: from Prague onward the blob schedule only changes at BPO forks.
509        // Named forks (Osaka, Amsterdam, Hegotá, LStar) carry no blob params of their own
510        // and inherit the highest activated BPO entry, so resolution falls through the BPO
511        // chain. A genesis that pins an entry for the named fork anyway takes precedence.
512        if (self.is_lstar_activated(block_timestamp)
513            || self.is_hegota_activated(block_timestamp)
514            || self.is_amsterdam_activated(block_timestamp))
515            && let Some(schedule) = self.blob_schedule.amsterdam
516        {
517            return Some(schedule);
518        }
519        if self.is_bpo5_activated(block_timestamp)
520            && let Some(schedule) = self.blob_schedule.bpo5
521        {
522            return Some(schedule);
523        }
524        if self.is_bpo4_activated(block_timestamp)
525            && let Some(schedule) = self.blob_schedule.bpo4
526        {
527            return Some(schedule);
528        }
529        if self.is_bpo3_activated(block_timestamp)
530            && let Some(schedule) = self.blob_schedule.bpo3
531        {
532            return Some(schedule);
533        }
534        if self.is_bpo2_activated(block_timestamp) {
535            Some(self.blob_schedule.bpo2)
536        } else if self.is_bpo1_activated(block_timestamp) {
537            Some(self.blob_schedule.bpo1)
538        } else if self.is_osaka_activated(block_timestamp) {
539            Some(self.blob_schedule.osaka)
540        } else if self.is_prague_activated(block_timestamp) {
541            Some(self.blob_schedule.prague)
542        } else if self.is_cancun_activated(block_timestamp) {
543            Some(self.blob_schedule.cancun)
544        } else {
545            None
546        }
547    }
548
549    pub fn fork(&self, block_timestamp: u64) -> Fork {
550        self.get_fork(block_timestamp)
551    }
552
553    pub fn next_fork(&self, block_timestamp: u64) -> Option<Fork> {
554        // Pick the scheduled fork with the smallest activation timestamp strictly
555        // greater than `block_timestamp`. Iterating all timestamp-based forks avoids
556        // bugs when intermediate forks (e.g. BPOs) are skipped in a network's schedule.
557        //
558        // NOTE: every timestamp-based fork MUST appear here in chronological order.
559        // Omitting a fork will silently cause `next_fork` to skip it; ties are
560        // broken by array position, so the order also encodes the canonical
561        // schedule independent of the `Fork` enum's discriminants.
562        [
563            Fork::Shanghai,
564            Fork::Cancun,
565            Fork::Prague,
566            Fork::Osaka,
567            Fork::BPO1,
568            Fork::BPO2,
569            Fork::BPO3,
570            Fork::BPO4,
571            Fork::BPO5,
572            Fork::Amsterdam,
573            Fork::Hegota,
574            Fork::LStar,
575        ]
576        .into_iter()
577        .enumerate()
578        .filter_map(|(pos, fork)| {
579            self.get_activation_timestamp_for_fork(fork)
580                .filter(|&t| t > block_timestamp)
581                .map(|t| (fork, t, pos))
582        })
583        .min_by(|a, b| a.1.cmp(&b.1).then_with(|| a.2.cmp(&b.2)))
584        .map(|(fork, _, _)| fork)
585    }
586
587    pub fn get_last_scheduled_fork(&self) -> Fork {
588        if self.lstar_time.is_some() {
589            Fork::LStar
590        } else if self.hegota_time.is_some() {
591            Fork::Hegota
592        } else if self.amsterdam_time.is_some() {
593            Fork::Amsterdam
594        } else if self.bpo5_time.is_some() {
595            Fork::BPO5
596        } else if self.bpo4_time.is_some() {
597            Fork::BPO4
598        } else if self.bpo3_time.is_some() {
599            Fork::BPO3
600        } else if self.bpo2_time.is_some() {
601            Fork::BPO2
602        } else if self.bpo1_time.is_some() {
603            Fork::BPO1
604        } else if self.osaka_time.is_some() {
605            Fork::Osaka
606        } else if self.prague_time.is_some() {
607            Fork::Prague
608        } else if self.cancun_time.is_some() {
609            Fork::Cancun
610        } else {
611            Fork::Paris
612        }
613    }
614
615    pub fn get_activation_timestamp_for_fork(&self, fork: Fork) -> Option<u64> {
616        match fork {
617            Fork::Cancun => self.cancun_time,
618            Fork::Prague => self.prague_time,
619            Fork::Osaka => self.osaka_time,
620            Fork::BPO1 => self.bpo1_time,
621            Fork::BPO2 => self.bpo2_time,
622            Fork::BPO3 => self.bpo3_time,
623            Fork::BPO4 => self.bpo4_time,
624            Fork::BPO5 => self.bpo5_time,
625            Fork::Amsterdam => self.amsterdam_time,
626            Fork::Hegota => self.hegota_time,
627            Fork::LStar => self.lstar_time,
628            Fork::Homestead => self.homestead_block,
629            Fork::DaoFork => self.dao_fork_block,
630            Fork::Byzantium => self.byzantium_block,
631            Fork::Constantinople => self.constantinople_block,
632            Fork::Petersburg => self.petersburg_block,
633            Fork::Istanbul => self.istanbul_block,
634            Fork::MuirGlacier => self.muir_glacier_block,
635            Fork::Berlin => self.berlin_block,
636            Fork::London => self.london_block,
637            Fork::ArrowGlacier => self.arrow_glacier_block,
638            Fork::GrayGlacier => self.gray_glacier_block,
639            Fork::Paris => self.merge_netsplit_block,
640            Fork::Shanghai => self.shanghai_time,
641            _ => None,
642        }
643    }
644
645    pub fn get_blob_schedule_for_fork(&self, fork: Fork) -> Option<ForkBlobSchedule> {
646        // Blob params are timestamp-scheduled from Cancun onward; earlier forks are
647        // activated by block number and have no blob schedule. Resolving through the
648        // fork's activation timestamp keeps this in step with the inheritance rules in
649        // `get_fork_blob_schedule`, so a named fork that declares no entry of its own
650        // reports the entry actually in force at its activation.
651        if fork < Fork::Cancun {
652            return None;
653        }
654        self.get_activation_timestamp_for_fork(fork)
655            .and_then(|timestamp| self.get_fork_blob_schedule(timestamp))
656    }
657
658    pub fn gather_forks(&self, genesis_header: BlockHeader) -> (Vec<u64>, Vec<u64>) {
659        let mut block_number_based_forks: Vec<u64> = vec![
660            self.homestead_block,
661            if self.dao_fork_support {
662                self.dao_fork_block
663            } else {
664                None
665            },
666            self.eip150_block,
667            self.eip155_block,
668            self.eip158_block,
669            self.byzantium_block,
670            self.constantinople_block,
671            self.petersburg_block,
672            self.istanbul_block,
673            self.muir_glacier_block,
674            self.berlin_block,
675            self.london_block,
676            self.arrow_glacier_block,
677            self.gray_glacier_block,
678            self.merge_netsplit_block,
679        ]
680        .into_iter()
681        .flatten()
682        .collect();
683
684        // Remove repeated values
685        block_number_based_forks.sort();
686        block_number_based_forks.dedup();
687
688        let mut timestamp_based_forks: Vec<u64> = vec![
689            self.shanghai_time,
690            self.cancun_time,
691            self.prague_time,
692            self.osaka_time,
693            self.bpo1_time,
694            self.bpo2_time,
695            self.bpo3_time,
696            self.bpo4_time,
697            self.bpo5_time,
698            self.amsterdam_time,
699            self.hegota_time,
700            self.verkle_time,
701        ]
702        .into_iter()
703        .flatten()
704        .collect();
705
706        // Remove repeated values
707        timestamp_based_forks.sort();
708        timestamp_based_forks.dedup();
709
710        // Filter forks before genesis
711        block_number_based_forks.retain(|block_number| *block_number != 0);
712        timestamp_based_forks.retain(|block_timestamp| *block_timestamp > genesis_header.timestamp);
713
714        (block_number_based_forks, timestamp_based_forks)
715    }
716}
717
718#[allow(unused)]
719#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
720pub struct GenesisAccount {
721    #[serde(default, with = "crate::serde_utils::bytes")]
722    pub code: Bytes,
723    #[serde(default)]
724    pub storage: BTreeMap<U256, U256>,
725    #[serde(deserialize_with = "crate::serde_utils::u256::deser_hex_or_dec_str")]
726    pub balance: U256,
727    #[serde(default, with = "crate::serde_utils::u64::hex_str")]
728    pub nonce: u64,
729}
730
731impl Genesis {
732    pub fn get_block(&self) -> Block {
733        Block::new(self.get_block_header(), self.get_block_body())
734    }
735
736    fn get_block_header(&self) -> BlockHeader {
737        let mut blob_gas_used: Option<u64> = None;
738        let mut excess_blob_gas: Option<u64> = None;
739
740        if let Some(cancun_time) = self.config.cancun_time
741            && cancun_time <= self.timestamp
742        {
743            blob_gas_used = Some(self.blob_gas_used.unwrap_or(0));
744            excess_blob_gas = Some(self.excess_blob_gas.unwrap_or(0));
745        }
746        let base_fee_per_gas = self.base_fee_per_gas.or_else(|| {
747            self.config
748                .is_london_activated(0)
749                .then_some(INITIAL_BASE_FEE)
750        });
751
752        let withdrawals_root = self
753            .config
754            .is_shanghai_activated(self.timestamp)
755            .then_some(compute_withdrawals_root(&[], &NativeCrypto));
756
757        let parent_beacon_block_root = self
758            .config
759            .is_cancun_activated(self.timestamp)
760            .then_some(H256::zero());
761
762        let requests_hash = self
763            .config
764            .is_prague_activated(self.timestamp)
765            .then_some(self.requests_hash.unwrap_or(*DEFAULT_REQUESTS_HASH));
766
767        let block_access_list_hash = self
768            .config
769            .is_amsterdam_activated(self.timestamp)
770            .then_some(
771                self.block_access_list_hash
772                    .unwrap_or(*EMPTY_BLOCK_ACCESS_LIST_HASH),
773            );
774
775        let slot_number = self
776            .config
777            .is_amsterdam_activated(self.timestamp)
778            .then_some(self.slot_number.unwrap_or(0));
779
780        BlockHeader {
781            parent_hash: H256::zero(),
782            ommers_hash: *DEFAULT_OMMERS_HASH,
783            coinbase: self.coinbase,
784            state_root: self.compute_state_root(),
785            transactions_root: compute_transactions_root(&[], &NativeCrypto),
786            receipts_root: compute_receipts_root(&[], &NativeCrypto),
787            logs_bloom: Bloom::zero(),
788            difficulty: self.difficulty,
789            number: 0,
790            gas_limit: self.gas_limit,
791            gas_used: 0,
792            timestamp: self.timestamp,
793            extra_data: self.extra_data.clone(),
794            prev_randao: self.mix_hash,
795            nonce: self.nonce,
796            base_fee_per_gas,
797            withdrawals_root,
798            blob_gas_used,
799            excess_blob_gas,
800            parent_beacon_block_root,
801            requests_hash,
802            block_access_list_hash,
803            slot_number,
804            ..Default::default()
805        }
806    }
807
808    fn get_block_body(&self) -> BlockBody {
809        BlockBody {
810            transactions: vec![],
811            ommers: vec![],
812            withdrawals: Some(vec![]),
813        }
814    }
815
816    pub fn compute_state_root(&self) -> H256 {
817        let iter = self.alloc.iter().map(|(addr, account)| {
818            (
819                keccak_hash(addr).to_vec(),
820                AccountState::from(account).encode_to_vec(),
821            )
822        });
823        Trie::compute_hash_from_unsorted_iter(iter, &NativeCrypto)
824    }
825}
826#[cfg(test)]
827mod tests {
828    use std::str::FromStr;
829    use std::{fs::File, io::BufReader};
830
831    use ethereum_types::H160;
832
833    use crate::types::INITIAL_BASE_FEE;
834
835    use super::*;
836
837    #[test]
838    fn terminal_total_difficulty_accepts_number_or_hex_string() {
839        // geth/reth-style genesis files encode terminalTotalDifficulty as a
840        // bare decimal number that exceeds u64::MAX; ethrex must accept it as
841        // well as the 0x-hex-string form.
842        let dca = r#""depositContractAddress":"0x00000000219ab540356cbb839cbe05303d7705fa""#;
843
844        let from_number: ChainConfig = serde_json::from_str(&format!(
845            r#"{{"chainId":1,"terminalTotalDifficulty":58750000000000000000000,{dca}}}"#
846        ))
847        .expect("number-encoded TTD should parse");
848        // f64 cast is lossy above u64::MAX; assert the known, stable
849        // approximation so a regression to Some(0) can't silently pass.
850        assert_eq!(
851            from_number.terminal_total_difficulty,
852            Some(58749999999999996329984u128)
853        );
854
855        let from_hex: ChainConfig = serde_json::from_str(&format!(
856            r#"{{"chainId":1,"terminalTotalDifficulty":"0xc70d808a128d7380000",{dca}}}"#
857        ))
858        .expect("hex-string TTD should parse");
859        assert_eq!(
860            from_hex.terminal_total_difficulty,
861            Some(58750000000000000000000u128)
862        );
863
864        let small: ChainConfig = serde_json::from_str(&format!(
865            r#"{{"chainId":1,"terminalTotalDifficulty":17000000000000000,{dca}}}"#
866        ))
867        .expect("small number TTD should parse");
868        assert_eq!(small.terminal_total_difficulty, Some(17000000000000000u128));
869
870        // A negative bare number must error, not silently saturate to Some(0).
871        let negative = serde_json::from_str::<ChainConfig>(&format!(
872            r#"{{"chainId":1,"terminalTotalDifficulty":-1,{dca}}}"#
873        ));
874        let err = negative.expect_err("negative TTD must be rejected");
875        assert!(
876            err.to_string().contains("finite, non-negative"),
877            "error should name the sign/finiteness cause, got: {err}"
878        );
879    }
880
881    #[test]
882    fn pow_genesis_detection() {
883        // Default genesis: difficulty 0 (post-merge at genesis) -> supported.
884        let mut g = Genesis::default();
885        assert!(!is_unsupported_pow_genesis(&g));
886
887        // PoW genesis: non-zero difficulty, no merge configured -> unsupported.
888        g.difficulty = U256::from(0x4000_0000u64);
889        assert!(is_unsupported_pow_genesis(&g));
890
891        // Mainnet-style: non-zero difficulty but TTD configured -> supported.
892        g.config.terminal_total_difficulty = Some(58750000000000000000000);
893        assert!(!is_unsupported_pow_genesis(&g));
894
895        // Merge-netsplit block configured (no TTD) -> supported.
896        g.config.terminal_total_difficulty = None;
897        g.config.merge_netsplit_block = Some(15537394);
898        assert!(!is_unsupported_pow_genesis(&g));
899
900        // TTD = Some(0) sentinel (PoS from genesis), non-zero difficulty -> supported.
901        g.config.merge_netsplit_block = None;
902        g.config.terminal_total_difficulty = Some(0);
903        assert!(!is_unsupported_pow_genesis(&g));
904
905        // terminal_total_difficulty_passed with no TTD value (post-merge snapshot
906        // that dropped the redundant field), non-zero difficulty -> supported.
907        g.config.terminal_total_difficulty = None;
908        g.config.terminal_total_difficulty_passed = true;
909        assert!(!is_unsupported_pow_genesis(&g));
910    }
911
912    #[test]
913    fn deserialize_genesis_file() {
914        // Deserialize genesis file
915        let file = File::open("../../fixtures/genesis/kurtosis.json")
916            .expect("Failed to open genesis file");
917        let reader = BufReader::new(file);
918        let genesis: Genesis =
919            serde_json::from_reader(reader).expect("Failed to deserialize genesis file");
920        // Check Genesis fields
921        // Chain config
922        let expected_chain_config = ChainConfig {
923            chain_id: 3151908_u64,
924            homestead_block: Some(0),
925            eip150_block: Some(0),
926            eip155_block: Some(0),
927            eip158_block: Some(0),
928            byzantium_block: Some(0),
929            constantinople_block: Some(0),
930            petersburg_block: Some(0),
931            istanbul_block: Some(0),
932            berlin_block: Some(0),
933            london_block: Some(0),
934            merge_netsplit_block: Some(0),
935            shanghai_time: Some(0),
936            cancun_time: Some(0),
937            prague_time: Some(1718232101),
938            terminal_total_difficulty: Some(0),
939            terminal_total_difficulty_passed: true,
940            deposit_contract_address: H160::from_str("0x4242424242424242424242424242424242424242")
941                .unwrap(),
942            // Note this BlobSchedule config is not the default
943            blob_schedule: BlobSchedule {
944                cancun: ForkBlobSchedule {
945                    target: 2,
946                    max: 3,
947                    base_fee_update_fraction: 6676954,
948                },
949                prague: ForkBlobSchedule {
950                    target: 3,
951                    max: 4,
952                    base_fee_update_fraction: 13353908,
953                },
954                ..Default::default()
955            },
956            ..Default::default()
957        };
958        assert_eq!(&genesis.config, &expected_chain_config);
959        // Genesis header fields
960        assert_eq!(genesis.coinbase, Address::from([0; 20]));
961        assert_eq!(genesis.difficulty, U256::from(1));
962        assert!(genesis.extra_data.is_empty());
963        assert_eq!(genesis.gas_limit, 0x17d7840);
964        assert_eq!(genesis.nonce, 0x1234);
965        assert_eq!(genesis.mix_hash, H256::from([0; 32]));
966        assert_eq!(genesis.timestamp, 1718040081);
967        // Check alloc field
968        // We will only check a couple of the hashmap's values as it is quite large
969        let addr_a = Address::from_str("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02").unwrap();
970        assert!(genesis.alloc.contains_key(&addr_a));
971        let expected_account_a = GenesisAccount {
972        code: Bytes::from(hex::decode("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500").unwrap()),
973        balance: 0.into(),
974        nonce: 1,
975        storage: Default::default(),
976    };
977        assert_eq!(genesis.alloc[&addr_a], expected_account_a);
978        // Check some storage values from another account
979        let addr_b = Address::from_str("0x4242424242424242424242424242424242424242").unwrap();
980        assert!(genesis.alloc.contains_key(&addr_b));
981        let addr_b_storage = &genesis.alloc[&addr_b].storage;
982        assert_eq!(
983            addr_b_storage.get(
984                &U256::from_str(
985                    "0x0000000000000000000000000000000000000000000000000000000000000022"
986                )
987                .unwrap()
988            ),
989            Some(
990                &U256::from_str(
991                    "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"
992                )
993                .unwrap()
994            )
995        );
996        assert_eq!(
997            addr_b_storage.get(
998                &U256::from_str(
999                    "0x0000000000000000000000000000000000000000000000000000000000000038"
1000                )
1001                .unwrap()
1002            ),
1003            Some(
1004                &U256::from_str(
1005                    "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7"
1006                )
1007                .unwrap()
1008            )
1009        );
1010    }
1011
1012    #[test]
1013    fn genesis_block() {
1014        // Deserialize genesis file
1015        let file = File::open("../../fixtures/genesis/kurtosis.json")
1016            .expect("Failed to open genesis file");
1017        let reader = BufReader::new(file);
1018        let genesis: Genesis =
1019            serde_json::from_reader(reader).expect("Failed to deserialize genesis file");
1020        let genesis_block = genesis.get_block();
1021        let header = genesis_block.header;
1022        let body = genesis_block.body;
1023        assert_eq!(header.parent_hash, H256::from([0; 32]));
1024        assert_eq!(header.ommers_hash, *DEFAULT_OMMERS_HASH);
1025        assert_eq!(header.coinbase, Address::default());
1026        assert_eq!(
1027            header.state_root,
1028            H256::from_str("0x2dab6a1d6d638955507777aecea699e6728825524facbd446bd4e86d44fa5ecd")
1029                .unwrap()
1030        );
1031        assert_eq!(
1032            header.transactions_root,
1033            compute_transactions_root(&[], &NativeCrypto)
1034        );
1035        assert_eq!(
1036            header.receipts_root,
1037            compute_receipts_root(&[], &NativeCrypto)
1038        );
1039        assert_eq!(header.logs_bloom, Bloom::default());
1040        assert_eq!(header.difficulty, U256::from(1));
1041        assert_eq!(header.gas_limit, 25_000_000);
1042        assert_eq!(header.gas_used, 0);
1043        assert_eq!(header.timestamp, 1_718_040_081);
1044        assert_eq!(header.extra_data, Bytes::default());
1045        assert_eq!(header.prev_randao, H256::from([0; 32]));
1046        assert_eq!(header.nonce, 4660);
1047        assert_eq!(
1048            header.base_fee_per_gas.unwrap_or(INITIAL_BASE_FEE),
1049            INITIAL_BASE_FEE
1050        );
1051        assert_eq!(
1052            header.withdrawals_root,
1053            Some(compute_withdrawals_root(&[], &NativeCrypto))
1054        );
1055        assert_eq!(header.blob_gas_used, Some(0));
1056        assert_eq!(header.excess_blob_gas, Some(0));
1057        assert_eq!(header.parent_beacon_block_root, Some(H256::zero()));
1058        assert!(body.transactions.is_empty());
1059        assert!(body.ommers.is_empty());
1060        assert!(body.withdrawals.is_some_and(|w| w.is_empty()));
1061    }
1062
1063    #[test]
1064    // Parses genesis received by kurtosis and checks that the hash matches the next block's parent hash
1065    fn read_and_compute_kurtosis_hash() {
1066        let file = File::open("../../fixtures/genesis/kurtosis.json")
1067            .expect("Failed to open genesis file");
1068        let reader = BufReader::new(file);
1069        let genesis: Genesis =
1070            serde_json::from_reader(reader).expect("Failed to deserialize genesis file");
1071        let genesis_block_hash = genesis.get_block().hash();
1072        assert_eq!(
1073            genesis_block_hash,
1074            H256::from_str("0xcb5306dd861d0f2c1f9952fbfbc75a46d0b6ce4f37bea370c3471fe8410bf40b")
1075                .unwrap()
1076        )
1077    }
1078
1079    #[test]
1080    fn parse_hive_genesis_file() {
1081        let file =
1082            File::open("../../fixtures/genesis/hive.json").expect("Failed to open genesis file");
1083        let reader = BufReader::new(file);
1084        let _genesis: Genesis =
1085            serde_json::from_reader(reader).expect("Failed to deserialize genesis file");
1086    }
1087
1088    #[test]
1089    fn read_and_compute_hive_hash() {
1090        let file =
1091            File::open("../../fixtures/genesis/hive.json").expect("Failed to open genesis file");
1092        let reader = BufReader::new(file);
1093        let genesis: Genesis =
1094            serde_json::from_reader(reader).expect("Failed to deserialize genesis file");
1095        let computed_block_hash = genesis.get_block().hash();
1096        let genesis_block_hash =
1097            H256::from_str("0x30f516e34fc173bb5fc4daddcc7532c4aca10b702c7228f3c806b4df2646fb7e")
1098                .unwrap();
1099        assert_eq!(genesis_block_hash, computed_block_hash)
1100    }
1101
1102    #[test]
1103    fn deserialize_chain_config_blob_schedule() {
1104        let json = r#"
1105
1106            {
1107                "chainId": 123,
1108                "blobSchedule": {
1109                  "cancun": {
1110                    "target": 1,
1111                    "max": 2,
1112                    "baseFeeUpdateFraction": 10000
1113                  },
1114                  "prague": {
1115                    "target": 3,
1116                    "max": 4,
1117                    "baseFeeUpdateFraction": 20000
1118                  }
1119                },
1120                "depositContractAddress": "0x4242424242424242424242424242424242424242"
1121            }
1122            "#;
1123
1124        let config: ChainConfig =
1125            serde_json::from_str(json).expect("Failed to deserialize ChainConfig");
1126        let expected_chain_config = ChainConfig {
1127            chain_id: 123,
1128            blob_schedule: BlobSchedule {
1129                cancun: ForkBlobSchedule {
1130                    target: 1,
1131                    max: 2,
1132                    base_fee_update_fraction: 10000,
1133                },
1134                prague: ForkBlobSchedule {
1135                    target: 3,
1136                    max: 4,
1137                    base_fee_update_fraction: 20000,
1138                },
1139                ..Default::default()
1140            },
1141            deposit_contract_address: H160::from_str("0x4242424242424242424242424242424242424242")
1142                .unwrap(),
1143            ..Default::default()
1144        };
1145        assert_eq!(&config, &expected_chain_config);
1146    }
1147
1148    #[test]
1149    fn deserialize_chain_config_missing_entire_blob_schedule() {
1150        let json = r#"
1151            {
1152                "chainId": 123,
1153                "depositContractAddress": "0x4242424242424242424242424242424242424242"
1154            }
1155            "#;
1156
1157        let config: ChainConfig =
1158            serde_json::from_str(json).expect("Failed to deserialize ChainConfig");
1159        let expected_chain_config = ChainConfig {
1160            chain_id: 123,
1161            blob_schedule: BlobSchedule {
1162                cancun: ForkBlobSchedule {
1163                    target: 3,
1164                    max: 6,
1165                    base_fee_update_fraction: 3338477,
1166                },
1167                prague: ForkBlobSchedule {
1168                    target: 6,
1169                    max: 9,
1170                    base_fee_update_fraction: 5007716,
1171                },
1172                ..Default::default()
1173            },
1174            deposit_contract_address: H160::from_str("0x4242424242424242424242424242424242424242")
1175                .unwrap(),
1176            ..Default::default()
1177        };
1178        assert_eq!(&config, &expected_chain_config);
1179    }
1180
1181    #[test]
1182    fn deserialize_chain_config_missing_cancun_blob_schedule() {
1183        let json = r#"
1184            {
1185                "chainId": 123,
1186                "blobSchedule": {
1187                    "prague": {
1188                      "target": 3,
1189                      "max": 4,
1190                      "baseFeeUpdateFraction": 20000
1191                    }
1192                },
1193                "depositContractAddress": "0x4242424242424242424242424242424242424242"
1194            }
1195            "#;
1196
1197        let config: ChainConfig =
1198            serde_json::from_str(json).expect("Failed to deserialize ChainConfig");
1199        let expected_chain_config = ChainConfig {
1200            chain_id: 123,
1201            blob_schedule: BlobSchedule {
1202                cancun: ForkBlobSchedule {
1203                    target: 3,
1204                    max: 6,
1205                    base_fee_update_fraction: 3338477,
1206                },
1207                prague: ForkBlobSchedule {
1208                    target: 3,
1209                    max: 4,
1210                    base_fee_update_fraction: 20000,
1211                },
1212                ..Default::default()
1213            },
1214            deposit_contract_address: H160::from_str("0x4242424242424242424242424242424242424242")
1215                .unwrap(),
1216            ..Default::default()
1217        };
1218        assert_eq!(&config, &expected_chain_config);
1219    }
1220
1221    #[test]
1222    fn deserialize_chain_config_missing_prague_blob_schedule() {
1223        let json = r#"
1224            {
1225                "chainId": 123,
1226                "blobSchedule": {
1227                  "cancun": {
1228                    "target": 1,
1229                    "max": 2,
1230                    "baseFeeUpdateFraction": 10000
1231                  }
1232                },
1233                "depositContractAddress": "0x4242424242424242424242424242424242424242"
1234            }
1235            "#;
1236
1237        let config: ChainConfig =
1238            serde_json::from_str(json).expect("Failed to deserialize ChainConfig");
1239        let expected_chain_config = ChainConfig {
1240            chain_id: 123,
1241            blob_schedule: BlobSchedule {
1242                cancun: ForkBlobSchedule {
1243                    target: 1,
1244                    max: 2,
1245                    base_fee_update_fraction: 10000,
1246                },
1247                prague: ForkBlobSchedule {
1248                    target: 6,
1249                    max: 9,
1250                    base_fee_update_fraction: 5007716,
1251                },
1252                ..Default::default()
1253            },
1254            deposit_contract_address: H160::from_str("0x4242424242424242424242424242424242424242")
1255                .unwrap(),
1256            ..Default::default()
1257        };
1258        assert_eq!(&config, &expected_chain_config);
1259    }
1260
1261    #[test]
1262    fn deserialize_chain_config_missing_deposit_contract_address() {
1263        let json = r#"
1264            {
1265                "chainId": 123
1266            }
1267            "#;
1268
1269        let result: Result<ChainConfig, _> = serde_json::from_str(json);
1270
1271        assert!(result.is_err());
1272
1273        let error_message = result.unwrap_err().to_string();
1274        assert!(error_message.contains("missing field `depositContractAddress`"),);
1275    }
1276
1277    #[test]
1278    fn lstar_fork_ordering_and_activation() {
1279        // LStar is the highest fork and strictly greater than Amsterdam.
1280        assert!(Fork::LStar > Fork::Amsterdam);
1281        assert_eq!(<&str>::from(Fork::LStar), "LStar");
1282
1283        let mut cfg = ChainConfig {
1284            lstar_time: Some(100),
1285            ..Default::default()
1286        };
1287        assert!(!cfg.is_lstar_activated(99));
1288        assert!(cfg.is_lstar_activated(100));
1289        assert!(cfg.is_lstar_activated(101));
1290
1291        cfg.lstar_time = None;
1292        assert!(!cfg.is_lstar_activated(u64::MAX));
1293    }
1294
1295    #[test]
1296    fn native_l2_genesis_activates_amsterdam() {
1297        let file = File::open("../../fixtures/genesis/native_l2.json")
1298            .expect("Failed to open native_l2.json");
1299        let reader = BufReader::new(file);
1300        let genesis: Genesis =
1301            serde_json::from_reader(reader).expect("Failed to deserialize native_l2.json");
1302        assert_eq!(
1303            genesis.config.get_fork(0),
1304            Fork::Amsterdam,
1305            "native_l2.json genesis should activate Amsterdam at timestamp 0"
1306        );
1307        // Confirm blobSchedule falls back to bpo2 (no explicit amsterdam entry).
1308        let bs = genesis
1309            .config
1310            .get_fork_blob_schedule(0)
1311            .expect("Amsterdam implies bpo2 blob schedule");
1312        assert_eq!(bs.max, 21, "Amsterdam should inherit bpo2 max=21");
1313    }
1314
1315    #[test]
1316    fn lstar_fork_resolution() {
1317        // Genesis-activated Amsterdam + LStar (LStar at a later time).
1318        let cfg = ChainConfig {
1319            cancun_time: Some(0),
1320            prague_time: Some(0),
1321            amsterdam_time: Some(0),
1322            lstar_time: Some(1000),
1323            ..Default::default()
1324        };
1325
1326        // Before LStar: highest active fork is Amsterdam.
1327        assert_eq!(cfg.get_fork(999), Fork::Amsterdam);
1328        // At/after LStar: highest active fork is LStar.
1329        assert_eq!(cfg.get_fork(1000), Fork::LStar);
1330
1331        // next_fork: at Amsterdam (pre-LStar) the next scheduled fork is LStar; at LStar there is none.
1332        assert_eq!(cfg.next_fork(999), Some(Fork::LStar));
1333        assert_eq!(cfg.next_fork(1000), None);
1334
1335        // get_last_scheduled_fork reflects LStar once scheduled.
1336        assert_eq!(cfg.get_last_scheduled_fork(), Fork::LStar);
1337
1338        // Activation timestamp round-trips.
1339        assert_eq!(
1340            cfg.get_activation_timestamp_for_fork(Fork::LStar),
1341            Some(1000)
1342        );
1343
1344        // Blob schedule at LStar inherits Amsterdam's (which inherits bpo2, max=9 default).
1345        let sched_lstar = cfg
1346            .get_fork_blob_schedule(1000)
1347            .expect("lstar blob schedule");
1348        let sched_amsterdam = cfg
1349            .get_fork_blob_schedule(999)
1350            .expect("amsterdam blob schedule");
1351        assert_eq!(sched_lstar.max, sched_amsterdam.max);
1352    }
1353
1354    #[test]
1355    fn test_hegota_after_amsterdam() {
1356        // Discriminant ordering: Hegota strictly follows Amsterdam.
1357        assert!(Fork::Hegota > Fork::Amsterdam);
1358        assert_eq!(Fork::Hegota as u8, 26);
1359        assert_eq!(Fork::Amsterdam as u8, 25);
1360
1361        // String conversion.
1362        let hegota_str: &str = Fork::Hegota.into();
1363        assert_eq!(hegota_str, "Hegota");
1364
1365        // Activation predicate boundary behavior.
1366        let mut config = ChainConfig {
1367            chain_id: 1,
1368            deposit_contract_address: Address::default(),
1369            ..Default::default()
1370        };
1371        config.hegota_time = Some(1000);
1372        assert!(!config.is_hegota_activated(999));
1373        assert!(config.is_hegota_activated(1000));
1374        assert!(config.is_hegota_activated(1001));
1375
1376        // None means inactive.
1377        config.hegota_time = None;
1378        assert!(!config.is_hegota_activated(0));
1379        assert!(!config.is_hegota_activated(u64::MAX));
1380
1381        // get_fork returns Hegota when both Amsterdam and Hegota are active.
1382        let mut config = ChainConfig {
1383            chain_id: 1,
1384            deposit_contract_address: Address::default(),
1385            ..Default::default()
1386        };
1387        config.amsterdam_time = Some(500);
1388        config.hegota_time = Some(1000);
1389        assert_eq!(config.get_fork(499), Fork::Paris);
1390        assert_eq!(config.get_fork(500), Fork::Amsterdam);
1391        assert_eq!(config.get_fork(999), Fork::Amsterdam);
1392        assert_eq!(config.get_fork(1000), Fork::Hegota);
1393        assert_eq!(config.get_fork(2000), Fork::Hegota);
1394
1395        // next_fork transitions correctly.
1396        assert_eq!(config.next_fork(500), Some(Fork::Hegota));
1397        assert_eq!(config.next_fork(1000), None);
1398
1399        // get_last_scheduled_fork picks Hegota when scheduled.
1400        assert_eq!(config.get_last_scheduled_fork(), Fork::Hegota);
1401
1402        // get_activation_timestamp_for_fork returns the right value.
1403        assert_eq!(
1404            config.get_activation_timestamp_for_fork(Fork::Hegota),
1405            Some(1000)
1406        );
1407    }
1408
1409    #[test]
1410    fn chain_config_accepts_heze_and_bogota_aliases_for_hegota_time() {
1411        for key in [
1412            "hegotaTime",
1413            "hezeTime",
1414            "heze_time",
1415            "bogotaTime",
1416            "bogota_time",
1417        ] {
1418            let json = format!(
1419                r#"{{
1420                    "chainId": 1,
1421                    "depositContractAddress": "0x0000000000000000000000000000000000000000",
1422                    "{key}": 1700000000
1423                }}"#
1424            );
1425            let cfg: ChainConfig = serde_json::from_str(&json)
1426                .unwrap_or_else(|e| panic!("alias {key} must deserialize: {e}"));
1427            assert_eq!(cfg.hegota_time, Some(1_700_000_000), "alias {key}");
1428        }
1429    }
1430
1431    #[test]
1432    fn next_fork_skips_unscheduled_intermediate_forks() {
1433        // bal-devnet-7 layout: every post-merge fork up to Osaka at t=0, Amsterdam
1434        // scheduled later, no BPOs scheduled. `next_fork` must return Amsterdam
1435        // even though the BPO chain between Osaka and Amsterdam is empty.
1436        let config = ChainConfig {
1437            shanghai_time: Some(0),
1438            cancun_time: Some(0),
1439            prague_time: Some(0),
1440            osaka_time: Some(0),
1441            amsterdam_time: Some(1_779_098_127),
1442            ..Default::default()
1443        };
1444        assert_eq!(config.next_fork(0), Some(Fork::Amsterdam));
1445        assert_eq!(config.next_fork(1_779_098_126), Some(Fork::Amsterdam));
1446        assert_eq!(config.next_fork(1_779_098_127), None);
1447    }
1448
1449    #[test]
1450    fn next_fork_picks_earliest_scheduled() {
1451        // Contiguous schedule: at Cancun, next should be Prague (not Osaka).
1452        let config = ChainConfig {
1453            shanghai_time: Some(0),
1454            cancun_time: Some(100),
1455            prague_time: Some(200),
1456            osaka_time: Some(300),
1457            ..Default::default()
1458        };
1459        assert_eq!(config.next_fork(150), Some(Fork::Prague));
1460        assert_eq!(config.next_fork(250), Some(Fork::Osaka));
1461        assert_eq!(config.next_fork(300), None);
1462    }
1463
1464    #[test]
1465    fn next_fork_returns_none_when_at_last_scheduled() {
1466        let config = ChainConfig {
1467            shanghai_time: Some(0),
1468            cancun_time: Some(0),
1469            ..Default::default()
1470        };
1471        assert_eq!(config.next_fork(0), None);
1472    }
1473
1474    #[test]
1475    fn amsterdam_inherits_highest_activated_bpo_schedule() {
1476        // EIP-7892: Amsterdam declares no blob params of its own, so a genesis that
1477        // schedules BPO1 and then Amsterdam keeps BPO1's target and max across the
1478        // Amsterdam boundary. An unscheduled BPO2 must never contribute its schedule.
1479        let config: ChainConfig = serde_json::from_str(
1480            r#"{
1481                "chainId": 1,
1482                "depositContractAddress": "0x00000000219ab540356cbb839cbe05303d7705fa",
1483                "osakaTime": 0,
1484                "bpo1Time": 0,
1485                "amsterdamTime": 100,
1486                "blobSchedule": {
1487                    "cancun": {"target": 3, "max": 6, "baseFeeUpdateFraction": 3338477},
1488                    "prague": {"target": 6, "max": 9, "baseFeeUpdateFraction": 5007716},
1489                    "bpo1": {"target": 10, "max": 15, "baseFeeUpdateFraction": 8346193}
1490                }
1491            }"#,
1492        )
1493        .expect("genesis should parse");
1494
1495        let bpo1 = ForkBlobSchedule {
1496            target: 10,
1497            max: 15,
1498            base_fee_update_fraction: 8346193,
1499        };
1500        assert_eq!(config.get_fork_blob_schedule(99), Some(bpo1));
1501        assert_eq!(config.get_fork_blob_schedule(100), Some(bpo1));
1502    }
1503
1504    #[test]
1505    fn blob_schedule_for_fork_reports_the_inherited_entry() {
1506        // `eth_config` (EIP-7910) must report the blob params actually in force at a
1507        // fork's activation, which for Amsterdam is the highest activated BPO entry.
1508        let config = ChainConfig {
1509            osaka_time: Some(0),
1510            bpo1_time: Some(0),
1511            amsterdam_time: Some(100),
1512            ..Default::default()
1513        };
1514
1515        let schedule = config
1516            .get_blob_schedule_for_fork(Fork::Amsterdam)
1517            .expect("Amsterdam must report a blob schedule");
1518        assert_eq!((schedule.target, schedule.max), (10, 15));
1519
1520        // Forks that were never scheduled have nothing to report, and pre-Cancun forks
1521        // are activated by block number rather than timestamp.
1522        assert_eq!(config.get_blob_schedule_for_fork(Fork::BPO2), None);
1523        assert_eq!(config.get_blob_schedule_for_fork(Fork::London), None);
1524    }
1525
1526    #[test]
1527    fn amsterdam_prefers_explicitly_pinned_schedule() {
1528        let mut config = ChainConfig {
1529            osaka_time: Some(0),
1530            bpo1_time: Some(0),
1531            bpo2_time: Some(50),
1532            amsterdam_time: Some(100),
1533            ..Default::default()
1534        };
1535        assert_eq!(
1536            config.get_fork_blob_schedule(100),
1537            Some(default_bpo2_schedule())
1538        );
1539
1540        let pinned = ForkBlobSchedule {
1541            target: 16,
1542            max: 24,
1543            base_fee_update_fraction: 13353910,
1544        };
1545        config.blob_schedule.amsterdam = Some(pinned);
1546        assert_eq!(config.get_fork_blob_schedule(100), Some(pinned));
1547    }
1548}