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