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
3 changes: 3 additions & 0 deletions pallets/subtensor/src/macros/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ mod errors {
HotKeyAlreadyRegisteredInSubNet,
/// The new hotkey is the same as old one
NewHotKeyIsSameWithOld,
/// The new hotkey has outstanding root claimable or non-zero root stake,
/// so the root rate-book cannot be merged without misallocating dividends.
NewHotKeyNotCleanForRootSwap,
/// The supplied PoW hash block is in the future or negative.
InvalidWorkBlock,
/// The supplied PoW hash block does not meet the network difficulty.
Expand Down
119 changes: 32 additions & 87 deletions pallets/subtensor/src/migrations/migrate_fix_root_claimed_overclaim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use frame_system::pallet_prelude::BlockNumberFor;
use scale_info::prelude::string::String;
use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32;
use substrate_fixed::types::U64F64;
use subtensor_runtime_common::{AlphaBalance, NetUid};

pub fn decode_account_id32<T: Config>(ss58_string: &str) -> Option<T::AccountId> {
let account_id32: AccountId32 = AccountId32::from_ss58check(ss58_string).ok()?;
Expand All @@ -13,24 +13,20 @@ pub fn decode_account_id32<T: Config>(ss58_string: &str) -> Option<T::AccountId>
}

struct HotkeySwapFix {
old_hotkey_ss58: &'static str,
new_hotkey_ss58: &'static str,
netuid: u16,
}

/// Fixes the consequences of a bug in `perform_hotkey_swap_on_one_subnet` where
/// `transfer_root_claimable_for_new_hotkey` unconditionally transferred the **entire**
/// `RootClaimable` BTreeMap (all subnets) from the old hotkey to the new hotkey, even
/// during a single-subnet swap.
/// Cleans up leftover `RootClaimable` state on new hotkeys produced by the buggy
/// `perform_hotkey_swap_on_one_subnet`, which unconditionally moved the entire
/// `RootClaimable` map from the old hotkey to the new hotkey during a
/// single-subnet swap.
///
/// This left the old hotkey with:
/// - `RootClaimable[old_hotkey]` = empty (wiped for ALL subnets)
/// - `RootClaimed[(subnet, old_hotkey, coldkey)]` = old watermarks (for non-swapped subnets)
///
/// Resulting in `owed = claimable_rate * root_stake - root_claimed = 0 - positive = negative → 0`,
/// effectively freezing root dividends for the old hotkey.
///
/// Remediation: restore the pre-swap `RootClaimable` and `RootClaimed` storage maps
/// These new hotkeys have no root stake (root swaps are and were guarded), so the
/// transferred claimable state produces no legitimate yield and only blocks future
/// flows. For each affected new hotkey we check that it truly holds no root-subnet
/// alpha and, if so, remove its `RootClaimable` entry. `RootClaimed` watermarks
/// are intentionally left in place — scanning that map does not fit in a single
/// block.
pub fn migrate_fix_root_claimed_overclaim<T: Config>() -> Weight {
let migration_name = b"migrate_fix_root_claimed_overclaim".to_vec();
let mut weight = T::DbWeight::get().reads(1);
Expand All @@ -52,129 +48,77 @@ pub fn migrate_fix_root_claimed_overclaim<T: Config>() -> Weight {
// Mainnet genesis: 0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03
let genesis_hash = frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero());
let genesis_bytes = genesis_hash.as_ref();
let mut claimed_restored: u64 = 0;
let mainnet_genesis =
hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03");

let mut cleared_hotkeys: u64 = 0;

if genesis_bytes == mainnet_genesis {
let fixes: &[HotkeySwapFix] = &[
HotkeySwapFix {
old_hotkey_ss58: "5GmvyePN9aYErXBBhBnxZKGoGk4LKZApE4NkaSzW62CYCYNA",
new_hotkey_ss58: "5H6BqkzjYvViiqp7rQLXjpnaEmW7U9CoKxXhQ4efMqtX1mQw",
netuid: 27,
},
HotkeySwapFix {
old_hotkey_ss58: "5CmKE9k1z1DDQBh81nfwRtbLq22mgS8wMPS9h36LVe4oGJTK",
new_hotkey_ss58: "5EnpBz2DoMTzMztFSVPSpi8jP2yfGadU6kgZgsjqnfvonMgu",
netuid: 9,
},
HotkeySwapFix {
old_hotkey_ss58: "5C4s95N2JJbWwPPAr8JYwQBZQwxbZTYGjYbm6XtH2LgYV8Zx",
new_hotkey_ss58: "5ChzWkapDYgVxT88ZmBQS8QM63V9VWSA3eFpSipsX2xbTNZN",
netuid: 13,
},
HotkeySwapFix {
old_hotkey_ss58: "5GHrTeuFnJYjNJx773URbYb9Pk3bRRDiJHJFBNECZpjGqZPY",
new_hotkey_ss58: "5DAmVrUgpTX9xmRyZ7R3UUFNSzh7ZNY6qYxv9N4VeCq6mHHL",
netuid: 65,
},
HotkeySwapFix {
old_hotkey_ss58: "5EtM9iXMAYRsmt6aoQAoWNDX6yaBnjhmnEQhWKv8HpwkVtML",
new_hotkey_ss58: "5ECzcM7sixWNEeD6RbpeEHW1YcYMFejwHuvDBgQxVSjGyrMS",
netuid: 11,
},
HotkeySwapFix {
old_hotkey_ss58: "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN",
new_hotkey_ss58: "5DF3nhgzpr4EZas8dXZYa4mYZBxRCU7AuiCV7Qs2JWAGA6sY",
netuid: 41,
},
HotkeySwapFix {
old_hotkey_ss58: "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN",
new_hotkey_ss58: "5E4pFBKCyk2RxQqifEBu37jb5vgoj9ZrVS7iQdQy4PNr33Ge",
netuid: 44,
},
HotkeySwapFix {
old_hotkey_ss58: "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN",
new_hotkey_ss58: "5DhQbRT3ZfHcVumNtAm5BbzeGHrFRHHi7nofgu76VWipnGSb",
netuid: 50,
},
HotkeySwapFix {
old_hotkey_ss58: "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN",
new_hotkey_ss58: "5Gj37iVQG5hMSxU3AE89x5p3aEEfPZk6Rtmtbwepght4tbri",
netuid: 51,
},
HotkeySwapFix {
old_hotkey_ss58: "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN",
new_hotkey_ss58: "5DyM1rxnDu8QSjbbh5bPV2GMK6UTPRXdUM6mNViBBut9Ma6w",
netuid: 54,
},
HotkeySwapFix {
old_hotkey_ss58: "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN",
new_hotkey_ss58: "5Ci5t4vPK3eCGhFWneB58fodg3x9oS2m8seKoDApFKUqyw4e",
netuid: 64,
},
HotkeySwapFix {
old_hotkey_ss58: "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN",
new_hotkey_ss58: "5Et5VQUMX7VqGyvZycjv5FBBC5FQbLGUJiRMWMnEVnMLXKm9",
netuid: 93,
},
];

let root_netuid = NetUid::from(0);

for fix in fixes {
let netuid = NetUid::from(fix.netuid);

let (old_hotkey, new_hotkey) = match (
decode_account_id32::<T>(fix.old_hotkey_ss58),
decode_account_id32::<T>(fix.new_hotkey_ss58),
) {
(Some(old), Some(new)) => (old, new),
_ => {
let new_hotkey = match decode_account_id32::<T>(fix.new_hotkey_ss58) {
Some(h) => h,
None => {
log::error!(
"Failed to decode hotkeys for netuid {}, skipping",
fix.netuid
"Failed to decode new hotkey {}, skipping",
fix.new_hotkey_ss58
);
continue;
}
};

// Reverting the Root Claimable because it only should happen for root subnet
Pallet::<T>::transfer_root_claimable_for_new_hotkey(&new_hotkey, &old_hotkey);
weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));

// Collect all coldkeys that have non-zero alpha on root subnet
// (meaning they had root stake at swap time)
let alpha_on_swapped_subnet: Vec<T::AccountId> =
Alpha::<T>::iter_prefix((&new_hotkey,))
.filter(|((coldkey, netuid_alpha), _)| {
// Must be on the subnet that was swapped
if *netuid_alpha != netuid {
return false;
}
// Must have non-zero alpha on root subnet for old hotkey
// (guards against reverting claims for keys with no root stake)
let root_alpha = Alpha::<T>::get((&old_hotkey, coldkey, root_netuid));
root_alpha != U64F64::from_num(0u64)
})
.map(|((coldkey, _), _)| coldkey)
.collect();
let root_stake = Pallet::<T>::get_stake_for_hotkey_on_subnet(&new_hotkey, NetUid::ROOT);
weight.saturating_accrue(T::DbWeight::get().reads(1));

weight.saturating_accrue(
T::DbWeight::get().reads((alpha_on_swapped_subnet.len() as u64).saturating_mul(2)),
);

// Revert RootClaimed for each qualifying coldkey
for coldkey in alpha_on_swapped_subnet {
claimed_restored = claimed_restored.saturating_add(1);
Pallet::<T>::transfer_root_claimed_for_new_keys(
netuid,
&new_hotkey,
&old_hotkey,
&coldkey,
&coldkey,
if root_stake != AlphaBalance::zero() {
log::info!(
"Skipping {} — new hotkey still has root stake",
fix.new_hotkey_ss58
);
weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));
continue;
}

RootClaimable::<T>::remove(&new_hotkey);
weight.saturating_accrue(T::DbWeight::get().writes(1));
cleared_hotkeys = cleared_hotkeys.saturating_add(1);
}
}

Expand All @@ -183,7 +127,8 @@ pub fn migrate_fix_root_claimed_overclaim<T: Config>() -> Weight {
weight.saturating_accrue(T::DbWeight::get().writes(1));

log::info!(
"Migration 'migrate_fix_root_claimed_overclaim' completed. Claimed restored: {claimed_restored}"
"Migration 'migrate_fix_root_claimed_overclaim' completed. \
Cleared RootClaimable for {cleared_hotkeys} hotkeys."
);

weight
Expand Down
18 changes: 18 additions & 0 deletions pallets/subtensor/src/swap/swap_hotkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ impl<T: Config> Pallet<T> {
/// * `HotKeySetTxRateLimitExceeded` - If the transaction rate limit is exceeded.
/// * `NewHotKeyIsSameWithOld` - If the new hotkey is the same as the old hotkey.
/// * `HotKeyAlreadyRegisteredInSubNet` - If the new hotkey is already registered in the subnet.
/// * `NewHotKeyNotCleanForRootSwap` - If the swap touches root and the new hotkey
/// has outstanding `RootClaimable` entries or non-zero root stake.
/// * `NotEnoughBalanceToPaySwapHotKey` - If there is not enough balance to pay for the swap.
pub fn do_swap_hotkey(
origin: OriginFor<T>,
Expand Down Expand Up @@ -77,6 +79,22 @@ impl<T: Config> Pallet<T> {
}
}

// 7.2 If the swap touches the root subnet, require that new_hotkey is clean
// on root (no outstanding claimable rate and no existing root stake). Merging
// a non-empty rate-book would either violate total conservation or misallocate
// dividends across coldkeys that never staked on old_hotkey.
let touches_root = match netuid {
None => true,
Some(n) => n == NetUid::ROOT,
};
if touches_root {
ensure!(
RootClaimable::<T>::get(new_hotkey).is_empty()
&& Self::get_stake_for_hotkey_on_subnet(new_hotkey, NetUid::ROOT).is_zero(),
Error::<T>::NewHotKeyNotCleanForRootSwap
);
}

// 8. Swap LastTxBlock
let last_tx_block: u64 = Self::get_last_tx_block(old_hotkey);
Self::set_last_tx_block(new_hotkey, last_tx_block);
Expand Down
Loading
Loading