diff --git a/pallets/subtensor/src/subnets/dissolution.rs b/pallets/subtensor/src/subnets/dissolution.rs index c32a38b563..7d49aac44c 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 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 1061ccf196..6e89530e66 100644 --- a/pallets/subtensor/src/subnets/uids.rs +++ b/pallets/subtensor/src/subnets/uids.rs @@ -116,8 +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. - // Drop a stale rename edge if this SS58 was previously swapped away. - Self::clear_stale_hotkey_successor(netuid, new_hotkey); + // 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); @@ -164,8 +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. - // Drop a stale rename edge if this SS58 was previously swapped away. - Self::clear_stale_hotkey_successor(netuid, new_hotkey); + // 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 2fd14ead2d..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 becomes live again and when it 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,10 +61,9 @@ 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) { + /// 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); } diff --git a/pallets/subtensor/src/tests/hotkey_lineage.rs b/pallets/subtensor/src/tests/hotkey_lineage.rs index c21466cace..059b219785 100644 --- a/pallets/subtensor/src/tests/hotkey_lineage.rs +++ b/pallets/subtensor/src/tests/hotkey_lineage.rs @@ -412,44 +412,53 @@ fn test_hotkey_lineage_reverse_swap_does_not_cycle() { } #[test] -fn test_reregister_clears_stale_successor_for_tip() { +fn test_deregistered_hotkey_keeps_unregistered_successor() { 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 replacement = U256::from(5); - 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( - 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)); + }); +} - // h0 becomes live again; tip must not keep following the old rename. +#[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); - 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)); + SubtensorModule::record_hotkey_swap_lineage(netuid, &h0, &h1); - 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)); + 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)); + + 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!(HotkeySuccessor::::get(netuid, h0).is_none()); }); }