Skip to main content

espresso_node/
genesis.rs

1use std::{
2    cmp::max,
3    collections::{BTreeMap, HashMap},
4    fmt::{self, Display, Formatter},
5    path::{Path, PathBuf},
6    str::FromStr,
7};
8
9use alloy::primitives::Address;
10use anyhow::{Context, Ok, ensure};
11use espresso_types::{
12    FeeAccount, FeeAmount, GenesisHeader, L1BlockInfo, L1Client, SeqTypes, Timestamp, Upgrade,
13    v0_3::ChainConfig,
14};
15use hotshot_types::{
16    VersionedDaCommittee, data::EpochNumber, utils::epoch_from_block_number, version_ser,
17};
18use serde::{Deserialize, Serialize, Serializer};
19use url::Url;
20use vbs::version::Version;
21use versions::{DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_VERSION};
22
23/// A location from which to load the genesis file.
24///
25/// Accepts a plain filesystem path (e.g. `/etc/espresso/genesis.toml`) or an `http://` /
26/// `https://` URL.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum GenesisSource {
29    Path(PathBuf),
30    Http(Url),
31}
32
33impl FromStr for GenesisSource {
34    type Err = anyhow::Error;
35
36    fn from_str(s: &str) -> anyhow::Result<Self> {
37        if s.starts_with("http://") || s.starts_with("https://") {
38            let url = Url::parse(s).with_context(|| format!("invalid genesis URL: {s}"))?;
39            Ok(Self::Http(url))
40        } else {
41            Ok(Self::Path(PathBuf::from(s)))
42        }
43    }
44}
45
46impl Display for GenesisSource {
47    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::Path(p) => write!(f, "{}", p.display()),
50            Self::Http(u) => write!(f, "{u}"),
51        }
52    }
53}
54
55impl Serialize for GenesisSource {
56    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
57        s.collect_str(self)
58    }
59}
60
61/// Initial configuration of an Espresso stake table.
62#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
63pub struct StakeTableConfig {
64    pub capacity: usize,
65}
66
67/// An L1 block from which an Espresso chain should start syncing.
68#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
69#[serde(untagged)]
70pub enum L1Finalized {
71    /// Complete block info.
72    ///
73    /// This allows a validator to specify the exact, existing L1 block to start syncing from. A
74    /// validator that specifies a specific L1 block will not be able to reach consensus with a
75    /// malicious validator that starts from a different L1 block.
76    Block(L1BlockInfo),
77
78    /// An L1 block number to sync from.
79    ///
80    /// This allows a validator to specify a future L1 block whose hash is not yet known, and start
81    /// syncing only when a finalized block with the given number becomes available. The configured
82    /// L1 client will be used to fetch the rest of the block info once available.
83    Number { number: u64 },
84
85    /// A time from which to start syncing L1 blocks.
86    ///
87    /// This allows a validator to specify a future time at which the network should start. The
88    /// network will start syncing from the first L1 block with timestamp greater than or equal to
89    /// this, once said block is finalized.
90    Timestamp { timestamp: Timestamp },
91}
92
93/// Genesis of an Espresso chain.
94#[derive(Clone, Debug, Deserialize, Serialize)]
95pub struct Genesis {
96    #[serde(with = "version_ser")]
97    pub base_version: Version,
98    #[serde(with = "version_ser")]
99    pub upgrade_version: Version,
100    #[serde(with = "version_ser")]
101    pub genesis_version: Version,
102    pub epoch_height: Option<u64>,
103    pub drb_difficulty: Option<u64>,
104    pub drb_upgrade_difficulty: Option<u64>,
105    pub epoch_start_block: Option<u64>,
106    pub stake_table_capacity: Option<usize>,
107    pub chain_config: ChainConfig,
108    pub stake_table: StakeTableConfig,
109    #[serde(default)]
110    pub accounts: HashMap<FeeAccount, FeeAmount>,
111    pub l1_finalized: L1Finalized,
112    pub header: GenesisHeader,
113    #[serde(rename = "upgrade", with = "upgrade_ser")]
114    #[serde(default)]
115    pub upgrades: BTreeMap<Version, Upgrade>,
116    #[serde(default)]
117    pub da_committees: Option<Vec<VersionedDaCommittee<SeqTypes>>>,
118}
119
120impl Genesis {
121    pub fn max_base_fee(&self) -> FeeAmount {
122        let mut base_fee = self.chain_config.base_fee;
123
124        let upgrades: Vec<&Upgrade> = self.upgrades.values().collect();
125
126        for upgrade in upgrades {
127            let chain_config = upgrade.upgrade_type.chain_config();
128
129            if let Some(cf) = chain_config {
130                base_fee = std::cmp::max(cf.base_fee, base_fee);
131            }
132        }
133
134        base_fee
135    }
136
137    /// `max_block_size` per protocol version: base version and upgrades with a chain config.
138    pub fn block_sizes(&self) -> BTreeMap<Version, u64> {
139        let upgrades = self.upgrades.iter().filter_map(|(version, upgrade)| {
140            let cf = upgrade.upgrade_type.chain_config()?;
141            Some((*version, *cf.max_block_size))
142        });
143        std::iter::once((self.base_version, *self.chain_config.max_block_size))
144            .chain(upgrades)
145            .collect()
146    }
147
148    /// The first epoch, which runs on the genesis stake table.
149    ///
150    /// Fails when `epoch_height` is missing or zero.
151    pub fn first_epoch(&self) -> anyhow::Result<EpochNumber> {
152        let epoch_height = self
153            .epoch_height
154            .filter(|&height| height > 0)
155            .context("genesis needs a positive epoch_height")?;
156        let start = self.epoch_start_block.unwrap_or_default();
157        Ok(EpochNumber::new(epoch_from_block_number(
158            start,
159            epoch_height,
160        )))
161    }
162}
163
164impl Genesis {
165    /// Validate that required fields are present for the configured protocol versions.
166    pub fn validate(&self) -> anyhow::Result<()> {
167        ensure!(
168            self.genesis_version <= self.base_version,
169            "genesis_version cannot be greater than base_version"
170        );
171
172        let version = max(self.base_version, self.upgrade_version);
173
174        if version >= EPOCH_VERSION {
175            self.epoch_height
176                .context("epoch_height missing from genesis")?;
177            self.epoch_start_block
178                .context("epoch_start_block missing from genesis")?;
179        }
180
181        if version >= DRB_AND_HEADER_UPGRADE_VERSION {
182            self.drb_difficulty
183                .context("drb_difficulty missing from genesis")?;
184            self.drb_upgrade_difficulty
185                .context("drb_upgrade_difficulty missing from genesis")?;
186        }
187
188        Ok(())
189    }
190
191    pub async fn validate_fee_contract(&self, l1: &L1Client) -> anyhow::Result<()> {
192        if let Some(fee_contract_address) = self.chain_config.fee_contract {
193            tracing::info!("validating fee contract at {fee_contract_address:x}");
194
195            if !l1
196                .retry_on_all_providers(|| l1.is_proxy_contract(fee_contract_address))
197                .await
198                .context("checking if fee contract is a proxy")?
199            {
200                anyhow::bail!("Fee contract address {fee_contract_address:x} is not a proxy");
201            }
202        }
203
204        // now iterate over each upgrade type and validate the fee contract if it exists
205        for (version, upgrade) in &self.upgrades {
206            let chain_config = &upgrade.upgrade_type.chain_config();
207
208            if chain_config.is_none() {
209                continue;
210            }
211
212            let chain_config = chain_config.unwrap();
213
214            if let Some(fee_contract_address) = chain_config.fee_contract {
215                if fee_contract_address == Address::default() {
216                    anyhow::bail!("Fee contract cannot use the zero address");
217                } else if !l1
218                    .retry_on_all_providers(|| l1.is_proxy_contract(fee_contract_address))
219                    .await
220                    .context(format!(
221                        "checking if fee contract is a proxy in upgrade {version}",
222                    ))?
223                {
224                    anyhow::bail!("Fee contract's address is not a proxy");
225                }
226            } else {
227                // The Fee Contract address has to be provided for an upgrade so return an error
228                anyhow::bail!("Fee contract's address for the upgrade is missing");
229            }
230        }
231        // TODO: it's optional for the fee contract to be included in a proxy in v1 so no need to panic but revisit this after v1 https://github.com/EspressoSystems/espresso-network/pull/2000#discussion_r1765174702
232        Ok(())
233    }
234}
235
236mod upgrade_ser {
237    use std::{collections::BTreeMap, fmt};
238
239    use espresso_types::{
240        Upgrade, UpgradeType,
241        v0_1::{TimeBasedUpgrade, UpgradeMode, ViewBasedUpgrade},
242    };
243    use serde::{
244        Deserialize, Deserializer, Serialize, Serializer,
245        de::{self, SeqAccess, Visitor},
246        ser::SerializeSeq,
247    };
248    use vbs::version::Version;
249
250    pub fn serialize<S>(map: &BTreeMap<Version, Upgrade>, serializer: S) -> Result<S::Ok, S::Error>
251    where
252        S: Serializer,
253    {
254        #[derive(Debug, Clone, Serialize, Deserialize)]
255        pub struct Fields {
256            pub version: String,
257            #[serde(flatten)]
258            pub mode: UpgradeMode,
259            #[serde(flatten)]
260            pub upgrade_type: UpgradeType,
261        }
262
263        let mut seq = serializer.serialize_seq(Some(map.len()))?;
264        for (version, upgrade) in map {
265            seq.serialize_element(&Fields {
266                version: version.to_string(),
267                mode: upgrade.mode.clone(),
268                upgrade_type: upgrade.upgrade_type.clone(),
269            })?
270        }
271        seq.end()
272    }
273
274    pub fn deserialize<'de, D>(deserializer: D) -> Result<BTreeMap<Version, Upgrade>, D::Error>
275    where
276        D: Deserializer<'de>,
277    {
278        struct VecToHashMap;
279
280        #[derive(Debug, Clone, Serialize, Deserialize)]
281        pub struct Fields {
282            pub version: String,
283            // If both `time_based` and `view_based` fields are provided
284            // and we use an enum for deserialization, then one of the variant fields will be ignored.
285            // We want to raise an error in such a case to avoid ambiguity
286            #[serde(flatten)]
287            pub time_based: Option<TimeBasedUpgrade>,
288            #[serde(flatten)]
289            pub view_based: Option<ViewBasedUpgrade>,
290            #[serde(flatten)]
291            pub upgrade_type: UpgradeType,
292        }
293
294        impl<'de> Visitor<'de> for VecToHashMap {
295            type Value = BTreeMap<Version, Upgrade>;
296
297            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
298                formatter.write_str("a vector of tuples (key-value pairs)")
299            }
300
301            fn visit_seq<A>(self, mut seq: A) -> Result<BTreeMap<Version, Upgrade>, A::Error>
302            where
303                A: SeqAccess<'de>,
304            {
305                let mut map = BTreeMap::new();
306
307                while let Some(fields) = seq.next_element::<Fields>()? {
308                    // add try_from in Version
309                    let version: Vec<_> = fields.version.split('.').collect();
310
311                    let version = Version {
312                        major: version[0]
313                            .parse()
314                            .map_err(|_| de::Error::custom("invalid version format"))?,
315                        minor: version[1]
316                            .parse()
317                            .map_err(|_| de::Error::custom("invalid version format"))?,
318                    };
319
320                    match (fields.time_based, fields.view_based) {
321                        (Some(_), Some(_)) => {
322                            return Err(de::Error::custom(
323                                "both view and time mode parameters are set",
324                            ));
325                        },
326                        (None, None) => {
327                            return Err(de::Error::custom(
328                                "no view or time mode parameters provided",
329                            ));
330                        },
331                        (None, Some(v)) => {
332                            if v.start_proposing_view > v.stop_proposing_view {
333                                return Err(de::Error::custom(
334                                    "stop_proposing_view is less than start_proposing_view",
335                                ));
336                            }
337
338                            map.insert(
339                                version,
340                                Upgrade {
341                                    mode: UpgradeMode::View(v),
342                                    upgrade_type: fields.upgrade_type,
343                                },
344                            );
345                        },
346                        (Some(t), None) => {
347                            if t.start_proposing_time.unix_timestamp()
348                                > t.stop_proposing_time.unix_timestamp()
349                            {
350                                return Err(de::Error::custom(
351                                    "stop_proposing_time is less than start_proposing_time",
352                                ));
353                            }
354
355                            map.insert(
356                                version,
357                                Upgrade {
358                                    mode: UpgradeMode::Time(t),
359                                    upgrade_type: fields.upgrade_type.clone(),
360                                },
361                            );
362                        },
363                    }
364                }
365
366                Ok(map)
367            }
368        }
369
370        deserializer.deserialize_seq(VecToHashMap)
371    }
372}
373
374impl Genesis {
375    pub fn to_file(&self, path: impl AsRef<Path>) -> anyhow::Result<()> {
376        let toml = toml::to_string_pretty(self)?;
377        std::fs::write(path, toml.as_bytes())?;
378        Ok(())
379    }
380
381    pub fn from_file(path: impl AsRef<Path>) -> anyhow::Result<Self> {
382        let path = path.as_ref();
383        let bytes = std::fs::read(path).context(format!("genesis file {}", path.display()))?;
384        let text = std::str::from_utf8(&bytes).context("genesis file must be UTF-8")?;
385        Self::from_toml(text)
386    }
387
388    /// Load a genesis configuration from a local file or HTTP(S) URL.
389    pub async fn load(source: &GenesisSource) -> anyhow::Result<Self> {
390        match source {
391            GenesisSource::Path(p) => Self::from_file(p),
392            GenesisSource::Http(url) => {
393                let client = reqwest::Client::builder()
394                    .timeout(std::time::Duration::from_secs(30))
395                    .build()
396                    .context("building HTTP client")?;
397                let text = client
398                    .get(url.clone())
399                    .send()
400                    .await
401                    .with_context(|| format!("fetching genesis from {url}"))?
402                    .error_for_status()
403                    .with_context(|| format!("fetching genesis from {url}"))?
404                    .text()
405                    .await
406                    .with_context(|| format!("reading genesis response from {url}"))?;
407                Self::from_toml(&text)
408            },
409        }
410    }
411
412    fn from_toml(text: &str) -> anyhow::Result<Self> {
413        let genesis: Self = toml::from_str(text).context("malformed genesis file")?;
414        genesis.validate().context("validating genesis")?;
415        Ok(genesis)
416    }
417}
418
419#[cfg(test)]
420mod test {
421    use std::{fs, path::Path, sync::Arc};
422
423    use alloy::{
424        node_bindings::Anvil,
425        primitives::{B256, U256},
426        providers::{ProviderBuilder, layers::AnvilProvider},
427    };
428    use espresso_contract_deployer::{self as deployer, Contracts};
429    use espresso_types::{
430        L1BlockInfo, TimeBasedUpgrade, Timestamp, UpgradeMode, UpgradeType, ViewBasedUpgrade,
431    };
432    use espresso_utils::ser::FromStringOrInteger;
433    use tempfile::NamedTempFile;
434    use toml::toml;
435
436    use super::*;
437
438    fn minimal_genesis_toml(version: &str, root_fields: &str) -> String {
439        format!(
440            r#"
441            base_version = "{version}"
442            upgrade_version = "{version}"
443            genesis_version = "{version}"
444            {root_fields}
445
446            [stake_table]
447            capacity = 10
448
449            [chain_config]
450            chain_id = 12345
451            max_block_size = 30000
452            base_fee = 1
453            fee_recipient = "0x0000000000000000000000000000000000000000"
454
455            [header]
456            timestamp = 123456
457
458            [header.chain_config]
459            chain_id = 35353
460            max_block_size = 30720
461            base_fee = 0
462            fee_recipient = "0x0000000000000000000000000000000000000000"
463
464            [l1_finalized]
465            number = 0
466            "#
467        )
468    }
469
470    #[test]
471    fn test_genesis_validation_allows_pre_epoch_without_epoch_fields() {
472        let genesis: Genesis = toml::from_str(&minimal_genesis_toml("0.2", "")).unwrap();
473        genesis.validate().unwrap();
474    }
475
476    #[test]
477    fn test_genesis_validation_requires_epoch_fields() {
478        let genesis: Genesis = toml::from_str(&minimal_genesis_toml("0.3", "")).unwrap();
479        assert!(genesis.validate().is_err());
480    }
481
482    #[test]
483    fn test_genesis_validation_requires_drb_fields() {
484        let genesis: Genesis = toml::from_str(&minimal_genesis_toml(
485            "0.4",
486            r#"
487                epoch_height = 20
488                epoch_start_block = 1
489                stake_table_capacity = 200
490                "#,
491        ))
492        .unwrap();
493        assert!(genesis.validate().is_err());
494    }
495
496    #[test]
497    fn test_genesis_from_file_accepts_complete_v04_genesis() {
498        let file = NamedTempFile::new().unwrap();
499        fs::write(
500            file.path(),
501            minimal_genesis_toml(
502                "0.4",
503                r#"
504                epoch_height = 20
505                epoch_start_block = 1
506                stake_table_capacity = 200
507                drb_difficulty = 10
508                drb_upgrade_difficulty = 20
509                "#,
510            ),
511        )
512        .unwrap();
513
514        assert!(Genesis::from_file(file.path()).is_ok());
515    }
516
517    #[test]
518    fn test_committed_genesis_files_validate() {
519        let genesis_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../data/genesis");
520        let mut checked = 0;
521
522        for entry in fs::read_dir(&genesis_dir).unwrap() {
523            let path = entry.unwrap().path();
524            if path.extension().is_none_or(|ext| ext != "toml") {
525                continue;
526            }
527
528            Genesis::from_file(&path)
529                .unwrap_or_else(|err| panic!("{} failed validation: {err:#}", path.display()));
530            checked += 1;
531        }
532
533        assert!(checked > 0, "no genesis files found");
534    }
535
536    #[test]
537    fn block_sizes_cover_upgrades() {
538        let path = Path::new(env!("CARGO_MANIFEST_DIR"))
539            .join("../../../data/genesis/demo-large-block-upgrade.toml");
540        let genesis = Genesis::from_file(&path).unwrap();
541        assert_eq!(
542            genesis.block_sizes(),
543            BTreeMap::from([
544                (Version { major: 0, minor: 6 }, 1_000_000),
545                (Version { major: 0, minor: 7 }, 10_000_000),
546            ])
547        );
548    }
549
550    /// Live networks pin the full genesis L1 block, so startup never fetches it from L1.
551    #[test]
552    fn test_live_genesis_files_pin_l1_block() {
553        let cases = [
554            (
555                "mainnet.toml",
556                21116303,
557                0x67291ceb,
558                "0x8c580daf96a83884d4fac219dea3c26d7a805e4c92c91e04c5efc26929b4b129",
559            ),
560            (
561                "decaf.toml",
562                6758037,
563                0x66f43a1c,
564                "0x7063208185b352ec9732ee8eda32fa997dffa3a7abac945b9d91b21d85b173e2",
565            ),
566        ];
567        for (file, number, timestamp, hash) in cases {
568            let path = Path::new(env!("CARGO_MANIFEST_DIR"))
569                .join("../../../data/genesis")
570                .join(file);
571            let genesis = Genesis::from_file(&path).unwrap();
572            assert_eq!(
573                genesis.l1_finalized,
574                L1Finalized::Block(L1BlockInfo {
575                    number,
576                    timestamp: U256::from(timestamp),
577                    hash: hash.parse::<B256>().unwrap(),
578                }),
579                "{file}",
580            );
581        }
582    }
583
584    #[test]
585    fn test_genesis_from_toml_with_optional_fields() {
586        let toml = toml! {
587            base_version = "0.1"
588            upgrade_version = "0.2"
589            genesis_version = "0.1"
590
591            [stake_table]
592            capacity = 10
593
594            [chain_config]
595            chain_id = 12345
596            max_block_size = 30000
597            base_fee = 1
598            fee_recipient = "0x0000000000000000000000000000000000000000"
599            fee_contract = "0x0000000000000000000000000000000000000000"
600
601            [header]
602            timestamp = 123456
603
604            [header.chain_config]
605            chain_id = 35353
606            max_block_size = 30720
607            base_fee = 0
608            fee_recipient = "0x0000000000000000000000000000000000000000"
609
610            [accounts]
611            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
612            "0x0000000000000000000000000000000000000000" = 42
613
614            [l1_finalized]
615            number = 64
616            timestamp = "0x123def"
617            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
618        }
619        .to_string();
620
621        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
622        assert_eq!(genesis.genesis_version, Version { major: 0, minor: 1 });
623        assert_eq!(genesis.stake_table, StakeTableConfig { capacity: 10 });
624        assert_eq!(
625            genesis.chain_config,
626            ChainConfig {
627                chain_id: 12345.into(),
628                max_block_size: 30000.into(),
629                base_fee: 1.into(),
630                fee_recipient: FeeAccount::default(),
631                fee_contract: Some(Address::default()),
632                stake_table_contract: None
633            }
634        );
635        assert_eq!(
636            genesis.header,
637            GenesisHeader {
638                timestamp: Timestamp::from_integer(123456).unwrap(),
639                chain_config: ChainConfig::default(),
640            }
641        );
642        assert_eq!(
643            genesis.accounts,
644            [
645                (
646                    FeeAccount::from(Address::from([
647                        0x23, 0x61, 0x8e, 0x81, 0xe3, 0xf5, 0xcd, 0xf7, 0xf5, 0x4c, 0x3d, 0x65,
648                        0xf7, 0xfb, 0xc0, 0xab, 0xf5, 0xb2, 0x1e, 0x8f
649                    ])),
650                    100000.into()
651                ),
652                (FeeAccount::default(), 42.into())
653            ]
654            .into_iter()
655            .collect::<HashMap<_, _>>()
656        );
657        assert_eq!(
658            genesis.l1_finalized,
659            L1Finalized::Block(L1BlockInfo {
660                number: 64,
661                timestamp: U256::from(0x123def),
662                // Can't do B256 here directly because it's the wrong endianness
663                hash: B256::from([
664                    0x80, 0xf5, 0xdd, 0x11, 0xf2, 0xbd, 0xda, 0x28, 0x14, 0xcb, 0x1a, 0xd9, 0x4e,
665                    0xf3, 0x0a, 0x47, 0xde, 0x02, 0xcf, 0x28, 0xad, 0x68, 0xc8, 0x9e, 0x10, 0x4c,
666                    0x00, 0xc4, 0xe5, 0x1b, 0xb7, 0xa5
667                ])
668            })
669        );
670    }
671
672    #[test]
673    fn test_genesis_from_toml_without_optional_fields() {
674        let toml = toml! {
675            base_version = "0.1"
676            upgrade_version = "0.2"
677            genesis_version = "0.1"
678
679            [stake_table]
680            capacity = 10
681
682            [chain_config]
683            chain_id = 12345
684            max_block_size = 30000
685            base_fee = 1
686            fee_recipient = "0x0000000000000000000000000000000000000000"
687
688            [header]
689            timestamp = 123456
690           [header.chain_config]
691            chain_id = 35353
692            max_block_size = 30720
693            base_fee = 0
694            fee_recipient = "0x0000000000000000000000000000000000000000"
695
696            [l1_finalized]
697            number = 0
698        }
699        .to_string();
700
701        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
702
703        assert_eq!(genesis.stake_table, StakeTableConfig { capacity: 10 });
704        assert_eq!(
705            genesis.chain_config,
706            ChainConfig {
707                chain_id: 12345.into(),
708                max_block_size: 30000.into(),
709                base_fee: 1.into(),
710                fee_recipient: FeeAccount::default(),
711                fee_contract: None,
712                stake_table_contract: None,
713            }
714        );
715        assert_eq!(
716            genesis.header,
717            GenesisHeader {
718                timestamp: Timestamp::from_integer(123456).unwrap(),
719                chain_config: ChainConfig::default(),
720            }
721        );
722        assert_eq!(genesis.accounts, HashMap::default());
723        assert_eq!(genesis.l1_finalized, L1Finalized::Number { number: 0 });
724    }
725
726    #[test]
727    fn test_genesis_l1_finalized_number_only() {
728        let toml = toml! {
729            base_version = "0.1"
730            upgrade_version = "0.2"
731            genesis_version = "0.1"
732
733            [stake_table]
734            capacity = 10
735
736            [chain_config]
737            chain_id = 12345
738            max_block_size = 30000
739            base_fee = 1
740            fee_recipient = "0x0000000000000000000000000000000000000000"
741
742            [header]
743            timestamp = 123456
744
745            [header.chain_config]
746            chain_id = 35353
747            max_block_size = 30720
748            base_fee = 0
749            fee_recipient = "0x0000000000000000000000000000000000000000"
750
751            [l1_finalized]
752            number = 42
753        }
754        .to_string();
755
756        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
757        assert_eq!(genesis.l1_finalized, L1Finalized::Number { number: 42 });
758    }
759
760    #[test]
761    fn test_genesis_l1_finalized_timestamp_only() {
762        let toml = toml! {
763            base_version = "0.1"
764            upgrade_version = "0.2"
765            genesis_version = "0.1"
766
767            [stake_table]
768            capacity = 10
769
770            [chain_config]
771            chain_id = 12345
772            max_block_size = 30000
773            base_fee = 1
774            fee_recipient = "0x0000000000000000000000000000000000000000"
775
776            [header]
777            timestamp = 123456
778
779            [header.chain_config]
780            chain_id = 35353
781            max_block_size = 30720
782            base_fee = 0
783            fee_recipient = "0x0000000000000000000000000000000000000000"
784
785            [l1_finalized]
786            timestamp = "2024-01-02T00:00:00Z"
787        }
788        .to_string();
789
790        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
791        assert_eq!(
792            genesis.l1_finalized,
793            L1Finalized::Timestamp {
794                timestamp: Timestamp::from_string("2024-01-02T00:00:00Z".to_string()).unwrap()
795            }
796        );
797    }
798
799    // tests for fee contract not being a proxy are removed, since we now only have one function in `deployer.rs` that ensures
800    // deploying of the fee contract behind proxy, and this function is being unit tested there.
801    // Here, we primarily focus on testing the config and validation logic, not deployment logic.
802
803    #[test_log::test(tokio::test(flavor = "multi_thread"))]
804    async fn test_genesis_fee_contract_is_a_proxy() -> anyhow::Result<()> {
805        let anvil = Arc::new(Anvil::new().spawn());
806        let wallet = anvil.wallet().unwrap();
807        let admin = wallet.default_signer().address();
808        let inner_provider = ProviderBuilder::new()
809            .wallet(wallet)
810            .connect_http(anvil.endpoint_url());
811        let provider = AnvilProvider::new(inner_provider, Arc::clone(&anvil));
812        let mut contracts = Contracts::new();
813
814        let proxy_addr =
815            deployer::deploy_fee_contract_proxy(&provider, &mut contracts, admin).await?;
816
817        let toml = format!(
818            r#"
819            base_version = "0.1"
820            upgrade_version = "0.2"
821            genesis_version = "0.1"
822
823            [stake_table]
824            capacity = 10
825
826            [chain_config]
827            chain_id = 12345
828            max_block_size = 30000
829            base_fee = 1
830            fee_recipient = "0x0000000000000000000000000000000000000000"
831            fee_contract = "{proxy_addr:?}"
832
833            [header]
834            timestamp = 123456
835
836            [header.chain_config]
837            chain_id = 35353
838            max_block_size = 30720
839            base_fee = 0
840            fee_recipient = "0x0000000000000000000000000000000000000000"
841
842            [l1_finalized]
843            number = 42
844        "#,
845        )
846        .to_string();
847
848        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
849
850        // Call the validation logic for the fee_contract address
851        let result = genesis
852            .validate_fee_contract(&L1Client::anvil(&anvil).unwrap())
853            .await;
854
855        assert!(
856            result.is_ok(),
857            "Expected Fee Contract to be a proxy, but it was not"
858        );
859        Ok(())
860    }
861
862    #[test_log::test(tokio::test(flavor = "multi_thread"))]
863    async fn test_genesis_fee_contract_is_a_proxy_with_upgrades() -> anyhow::Result<()> {
864        let anvil = Arc::new(Anvil::new().spawn());
865        let wallet = anvil.wallet().unwrap();
866        let admin = wallet.default_signer().address();
867        let inner_provider = ProviderBuilder::new()
868            .wallet(wallet)
869            .connect_http(anvil.endpoint_url());
870        let provider = AnvilProvider::new(inner_provider, Arc::clone(&anvil));
871        let mut contracts = Contracts::new();
872
873        let proxy_addr =
874            deployer::deploy_fee_contract_proxy(&provider, &mut contracts, admin).await?;
875
876        let toml = format!(
877            r#"
878            base_version = "0.1"
879            upgrade_version = "0.2"
880            genesis_version = "0.1"
881
882            [stake_table]
883            capacity = 10
884
885            [chain_config]
886            chain_id = 12345
887            max_block_size = 30000
888            base_fee = 1
889            fee_recipient = "0x0000000000000000000000000000000000000000"
890
891            [header]
892            timestamp = 123456
893
894            [header.chain_config]
895            chain_id = 35353
896            max_block_size = 30720
897            base_fee = 0
898            fee_recipient = "0x0000000000000000000000000000000000000000"
899
900            [l1_finalized]
901            number = 42
902
903            [[upgrade]]
904            version = "0.2"
905            start_proposing_view = 5
906            stop_proposing_view = 15
907
908            [upgrade.fee]
909
910            [upgrade.fee.chain_config]
911            chain_id = 12345
912            max_block_size = 30000
913            base_fee = 1
914            fee_recipient = "0x0000000000000000000000000000000000000000"
915            fee_contract = "{proxy_addr:?}"
916
917
918        "#,
919        )
920        .to_string();
921
922        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
923
924        // Call the validation logic for the fee_contract address
925        let result = genesis
926            .validate_fee_contract(&L1Client::anvil(&anvil).unwrap())
927            .await;
928
929        assert!(
930            result.is_ok(),
931            "Expected Fee Contract to be a proxy, but it was not"
932        );
933        Ok(())
934    }
935
936    #[test_log::test(tokio::test(flavor = "multi_thread"))]
937    async fn test_genesis_missing_fee_contract_with_upgrades() {
938        let toml = toml! {
939            base_version = "0.1"
940            upgrade_version = "0.2"
941            genesis_version = "0.1"
942
943            [stake_table]
944            capacity = 10
945
946            [chain_config]
947            chain_id = 12345
948            max_block_size = 30000
949            base_fee = 1
950            fee_recipient = "0x0000000000000000000000000000000000000000"
951
952            [header]
953            timestamp = 123456
954
955            [header.chain_config]
956            chain_id = 35353
957            max_block_size = 30720
958            base_fee = 0
959            fee_recipient = "0x0000000000000000000000000000000000000000"
960
961            [l1_finalized]
962            number = 42
963
964            [[upgrade]]
965            version = "0.2"
966            start_proposing_view = 5
967            stop_proposing_view = 15
968
969            [upgrade.fee]
970
971            [upgrade.fee.chain_config]
972            chain_id = 12345
973            max_block_size = 30000
974            base_fee = 1
975            fee_recipient = "0x0000000000000000000000000000000000000000"
976
977            [[upgrade]]
978            version = "0.3"
979            start_proposing_view = 5
980            stop_proposing_view = 15
981
982            [upgrade.epoch]
983            [upgrade.epoch.chain_config]
984            chain_id = 999999999
985            max_block_size = 3000
986            base_fee = 1
987            fee_recipient = "0x0000000000000000000000000000000000000000"
988            bid_recipient = "0x0000000000000000000000000000000000000000"
989            fee_contract = "0xa15bb66138824a1c7167f5e85b957d04dd34e468" //not a proxy
990        }
991        .to_string();
992
993        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
994        let rpc_url = "https://ethereum-sepolia.publicnode.com";
995
996        // validate the fee_contract address
997        let result = genesis
998            .validate_fee_contract(&L1Client::new(vec![rpc_url.parse().unwrap()]).unwrap())
999            .await;
1000
1001        // check if the result from the validation is an error
1002        if let Err(e) = result {
1003            // assert that the error message contains "Fee contract's address is not a proxy"
1004            assert!(
1005                e.to_string()
1006                    .contains("Fee contract's address for the upgrade is missing")
1007            );
1008        } else {
1009            panic!("Expected the fee contract to be missing, but the validation succeeded");
1010        }
1011    }
1012
1013    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1014    async fn test_genesis_upgrade_fee_contract_address_is_zero() {
1015        let toml = toml! {
1016            base_version = "0.1"
1017            upgrade_version = "0.2"
1018            genesis_version = "0.1"
1019
1020            [stake_table]
1021            capacity = 10
1022
1023            [chain_config]
1024            chain_id = 12345
1025            max_block_size = 30000
1026            base_fee = 1
1027            fee_recipient = "0x0000000000000000000000000000000000000000"
1028
1029            [header]
1030            timestamp = 123456
1031
1032            [header.chain_config]
1033            chain_id = 35353
1034            max_block_size = 30720
1035            base_fee = 0
1036            fee_recipient = "0x0000000000000000000000000000000000000000"
1037
1038            [l1_finalized]
1039            number = 42
1040
1041            [[upgrade]]
1042            version = "0.2"
1043            start_proposing_view = 5
1044            stop_proposing_view = 15
1045
1046            [upgrade.fee]
1047            [upgrade.fee.chain_config]
1048            chain_id = 12345
1049            max_block_size = 30000
1050            base_fee = 1
1051            fee_recipient = "0x0000000000000000000000000000000000000000"
1052            fee_contract = "0x0000000000000000000000000000000000000000"
1053        }
1054        .to_string();
1055
1056        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
1057        let rpc_url = "https://ethereum-sepolia.publicnode.com";
1058
1059        // validate the fee_contract address
1060        let result = genesis
1061            .validate_fee_contract(&L1Client::new(vec![rpc_url.parse().unwrap()]).unwrap())
1062            .await;
1063
1064        // check if the result from the validation is an error
1065        if let Err(e) = result {
1066            // assert that the error message contains "Fee contract's address is not a proxy"
1067            assert!(
1068                e.to_string()
1069                    .contains("Fee contract cannot use the zero address")
1070            );
1071        } else {
1072            panic!(
1073                "Expected the fee contract to complain about the zero address but the validation \
1074                 succeeded"
1075            );
1076        }
1077    }
1078
1079    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1080    async fn test_genesis_fee_contract_l1_failover() -> anyhow::Result<()> {
1081        let anvil = Arc::new(Anvil::new().spawn());
1082        let wallet = anvil.wallet().unwrap();
1083        let admin = wallet.default_signer().address();
1084        let inner_provider = ProviderBuilder::new()
1085            .wallet(wallet)
1086            .connect_http(anvil.endpoint_url());
1087        let provider = AnvilProvider::new(inner_provider, Arc::clone(&anvil));
1088        let mut contracts = Contracts::new();
1089
1090        let proxy_addr =
1091            deployer::deploy_fee_contract_proxy(&provider, &mut contracts, admin).await?;
1092
1093        let toml = format!(
1094            r#"
1095            base_version = "0.1"
1096            upgrade_version = "0.2"
1097            genesis_version = "0.1"
1098
1099            [stake_table]
1100            capacity = 10
1101
1102            [chain_config]
1103            chain_id = 12345
1104            max_block_size = 30000
1105            base_fee = 1
1106            fee_recipient = "0x0000000000000000000000000000000000000000"
1107            fee_contract = "{proxy_addr:?}"
1108
1109            [header]
1110            timestamp = 123456
1111
1112            [header.chain_config]
1113            chain_id = 35353
1114            max_block_size = 30720
1115            base_fee = 0
1116            fee_recipient = "0x0000000000000000000000000000000000000000"
1117
1118            [l1_finalized]
1119            number = 42
1120        "#
1121        )
1122        .to_string();
1123
1124        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
1125        genesis
1126            .validate_fee_contract(
1127                &L1Client::new(vec![
1128                    "http://notareall1provider".parse().unwrap(),
1129                    anvil.endpoint().parse().unwrap(),
1130                ])
1131                .unwrap(),
1132            )
1133            .await
1134            .unwrap();
1135
1136        Ok(())
1137    }
1138
1139    #[test]
1140    fn test_genesis_from_toml_units() {
1141        let toml = toml! {
1142            base_version = "0.1"
1143            upgrade_version = "0.2"
1144            genesis_version = "0.1"
1145
1146            [stake_table]
1147            capacity = 10
1148
1149            [chain_config]
1150            chain_id = 12345
1151            max_block_size = "30mb"
1152            base_fee = "1 gwei"
1153            fee_recipient = "0x0000000000000000000000000000000000000000"
1154
1155            [header]
1156            timestamp = "2024-05-16T11:20:28-04:00"
1157
1158
1159
1160           [header.chain_config]
1161            chain_id = 35353
1162            max_block_size = 30720
1163            base_fee = 0
1164            fee_recipient = "0x0000000000000000000000000000000000000000"
1165
1166            [l1_finalized]
1167            number = 0
1168        }
1169        .to_string();
1170
1171        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
1172        assert_eq!(genesis.stake_table, StakeTableConfig { capacity: 10 });
1173        assert_eq!(*genesis.chain_config.max_block_size, 30000000);
1174        assert_eq!(genesis.chain_config.base_fee, 1_000_000_000.into());
1175        assert_eq!(
1176            genesis.header,
1177            GenesisHeader {
1178                timestamp: Timestamp::from_integer(1715872828).unwrap(),
1179                chain_config: ChainConfig::default(),
1180            }
1181        )
1182    }
1183
1184    #[test]
1185    fn test_genesis_toml_fee_upgrade_view_mode() {
1186        // without optional fields
1187        // with view settings
1188        let toml = toml! {
1189            base_version = "0.1"
1190            upgrade_version = "0.2"
1191            genesis_version = "0.1"
1192
1193            [stake_table]
1194            capacity = 10
1195
1196            [chain_config]
1197            chain_id = 12345
1198            max_block_size = 30000
1199            base_fee = 1
1200            fee_recipient = "0x0000000000000000000000000000000000000000"
1201            fee_contract = "0x0000000000000000000000000000000000000000"
1202
1203            [header]
1204            timestamp = 123456
1205
1206            [header.chain_config]
1207            chain_id = 35353
1208            max_block_size = 30720
1209            base_fee = 0
1210            fee_recipient = "0x0000000000000000000000000000000000000000"
1211
1212            [accounts]
1213            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
1214            "0x0000000000000000000000000000000000000000" = 42
1215
1216            [l1_finalized]
1217            number = 64
1218            timestamp = "0x123def"
1219            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
1220
1221            [[upgrade]]
1222            version = "0.2"
1223            start_proposing_view = 1
1224            stop_proposing_view = 15
1225
1226            [upgrade.fee]
1227
1228            [upgrade.fee.chain_config]
1229            chain_id = 12345
1230            max_block_size = 30000
1231            base_fee = 1
1232            fee_recipient = "0x0000000000000000000000000000000000000000"
1233            fee_contract = "0x0000000000000000000000000000000000000000"
1234        }
1235        .to_string();
1236
1237        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
1238
1239        let (version, genesis_upgrade) = genesis.upgrades.last_key_value().unwrap();
1240        println!("{genesis_upgrade:?}");
1241
1242        assert_eq!(*version, Version { major: 0, minor: 2 });
1243
1244        let upgrade = Upgrade {
1245            mode: UpgradeMode::View(ViewBasedUpgrade {
1246                start_voting_view: None,
1247                stop_voting_view: None,
1248                start_proposing_view: 1,
1249                stop_proposing_view: 15,
1250            }),
1251            upgrade_type: UpgradeType::Fee {
1252                chain_config: genesis.chain_config,
1253            },
1254        };
1255
1256        assert_eq!(*genesis_upgrade, upgrade);
1257    }
1258
1259    #[test]
1260    fn test_genesis_toml_large_block_upgrade_view_mode() {
1261        let toml = toml! {
1262            base_version = "0.6"
1263            upgrade_version = "0.7"
1264            genesis_version = "0.6"
1265
1266            [stake_table]
1267            capacity = 10
1268
1269            [chain_config]
1270            chain_id = 12345
1271            max_block_size = 30000
1272            base_fee = 1
1273            fee_recipient = "0x0000000000000000000000000000000000000000"
1274            fee_contract = "0x0000000000000000000000000000000000000000"
1275            stake_table_contract = "0x0000000000000000000000000000000000000000"
1276
1277            [header]
1278            timestamp = 123456
1279
1280            [header.chain_config]
1281            chain_id = 35353
1282            max_block_size = 30720
1283            base_fee = 0
1284            fee_recipient = "0x0000000000000000000000000000000000000000"
1285
1286            [accounts]
1287            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
1288            "0x0000000000000000000000000000000000000000" = 42
1289
1290            [l1_finalized]
1291            number = 64
1292            timestamp = "0x123def"
1293            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
1294
1295            [[upgrade]]
1296            version = "0.7"
1297            start_proposing_view = 1
1298            stop_proposing_view = 15
1299
1300            [upgrade.large_block]
1301
1302            [upgrade.large_block.chain_config]
1303            chain_id = 12345
1304            max_block_size = 10485760
1305            base_fee = 1
1306            fee_recipient = "0x0000000000000000000000000000000000000000"
1307            fee_contract = "0x0000000000000000000000000000000000000000"
1308            stake_table_contract = "0x0000000000000000000000000000000000000000"
1309        }
1310        .to_string();
1311
1312        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
1313
1314        let (version, genesis_upgrade) = genesis.upgrades.last_key_value().unwrap();
1315
1316        assert_eq!(*version, Version { major: 0, minor: 7 });
1317
1318        let UpgradeType::LargeBlock { chain_config } = genesis_upgrade.upgrade_type else {
1319            panic!("expected large_block upgrade, got {genesis_upgrade:?}");
1320        };
1321        assert_eq!(chain_config.max_block_size, (10 * 1024 * 1024).into());
1322
1323        let upgrade = Upgrade {
1324            mode: UpgradeMode::View(ViewBasedUpgrade {
1325                start_voting_view: None,
1326                stop_voting_view: None,
1327                start_proposing_view: 1,
1328                stop_proposing_view: 15,
1329            }),
1330            upgrade_type: UpgradeType::LargeBlock { chain_config },
1331        };
1332
1333        assert_eq!(*genesis_upgrade, upgrade);
1334    }
1335
1336    #[test]
1337    fn test_genesis_toml_fee_upgrade_time_mode() {
1338        // without optional fields
1339        // with time settings
1340        let toml = toml! {
1341            base_version = "0.1"
1342            upgrade_version = "0.2"
1343            genesis_version = "0.1"
1344
1345            [stake_table]
1346            capacity = 10
1347
1348            [chain_config]
1349            chain_id = 12345
1350            max_block_size = 30000
1351            base_fee = 1
1352            fee_recipient = "0x0000000000000000000000000000000000000000"
1353            fee_contract = "0x0000000000000000000000000000000000000000"
1354
1355            [header]
1356            timestamp = 123456
1357
1358            [header.chain_config]
1359            chain_id = 35353
1360            max_block_size = 30720
1361            base_fee = 0
1362            fee_recipient = "0x0000000000000000000000000000000000000000"
1363
1364            [accounts]
1365            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
1366            "0x0000000000000000000000000000000000000000" = 42
1367
1368            [l1_finalized]
1369            number = 64
1370            timestamp = "0x123def"
1371            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
1372
1373            [[upgrade]]
1374            version = "0.2"
1375            start_proposing_time = "2024-01-01T00:00:00Z"
1376            stop_proposing_time = "2024-01-02T00:00:00Z"
1377
1378            [upgrade.fee]
1379
1380            [upgrade.fee.chain_config]
1381            chain_id = 12345
1382            max_block_size = 30000
1383            base_fee = 1
1384            fee_recipient = "0x0000000000000000000000000000000000000000"
1385            fee_contract = "0x0000000000000000000000000000000000000000"
1386        }
1387        .to_string();
1388
1389        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
1390
1391        let (version, genesis_upgrade) = genesis.upgrades.last_key_value().unwrap();
1392
1393        assert_eq!(*version, Version { major: 0, minor: 2 });
1394
1395        let upgrade = Upgrade {
1396            mode: UpgradeMode::Time(TimeBasedUpgrade {
1397                start_voting_time: None,
1398                stop_voting_time: None,
1399                start_proposing_time: Timestamp::from_string("2024-01-01T00:00:00Z".to_string())
1400                    .unwrap(),
1401                stop_proposing_time: Timestamp::from_string("2024-01-02T00:00:00Z".to_string())
1402                    .unwrap(),
1403            }),
1404            upgrade_type: UpgradeType::Fee {
1405                chain_config: genesis.chain_config,
1406            },
1407        };
1408
1409        assert_eq!(*genesis_upgrade, upgrade);
1410    }
1411
1412    #[test]
1413    fn test_genesis_toml_fee_upgrade_view_and_time_mode() {
1414        // set both time and view parameters
1415        // this should err
1416        let toml = toml! {
1417            base_version = "0.1"
1418            upgrade_version = "0.2"
1419            genesis_version = "0.1"
1420
1421            [stake_table]
1422            capacity = 10
1423
1424            [chain_config]
1425            chain_id = 12345
1426            max_block_size = 30000
1427            base_fee = 1
1428            fee_recipient = "0x0000000000000000000000000000000000000000"
1429            fee_contract = "0x0000000000000000000000000000000000000000"
1430
1431            [header]
1432            timestamp = 123456
1433
1434            [header.chain_config]
1435            chain_id = 35353
1436            max_block_size = 30720
1437            base_fee = 0
1438            fee_recipient = "0x0000000000000000000000000000000000000000"
1439
1440            [accounts]
1441            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
1442            "0x0000000000000000000000000000000000000000" = 42
1443
1444            [l1_finalized]
1445            number = 64
1446            timestamp = "0x123def"
1447            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
1448
1449            [[upgrade]]
1450            version = "0.2"
1451            start_proposing_view = 1
1452            stop_proposing_view = 10
1453            start_proposing_time = 1
1454            stop_proposing_time = 10
1455
1456            [upgrade.fee]
1457
1458            [upgrade.fee.chain_config]
1459            chain_id = 12345
1460            max_block_size = 30000
1461            base_fee = 1
1462            fee_recipient = "0x0000000000000000000000000000000000000000"
1463            fee_contract = "0x0000000000000000000000000000000000000000"
1464        }
1465        .to_string();
1466
1467        toml::from_str::<Genesis>(&toml).unwrap_err();
1468    }
1469
1470    #[test]
1471    fn test_fee_and_epoch_upgrade_toml() {
1472        let toml = toml! {
1473            base_version = "0.1"
1474            upgrade_version = "0.2"
1475            genesis_version = "0.1"
1476            epoch_height = 20
1477            drb_difficulty = 10
1478            drb_upgrade_difficulty = 20
1479            epoch_start_block = 1
1480            stake_table_capacity = 200
1481
1482            [stake_table]
1483            capacity = 10
1484
1485            [chain_config]
1486            chain_id = 12345
1487            max_block_size = 30000
1488            base_fee = 1
1489            fee_recipient = "0x0000000000000000000000000000000000000000"
1490            fee_contract = "0x0000000000000000000000000000000000000000"
1491
1492            [header]
1493            timestamp = 123456
1494
1495         [header.chain_config]
1496            chain_id = 35353
1497            max_block_size = 30720
1498            base_fee = 0
1499            fee_recipient = "0x0000000000000000000000000000000000000000"
1500
1501            [accounts]
1502            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
1503            "0x0000000000000000000000000000000000000000" = 42
1504
1505            [l1_finalized]
1506            number = 64
1507            timestamp = "0x123def"
1508            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
1509
1510            [[upgrade]]
1511            version = "0.3"
1512            start_proposing_view = 1
1513            stop_proposing_view = 10
1514
1515            [upgrade.epoch]
1516            [upgrade.epoch.chain_config]
1517            chain_id = 12345
1518            max_block_size = 30000
1519            base_fee = 1
1520            fee_recipient = "0x0000000000000000000000000000000000000000"
1521            fee_contract = "0x0000000000000000000000000000000000000000"
1522            stake_table_contract = "0x0000000000000000000000000000000000000000"
1523
1524            [[upgrade]]
1525            version = "0.2"
1526            start_proposing_view = 1
1527            stop_proposing_view = 15
1528
1529            [upgrade.fee]
1530
1531            [upgrade.fee.chain_config]
1532            chain_id = 12345
1533            max_block_size = 30000
1534            base_fee = 1
1535            fee_recipient = "0x0000000000000000000000000000000000000000"
1536            fee_contract = "0x0000000000000000000000000000000000000000"
1537        }
1538        .to_string();
1539
1540        toml::from_str::<Genesis>(&toml).unwrap();
1541    }
1542
1543    #[test]
1544    fn test_genesis_chain_config() {
1545        let toml = toml! {
1546            base_version = "0.1"
1547            upgrade_version = "0.2"
1548            genesis_version = "0.2"
1549            epoch_height = 20
1550            drb_difficulty = 10
1551            drb_upgrade_difficulty = 20
1552            epoch_start_block = 1
1553            stake_table_capacity = 200
1554
1555            [stake_table]
1556            capacity = 10
1557
1558            [genesis_chain_config]
1559            chain_id = 33
1560            max_block_size = 5000
1561            base_fee = 1
1562            fee_recipient = "0x0000000000000000000000000000000000000000"
1563            fee_contract = "0x0000000000000000000000000000000000000000"
1564
1565            [chain_config]
1566            chain_id = 12345
1567            max_block_size = 30000
1568            base_fee = 1
1569            fee_recipient = "0x0000000000000000000000000000000000000000"
1570            fee_contract = "0x0000000000000000000000000000000000000000"
1571
1572            [header]
1573            timestamp = 123456
1574
1575            [header.chain_config]
1576            chain_id = 33
1577            max_block_size = 5000
1578            base_fee = 1
1579            fee_recipient = "0x0000000000000000000000000000000000000000"
1580            fee_contract = "0x0000000000000000000000000000000000000000"
1581
1582            [accounts]
1583            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
1584            "0x0000000000000000000000000000000000000000" = 42
1585
1586            [l1_finalized]
1587            number = 64
1588            timestamp = "0x123def"
1589            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
1590
1591            [[upgrade]]
1592            version = "0.3"
1593            start_proposing_view = 1
1594            stop_proposing_view = 10
1595
1596            [upgrade.epoch]
1597            [upgrade.epoch.chain_config]
1598            chain_id = 12345
1599            max_block_size = 30000
1600            base_fee = 1
1601            fee_recipient = "0x0000000000000000000000000000000000000000"
1602            fee_contract = "0x0000000000000000000000000000000000000000"
1603            stake_table_contract = "0x0000000000000000000000000000000000000000"
1604
1605            [[upgrade]]
1606            version = "0.2"
1607            start_proposing_view = 1
1608            stop_proposing_view = 15
1609
1610            [upgrade.fee]
1611
1612            [upgrade.fee.chain_config]
1613            chain_id = 12345
1614            max_block_size = 30000
1615            base_fee = 1
1616            fee_recipient = "0x0000000000000000000000000000000000000000"
1617            fee_contract = "0x0000000000000000000000000000000000000000"
1618        }
1619        .to_string();
1620
1621        let genesis = toml::from_str::<Genesis>(&toml).unwrap();
1622
1623        assert_eq!(genesis.header.chain_config.chain_id, 33.into());
1624        assert_eq!(genesis.chain_config.chain_id, 12345.into());
1625
1626        assert_eq!(genesis.header.chain_config.max_block_size, 5000.into());
1627        assert_eq!(genesis.chain_config.max_block_size, 30000.into());
1628    }
1629
1630    #[test]
1631    fn test_genesis_da_committees() {
1632        let toml = toml! {
1633            base_version = "0.1"
1634            upgrade_version = "0.5"
1635            genesis_version = "0.1"
1636            epoch_height = 20
1637            drb_difficulty = 10
1638            drb_upgrade_difficulty = 20
1639            epoch_start_block = 1
1640            stake_table_capacity = 200
1641
1642            [stake_table]
1643            capacity = 10
1644
1645            [chain_config]
1646            chain_id = 12345
1647            max_block_size = 30000
1648            base_fee = 1
1649            fee_recipient = "0x0000000000000000000000000000000000000000"
1650            fee_contract = "0x0000000000000000000000000000000000000000"
1651
1652            [l1_finalized]
1653            number = 64
1654            timestamp = "0x123def"
1655            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
1656
1657            [header]
1658            timestamp = 123456
1659
1660            [header.chain_config]
1661            chain_id = 33
1662            max_block_size = 5000
1663            base_fee = 1
1664            fee_recipient = "0x0000000000000000000000000000000000000000"
1665            fee_contract = "0x0000000000000000000000000000000000000000"
1666
1667            [[upgrade]]
1668            version = "0.5"
1669            start_proposing_view = 1
1670            stop_proposing_view = 15
1671
1672            [upgrade.new_protocol]
1673            [upgrade.new_protocol.chain_config]
1674            chain_id = 12345
1675            max_block_size = 30000
1676            base_fee = 1
1677            fee_recipient = "0x0000000000000000000000000000000000000000"
1678            fee_contract = "0x0000000000000000000000000000000000000000"
1679            stake_table_contract = "0x0000000000000000000000000000000000000000"
1680
1681            [[da_committees]]
1682            start_version = "0.6"
1683            start_epoch = 10
1684            committee = [
1685                { stake_table_entry = { stake_key = "BLS_VER_KEY~bQszS-QKYvUij2g20VqS8asttGSb95NrTu2PUj0uMh1CBUxNy1FqyPDjZqB29M7ZbjWqj79QkEOWkpga84AmDYUeTuWmy-0P1AdKHD3ehc-dKvei78BDj5USwXPJiDUlCxvYs_9rWYhagaq-5_LXENr78xel17spftNd5MA1Mw5U", stake_amount = "0x1"}, state_ver_key = "SCHNORR_VER_KEY~lJqDaVZyM0hWP2Br52IX5FeE-dCAIC-dPX7bL5-qUx-vjbunwe-ENOeZxj6FuOyvDCFzoGeP7yZ0fM995qF-CRE"},
1686                { stake_table_entry = { stake_key = "BLS_VER_KEY~bQszS-QKYvUij2g20VqS8asttGSb95NrTu2PUj0uMh1CBUxNy1FqyPDjZqB29M7ZbjWqj79QkEOWkpga84AmDYUeTuWmy-0P1AdKHD3ehc-dKvei78BDj5USwXPJiDUlCxvYs_9rWYhagaq-5_LXENr78xel17spftNd5MA1Mw5U", stake_amount = "0x1"}, state_ver_key = "SCHNORR_VER_KEY~lJqDaVZyM0hWP2Br52IX5FeE-dCAIC-dPX7bL5-qUx-vjbunwe-ENOeZxj6FuOyvDCFzoGeP7yZ0fM995qF-CRE"}
1687            ]
1688        }
1689        .to_string();
1690
1691        let genesis = toml::from_str::<Genesis>(&toml).unwrap();
1692
1693        let da_committees = genesis
1694            .da_committees
1695            .expect("DA committees should be present");
1696        assert_eq!(da_committees.len(), 1);
1697
1698        let da_committee = &da_committees[0];
1699
1700        assert_eq!(da_committee.start_version, Version { major: 0, minor: 6 });
1701        assert_eq!(da_committee.start_epoch, 10);
1702        assert_eq!(da_committee.committee.len(), 2);
1703        assert_eq!(
1704            da_committee.committee[0].stake_table_entry.stake_amount,
1705            U256::from(1)
1706        );
1707        assert_eq!(
1708            da_committee.committee[1].stake_table_entry.stake_amount,
1709            U256::from(1)
1710        );
1711    }
1712
1713    /// Verify that every BLS / Schnorr public key referenced in
1714    /// `data/genesis/demo-da-committees.toml` is derived from `DEV_MNEMONIC` at index
1715    /// `DEMO_VALIDATOR_START_INDEX + N`. Otherwise the live demo nodes (which use those
1716    /// mnemonic-derived keys) cannot sign for the genesis-defined DA committees, and the chain
1717    /// stalls at the first DA-committee epoch transition.
1718    #[test]
1719    fn demo_da_committees_match_dev_mnemonic() {
1720        use std::collections::HashSet;
1721
1722        use alloy::signers::local::coins_bip39::{English, Mnemonic};
1723        use espresso_keyset::{KeySet, KeySetOptions};
1724        use hotshot_types::{
1725            light_client::StateKeyPair,
1726            signature_key::{BLSKeyPair, BLSPubKey, SchnorrPubKey},
1727        };
1728        use staking_cli::{DEMO_VALIDATOR_START_INDEX, DEV_MNEMONIC};
1729
1730        let mnemonic = Mnemonic::<English>::new_from_phrase(DEV_MNEMONIC).unwrap();
1731        let mut expected_bls: HashSet<BLSPubKey> = HashSet::new();
1732        let mut expected_schnorr: HashSet<SchnorrPubKey> = HashSet::new();
1733        for val_index in 0..5u64 {
1734            let keyset = KeySet::try_from(KeySetOptions {
1735                mnemonic: Some(mnemonic.clone()),
1736                index: Some(u64::from(DEMO_VALIDATOR_START_INDEX) + val_index),
1737                key_file: None,
1738                private_staking_key: None,
1739                private_state_key: None,
1740                private_x25519_key: None,
1741            })
1742            .unwrap();
1743            expected_bls.insert(BLSKeyPair::from(keyset.staking).ver_key());
1744            expected_schnorr.insert(StateKeyPair::from_sign_key(keyset.state).ver_key());
1745        }
1746
1747        let path = Path::new(env!("CARGO_MANIFEST_DIR"))
1748            .join("../../../data/genesis/demo-da-committees.toml");
1749        let genesis = Genesis::from_file(&path).unwrap();
1750        let da_committees = genesis.da_committees.expect("da_committees in genesis");
1751        assert!(!da_committees.is_empty());
1752        for committee in &da_committees {
1753            for entry in &committee.committee {
1754                assert!(
1755                    expected_bls.contains(&entry.stake_table_entry.stake_key),
1756                    "{path:?} epoch {} references a BLS key not derived from DEV_MNEMONIC at \
1757                     indices {DEMO_VALIDATOR_START_INDEX}..{}",
1758                    committee.start_epoch,
1759                    u64::from(DEMO_VALIDATOR_START_INDEX) + 5,
1760                );
1761                assert!(
1762                    expected_schnorr.contains(&entry.state_ver_key),
1763                    "{path:?} epoch {} references a Schnorr key not derived from DEV_MNEMONIC at \
1764                     indices {DEMO_VALIDATOR_START_INDEX}..{}",
1765                    committee.start_epoch,
1766                    u64::from(DEMO_VALIDATOR_START_INDEX) + 5,
1767                );
1768            }
1769        }
1770    }
1771
1772    /// A genesis with the given epoch fields. The other fields do not matter.
1773    fn genesis_with_epochs(epoch_height: Option<u64>, epoch_start_block: Option<u64>) -> Genesis {
1774        let mut genesis: Genesis = toml::from_str(&minimal_genesis_toml("0.4", "")).unwrap();
1775        genesis.epoch_height = epoch_height;
1776        genesis.epoch_start_block = epoch_start_block;
1777        genesis
1778    }
1779
1780    // The first epoch is the epoch that holds `epoch_start_block`. Epochs are
1781    // numbered from 1: block 0 is in epoch 1, and block b > 0 is in epoch
1782    // ceil(b / epoch_height).
1783    #[rstest::rstest]
1784    #[case::mainnet(40000, Some(10960201), 275)]
1785    #[case::decaf(3000, Some(3160636), 1054)]
1786    #[case::last_block_of_epoch(300, Some(600), 2)]
1787    #[case::first_block_of_epoch(300, Some(601), 3)]
1788    #[case::block_one(300, Some(1), 1)]
1789    #[case::block_zero(300, Some(0), 1)]
1790    #[case::start_block_missing(300, None, 1)]
1791    fn first_epoch_holds_epoch_start_block(
1792        #[case] epoch_height: u64,
1793        #[case] epoch_start_block: Option<u64>,
1794        #[case] expected: u64,
1795    ) {
1796        let genesis = genesis_with_epochs(Some(epoch_height), epoch_start_block);
1797        assert_eq!(
1798            genesis.first_epoch().unwrap(),
1799            EpochNumber::new(expected),
1800            "epoch_height={epoch_height}, epoch_start_block={epoch_start_block:?}"
1801        );
1802    }
1803
1804    #[rstest::rstest]
1805    #[case::missing(None)]
1806    #[case::zero(Some(0))]
1807    fn first_epoch_needs_nonzero_epoch_height(#[case] epoch_height: Option<u64>) {
1808        let genesis = genesis_with_epochs(epoch_height, Some(600));
1809        assert!(
1810            genesis.first_epoch().is_err(),
1811            "epoch_height={epoch_height:?} must be rejected"
1812        );
1813    }
1814
1815    #[rstest::rstest]
1816    #[case::mainnet("mainnet.toml", 275)]
1817    #[case::decaf("decaf.toml", 1054)]
1818    fn first_epoch_of_committed_genesis_files(#[case] file: &str, #[case] expected: u64) {
1819        let path = Path::new(env!("CARGO_MANIFEST_DIR"))
1820            .join("../../../data/genesis")
1821            .join(file);
1822        let genesis = Genesis::from_file(&path).unwrap();
1823        assert_eq!(genesis.first_epoch().unwrap(), EpochNumber::new(expected));
1824    }
1825}