From 7e9066869d2d11c1ce9545be5ef2d27cfcc6a8eb Mon Sep 17 00:00:00 2001 From: nonast <29281463+nonast@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:56:44 +0800 Subject: [PATCH] feat(genesis): add delegator map type to genesis builder --- crates/iota-config/src/genesis.rs | 114 +++++++++++- crates/iota-genesis-builder/src/lib.rs | 46 +++-- crates/iota-genesis-builder/src/stake.rs | 210 +++++++++++------------ crates/iota/src/genesis_ceremony.rs | 17 +- 4 files changed, 257 insertions(+), 130 deletions(-) diff --git a/crates/iota-config/src/genesis.rs b/crates/iota-config/src/genesis.rs index 3cb724e34b6..766f24ce4e1 100644 --- a/crates/iota-config/src/genesis.rs +++ b/crates/iota-config/src/genesis.rs @@ -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 = @@ -628,3 +623,110 @@ impl TokenDistributionScheduleBuilder { schedule } } + +#[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 address: 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, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct DelegatorDistribution { + /// The address from which take the nanos for staking/gas + pub delegator: IotaAddress, + /// The allocation to a validator receiving a stake and/or a gas payment + pub validator_allocation: ValidatorAllocation, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct DelegatorMap { + // Maps a delegator address to a tuple containing the address of a validator (1nd element of + // the tuple) that will receive an amount of nanos to stake (2nd element) and an amount as gas + // payment (3rd element). + pub allocations: HashMap>, +} + +impl DelegatorMap { + pub fn new_for_validators_with_default_allocation>( + validators: I, + delegator: IotaAddress, + ) -> Self { + let default_allocation = iota_types::governance::MIN_VALIDATOR_JOINING_STAKE_NANOS; + + let allocations = validators.into_iter().fold( + HashMap::new(), + |mut allocations: HashMap>, address| { + allocations + .entry(delegator) + .or_default() + .push(ValidatorAllocation { + address, + amount_nanos_to_stake: default_allocation, + amount_nanos_to_pay_gas: 0, + }); + allocations + }, + ); + + Self { allocations } + } + + /// Helper to read a DelegatorMap 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 allocations = reader + .deserialize::() + .collect::, _>>()? + .into_iter() + .fold( + HashMap::new(), + |mut allocations: HashMap>, allocation| { + allocations + .entry(allocation.delegator) + .or_default() + .push(allocation.validator_allocation); + allocations + }, + ); + + Ok(Self { allocations }) + } + + /// Helper to write a DelegatorMap 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); + + for (&delegator, validators_allocations) in &self.allocations { + for &validator_allocation in validators_allocations { + writer.serialize(DelegatorDistribution { + delegator, + validator_allocation, + })?; + } + } + + Ok(()) + } +} diff --git a/crates/iota-genesis-builder/src/lib.rs b/crates/iota-genesis-builder/src/lib.rs index 97cc6710209..02792a1e12b 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, + DelegatorMap, Genesis, GenesisCeremonyParameters, GenesisChainParameters, + TokenDistributionSchedule, UnsignedGenesis, }, migration_tx_data::{MigrationTxData, TransactionsData}, }; @@ -78,7 +78,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}; @@ -115,6 +115,7 @@ pub struct Builder { migration_sources: Vec, migration_tx_data: Option, delegator: Option, + delegator_map: Option, } impl Default for Builder { @@ -137,6 +138,7 @@ impl Builder { migration_sources: Default::default(), migration_tx_data: Default::default(), delegator: None, + delegator_map: None, } } @@ -145,6 +147,11 @@ impl Builder { self } + pub fn with_delegator_map(mut self, delegator_map: DelegatorMap) -> Self { + self.delegator_map = Some(delegator_map); + self + } + /// Checks if the genesis to be built has no migration or if it includes /// Stardust migration stakes pub fn contains_migrations(&self) -> bool { @@ -258,23 +265,27 @@ impl Builder { /// contains migrated objects. fn create_and_cache_genesis_stake(&mut self) -> anyhow::Result<()> { if !self.migration_objects.is_empty() { - if let Some(if_delegator_address) = &self.delegator { - let delegator = stardust_to_iota_address( - Address::try_from_bech32(if_delegator_address).unwrap(), + let mut genesis_stake = GenesisStake::default(); + + let delegator_map = if let Some(delegator_map) = &self.delegator_map { + delegator_map.clone() + } else if let Some(delegator) = &self.delegator { + DelegatorMap::new_for_validators_with_default_allocation( + self.validators.values().map(|v| v.info.iota_address()), + stardust_to_iota_address(Address::try_from_bech32(delegator)?)?, ) - .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(), + } else { + bail!("no delegator/s assigned with a migration"); + }; + + for (delegator, validators_allocations) in delegator_map.allocations { + genesis_stake.delegate_genesis_stake( + &validators_allocations, delegator, &self.migration_objects, - minimum_stake, )?; - } else { - bail!("A genesis with migrated state should have a delegator assigned"); } + self.genesis_stake = genesis_stake; } Ok(()) } @@ -828,7 +839,8 @@ impl Builder { genesis_stake: Default::default(), migration_sources, migration_tx_data, - delegator: None, + delegator: None, // todo: Probably need to load the delegator? + delegator_map: None, // todo: Probably need to load the delegator_map? }; let unsigned_genesis_file = path.join(GENESIS_BUILDER_UNSIGNED_GENESIS_FILE); @@ -916,6 +928,8 @@ impl Builder { .save(file)?; } + // todo: probably need to save the delegator and delegator_map? + Ok(()) } } diff --git a/crates/iota-genesis-builder/src/stake.rs b/crates/iota-genesis-builder/src/stake.rs index bdbceac5ae7..b15ac5ddefc 100644 --- a/crates/iota-genesis-builder/src/stake.rs +++ b/crates/iota-genesis-builder/src/stake.rs @@ -4,6 +4,7 @@ //! Logic and types to account for stake delegation during genesis. use iota_config::genesis::{ TokenAllocation, TokenDistributionSchedule, TokenDistributionScheduleBuilder, + ValidatorAllocation, }; use iota_types::{ base_types::{IotaAddress, ObjectRef}, @@ -11,10 +12,7 @@ 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 { @@ -105,6 +103,107 @@ impl GenesisStake { fn calculate_pre_minted_supply(&self, total_supply_nanos: u64) -> u64 { total_supply_nanos - self.sum_token_allocation() } + + /// 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( + &mut self, + validators_allocations: &[ValidatorAllocation], + delegator: IotaAddress, + migration_objects: &MigrationObjects, + ) -> 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:?}"); + } + 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)); + + // For each validator we try to fill their allocation up to + // total_amount_to_stake_per_validator + for validator_allocation in validators_allocations { + let validator = validator_allocation.address; + let target_stake = validator_allocation.amount_nanos_to_stake; + + // 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 + self.token_allocation.push(TokenAllocation { + recipient_address: delegator, + amount_nanos: timelocked_amount, + staked_with_validator: Some(validator), + 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 + self.timelocks_to_burn.append(&mut timelock_objects.inner); + // Save the reference for the token to split (and then burn) + self.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); + self.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` + self.token_allocation.push(TokenAllocation { + recipient_address: delegator, + amount_nanos: gas_coin_objects.amount_nanos, + staked_with_validator: Some(validator), + 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 + self.token_allocation.push(TokenAllocation { + recipient_address: delegator, + amount_nanos: gas_coin_objects.surplus_nanos, + staked_with_validator: None, + staked_with_timelock_expiration: None, + }); + } + } + } + Ok(()) + } } /// The objects picked for token allocation during genesis @@ -167,106 +266,3 @@ pub fn pick_objects_for_allocation<'obj>( staked_with_timelock, } } - -/// 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:?}"); - } - 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, - }); - } - } - } - Ok(genesis_stake) -} diff --git a/crates/iota/src/genesis_ceremony.rs b/crates/iota/src/genesis_ceremony.rs index 162acf416ce..2371f20bf72 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::{DelegatorMap, TokenDistributionScheduleBuilder, UnsignedGenesis}, }; use iota_genesis_builder::{ Builder, GENESIS_BUILDER_PARAMETERS_FILE, SnapshotSource, SnapshotUrl, @@ -118,6 +118,12 @@ pub enum CeremonyCommand { #[clap(long, name = "iota|", help = "Remote migration snapshots.")] #[arg(num_args(0..))] remote_migration_snapshots: Vec, + #[clap( + long, + help = "Path to the delegator map file.", + name = "delegator_map.csv" + )] + delegator_map: Option, }, /// Examine the details of the built Genesis checkpoint. ExamineGenesisCheckpoint, @@ -259,6 +265,7 @@ pub async fn run(cmd: Ceremony) -> Result<()> { CeremonyCommand::BuildUnsignedCheckpoint { local_migration_snapshots, remote_migration_snapshots, + delegator_map, } => { let local_snapshots = local_migration_snapshots .into_iter() @@ -271,6 +278,13 @@ pub async fn run(cmd: Ceremony) -> Result<()> { for source in local_snapshots.chain(remote_snapshots) { builder = builder.add_migration_source(source); } + + if let Some(delegator_map) = delegator_map { + let file = File::open(delegator_map)?; + let delegator_map = DelegatorMap::from_csv(file)?; + builder = builder.with_delegator_map(delegator_map); + } + tokio::task::spawn_blocking(move || { let UnsignedGenesis { checkpoint, .. } = builder.get_or_build_unsigned_genesis(); println!( @@ -472,6 +486,7 @@ mod test { command: CeremonyCommand::BuildUnsignedCheckpoint { local_migration_snapshots: vec![], remote_migration_snapshots: vec![], + delegator_map: None, }, }; command.run().await?;