diff --git a/crates/iota-config/src/genesis.rs b/crates/iota-config/src/genesis.rs index d7c850ca148..7e95ac0a148 100644 --- a/crates/iota-config/src/genesis.rs +++ b/crates/iota-config/src/genesis.rs @@ -454,7 +454,7 @@ impl TokenDistributionSchedule { for allocation in &self.allocations { total_nanos = total_nanos .checked_add(allocation.amount_nanos) - .expect("TokenDistributionSchedule allocates more than the maximum supply which equals u64::MAX", ); + .expect("TokenDistributionSchedule allocates more than the maximum supply which equals u64::MAX"); } } @@ -516,12 +516,7 @@ impl TokenDistributionSchedule { /// Helper to read a TokenDistributionSchedule from a csv file. /// /// The file is encoded such that the final entry in the CSV file is used to - /// denote the allocation to the stake subsidy fund. It must be in the - /// following format: - /// `0x0000000000000000000000000000000000000000000000000000000000000000, - ///
minted supply
,` - /// - /// All entries in a token distribution schedule must add up to 10B Iota. + /// denote the allocation to the stake subsidy fund. pub fn from_csv(reader: R) -> Result { let mut reader = csv::Reader::from_reader(reader); let mut allocations: Vec = @@ -568,7 +563,17 @@ impl TokenDistributionSchedule { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct TokenAllocation { + /// Indicates the address that owns the tokens. It means that this + /// `TokenAllocation` can serve to stake some funds to the + /// `staked_with_validator` during genesis, but it's the `recipient_address` + /// which will receive the associated StakedIota (or TimelockedStakedIota) + /// object. pub recipient_address: IotaAddress, + /// Indicates an amount of nanos that is: + /// - minted for the `recipient_address` and staked to a validator, only in + /// the case `staked_with_validator` is Some + /// - minted for the `recipient_address` and transferred that address, + /// otherwise. pub amount_nanos: u64, /// Indicates if this allocation should be staked at genesis and with which @@ -628,3 +633,117 @@ impl TokenDistributionScheduleBuilder { schedule } } + +/// Represents the allocation of stake and gas payment to a validator. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct ValidatorAllocation { + /// The validator address receiving the stake and/or gas payment + pub validator: IotaAddress, + /// The amount of nanos to stake to the validator + pub amount_nanos_to_stake: u64, + /// The amount of nanos to transfer as gas payment to the validator + pub amount_nanos_to_pay_gas: u64, +} + +/// Represents a delegation of stake and gas payment to a validator, +/// coming from a delegator. This struct is used to serialize and deserialize +/// delegations to and from a csv file. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Delegation { + /// The address from which to take the nanos for staking/gas + pub delegator: IotaAddress, + /// The allocation to a validator receiving a stake and/or a gas payment + #[serde(flatten)] + pub validator_allocation: ValidatorAllocation, +} + +/// Represents genesis delegations to validators. +/// +/// This struct maps a delegator address to a list of validators and their +/// stake and gas allocations. Each ValidatorAllocation contains the address of +/// a validator that will receive an amount of nanos to stake and an amount as +/// gas payment. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Delegations { + pub allocations: HashMap>, +} + +impl Delegations { + pub fn new_for_validators_with_default_allocation( + validators: impl IntoIterator, + delegator: IotaAddress, + ) -> Self { + let validator_allocations = validators + .into_iter() + .map(|address| ValidatorAllocation { + validator: address, + amount_nanos_to_stake: iota_types::governance::MIN_VALIDATOR_JOINING_STAKE_NANOS, + amount_nanos_to_pay_gas: 0, + }) + .collect(); + + let mut allocations = HashMap::new(); + allocations.insert(delegator, validator_allocations); + + Self { allocations } + } + + /// Helper to read a Delegations struct from a csv file. + /// + /// The file is encoded such that the final entry in the CSV file is used to + /// denote the allocation coming from a delegator. It must be in the + /// following format: + /// `delegator,validator,amount-nanos-to-stake,amount-nanos-to-pay-gas + /// ,,2000000000000000,5000000000 + /// ,,3000000000000000,5000000000 + /// ,,4500000000000000,5000000000` + pub fn from_csv(reader: R) -> Result { + let mut reader = csv::Reader::from_reader(reader); + + let mut delegations = Self::default(); + for delegation in reader.deserialize::() { + let delegation = delegation?; + delegations + .allocations + .entry(delegation.delegator) + .or_default() + .push(delegation.validator_allocation); + } + + Ok(delegations) + } + + /// Helper to write a Delegations struct into a csv file. + /// + /// It writes in the following format: + /// `delegator,validator,amount-nanos-to-stake,amount-nanos-to-pay-gas + /// ,,2000000000000000,5000000000 + /// ,,3000000000000000,5000000000 + /// ,,4500000000000000,5000000000` + pub fn to_csv(&self, writer: W) -> Result<()> { + let mut writer = csv::Writer::from_writer(writer); + + writer.write_record([ + "delegator", + "validator", + "amount-nanos-to-stake", + "amount-nanos-to-pay-gas", + ])?; + + for (&delegator, validator_allocations) in &self.allocations { + for validator_allocation in validator_allocations { + writer.write_record(&[ + delegator.to_string(), + validator_allocation.validator.to_string(), + validator_allocation.amount_nanos_to_stake.to_string(), + validator_allocation.amount_nanos_to_pay_gas.to_string(), + ])?; + } + } + + Ok(()) + } +} diff --git a/crates/iota-genesis-builder/examples/snapshot_only_test_outputs.rs b/crates/iota-genesis-builder/examples/snapshot_only_test_outputs.rs index 55b25187a01..d56d1b53716 100644 --- a/crates/iota-genesis-builder/examples/snapshot_only_test_outputs.rs +++ b/crates/iota-genesis-builder/examples/snapshot_only_test_outputs.rs @@ -7,15 +7,14 @@ use std::{fs::File, path::Path}; use clap::{Parser, Subcommand}; -use iota_genesis_builder::{ - IF_STARDUST_ADDRESS, - stardust::{ - parse::HornetSnapshotParser, - test_outputs::{add_snapshot_test_outputs, to_nanos}, - }, +use iota_genesis_builder::stardust::{ + parse::HornetSnapshotParser, + test_outputs::{add_snapshot_test_outputs, to_nanos}, +}; +use iota_sdk::types::block::address::Ed25519Address; +use iota_types::{ + base_types::IotaAddress, gas_coin::STARDUST_TOTAL_SUPPLY_IOTA, stardust::coin_type::CoinType, }; -use iota_sdk::types::block::address::Address; -use iota_types::{gas_coin::STARDUST_TOTAL_SUPPLY_IOTA, stardust::coin_type::CoinType}; const WITH_SAMPLING: bool = false; @@ -32,6 +31,8 @@ enum Snapshot { Iota { #[clap(long, help = "Path to the Iota Hornet full-snapshot file")] snapshot_path: String, + #[clap(long, help = "Specify the delegator address")] + delegator: IotaAddress, }, } @@ -62,8 +63,11 @@ fn parse_snapshot( #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - let (current_path, coin_type) = match cli.snapshot { - Snapshot::Iota { snapshot_path } => (snapshot_path, CoinType::Iota), + let (current_path, delegator, coin_type) = match cli.snapshot { + Snapshot::Iota { + snapshot_path, + delegator, + } => (snapshot_path, delegator, CoinType::Iota), }; let mut new_path = String::from("test-"); // prepend "test-" before the file name @@ -80,7 +84,7 @@ async fn main() -> anyhow::Result<()> { let (randomness_seed, delegator_address) = match coin_type { CoinType::Iota => { // IOTA coin type values - (0, IF_STARDUST_ADDRESS) + (0, delegator) } }; @@ -89,7 +93,7 @@ async fn main() -> anyhow::Result<()> { &new_path, coin_type, randomness_seed, - *Address::try_from_bech32(delegator_address)?.as_ed25519(), + Ed25519Address::from(delegator_address.to_inner()), WITH_SAMPLING, ) .await?; diff --git a/crates/iota-genesis-builder/src/lib.rs b/crates/iota-genesis-builder/src/lib.rs index ac464c309ce..bbde092580a 100644 --- a/crates/iota-genesis-builder/src/lib.rs +++ b/crates/iota-genesis-builder/src/lib.rs @@ -19,8 +19,8 @@ use genesis_build_effects::GenesisBuildEffects; use iota_config::{ IOTA_GENESIS_MIGRATION_TX_DATA_FILENAME, genesis::{ - Genesis, GenesisCeremonyParameters, GenesisChainParameters, TokenDistributionSchedule, - UnsignedGenesis, + Delegations, Genesis, GenesisCeremonyParameters, GenesisChainParameters, + TokenDistributionSchedule, UnsignedGenesis, }, migration_tx_data::{MigrationTxData, TransactionsData}, }; @@ -28,7 +28,7 @@ use iota_execution::{self, Executor}; use iota_framework::{BuiltInFramework, SystemPackage}; use iota_genesis_common::{execute_genesis_transaction, get_genesis_protocol_config}; use iota_protocol_config::{Chain, ProtocolConfig, ProtocolVersion}; -use iota_sdk::{Url, types::block::address::Address}; +use iota_sdk::Url; use iota_types::{ BRIDGE_ADDRESS, IOTA_BRIDGE_OBJECT_ID, IOTA_FRAMEWORK_PACKAGE_ID, IOTA_SYSTEM_ADDRESS, balance::{BALANCE_MODULE_NAME, Balance}, @@ -63,7 +63,6 @@ use iota_types::{ object::{Object, Owner}, programmable_transaction_builder::ProgrammableTransactionBuilder, randomness_state::{RANDOMNESS_MODULE_NAME, RANDOMNESS_STATE_CREATE_FUNCTION_NAME}, - stardust::stardust_to_iota_address, system_admin_cap::IOTA_SYSTEM_ADMIN_CAP_MODULE_NAME, timelock::{ stardust_upgrade_label::STARDUST_UPGRADE_LABEL_VALUE, @@ -78,7 +77,7 @@ use move_binary_format::CompiledModule; use move_core_types::ident_str; use serde::{Deserialize, Serialize}; use shared_crypto::intent::{Intent, IntentMessage, IntentScope}; -use stake::{GenesisStake, delegate_genesis_stake}; +use stake::GenesisStake; use stardust::migration::MigrationObjects; use tracing::trace; use validator_info::{GenesisValidatorInfo, GenesisValidatorMetadata, ValidatorInfo}; @@ -88,16 +87,14 @@ mod stake; pub mod stardust; pub mod validator_info; -// TODO: Lazy static `stardust_to_iota_address` -pub const IF_STARDUST_ADDRESS: &str = - "iota1qp8h9augeh6tk3uvlxqfapuwv93atv63eqkpru029p6sgvr49eufyz7katr"; - const GENESIS_BUILDER_COMMITTEE_DIR: &str = "committee"; pub const GENESIS_BUILDER_PARAMETERS_FILE: &str = "parameters"; const GENESIS_BUILDER_TOKEN_DISTRIBUTION_SCHEDULE_FILE: &str = "token-distribution-schedule"; const GENESIS_BUILDER_SIGNATURE_DIR: &str = "signatures"; const GENESIS_BUILDER_UNSIGNED_GENESIS_FILE: &str = "unsigned-genesis"; const GENESIS_BUILDER_MIGRATION_SOURCES_FILE: &str = "migration-sources"; +const GENESIS_BUILDER_DELEGATOR_FILE: &str = "delegator"; +const GENESIS_BUILDER_DELEGATOR_MAP_FILE: &str = "delegator-map"; pub const OBJECT_SNAPSHOT_FILE_PATH: &str = "stardust_object_snapshot.bin"; pub const IOTA_OBJECT_SNAPSHOT_URL: &str = "https://stardust-objects.s3.eu-central-1.amazonaws.com/iota/alphanet/latest/stardust_object_snapshot.bin.gz"; @@ -118,6 +115,15 @@ pub struct Builder { genesis_stake: GenesisStake, migration_sources: Vec, migration_tx_data: Option, + delegation: Option, +} + +enum GenesisDelegation { + /// Represents a single delegator address that applies to all validators. + OneToAll(IotaAddress), + /// Represents a map of delegator addresses to validator addresses and + /// a specified stake and gas allocation. + ManyToMany(Delegations), } impl Default for Builder { @@ -139,9 +145,20 @@ impl Builder { genesis_stake: Default::default(), migration_sources: Default::default(), migration_tx_data: Default::default(), + delegation: None, } } + pub fn with_delegator(mut self, delegator: IotaAddress) -> Self { + self.delegation = Some(GenesisDelegation::OneToAll(delegator)); + self + } + + pub fn with_delegations(mut self, delegations: Delegations) -> Self { + self.delegation = Some(GenesisDelegation::ManyToMany(delegations)); + self + } + /// Checks if the genesis to be built has no migration or if it includes /// Stardust migration stakes pub fn contains_migrations(&self) -> bool { @@ -253,19 +270,32 @@ impl Builder { /// Create and cache the [`GenesisStake`] if the builder /// contains migrated objects. + /// + /// Two cases can happen here: + /// 1. if a delegator map is given as input -> then use the map input to + /// create and cache the genesis stake. + /// 2. if a delegator map is NOT given as input -> then use one default + /// delegator passed as input and delegate the minimum required stake to + /// all validators to create and cache the genesis stake. fn create_and_cache_genesis_stake(&mut self) -> anyhow::Result<()> { if !self.migration_objects.is_empty() { - let delegator = - stardust_to_iota_address(Address::try_from_bech32(IF_STARDUST_ADDRESS).unwrap()) - .unwrap(); - // TODO: check whether we need to start with - // VALIDATOR_LOW_STAKE_THRESHOLD_NANOS - let minimum_stake = iota_types::governance::MIN_VALIDATOR_JOINING_STAKE_NANOS; - self.genesis_stake = delegate_genesis_stake( - self.validators.values(), - delegator, + self.genesis_stake = GenesisStake::new_with_delegations( + match &self.delegation { + Some(GenesisDelegation::ManyToMany(delegations)) => { + // Case 1 -> use the delegations input to create and cache the genesis stake + delegations.clone() + } + Some(GenesisDelegation::OneToAll(delegator)) => { + // Case 2 -> use one default delegator passed as input and delegate the + // minimum required stake to all validators to create the genesis stake + Delegations::new_for_validators_with_default_allocation( + self.validators.values().map(|v| v.info.iota_address()), + *delegator, + ) + } + None => bail!("no delegator/s assigned with a migration"), + }, &self.migration_objects, - minimum_stake, )?; } Ok(()) @@ -273,55 +303,76 @@ impl Builder { /// Evaluate the genesis [`TokenDistributionSchedule`]. /// - /// This merges conditionally the cached token distribution - /// (i.e. `self.token_distribution_schedule`) with the genesis stake - /// resulting from the migrated state. - /// - /// If the cached token distribution schedule contains timelocked stake, it - /// is assumed that the genesis stake is already merged and no operation - /// is performed. This is the case where we load a [`Builder`] from disk - /// that has already built genesis with the migrated state. + /// There are 6 cases for evaluating this: + /// 1. The genesis is built WITHOUT migration + /// 1. and a schedule is given as input -> then just use the input + /// schedule; + /// 2. and the schedule is NOT given as input -> then instantiate a + /// default token distribution schedule for a genesis without + /// migration. + /// 2. The genesis is built with migration, + /// 1. and token distribution schedule is given as input + /// 1. if the token distribution schedule contains a timelocked stake + /// -> then just use the input schedule, because it was initialized + /// for migration before this execution (this is the case where we + /// load a [`Builder`] from disk that has already built genesis + /// with the migrated state.); + /// 2. if the token distribution schedule does NOT contain any + /// timelocked stake -> then fetch the cached the genesis stake and + /// merge it to the token distribution schedule; + /// 2. and token distribution schedule is NOT given as input -> then + /// fetch the cached genesis stake and initialize a new token + /// distribution schedule with it. fn resolve_token_distribution_schedule(&mut self) -> TokenDistributionSchedule { - let validator_addresses = self.validators.values().map(|v| v.info.iota_address()); - let token_distribution_schedule = self.token_distribution_schedule.take(); + let is_genesis_with_migration = !self.migration_objects.is_empty(); let stardust_total_supply_nanos = self.migration_sources.len() as u64 * STARDUST_TOTAL_SUPPLY_NANOS; - if self.genesis_stake.is_empty() { - token_distribution_schedule.unwrap_or_else(|| { - TokenDistributionSchedule::new_for_validators_with_default_allocation( - validator_addresses, - ) - }) - } else if let Some(schedule) = token_distribution_schedule { - if schedule.contains_timelocked_stake() { - // Genesis stake is already included + + if let Some(schedule) = self.token_distribution_schedule.take() { + if !is_genesis_with_migration || schedule.contains_timelocked_stake() { + // Case 1.1 and 2.1.1 schedule } else { + // Case 2.1.2 self.genesis_stake .extend_token_distribution_schedule_without_migration( schedule, stardust_total_supply_nanos, ) } + } else if !is_genesis_with_migration { + // Case 1.2 + TokenDistributionSchedule::new_for_validators_with_default_allocation( + self.validators.values().map(|v| v.info.iota_address()), + ) } else { + // Case 2.2 self.genesis_stake .to_token_distribution_schedule(stardust_total_supply_nanos) } } fn build_and_cache_unsigned_genesis(&mut self) { - // Verify that all input data is valid + // Verify that all input data is valid. + // Check that if extra objects are present then it is allowed by the paramenters + // to add extra objects and it also validates the validator info self.validate_inputs().unwrap(); + // If migration sources are present, then load them into memory. + // Otherwise do nothing. self.load_migration_sources() .expect("migration sources should be loaded without errors"); + // If migration objects are present, then create and cache the genesis stake; + // this also prepares the data needed to resolve the token distribution + // schedule. Otherwise do nothing. self.create_and_cache_genesis_stake() .expect("genesis stake should be created without errors"); - // Get the token distribution schedule without migration or merge it with + // Resolve the token distribution schedule based on inputs and a possible // genesis stake let token_distribution_schedule = self.resolve_token_distribution_schedule(); + // Verify that token distribution schedule is valid token_distribution_schedule.validate(); token_distribution_schedule @@ -330,17 +381,17 @@ impl Builder { ) .expect("all validators should have the required stake"); - let objects = self.objects.clone().into_values().collect::>(); - // Finally build the genesis and migration data let (unsigned_genesis, migration_tx_data) = build_unsigned_genesis_data( &self.parameters, &token_distribution_schedule, self.validators.values(), - objects, + self.objects.clone().into_values().collect::>(), &mut self.genesis_stake, &mut self.migration_objects, ); + + // Store built data self.migration_tx_data = (!migration_tx_data.is_empty()).then_some(migration_tx_data); self.built_genesis = Some(unsigned_genesis); self.token_distribution_schedule = Some(token_distribution_schedule); @@ -809,6 +860,26 @@ impl Builder { None }; + // Load delegator + let delegator_file = path.join(GENESIS_BUILDER_DELEGATOR_FILE); + let delegator = if delegator_file.exists() { + Some(serde_json::from_slice(&fs::read(delegator_file)?)?) + } else { + None + }; + + // Load delegator map + let delegator_map_file = path.join(GENESIS_BUILDER_DELEGATOR_MAP_FILE); + let delegator_map = if delegator_map_file.exists() { + Some(Delegations::from_csv(fs::File::open(delegator_map_file)?)?) + } else { + None + }; + + let delegation = delegator + .map(GenesisDelegation::OneToAll) + .or(delegator_map.map(GenesisDelegation::ManyToMany)); + let mut builder = Self { parameters, token_distribution_schedule, @@ -820,6 +891,7 @@ impl Builder { genesis_stake: Default::default(), migration_sources, migration_tx_data, + delegation, }; let unsigned_genesis_file = path.join(GENESIS_BUILDER_UNSIGNED_GENESIS_FILE); @@ -907,6 +979,23 @@ impl Builder { .save(file)?; } + if let Some(delegation) = &self.delegation { + match delegation { + GenesisDelegation::OneToAll(delegator) => { + // Write delegator to file + let file = path.join(GENESIS_BUILDER_DELEGATOR_FILE); + let delegator_json = serde_json::to_string(delegator)?; + fs::write(file, delegator_json)?; + } + GenesisDelegation::ManyToMany(delegator_map) => { + // Write delegator map to CSV file + delegator_map.to_csv(fs::File::create( + path.join(GENESIS_BUILDER_DELEGATOR_MAP_FILE), + )?)?; + } + } + } + Ok(()) } } @@ -1009,9 +1098,9 @@ fn build_unsigned_genesis_data<'info>( // migration data. These are either timelocked coins or gas coins. The token // distribution schedule logic assumes that these assets are indeed distributed // to some addresses and this happens above during the creation of the genesis - // objects. Here then we need to burn those assets from the original set of + // objects. Here then we need to destroy those assets from the original set of // migration objects. - let migration_objects = burn_staked_migration_objects( + let migration_objects = destroy_staked_migration_objects( &mut genesis_ctx, migration_objects.take_objects(), &genesis_objects, @@ -1538,9 +1627,9 @@ pub fn generate_genesis_system_object( // Migration objects as input to this function were previously used to create a // genesis stake, that in turn helps to create a token distribution schedule for -// the genesis. In this function the objects needed for the stake are burned +// the genesis. In this function the objects needed for the stake are destroyed // (and, if needed, split) to provide a new set of migration object as output. -fn burn_staked_migration_objects( +fn destroy_staked_migration_objects( genesis_ctx: &mut TxContext, migration_objects: Vec, genesis_objects: &[Object], @@ -1577,15 +1666,15 @@ fn burn_staked_migration_objects( // Extract objects from the store let mut intermediate_store = store.into_inner(); - // Second operation: burn gas and timelocks objects. - // If the genesis stake was created, then burn gas and timelock objects that + // Second operation: destroy gas and timelocks objects. + // If the genesis stake was created, then destroy gas and timelock objects that // were added to the token distribution schedule, because they will be // created on the Move side during genesis. That means we need to prevent // cloning value by evicting these here. - for (id, _, _) in genesis_stake.take_gas_coins_to_burn() { + for (id, _, _) in genesis_stake.take_gas_coins_to_destroy() { intermediate_store.remove(&id); } - for (id, _, _) in genesis_stake.take_timelocks_to_burn() { + for (id, _, _) in genesis_stake.take_timelocks_to_destroy() { intermediate_store.remove(&id); } diff --git a/crates/iota-genesis-builder/src/stake.rs b/crates/iota-genesis-builder/src/stake.rs index bdbceac5ae7..81cab76653e 100644 --- a/crates/iota-genesis-builder/src/stake.rs +++ b/crates/iota-genesis-builder/src/stake.rs @@ -3,7 +3,8 @@ //! Logic and types to account for stake delegation during genesis. use iota_config::genesis::{ - TokenAllocation, TokenDistributionSchedule, TokenDistributionScheduleBuilder, + Delegations, TokenAllocation, TokenDistributionSchedule, TokenDistributionScheduleBuilder, + ValidatorAllocation, }; use iota_types::{ base_types::{IotaAddress, ObjectRef}, @@ -11,32 +12,29 @@ use iota_types::{ stardust::coin_kind::get_gas_balance_maybe, }; -use crate::{ - stardust::migration::{ExpirationTimestamp, MigrationObjects}, - validator_info::GenesisValidatorInfo, -}; +use crate::stardust::migration::{ExpirationTimestamp, MigrationObjects}; #[derive(Default, Debug, Clone)] pub struct GenesisStake { token_allocation: Vec, - gas_coins_to_burn: Vec, - timelocks_to_burn: Vec, + gas_coins_to_destroy: Vec, + timelocks_to_destroy: Vec, timelocks_to_split: Vec<(ObjectRef, u64, IotaAddress)>, } impl GenesisStake { - /// Take the inner gas-coin objects that must be burned. + /// Take the inner gas-coin objects that must be destroyed. /// /// This follows the semantics of [`std::mem::take`]. - pub fn take_gas_coins_to_burn(&mut self) -> Vec { - std::mem::take(&mut self.gas_coins_to_burn) + pub fn take_gas_coins_to_destroy(&mut self) -> Vec { + std::mem::take(&mut self.gas_coins_to_destroy) } - /// Take the inner timelock objects that must be burned. + /// Take the inner timelock objects that must be destroyed. /// /// This follows the semantics of [`std::mem::take`]. - pub fn take_timelocks_to_burn(&mut self) -> Vec { - std::mem::take(&mut self.timelocks_to_burn) + pub fn take_timelocks_to_destroy(&mut self) -> Vec { + std::mem::take(&mut self.timelocks_to_destroy) } /// Take the inner timelock objects that must be split. @@ -48,8 +46,8 @@ impl GenesisStake { pub fn is_empty(&self) -> bool { self.token_allocation.is_empty() - && self.gas_coins_to_burn.is_empty() - && self.timelocks_to_burn.is_empty() + && self.gas_coins_to_destroy.is_empty() + && self.timelocks_to_destroy.is_empty() } /// Calculate the total amount of token allocations. @@ -105,168 +103,332 @@ impl GenesisStake { fn calculate_pre_minted_supply(&self, total_supply_nanos: u64) -> u64 { total_supply_nanos - self.sum_token_allocation() } + + /// Creates a `GenesisStake` using a `Delegations` containing the necessary + /// allocations for validators by some delegators. + /// + /// This function invokes `delegate_genesis_stake` for each delegator found + /// in `Delegations`. + pub fn new_with_delegations( + delegations: Delegations, + migration_objects: &MigrationObjects, + ) -> anyhow::Result { + let mut stake = GenesisStake::default(); + + for (delegator, validators_allocations) in delegations.allocations { + // Fetch all timelock and gas objects owned by the delegator + let timelocks_pool = + migration_objects.get_sorted_timelocks_and_expiration_by_owner(delegator); + let gas_coins_pool = migration_objects.get_gas_coins_by_owner(delegator); + if timelocks_pool.is_none() && gas_coins_pool.is_none() { + anyhow::bail!("no timelocks or gas-coin objects found for delegator {delegator:?}"); + } + stake.delegate_genesis_stake( + &validators_allocations, + delegator, + &mut timelocks_pool.unwrap_or_default().into_iter(), + &mut gas_coins_pool + .unwrap_or_default() + .into_iter() + .map(|object| (object, 0)), + )?; + } + + Ok(stake) + } + + fn create_token_allocation( + &mut self, + recipient_address: IotaAddress, + amount_nanos: u64, + staked_with_validator: Option, + staked_with_timelock_expiration: Option, + ) { + self.token_allocation.push(TokenAllocation { + recipient_address, + amount_nanos, + staked_with_validator, + staked_with_timelock_expiration, + }); + } + + /// Create the necessary allocations for `validators_allocations` using the + /// assets of the `delegator`. + /// + /// This function iterates in turn over [`TimeLock`] and + /// [`GasCoin`][iota_types::gas_coin::GasCoin] objects created + /// during stardust migration that are owned by the `delegator`. + pub fn delegate_genesis_stake<'obj>( + &mut self, + validators_allocations: &[ValidatorAllocation], + delegator: IotaAddress, + timelocks_pool: &mut impl Iterator, + gas_coins_pool: &mut impl Iterator, + ) -> anyhow::Result<()> { + // Temp stores for holding the surplus + let mut timelock_surplus = SurplusCoin::default(); + let mut gas_surplus = SurplusCoin::default(); + + // Then, try to create new token allocations for each validator using the + // objects fetched above + for validator_allocation in validators_allocations { + // The validator address + let validator = validator_allocation.validator; + // The target amount of nanos to be staked, either with timelock or gas objects + let mut target_stake_nanos = validator_allocation.amount_nanos_to_stake; + // The gas to pay to the validator + let gas_to_pay_nanos = validator_allocation.amount_nanos_to_pay_gas; + + // Start filling allocations with timelocks + + // Pick fresh timelock objects (if present) and possibly reuse the surplus + // coming from the previous iteration. + // The method `pick_objects_for_allocation` firstly checks if the + // `timelock_surplus` can be used to reach or reduce the `target_stake_nanos`. + // Then it iterates over the `timelocks_pool`. For each timelock object, its + // balance is used to reduce the `target_stake_nanos` while its the object + // reference is placed into a vector `to_destroy`. At the end, the + // `pick_objects_for_allocation` method returns an `AllocationObjects` including + // the list of objects to destroy, the list `staked_with_timelock` containing + // the information for creating token allocations with timestamps + // and a CoinSurplus (even empty). + let mut timelock_allocation_objects = pick_objects_for_allocation( + timelocks_pool, + target_stake_nanos, + &mut timelock_surplus, + ); + if !timelock_allocation_objects.to_destroy.is_empty() { + // Inside this block some timelock objects were picked from the pool; so we can + // save all the references to timelocks to destroy + self.timelocks_to_destroy + .append(&mut timelock_allocation_objects.to_destroy); + // Finally we create some token allocations based on timelock_allocation_objects + timelock_allocation_objects + .staked_with_timelock + .iter() + .for_each(|&(timelocked_amount, expiration_timestamp)| { + // For timelocks we create a `TokenAllocation` object with + // `staked_with_timelock` filled with entries + self.create_token_allocation( + delegator, + timelocked_amount, + Some(validator), + Some(expiration_timestamp), + ); + }); + } + // The remainder of the target stake after timelock objects were used. + target_stake_nanos -= timelock_allocation_objects.amount_nanos; + + // After allocating timelocked stakes, then + // 1. allocate gas coin stakes (if timelocked funds were not enough) + // 2. and/or allocate gas coin payments (if indicated in the validator + // allocation). + + // The target amount of gas coin nanos to be allocated, either with staking or + // to pay + let target_gas_nanos = target_stake_nanos + gas_to_pay_nanos; + // Pick fresh gas coin objects (if present) and possibly reuse the surplus + // coming from the previous iteration. The logic is the same as above with + // timelocks. + let mut gas_coin_objects = + pick_objects_for_allocation(gas_coins_pool, target_gas_nanos, &mut gas_surplus); + if gas_coin_objects.amount_nanos >= target_gas_nanos { + // Inside this block some gas coin objects were picked from the pool; so we can + // save all the references to gas coins to destroy + self.gas_coins_to_destroy + .append(&mut gas_coin_objects.to_destroy); + // Then + // Case 1. allocate gas stakes + if target_stake_nanos > 0 { + // For staking gas coins we create a `TokenAllocation` object with + // an empty `staked_with_timelock` + self.create_token_allocation( + delegator, + target_stake_nanos, + Some(validator), + None, + ); + } + // Case 2. allocate gas payments + if gas_to_pay_nanos > 0 { + // For gas coins payments we create a `TokenAllocation` object with + // `recipient_address` being the validator and no stake + self.create_token_allocation(validator, gas_to_pay_nanos, None, None); + } + } else { + // It means the delegator finished all the timelock or gas funds + return Err(anyhow::anyhow!( + "Not enough funds for delegator {:?}", + delegator + )); + } + } + + // If some surplus amount is left, then return it to the delegator + // In the case of a timelock object, it must be split during the `genesis` PTB + // execution + if let (Some(surplus_timelock), surplus_nanos) = timelock_surplus.take() { + self.timelocks_to_split + .push((surplus_timelock, surplus_nanos, delegator)); + } + // In the case of a gas coin, it must be destroyed and the surplus re-allocated + // to the delegator (no split) + if let (Some(surplus_gas_coin), surplus_nanos) = gas_surplus.take() { + self.gas_coins_to_destroy.push(surplus_gas_coin); + self.create_token_allocation(delegator, surplus_nanos, None, None); + } + + Ok(()) + } } /// The objects picked for token allocation during genesis #[derive(Default, Debug, Clone)] -pub struct AllocationObjects { - inner: Vec, +struct AllocationObjects { + /// The list of objects to destroy for the allocations + to_destroy: Vec, /// The total amount of nanos to be allocated from this /// collection of objects. amount_nanos: u64, - /// The surplus amount that is not be allocated from this - /// collection of objects. - surplus_nanos: u64, /// A (possible empty) vector of (amount, timelock_expiration) pairs /// indicating the amount to timelock stake and its expiration staked_with_timelock: Vec<(u64, u64)>, } +/// The surplus object that should be split for this allocation. Only part +/// of its balance will be used for this collection of this +/// `AllocationObjects`, the surplus might be used later. +#[derive(Default, Debug, Clone)] +struct SurplusCoin { + // The reference of the coin to possibly split to get the surplus. + coin_object_ref: Option, + /// The surplus amount for that coin object. + surplus_nanos: u64, + /// Possibly indicate a timelock stake expiration. + timestamp: u64, +} + +impl SurplusCoin { + // Check if the current surplus can be reused. + // The surplus coin_object_ref is returned to be included in a `to_destroy` list + // when surplus_nanos <= target_amount_nanos. Otherwise it means the + // target_amount_nanos is completely reached, so we can still keep + // coin_object_ref as surplus coin and only reduce the surplus_nanos value. + pub fn maybe_reuse_surplus( + &mut self, + target_amount_nanos: u64, + ) -> (Option, Option, u64) { + if self.coin_object_ref.is_some() { + if self.surplus_nanos <= target_amount_nanos { + let surplus = self.surplus_nanos; + self.surplus_nanos = 0; + (self.coin_object_ref.take(), Some(surplus), self.timestamp) + } else { + self.surplus_nanos -= target_amount_nanos; + (None, Some(target_amount_nanos), self.timestamp) + } + } else { + (None, None, 0) + } + } + + // Destroy the `CoinSurplus` and take the fields. + pub fn take(self) -> (Option, u64) { + (self.coin_object_ref, self.surplus_nanos) + } +} + /// Pick gas-coin like objects from a pool to cover -/// the `target_amount`. +/// the `target_amount_nanos`. It might also make use of a previous coin +/// surplus. /// /// This does not split any surplus balance, but delegates /// splitting to the caller. -pub fn pick_objects_for_allocation<'obj>( +fn pick_objects_for_allocation<'obj>( pool: &mut impl Iterator, - target_amount: u64, + target_amount_nanos: u64, + previous_surplus_coin: &mut SurplusCoin, ) -> AllocationObjects { - let mut amount_nanos = 0; - let mut surplus_nanos = 0; + let mut allocation_tot_amount_nanos = 0; + let mut surplus_coin = SurplusCoin::default(); // Will be left empty in the case of gas coins let mut staked_with_timelock = vec![]; + let mut to_destroy = vec![]; - let objects = pool - .by_ref() - .map_while(|(object, timestamp)| { - if amount_nanos < target_amount { - let mut object_balance = get_gas_balance_maybe(object)?.value(); - // Check if remaining is needed to be handled - let remaining_needed = target_amount - amount_nanos; - if object_balance > remaining_needed { - surplus_nanos = object_balance - remaining_needed; - object_balance = remaining_needed; - } - // Finally update amount - amount_nanos += object_balance; - // Store timestamp if it is a Timelock - if timestamp > 0 { - staked_with_timelock.push((object_balance, timestamp)); - } - Some(object.compute_object_reference()) - } else { - None - } - }) - .collect(); - - AllocationObjects { - inner: objects, - amount_nanos, - surplus_nanos, - staked_with_timelock, + if let (surplus_object_option, Some(surplus_nanos), timestamp) = + previous_surplus_coin.maybe_reuse_surplus(target_amount_nanos) + { + // In here it means there are some surplus nanos that can be used. + // `maybe_reuse_surplus` already deducted the `surplus_nanos` from the + // `surplus_object`. So these can be counted in the + // `allocation_tot_amount_nanos`. + allocation_tot_amount_nanos += surplus_nanos; + // If the ´surplus_object´ is a timelock then store also its timestamp. + if timestamp > 0 { + staked_with_timelock.push((surplus_nanos, timestamp)); + } + // If the `surplus_object` is returned by `maybe_reuse_surplus`, then it means + // it used all its `surplus_nanos` and it can be destroyed. + if let Some(surplus_object) = surplus_object_option { + to_destroy.push(surplus_object); + } + // Else, if the `surplus_object` was not completely drained, then we + // don't need to continue. In this case `allocation_tot_amount_nanos == + // target_amount_nanos`. } -} -/// Create the necessary allocations to cover `amount_nanos` for all -/// `validators`. -/// -/// This function iterates in turn over [`TimeLock`] and -/// [`GasCoin`][iota_types::gas_coin::GasCoin] objects created -/// during stardust migration that are owned by the `delegator`. -pub fn delegate_genesis_stake<'info>( - validators: impl Iterator, - delegator: IotaAddress, - migration_objects: &MigrationObjects, - amount_nanos: u64, -) -> anyhow::Result { - let timelocks_pool = migration_objects.get_sorted_timelocks_and_expiration_by_owner(delegator); - let gas_coins_pool = migration_objects.get_gas_coins_by_owner(delegator); - if timelocks_pool.is_none() && gas_coins_pool.is_none() { - anyhow::bail!("no timelocks or gas-coin objects found for delegator {delegator:?}"); + // We need this check to not consume the first element of the pool in the case + // `allocation_tot_amount_nanos == target_amount_nanos`; this case can only + // happen if the `surplus_coin` contained enough balance to cover for + // `target_amount_nanos`. + if allocation_tot_amount_nanos < target_amount_nanos { + to_destroy.append( + &mut pool + .by_ref() + .map_while(|(object, timestamp)| { + if allocation_tot_amount_nanos < target_amount_nanos { + let difference_from_target = + target_amount_nanos - allocation_tot_amount_nanos; + let obj_ref = object.compute_object_reference(); + let object_balance = get_gas_balance_maybe(object)?.value(); + + if object_balance <= difference_from_target { + if timestamp > 0 { + staked_with_timelock.push((object_balance, timestamp)); + } + allocation_tot_amount_nanos += object_balance; + // Place `obj_ref` in `to_destroy` and continue + Some(obj_ref) + } else { + surplus_coin = SurplusCoin { + coin_object_ref: Some(obj_ref), + surplus_nanos: object_balance - difference_from_target, + timestamp, + }; + if timestamp > 0 { + staked_with_timelock.push((difference_from_target, timestamp)); + } + allocation_tot_amount_nanos += difference_from_target; + // Do NOT place `obj_ref` in `to_destroy` because it is reused in the + // CoinSurplus and then break the map_while + None + } + } else { + // Break the map_while + None + } + }) + .collect::>(), + ); } - let mut timelocks_pool = timelocks_pool.unwrap_or_default().into_iter(); - let mut gas_coins_pool = gas_coins_pool - .unwrap_or_default() - .into_iter() - .map(|object| (object, 0)); - let mut genesis_stake = GenesisStake::default(); - - // For each validator we try to fill their allocation up to - // total_amount_to_stake_per_validator - for validator in validators { - let target_stake = amount_nanos; - - // Start filling allocations with timelocks - let mut timelock_objects = pick_objects_for_allocation(&mut timelocks_pool, target_stake); - // TODO: This is not an optimal solution because the last timelock - // might have a surplus amount, which cannot be used without splitting. - if !timelock_objects.inner.is_empty() { - timelock_objects.staked_with_timelock.iter().for_each( - |&(timelocked_amount, expiration_timestamp)| { - // For timelocks we create a `TokenAllocation` object with - // `staked_with_timelock` filled with entries - genesis_stake.token_allocation.push(TokenAllocation { - recipient_address: delegator, - amount_nanos: timelocked_amount, - staked_with_validator: Some(validator.info.iota_address()), - staked_with_timelock_expiration: Some(expiration_timestamp), - }); - }, - ); - // Get the reference to the timelock to split needed to get exactly - // `amount_nanos` - let timelock_to_split = *timelock_objects - .inner - .last() - .expect("there should be at least two objects"); - // Save all the references to timelocks to burn - genesis_stake - .timelocks_to_burn - .append(&mut timelock_objects.inner); - // Save the reference for the token to split (and then burn) - genesis_stake.timelocks_to_split.push(( - timelock_to_split, - timelock_objects.surplus_nanos, - delegator, - )) - } - // Then cover any remaining target stake with gas coins - let remainder_target_stake = target_stake - timelock_objects.amount_nanos; - let mut gas_coin_objects = - pick_objects_for_allocation(&mut gas_coins_pool, remainder_target_stake); - genesis_stake - .gas_coins_to_burn - .append(&mut gas_coin_objects.inner); - // TODO: also here, this is not an optimal solution because the last gas object - // might have a surplus amount, which cannot be used without splitting. - if gas_coin_objects.amount_nanos < remainder_target_stake { - return Err(anyhow::anyhow!( - "Not enough funds for delegator {:?}", - delegator - )); - } else if gas_coin_objects.amount_nanos > 0 { - // For gas coins we create a `TokenAllocation` object with - // an empty`staked_with_timelock` - genesis_stake.token_allocation.push(TokenAllocation { - recipient_address: delegator, - amount_nanos: gas_coin_objects.amount_nanos, - staked_with_validator: Some(validator.info.iota_address()), - staked_with_timelock_expiration: None, - }); - if gas_coin_objects.surplus_nanos > 0 { - // This essentially schedules returning any surplus amount - // from the last coin in `gas_coin_objects` to the delegator - // as a new coin, so that the split is not needed - genesis_stake.token_allocation.push(TokenAllocation { - recipient_address: delegator, - amount_nanos: gas_coin_objects.surplus_nanos, - staked_with_validator: None, - staked_with_timelock_expiration: None, - }); - } - } + // Update the surplus coin passed from the caller + *previous_surplus_coin = surplus_coin; + + AllocationObjects { + to_destroy, + amount_nanos: allocation_tot_amount_nanos, + staked_with_timelock, } - Ok(genesis_stake) } diff --git a/crates/iota-swarm-config/src/genesis_config.rs b/crates/iota-swarm-config/src/genesis_config.rs index 35620d1911d..0ac26197131 100644 --- a/crates/iota-swarm-config/src/genesis_config.rs +++ b/crates/iota-swarm-config/src/genesis_config.rs @@ -209,6 +209,7 @@ pub struct GenesisConfig { pub parameters: GenesisCeremonyParameters, pub accounts: Vec, pub migration_sources: Vec, + pub delegator: Option, } impl Config for GenesisConfig {} @@ -390,6 +391,7 @@ impl GenesisConfig { parameters, accounts: account_configs, migration_sources: Default::default(), + delegator: Default::default(), } } @@ -411,4 +413,9 @@ impl GenesisConfig { }); self } + + pub fn add_delegator(mut self, address: IotaAddress) -> Self { + self.delegator = Some(address); + self + } } diff --git a/crates/iota-swarm-config/src/network_config_builder.rs b/crates/iota-swarm-config/src/network_config_builder.rs index cf9084b29cf..dc8e3cf7a83 100644 --- a/crates/iota-swarm-config/src/network_config_builder.rs +++ b/crates/iota-swarm-config/src/network_config_builder.rs @@ -438,6 +438,11 @@ impl ConfigBuilder { builder = builder.with_token_distribution_schedule(token_distribution_schedule); + // Add delegator to genesis builder. + if let Some(delegator) = genesis_config.delegator { + builder = builder.with_delegator(delegator); + } + for validator in &validators { builder = builder.add_validator_signature(&validator.authority_key_pair); } diff --git a/crates/iota/src/genesis_ceremony.rs b/crates/iota/src/genesis_ceremony.rs index 162acf416ce..a1366c4499d 100644 --- a/crates/iota/src/genesis_ceremony.rs +++ b/crates/iota/src/genesis_ceremony.rs @@ -10,7 +10,7 @@ use clap::Parser; use fastcrypto::encoding::{Encoding, Hex}; use iota_config::{ IOTA_GENESIS_FILENAME, - genesis::{TokenDistributionScheduleBuilder, UnsignedGenesis}, + genesis::{Delegations, TokenDistributionScheduleBuilder, UnsignedGenesis}, }; use iota_genesis_builder::{ Builder, GENESIS_BUILDER_PARAMETERS_FILE, SnapshotSource, SnapshotUrl, @@ -106,6 +106,11 @@ pub enum CeremonyCommand { }, /// List the current validators in the Genesis builder. ListValidators, + /// Initialize the validator delegations. + InitDelegations { + #[clap(long, help = "Path to the delegations file.", name = "delegations.csv")] + delegations_path: PathBuf, + }, /// Build the Genesis checkpoint. BuildUnsignedCheckpoint { #[clap( @@ -256,6 +261,14 @@ pub async fn run(cmd: Ceremony) -> Result<()> { } } + CeremonyCommand::InitDelegations { delegations_path } => { + let mut builder = Builder::load(&dir).await?; + let file = File::open(delegations_path)?; + let delegations = Delegations::from_csv(file)?; + builder = builder.with_delegations(delegations); + builder.save(dir)?; + } + CeremonyCommand::BuildUnsignedCheckpoint { local_migration_snapshots, remote_migration_snapshots, @@ -271,6 +284,7 @@ pub async fn run(cmd: Ceremony) -> Result<()> { for source in local_snapshots.chain(remote_snapshots) { builder = builder.add_migration_source(source); } + tokio::task::spawn_blocking(move || { let UnsignedGenesis { checkpoint, .. } = builder.get_or_build_unsigned_genesis(); println!( diff --git a/crates/iota/src/iota_commands.rs b/crates/iota/src/iota_commands.rs index e607b8dc183..7ea37eef7c2 100644 --- a/crates/iota/src/iota_commands.rs +++ b/crates/iota/src/iota_commands.rs @@ -229,6 +229,8 @@ pub enum IotaCommand { #[clap(long, name = "iota|")] #[arg(num_args(0..))] remote_migration_snapshots: Vec, + #[clap(long, help = "Specify the delegator address")] + delegator: Option, }, /// Bootstrap and initialize a new iota network #[clap(name = "genesis")] @@ -273,6 +275,8 @@ pub enum IotaCommand { #[clap(long, name = "iota|")] #[arg(num_args(0..))] remote_migration_snapshots: Vec, + #[clap(long, help = "Specify the delegator address")] + delegator: Option, }, /// Create an IOTA Genesis Ceremony with multiple remote validators. GenesisCeremony(Ceremony), @@ -381,6 +385,7 @@ impl IotaCommand { epoch_duration_ms, local_migration_snapshots, remote_migration_snapshots, + delegator, } => { start( config_dir.clone(), @@ -394,6 +399,7 @@ impl IotaCommand { no_full_node, local_migration_snapshots, remote_migration_snapshots, + delegator, ) .await?; @@ -410,6 +416,7 @@ impl IotaCommand { num_validators, local_migration_snapshots: with_local_migration_snapshot, remote_migration_snapshots: with_remote_migration_snapshot, + delegator, } => { genesis( from_config, @@ -422,6 +429,7 @@ impl IotaCommand { num_validators, with_local_migration_snapshot, with_remote_migration_snapshot, + delegator, ) .await } @@ -607,6 +615,7 @@ async fn start( no_full_node: bool, local_migration_snapshots: Vec, remote_migration_snapshots: Vec, + delegator: Option, ) -> Result<(), anyhow::Error> { if force_regenesis { ensure!( @@ -641,7 +650,7 @@ async fn start( if epoch_duration_ms.is_some() && genesis_blob_exists(config_dir.clone()) && !force_regenesis { bail!( - "Epoch duration can only be set when passing the `--force-regenesis` flag, or when \ + "epoch duration can only be set when passing the `--force-regenesis` flag, or when \ there is no genesis configuration in the default Iota configuration folder or the given \ network.config argument.", ); @@ -666,6 +675,17 @@ async fn start( .into_iter() .map(SnapshotSource::S3); genesis_config.migration_sources = local_snapshots.chain(remote_snapshots).collect(); + + // A delegator must be supplied when migration snapshots are provided. + if !genesis_config.migration_sources.is_empty() { + if let Some(delegator) = delegator { + // Add a delegator account to the genesis. + genesis_config = genesis_config.add_delegator(delegator); + } else { + bail!("a delegator must be supplied when migration snapshots are provided."); + } + } + swarm_builder = swarm_builder.with_genesis_config(genesis_config); let epoch_duration_ms = epoch_duration_ms.unwrap_or(DEFAULT_EPOCH_DURATION_MS); swarm_builder = swarm_builder.with_epoch_duration_ms(epoch_duration_ms); @@ -683,6 +703,7 @@ async fn start( DEFAULT_NUMBER_OF_AUTHORITIES, local_migration_snapshots, remote_migration_snapshots, + delegator, ) .await?; } @@ -809,7 +830,7 @@ async fn start( let host_ip = match faucet_address { SocketAddr::V4(addr) => *addr.ip(), - _ => bail!("Faucet configuration requires an IPv4 address"), + _ => bail!("faucet configuration requires an IPv4 address"), }; let config = FaucetConfig { @@ -886,6 +907,7 @@ async fn genesis( num_validators: usize, local_migration_snapshots: Vec, remote_migration_snapshots: Vec, + delegator: Option, ) -> Result<(), anyhow::Error> { let iota_config_dir = &match working_dir { // if a directory is specified, it must exist (it @@ -948,7 +970,7 @@ async fn genesis( } } else if files.len() != 2 || !client_path.exists() || !keystore_path.exists() { bail!( - "Cannot run genesis with non-empty Iota config directory {}, please use the --force/-f option to remove the existing configuration", + "cannot run genesis with non-empty Iota config directory {}, please use the --force/-f option to remove the existing configuration", iota_config_dir.to_str().unwrap() ); } @@ -987,6 +1009,16 @@ async fn genesis( .map(SnapshotSource::S3); genesis_conf.migration_sources = local_snapshots.chain(remote_snapshots).collect(); + // A delegator must be supplied when migration snapshots are provided. + if !genesis_conf.migration_sources.is_empty() { + if let Some(delegator) = delegator { + // Add a delegator account to the genesis. + genesis_conf = genesis_conf.add_delegator(delegator); + } else { + bail!("a delegator must be supplied when migration snapshots are provided."); + } + } + // Adds an extra faucet account to the genesis if with_faucet { info!("Adding faucet account in genesis config..."); diff --git a/crates/iota/tests/cli_tests.rs b/crates/iota/tests/cli_tests.rs index aaded6cecdb..f6fe5dfdde8 100644 --- a/crates/iota/tests/cli_tests.rs +++ b/crates/iota/tests/cli_tests.rs @@ -91,6 +91,7 @@ async fn test_genesis() -> Result<(), anyhow::Error> { num_validators: DEFAULT_NUMBER_OF_AUTHORITIES, local_migration_snapshots: vec![], remote_migration_snapshots: vec![], + delegator: None, } .execute() .await?; @@ -133,6 +134,7 @@ async fn test_genesis() -> Result<(), anyhow::Error> { num_validators: DEFAULT_NUMBER_OF_AUTHORITIES, local_migration_snapshots: vec![], remote_migration_snapshots: vec![], + delegator: None, } .execute() .await; @@ -161,6 +163,7 @@ async fn test_start() -> Result<(), anyhow::Error> { indexer_feature_args: IndexerFeatureArgs::for_testing(), local_migration_snapshots: vec![], remote_migration_snapshots: vec![], + delegator: None, } .execute(), ) diff --git a/docker/pg-services-local/docker-compose.yaml b/docker/pg-services-local/docker-compose.yaml index e5948b54dad..3c8b78ad929 100644 --- a/docker/pg-services-local/docker-compose.yaml +++ b/docker/pg-services-local/docker-compose.yaml @@ -23,6 +23,7 @@ services: - --epoch-duration-ms=120000 - --remote-migration-snapshots=https://stardust-objects.s3.eu-central-1.amazonaws.com/iota/alphanet/test/stardust_object_snapshot.bin.gz - --force-regenesis + - --delegator=0x4f72f788cdf4bb478cf9809e878e6163d5b351c82c11f1ea28750430752e7892 expose: - 9000 ports: