Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion pallets/subtensor/src/macros/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ mod hooks {
// Reset testnet conviction lock storage before deploying the current design.
.saturating_add(migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::<T>())
// Capture the runtime-upgrade block for TAO-in refund cutover.
.saturating_add(migrations::migrate_tao_in_refund_deployment_block::migrate_tao_in_refund_deployment_block::<T>());
.saturating_add(migrations::migrate_tao_in_refund_deployment_block::migrate_tao_in_refund_deployment_block::<T>())
// Fix lock state left behind by subnet-scoped hotkey swaps.
.saturating_add(migrations::migrate_fix_subnet_hotkey_lock_swaps::migrate_fix_subnet_hotkey_lock_swaps::<T>());
weight
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
use super::*;
use crate::staking::lock::LockState;
use frame_support::weights::Weight;
use scale_info::prelude::string::String;
use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32;
use substrate_fixed::types::U64F64;

struct HotkeySwapLockFix {
coldkey: Option<&'static str>,
netuid: u16,
old_hotkey: &'static str,
new_hotkey: &'static str,
}

const HOTKEY_SWAP_LOCK_FIXES: &[HotkeySwapLockFix] = &[
HotkeySwapLockFix {
coldkey: None,
netuid: 28,
old_hotkey: "5Ca8L8PkbqXUtzohKtSM3i1naGQxANGLx51kJsEPNB14Admz",
new_hotkey: "5Evgh9QTXJLxYLusVy3tcY5S6Z3GgRSNDb9AzXUchX5dco3P",
},
HotkeySwapLockFix {
coldkey: Some("5EWUPMenvyvHdEGUHfUhSTeTDJDLzLkKZq74LFLRWtzcqZiS"),
netuid: 97,
old_hotkey: "5EU83xGi9piVeTEQsjAod1Jrog7bFKuHRVQekM4LURwXqNdJ",
new_hotkey: "5DSEX7ww3K5i2rpCuv6cyvQ2nVn1qi7b5Ur86Vqop3muxXcC",
},
HotkeySwapLockFix {
coldkey: Some("5C53wCYowihKwAxwTKd7ZA8hyzBkZJ1Qqa3Ry7v75Ed6eRNP"),
netuid: 97,
old_hotkey: "5F1dKAbbJNtf4Yce8ostaU5e1iPfrL6q8cjqH1KUGbBzmees",
new_hotkey: "5CtNXpjaK79SX9QC1GqRbS3C4KNETT7jh6GgZDrmCxvYrAdJ",
},
HotkeySwapLockFix {
coldkey: Some("5EWUPMenvyvHdEGUHfUhSTeTDJDLzLkKZq74LFLRWtzcqZiS"),
netuid: 97,
old_hotkey: "5DAGsDentUAs6Uh9SYJ2uLEQvYRWu1Euqai97AHF3A7RiGoT",
new_hotkey: "5GsqcuatjJtgSwJuaZWpPV8QWcQ4aHcPkF8DG7oqMNJLoN93",
},
HotkeySwapLockFix {
coldkey: Some("5C53wCYowihKwAxwTKd7ZA8hyzBkZJ1Qqa3Ry7v75Ed6eRNP"),
netuid: 97,
old_hotkey: "5CtNXpjaK79SX9QC1GqRbS3C4KNETT7jh6GgZDrmCxvYrAdJ",
new_hotkey: "5DMN2AnnbUqbnSvbHkXHF7A8JUKBT8vxJJhKkDKG1GCwGDwf",
},
HotkeySwapLockFix {
coldkey: Some("5D2n9CKP4KQ1FMf1ybm2psFsQSMKkCiCFMRchcvo3EUECFro"),
netuid: 120,
old_hotkey: "5GCN5Bo2djDGQ6aqVjgdfMzWbLuqhcN5pyNmrbkkJ4n7jZpQ",
new_hotkey: "5GRaijFsfTR723LeofVrjrq8kNAdyucmT9TPPGdiyrxckwGg",
},
HotkeySwapLockFix {
coldkey: Some("5EWUPMenvyvHdEGUHfUhSTeTDJDLzLkKZq74LFLRWtzcqZiS"),
netuid: 120,
old_hotkey: "5EU83xGi9piVeTEQsjAod1Jrog7bFKuHRVQekM4LURwXqNdJ",
new_hotkey: "5DAGsDentUAs6Uh9SYJ2uLEQvYRWu1Euqai97AHF3A7RiGoT",
},
HotkeySwapLockFix {
coldkey: Some("5EWUPMenvyvHdEGUHfUhSTeTDJDLzLkKZq74LFLRWtzcqZiS"),
netuid: 97,
old_hotkey: "5H3Kuy7L7DBSy7BS2c9EBayJYGkHV1pzWtnJm3iXvThT4VUJ",
new_hotkey: "5CSiRF3sMKt1c3MT4KsRLBWENGkymVE7wA2zUDPsYy6JtpGE",
},
HotkeySwapLockFix {
coldkey: Some("5C53wCYowihKwAxwTKd7ZA8hyzBkZJ1Qqa3Ry7v75Ed6eRNP"),
netuid: 97,
old_hotkey: "5Cm7DPowNeA2b8b2ET4EkyqgviZUnsTUqQuqAnGp1SfXuPSw",
new_hotkey: "5GbMdbCdt4TJ94JUbf22uWGRvf17u99DdKpHuJGyMiexuCKx",
},
HotkeySwapLockFix {
coldkey: Some("5D2n9CKP4KQ1FMf1ybm2psFsQSMKkCiCFMRchcvo3EUECFro"),
netuid: 120,
old_hotkey: "5GRaijFsfTR723LeofVrjrq8kNAdyucmT9TPPGdiyrxckwGg",
new_hotkey: "5CAkU49aHNYcDVLKKYSHnBuuymWr1A7aoAiAhK8FZpNYF6YH",
},
HotkeySwapLockFix {
coldkey: Some("5DywxdtESjskgPZrDXL86qV44SpPgJuqs9X6noyJJwX9PaSD"),
netuid: 128,
old_hotkey: "5GRViDgqddpH3qB9A6nqPgMepgum51ZUZ199ksXQuCFsn128",
new_hotkey: "5Gq2gs4ft5dhhjbHabvVbAhjMCV2RgKmVJKAFCUWiirbRT21",
},
HotkeySwapLockFix {
coldkey: Some("5D2n9CKP4KQ1FMf1ybm2psFsQSMKkCiCFMRchcvo3EUECFro"),
netuid: 120,
old_hotkey: "5CAkU49aHNYcDVLKKYSHnBuuymWr1A7aoAiAhK8FZpNYF6YH",
new_hotkey: "5HbgNXyw4mCMQWLL6Hb7inA2qQ81A8pqw1GFxpcshpKu11Aj",
},
HotkeySwapLockFix {
coldkey: Some("5C53wCYowihKwAxwTKd7ZA8hyzBkZJ1Qqa3Ry7v75Ed6eRNP"),
netuid: 97,
old_hotkey: "5CfXcxCex4Up1S2SjP4MhBPM55qioPd8dCt2SEMC94m4M5Md",
new_hotkey: "5EWk5uun4rdLHfst1DXiU6e4QqTXSNpdkCtGVBGjEkDoorfN",
},
HotkeySwapLockFix {
coldkey: Some("5EWUPMenvyvHdEGUHfUhSTeTDJDLzLkKZq74LFLRWtzcqZiS"),
netuid: 97,
old_hotkey: "5HVyG7q3AiMLvG4GvkXTCfarerA3GnJ6a3r8pSVVeUiSLTng",
new_hotkey: "5GWJ5cdmEAiCL8V9sopDvntQjKtw5ciHy8urPh9AMLkpmtEw",
},
HotkeySwapLockFix {
coldkey: Some("5C8SMSqb1i3tFao2vwdAnFWM6KA38y5UFCwBCLVr5a48tXtz"),
netuid: 97,
old_hotkey: "5GCFXhD1E7aY1Eq9hDWe24fXwRe4gqJ4nxw7XcV19SwTgtoq",
new_hotkey: "5FCXQcqNd8W5CJuTgNvPjR2R82N5TMJ66sPmWPhEDs3GkgZQ",
},
HotkeySwapLockFix {
coldkey: Some("5EZWeiJunm2PCdsyUCv6UvckY5daLKGrxzRu1K9QHBAYiVhm"),
netuid: 120,
old_hotkey: "5ECiTKuujAHqf29cUDvsiEPwtAC6Yg3cT8aHJ4riAp41p1bS",
new_hotkey: "5CMFnjWR72kCMi9rChg3DZAH4MidSLHNfRaKCwyqaTyyRsev",
},
HotkeySwapLockFix {
coldkey: Some("5EWUPMenvyvHdEGUHfUhSTeTDJDLzLkKZq74LFLRWtzcqZiS"),
netuid: 120,
old_hotkey: "5DAGsDentUAs6Uh9SYJ2uLEQvYRWu1Euqai97AHF3A7RiGoT",
new_hotkey: "5HVyG7q3AiMLvG4GvkXTCfarerA3GnJ6a3r8pSVVeUiSLTng",
},
HotkeySwapLockFix {
coldkey: Some("5C53wCYowihKwAxwTKd7ZA8hyzBkZJ1Qqa3Ry7v75Ed6eRNP"),
netuid: 97,
old_hotkey: "5GbMdbCdt4TJ94JUbf22uWGRvf17u99DdKpHuJGyMiexuCKx",
new_hotkey: "5CURjyKkCiSnaSPwMBUJXLC7mkadbPPkKQamyFhdsfb5DnSp",
},
HotkeySwapLockFix {
coldkey: Some("5EWUPMenvyvHdEGUHfUhSTeTDJDLzLkKZq74LFLRWtzcqZiS"),
netuid: 97,
old_hotkey: "5CSiRF3sMKt1c3MT4KsRLBWENGkymVE7wA2zUDPsYy6JtpGE",
new_hotkey: "5EsnHJK89FgF55EYwXtqhUwLu3c14xakyQ8PWoomcFwpxk5e",
},
];

fn decode_account_id32<T: Config>(ss58_string: &str) -> Option<T::AccountId> {
let account_id32: AccountId32 = AccountId32::from_ss58check(ss58_string).ok()?;
let mut account_id32_slice: &[u8] = account_id32.as_ref();
T::AccountId::decode(&mut account_id32_slice).ok()
}

fn is_non_zero_lock(lock: &LockState) -> bool {
!lock.locked_mass.is_zero() || lock.conviction > U64F64::saturating_from_num(0)
}

fn add_lock_state(mut lhs: LockState, rhs: &LockState) -> LockState {
lhs.locked_mass = lhs.locked_mass.saturating_add(rhs.locked_mass);
lhs.conviction = lhs.conviction.saturating_add(rhs.conviction);
lhs.last_update = lhs.last_update.max(rhs.last_update);
lhs
}

fn subtract_lock_state(mut lhs: LockState, rhs: &LockState) -> LockState {
lhs.locked_mass = lhs.locked_mass.saturating_sub(rhs.locked_mass);
lhs.conviction = lhs.conviction.saturating_sub(rhs.conviction);
lhs
}

fn mutate_aggregate<T: Config, F>(
coldkey: &T::AccountId,
netuid: NetUid,
hotkey: &T::AccountId,
mutate: F,
) where
F: FnOnce(LockState) -> LockState + Clone,
{
let perpetual = DecayingLock::<T>::get(coldkey, netuid) == Some(false);
let owner = SubnetOwnerHotkey::<T>::get(netuid) == *hotkey;

match (owner, perpetual) {
(true, true) => OwnerLock::<T>::mutate(netuid, |maybe_lock| {
if let Some(lock) = maybe_lock.take() {
let updated = mutate(lock);
if is_non_zero_lock(&updated) {
*maybe_lock = Some(updated);
}
}
}),
(true, false) => DecayingOwnerLock::<T>::mutate(netuid, |maybe_lock| {
if let Some(lock) = maybe_lock.take() {
let updated = mutate(lock);
if is_non_zero_lock(&updated) {
*maybe_lock = Some(updated);
}
}
}),
(false, true) => HotkeyLock::<T>::mutate(netuid, hotkey, |maybe_lock| {
if let Some(lock) = maybe_lock.take() {
let updated = mutate(lock);
if is_non_zero_lock(&updated) {
*maybe_lock = Some(updated);
}
}
}),
(false, false) => DecayingHotkeyLock::<T>::mutate(netuid, hotkey, |maybe_lock| {
if let Some(lock) = maybe_lock.take() {
let updated = mutate(lock);
if is_non_zero_lock(&updated) {
*maybe_lock = Some(updated);
}
}
}),
}
}

fn add_to_aggregate<T: Config>(
coldkey: &T::AccountId,
netuid: NetUid,
hotkey: &T::AccountId,
added: &LockState,
) {
let perpetual = DecayingLock::<T>::get(coldkey, netuid) == Some(false);
let owner = SubnetOwnerHotkey::<T>::get(netuid) == *hotkey;

match (owner, perpetual) {
(true, true) => OwnerLock::<T>::mutate(netuid, |maybe_lock| {
*maybe_lock = Some(match maybe_lock.take() {
Some(lock) => add_lock_state(lock, added),
None => added.clone(),
});
}),
(true, false) => DecayingOwnerLock::<T>::mutate(netuid, |maybe_lock| {
*maybe_lock = Some(match maybe_lock.take() {
Some(lock) => add_lock_state(lock, added),
None => added.clone(),
});
}),
(false, true) => HotkeyLock::<T>::mutate(netuid, hotkey, |maybe_lock| {
*maybe_lock = Some(match maybe_lock.take() {
Some(lock) => add_lock_state(lock, added),
None => added.clone(),
});
}),
(false, false) => DecayingHotkeyLock::<T>::mutate(netuid, hotkey, |maybe_lock| {
*maybe_lock = Some(match maybe_lock.take() {
Some(lock) => add_lock_state(lock, added),
None => added.clone(),
});
}),
}
}

/// Fixes lock state left behind by subnet-scoped hotkey swaps.
///
/// If a destination lock already exists for the same coldkey, the old lock is
/// discarded instead of merged.
pub fn migrate_fix_subnet_hotkey_lock_swaps<T: Config>() -> Weight {
let migration_name = b"migrate_fix_subnet_hotkey_lock_swaps".to_vec();
let mut weight = T::DbWeight::get().reads(1);

if HasMigrationRun::<T>::get(&migration_name) {
log::info!(
"Migration '{:?}' has already run. Skipping.",
String::from_utf8_lossy(&migration_name)
);
return weight;
}

log::info!(
"Running migration '{}'",
String::from_utf8_lossy(&migration_name)
);

let mut moved_locks = 0u64;
let mut discarded_locks = 0u64;
let mut missing_locks = 0u64;

for fix in HOTKEY_SWAP_LOCK_FIXES {
let Some(old_hotkey) = decode_account_id32::<T>(fix.old_hotkey) else {
log::error!("Failed to decode old hotkey: {}", fix.old_hotkey);
continue;
};
let Some(new_hotkey) = decode_account_id32::<T>(fix.new_hotkey) else {
log::error!("Failed to decode new hotkey: {}", fix.new_hotkey);
continue;
};
let netuid = NetUid::from(fix.netuid);

let locks_to_fix: Vec<(T::AccountId, LockState)> = if let Some(coldkey) = fix.coldkey {
let Some(coldkey) = decode_account_id32::<T>(coldkey) else {
log::error!("Failed to decode coldkey: {}", coldkey);
continue;
};
Lock::<T>::take((coldkey.clone(), netuid, old_hotkey.clone()))
.map(|lock| vec![(coldkey, lock)])
.unwrap_or_default()
} else {
let locks: Vec<(T::AccountId, LockState)> = Lock::<T>::iter()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Runtime upgrade performs an unbounded Lock scan

For the coldkey: None fix, the migration iterates the entire Lock storage map during on_runtime_upgrade, collects matching entries, then only charges weight based on the number of matches. Lock is keyed by coldkey first, so this is a full-map scan whose execution time scales with all live locks, not the handful of affected rows. A mainnet runtime upgrade must avoid unbounded storage scans or account for them with a proven bounded input; enumerate the affected coldkeys or split the repair into bounded keys instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Runtime upgrade still scans the entire Lock map

This Lock::<T>::iter() path remains unbounded in on_runtime_upgrade. It scans every Lock entry just to find the one hotkey/netuid pair with an unknown coldkey, and the reported weight only accounts for locks_to_fix_count, not for every scanned entry. On mainnet, a large lock map can make the runtime upgrade exceed block limits or materially underreport upgrade weight. Require an explicit coldkey for this fix, use a bounded/paged migration, or otherwise avoid whole-map iteration during the upgrade.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Runtime upgrade still scans the entire Lock map

For the coldkey: None fix, this migration iterates Lock::<T>::iter() and collects every matching entry by filtering on (netuid, old_hotkey). Lock is keyed by (coldkey, netuid, hotkey), so this scans the whole map during on_runtime_upgrade; the returned weight only accounts for locks_to_fix_count, not the total entries scanned. On mainnet this can make the upgrade block exceed its real work budget. Make every fix target explicit coldkeys, or add/use a bounded index that can be iterated by (netuid, hotkey) with weight charged for every scanned entry.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Runtime upgrade still scans the entire Lock map

This coldkey: None migration path still iterates Lock::<T>::iter() and collects every matching lock for one hard-coded hotkey. Because this runs during on_runtime_upgrade, the work is bounded by total Lock storage size rather than the number of known fixes, and the returned weight only charges locks_to_fix_count + 1 reads instead of every scanned entry. On a large mainnet Lock map, that can make the runtime upgrade exceed the block budget and stall the chain. Require an explicit coldkey for this fix or use a storage layout/prefix that bounds the scan to the affected keyspace and accounts every read.

.filter_map(|((coldkey, lock_netuid, hotkey), lock)| {
(lock_netuid == netuid && hotkey == old_hotkey).then_some((coldkey, lock))
})
.collect();
for (coldkey, _) in &locks {
Lock::<T>::remove((coldkey.clone(), netuid, old_hotkey.clone()));
}
locks
};
let locks_to_fix_count = locks_to_fix.len() as u64;
weight = weight.saturating_add(
T::DbWeight::get()
.reads_writes(locks_to_fix_count.saturating_add(1), locks_to_fix_count),
);

if locks_to_fix.is_empty() {
missing_locks = missing_locks.saturating_add(1);
continue;
}

for (coldkey, lock) in locks_to_fix {
let destination_conflict =
Lock::<T>::contains_key((coldkey.clone(), netuid, new_hotkey.clone()));
weight = weight.saturating_add(T::DbWeight::get().reads(1));

let new_hotkey_is_owner = SubnetOwnerHotkey::<T>::get(netuid) == new_hotkey;
if !new_hotkey_is_owner || destination_conflict {
let removed = lock.clone();
mutate_aggregate::<T, _>(&coldkey, netuid, &old_hotkey, |aggregate| {
subtract_lock_state(aggregate, &removed)
});
weight = weight.saturating_add(T::DbWeight::get().reads_writes(2, 1));
}

if destination_conflict {
discarded_locks = discarded_locks.saturating_add(1);
continue;
}

Lock::<T>::insert((coldkey.clone(), netuid, new_hotkey.clone()), lock.clone());
weight = weight.saturating_add(T::DbWeight::get().writes(1));

if !new_hotkey_is_owner {
add_to_aggregate::<T>(&coldkey, netuid, &new_hotkey, &lock);
weight = weight.saturating_add(T::DbWeight::get().reads_writes(2, 1));
}

moved_locks = moved_locks.saturating_add(1);
}
}

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

log::info!(
"Migration '{:?}' completed successfully. Moved locks: {:?}, discarded locks: {:?}, missing locks: {:?}.",
String::from_utf8_lossy(&migration_name),
moved_locks,
discarded_locks,
missing_locks,
);

weight
}
1 change: 1 addition & 0 deletions pallets/subtensor/src/migrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod migrate_fix_root_claimed_overclaim;
pub mod migrate_fix_root_subnet_tao;
pub mod migrate_fix_root_tao_and_alpha_in;
pub mod migrate_fix_staking_hot_keys;
pub mod migrate_fix_subnet_hotkey_lock_swaps;
pub mod migrate_fix_total_issuance_evm_fees;
pub mod migrate_init_tao_flow;
pub mod migrate_init_total_issuance;
Expand Down
Loading
Loading