diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index fa7fbb3b16..4639dc2e91 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -171,7 +171,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::()) // 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::()); + .saturating_add(migrations::migrate_tao_in_refund_deployment_block::migrate_tao_in_refund_deployment_block::()) + // 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::()); weight } diff --git a/pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs b/pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs new file mode 100644 index 0000000000..7ff1dea7c8 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs @@ -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(ss58_string: &str) -> Option { + 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( + coldkey: &T::AccountId, + netuid: NetUid, + hotkey: &T::AccountId, + mutate: F, +) where + F: FnOnce(LockState) -> LockState + Clone, +{ + let perpetual = DecayingLock::::get(coldkey, netuid) == Some(false); + let owner = SubnetOwnerHotkey::::get(netuid) == *hotkey; + + match (owner, perpetual) { + (true, true) => OwnerLock::::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::::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::::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::::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( + coldkey: &T::AccountId, + netuid: NetUid, + hotkey: &T::AccountId, + added: &LockState, +) { + let perpetual = DecayingLock::::get(coldkey, netuid) == Some(false); + let owner = SubnetOwnerHotkey::::get(netuid) == *hotkey; + + match (owner, perpetual) { + (true, true) => OwnerLock::::mutate(netuid, |maybe_lock| { + *maybe_lock = Some(match maybe_lock.take() { + Some(lock) => add_lock_state(lock, added), + None => added.clone(), + }); + }), + (true, false) => DecayingOwnerLock::::mutate(netuid, |maybe_lock| { + *maybe_lock = Some(match maybe_lock.take() { + Some(lock) => add_lock_state(lock, added), + None => added.clone(), + }); + }), + (false, true) => HotkeyLock::::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::::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() -> Weight { + let migration_name = b"migrate_fix_subnet_hotkey_lock_swaps".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::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::(fix.old_hotkey) else { + log::error!("Failed to decode old hotkey: {}", fix.old_hotkey); + continue; + }; + let Some(new_hotkey) = decode_account_id32::(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::(coldkey) else { + log::error!("Failed to decode coldkey: {}", coldkey); + continue; + }; + Lock::::take((coldkey.clone(), netuid, old_hotkey.clone())) + .map(|lock| vec![(coldkey, lock)]) + .unwrap_or_default() + } else { + let locks: Vec<(T::AccountId, LockState)> = Lock::::iter() + .filter_map(|((coldkey, lock_netuid, hotkey), lock)| { + (lock_netuid == netuid && hotkey == old_hotkey).then_some((coldkey, lock)) + }) + .collect(); + for (coldkey, _) in &locks { + Lock::::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::::contains_key((coldkey.clone(), netuid, new_hotkey.clone())); + weight = weight.saturating_add(T::DbWeight::get().reads(1)); + + let new_hotkey_is_owner = SubnetOwnerHotkey::::get(netuid) == new_hotkey; + if !new_hotkey_is_owner || destination_conflict { + let removed = lock.clone(); + mutate_aggregate::(&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::::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::(&coldkey, netuid, &new_hotkey, &lock); + weight = weight.saturating_add(T::DbWeight::get().reads_writes(2, 1)); + } + + moved_locks = moved_locks.saturating_add(1); + } + } + + HasMigrationRun::::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 +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index 0e84214099..04f7615293 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -24,6 +24,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; diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index 27c5e5d646..aa4a6508ab 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -1384,14 +1384,29 @@ impl Pallet { /// Conviction is not reset because the hotkey ownership does not change, it's still /// the same hotkey owner who will own the new hotkey. pub fn swap_hotkey_locks(old_hotkey: &T::AccountId, new_hotkey: &T::AccountId) -> (u64, u64) { + Self::swap_hotkey_locks_for_netuids(old_hotkey, new_hotkey, Self::get_all_subnet_netuids()) + } + + /// Swap locks made to the old_hotkey to new_hotkey on one netuid. + pub fn swap_hotkey_locks_on_subnet( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuid: NetUid, + ) -> (u64, u64) { + Self::swap_hotkey_locks_for_netuids(old_hotkey, new_hotkey, vec![netuid]) + } + + fn swap_hotkey_locks_for_netuids( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuids: Vec, + ) -> (u64, u64) { let mut locks_to_transfer: Vec<(T::AccountId, NetUid, LockState)> = Vec::new(); let mut netuids_to_transfer: Vec<(NetUid, bool, bool)> = Vec::new(); let mut reads: u64 = 0; let mut writes: u64 = 0; - let netuids = Self::get_all_subnet_netuids(); - - for netuid in netuids { + for netuid in netuids.iter().copied() { let old_is_owner_hotkey = Self::is_subnet_owner_hotkey(netuid, old_hotkey); let new_is_owner_hotkey = Self::is_subnet_owner_hotkey(netuid, new_hotkey); let has_hotkey_lock = HotkeyLock::::contains_key(netuid, old_hotkey); @@ -1419,7 +1434,11 @@ impl Pallet { if !netuids_to_transfer.is_empty() { for ((coldkey, netuid, hotkey), lock) in Lock::::iter() { - if hotkey == *old_hotkey { + if hotkey == *old_hotkey + && netuids_to_transfer + .iter() + .any(|(rebuild_netuid, _, _)| *rebuild_netuid == netuid) + { locks_to_transfer.push((coldkey, netuid, lock)); } reads = reads.saturating_add(1); diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 944ea5877f..4c8a0af5a8 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -350,7 +350,11 @@ impl Pallet { weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); } - // 9. Perform the hotkey swap + // 9. Swap the stake locks + let (reads, writes) = Self::swap_hotkey_locks_on_subnet(old_hotkey, new_hotkey, netuid); + weight.saturating_accrue(T::DbWeight::get().reads_writes(reads, writes)); + + // 10. Perform the hotkey swap Self::perform_hotkey_swap_on_one_subnet( old_hotkey, new_hotkey, @@ -359,12 +363,12 @@ impl Pallet { keep_stake, )?; - // 10. Update the last transaction block for the coldkey + // 11. Update the last transaction block for the coldkey Self::set_last_tx_block(coldkey, block); LastHotkeySwapOnNetuid::::insert(netuid, coldkey, block); weight.saturating_accrue(T::DbWeight::get().writes(2)); - // 11. Emit an event for the hotkey swap + // 12. Emit an event for the hotkey swap Self::deposit_event(Event::HotkeySwappedOnSubnet { coldkey: coldkey.clone(), old_hotkey: old_hotkey.clone(), diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index ecefd49a6f..47f18cafa6 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -3836,3 +3836,172 @@ fn test_moving_partial_lock_same_owners() { ); }); } + +#[test] +fn test_hotkey_swap_moves_lock_and_conviction_to_new_hotkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let old_hotkey = U256::from(2); + let new_hotkey = U256::from(3); + let netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); + let lock_amount: AlphaBalance = 5000u64.into(); + let conviction = U64F64::from_num(1000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &old_hotkey, + lock_amount, + )); + + let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); + lock.conviction = conviction; + Lock::::insert((coldkey, netuid, old_hotkey), lock); + + let mut hotkey_lock = HotkeyLock::::get(netuid, old_hotkey).unwrap(); + hotkey_lock.conviction = conviction; + HotkeyLock::::insert(netuid, old_hotkey, hotkey_lock); + + add_balance_to_coldkey_account( + &coldkey, + (SubtensorModule::get_key_swap_cost() + 1000.into()).into(), + ); + assert_ok!(SubtensorModule::do_swap_hotkey( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + None, + false, + )); + + assert!(Lock::::get((coldkey, netuid, old_hotkey)).is_none()); + assert!(HotkeyLock::::get(netuid, old_hotkey).is_none()); + + let moved_lock = Lock::::get((coldkey, netuid, new_hotkey)).unwrap(); + assert_eq!(moved_lock.locked_mass, lock_amount); + assert_eq!(moved_lock.conviction, conviction); + + let moved_hotkey_lock = HotkeyLock::::get(netuid, new_hotkey).unwrap(); + assert_eq!(moved_hotkey_lock.locked_mass, lock_amount); + assert_eq!(moved_hotkey_lock.conviction, conviction); + assert_eq!( + SubtensorModule::hotkey_conviction(&new_hotkey, netuid), + conviction + ); + }); +} + +#[test] +fn test_swap_hotkey_v2_on_subnet_moves_lock_and_conviction_to_new_hotkey() { + new_test_ext(100).execute_with(|| { + let coldkey = U256::from(1); + let old_hotkey = U256::from(2); + let new_hotkey = U256::from(3); + let netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); + let lock_amount: AlphaBalance = 5000u64.into(); + let conviction = U64F64::from_num(1000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &old_hotkey, + lock_amount, + )); + + let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); + lock.conviction = conviction; + Lock::::insert((coldkey, netuid, old_hotkey), lock); + + let mut hotkey_lock = HotkeyLock::::get(netuid, old_hotkey).unwrap(); + hotkey_lock.conviction = conviction; + HotkeyLock::::insert(netuid, old_hotkey, hotkey_lock); + + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000u64.into()); + assert_ok!(SubtensorModule::swap_hotkey_v2( + RuntimeOrigin::signed(coldkey), + old_hotkey, + new_hotkey, + Some(netuid), + false, + )); + + assert!(Lock::::get((coldkey, netuid, old_hotkey)).is_none()); + assert!(HotkeyLock::::get(netuid, old_hotkey).is_none()); + + let moved_lock = Lock::::get((coldkey, netuid, new_hotkey)).unwrap(); + assert_eq!(moved_lock.locked_mass, lock_amount); + assert_eq!(moved_lock.conviction, conviction); + + let moved_hotkey_lock = HotkeyLock::::get(netuid, new_hotkey).unwrap(); + assert_eq!(moved_hotkey_lock.locked_mass, lock_amount); + assert_eq!(moved_hotkey_lock.conviction, conviction); + assert_eq!( + SubtensorModule::hotkey_conviction(&new_hotkey, netuid), + conviction + ); + }); +} + +#[test] +fn test_swap_hotkey_v2_on_subnet_does_not_move_locks_on_other_subnets() { + new_test_ext(100).execute_with(|| { + let coldkey = U256::from(1); + let old_hotkey = U256::from(2); + let new_hotkey = U256::from(3); + let swapped_netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); + let untouched_netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); + let lock_amount: AlphaBalance = 5000u64.into(); + let conviction = U64F64::from_num(1000); + + for netuid in [swapped_netuid, untouched_netuid] { + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &old_hotkey, + lock_amount, + )); + + let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); + lock.conviction = conviction; + Lock::::insert((coldkey, netuid, old_hotkey), lock); + + let mut hotkey_lock = HotkeyLock::::get(netuid, old_hotkey).unwrap(); + hotkey_lock.conviction = conviction; + HotkeyLock::::insert(netuid, old_hotkey, hotkey_lock); + } + + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000u64.into()); + assert_ok!(SubtensorModule::swap_hotkey_v2( + RuntimeOrigin::signed(coldkey), + old_hotkey, + new_hotkey, + Some(swapped_netuid), + false, + )); + + assert!(Lock::::get((coldkey, swapped_netuid, old_hotkey)).is_none()); + assert!(HotkeyLock::::get(swapped_netuid, old_hotkey).is_none()); + assert_eq!( + Lock::::get((coldkey, swapped_netuid, new_hotkey)) + .unwrap() + .conviction, + conviction + ); + assert_eq!( + HotkeyLock::::get(swapped_netuid, new_hotkey) + .unwrap() + .conviction, + conviction + ); + + let untouched_lock = Lock::::get((coldkey, untouched_netuid, old_hotkey)).unwrap(); + assert_eq!(untouched_lock.locked_mass, lock_amount); + assert_eq!(untouched_lock.conviction, conviction); + assert!(Lock::::get((coldkey, untouched_netuid, new_hotkey)).is_none()); + + let untouched_hotkey_lock = HotkeyLock::::get(untouched_netuid, old_hotkey).unwrap(); + assert_eq!(untouched_hotkey_lock.locked_mass, lock_amount); + assert_eq!(untouched_hotkey_lock.conviction, conviction); + assert!(HotkeyLock::::get(untouched_netuid, new_hotkey).is_none()); + }); +} diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 75be412858..6f2c3b2609 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -68,6 +68,143 @@ fn test_migrate_tao_in_refund_deployment_block() { assert_eq!(TaoInRefundDeploymentBlock::::get(), deployment_block); }); } + +#[test] +fn test_migrate_fix_subnet_hotkey_lock_swaps_moves_or_discards_conflicts() { + new_test_ext(1).execute_with(|| { + let migration_name = b"migrate_fix_subnet_hotkey_lock_swaps".to_vec(); + let old_hotkey = + decode_account_id32::("5Ca8L8PkbqXUtzohKtSM3i1naGQxANGLx51kJsEPNB14Admz") + .expect("old hotkey should decode"); + let new_hotkey = + decode_account_id32::("5Evgh9QTXJLxYLusVy3tcY5S6Z3GgRSNDb9AzXUchX5dco3P") + .expect("new hotkey should decode"); + let netuid = NetUid::from(28); + let coldkey_to_move = U256::from(1); + let coldkey_with_conflict = U256::from(2); + let chained_coldkey = + decode_account_id32::("5EWUPMenvyvHdEGUHfUhSTeTDJDLzLkKZq74LFLRWtzcqZiS") + .expect("chained coldkey should decode"); + let chained_first_hotkey = + decode_account_id32::("5H3Kuy7L7DBSy7BS2c9EBayJYGkHV1pzWtnJm3iXvThT4VUJ") + .expect("chained first hotkey should decode"); + let chained_middle_hotkey = + decode_account_id32::("5CSiRF3sMKt1c3MT4KsRLBWENGkymVE7wA2zUDPsYy6JtpGE") + .expect("chained middle hotkey should decode"); + let chained_final_hotkey = + decode_account_id32::("5EsnHJK89FgF55EYwXtqhUwLu3c14xakyQ8PWoomcFwpxk5e") + .expect("chained final hotkey should decode"); + let chained_netuid = NetUid::from(97); + + HasMigrationRun::::remove(&migration_name); + + let moved_lock = LockState { + locked_mass: AlphaBalance::from(10_u64), + conviction: U64F64::from_num(3), + last_update: 11, + }; + let discarded_lock = LockState { + locked_mass: AlphaBalance::from(20_u64), + conviction: U64F64::from_num(5), + last_update: 12, + }; + let existing_destination_lock = LockState { + locked_mass: AlphaBalance::from(77_u64), + conviction: U64F64::from_num(7), + last_update: 10, + }; + let chained_lock = LockState { + locked_mass: AlphaBalance::from(33_u64), + conviction: U64F64::from_num(4), + last_update: 13, + }; + + Lock::::insert( + (coldkey_to_move, netuid, old_hotkey), + moved_lock.clone(), + ); + Lock::::insert( + (coldkey_with_conflict, netuid, old_hotkey), + discarded_lock.clone(), + ); + Lock::::insert( + (coldkey_with_conflict, netuid, new_hotkey), + existing_destination_lock.clone(), + ); + DecayingLock::::insert(coldkey_to_move, netuid, false); + DecayingLock::::insert(coldkey_with_conflict, netuid, false); + DecayingLock::::insert(chained_coldkey, chained_netuid, false); + HotkeyLock::::insert( + netuid, + old_hotkey, + LockState { + locked_mass: AlphaBalance::from(30_u64), + conviction: U64F64::from_num(8), + last_update: 12, + }, + ); + HotkeyLock::::insert(netuid, new_hotkey, existing_destination_lock.clone()); + Lock::::insert( + (chained_coldkey, chained_netuid, chained_first_hotkey), + chained_lock.clone(), + ); + HotkeyLock::::insert(chained_netuid, chained_first_hotkey, chained_lock.clone()); + + let weight = + crate::migrations::migrate_fix_subnet_hotkey_lock_swaps::migrate_fix_subnet_hotkey_lock_swaps::(); + + assert!(!weight.is_zero(), "migration weight should be non-zero"); + assert!(HasMigrationRun::::get(&migration_name)); + assert!(Lock::::get((coldkey_to_move, netuid, old_hotkey)).is_none()); + assert!(Lock::::get((coldkey_with_conflict, netuid, old_hotkey)).is_none()); + assert_eq!( + Lock::::get((coldkey_to_move, netuid, new_hotkey)), + Some(moved_lock.clone()) + ); + assert_eq!( + Lock::::get((coldkey_with_conflict, netuid, new_hotkey)), + Some(existing_destination_lock.clone()) + ); + assert!(HotkeyLock::::get(netuid, old_hotkey).is_none()); + + let new_aggregate = HotkeyLock::::get(netuid, new_hotkey) + .expect("new aggregate should exist"); + assert_eq!( + new_aggregate.locked_mass, + existing_destination_lock + .locked_mass + .saturating_add(moved_lock.locked_mass) + ); + assert_eq!( + new_aggregate.conviction, + existing_destination_lock + .conviction + .saturating_add(moved_lock.conviction) + ); + assert!(Lock::::get(( + chained_coldkey, + chained_netuid, + chained_first_hotkey + )) + .is_none()); + assert!(Lock::::get(( + chained_coldkey, + chained_netuid, + chained_middle_hotkey + )) + .is_none()); + assert_eq!( + Lock::::get((chained_coldkey, chained_netuid, chained_final_hotkey)), + Some(chained_lock.clone()) + ); + assert!(HotkeyLock::::get(chained_netuid, chained_first_hotkey).is_none()); + assert!(HotkeyLock::::get(chained_netuid, chained_middle_hotkey).is_none()); + assert_eq!( + HotkeyLock::::get(chained_netuid, chained_final_hotkey), + Some(chained_lock) + ); + }); +} #[test] fn test_migration_transfer_nets_to_foundation() { new_test_ext(1).execute_with(|| {