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?;