diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 067d5855b..5f8caade1 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -1140,30 +1140,47 @@ impl Pallet { let owner_coldkey: T::AccountId = SubnetOwner::::get(netuid); let reserved_amount: u64 = Self::get_subnet_locked_balance(netuid); - // --- 2. Remove network count. + // --- 2. Unstake nominators and delegates + as IterableStorageDoubleMap>::iter().for_each( + |(hotkey, coldkey, stake)| { + if !Uids::::contains_key(netuid, &hotkey) { + return; + } + + let staking_key = match Self::hotkey_is_delegate(&hotkey) { + true => Owner::::get(&hotkey), + false => coldkey.clone(), + }; + + Self::add_balance_to_coldkey_account(&staking_key, stake); + Self::decrease_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake); + }, + ); + + // --- 3. Remove network count. SubnetworkN::::remove(netuid); - // --- 3. Remove network modality storage. + // --- 4. Remove network modality storage. NetworkModality::::remove(netuid); - // --- 4. Remove netuid from added networks. + // --- 5. Remove netuid from added networks. NetworksAdded::::remove(netuid); - // --- 5. Decrement the network counter. + // --- 6. Decrement the network counter. TotalNetworks::::mutate(|n: &mut u16| *n = n.saturating_sub(1)); - // --- 6. Remove various network-related storages. + // --- 7. Remove various network-related storages. NetworkRegisteredAt::::remove(netuid); - // --- 7. Remove incentive mechanism memory. + // --- 8. Remove incentive mechanism memory. let _ = Uids::::clear_prefix(netuid, u32::MAX, None); let _ = Keys::::clear_prefix(netuid, u32::MAX, None); let _ = Bonds::::clear_prefix(netuid, u32::MAX, None); - // --- 8. Removes the weights for this subnet (do not remove). + // --- 9. Removes the weights for this subnet (do not remove). let _ = Weights::::clear_prefix(netuid, u32::MAX, None); - // --- 9. Iterate over stored weights and fill the matrix. + // --- 10. Iterate over stored weights and fill the matrix. for (uid_i, weights_i) in as IterableStorageDoubleMap>>::iter_prefix( Self::get_root_netuid(), @@ -1181,7 +1198,7 @@ impl Pallet { Weights::::insert(Self::get_root_netuid(), uid_i, modified_weights); } - // --- 10. Remove various network-related parameters. + // --- 11. Remove various network-related parameters. Rank::::remove(netuid); Trust::::remove(netuid); Active::::remove(netuid); @@ -1194,7 +1211,7 @@ impl Pallet { ValidatorPermit::::remove(netuid); ValidatorTrust::::remove(netuid); - // --- 11. Erase network parameters. + // --- 12. Erase network parameters. Tempo::::remove(netuid); Kappa::::remove(netuid); Difficulty::::remove(netuid); @@ -1208,12 +1225,12 @@ impl Pallet { POWRegistrationsThisInterval::::remove(netuid); BurnRegistrationsThisInterval::::remove(netuid); - // --- 12. Add the balance back to the owner. + // --- 13. Add the balance back to the owner. Self::add_balance_to_coldkey_account(&owner_coldkey, reserved_amount); Self::set_subnet_locked_balance(netuid, 0); SubnetOwner::::remove(netuid); - // --- 13. Remove subnet identity if it exists. + // --- 14. Remove subnet identity if it exists. if SubnetIdentities::::contains_key(netuid) { SubnetIdentities::::remove(netuid); Self::deposit_event(Event::SubnetIdentityRemoved(netuid)); diff --git a/pallets/subtensor/tests/root.rs b/pallets/subtensor/tests/root.rs index caf1e5935..93b90233e 100644 --- a/pallets/subtensor/tests/root.rs +++ b/pallets/subtensor/tests/root.rs @@ -983,6 +983,310 @@ fn test_dissolve_network_does_not_exist_err() { }); } +#[test] +fn test_dissolve_network_unstake_nominators_ok() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 30; + let hotkey = U256::from(1); + let coldkey = U256::from(2); + let stake: u64 = 1000; + + // Set up the network and stake + add_network(netuid, 0, 0); + pallet_subtensor::SubnetOwner::::insert(netuid, coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 3); + + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 10); + let initial_balance: u64 = SubtensorModule::get_coldkey_balance(&coldkey); + + >::insert(hotkey, coldkey, stake); + pallet_subtensor::Uids::::insert(netuid, hotkey, 0); + + // Call the dissolve_network function, which should trigger unstaking + assert_ok!(SubtensorModule::dissolve_network( + RuntimeOrigin::root(), + coldkey, + netuid + )); + + let new_balance: u64 = SubtensorModule::get_coldkey_balance(&coldkey); + + // Check that the balance was correctly updated and the stake was removed + assert_eq!(new_balance, initial_balance + stake); + assert_eq!(>::get(hotkey, coldkey), 0); + }); +} + +#[test] +fn test_dissolve_network_unstake_delegate_ok() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 30; + let hotkey = U256::from(1); + let coldkey = U256::from(2); + let delegate_coldkey = U256::from(3); + let stake: u64 = 1000; + + // Set up the network and associate the coldkey with the hotkey + add_network(netuid, 0, 0); + pallet_subtensor::SubnetOwner::::insert(netuid, coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 3); + >::insert(hotkey, coldkey, stake); + pallet_subtensor::Uids::::insert(netuid, hotkey, 0); + + // Insert delegate ownership for the hotkey + pallet_subtensor::Owner::::insert(hotkey, coldkey); + + // Ensure the hotkey is treated as a delegate + assert_ok!(SubtensorModule::do_become_delegate( + <::RuntimeOrigin>::signed(coldkey), + hotkey, + u16::MAX / 10 + )); + + // Update the owner to the delegate coldkey + pallet_subtensor::Owner::::insert(hotkey, delegate_coldkey); + + let initial_delegate_balance = SubtensorModule::get_coldkey_balance(&delegate_coldkey); + + // Call the dissolve_network function + assert_ok!(SubtensorModule::dissolve_network( + RuntimeOrigin::root(), + coldkey, + netuid + )); + + let new_delegate_balance: u64 = SubtensorModule::get_coldkey_balance(&delegate_coldkey); + + // Check that the delegate's balance was correctly updated and the stake was removed + assert_eq!(new_delegate_balance, initial_delegate_balance + stake); + assert_eq!(>::get(hotkey, coldkey), 0); + }); +} + +#[test] +fn test_dissolve_network_unstake_nominators_and_delegates_partial_match() { + new_test_ext(1).execute_with(|| { + let netuid1: u16 = 30; + let hotkey1 = U256::from(1); + let coldkey1 = U256::from(2); + let stake1: u64 = 1000; + + let netuid2: u16 = 31; + let hotkey2 = U256::from(3); + let coldkey2 = U256::from(4); + let stake2: u64 = 500; + + // Set up two distinct networks and stakes + add_network(netuid1, 0, 0); + add_network(netuid2, 0, 0); + pallet_subtensor::SubnetOwner::::insert(netuid1, coldkey1); + pallet_subtensor::SubnetOwner::::insert(netuid2, coldkey2); + register_ok_neuron(netuid1, hotkey1, coldkey1, 3); + register_ok_neuron(netuid2, hotkey2, coldkey2, 3); + >::insert(hotkey1, coldkey1, stake1); + >::insert(hotkey2, coldkey2, stake2); + pallet_subtensor::Uids::::insert(netuid1, hotkey1, 0); // Only hotkey1 is in Uids for the first network + + let initial_balance1: u64 = SubtensorModule::get_coldkey_balance(&coldkey1); + let initial_balance2: u64 = SubtensorModule::get_coldkey_balance(&coldkey2); + + // Call the dissolve_network function on the second network, which should only affect hotkey2 + assert_ok!(SubtensorModule::dissolve_network( + RuntimeOrigin::root(), + coldkey2, + netuid2 + )); + + let new_balance1: u64 = SubtensorModule::get_coldkey_balance(&coldkey1); + let new_balance2: u64 = SubtensorModule::get_coldkey_balance(&coldkey2); + + // Check that only the second network's stake was removed and balance updated + assert_eq!(new_balance1, initial_balance1); + assert_eq!(new_balance2, initial_balance2 + stake2); // Only change for hotkey2 + assert_eq!( + >::get(hotkey1, coldkey1), + stake1 + ); + assert_eq!(>::get(hotkey2, coldkey2), 0); + }); +} + +#[test] +fn test_dissolve_network_unstake_multiple_stakes_same_hotkey() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 30; + let hotkey = U256::from(1); + let coldkey1 = U256::from(2); + let coldkey2 = U256::from(3); + let stake1: u64 = 1000; + let stake2: u64 = 500; + + // Set up the network and multiple stakes + add_network(netuid, 0, 0); + pallet_subtensor::SubnetOwner::::insert(netuid, coldkey1); + register_ok_neuron(netuid, hotkey, coldkey1, 3); + >::insert(hotkey, coldkey1, stake1); + >::insert(hotkey, coldkey2, stake2); + pallet_subtensor::Uids::::insert(netuid, hotkey, 0); + + let initial_balance1: u64 = SubtensorModule::get_coldkey_balance(&coldkey1); + let initial_balance2: u64 = SubtensorModule::get_coldkey_balance(&coldkey2); + + // Call the dissolve_network function + assert_ok!(SubtensorModule::dissolve_network( + RuntimeOrigin::root(), + coldkey1, + netuid + )); + + let new_balance1: u64 = SubtensorModule::get_coldkey_balance(&coldkey1); + let new_balance2: u64 = SubtensorModule::get_coldkey_balance(&coldkey2); + + // Check that both stakes were removed and the balances updated + assert_eq!(new_balance1, initial_balance1 + stake1); + assert_eq!(new_balance2, initial_balance2 + stake2); + assert_eq!(>::get(hotkey, coldkey1), 0); + assert_eq!(>::get(hotkey, coldkey2), 0); + }); +} + +#[test] +fn test_dissolve_network_complex_state() { + new_test_ext(1).execute_with(|| { + let num_networks = 10; + let mut netuids = vec![]; + let mut hotkeys = vec![]; + let mut coldkeys = vec![]; + let mut stakes = vec![]; + let mut subnet_owners = vec![]; + let mut initial_balances = vec![]; + + // Setup: Create 10 networks and populate each with hotkeys, coldkeys, and stakes + for netuid in 1..=num_networks { + let hotkey_base = U256::from(netuid * 10 + 1); + let coldkey_base = U256::from(netuid * 10 + 2); + + netuids.push(netuid); + subnet_owners.push(coldkey_base); + + add_network(netuid, 0, 0); + pallet_subtensor::SubnetOwner::::insert(netuid, coldkey_base); + SubtensorModule::set_max_registrations_per_block(netuid, 4096); + SubtensorModule::set_target_registrations_per_interval(netuid, 9999); + + for i in 1..=5 { + // 5 UIDs in each network + let hotkey = U256::from(hotkey_base.as_u64() + i); + let coldkey = U256::from(coldkey_base.as_u64() + i); + + let initial_balance = SubtensorModule::get_coldkey_balance(&coldkey_base); + let new_balance = initial_balance + i * 2; + initial_balances.push(new_balance); + SubtensorModule::add_balance_to_coldkey_account(&coldkey, new_balance); + + hotkeys.push(hotkey); + coldkeys.push(coldkey); + + // Alternate between having stake and no stake + if i % 2 == 0 { + let stake = (i + 1) * 1000; + stakes.push(stake); + + register_ok_neuron(netuid, hotkey, coldkey, 3); + >::insert(hotkey, coldkey, stake); + + log::debug!( + "Registered neuron {} in network {} with coldkey {} and stake {}", + hotkey, + netuid, + coldkey, + stake + ); + } else if i % 3 == 0 { + let stake = (i + 1) * 1000; + stakes.push(stake); + + register_ok_neuron(netuid, hotkey, coldkey, 3); + >::insert(hotkey, coldkey, stake); + + assert_ok!(SubtensorModule::do_become_delegate( + <::RuntimeOrigin>::signed(coldkey), + hotkey, + u16::MAX / 10 + )); + + log::debug!( + "Registered neuron as delegate {} in network {} with coldkey {} and stake {}", + hotkey, + netuid, + coldkey, + stake + ); + } else { + stakes.push(0); // No stake for this UID + register_ok_neuron(netuid, hotkey, coldkey, 3); + + log::debug!( + "Registered neuron {} in network {} with coldkey {}, no stake", + hotkey, + netuid, + coldkey + ); + } + + pallet_subtensor::Uids::::insert(netuid, hotkey, i as u16); + } + } + + // Test: Dissolve each network and ensure correct unstaking behavior + for (netuid, subnet_owner) in netuids.iter().zip(subnet_owners.iter()) { + log::debug!( + "Dissolving network {} with subnet owner {}", + netuid, + subnet_owner + ); + assert_ok!(SubtensorModule::dissolve_network( + RuntimeOrigin::root(), + *subnet_owner, + *netuid + )); + } + + // Verify: Check that all stakes were correctly unstaked and balances updated for stakeholders + for i in 0..hotkeys.len() { + let hotkey = &hotkeys[i]; + let coldkey = &coldkeys[i]; + let stake = stakes[i]; + + log::debug!( + "Checking stake for hotkey {} and coldkey {}: stake = {}", + hotkey, + coldkey, + stake + ); + + let staking_key = if SubtensorModule::hotkey_is_delegate(hotkey) { + pallet_subtensor::Owner::::get(hotkey) + } else { + *coldkey + }; + + let new_balance = SubtensorModule::get_coldkey_balance(&staking_key); + + log::debug!( + "Hotkey {} has staking key {}. New balance is {}, expected stake is {}", + hotkey, + staking_key, + new_balance, + stake + ); + + assert_eq!(new_balance, stake + initial_balances[i]); + assert_eq!(>::get(hotkey, coldkey), 0); + } + }); +} + #[test] fn test_user_add_network_with_identity_fields_ok() { new_test_ext(1).execute_with(|| { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 9622c6e2a..b7a347c46 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -142,7 +142,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: 196, + spec_version: 197, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,