Skip to main content

espresso_node/
genesis.rs

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