From fd2be3f4ea9b6ced9764983e37fdccf9b15991f1 Mon Sep 17 00:00:00 2001 From: ArtificialXai Date: Mon, 20 Apr 2026 03:54:27 -0400 Subject: [PATCH] feat(subtensor): reset subnet identity on subnet create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. 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. --- pallets/subtensor/src/subnets/subnet.rs | 33 +++++-- pallets/subtensor/src/tests/networks.rs | 117 ++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 9 deletions(-) diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index c6a87af63a..f15cdad502 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -235,15 +235,30 @@ impl Pallet { Self::increase_total_stake(pool_initial_tao); } - // --- 17. Add the identity if it exists - if let Some(identity_value) = identity { - ensure!( - Self::is_valid_subnet_identity(&identity_value), - Error::::InvalidIdentity - ); - - SubnetIdentitiesV3::::insert(netuid_to_register, identity_value); - Self::deposit_event(Event::SubnetIdentitySet(netuid_to_register)); + // --- 17. Set or reset the subnet identity. + // + // If the caller provided identity info, validate and store it. + // Otherwise clear any identity that may still be attached to this + // netuid slot from a previous owner — storage migrations or older + // code paths can leave orphaned `SubnetIdentitiesV3` entries which + // would otherwise leak into the newly-created subnet and mislead + // participants about what the subnet is about (see issue #2572). + match identity { + Some(identity_value) => { + ensure!( + Self::is_valid_subnet_identity(&identity_value), + Error::::InvalidIdentity + ); + + SubnetIdentitiesV3::::insert(netuid_to_register, identity_value); + Self::deposit_event(Event::SubnetIdentitySet(netuid_to_register)); + } + None => { + if SubnetIdentitiesV3::::contains_key(netuid_to_register) { + SubnetIdentitiesV3::::remove(netuid_to_register); + Self::deposit_event(Event::SubnetIdentityRemoved(netuid_to_register)); + } + } } // --- 18. Emit the NetworkAdded event. diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index f6a50cf4ff..46950dccba 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -1435,6 +1435,123 @@ fn register_network_fails_before_prune_keeps_existing() { }); } +// Regression for https://github.com/opentensor/subtensor/issues/2572: +// a stale `SubnetIdentitiesV3` entry on a netuid slot that is about to be +// assigned to a new subnet must be cleared when the new owner does not +// provide identity information of their own. +#[test] +fn register_network_with_none_identity_clears_stale_entry() { + new_test_ext(0).execute_with(|| { + let expected_netuid = SubtensorModule::get_next_netuid(); + + let stale_identity = SubnetIdentityOfV3 { + subnet_name: b"Previous Owner".to_vec(), + github_repo: b"https://github.com/previous/subnet".to_vec(), + subnet_contact: b"previous@example.com".to_vec(), + subnet_url: b"https://previous.example.com".to_vec(), + discord: b"previous#1234".to_vec(), + description: b"left over from a prior owner".to_vec(), + logo_url: b"https://previous.example.com/logo.png".to_vec(), + additional: b"stale".to_vec(), + }; + SubnetIdentitiesV3::::insert(expected_netuid, stale_identity); + assert!(SubnetIdentitiesV3::::contains_key(expected_netuid)); + + let caller_cold = U256::from(61); + let caller_hot = U256::from(62); + let lock_cost: u64 = SubtensorModule::get_network_lock_cost().into(); + SubtensorModule::add_balance_to_coldkey_account(&caller_cold, lock_cost.into()); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(caller_cold), + &caller_hot, + 1, + None, + )); + + assert_eq!(SubnetOwner::::get(expected_netuid), caller_cold); + assert!( + !SubnetIdentitiesV3::::contains_key(expected_netuid), + "stale identity must be cleared when the new owner provides None", + ); + }); +} + +// Companion to `register_network_with_none_identity_clears_stale_entry`: +// when the new owner *does* provide identity info, it must simply replace +// the stale entry (not be treated as a no-op, not panic). +#[test] +fn register_network_with_some_identity_overwrites_stale_entry() { + new_test_ext(0).execute_with(|| { + let expected_netuid = SubtensorModule::get_next_netuid(); + + let stale_identity = SubnetIdentityOfV3 { + subnet_name: b"Previous Owner".to_vec(), + github_repo: b"https://github.com/previous/subnet".to_vec(), + subnet_contact: b"previous@example.com".to_vec(), + subnet_url: b"https://previous.example.com".to_vec(), + discord: b"previous#1234".to_vec(), + description: b"left over from a prior owner".to_vec(), + logo_url: b"https://previous.example.com/logo.png".to_vec(), + additional: b"stale".to_vec(), + }; + SubnetIdentitiesV3::::insert(expected_netuid, stale_identity); + + let new_identity = SubnetIdentityOfV3 { + subnet_name: b"Brand New Subnet".to_vec(), + github_repo: b"https://github.com/new/subnet".to_vec(), + subnet_contact: b"new@example.com".to_vec(), + subnet_url: b"https://new.example.com".to_vec(), + discord: b"new#5678".to_vec(), + description: b"a fresh subnet".to_vec(), + logo_url: b"https://new.example.com/logo.png".to_vec(), + additional: b"fresh".to_vec(), + }; + + let caller_cold = U256::from(71); + let caller_hot = U256::from(72); + let lock_cost: u64 = SubtensorModule::get_network_lock_cost().into(); + SubtensorModule::add_balance_to_coldkey_account(&caller_cold, lock_cost.into()); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(caller_cold), + &caller_hot, + 1, + Some(new_identity.clone()), + )); + + let stored = SubnetIdentitiesV3::::get(expected_netuid) + .expect("new identity should be stored"); + assert_eq!(stored, new_identity); + }); +} + +// Complement to `register_network_with_none_identity_clears_stale_entry`: +// if the netuid slot had no stale identity to begin with, registering +// with `None` must leave the storage map untouched (and must not emit +// a spurious `SubnetIdentityRemoved` event). +#[test] +fn register_network_with_none_identity_no_op_when_slot_empty() { + new_test_ext(0).execute_with(|| { + let expected_netuid = SubtensorModule::get_next_netuid(); + assert!(!SubnetIdentitiesV3::::contains_key(expected_netuid)); + + let caller_cold = U256::from(81); + let caller_hot = U256::from(82); + let lock_cost: u64 = SubtensorModule::get_network_lock_cost().into(); + SubtensorModule::add_balance_to_coldkey_account(&caller_cold, lock_cost.into()); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(caller_cold), + &caller_hot, + 1, + None, + )); + + assert!(!SubnetIdentitiesV3::::contains_key(expected_netuid)); + }); +} + #[test] fn test_migrate_network_immunity_period() { new_test_ext(0).execute_with(|| {