1use std::{
6 collections::HashMap,
7 fs::File,
8 io::{BufReader, BufWriter},
9 path::Path,
10};
11
12use anyhow::{Context, Result};
13use fastcrypto::{
14 encoding::{Base64, Encoding},
15 hash::HashFunction,
16};
17use iota_protocol_config::{Chain, ProtocolConfig};
18use iota_sdk_types::{
19 Address, ObjectId, TransactionEffects, TransactionEvents,
20 checkpoint::{CheckpointContents, CheckpointSummary},
21};
22use iota_types::{
23 clock::Clock,
24 committee::{Committee, CommitteeWithNetworkMetadata, EpochId, ProtocolVersion},
25 crypto::DefaultHash,
26 deny_list_v1::get_deny_list_root_object,
27 error::IotaResult,
28 iota_system_state::{
29 IotaSystemState, IotaSystemStateTrait, IotaSystemStateWrapper, IotaValidatorGenesis,
30 get_iota_system_state, get_iota_system_state_wrapper,
31 },
32 messages_checkpoint::{CertifiedCheckpointSummary, VerifiedCheckpoint},
33 object::Object,
34 storage::ObjectStore,
35 transaction::TransactionEnvelope,
36};
37use serde::{Deserialize, Deserializer, Serialize, Serializer};
38use tracing::trace;
39
40#[derive(Clone, Debug)]
41pub struct Genesis {
42 checkpoint: CertifiedCheckpointSummary,
43 checkpoint_contents: CheckpointContents,
44 transaction: TransactionEnvelope,
45 effects: TransactionEffects,
46 events: TransactionEvents,
47 objects: Vec<Object>,
48}
49
50#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
51pub struct UnsignedGenesis {
52 pub checkpoint: CheckpointSummary,
53 pub checkpoint_contents: CheckpointContents,
54 pub transaction: TransactionEnvelope,
55 pub effects: TransactionEffects,
56 pub events: TransactionEvents,
57 pub objects: Vec<Object>,
58}
59
60impl PartialEq for Genesis {
63 fn eq(&self, other: &Self) -> bool {
64 self.checkpoint.data() == other.checkpoint.data()
65 && {
66 let this = self.checkpoint.auth_sig();
67 let other = other.checkpoint.auth_sig();
68
69 this.epoch == other.epoch
70 && this.signature.as_ref() == other.signature.as_ref()
71 && this.signers_map == other.signers_map
72 }
73 && self.checkpoint_contents == other.checkpoint_contents
74 && self.transaction == other.transaction
75 && self.effects == other.effects
76 && self.objects == other.objects
77 }
78}
79
80impl Eq for Genesis {}
81
82impl Genesis {
83 pub fn new(
84 checkpoint: CertifiedCheckpointSummary,
85 checkpoint_contents: CheckpointContents,
86 transaction: TransactionEnvelope,
87 effects: TransactionEffects,
88 events: TransactionEvents,
89 objects: Vec<Object>,
90 ) -> Self {
91 Self {
92 checkpoint,
93 checkpoint_contents,
94 transaction,
95 effects,
96 events,
97 objects,
98 }
99 }
100
101 pub fn into_objects(self) -> Vec<Object> {
102 self.objects
103 }
104
105 pub fn objects(&self) -> &[Object] {
106 &self.objects
107 }
108
109 pub fn object(&self, id: ObjectId) -> Option<Object> {
110 self.objects.iter().find(|o| o.id() == id).cloned()
111 }
112
113 pub fn transaction(&self) -> &TransactionEnvelope {
114 &self.transaction
115 }
116
117 pub fn effects(&self) -> &TransactionEffects {
118 &self.effects
119 }
120 pub fn events(&self) -> &TransactionEvents {
121 &self.events
122 }
123
124 pub fn checkpoint(&self) -> VerifiedCheckpoint {
125 self.checkpoint
126 .clone()
127 .try_into_verified(&self.committee().unwrap())
128 .unwrap()
129 }
130
131 pub fn checkpoint_contents(&self) -> &CheckpointContents {
132 &self.checkpoint_contents
133 }
134
135 pub fn epoch(&self) -> EpochId {
136 0
137 }
138
139 pub fn validator_set_for_tooling(&self) -> Vec<IotaValidatorGenesis> {
140 self.iota_system_object()
141 .into_genesis_version_for_tooling()
142 .validators
143 .active_validators
144 }
145
146 pub fn committee_with_network(&self) -> CommitteeWithNetworkMetadata {
147 self.iota_system_object().get_current_epoch_committee()
148 }
149
150 pub fn reference_gas_price(&self) -> u64 {
151 self.iota_system_object().reference_gas_price()
152 }
153
154 pub fn committee(&self) -> IotaResult<Committee> {
156 Ok(self.committee_with_network().committee().clone())
157 }
158
159 pub fn iota_system_wrapper_object(&self) -> IotaSystemStateWrapper {
160 get_iota_system_state_wrapper(&self.objects())
161 .expect("IOTA System State Wrapper object must always exist")
162 }
163
164 pub fn contains_migrations(&self) -> bool {
165 self.checkpoint_contents.len() > 1
166 }
167
168 pub fn iota_system_object(&self) -> IotaSystemState {
169 get_iota_system_state(&self.objects()).expect("IOTA System State object must always exist")
170 }
171
172 pub fn clock(&self) -> Clock {
173 let clock = self
174 .objects()
175 .iter()
176 .find(|o| o.id() == ObjectId::CLOCK)
177 .expect("clock must always exist")
178 .data
179 .as_opt_struct()
180 .expect("clock must be a Move object");
181 bcs::from_bytes::<Clock>(clock.contents())
182 .expect("clock object deserialization cannot fail")
183 }
184
185 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, anyhow::Error> {
186 let path = path.as_ref();
187 trace!("reading Genesis from {}", path.display());
188 let read = File::open(path)
189 .with_context(|| format!("unable to load Genesis from {}", path.display()))?;
190 bcs::from_reader(BufReader::new(read))
191 .with_context(|| format!("unable to parse Genesis from {}", path.display()))
192 }
193
194 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), anyhow::Error> {
195 let path = path.as_ref();
196 trace!("writing Genesis to {}", path.display());
197 let mut write = BufWriter::new(File::create(path)?);
198 bcs::serialize_into(&mut write, &self)
199 .with_context(|| format!("unable to save Genesis to {}", path.display()))?;
200 Ok(())
201 }
202
203 pub fn to_bytes(&self) -> Vec<u8> {
204 bcs::to_bytes(self).expect("failed to serialize genesis")
205 }
206
207 pub fn hash(&self) -> [u8; 32] {
208 use std::io::Write;
209
210 let mut digest = DefaultHash::default();
211 digest.write_all(&self.to_bytes()).unwrap();
212 let hash = digest.finalize();
213 hash.into()
214 }
215}
216
217impl Serialize for Genesis {
218 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
219 where
220 S: Serializer,
221 {
222 use serde::ser::Error;
223
224 #[derive(Serialize)]
225 struct RawGenesis<'a> {
226 checkpoint: &'a CertifiedCheckpointSummary,
227 checkpoint_contents: &'a CheckpointContents,
228 transaction: &'a TransactionEnvelope,
229 effects: &'a TransactionEffects,
230 events: &'a TransactionEvents,
231 objects: &'a [Object],
232 }
233
234 let raw_genesis = RawGenesis {
235 checkpoint: &self.checkpoint,
236 checkpoint_contents: &self.checkpoint_contents,
237 transaction: &self.transaction,
238 effects: &self.effects,
239 events: &self.events,
240 objects: &self.objects,
241 };
242
243 if serializer.is_human_readable() {
244 let bytes = bcs::to_bytes(&raw_genesis).map_err(|e| Error::custom(e.to_string()))?;
245 let s = Base64::encode(bytes);
246 serializer.serialize_str(&s)
247 } else {
248 raw_genesis.serialize(serializer)
249 }
250 }
251}
252
253impl<'de> Deserialize<'de> for Genesis {
254 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
255 where
256 D: Deserializer<'de>,
257 {
258 use serde::de::Error;
259
260 #[derive(Deserialize)]
261 struct RawGenesis {
262 checkpoint: CertifiedCheckpointSummary,
263 checkpoint_contents: CheckpointContents,
264 transaction: TransactionEnvelope,
265 effects: TransactionEffects,
266 events: TransactionEvents,
267 objects: Vec<Object>,
268 }
269
270 let raw_genesis = if deserializer.is_human_readable() {
271 let s = String::deserialize(deserializer)?;
272 let bytes = Base64::decode(&s).map_err(|e| Error::custom(e.to_string()))?;
273 bcs::from_bytes(&bytes).map_err(|e| Error::custom(e.to_string()))?
274 } else {
275 RawGenesis::deserialize(deserializer)?
276 };
277
278 Ok(Genesis {
279 checkpoint: raw_genesis.checkpoint,
280 checkpoint_contents: raw_genesis.checkpoint_contents,
281 transaction: raw_genesis.transaction,
282 effects: raw_genesis.effects,
283 events: raw_genesis.events,
284 objects: raw_genesis.objects,
285 })
286 }
287}
288
289impl UnsignedGenesis {
290 pub fn objects(&self) -> &[Object] {
291 &self.objects
292 }
293
294 pub fn object(&self, id: ObjectId) -> Option<Object> {
295 self.objects.iter().find(|o| o.id() == id).cloned()
296 }
297
298 pub fn transaction(&self) -> &TransactionEnvelope {
299 &self.transaction
300 }
301
302 pub fn effects(&self) -> &TransactionEffects {
303 &self.effects
304 }
305 pub fn events(&self) -> &TransactionEvents {
306 &self.events
307 }
308
309 pub fn checkpoint(&self) -> &CheckpointSummary {
310 &self.checkpoint
311 }
312
313 pub fn checkpoint_contents(&self) -> &CheckpointContents {
314 &self.checkpoint_contents
315 }
316
317 pub fn epoch(&self) -> EpochId {
318 0
319 }
320
321 pub fn iota_system_wrapper_object(&self) -> IotaSystemStateWrapper {
322 get_iota_system_state_wrapper(&self.objects())
323 .expect("IOTA System State Wrapper object must always exist")
324 }
325
326 pub fn iota_system_object(&self) -> IotaSystemState {
327 get_iota_system_state(&self.objects()).expect("IOTA System State object must always exist")
328 }
329
330 pub fn has_randomness_state_object(&self) -> bool {
331 self.objects()
332 .get_object(&ObjectId::RANDOMNESS_STATE)
333 .is_some()
334 }
335
336 pub fn has_bridge_object(&self) -> bool {
337 self.objects()
338 .get_object(&ObjectId::GENESIS_BRIDGE)
339 .is_some()
340 }
341
342 pub fn has_coin_deny_list_object(&self) -> bool {
343 get_deny_list_root_object(&self.objects()).is_some()
344 }
345}
346
347#[derive(Clone, Debug, Serialize, Deserialize)]
348#[serde(rename_all = "kebab-case")]
349pub struct GenesisChainParameters {
350 pub protocol_version: u64,
351 pub chain_start_timestamp_ms: u64,
352 pub epoch_duration_ms: u64,
353
354 pub max_validator_count: u64,
358 pub min_validator_joining_stake: u64,
359 pub validator_low_stake_threshold: u64,
360 pub validator_very_low_stake_threshold: u64,
361 pub validator_low_stake_grace_period: u64,
362}
363
364const PRE_V32_MIN_VALIDATOR_JOINING_STAKE: u64 = 2_000_000_000_000_000;
370pub const PRE_V32_VALIDATOR_LOW_STAKE_THRESHOLD: u64 = 1_500_000_000_000_000;
371const PRE_V32_VALIDATOR_VERY_LOW_STAKE_THRESHOLD: u64 = 1_000_000_000_000_000;
372const PRE_V32_VALIDATOR_LOW_STAKE_GRACE_PERIOD: u64 = 7;
373const PRE_V32_MAX_VALIDATOR_COUNT: u64 = 150;
374
375#[derive(Serialize, Deserialize)]
377pub struct GenesisCeremonyParameters {
378 #[serde(default = "GenesisCeremonyParameters::default_timestamp_ms")]
379 pub chain_start_timestamp_ms: u64,
380
381 #[serde(default = "ProtocolVersion::max")]
383 pub protocol_version: ProtocolVersion,
384
385 #[serde(default = "GenesisCeremonyParameters::default_allow_insertion_of_extra_objects")]
386 pub allow_insertion_of_extra_objects: bool,
387
388 #[serde(default = "GenesisCeremonyParameters::default_epoch_duration_ms")]
390 pub epoch_duration_ms: u64,
391}
392
393impl GenesisCeremonyParameters {
394 pub fn new() -> Self {
395 Self {
396 chain_start_timestamp_ms: Self::default_timestamp_ms(),
397 protocol_version: ProtocolVersion::MAX,
398 allow_insertion_of_extra_objects: true,
399 epoch_duration_ms: Self::default_epoch_duration_ms(),
400 }
401 }
402
403 fn default_timestamp_ms() -> u64 {
404 std::time::SystemTime::now()
405 .duration_since(std::time::UNIX_EPOCH)
406 .unwrap()
407 .as_millis() as u64
408 }
409
410 fn default_allow_insertion_of_extra_objects() -> bool {
411 true
412 }
413
414 fn default_epoch_duration_ms() -> u64 {
415 24 * 60 * 60 * 1000
417 }
418
419 pub fn to_genesis_chain_parameters(&self) -> GenesisChainParameters {
420 let (
421 max_validator_count,
422 min_validator_joining_stake,
423 validator_low_stake_threshold,
424 validator_very_low_stake_threshold,
425 validator_low_stake_grace_period,
426 ) = if self.protocol_version.as_u64() >= 32 {
427 (0, 0, 0, 0, 0)
431 } else {
432 (
437 PRE_V32_MAX_VALIDATOR_COUNT,
438 PRE_V32_MIN_VALIDATOR_JOINING_STAKE,
439 PRE_V32_VALIDATOR_LOW_STAKE_THRESHOLD,
440 PRE_V32_VALIDATOR_VERY_LOW_STAKE_THRESHOLD,
441 PRE_V32_VALIDATOR_LOW_STAKE_GRACE_PERIOD,
442 )
443 };
444 GenesisChainParameters {
445 protocol_version: self.protocol_version.as_u64(),
446 chain_start_timestamp_ms: self.chain_start_timestamp_ms,
447 epoch_duration_ms: self.epoch_duration_ms,
448 max_validator_count,
449 min_validator_joining_stake,
450 validator_low_stake_threshold,
451 validator_very_low_stake_threshold,
452 validator_low_stake_grace_period,
453 }
454 }
455}
456
457impl Default for GenesisCeremonyParameters {
458 fn default() -> Self {
459 Self::new()
460 }
461}
462
463#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
464#[serde(rename_all = "kebab-case")]
465pub struct TokenDistributionSchedule {
466 pub pre_minted_supply: u64,
467 pub allocations: Vec<TokenAllocation>,
468}
469
470impl TokenDistributionSchedule {
471 pub fn contains_timelocked_stake(&self) -> bool {
472 self.allocations
473 .iter()
474 .find_map(|allocation| allocation.staked_with_timelock_expiration)
475 .is_some()
476 }
477
478 pub fn validate(&self) {
486 assert!(
487 !self.contains_timelocked_stake(),
488 "timelocked stake is not supported at genesis"
489 );
490 assert_eq!(
491 self.pre_minted_supply, 0,
492 "a non-zero pre-minted supply is not supported at genesis"
493 );
494
495 let mut total_nanos = self.pre_minted_supply;
496
497 for allocation in &self.allocations {
498 total_nanos = total_nanos
499 .checked_add(allocation.amount_nanos)
500 .expect("TokenDistributionSchedule allocates more than the maximum supply which equals u64::MAX");
501 }
502 }
503
504 pub fn check_minimum_stake_for_validators<I: IntoIterator<Item = Address>>(
505 &self,
506 validators: I,
507 protocol_version: ProtocolVersion,
508 ) -> Result<()> {
509 let mut validators: HashMap<Address, u64> =
510 validators.into_iter().map(|a| (a, 0)).collect();
511
512 for allocation in &self.allocations {
515 if let Some(staked_with_validator) = &allocation.staked_with_validator {
516 *validators
517 .get_mut(staked_with_validator)
518 .expect("allocation must be staked with valid validator") +=
519 allocation.amount_nanos;
520 }
521 }
522
523 let minimum_required_stake =
527 ProtocolConfig::get_for_version(protocol_version, Chain::Unknown)
528 .validator_low_stake_threshold_as_option()
529 .unwrap_or(PRE_V32_VALIDATOR_LOW_STAKE_THRESHOLD);
530 for (validator, stake) in validators {
531 if stake < minimum_required_stake {
532 anyhow::bail!(
533 "validator {validator} has '{stake}' stake and does not meet the minimum required stake threshold of '{minimum_required_stake}'"
534 );
535 }
536 }
537 Ok(())
538 }
539
540 pub fn new_for_validators_with_default_allocation<I: IntoIterator<Item = Address>>(
541 validators: I,
542 protocol_version: ProtocolVersion,
543 ) -> Self {
544 let default_allocation = ProtocolConfig::get_for_version(protocol_version, Chain::Unknown)
547 .validator_low_stake_threshold_as_option()
548 .unwrap_or(PRE_V32_VALIDATOR_LOW_STAKE_THRESHOLD);
549
550 let allocations = validators
551 .into_iter()
552 .map(|a| TokenAllocation {
553 recipient_address: a,
554 amount_nanos: default_allocation,
555 staked_with_validator: Some(a),
556 staked_with_timelock_expiration: None,
557 })
558 .collect();
559
560 let schedule = Self {
561 pre_minted_supply: 0,
562 allocations,
563 };
564
565 schedule.validate();
566 schedule
567 }
568
569 pub fn from_csv<R: std::io::Read>(reader: R) -> Result<Self> {
577 let mut reader = csv_reader_with_comments(reader);
578 let mut allocations: Vec<TokenAllocation> =
579 reader.deserialize().collect::<Result<_, _>>()?;
580
581 let pre_minted_supply = allocations.pop().unwrap();
582 assert_eq!(
583 Address::ZERO,
584 pre_minted_supply.recipient_address,
585 "final allocation must be for the pre-minted supply amount",
586 );
587 assert!(
588 pre_minted_supply.staked_with_validator.is_none(),
589 "cannot stake the pre-minted supply amount",
590 );
591
592 let schedule = Self {
593 pre_minted_supply: pre_minted_supply.amount_nanos,
594 allocations,
595 };
596
597 schedule.validate();
598 Ok(schedule)
599 }
600
601 pub fn to_csv<W: std::io::Write>(&self, writer: W) -> Result<()> {
602 let mut writer = csv::Writer::from_writer(writer);
603
604 for allocation in &self.allocations {
605 writer.serialize(allocation)?;
606 }
607
608 writer.serialize(TokenAllocation {
609 recipient_address: Address::ZERO,
610 amount_nanos: self.pre_minted_supply,
611 staked_with_validator: None,
612 staked_with_timelock_expiration: None,
613 })?;
614
615 Ok(())
616 }
617}
618
619#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
620#[serde(rename_all = "kebab-case")]
621pub struct TokenAllocation {
622 pub recipient_address: Address,
628 pub amount_nanos: u64,
634
635 pub staked_with_validator: Option<Address>,
638 pub staked_with_timelock_expiration: Option<u64>,
641}
642
643#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
644pub struct TokenDistributionScheduleBuilder {
645 pre_minted_supply: u64,
646 allocations: Vec<TokenAllocation>,
647}
648
649impl TokenDistributionScheduleBuilder {
650 #[expect(clippy::new_without_default)]
651 pub fn new() -> Self {
652 Self {
653 pre_minted_supply: 0,
654 allocations: vec![],
655 }
656 }
657
658 pub fn default_allocation_for_validators<I: IntoIterator<Item = Address>>(
659 &mut self,
660 validators: I,
661 protocol_version: ProtocolVersion,
662 ) {
663 let default_allocation = ProtocolConfig::get_for_version(protocol_version, Chain::Unknown)
666 .validator_low_stake_threshold_as_option()
667 .unwrap_or(PRE_V32_VALIDATOR_LOW_STAKE_THRESHOLD);
668
669 for validator in validators {
670 self.add_allocation(TokenAllocation {
671 recipient_address: validator,
672 amount_nanos: default_allocation,
673 staked_with_validator: Some(validator),
674 staked_with_timelock_expiration: None,
675 });
676 }
677 }
678
679 pub fn add_allocation(&mut self, allocation: TokenAllocation) {
680 self.allocations.push(allocation);
681 }
682
683 pub fn build(&self) -> TokenDistributionSchedule {
684 let schedule = TokenDistributionSchedule {
685 pre_minted_supply: self.pre_minted_supply,
686 allocations: self.allocations.clone(),
687 };
688
689 schedule.validate();
690 schedule
691 }
692}
693
694pub fn csv_reader_with_comments<R: std::io::Read>(reader: R) -> csv::Reader<R> {
697 csv::ReaderBuilder::new()
698 .comment(Some(b'#'))
699 .from_reader(reader)
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705
706 fn timelocked_schedule() -> TokenDistributionSchedule {
707 TokenDistributionSchedule {
708 pre_minted_supply: 0,
709 allocations: vec![TokenAllocation {
710 recipient_address: Address::ZERO,
711 amount_nanos: 1_500_000_000_000_000,
712 staked_with_validator: Some(Address::ZERO),
713 staked_with_timelock_expiration: Some(1_000_000),
714 }],
715 }
716 }
717
718 #[test]
719 #[should_panic(expected = "timelocked stake is not supported at genesis")]
720 fn validate_rejects_timelocked_stake() {
721 timelocked_schedule().validate();
722 }
723
724 #[test]
725 #[should_panic(expected = "non-zero pre-minted supply is not supported at genesis")]
726 fn validate_rejects_pre_minted_supply() {
727 let schedule = TokenDistributionSchedule {
728 pre_minted_supply: 100,
729 allocations: vec![],
730 };
731 schedule.validate();
732 }
733
734 #[test]
738 #[should_panic(expected = "timelocked stake is not supported at genesis")]
739 fn from_csv_rejects_timelocked_stake() {
740 let mut csv = Vec::new();
741 timelocked_schedule().to_csv(&mut csv).unwrap();
742
743 let _ = TokenDistributionSchedule::from_csv(csv.as_slice());
744 }
745}