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() -> Weight { let mig_name: Vec = 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::::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::::get_subnet_owner_cut(); total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); let netuids: Vec = as IterableStorageMap>::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::(netuid, legacy_owner_cut); total_weight = total_weight.saturating_add(T::DbWeight::get().reads(2)); SubnetEmissionWeights::::insert(netuid, Pallet::::emission_weights_to_vec(weights)); SubnetTaoWeight::::remove(netuid); SubnetOwnerCutAccrued::::insert( netuid, legacy_owner_cut_accrued::(netuid, legacy_owner_cut), ); total_weight = total_weight.saturating_add(T::DbWeight::get().reads(2)); if SubnetUidToLeaseId::::contains_key(netuid) { SubnetCreatedViaLease::::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::(legacy_owner_cut); total_weight = total_weight.saturating_add(dissolving_weight); HasMigrationRun::::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() -> Vec { let current = CurrentDissolveCleanupStatus::::get(); DissolveCleanupQueue::::get() .into_iter() .filter(|netuid| !netuid.is_root()) .filter(|netuid| match ¤t { 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(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::() { weight = weight.saturating_add(T::DbWeight::get().reads(1)); if SubnetOwnerCutAccrued::::contains_key(netuid) { continue; } SubnetOwnerCutAccrued::::insert( netuid, Pallet::::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(netuid: NetUid, legacy_owner_cut: u16) -> EmissionWeights { let owner = if OwnerCutEnabled::::get(netuid) { Pallet::::scale_u16_fraction_to_emission_weight(legacy_owner_cut) } else { 0 }; Pallet::::emission_weights_with_owner(owner) } /// The owner α the legacy lock refund would have deducted before this upgrade. pub fn legacy_owner_cut_accrued(netuid: NetUid, legacy_owner_cut: u16) -> AlphaBalance { Pallet::::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(PhantomData); impl OnRuntimeUpgrade for Migration { fn on_runtime_upgrade() -> Weight { migrate_seed_subnet_emission_weights::() } #[cfg(feature = "try-runtime")] fn pre_upgrade() -> Result, TryRuntimeError> { let already_run = HasMigrationRun::::get(MIGRATION_NAME.to_vec()); let legacy_owner_cut = Pallet::::get_subnet_owner_cut(); let subnets: Vec<(NetUid, bool, bool, AlphaBalance)> = as IterableStorageMap>::iter() .map(|(netuid, _)| netuid) .filter(|n| !n.is_root()) .map(|netuid| { ( netuid, OwnerCutEnabled::::get(netuid), SubnetUidToLeaseId::::contains_key(netuid), legacy_owner_cut_accrued::(netuid, legacy_owner_cut), ) }) .collect(); let dissolving: Vec<(NetUid, bool, AlphaBalance)> = dissolving_with_refund_pending::() .into_iter() .map(|netuid| { ( netuid, SubnetOwnerCutAccrued::::contains_key(netuid), legacy_owner_cut_accrued::(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) -> Result<(), TryRuntimeError> { let (already_run, legacy_owner_cut, subnets, dissolving): PreUpgradeState = Decode::decode(&mut &state[..]).map_err(|_| "pre_upgrade state must decode")?; ensure!( HasMigrationRun::::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::() { ensure!( SubnetOwnerCutAccrued::::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::::get(netuid) == legacy_accrued, "a dissolving subnet must seed its accrued owner cut to the legacy refund deduction" ); } ensure!( !SubnetEmissionWeights::::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::::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::::get_subnet_owner_emission_cut(netuid); if owner_cut_enabled { ensure!( owner == Pallet::::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::::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::::contains_key(netuid) && Pallet::::get_subnet_tao_weight_parts(netuid) == TaoWeight::::get(), "seeded subnets must follow the global TaoWeight" ); ensure!( Pallet::::get_subnet_created_via_lease(netuid) == leased, "lease marker must mirror the live lease mapping" ); ensure!( Pallet::::get_subnet_burn_emission(netuid) == 0 && Pallet::::get_subnet_recycle_emission(netuid) == 0, "burn and recycle slots must seed to zero" ); ensure!( SubnetOwnerCutAccrued::::get(netuid) == legacy_accrued, "accrued owner cut must seed to the legacy refund deduction" ); } Ok(()) } } }