From 3aa040eca81db7167d482d906bd389d67081f5ff Mon Sep 17 00:00:00 2001 From: Loom Agent Date: Mon, 13 Jul 2026 12:36:46 +0000 Subject: [PATCH 1/2] feat(subtensor): add migration to clear orphan subnet identities Add a one-shot runtime migration migrate_clear_orphan_subnet_identities_v3 that removes SubnetIdentitiesV3 entries for netuids no longer in NetworksAdded (orphan identities left behind when a subnet slot is recycled). Each removal emits SubnetIdentityRemoved; identities of live subnets are untouched. Guarded by HasMigrationRun (idempotent, runs once at upgrade) and wired into on_runtime_upgrade in macros/hooks.rs, next to the adjacent wired migrations. Closes #2572. Builds on the migration-only approach discussed in #2601. spec_version 429 -> 432. --- pallets/subtensor/src/macros/hooks.rs | 4 +- ...grate_clear_orphan_subnet_identities_v3.rs | 63 +++++++++++++++++++ pallets/subtensor/src/migrations/mod.rs | 1 + pallets/subtensor/src/tests/migration.rs | 45 +++++++++++++ runtime/src/lib.rs | 2 +- 5 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 pallets/subtensor/src/migrations/migrate_clear_orphan_subnet_identities_v3.rs diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index ed0b46314b..7f7320ce2c 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -179,7 +179,9 @@ mod hooks { // Fix lock state left behind by subnet-scoped hotkey swaps. .saturating_add(migrations::migrate_fix_subnet_hotkey_lock_swaps::migrate_fix_subnet_hotkey_lock_swaps::()) // Populate reverse lookup index for EVM address associations. - .saturating_add(migrations::migrate_associated_evm_address_index::migrate_associated_evm_address_index::()); + .saturating_add(migrations::migrate_associated_evm_address_index::migrate_associated_evm_address_index::()) + // Remove orphan SubnetIdentitiesV3 entries left for recycled netuids. + .saturating_add(migrations::migrate_clear_orphan_subnet_identities_v3::migrate_clear_orphan_subnet_identities_v3::()); weight } diff --git a/pallets/subtensor/src/migrations/migrate_clear_orphan_subnet_identities_v3.rs b/pallets/subtensor/src/migrations/migrate_clear_orphan_subnet_identities_v3.rs new file mode 100644 index 0000000000..b024daca47 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_clear_orphan_subnet_identities_v3.rs @@ -0,0 +1,63 @@ +use crate::{Config, Event, HasMigrationRun, NetworksAdded, Pallet, SubnetIdentitiesV3, Weight}; +use frame_support::traits::Get; +use scale_info::prelude::string::String; +use sp_std::vec::Vec; +use subtensor_runtime_common::NetUid; + +/// Remove `SubnetIdentitiesV3` entries that belong to netuids which are no +/// longer registered networks (`!NetworksAdded`). +/// +/// Such orphan identities accumulate when a subnet slot is recycled: a new +/// owner registers a subnet in a previously-used netuid without supplying an +/// identity, and the stale identity from the prior owner is left in place, +/// misleading participants about what the subnet is. The registration path +/// (`set_new_network_state`) only writes on `Some(identity)` and never clears a +/// pre-existing entry, and the historical `migrate_subnet_identities_to_v3` +/// migration copied V2 entries unconditionally. +/// +/// This mirrors the identity-clear in `do_dissolve_network` as a one-shot +/// upgrade migration: orphan entries are removed and a `SubnetIdentityRemoved` +/// event is emitted for each; identities of live subnets are untouched. +pub fn migrate_clear_orphan_subnet_identities_v3() -> Weight { + let migration_name = b"migrate_clear_orphan_subnet_identities_v3".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + target: "runtime", + "Migration '{}' already run - skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + target: "runtime", + "Running migration '{}'", + String::from_utf8_lossy(&migration_name), + ); + + // Collect identity netuids that no longer correspond to a registered subnet. + let orphan_netuids: Vec = SubnetIdentitiesV3::::iter_keys() + .filter(|netuid| !NetworksAdded::::get(netuid)) + .collect(); + let removed = orphan_netuids.len() as u64; + weight = weight.saturating_add(T::DbWeight::get().reads(removed)); + + for netuid in orphan_netuids { + SubnetIdentitiesV3::::remove(netuid); + Pallet::::deposit_event(Event::SubnetIdentityRemoved(netuid)); + } + weight = weight.saturating_add(T::DbWeight::get().writes(removed)); + + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + target: "runtime", + "Migration '{}' completed.", + String::from_utf8_lossy(&migration_name), + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index 98699ccbce..ff9829ec1f 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -8,6 +8,7 @@ pub mod migrate_associated_evm_address_index; pub mod migrate_auto_stake_destination; pub mod migrate_cleanup_swap_v3; pub mod migrate_clear_deprecated_registration_maps; +pub mod migrate_clear_orphan_subnet_identities_v3; pub mod migrate_coldkey_swap_scheduled; pub mod migrate_coldkey_swap_scheduled_to_announcements; pub mod migrate_commit_reveal_settings; diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 8e81447fbc..c002070d63 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -78,6 +78,51 @@ fn test_migrate_associated_evm_address_index() { }); } +#[test] +fn test_migrate_clear_orphan_subnet_identities_v3() { + new_test_ext(1).execute_with(|| { + let migration_name = b"migrate_clear_orphan_subnet_identities_v3".to_vec(); + HasMigrationRun::::remove(&migration_name); + + let orphan_netuid = NetUid::from(1); + let live_netuid = NetUid::from(2); + + // live_netuid is a registered network; orphan_netuid is not. + NetworksAdded::::insert(live_netuid, true); + + let orphan_identity = SubnetIdentityV3 { + subnet_name: b"orphan".to_vec(), + ..Default::default() + }; + let live_identity = SubnetIdentityV3 { + subnet_name: b"live".to_vec(), + ..Default::default() + }; + + SubnetIdentitiesV3::::insert(orphan_netuid, orphan_identity); + SubnetIdentitiesV3::::insert(live_netuid, live_identity.clone()); + + crate::migrations::migrate_clear_orphan_subnet_identities_v3::migrate_clear_orphan_subnet_identities_v3::(); + + // The orphan identity is removed; the live subnet identity is preserved. + assert!(!SubnetIdentitiesV3::::contains_key(orphan_netuid)); + assert_eq!( + SubnetIdentitiesV3::::get(live_netuid), + Some(live_identity.clone()) + ); + + // Migration is marked as run. + assert!(HasMigrationRun::::get(&migration_name)); + + // Idempotent: re-running is a no-op (live identity still present). + crate::migrations::migrate_clear_orphan_subnet_identities_v3::migrate_clear_orphan_subnet_identities_v3::(); + assert_eq!( + SubnetIdentitiesV3::::get(live_netuid), + Some(live_identity) + ); + }); +} + #[test] fn test_migrate_associated_evm_address_index_reconciles_over_cap_buckets() { new_test_ext(1).execute_with(|| { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 017a71a855..3ab1e61284 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -235,7 +235,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: 429, + spec_version: 432, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 4e5789a30e8cab9332bc1bdd691b99cd2aec71b3 Mon Sep 17 00:00:00 2001 From: Loom Agent Date: Mon, 13 Jul 2026 12:36:50 +0000 Subject: [PATCH 2/2] fix(subtensor): allow deprecated storage in legacy migration test main's Check Rust CI is currently red because the typed-units WIP marked the LastTxBlockDelegateTake storage map #[deprecated] while its legacy-migration test (test_migrate_last_tx_block_delegate_take) still references it, so 'cargo clippy --workspace --all-targets -- -D warnings' fails on main. That test deliberately touches the deprecated legacy storage to verify migration off it, so annotate the test fn with #[allow(deprecated)]. Behavior-neutral lint suppression, unrelated to #2572. --- pallets/subtensor/src/tests/migration.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index c002070d63..183a409d52 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -1299,6 +1299,7 @@ fn test_per_u16_encodes_identically_to_u16() { } #[test] +#[allow(deprecated)] fn test_migrate_last_tx_block_delegate_take() { new_test_ext(1).execute_with(|| { // ------------------------------