Skip to content
Open
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
22 changes: 22 additions & 0 deletions pallets/subtensor/src/coinbase/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,28 @@ impl<T: Config> Pallet<T> {
StakingOperationRateLimiter::<T>::remove((hot, cold, netuid));
}
}
// AutoStakeDestination: (cold, netuid) → hot. Without this cleanup, a
// stale destination from a dissolved subnet would silently redirect
// mining incentive when the same netuid is later re-registered (see
// `run_coinbase` auto-stake path).
{
let to_rm: sp_std::vec::Vec<T::AccountId> = AutoStakeDestination::<T>::iter()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 14861 keys on mainnet and 12066 for AutoStakeDestinationColdkeys, so we should be good.

.filter_map(|(cold, n, _)| if n == netuid { Some(cold) } else { None })
.collect();
for cold in to_rm {
AutoStakeDestination::<T>::remove(&cold, netuid);
}
}
// AutoStakeDestinationColdkeys: (hot, netuid) → Vec<cold>. Companion
// reverse-index to AutoStakeDestination; must be cleared in lockstep.
{
let to_rm: sp_std::vec::Vec<T::AccountId> = AutoStakeDestinationColdkeys::<T>::iter()
.filter_map(|(hot, n, _)| if n == netuid { Some(hot) } else { None })
.collect();
for hot in to_rm {
AutoStakeDestinationColdkeys::<T>::remove(&hot, netuid);
}
}

// --- 22. Subnet leasing: remove mapping and any lease-scoped state linked to this netuid.
if let Some(lease_id) = SubnetUidToLeaseId::<T>::take(netuid) {
Expand Down
117 changes: 117 additions & 0 deletions pallets/subtensor/src/tests/networks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,10 @@ fn dissolve_clears_all_per_subnet_storages() {
// EVM association indexed by (netuid, uid)
AssociatedEvmAddress::<Test>::insert(net, 0u16, (sp_core::H160::zero(), 1u64));

// Auto-stake destination (cold,netuid) -> hot + reverse index
AutoStakeDestination::<Test>::insert(owner_cold, net, owner_hot);
AutoStakeDestinationColdkeys::<Test>::mutate(owner_hot, net, |v| v.push(owner_cold));

// (Optional) subnet -> lease link
SubnetUidToLeaseId::<Test>::insert(net, 42u32);

Expand Down Expand Up @@ -626,13 +630,126 @@ fn dissolve_clears_all_per_subnet_storages() {
// Subnet -> lease link
assert!(!SubnetUidToLeaseId::<Test>::contains_key(net));

// Auto-stake destination + reverse index cleared
assert!(AutoStakeDestination::<Test>::get(owner_cold, net).is_none());
assert!(AutoStakeDestinationColdkeys::<Test>::get(owner_hot, net).is_empty());

// ------------------------------------------------------------------
// Final subnet removal confirmation
// ------------------------------------------------------------------
assert!(!SubtensorModule::if_subnet_exist(net));
});
}

// Focused regression for the AutoStakeDestination orphan: without cleanup on
// dissolve, a stale (coldkey, netuid) → hotkey mapping would survive the
// subnet's dissolution and silently redirect mining incentive when the same
// netuid is later re-registered (see `coinbase::run_coinbase` auto-stake
// path). This test proves the cleanup wipes both halves of the index.
#[test]
fn dissolve_clears_auto_stake_destination_preventing_stale_routing() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add more netuids to guarantee it will only clear the one for the network being dissolved and not other networks?

new_test_ext(0).execute_with(|| {
let owner_cold = U256::from(101);
let owner_hot = U256::from(102);
let net = add_dynamic_network(&owner_hot, &owner_cold);

let staker_cold = U256::from(201);
let stale_dest_hot = U256::from(202);

AutoStakeDestination::<Test>::insert(staker_cold, net, stale_dest_hot);
AutoStakeDestinationColdkeys::<Test>::mutate(stale_dest_hot, net, |v| v.push(staker_cold));

// Sanity: both halves of the index are populated before dissolve.
assert_eq!(
AutoStakeDestination::<Test>::get(staker_cold, net),
Some(stale_dest_hot)
);
assert_eq!(
AutoStakeDestinationColdkeys::<Test>::get(stale_dest_hot, net),
vec![staker_cold]
);

assert_ok!(SubtensorModule::do_dissolve_network(net));

assert!(AutoStakeDestination::<Test>::get(staker_cold, net).is_none());
assert!(AutoStakeDestinationColdkeys::<Test>::get(stale_dest_hot, net).is_empty());
});
}

// Companion regression: dissolving one subnet must NOT clear
// AutoStakeDestination entries for *other* live subnets. Without per-netuid
// scoping in the cleanup, a single dissolve could blow away the auto-stake
// routing for every subnet a coldkey participates in. This test sets up the
// same (staker_cold, stale_dest_hot) pair across three distinct netuids and
// asserts that only the dissolved netuid's entries are removed.
#[test]
fn dissolve_only_clears_auto_stake_destination_for_dissolved_netuid() {
new_test_ext(0).execute_with(|| {
let owner_cold = U256::from(101);
let owner_hot = U256::from(102);
let other_owner_cold = U256::from(103);
let other_owner_hot = U256::from(104);
let third_owner_cold = U256::from(105);
let third_owner_hot = U256::from(106);

let net = add_dynamic_network(&owner_hot, &owner_cold);
let other_net = add_dynamic_network(&other_owner_hot, &other_owner_cold);
let third_net = add_dynamic_network(&third_owner_hot, &third_owner_cold);

// Same (staker_cold, dest_hot) pair routed across three netuids — only
// the dissolved one should disappear after the call.
let staker_cold = U256::from(201);
let stale_dest_hot = U256::from(202);

for n in [net, other_net, third_net] {
AutoStakeDestination::<Test>::insert(staker_cold, n, stale_dest_hot);
AutoStakeDestinationColdkeys::<Test>::mutate(stale_dest_hot, n, |v| {
v.push(staker_cold)
});
}

// Sanity: all three netuids have their auto-stake routing in place.
for n in [net, other_net, third_net] {
assert_eq!(
AutoStakeDestination::<Test>::get(staker_cold, n),
Some(stale_dest_hot),
"pre-dissolve forward index missing for netuid {n:?}"
);
assert_eq!(
AutoStakeDestinationColdkeys::<Test>::get(stale_dest_hot, n),
vec![staker_cold],
"pre-dissolve reverse index missing for netuid {n:?}"
);
}

assert_ok!(SubtensorModule::do_dissolve_network(net));

// Dissolved netuid: both halves of the index are gone.
assert!(
AutoStakeDestination::<Test>::get(staker_cold, net).is_none(),
"forward index was not cleared for dissolved netuid {net:?}"
);
assert!(
AutoStakeDestinationColdkeys::<Test>::get(stale_dest_hot, net).is_empty(),
"reverse index was not cleared for dissolved netuid {net:?}"
);

// Surviving netuids: routing is intact, untouched by the dissolve.
for n in [other_net, third_net] {
assert_eq!(
AutoStakeDestination::<Test>::get(staker_cold, n),
Some(stale_dest_hot),
"forward index was incorrectly cleared for surviving netuid {n:?}"
);
assert_eq!(
AutoStakeDestinationColdkeys::<Test>::get(stale_dest_hot, n),
vec![staker_cold],
"reverse index was incorrectly cleared for surviving netuid {n:?}"
);
}
});
}

#[test]
fn dissolve_alpha_out_but_zero_tao_no_rewards() {
new_test_ext(0).execute_with(|| {
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// `spec_version`, and `authoring_version` are the same between Wasm and native.
// This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use
// the compatible custom types.
spec_version: 406,
spec_version: 407,
impl_version: 1,
apis: RUNTIME_API_VERSIONS,
transaction_version: 1,
Expand Down
Loading