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_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
62// Hand implement PartialEq in order to get around the fact that AuthSigs don't
63// impl Eq
64impl 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    // TODO: No need to return IotaResult. Also consider return &.
157    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    // The validator count limits and stake thresholds are enforced from the
357    // protocol config; the fields below are retained for layout
358    // compatibility only.
359    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
366// These constants exist solely so that tests pinning genesis to a protocol
367// version below 32 keep producing historically-correct genesis content: real
368// genesis ceremonies require protocol version >= 32 (see the CLI guard in
369// `iota-tool`), at which point these values are enforced through the protocol
370// config instead and genesis records zeros here.
371const 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/// Initial set of parameters for a chain.
378#[derive(Clone, Serialize, Deserialize)]
379pub struct GenesisCeremonyParameters {
380    #[serde(default = "GenesisCeremonyParameters::default_timestamp_ms")]
381    pub chain_start_timestamp_ms: u64,
382
383    /// protocol version that the chain starts at.
384    #[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    /// The duration of an epoch, in milliseconds.
391    #[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 hrs
418        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            // The validator count limits and stake thresholds are enforced
430            // from the protocol config; the deprecated fields are recorded
431            // as zero.
432            (0, 0, 0, 0, 0)
433        } else {
434            // Real genesis ceremonies require protocol version >= 32; this branch
435            // exists so that tests pinning genesis to an older version still get
436            // the historical values that pre-version-32 framework snapshots read
437            // out of these fields.
438            (
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    /// Validates the schedule.
481    ///
482    /// # Panics
483    ///
484    /// Panics if the schedule contains timelocked stake or a non-zero
485    /// pre-minted supply (neither is supported at genesis), or if the total
486    /// allocated amount overflows `u64`.
487    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        // Check that all allocations are for valid validators, while summing up all
515        // allocations for each validator
516        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        // Check that all validators have sufficient stake allocated to ensure they meet
526        // the minimum stake threshold. Below protocol version 32 the threshold isn't
527        // present in the protocol config, so fall back to the historical genesis value.
528        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        // Below protocol version 32 the threshold isn't present in the protocol
547        // config, so fall back to the historical genesis value.
548        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    /// Helper to read a TokenDistributionSchedule from a csv file.
572    ///
573    /// The file is encoded such that the final entry in the CSV file is used to
574    /// denote the allocation to the stake subsidy fund.
575    ///
576    /// Comments are optional, and start with a `#` character.
577    /// Only entries that start with this character are treated as comments.
578    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    /// Indicates the address that owns the tokens. It means that this
625    /// `TokenAllocation` can serve to stake some funds to the
626    /// `staked_with_validator` during genesis, but it's the `recipient_address`
627    /// which will receive the associated StakedIota (or TimelockedStakedIota)
628    /// object.
629    pub recipient_address: Address,
630    /// Indicates an amount of nanos that is:
631    /// - minted for the `recipient_address` and staked to a validator, only in the case
632    ///   `staked_with_validator` is Some
633    /// - minted for the `recipient_address` and transferred that address, otherwise.
634    pub amount_nanos: u64,
635
636    /// Indicates if this allocation should be staked at genesis and with which
637    /// validator
638    pub staked_with_validator: Option<Address>,
639    /// Indicates if this allocation should be staked with timelock at genesis
640    /// and contains its timelock_expiration
641    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        // Below protocol version 32 the threshold isn't present in the protocol
665        // config, so fall back to the historical genesis value.
666        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
695/// Helper function to create a CSV reader with custom settings.
696/// In this case, it sets the comment character to `#`.
697pub 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    /// A ceremony directory saved by an older release may contain a
736    /// token-distribution-schedule CSV with timelocked allocations; parsing
737    /// it must fail up front rather than deep inside genesis execution.
738    #[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}