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