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