Skip to main content

iota_config/
genesis.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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
59// Hand implement PartialEq in order to get around the fact that AuthSigs don't
60// impl Eq
61impl 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    // TODO: No need to return IotaResult. Also consider return &.
154    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    // The validator count limits and stake thresholds are enforced from the
354    // protocol config; the fields below are retained for layout
355    // compatibility only.
356    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
363// These constants exist solely so that tests pinning genesis to a protocol
364// version below 32 keep producing historically-correct genesis content: real
365// genesis ceremonies require protocol version >= 32 (see the CLI guard in
366// `iota-tool`), at which point these values are enforced through the protocol
367// config instead and genesis records zeros here.
368const 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/// Initial set of parameters for a chain.
375#[derive(Clone, Serialize, Deserialize)]
376pub struct GenesisCeremonyParameters {
377    #[serde(default = "GenesisCeremonyParameters::default_timestamp_ms")]
378    pub chain_start_timestamp_ms: u64,
379
380    /// protocol version that the chain starts at.
381    #[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    /// The duration of an epoch, in milliseconds.
388    #[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 hrs
415        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            // The validator count limits and stake thresholds are enforced
427            // from the protocol config; the deprecated fields are recorded
428            // as zero.
429            (0, 0, 0, 0, 0)
430        } else {
431            // Real genesis ceremonies require protocol version >= 32; this branch
432            // exists so that tests pinning genesis to an older version still get
433            // the historical values that pre-version-32 framework snapshots read
434            // out of these fields.
435            (
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    /// Validates the schedule.
478    ///
479    /// # Panics
480    ///
481    /// Panics if the schedule contains timelocked stake or a non-zero
482    /// pre-minted supply (neither is supported at genesis), or if the total
483    /// allocated amount overflows `u64`.
484    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        // Check that all allocations are for valid validators, while summing up all
512        // allocations for each validator
513        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        // Check that all validators have sufficient stake allocated to ensure they meet
523        // the minimum stake threshold. Below protocol version 32 the threshold isn't
524        // present in the protocol config, so fall back to the historical genesis value.
525        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        // Below protocol version 32 the threshold isn't present in the protocol
544        // config, so fall back to the historical genesis value.
545        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    /// Helper to read a TokenDistributionSchedule from a csv file.
569    ///
570    /// The file is encoded such that the final entry in the CSV file is used to
571    /// denote the allocation to the stake subsidy fund.
572    ///
573    /// Comments are optional, and start with a `#` character.
574    /// Only entries that start with this character are treated as comments.
575    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    /// Indicates the address that owns the tokens. It means that this
622    /// `TokenAllocation` can serve to stake some funds to the
623    /// `staked_with_validator` during genesis, but it's the `recipient_address`
624    /// which will receive the associated StakedIota (or TimelockedStakedIota)
625    /// object.
626    pub recipient_address: Address,
627    /// Indicates an amount of nanos that is:
628    /// - minted for the `recipient_address` and staked to a validator, only in
629    ///   the case `staked_with_validator` is Some
630    /// - minted for the `recipient_address` and transferred that address,
631    ///   otherwise.
632    pub amount_nanos: u64,
633
634    /// Indicates if this allocation should be staked at genesis and with which
635    /// validator
636    pub staked_with_validator: Option<Address>,
637    /// Indicates if this allocation should be staked with timelock at genesis
638    /// and contains its timelock_expiration
639    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        // Below protocol version 32 the threshold isn't present in the protocol
663        // config, so fall back to the historical genesis value.
664        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
693/// Helper function to create a CSV reader with custom settings.
694/// In this case, it sets the comment character to `#`.
695pub 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    /// A ceremony directory saved by an older release may contain a
734    /// token-distribution-schedule CSV with timelocked allocations; parsing
735    /// it must fail up front rather than deep inside genesis execution.
736    #[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}