code/pallets/subtensor/src/migrations/migrate_seed_subnet_emission_weights.rs

migrate_seed_subnet_emission_weights.rs

285 lines · 13,019 bytes · 7ba9601d81RawGitHub
use super::*;
use frame_support::{IterableStorageMap, traits::Get, weights::Weight};
use log;
use scale_info::prelude::string::String;
use sp_std::vec::Vec;
use subtensor_runtime_common::{AlphaBalance, NetUid};

/// `HasMigrationRun` key for this migration.
pub const MIGRATION_NAME: &[u8] = b"seed_subnet_emission_weights_v1";

/// Seed every live subnet's emission weight vector from the legacy owner-cut state so the
/// first post-upgrade block pays out exactly what the last pre-upgrade block did:
///
/// * owner slot = legacy `SubnetOwnerCut` scaled to u64 parts when `OwnerCutEnabled`, else 0;
/// * validator slot = half of the remainder (the old 50/50 split);
/// * miner slot = whatever is left, absorbing the rounding ulp;
/// * effective TAO weight = the global `TaoWeight` (no per-subnet entry, so the subnet keeps
///   following root's knob until it changes its validator share);
/// * accrued owner cut = `alpha_issuance × legacy SubnetOwnerCut`, the exact deduction the
///   legacy lock refund applied before this upgrade, so dissolution pays no more than it
///   would have;
/// * `SubnetCreatedViaLease` = true for subnets with a live lease mapping (earlier leases
///   that already ended cannot be recovered and are not flagged).
///
/// Subnets that are already dissolving (out of `NetworksAdded`, still in
/// `DissolveCleanupQueue`) are paid their lock refund later from the cleanup queue, so the
/// ones whose refund is still pending get their accrued owner cut seeded too; otherwise the
/// refund would read an empty entry. Nothing else is seeded for them: the rest of the
/// vector state would only be orphaned by cleanup.
pub fn migrate_seed_subnet_emission_weights<T: Config>() -> Weight {
    let mig_name: Vec<u8> = MIGRATION_NAME.to_vec();
    let mig_name_str = String::from_utf8_lossy(&mig_name);

    let mut total_weight = T::DbWeight::get().reads(1);

    if HasMigrationRun::<T>::get(&mig_name) {
        log::info!(target: "runtime", "Migration '{mig_name_str}' already executed - skipping");
        return total_weight;
    }

    log::info!(target: "runtime", "Running migration '{mig_name_str}'");

    let legacy_owner_cut = Pallet::<T>::get_subnet_owner_cut();
    total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1));

    let netuids: Vec<NetUid> = <NetworksAdded<T> as IterableStorageMap<NetUid, bool>>::iter()
        .map(|(netuid, _)| netuid)
        .collect();
    total_weight = total_weight.saturating_add(T::DbWeight::get().reads(netuids.len() as u64));

    let mut seeded: u32 = 0;
    for netuid in netuids.into_iter().filter(|n| !n.is_root()) {
        let weights = seed_for::<T>(netuid, legacy_owner_cut);
        total_weight = total_weight.saturating_add(T::DbWeight::get().reads(2));

        SubnetEmissionWeights::<T>::insert(netuid, Pallet::<T>::emission_weights_to_vec(weights));
        SubnetTaoWeight::<T>::remove(netuid);
        SubnetOwnerCutAccrued::<T>::insert(
            netuid,
            legacy_owner_cut_accrued::<T>(netuid, legacy_owner_cut),
        );
        total_weight = total_weight.saturating_add(T::DbWeight::get().reads(2));
        if SubnetUidToLeaseId::<T>::contains_key(netuid) {
            SubnetCreatedViaLease::<T>::insert(netuid, true);
            total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1));
        }
        total_weight = total_weight.saturating_add(T::DbWeight::get().writes(4));
        seeded = seeded.saturating_add(1);
    }

    let (dissolving, dissolving_weight) = seed_dissolving_subnets::<T>(legacy_owner_cut);
    total_weight = total_weight.saturating_add(dissolving_weight);

    HasMigrationRun::<T>::insert(&mig_name, true);
    total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1));

    log::info!(
        target: "runtime",
        "Migration '{mig_name_str}' completed: seeded {seeded} subnets, {dissolving} dissolving subnets with a pending lock refund"
    );

    total_weight
}

/// Netuids in the dissolve cleanup queue whose owner lock refund has not been paid yet.
/// The subnet under cleanup is still in the queue; its phase decides, every other queued
/// subnet has not started cleanup at all.
pub fn dissolving_with_refund_pending<T: Config>() -> Vec<NetUid> {
    let current = CurrentDissolveCleanupStatus::<T>::get();
    DissolveCleanupQueue::<T>::get()
        .into_iter()
        .filter(|netuid| !netuid.is_root())
        .filter(|netuid| match &current {
            Some(status) if status.netuid == *netuid => status.phase.lock_refund_pending(),
            _ => true,
        })
        .collect()
}

/// Seed `SubnetOwnerCutAccrued` for dissolving subnets that still owe a lock refund and
/// have no entry yet. Returns `(count, weight)`.
fn seed_dissolving_subnets<T: Config>(legacy_owner_cut: u16) -> (u32, Weight) {
    let mut weight = T::DbWeight::get().reads(2);
    let mut count: u32 = 0;
    for netuid in dissolving_with_refund_pending::<T>() {
        weight = weight.saturating_add(T::DbWeight::get().reads(1));
        if SubnetOwnerCutAccrued::<T>::contains_key(netuid) {
            continue;
        }
        SubnetOwnerCutAccrued::<T>::insert(
            netuid,
            Pallet::<T>::legacy_owner_cut_accrued(netuid, legacy_owner_cut),
        );
        weight = weight.saturating_add(T::DbWeight::get().reads_writes(2, 1));
        count = count.saturating_add(1);
    }
    (count, weight)
}

/// The vector a subnet receives, given the legacy global owner cut.
pub fn seed_for<T: Config>(netuid: NetUid, legacy_owner_cut: u16) -> EmissionWeights {
    let owner = if OwnerCutEnabled::<T>::get(netuid) {
        Pallet::<T>::scale_u16_fraction_to_emission_weight(legacy_owner_cut)
    } else {
        0
    };
    Pallet::<T>::emission_weights_with_owner(owner)
}

/// The owner α the legacy lock refund would have deducted before this upgrade.
pub fn legacy_owner_cut_accrued<T: Config>(netuid: NetUid, legacy_owner_cut: u16) -> AlphaBalance {
    Pallet::<T>::legacy_owner_cut_accrued(netuid, legacy_owner_cut)
}

/// [`OnRuntimeUpgrade`] wrapper for [`migrate_seed_subnet_emission_weights`], registered in
/// the runtime `Migrations` tuple so try-runtime validates the seed against real network
/// state.
pub mod seed_subnet_emission_weights {
    use super::*;
    use frame_support::traits::OnRuntimeUpgrade;
    use sp_std::marker::PhantomData;

    #[cfg(feature = "try-runtime")]
    use crate::subnets::emission_weights::EMISSION_WEIGHT_ONE;
    #[cfg(feature = "try-runtime")]
    use codec::{Decode, Encode};
    #[cfg(feature = "try-runtime")]
    use frame_support::ensure;
    #[cfg(feature = "try-runtime")]
    use sp_runtime::TryRuntimeError;

    /// State carried from `pre_upgrade` to `post_upgrade`: whether the migration had
    /// already run, the legacy owner cut, `(netuid, owner_cut_enabled, leased,
    /// legacy_accrued_owner_alpha)` per live non-root subnet, and
    /// `(netuid, had_accrued_entry, legacy_accrued_owner_alpha)` per dissolving subnet
    /// whose lock refund is still pending.
    #[cfg(feature = "try-runtime")]
    type PreUpgradeState = (
        bool,
        u16,
        Vec<(NetUid, bool, bool, AlphaBalance)>,
        Vec<(NetUid, bool, AlphaBalance)>,
    );

    pub struct Migration<T: Config>(PhantomData<T>);

    impl<T: Config> OnRuntimeUpgrade for Migration<T> {
        fn on_runtime_upgrade() -> Weight {
            migrate_seed_subnet_emission_weights::<T>()
        }

        #[cfg(feature = "try-runtime")]
        fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
            let already_run = HasMigrationRun::<T>::get(MIGRATION_NAME.to_vec());
            let legacy_owner_cut = Pallet::<T>::get_subnet_owner_cut();
            let subnets: Vec<(NetUid, bool, bool, AlphaBalance)> =
                <NetworksAdded<T> as IterableStorageMap<NetUid, bool>>::iter()
                    .map(|(netuid, _)| netuid)
                    .filter(|n| !n.is_root())
                    .map(|netuid| {
                        (
                            netuid,
                            OwnerCutEnabled::<T>::get(netuid),
                            SubnetUidToLeaseId::<T>::contains_key(netuid),
                            legacy_owner_cut_accrued::<T>(netuid, legacy_owner_cut),
                        )
                    })
                    .collect();
            let dissolving: Vec<(NetUid, bool, AlphaBalance)> =
                dissolving_with_refund_pending::<T>()
                    .into_iter()
                    .map(|netuid| {
                        (
                            netuid,
                            SubnetOwnerCutAccrued::<T>::contains_key(netuid),
                            legacy_owner_cut_accrued::<T>(netuid, legacy_owner_cut),
                        )
                    })
                    .collect();
            log::info!(
                target: "runtime",
                "seed_subnet_emission_weights pre_upgrade: {} live subnets, {} dissolving subnets with a pending lock refund",
                subnets.len(),
                dissolving.len()
            );
            Ok((already_run, legacy_owner_cut, subnets, dissolving).encode())
        }

        #[cfg(feature = "try-runtime")]
        fn post_upgrade(state: Vec<u8>) -> Result<(), TryRuntimeError> {
            let (already_run, legacy_owner_cut, subnets, dissolving): PreUpgradeState =
                Decode::decode(&mut &state[..]).map_err(|_| "pre_upgrade state must decode")?;
            ensure!(
                HasMigrationRun::<T>::get(MIGRATION_NAME.to_vec()),
                "the migration must be stamped"
            );
            // Holds whether or not the migration ran in this upgrade: no subnet that still
            // owes a lock refund may be without an accrued owner-cut entry.
            for netuid in dissolving_with_refund_pending::<T>() {
                ensure!(
                    SubnetOwnerCutAccrued::<T>::contains_key(netuid),
                    "a dissolving subnet with a pending lock refund must have an accrued owner cut entry"
                );
            }
            if already_run {
                return Ok(());
            }
            for (netuid, had_entry, legacy_accrued) in dissolving {
                if !had_entry {
                    ensure!(
                        SubnetOwnerCutAccrued::<T>::get(netuid) == legacy_accrued,
                        "a dissolving subnet must seed its accrued owner cut to the legacy refund deduction"
                    );
                }
                ensure!(
                    !SubnetEmissionWeights::<T>::contains_key(netuid),
                    "dissolving subnets must not receive an emission weight vector"
                );
            }
            for (netuid, owner_cut_enabled, leased, legacy_accrued) in subnets {
                let weights = Pallet::<T>::get_subnet_emission_weights(netuid);
                let total: u128 = weights.iter().map(|w| *w as u128).sum();
                ensure!(
                    total == EMISSION_WEIGHT_ONE as u128,
                    "seeded vector must sum to one"
                );
                let owner = Pallet::<T>::get_subnet_owner_emission_cut(netuid);
                if owner_cut_enabled {
                    ensure!(
                        owner
                            == Pallet::<T>::scale_u16_fraction_to_emission_weight(legacy_owner_cut),
                        "owner slot must equal the scaled legacy owner cut"
                    );
                } else {
                    ensure!(owner == 0, "disabled owner cut must seed a zero owner slot");
                }
                let validator = Pallet::<T>::get_subnet_validator_emission_cut(netuid);
                ensure!(
                    validator == EMISSION_WEIGHT_ONE.saturating_sub(owner) / 2,
                    "validator slot must be half of the post-owner remainder"
                );
                ensure!(
                    !SubnetTaoWeight::<T>::contains_key(netuid)
                        && Pallet::<T>::get_subnet_tao_weight_parts(netuid)
                            == TaoWeight::<T>::get(),
                    "seeded subnets must follow the global TaoWeight"
                );
                ensure!(
                    Pallet::<T>::get_subnet_created_via_lease(netuid) == leased,
                    "lease marker must mirror the live lease mapping"
                );
                ensure!(
                    Pallet::<T>::get_subnet_burn_emission(netuid) == 0
                        && Pallet::<T>::get_subnet_recycle_emission(netuid) == 0,
                    "burn and recycle slots must seed to zero"
                );
                ensure!(
                    SubnetOwnerCutAccrued::<T>::get(netuid) == legacy_accrued,
                    "accrued owner cut must seed to the legacy refund deduction"
                );
            }
            Ok(())
        }
    }
}