Skip to content
Draft
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
4 changes: 4 additions & 0 deletions pallets/subtensor/src/subnets/dissolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ impl<T: Config> Pallet<T> {
MechanismCountCurrent::<T>::remove(netuid);
MechanismEmissionSplit::<T>::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::<T>::clear_prefix(netuid, limit, None)
}) || !clear_prefix_with_meter(weight_meter, write_weight, |limit| {
Expand Down
8 changes: 4 additions & 4 deletions pallets/subtensor/src/subnets/uids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ impl<T: Config> Pallet<T> {
Uids::<T>::insert(netuid, new_hotkey.clone(), uid_to_replace); // Make uid - hotkey association.
BlockAtRegistration::<T>::insert(netuid, uid_to_replace, block_number); // Fill block at registration.
IsNetworkMember::<T>::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::<T>::remove(netuid, &old_hotkey);
Expand Down Expand Up @@ -164,8 +164,8 @@ impl<T: Config> Pallet<T> {
Uids::<T>::insert(netuid, new_hotkey.clone(), next_uid); // Make uid - hotkey association.
BlockAtRegistration::<T>::insert(netuid, next_uid, block_number); // Fill block at registration.
IsNetworkMember::<T>::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 {
Expand Down
11 changes: 5 additions & 6 deletions pallets/subtensor/src/swap/hotkey_lineage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -61,10 +61,9 @@ impl<T: Config> Pallet<T> {
HotkeyRoot::<T>::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::<T>::remove(netuid, hotkey);
}

Expand Down
69 changes: 39 additions & 30 deletions pallets/subtensor/src/tests/hotkey_lineage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

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.

[MEDIUM] Test does not reproduce the claimed regression

This directly creates h0 → h1 while h0 remains registered, then replaces h0 with an unrelated key. That is not the stated swap-then-deregistration flow: after a real h0 → h1 swap, h1 is registered and h0 is not. Moreover, the pre-PR replace_neuron already cleared the successor of new_hotkey (replacement here), so this test passes without the PR. Exercise do_swap_hotkey followed by deregistration/replacement of the resulting live hotkey and first demonstrate that the test fails against the base branch.


let uid = Uids::<Test>::get(netuid, h0).expect("registered before deregistration");
SubtensorModule::replace_neuron(netuid, uid, &replacement, System::block_number());

assert!(Uids::<Test>::get(netuid, h0).is_none());
assert_eq!(Uids::<Test>::get(netuid, replacement), Some(uid));
assert!(Uids::<Test>::get(netuid, h1).is_none());
assert_eq!(HotkeySuccessor::<Test>::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::<Test>::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::<Test>::get(netuid, h0), Some(h2));
assert_eq!(SubtensorModule::hotkey_root(netuid, &h2), h0);
assert!(SubtensorModule::same_hotkey_lineage(netuid, &h1, &h2));
let uid = Uids::<Test>::get(netuid, h0).expect("registered before deregistration");
SubtensorModule::replace_neuron(netuid, uid, &replacement, System::block_number());
assert_eq!(HotkeySuccessor::<Test>::get(netuid, h0), Some(h1));

SubtensorModule::replace_neuron(netuid, uid, &h0, System::block_number());

assert_eq!(Uids::<Test>::get(netuid, h0), Some(uid));
assert!(Uids::<Test>::get(netuid, h1).is_none());
assert!(HotkeySuccessor::<Test>::get(netuid, h0).is_none());
});
}

Expand Down
Loading