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