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