From c638f4001c62dbf2c2e479d285dc62761b8a360c Mon Sep 17 00:00:00 2001 From: UnarbosFour Date: Fri, 28 Aug 2026 12:35:32 -0400 Subject: [PATCH 1/2] preserve hotkey successors across neuron deregistration --- pallets/subtensor/src/subnets/dissolution.rs | 4 ++ pallets/subtensor/src/subnets/uids.rs | 4 -- pallets/subtensor/src/swap/hotkey_lineage.rs | 11 +----- pallets/subtensor/src/tests/hotkey_lineage.rs | 38 ++++++++----------- 4 files changed, 22 insertions(+), 35 deletions(-) diff --git a/pallets/subtensor/src/subnets/dissolution.rs b/pallets/subtensor/src/subnets/dissolution.rs index c32a38b563..6f8c631d84 100644 --- a/pallets/subtensor/src/subnets/dissolution.rs +++ b/pallets/subtensor/src/subnets/dissolution.rs @@ -257,6 +257,10 @@ impl Pallet { MechanismCountCurrent::::remove(netuid); MechanismEmissionSplit::::remove(netuid); + // Hotkey lineage must survive individual neuron deregistration. A hotkey + // swap and deregistration can happen in quick succession, even in one + // batch, and contracts need the successor mapping to locate their funds. + // Clear it only when the entire netuid is deregistered. if !clear_prefix_with_meter(weight_meter, write_weight, |limit| { LastHotkeySwapOnNetuid::::clear_prefix(netuid, limit, None) }) || !clear_prefix_with_meter(weight_meter, write_weight, |limit| { diff --git a/pallets/subtensor/src/subnets/uids.rs b/pallets/subtensor/src/subnets/uids.rs index 1061ccf196..4d5c856a7e 100644 --- a/pallets/subtensor/src/subnets/uids.rs +++ b/pallets/subtensor/src/subnets/uids.rs @@ -116,8 +116,6 @@ impl Pallet { Uids::::insert(netuid, new_hotkey.clone(), uid_to_replace); // Make uid - hotkey association. BlockAtRegistration::::insert(netuid, uid_to_replace, block_number); // Fill block at registration. IsNetworkMember::::insert(new_hotkey.clone(), netuid, true); // Fill network is member. - // Drop a stale rename edge if this SS58 was previously swapped away. - Self::clear_stale_hotkey_successor(netuid, new_hotkey); // 4. Clear neuron axons, certificates and prometheus info Axons::::remove(netuid, &old_hotkey); @@ -164,8 +162,6 @@ impl Pallet { Uids::::insert(netuid, new_hotkey.clone(), next_uid); // Make uid - hotkey association. BlockAtRegistration::::insert(netuid, next_uid, block_number); // Fill block at registration. IsNetworkMember::::insert(new_hotkey.clone(), netuid, true); // Fill network is member. - // Drop a stale rename edge if this SS58 was previously swapped away. - Self::clear_stale_hotkey_successor(netuid, new_hotkey); } pub fn trim_to_max_allowed_uids(netuid: NetUid, max_n: u16) -> DispatchResult { diff --git a/pallets/subtensor/src/swap/hotkey_lineage.rs b/pallets/subtensor/src/swap/hotkey_lineage.rs index 2fd14ead2d..e5cdfbdd7d 100644 --- a/pallets/subtensor/src/swap/hotkey_lineage.rs +++ b/pallets/subtensor/src/swap/hotkey_lineage.rs @@ -11,8 +11,8 @@ //! //! Prefer [`Self::hotkey_root`] / [`Self::same_hotkey_lineage`] for ban/score. //! [`Self::hotkey_lineage_tip`] is best-effort: successor edges are cleared when -//! a hotkey becomes live again and when it is written as a swap destination, -//! but consumers should still treat tip walks as advisory. +//! a hotkey is written as a swap destination, but consumers should still treat +//! tip walks as advisory. use frame_support::weights::Weight; @@ -61,13 +61,6 @@ impl Pallet { HotkeyRoot::::insert(netuid, new_hotkey, root); } - /// Drop a stale outgoing successor when `hotkey` becomes live on `netuid` - /// again (registration / UID replace). Keeps tip walks from following a - /// previous rename of the same SS58. - pub fn clear_stale_hotkey_successor(netuid: NetUid, hotkey: &T::AccountId) { - HotkeySuccessor::::remove(netuid, hotkey); - } - /// Canonical (first) hotkey in this subnet's swap lineage for `hotkey`. pub fn hotkey_root(netuid: NetUid, hotkey: &T::AccountId) -> T::AccountId { HotkeyRoot::::get(netuid, hotkey).unwrap_or_else(|| hotkey.clone()) diff --git a/pallets/subtensor/src/tests/hotkey_lineage.rs b/pallets/subtensor/src/tests/hotkey_lineage.rs index c21466cace..005f070fb1 100644 --- a/pallets/subtensor/src/tests/hotkey_lineage.rs +++ b/pallets/subtensor/src/tests/hotkey_lineage.rs @@ -412,15 +412,16 @@ fn test_hotkey_lineage_reverse_swap_does_not_cycle() { } #[test] -fn test_reregister_clears_stale_successor_for_tip() { +fn test_neuron_deregistration_preserves_successor_for_contract_fund_tracking() { new_test_ext(1).execute_with(|| { let coldkey = U256::from(1); - let h0 = U256::from(2); - let h1 = U256::from(3); - let h2 = U256::from(4); + let owner_hotkey = U256::from(2); + let h0 = U256::from(3); + let h1 = U256::from(4); - let netuid = add_dynamic_network(&h0, &coldkey); + let netuid = add_dynamic_network(&owner_hotkey, &coldkey); add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + register_ok_neuron(netuid, h0, coldkey, 0); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); assert_ok!(SubtensorModule::do_swap_hotkey( @@ -432,24 +433,17 @@ fn test_reregister_clears_stale_successor_for_tip() { )); assert_eq!(SubtensorModule::hotkey_lineage_tip(netuid, &h0), h1); - // h0 becomes live again; tip must not keep following the old rename. - register_ok_neuron(netuid, h0, coldkey, 0); - assert!(HotkeySuccessor::::get(netuid, h0).is_none()); - assert_eq!(SubtensorModule::hotkey_lineage_tip(netuid, &h0), h0); - // Root-based identity still links the prior tip. - assert!(SubtensorModule::same_hotkey_lineage(netuid, &h0, &h1)); + // Deregister h1 and re-register h0 in the same block as the swap. + // Contracts must still be able to follow the successor and locate funds + // held under h1. + let uid = Uids::::get(netuid, h1).expect("registered after swap"); + SubtensorModule::replace_neuron(netuid, uid, &h0, System::block_number()); - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &h0, - &h2, - Some(netuid), - false, - )); - assert_eq!(HotkeySuccessor::::get(netuid, h0), Some(h2)); - assert_eq!(SubtensorModule::hotkey_root(netuid, &h2), h0); - assert!(SubtensorModule::same_hotkey_lineage(netuid, &h1, &h2)); + assert_eq!(Uids::::get(netuid, h0), Some(uid)); + assert!(Uids::::get(netuid, h1).is_none()); + assert_eq!(HotkeySuccessor::::get(netuid, h0), Some(h1)); + assert_eq!(SubtensorModule::hotkey_lineage_tip(netuid, &h0), h1); + assert!(SubtensorModule::same_hotkey_lineage(netuid, &h0, &h1)); }); } From 1b9b87d8c34d8a42462e72d318ef09647a234c7a Mon Sep 17 00:00:00 2001 From: UnarbosFour Date: Fri, 28 Aug 2026 13:18:31 -0400 Subject: [PATCH 2/2] Handle cornercase --- pallets/subtensor/src/subnets/dissolution.rs | 8 +-- pallets/subtensor/src/subnets/uids.rs | 4 ++ pallets/subtensor/src/swap/hotkey_lineage.rs | 10 +++- pallets/subtensor/src/tests/hotkey_lineage.rs | 49 ++++++++++++------- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/pallets/subtensor/src/subnets/dissolution.rs b/pallets/subtensor/src/subnets/dissolution.rs index 6f8c631d84..7d49aac44c 100644 --- a/pallets/subtensor/src/subnets/dissolution.rs +++ b/pallets/subtensor/src/subnets/dissolution.rs @@ -257,10 +257,10 @@ impl Pallet { MechanismCountCurrent::::remove(netuid); MechanismEmissionSplit::::remove(netuid); - // Hotkey lineage must survive individual neuron deregistration. A hotkey - // swap and deregistration can happen in quick succession, even in one - // batch, and contracts need the successor mapping to locate their funds. - // Clear it only when the entire netuid is deregistered. + // Hotkey lineage must survive individual neuron deregistration: a swap + // and deregistration can happen in quick succession, even in one batch, + // and contracts still need the successor to locate funds. Re-registration + // cancels only that hotkey's edge; netuid deregistration clears all edges. if !clear_prefix_with_meter(weight_meter, write_weight, |limit| { LastHotkeySwapOnNetuid::::clear_prefix(netuid, limit, None) }) || !clear_prefix_with_meter(weight_meter, write_weight, |limit| { diff --git a/pallets/subtensor/src/subnets/uids.rs b/pallets/subtensor/src/subnets/uids.rs index 4d5c856a7e..6e89530e66 100644 --- a/pallets/subtensor/src/subnets/uids.rs +++ b/pallets/subtensor/src/subnets/uids.rs @@ -116,6 +116,8 @@ impl Pallet { Uids::::insert(netuid, new_hotkey.clone(), uid_to_replace); // Make uid - hotkey association. BlockAtRegistration::::insert(netuid, uid_to_replace, block_number); // Fill block at registration. IsNetworkMember::::insert(new_hotkey.clone(), netuid, true); // Fill network is member. + // Re-registration cancels any scheduled swap from this hotkey. + Self::cancel_hotkey_successor_on_reregistration(netuid, new_hotkey); // 4. Clear neuron axons, certificates and prometheus info Axons::::remove(netuid, &old_hotkey); @@ -162,6 +164,8 @@ impl Pallet { Uids::::insert(netuid, new_hotkey.clone(), next_uid); // Make uid - hotkey association. BlockAtRegistration::::insert(netuid, next_uid, block_number); // Fill block at registration. IsNetworkMember::::insert(new_hotkey.clone(), netuid, true); // Fill network is member. + // Re-registration cancels any scheduled swap from this hotkey. + Self::cancel_hotkey_successor_on_reregistration(netuid, new_hotkey); } pub fn trim_to_max_allowed_uids(netuid: NetUid, max_n: u16) -> DispatchResult { diff --git a/pallets/subtensor/src/swap/hotkey_lineage.rs b/pallets/subtensor/src/swap/hotkey_lineage.rs index e5cdfbdd7d..f8eab1261e 100644 --- a/pallets/subtensor/src/swap/hotkey_lineage.rs +++ b/pallets/subtensor/src/swap/hotkey_lineage.rs @@ -11,8 +11,8 @@ //! //! Prefer [`Self::hotkey_root`] / [`Self::same_hotkey_lineage`] for ban/score. //! [`Self::hotkey_lineage_tip`] is best-effort: successor edges are cleared when -//! a hotkey is written as a swap destination, but consumers should still treat -//! tip walks as advisory. +//! a hotkey re-registers or is written as a swap destination, but consumers +//! should still treat tip walks as advisory. use frame_support::weights::Weight; @@ -61,6 +61,12 @@ impl Pallet { HotkeyRoot::::insert(netuid, new_hotkey, root); } + /// Cancel the outgoing successor when `hotkey` re-registers on `netuid`. + /// Deregistration alone deliberately leaves the edge intact. + pub fn cancel_hotkey_successor_on_reregistration(netuid: NetUid, hotkey: &T::AccountId) { + HotkeySuccessor::::remove(netuid, hotkey); + } + /// Canonical (first) hotkey in this subnet's swap lineage for `hotkey`. pub fn hotkey_root(netuid: NetUid, hotkey: &T::AccountId) -> T::AccountId { HotkeyRoot::::get(netuid, hotkey).unwrap_or_else(|| hotkey.clone()) diff --git a/pallets/subtensor/src/tests/hotkey_lineage.rs b/pallets/subtensor/src/tests/hotkey_lineage.rs index 005f070fb1..059b219785 100644 --- a/pallets/subtensor/src/tests/hotkey_lineage.rs +++ b/pallets/subtensor/src/tests/hotkey_lineage.rs @@ -412,38 +412,53 @@ fn test_hotkey_lineage_reverse_swap_does_not_cycle() { } #[test] -fn test_neuron_deregistration_preserves_successor_for_contract_fund_tracking() { +fn test_deregistered_hotkey_keeps_unregistered_successor() { new_test_ext(1).execute_with(|| { let coldkey = U256::from(1); let owner_hotkey = U256::from(2); let h0 = U256::from(3); let h1 = U256::from(4); + let replacement = U256::from(5); let netuid = add_dynamic_network(&owner_hotkey, &coldkey); add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); register_ok_neuron(netuid, h0, coldkey, 0); - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &h0, - &h1, - Some(netuid), - false, - )); - assert_eq!(SubtensorModule::hotkey_lineage_tip(netuid, &h0), h1); + SubtensorModule::record_hotkey_swap_lineage(netuid, &h0, &h1); + + let uid = Uids::::get(netuid, h0).expect("registered before deregistration"); + SubtensorModule::replace_neuron(netuid, uid, &replacement, System::block_number()); + + assert!(Uids::::get(netuid, h0).is_none()); + assert_eq!(Uids::::get(netuid, replacement), Some(uid)); + assert!(Uids::::get(netuid, h1).is_none()); + assert_eq!(HotkeySuccessor::::get(netuid, h0), Some(h1)); + }); +} + +#[test] +fn test_reregistered_hotkey_cancels_successor() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let owner_hotkey = U256::from(2); + let h0 = U256::from(3); + let h1 = U256::from(4); + let replacement = U256::from(5); + + let netuid = add_dynamic_network(&owner_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + register_ok_neuron(netuid, h0, coldkey, 0); + SubtensorModule::record_hotkey_swap_lineage(netuid, &h0, &h1); + + let uid = Uids::::get(netuid, h0).expect("registered before deregistration"); + SubtensorModule::replace_neuron(netuid, uid, &replacement, System::block_number()); + assert_eq!(HotkeySuccessor::::get(netuid, h0), Some(h1)); - // Deregister h1 and re-register h0 in the same block as the swap. - // Contracts must still be able to follow the successor and locate funds - // held under h1. - let uid = Uids::::get(netuid, h1).expect("registered after swap"); SubtensorModule::replace_neuron(netuid, uid, &h0, System::block_number()); assert_eq!(Uids::::get(netuid, h0), Some(uid)); assert!(Uids::::get(netuid, h1).is_none()); - assert_eq!(HotkeySuccessor::::get(netuid, h0), Some(h1)); - assert_eq!(SubtensorModule::hotkey_lineage_tip(netuid, &h0), h1); - assert!(SubtensorModule::same_hotkey_lineage(netuid, &h0, &h1)); + assert!(HotkeySuccessor::::get(netuid, h0).is_none()); }); }