//! Rebuild associations lost before the alpha-key retention fix in spec 473. //! //! Scan authoritative share rows, rather than the incomplete StakingHotkeys index or //! a snapshot list. Zero and retired share rows also keep an association alive under //! staking_hotkey_must_remain. Nonzero basket watermarks need the index independently //! of alpha. Run after conversion and the existing destructive cleanup have finished. use super::*; use codec::{Decode, DecodeWithMemTracking, Encode}; #[cfg(feature = "try-runtime")] use frame_support::traits::fungible::Inspect; use frame_support::{ storage_alias, traits::{Get, OnRuntimeUpgrade}, weights::Weight, }; use scale_info::TypeInfo; use sp_std::marker::PhantomData; pub const MIGRATION_NAME: &[u8] = b"migrate_restore_staking_hotkeys_v1"; const MAX_ROWS_PER_BLOCK: usize = 64; #[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, Default)] pub struct RestoreProgress { /// 0: AlphaV2; 1: BasketClaimed; 2: complete. pub phase: u8, pub after: Option>, pub scanned: u64, pub restored: u64, } #[storage_alias] pub type StakingHotkeysRestoreMigration = StorageValue, RestoreProgress, OptionQuery>; pub fn kickoff() -> Weight { let mut used = T::DbWeight::get().reads(2); if !HasMigrationRun::::get(MIGRATION_NAME) && !StakingHotkeysRestoreMigration::::exists() { StakingHotkeysRestoreMigration::::put(RestoreProgress::default()); used.saturating_accrue(T::DbWeight::get().writes(1)); } used } pub struct Migration(PhantomData); impl OnRuntimeUpgrade for Migration { fn on_runtime_upgrade() -> Weight { kickoff::() } #[cfg(feature = "try-runtime")] fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { Ok(::Currency::total_issuance().encode()) } #[cfg(feature = "try-runtime")] fn post_upgrade(state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { let issuance = TaoBalance::decode(&mut &state[..]) .map_err(|_| "invalid staking-hotkeys restore pre-upgrade state")?; ensure!( HasMigrationRun::::get(MIGRATION_NAME) || StakingHotkeysRestoreMigration::::exists(), "staking-hotkeys restoration was not scheduled" ); ensure!( ::Currency::total_issuance() == issuance, "staking-hotkeys restoration changed issuance" ); Ok(()) } } pub fn continue_restore(limit: Weight) -> Weight { // Reserve the status read and progress/completion writes before doing any work. let reserve = T::DbWeight::get().reads_writes(1, 2); if !reserve.all_lte(limit) { return Weight::zero(); } let mut used = T::DbWeight::get().reads(1); let Some(mut progress) = StakingHotkeysRestoreMigration::::get() else { return used; }; if progress.phase == 2 { return used; } let available = limit.saturating_sub(T::DbWeight::get().writes(2)); for _ in 0..MAX_ROWS_PER_BLOCK { // next_key and value load, plus decode_len for the destination vector. let probe = T::DbWeight::get().reads(3); if !used.saturating_add(probe).all_lte(available) { break; } // Drop the iterator before changing any storage. The cursor advances only // when the relationship has been handled, so a small budget retries the row. let next = if progress.phase == 0 { let mut iter = match &progress.after { Some(after) => AlphaV2::::iter_from(after.clone()), None => AlphaV2::::iter(), }; iter.next().map(|((hotkey, coldkey, netuid), _)| { let raw = AlphaV2::::hashed_key_for((&hotkey, &coldkey, netuid)); (hotkey, coldkey, raw, true) }) } else { let mut iter = match &progress.after { Some(after) => BasketClaimed::::iter_from(after.clone()), None => BasketClaimed::::iter(), }; iter.next().map(|(hotkey, coldkey, claimed)| { let raw = BasketClaimed::::hashed_key_for(&hotkey, &coldkey); (hotkey, coldkey, raw, claimed != 0) }) }; used.saturating_accrue(T::DbWeight::get().reads(2)); let Some((hotkey, coldkey, raw, must_remain)) = next else { progress.phase = progress.phase.saturating_add(1); progress.after = None; if progress.phase == 2 { HasMigrationRun::::insert(MIGRATION_NAME, true); used.saturating_accrue(T::DbWeight::get().writes(1)); break; } continue; }; if must_remain { // Reuse the existing conservative, length-dependent vector-walk // allowance, including oversized historical lists. Repair must not // silently omit existing positions at the admission cap. let rewrite = Pallet::::staking_hotkeys_walk_actual(&coldkey) .saturating_add(T::DbWeight::get().writes(1)); used.saturating_accrue(T::DbWeight::get().reads(1)); if !used.saturating_add(rewrite).all_lte(available) { break; } let mut hotkeys = StakingHotkeys::::get(&coldkey); if !hotkeys.contains(&hotkey) { hotkeys.push(hotkey); StakingHotkeys::::insert(&coldkey, hotkeys); progress.restored = progress.restored.saturating_add(1); } used.saturating_accrue(rewrite); } progress.after = Some(raw); progress.scanned = progress.scanned.saturating_add(1); } // Keep final counters for chain audits and make upgrades idempotent. StakingHotkeysRestoreMigration::::put(progress); used.saturating_add(T::DbWeight::get().writes(1)) }