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