diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index b2926323db..2394de6dc1 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -475,6 +475,28 @@ impl Pallet { StakingOperationRateLimiter::::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 = AutoStakeDestination::::iter() + .filter_map(|(cold, n, _)| if n == netuid { Some(cold) } else { None }) + .collect(); + for cold in to_rm { + AutoStakeDestination::::remove(&cold, netuid); + } + } + // AutoStakeDestinationColdkeys: (hot, netuid) → Vec. Companion + // reverse-index to AutoStakeDestination; must be cleared in lockstep. + { + let to_rm: sp_std::vec::Vec = AutoStakeDestinationColdkeys::::iter() + .filter_map(|(hot, n, _)| if n == netuid { Some(hot) } else { None }) + .collect(); + for hot in to_rm { + AutoStakeDestinationColdkeys::::remove(&hot, netuid); + } + } // --- 22. Subnet leasing: remove mapping and any lease-scoped state linked to this netuid. if let Some(lease_id) = SubnetUidToLeaseId::::take(netuid) { diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index c4efc75825..3e988c0321 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -461,6 +461,10 @@ fn dissolve_clears_all_per_subnet_storages() { // EVM association indexed by (netuid, uid) AssociatedEvmAddress::::insert(net, 0u16, (sp_core::H160::zero(), 1u64)); + // Auto-stake destination (cold,netuid) -> hot + reverse index + AutoStakeDestination::::insert(owner_cold, net, owner_hot); + AutoStakeDestinationColdkeys::::mutate(owner_hot, net, |v| v.push(owner_cold)); + // (Optional) subnet -> lease link SubnetUidToLeaseId::::insert(net, 42u32); @@ -626,6 +630,10 @@ fn dissolve_clears_all_per_subnet_storages() { // Subnet -> lease link assert!(!SubnetUidToLeaseId::::contains_key(net)); + // Auto-stake destination + reverse index cleared + assert!(AutoStakeDestination::::get(owner_cold, net).is_none()); + assert!(AutoStakeDestinationColdkeys::::get(owner_hot, net).is_empty()); + // ------------------------------------------------------------------ // Final subnet removal confirmation // ------------------------------------------------------------------ @@ -633,6 +641,41 @@ fn dissolve_clears_all_per_subnet_storages() { }); } +// 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() { + 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::::insert(staker_cold, net, stale_dest_hot); + AutoStakeDestinationColdkeys::::mutate(stale_dest_hot, net, |v| v.push(staker_cold)); + + // Sanity: both halves of the index are populated before dissolve. + assert_eq!( + AutoStakeDestination::::get(staker_cold, net), + Some(stale_dest_hot) + ); + assert_eq!( + AutoStakeDestinationColdkeys::::get(stale_dest_hot, net), + vec![staker_cold] + ); + + assert_ok!(SubtensorModule::do_dissolve_network(net)); + + assert!(AutoStakeDestination::::get(staker_cold, net).is_none()); + assert!(AutoStakeDestinationColdkeys::::get(stale_dest_hot, net).is_empty()); + }); +} + #[test] fn dissolve_alpha_out_but_zero_tao_no_rewards() { new_test_ext(0).execute_with(|| { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 00d3839fa7..85a35d21c8 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -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,