Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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 @@ -166,7 +166,9 @@ mod hooks {
// Fix staking hot keys
.saturating_add(migrations::migrate_fix_staking_hot_keys::migrate_fix_staking_hot_keys::<T>())
// Migrate coldkey swap scheduled to announcements
.saturating_add(migrations::migrate_coldkey_swap_scheduled_to_announcements::migrate_coldkey_swap_scheduled_to_announcements::<T>());
.saturating_add(migrations::migrate_coldkey_swap_scheduled_to_announcements::migrate_coldkey_swap_scheduled_to_announcements::<T>())
// Fix RootClaimed overclaim caused by single-subnet hotkey swap bug
.saturating_add(migrations::migrate_fix_root_claimed_overclaim::migrate_fix_root_claimed_overclaim::<T>());
weight
}

Expand Down
110 changes: 110 additions & 0 deletions pallets/subtensor/src/migrations/migrate_fix_root_claimed_overclaim.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use super::*;
use frame_support::pallet_prelude::Weight;
use frame_system::pallet_prelude::BlockNumberFor;
use scale_info::prelude::string::String;
use share_pool::SafeFloat;
use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32;
use substrate_fixed::types::U64F64;

pub 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()
}

/// 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.
///
/// 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
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);

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)
);

// Only run on mainnet.
// Mainnet genesis: 0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03
let genesis_hash = frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero());
let genesis_bytes = genesis_hash.as_ref();
let mainnet_genesis =
hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03");
if genesis_bytes == mainnet_genesis {
let old_hotkey_ss58 = "5GmvyePN9aYErXBBhBnxZKGoGk4LKZApE4NkaSzW62CYCYNA";
let new_hotkey_ss58 = "5H6BqkzjYvViiqp7rQLXjpnaEmW7U9CoKxXhQ4efMqtX1mQw";

if let (Some(old_hotkey), Some(new_hotkey)) = (
decode_account_id32::<T>(old_hotkey_ss58),
decode_account_id32::<T>(new_hotkey_ss58),
) {
let netuid = NetUid::from(27);

// Reverting the Root Claimable because it only should happen for root subnet
Pallet::<T>::transfer_root_claimable_for_new_hotkey(&new_hotkey, &old_hotkey);

let old_alpha_values: Vec<((T::AccountId, NetUid), U64F64)> =
Alpha::<T>::iter_prefix((&new_hotkey,)).collect();
weight.saturating_accrue(T::DbWeight::get().reads(old_alpha_values.len() as u64));

let old_alpha_values_v2: Vec<((T::AccountId, NetUid), SafeFloat)> =
AlphaV2::<T>::iter_prefix((&new_hotkey,)).collect();
weight.saturating_accrue(T::DbWeight::get().reads(old_alpha_values_v2.len() as u64));

// Reverting back root claimable
for ((coldkey, netuid_alpha), alpha) in old_alpha_values {
if netuid == netuid_alpha && alpha != 0 {
Pallet::<T>::transfer_root_claimed_for_new_keys(
netuid,
&new_hotkey,
&old_hotkey,
&coldkey,
&coldkey,
);
}
}

// Reverting back root claimable
for ((coldkey, netuid_alpha), alpha) in old_alpha_values_v2 {
if netuid == netuid_alpha && !alpha.is_zero() {
Pallet::<T>::transfer_root_claimed_for_new_keys(
netuid,
&new_hotkey,
&old_hotkey,
&coldkey,
&coldkey,
);
}
}
} else {
log::error!("Failed to decode hotkeys, skipping");
}
}
Comment on lines +48 to +103
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.

would be a nice macro @sam0x17

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.

make sure to only clear the RootClaim- able/ed maps if the root stake is still == 0. Otherwise we can just leave it on the old one.


// Mark migration as completed
HasMigrationRun::<T>::insert(&migration_name, true);
weight.saturating_accrue(T::DbWeight::get().writes(1));

log::info!("Migration 'migrate_fix_root_claimed_overclaim' completed.");

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 @@ -18,6 +18,7 @@ pub mod migrate_delete_subnet_3;
pub mod migrate_disable_commit_reveal;
pub mod migrate_fix_childkeys;
pub mod migrate_fix_is_network_member;
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;
Expand Down
39 changes: 28 additions & 11 deletions pallets/subtensor/src/swap/swap_hotkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,16 +561,9 @@ impl<T: Config> Pallet<T> {
weight.saturating_accrue(T::DbWeight::get().reads(old_alpha_values_v2.len() as u64));
weight.saturating_accrue(T::DbWeight::get().writes(old_alpha_values_v2.len() as u64));

// 9.1. Transfer root claimable
Self::transfer_root_claimable_for_new_hotkey(old_hotkey, new_hotkey);

// 9.2. Insert the new alpha values.
for ((coldkey, netuid_alpha), alpha) in old_alpha_values {
if netuid == netuid_alpha {
Self::transfer_root_claimed_for_new_keys(
netuid, old_hotkey, new_hotkey, &coldkey, &coldkey,
);

let new_alpha = Alpha::<T>::take((new_hotkey, &coldkey, netuid));
Alpha::<T>::remove((old_hotkey, &coldkey, netuid));

Expand All @@ -596,10 +589,6 @@ impl<T: Config> Pallet<T> {

for ((coldkey, netuid_alpha), alpha) in old_alpha_values_v2 {
if netuid == netuid_alpha {
Self::transfer_root_claimed_for_new_keys(
netuid, old_hotkey, new_hotkey, &coldkey, &coldkey,
);

let new_alpha_v2 = AlphaV2::<T>::take((new_hotkey, &coldkey, netuid));
AlphaV2::<T>::remove((old_hotkey, &coldkey, netuid));
AlphaV2::<T>::insert(
Expand All @@ -620,6 +609,34 @@ impl<T: Config> Pallet<T> {
}
}
}

// 9.3. Transfer root claimable and root claimed only for the root subnet
// NOTE: we shouldn't transfer root claimable and root claimed for other subnets,
// otherwise root stakers won't be able to receive dividends.
if netuid == NetUid::ROOT {
Self::transfer_root_claimable_for_new_hotkey(old_hotkey, new_hotkey);
weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));

// After transfer, new_hotkey has the full RootClaimable map.
// We use it to know which subnets have outstanding claims.
let subnets: Vec<NetUid> = RootClaimable::<T>::get(new_hotkey)
.keys()
.copied()
.collect();

for subnet in subnets {
let claimed_coldkeys: Vec<T::AccountId> =
RootClaimed::<T>::iter_prefix((subnet, old_hotkey))
.map(|(coldkey, _)| coldkey)
.collect();

for coldkey in claimed_coldkeys {
Self::transfer_root_claimed_for_new_keys(
subnet, old_hotkey, new_hotkey, &coldkey, &coldkey,
);
}
}
}
}

Ok(())
Expand Down
13 changes: 5 additions & 8 deletions pallets/subtensor/src/tests/claim_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1284,22 +1284,19 @@ fn test_claim_root_with_swap_hotkey() {
false,
));

// Check swapped keys claimed values

// Check swapped keys claimed values.
assert_eq!(
0u128,
u128::from(new_stake), // It shouldn't change, because we didn't swap the root hotkey
RootClaimed::<Test>::get((netuid, &hotkey, &coldkey,))
);
assert_eq!(
u128::from(new_stake),
0u128,
RootClaimed::<Test>::get((netuid, &new_hotkey, &coldkey,))
);

assert!(!RootClaimable::<Test>::get(hotkey).contains_key(&netuid));
assert!(RootClaimable::<Test>::get(hotkey).contains_key(&netuid));

let _new_claimable = *RootClaimable::<Test>::get(new_hotkey)
.get(&netuid)
.expect("claimable must exist at this point");
assert!(!RootClaimable::<Test>::get(new_hotkey).contains_key(&netuid));
});
}

Expand Down
5 changes: 5 additions & 0 deletions pallets/subtensor/src/tests/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3122,3 +3122,8 @@ fn test_migrate_coldkey_swap_scheduled_to_announcements() {
);
});
}

#[test]
fn test_migrate_fix_root_claimed_overclaim() {
// TODO
}
Loading
Loading