sequencer/
genesis.rs

1use std::{
2    collections::{BTreeMap, HashMap},
3    path::Path,
4};
5
6use alloy::primitives::Address;
7use anyhow::{Context, Ok};
8use espresso_types::{
9    v0_3::ChainConfig, FeeAccount, FeeAmount, GenesisHeader, L1BlockInfo, L1Client, Timestamp,
10    Upgrade,
11};
12use serde::{Deserialize, Serialize};
13use vbs::version::Version;
14
15/// Initial configuration of an Espresso stake table.
16#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
17pub struct StakeTableConfig {
18    pub capacity: usize,
19}
20
21/// An L1 block from which an Espresso chain should start syncing.
22#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
23#[serde(untagged)]
24pub enum L1Finalized {
25    /// Complete block info.
26    ///
27    /// This allows a validator to specify the exact, existing L1 block to start syncing from. A
28    /// validator that specifies a specific L1 block will not be able to reach consensus with a
29    /// malicious validator that starts from a different L1 block.
30    Block(L1BlockInfo),
31
32    /// An L1 block number to sync from.
33    ///
34    /// This allows a validator to specify a future L1 block whose hash is not yet known, and start
35    /// syncing only when a finalized block with the given number becomes available. The configured
36    /// L1 client will be used to fetch the rest of the block info once available.
37    Number { number: u64 },
38
39    /// A time from which to start syncing L1 blocks.
40    ///
41    /// This allows a validator to specify a future time at which the network should start. The
42    /// network will start syncing from the first L1 block with timestamp greater than or equal to
43    /// this, once said block is finalized.
44    Timestamp { timestamp: Timestamp },
45}
46
47/// Genesis of an Espresso chain.
48#[derive(Clone, Debug, Deserialize, Serialize)]
49pub struct Genesis {
50    #[serde(with = "version_ser")]
51    pub base_version: Version,
52    #[serde(with = "version_ser")]
53    pub upgrade_version: Version,
54    #[serde(with = "version_ser")]
55    pub genesis_version: Version,
56    pub epoch_height: Option<u64>,
57    pub drb_difficulty: Option<u64>,
58    pub drb_upgrade_difficulty: Option<u64>,
59    pub epoch_start_block: Option<u64>,
60    pub stake_table_capacity: Option<usize>,
61    pub chain_config: ChainConfig,
62    pub stake_table: StakeTableConfig,
63    #[serde(default)]
64    pub accounts: HashMap<FeeAccount, FeeAmount>,
65    pub l1_finalized: L1Finalized,
66    pub header: GenesisHeader,
67    #[serde(rename = "upgrade", with = "upgrade_ser")]
68    #[serde(default)]
69    pub upgrades: BTreeMap<Version, Upgrade>,
70}
71
72impl Genesis {
73    pub fn max_base_fee(&self) -> FeeAmount {
74        let mut base_fee = self.chain_config.base_fee;
75
76        let upgrades: Vec<&Upgrade> = self.upgrades.values().collect();
77
78        for upgrade in upgrades {
79            let chain_config = upgrade.upgrade_type.chain_config();
80
81            if let Some(cf) = chain_config {
82                base_fee = std::cmp::max(cf.base_fee, base_fee);
83            }
84        }
85
86        base_fee
87    }
88}
89
90impl Genesis {
91    pub async fn validate_fee_contract(&self, l1: &L1Client) -> anyhow::Result<()> {
92        if let Some(fee_contract_address) = self.chain_config.fee_contract {
93            tracing::info!("validating fee contract at {fee_contract_address:x}");
94
95            if !l1
96                .retry_on_all_providers(|| l1.is_proxy_contract(fee_contract_address))
97                .await
98                .context("checking if fee contract is a proxy")?
99            {
100                anyhow::bail!("Fee contract address {fee_contract_address:x} is not a proxy");
101            }
102        }
103
104        // now iterate over each upgrade type and validate the fee contract if it exists
105        for (version, upgrade) in &self.upgrades {
106            let chain_config = &upgrade.upgrade_type.chain_config();
107
108            if chain_config.is_none() {
109                continue;
110            }
111
112            let chain_config = chain_config.unwrap();
113
114            if let Some(fee_contract_address) = chain_config.fee_contract {
115                if fee_contract_address == Address::default() {
116                    anyhow::bail!("Fee contract cannot use the zero address");
117                } else if !l1
118                    .retry_on_all_providers(|| l1.is_proxy_contract(fee_contract_address))
119                    .await
120                    .context(format!(
121                        "checking if fee contract is a proxy in upgrade {version}",
122                    ))?
123                {
124                    anyhow::bail!("Fee contract's address is not a proxy");
125                }
126            } else {
127                // The Fee Contract address has to be provided for an upgrade so return an error
128                anyhow::bail!("Fee contract's address for the upgrade is missing");
129            }
130        }
131        // 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-sequencer/pull/2000#discussion_r1765174702
132        Ok(())
133    }
134}
135
136mod version_ser {
137
138    use serde::{de, Deserialize, Deserializer, Serializer};
139    use vbs::version::Version;
140
141    pub fn serialize<S>(ver: &Version, serializer: S) -> Result<S::Ok, S::Error>
142    where
143        S: Serializer,
144    {
145        serializer.serialize_str(&ver.to_string())
146    }
147
148    pub fn deserialize<'de, D>(deserializer: D) -> Result<Version, D::Error>
149    where
150        D: Deserializer<'de>,
151    {
152        let version_str = String::deserialize(deserializer)?;
153
154        let version: Vec<_> = version_str.split('.').collect();
155
156        let version = Version {
157            major: version[0]
158                .parse()
159                .map_err(|_| de::Error::custom("invalid version format"))?,
160            minor: version[1]
161                .parse()
162                .map_err(|_| de::Error::custom("invalid version format"))?,
163        };
164
165        Ok(version)
166    }
167}
168
169mod upgrade_ser {
170
171    use std::{collections::BTreeMap, fmt};
172
173    use espresso_types::{
174        v0_1::{TimeBasedUpgrade, UpgradeMode, ViewBasedUpgrade},
175        Upgrade, UpgradeType,
176    };
177    use serde::{
178        de::{self, SeqAccess, Visitor},
179        ser::SerializeSeq,
180        Deserialize, Deserializer, Serialize, Serializer,
181    };
182    use vbs::version::Version;
183
184    pub fn serialize<S>(map: &BTreeMap<Version, Upgrade>, serializer: S) -> Result<S::Ok, S::Error>
185    where
186        S: Serializer,
187    {
188        #[derive(Debug, Clone, Serialize, Deserialize)]
189        pub struct Fields {
190            pub version: String,
191            #[serde(flatten)]
192            pub mode: UpgradeMode,
193            #[serde(flatten)]
194            pub upgrade_type: UpgradeType,
195        }
196
197        let mut seq = serializer.serialize_seq(Some(map.len()))?;
198        for (version, upgrade) in map {
199            seq.serialize_element(&Fields {
200                version: version.to_string(),
201                mode: upgrade.mode.clone(),
202                upgrade_type: upgrade.upgrade_type.clone(),
203            })?
204        }
205        seq.end()
206    }
207
208    pub fn deserialize<'de, D>(deserializer: D) -> Result<BTreeMap<Version, Upgrade>, D::Error>
209    where
210        D: Deserializer<'de>,
211    {
212        struct VecToHashMap;
213
214        #[derive(Debug, Clone, Serialize, Deserialize)]
215        pub struct Fields {
216            pub version: String,
217            // If both `time_based` and `view_based` fields are provided
218            // and we use an enum for deserialization, then one of the variant fields will be ignored.
219            // We want to raise an error in such a case to avoid ambiguity
220            #[serde(flatten)]
221            pub time_based: Option<TimeBasedUpgrade>,
222            #[serde(flatten)]
223            pub view_based: Option<ViewBasedUpgrade>,
224            #[serde(flatten)]
225            pub upgrade_type: UpgradeType,
226        }
227
228        impl<'de> Visitor<'de> for VecToHashMap {
229            type Value = BTreeMap<Version, Upgrade>;
230
231            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
232                formatter.write_str("a vector of tuples (key-value pairs)")
233            }
234
235            fn visit_seq<A>(self, mut seq: A) -> Result<BTreeMap<Version, Upgrade>, A::Error>
236            where
237                A: SeqAccess<'de>,
238            {
239                let mut map = BTreeMap::new();
240
241                while let Some(fields) = seq.next_element::<Fields>()? {
242                    // add try_from in Version
243                    let version: Vec<_> = fields.version.split('.').collect();
244
245                    let version = Version {
246                        major: version[0]
247                            .parse()
248                            .map_err(|_| de::Error::custom("invalid version format"))?,
249                        minor: version[1]
250                            .parse()
251                            .map_err(|_| de::Error::custom("invalid version format"))?,
252                    };
253
254                    match (fields.time_based, fields.view_based) {
255                        (Some(_), Some(_)) => {
256                            return Err(de::Error::custom(
257                                "both view and time mode parameters are set",
258                            ))
259                        },
260                        (None, None) => {
261                            return Err(de::Error::custom(
262                                "no view or time mode parameters provided",
263                            ))
264                        },
265                        (None, Some(v)) => {
266                            if v.start_proposing_view > v.stop_proposing_view {
267                                return Err(de::Error::custom(
268                                    "stop_proposing_view is less than start_proposing_view",
269                                ));
270                            }
271
272                            map.insert(
273                                version,
274                                Upgrade {
275                                    mode: UpgradeMode::View(v),
276                                    upgrade_type: fields.upgrade_type,
277                                },
278                            );
279                        },
280                        (Some(t), None) => {
281                            if t.start_proposing_time.unix_timestamp()
282                                > t.stop_proposing_time.unix_timestamp()
283                            {
284                                return Err(de::Error::custom(
285                                    "stop_proposing_time is less than start_proposing_time",
286                                ));
287                            }
288
289                            map.insert(
290                                version,
291                                Upgrade {
292                                    mode: UpgradeMode::Time(t),
293                                    upgrade_type: fields.upgrade_type.clone(),
294                                },
295                            );
296                        },
297                    }
298                }
299
300                Ok(map)
301            }
302        }
303
304        deserializer.deserialize_seq(VecToHashMap)
305    }
306}
307
308impl Genesis {
309    pub fn to_file(&self, path: impl AsRef<Path>) -> anyhow::Result<()> {
310        let toml = toml::to_string_pretty(self)?;
311        std::fs::write(path, toml.as_bytes())?;
312        Ok(())
313    }
314
315    pub fn from_file(path: impl AsRef<Path>) -> anyhow::Result<Self> {
316        let path = path.as_ref();
317        let bytes = std::fs::read(path).context(format!("genesis file {}", path.display()))?;
318        let text = std::str::from_utf8(&bytes).context("genesis file must be UTF-8")?;
319
320        toml::from_str(text).context("malformed genesis file")
321    }
322}
323
324#[cfg(test)]
325mod test {
326    use std::sync::Arc;
327
328    use alloy::{
329        node_bindings::Anvil,
330        primitives::{B256, U256},
331        providers::{layers::AnvilProvider, ProviderBuilder},
332    };
333    use espresso_contract_deployer::{self as deployer, Contracts};
334    use espresso_types::{
335        L1BlockInfo, TimeBasedUpgrade, Timestamp, UpgradeMode, UpgradeType, ViewBasedUpgrade,
336    };
337    use sequencer_utils::ser::FromStringOrInteger;
338    use toml::toml;
339
340    use super::*;
341
342    #[test]
343    fn test_genesis_from_toml_with_optional_fields() {
344        let toml = toml! {
345            base_version = "0.1"
346            upgrade_version = "0.2"
347            genesis_version = "0.1"
348
349            [stake_table]
350            capacity = 10
351
352            [chain_config]
353            chain_id = 12345
354            max_block_size = 30000
355            base_fee = 1
356            fee_recipient = "0x0000000000000000000000000000000000000000"
357            fee_contract = "0x0000000000000000000000000000000000000000"
358
359            [header]
360            timestamp = 123456
361
362            [accounts]
363            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
364            "0x0000000000000000000000000000000000000000" = 42
365
366            [l1_finalized]
367            number = 64
368            timestamp = "0x123def"
369            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
370        }
371        .to_string();
372
373        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
374        assert_eq!(genesis.genesis_version, Version { major: 0, minor: 1 });
375        assert_eq!(genesis.stake_table, StakeTableConfig { capacity: 10 });
376        assert_eq!(
377            genesis.chain_config,
378            ChainConfig {
379                chain_id: 12345.into(),
380                max_block_size: 30000.into(),
381                base_fee: 1.into(),
382                fee_recipient: FeeAccount::default(),
383                fee_contract: Some(Address::default()),
384                stake_table_contract: None
385            }
386        );
387        assert_eq!(
388            genesis.header,
389            GenesisHeader {
390                timestamp: Timestamp::from_integer(123456).unwrap(),
391            }
392        );
393        assert_eq!(
394            genesis.accounts,
395            [
396                (
397                    FeeAccount::from(Address::from([
398                        0x23, 0x61, 0x8e, 0x81, 0xe3, 0xf5, 0xcd, 0xf7, 0xf5, 0x4c, 0x3d, 0x65,
399                        0xf7, 0xfb, 0xc0, 0xab, 0xf5, 0xb2, 0x1e, 0x8f
400                    ])),
401                    100000.into()
402                ),
403                (FeeAccount::default(), 42.into())
404            ]
405            .into_iter()
406            .collect::<HashMap<_, _>>()
407        );
408        assert_eq!(
409            genesis.l1_finalized,
410            L1Finalized::Block(L1BlockInfo {
411                number: 64,
412                timestamp: U256::from(0x123def),
413                // Can't do B256 here directly because it's the wrong endianness
414                hash: B256::from([
415                    0x80, 0xf5, 0xdd, 0x11, 0xf2, 0xbd, 0xda, 0x28, 0x14, 0xcb, 0x1a, 0xd9, 0x4e,
416                    0xf3, 0x0a, 0x47, 0xde, 0x02, 0xcf, 0x28, 0xad, 0x68, 0xc8, 0x9e, 0x10, 0x4c,
417                    0x00, 0xc4, 0xe5, 0x1b, 0xb7, 0xa5
418                ])
419            })
420        );
421    }
422
423    #[test]
424    fn test_genesis_from_toml_without_optional_fields() {
425        let toml = toml! {
426            base_version = "0.1"
427            upgrade_version = "0.2"
428            genesis_version = "0.1"
429
430            [stake_table]
431            capacity = 10
432
433            [chain_config]
434            chain_id = 12345
435            max_block_size = 30000
436            base_fee = 1
437            fee_recipient = "0x0000000000000000000000000000000000000000"
438
439            [header]
440            timestamp = 123456
441
442            [l1_finalized]
443            number = 0
444        }
445        .to_string();
446
447        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
448
449        assert_eq!(genesis.stake_table, StakeTableConfig { capacity: 10 });
450        assert_eq!(
451            genesis.chain_config,
452            ChainConfig {
453                chain_id: 12345.into(),
454                max_block_size: 30000.into(),
455                base_fee: 1.into(),
456                fee_recipient: FeeAccount::default(),
457                fee_contract: None,
458                stake_table_contract: None,
459            }
460        );
461        assert_eq!(
462            genesis.header,
463            GenesisHeader {
464                timestamp: Timestamp::from_integer(123456).unwrap(),
465            }
466        );
467        assert_eq!(genesis.accounts, HashMap::default());
468        assert_eq!(genesis.l1_finalized, L1Finalized::Number { number: 0 });
469    }
470
471    #[test]
472    fn test_genesis_l1_finalized_number_only() {
473        let toml = toml! {
474            base_version = "0.1"
475            upgrade_version = "0.2"
476            genesis_version = "0.1"
477
478            [stake_table]
479            capacity = 10
480
481            [chain_config]
482            chain_id = 12345
483            max_block_size = 30000
484            base_fee = 1
485            fee_recipient = "0x0000000000000000000000000000000000000000"
486
487            [header]
488            timestamp = 123456
489
490            [l1_finalized]
491            number = 42
492        }
493        .to_string();
494
495        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
496        assert_eq!(genesis.l1_finalized, L1Finalized::Number { number: 42 });
497    }
498
499    #[test]
500    fn test_genesis_l1_finalized_timestamp_only() {
501        let toml = toml! {
502            base_version = "0.1"
503            upgrade_version = "0.2"
504            genesis_version = "0.1"
505
506            [stake_table]
507            capacity = 10
508
509            [chain_config]
510            chain_id = 12345
511            max_block_size = 30000
512            base_fee = 1
513            fee_recipient = "0x0000000000000000000000000000000000000000"
514
515            [header]
516            timestamp = 123456
517
518            [l1_finalized]
519            timestamp = "2024-01-02T00:00:00Z"
520        }
521        .to_string();
522
523        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
524        assert_eq!(
525            genesis.l1_finalized,
526            L1Finalized::Timestamp {
527                timestamp: Timestamp::from_string("2024-01-02T00:00:00Z".to_string()).unwrap()
528            }
529        );
530    }
531
532    // tests for fee contract not being a proxy are removed, since we now only have one function in `deployer.rs` that ensures
533    // deploying of the fee contract behind proxy, and this function is being unit tested there.
534    // Here, we primarily focus on testing the config and validation logic, not deployment logic.
535
536    #[test_log::test(tokio::test(flavor = "multi_thread"))]
537    async fn test_genesis_fee_contract_is_a_proxy() -> anyhow::Result<()> {
538        let anvil = Arc::new(Anvil::new().spawn());
539        let wallet = anvil.wallet().unwrap();
540        let admin = wallet.default_signer().address();
541        let inner_provider = ProviderBuilder::new()
542            .wallet(wallet)
543            .on_http(anvil.endpoint_url());
544        let provider = AnvilProvider::new(inner_provider, Arc::clone(&anvil));
545        let mut contracts = Contracts::new();
546
547        let proxy_addr =
548            deployer::deploy_fee_contract_proxy(&provider, &mut contracts, admin).await?;
549
550        let toml = format!(
551            r#"
552            base_version = "0.1"
553            upgrade_version = "0.2"
554            genesis_version = "0.1"
555
556            [stake_table]
557            capacity = 10
558
559            [chain_config]
560            chain_id = 12345
561            max_block_size = 30000
562            base_fee = 1
563            fee_recipient = "0x0000000000000000000000000000000000000000"
564            fee_contract = "{proxy_addr:?}"
565
566            [header]
567            timestamp = 123456
568
569            [l1_finalized]
570            number = 42
571        "#,
572        )
573        .to_string();
574
575        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
576
577        // Call the validation logic for the fee_contract address
578        let result = genesis
579            .validate_fee_contract(&L1Client::anvil(&anvil).unwrap())
580            .await;
581
582        assert!(
583            result.is_ok(),
584            "Expected Fee Contract to be a proxy, but it was not"
585        );
586        Ok(())
587    }
588
589    #[test_log::test(tokio::test(flavor = "multi_thread"))]
590    async fn test_genesis_fee_contract_is_a_proxy_with_upgrades() -> anyhow::Result<()> {
591        let anvil = Arc::new(Anvil::new().spawn());
592        let wallet = anvil.wallet().unwrap();
593        let admin = wallet.default_signer().address();
594        let inner_provider = ProviderBuilder::new()
595            .wallet(wallet)
596            .on_http(anvil.endpoint_url());
597        let provider = AnvilProvider::new(inner_provider, Arc::clone(&anvil));
598        let mut contracts = Contracts::new();
599
600        let proxy_addr =
601            deployer::deploy_fee_contract_proxy(&provider, &mut contracts, admin).await?;
602
603        let toml = format!(
604            r#"
605            base_version = "0.1"
606            upgrade_version = "0.2"
607            genesis_version = "0.1"
608
609            [stake_table]
610            capacity = 10
611
612            [chain_config]
613            chain_id = 12345
614            max_block_size = 30000
615            base_fee = 1
616            fee_recipient = "0x0000000000000000000000000000000000000000"
617
618            [header]
619            timestamp = 123456
620
621            [l1_finalized]
622            number = 42
623
624            [[upgrade]]
625            version = "0.2"
626            start_proposing_view = 5
627            stop_proposing_view = 15
628
629            [upgrade.fee]
630
631            [upgrade.fee.chain_config]
632            chain_id = 12345
633            max_block_size = 30000
634            base_fee = 1
635            fee_recipient = "0x0000000000000000000000000000000000000000"
636            fee_contract = "{proxy_addr:?}"
637
638           
639        "#,
640        )
641        .to_string();
642
643        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
644
645        // Call the validation logic for the fee_contract address
646        let result = genesis
647            .validate_fee_contract(&L1Client::anvil(&anvil).unwrap())
648            .await;
649
650        assert!(
651            result.is_ok(),
652            "Expected Fee Contract to be a proxy, but it was not"
653        );
654        Ok(())
655    }
656
657    #[test_log::test(tokio::test(flavor = "multi_thread"))]
658    async fn test_genesis_missing_fee_contract_with_upgrades() {
659        let toml = toml! {
660            base_version = "0.1"
661            upgrade_version = "0.2"
662            genesis_version = "0.1"
663
664            [stake_table]
665            capacity = 10
666
667            [chain_config]
668            chain_id = 12345
669            max_block_size = 30000
670            base_fee = 1
671            fee_recipient = "0x0000000000000000000000000000000000000000"
672
673            [header]
674            timestamp = 123456
675
676            [l1_finalized]
677            number = 42
678
679            [[upgrade]]
680            version = "0.2"
681            start_proposing_view = 5
682            stop_proposing_view = 15
683
684            [upgrade.fee]
685
686            [upgrade.fee.chain_config]
687            chain_id = 12345
688            max_block_size = 30000
689            base_fee = 1
690            fee_recipient = "0x0000000000000000000000000000000000000000"
691
692            [[upgrade]]
693            version = "0.3"
694            start_proposing_view = 5
695            stop_proposing_view = 15
696
697            [upgrade.epoch]
698            [upgrade.epoch.chain_config]
699            chain_id = 999999999
700            max_block_size = 3000
701            base_fee = 1
702            fee_recipient = "0x0000000000000000000000000000000000000000"
703            bid_recipient = "0x0000000000000000000000000000000000000000"
704            fee_contract = "0xa15bb66138824a1c7167f5e85b957d04dd34e468" //not a proxy
705        }
706        .to_string();
707
708        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
709        let rpc_url = "https://ethereum-sepolia.publicnode.com";
710
711        // validate the fee_contract address
712        let result = genesis
713            .validate_fee_contract(&L1Client::new(vec![rpc_url.parse().unwrap()]).unwrap())
714            .await;
715
716        // check if the result from the validation is an error
717        if let Err(e) = result {
718            // assert that the error message contains "Fee contract's address is not a proxy"
719            assert!(e
720                .to_string()
721                .contains("Fee contract's address for the upgrade is missing"));
722        } else {
723            panic!("Expected the fee contract to be missing, but the validation succeeded");
724        }
725    }
726
727    #[test_log::test(tokio::test(flavor = "multi_thread"))]
728    async fn test_genesis_upgrade_fee_contract_address_is_zero() {
729        let toml = toml! {
730            base_version = "0.1"
731            upgrade_version = "0.2"
732            genesis_version = "0.1"
733
734            [stake_table]
735            capacity = 10
736
737            [chain_config]
738            chain_id = 12345
739            max_block_size = 30000
740            base_fee = 1
741            fee_recipient = "0x0000000000000000000000000000000000000000"
742
743            [header]
744            timestamp = 123456
745
746            [l1_finalized]
747            number = 42
748
749            [[upgrade]]
750            version = "0.2"
751            start_proposing_view = 5
752            stop_proposing_view = 15
753
754            [upgrade.fee]
755            [upgrade.fee.chain_config]
756            chain_id = 12345
757            max_block_size = 30000
758            base_fee = 1
759            fee_recipient = "0x0000000000000000000000000000000000000000"
760            fee_contract = "0x0000000000000000000000000000000000000000"
761        }
762        .to_string();
763
764        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
765        let rpc_url = "https://ethereum-sepolia.publicnode.com";
766
767        // validate the fee_contract address
768        let result = genesis
769            .validate_fee_contract(&L1Client::new(vec![rpc_url.parse().unwrap()]).unwrap())
770            .await;
771
772        // check if the result from the validation is an error
773        if let Err(e) = result {
774            // assert that the error message contains "Fee contract's address is not a proxy"
775            assert!(e
776                .to_string()
777                .contains("Fee contract cannot use the zero address"));
778        } else {
779            panic!(
780                "Expected the fee contract to complain about the zero address but the validation \
781                 succeeded"
782            );
783        }
784    }
785
786    #[test_log::test(tokio::test(flavor = "multi_thread"))]
787    async fn test_genesis_fee_contract_l1_failover() -> 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            .on_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            fee_contract = "{proxy_addr:?}"
815
816            [header]
817            timestamp = 123456
818
819            [l1_finalized]
820            number = 42
821        "#
822        )
823        .to_string();
824
825        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
826        genesis
827            .validate_fee_contract(
828                &L1Client::new(vec![
829                    "http://notareall1provider".parse().unwrap(),
830                    anvil.endpoint().parse().unwrap(),
831                ])
832                .unwrap(),
833            )
834            .await
835            .unwrap();
836
837        Ok(())
838    }
839
840    #[test]
841    fn test_genesis_from_toml_units() {
842        let toml = toml! {
843            base_version = "0.1"
844            upgrade_version = "0.2"
845            genesis_version = "0.1"
846
847            [stake_table]
848            capacity = 10
849
850            [chain_config]
851            chain_id = 12345
852            max_block_size = "30mb"
853            base_fee = "1 gwei"
854            fee_recipient = "0x0000000000000000000000000000000000000000"
855
856            [header]
857            timestamp = "2024-05-16T11:20:28-04:00"
858
859            [l1_finalized]
860            number = 0
861        }
862        .to_string();
863
864        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
865        assert_eq!(genesis.stake_table, StakeTableConfig { capacity: 10 });
866        assert_eq!(*genesis.chain_config.max_block_size, 30000000);
867        assert_eq!(genesis.chain_config.base_fee, 1_000_000_000.into());
868        assert_eq!(
869            genesis.header,
870            GenesisHeader {
871                timestamp: Timestamp::from_integer(1715872828).unwrap(),
872            }
873        )
874    }
875
876    #[test]
877    fn test_genesis_toml_fee_upgrade_view_mode() {
878        // without optional fields
879        // with view settings
880        let toml = toml! {
881            base_version = "0.1"
882            upgrade_version = "0.2"
883            genesis_version = "0.1"
884
885            [stake_table]
886            capacity = 10
887
888            [chain_config]
889            chain_id = 12345
890            max_block_size = 30000
891            base_fee = 1
892            fee_recipient = "0x0000000000000000000000000000000000000000"
893            fee_contract = "0x0000000000000000000000000000000000000000"
894
895            [header]
896            timestamp = 123456
897
898            [accounts]
899            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
900            "0x0000000000000000000000000000000000000000" = 42
901
902            [l1_finalized]
903            number = 64
904            timestamp = "0x123def"
905            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
906
907            [[upgrade]]
908            version = "0.2"
909            start_proposing_view = 1
910            stop_proposing_view = 15
911
912            [upgrade.fee]
913
914            [upgrade.fee.chain_config]
915            chain_id = 12345
916            max_block_size = 30000
917            base_fee = 1
918            fee_recipient = "0x0000000000000000000000000000000000000000"
919            fee_contract = "0x0000000000000000000000000000000000000000"
920        }
921        .to_string();
922
923        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
924
925        let (version, genesis_upgrade) = genesis.upgrades.last_key_value().unwrap();
926        println!("{genesis_upgrade:?}");
927
928        assert_eq!(*version, Version { major: 0, minor: 2 });
929
930        let upgrade = Upgrade {
931            mode: UpgradeMode::View(ViewBasedUpgrade {
932                start_voting_view: None,
933                stop_voting_view: None,
934                start_proposing_view: 1,
935                stop_proposing_view: 15,
936            }),
937            upgrade_type: UpgradeType::Fee {
938                chain_config: genesis.chain_config,
939            },
940        };
941
942        assert_eq!(*genesis_upgrade, upgrade);
943    }
944
945    #[test]
946    fn test_genesis_toml_fee_upgrade_time_mode() {
947        // without optional fields
948        // with time settings
949        let toml = toml! {
950            base_version = "0.1"
951            upgrade_version = "0.2"
952            genesis_version = "0.1"
953
954            [stake_table]
955            capacity = 10
956
957            [chain_config]
958            chain_id = 12345
959            max_block_size = 30000
960            base_fee = 1
961            fee_recipient = "0x0000000000000000000000000000000000000000"
962            fee_contract = "0x0000000000000000000000000000000000000000"
963
964            [header]
965            timestamp = 123456
966
967            [accounts]
968            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
969            "0x0000000000000000000000000000000000000000" = 42
970
971            [l1_finalized]
972            number = 64
973            timestamp = "0x123def"
974            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
975
976            [[upgrade]]
977            version = "0.2"
978            start_proposing_time = "2024-01-01T00:00:00Z"
979            stop_proposing_time = "2024-01-02T00:00:00Z"
980
981            [upgrade.fee]
982
983            [upgrade.fee.chain_config]
984            chain_id = 12345
985            max_block_size = 30000
986            base_fee = 1
987            fee_recipient = "0x0000000000000000000000000000000000000000"
988            fee_contract = "0x0000000000000000000000000000000000000000"
989        }
990        .to_string();
991
992        let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));
993
994        let (version, genesis_upgrade) = genesis.upgrades.last_key_value().unwrap();
995
996        assert_eq!(*version, Version { major: 0, minor: 2 });
997
998        let upgrade = Upgrade {
999            mode: UpgradeMode::Time(TimeBasedUpgrade {
1000                start_voting_time: None,
1001                stop_voting_time: None,
1002                start_proposing_time: Timestamp::from_string("2024-01-01T00:00:00Z".to_string())
1003                    .unwrap(),
1004                stop_proposing_time: Timestamp::from_string("2024-01-02T00:00:00Z".to_string())
1005                    .unwrap(),
1006            }),
1007            upgrade_type: UpgradeType::Fee {
1008                chain_config: genesis.chain_config,
1009            },
1010        };
1011
1012        assert_eq!(*genesis_upgrade, upgrade);
1013    }
1014
1015    #[test]
1016    fn test_genesis_toml_fee_upgrade_view_and_time_mode() {
1017        // set both time and view parameters
1018        // this should err
1019        let toml = toml! {
1020            base_version = "0.1"
1021            upgrade_version = "0.2"
1022            genesis_version = "0.1"
1023
1024            [stake_table]
1025            capacity = 10
1026
1027            [chain_config]
1028            chain_id = 12345
1029            max_block_size = 30000
1030            base_fee = 1
1031            fee_recipient = "0x0000000000000000000000000000000000000000"
1032            fee_contract = "0x0000000000000000000000000000000000000000"
1033
1034            [header]
1035            timestamp = 123456
1036
1037            [accounts]
1038            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
1039            "0x0000000000000000000000000000000000000000" = 42
1040
1041            [l1_finalized]
1042            number = 64
1043            timestamp = "0x123def"
1044            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
1045
1046            [[upgrade]]
1047            version = "0.2"
1048            start_proposing_view = 1
1049            stop_proposing_view = 10
1050            start_proposing_time = 1
1051            stop_proposing_time = 10
1052
1053            [upgrade.fee]
1054
1055            [upgrade.fee.chain_config]
1056            chain_id = 12345
1057            max_block_size = 30000
1058            base_fee = 1
1059            fee_recipient = "0x0000000000000000000000000000000000000000"
1060            fee_contract = "0x0000000000000000000000000000000000000000"
1061        }
1062        .to_string();
1063
1064        toml::from_str::<Genesis>(&toml).unwrap_err();
1065    }
1066
1067    #[test]
1068    fn test_fee_and_epoch_upgrade_toml() {
1069        let toml = toml! {
1070            base_version = "0.1"
1071            upgrade_version = "0.2"
1072            genesis_version = "0.1"
1073            epoch_height = 20
1074            drb_difficulty = 10
1075            drb_upgrade_difficulty = 20
1076            epoch_start_block = 1
1077            stake_table_capacity = 200
1078
1079            [stake_table]
1080            capacity = 10
1081
1082            [chain_config]
1083            chain_id = 12345
1084            max_block_size = 30000
1085            base_fee = 1
1086            fee_recipient = "0x0000000000000000000000000000000000000000"
1087            fee_contract = "0x0000000000000000000000000000000000000000"
1088
1089            [header]
1090            timestamp = 123456
1091
1092            [accounts]
1093            "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" = 100000
1094            "0x0000000000000000000000000000000000000000" = 42
1095
1096            [l1_finalized]
1097            number = 64
1098            timestamp = "0x123def"
1099            hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5"
1100
1101            [[upgrade]]
1102            version = "0.3"
1103            start_proposing_view = 1
1104            stop_proposing_view = 10
1105
1106            [upgrade.epoch]
1107            [upgrade.epoch.chain_config]
1108            chain_id = 12345
1109            max_block_size = 30000
1110            base_fee = 1
1111            fee_recipient = "0x0000000000000000000000000000000000000000"
1112            fee_contract = "0x0000000000000000000000000000000000000000"
1113            stake_table_contract = "0x0000000000000000000000000000000000000000"
1114
1115            [[upgrade]]
1116            version = "0.2"
1117            start_proposing_view = 1
1118            stop_proposing_view = 15
1119
1120            [upgrade.fee]
1121
1122            [upgrade.fee.chain_config]
1123            chain_id = 12345
1124            max_block_size = 30000
1125            base_fee = 1
1126            fee_recipient = "0x0000000000000000000000000000000000000000"
1127            fee_contract = "0x0000000000000000000000000000000000000000"
1128        }
1129        .to_string();
1130
1131        toml::from_str::<Genesis>(&toml).unwrap();
1132    }
1133}