Skip to content

feat(subtensor): reset subnet identity on subnet create#2601

Open
ArtificialXai wants to merge 1 commit intoopentensor:mainfrom
ArtificialXai:artificialxai/reset-subnet-identity-on-create
Open

feat(subtensor): reset subnet identity on subnet create#2601
ArtificialXai wants to merge 1 commit intoopentensor:mainfrom
ArtificialXai:artificialxai/reset-subnet-identity-on-create

Conversation

@ArtificialXai
Copy link
Copy Markdown

Summary

Closes #2572.

do_register_network previously only set a SubnetIdentityV3 entry when the caller passed Some(identity) and left the storage map untouched otherwise. Storage migrations and legacy code paths can leave an orphan SubnetIdentitiesV3 entry on a netuid slot that later gets assigned to a new owner; that orphan then leaks into the fresh subnet and misrepresents what the subnet is about (wrong name, wrong GitHub repo, wrong Discord, etc.).

This PR makes the identity argument authoritative, matching the natural intuition of "the caller of register_network fully owns the resulting subnet's presentation":

  • Some(identity) — validate and insert (existing behavior, unchanged).
  • None — if an entry already exists for the target netuid, remove it and emit SubnetIdentityRemoved; no-op if the slot is already empty.

No runtime migration required — the fix is forward-only and only kicks in when do_register_network is called, so existing subnets are unaffected.

Changes

  • pallets/subtensor/src/subnets/subnet.rs — step 17 of do_register_network now uses a match identity instead of if let Some(..), with a None branch that clears any stale SubnetIdentitiesV3 entry and emits the existing SubnetIdentityRemoved event (already used by do_dissolve_network in coinbase/root.rs).
  • pallets/subtensor/src/tests/networks.rs — three regression tests:
    • register_network_with_none_identity_clears_stale_entry — pre-inserts a stale SubnetIdentityV3 at the predicted next netuid, calls do_register_network(..., None), asserts the entry is gone.
    • register_network_with_some_identity_overwrites_stale_entry — same setup but passes Some(new_identity), asserts the stored entry equals new_identity.
    • register_network_with_none_identity_no_op_when_slot_empty — registers with None against an empty slot, asserts no SubnetIdentitiesV3 entry is created.

Test plan

  • cargo check -p pallet-subtensor
  • cargo test -p pallet-subtensor --features pow-faucet -- tests::networks::register_network_with_none_identity_clears_stale_entry
  • cargo test -p pallet-subtensor --features pow-faucet -- tests::networks::register_network_with_some_identity_overwrites_stale_entry
  • cargo test -p pallet-subtensor --features pow-faucet -- tests::networks::register_network_with_none_identity_no_op_when_slot_empty
  • Full cargo test -p pallet-subtensor (CI)

Notes

  • SubnetIdentityRemoved event is already emitted by do_dissolve_network when a subnet is torn down, so consumers that index events should already handle it gracefully.
  • The new None-branch behavior only removes stale state; it can never overwrite a caller's current subnet identity, since it runs inside do_register_network which creates the subnet.
  • Fixes the user-visible symptom of a newly registered subnet appearing with a previous owner's subnet_name / github_repo / discord until the new owner manually calls set_subnet_identity.

@open-junius
Copy link
Copy Markdown
Contributor

can you identity the "Storage migrations and legacy code paths can leave an orphan SubnetIdentitiesV3 entry". then we fix it, instead of add the check in registration.

Closes opentensor#2572.

`do_register_network` previously only *set* a `SubnetIdentityV3` entry
when the caller passed `Some(identity)` and left the storage map
untouched otherwise. Storage migrations and legacy code paths can leave
an orphan `SubnetIdentitiesV3` entry on a netuid slot that later gets
assigned to a new owner; that orphan then leaks into the fresh subnet
and misrepresents what the subnet is about.

This change makes the `identity` argument authoritative:

* `Some(identity)` — validate and insert (existing behavior).
* `None`           — if an entry already exists for the target netuid,
                      remove it and emit `SubnetIdentityRemoved`; no-op
                      if the slot is already empty.

Three regression tests in `tests::networks` cover the three branches.
@ArtificialXai ArtificialXai force-pushed the artificialxai/reset-subnet-identity-on-create branch from 8e169dd to fd2be3f Compare April 24, 2026 15:15
@ArtificialXai
Copy link
Copy Markdown
Author

Signed and re-pushed (fd2be3f).

@ArtificialXai
Copy link
Copy Markdown
Author

@open-junius Thanks for pushing back — chased it down, and the orphan source is migrate_subnet_identities_to_v3.

Introduced in 8bcfa2ae9 (2025-06-12, "feat: add logo_url to subnet identities"), file pallets/subtensor/src/migrations/migrate_subnet_identities_to_v3.rs:

let old_subnet_identities = SubnetIdentitiesV2::<T>::iter().collect::<Vec<_>>();
for (netuid, old_subnet_identity) in old_subnet_identities.clone() {
    let new_subnet_identity = SubnetIdentityV3 { /* copy fields, blanks for new ones */ };
    SubnetIdentitiesV3::<T>::insert(netuid, &new_subnet_identity);
    SubnetIdentitiesV2::<T>::remove(netuid);
}

The loop copies every SubnetIdentitiesV2 entry into V3 unconditionally. It never checks NetworksAdded::<T>::get(netuid) or SubnetOwner::<T>::contains_key(netuid), so any V2 entry that was already orphaned at migration time gets faithfully carried forward into V3.

V2 had the same hazard before it: older dissolve paths didn't scrub SubnetIdentitiesV2 (the V3 scrub in do_dissolve_network only lands in the same 8bcfa2ae9 that introduces V3 — for V2, dissolve never cleared identity). So any subnet dissolved before 2025-06-12 left its V2 entry behind, and the V3 migration then promoted that orphan.

4999dcbe21 (2025-07-08, "fix: run subnet identity v3 migration (and ignore if already exists)") re-ran the migration after a skip, which would have re-promoted any V2 entries recreated in the interim if that path existed — but the primary leak is still the unconditional copy.

Given this, the surgical source-level fix you're asking for is a one-shot cleanup migration that iterates SubnetIdentitiesV3 and removes entries where NetworksAdded::<T>::get(netuid).unwrap_or(false) == false. Something like:

pub fn migrate_clear_orphan_subnet_identities_v3<T: Config>() -> Weight {
    let migration_name = b"migrate_clear_orphan_subnet_identities_v3".to_vec();
    let mut weight = T::DbWeight::get().reads(1);
    if HasMigrationRun::<T>::get(&migration_name) { return weight; }

    let netuids: Vec<_> = SubnetIdentitiesV3::<T>::iter_keys().collect();
    for netuid in netuids {
        weight = weight.saturating_add(T::DbWeight::get().reads(1));
        if !NetworksAdded::<T>::get(netuid) {
            SubnetIdentitiesV3::<T>::remove(netuid);
            Pallet::<T>::deposit_event(Event::SubnetIdentityRemoved(netuid));
            weight = weight.saturating_add(T::DbWeight::get().writes(1));
        }
    }
    HasMigrationRun::<T>::insert(&migration_name, true);
    weight.saturating_add(T::DbWeight::get().writes(1))
}

Happy to fold that migration into this PR and drop the None-path clear in do_register_network if you'd rather land the source-level fix alone. Or keep both (migration + defensive clear at registration) as belt-and-braces against a future V3→V4 migration reintroducing the same class of bug — that decision is yours. Let me know which shape you'd like and I'll push.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reset subnet identity upon subnet create

2 participants