Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use super::*;
use frame_support::pallet_prelude::Weight;
use frame_system::pallet_prelude::BlockNumberFor;
use scale_info::prelude::string::String;

/// 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` rates (from chain history snapshots)
/// back to the affected old_hotkeys, excluding subnets that were legitimately swapped.
/// This adds the snapshot rates to whatever has re-accumulated since the bug, making
/// `owed = (restored_rate + new_increments) * stake - claimed ≈ new_increments * stake > 0`.
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 {
// TODO
}
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
16 changes: 6 additions & 10 deletions pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use share_pool::SafeFloat;
use sp_core::{Get, H160, H256, U256};
use sp_runtime::SaturatedConversion;
use std::collections::BTreeSet;
use substrate_fixed::types::U64F64;
use substrate_fixed::types::{I96F32, U64F64};

// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_owner --exact --nocapture
#[test]
Expand Down Expand Up @@ -2501,21 +2501,17 @@ fn test_revert_claim_root_with_swap_hotkey() {
));

assert_eq!(
RootClaimed::<Test>::get((netuid, &hk1, &coldkey)),
RootClaimed::<Test>::get((netuid, &hk2, &coldkey)),
0u128,
"hk1 RootClaimed must be zero after swap"
"hk2 RootClaimed must be zero after swap"
);
assert_eq!(
RootClaimed::<Test>::get((netuid, &hk2, &coldkey)),
RootClaimed::<Test>::get((netuid, &hk1, &coldkey)),
hk1_root_claimed,
"hk2 must have hk1's RootClaimed after swap"
);
assert!(!RootClaimable::<Test>::get(hk1).contains_key(&netuid));
assert_eq!(
*RootClaimable::<Test>::get(hk2).get(&netuid).unwrap(),
hk1_claimable,
"hk2 must have hk1's RootClaimable after swap"
);
assert!(RootClaimable::<Test>::get(hk1).contains_key(&netuid));
assert!(!RootClaimable::<Test>::get(hk2).contains_key(&netuid));

// Revert: hk2 -> hk1
step_block(20);
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,7 @@ parameter_types! {
// 0 days
pub const InitialStartCallDelay: u64 = 0;
pub const SubtensorInitialKeySwapOnSubnetCost: TaoBalance = TaoBalance::new(1_000_000); // 0.001 TAO
pub const HotkeySwapOnSubnetInterval : BlockNumber = 24 * 60 * 60 / 12; // 1 day
pub const HotkeySwapOnSubnetInterval : BlockNumber = prod_or_fast!(24 * 60 * 60 / 12, 1); // 1 day
pub const LeaseDividendsDistributionInterval: BlockNumber = 100; // 100 blocks
pub const MaxImmuneUidsPercentage: Percent = Percent::from_percent(80);
pub const EvmKeyAssociateRateLimit: u64 = EVM_KEY_ASSOCIATE_RATELIMIT;
Expand Down
Loading
Loading