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