Skip to content

dTao-Compatible Subnet Deregistration #1685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 40 commits into
base: devnet-ready
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0178b88
impl get_network_to_prune
JohnReedV May 26, 2025
562823c
use NetworkImmunityPeriod
JohnReedV May 27, 2025
b2ef90e
update get_network_to_prune.
JohnReedV May 27, 2025
d569309
add `NetworkActivationDeadline `
JohnReedV May 27, 2025
8016183
WIP
JohnReedV Jun 2, 2025
000c3d0
remove NetworkActivationDeadline
JohnReedV Jun 2, 2025
dfe72d5
update InitialNetworkImmunityPeriod
JohnReedV Jun 2, 2025
2676c63
add dissolve_network tests
JohnReedV Jun 5, 2025
a4c78f3
fmt
JohnReedV Jun 5, 2025
ac29e34
unused param
JohnReedV Jun 9, 2025
17f0982
clean up destroy_alpha_in_out_stakes
JohnReedV Jun 9, 2025
4b03bc9
update destroy_alpha_in_out_stakes
JohnReedV Jun 9, 2025
7c205a9
add more tests
JohnReedV Jun 9, 2025
f948305
add destroy_alpha_out_multiple_stakers_pro_rata
JohnReedV Jun 9, 2025
39f77cf
add test destroy_alpha_out_many_stakers_complex_distribution
JohnReedV Jun 9, 2025
ee61b24
remove comment
JohnReedV Jun 9, 2025
512578b
add pruning tests
JohnReedV Jun 9, 2025
b45c9eb
fmt
JohnReedV Jun 9, 2025
b12164a
use saturating math
JohnReedV Jun 9, 2025
bc9cab8
clippy
JohnReedV Jun 9, 2025
fc5c05c
add SubnetLimit
JohnReedV Jun 9, 2025
4d746b3
prune subnets in do_register_network
JohnReedV Jun 9, 2025
90216c2
update doc comment
JohnReedV Jun 9, 2025
cf6a750
add register_network tests
JohnReedV Jun 9, 2025
8593dae
update weights
JohnReedV Jun 9, 2025
9796751
Merge branch 'devnet-ready' into dtao-subnet-deregistration
JohnReedV Jun 10, 2025
e35cc67
move alpha to root instead of wallet balance
JohnReedV Jun 18, 2025
af4ddc7
update tests
JohnReedV Jun 18, 2025
9e18ea9
Merge branch 'devnet-ready' into dtao-subnet-deregistration
JohnReedV Jun 18, 2025
02e2d07
resolve conflict errors
JohnReedV Jun 18, 2025
f42d45f
fmt
JohnReedV Jun 18, 2025
77ceec2
add migration for immunity_period
JohnReedV Jun 18, 2025
1e03849
Merge branch 'devnet-ready' into dtao-subnet-deregistration
JohnReedV Jun 22, 2025
a2913d9
fix cargo lock
JohnReedV Jun 22, 2025
800471f
add root_dissolve_network
JohnReedV Jun 23, 2025
6d86784
fmt
JohnReedV Jun 23, 2025
43a9270
clippy
JohnReedV Jun 23, 2025
8e9b08c
rm DefaultStakingFee
JohnReedV Jun 23, 2025
1edbfd6
fix test
JohnReedV Jun 23, 2025
2c52732
fmt
JohnReedV Jun 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 128 additions & 119 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion pallets/admin-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1015,8 +1015,10 @@ pub mod pallet {
DispatchClass::Operational,
Pays::No
))]
pub fn sudo_set_subnet_limit(origin: OriginFor<T>, _max_subnets: u16) -> DispatchResult {
pub fn sudo_set_subnet_limit(origin: OriginFor<T>, max_subnets: u16) -> DispatchResult {
ensure_root(origin)?;
pallet_subtensor::Pallet::<T>::set_max_subnets(max_subnets);
log::debug!("MaxSubnets ( max_subnets: {:?} ) ", max_subnets);
Ok(())
}

Expand Down
2 changes: 1 addition & 1 deletion pallets/admin-utils/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ parameter_types! {
pub const InitialMaxDifficulty: u64 = u64::MAX;
pub const InitialRAORecycledForRegistration: u64 = 0;
pub const InitialSenateRequiredStakePercentage: u64 = 2; // 2 percent of total stake
pub const InitialNetworkImmunityPeriod: u64 = 7200 * 7;
pub const InitialNetworkImmunityPeriod: u64 = 1_296_000;
pub const InitialNetworkMinAllowedUids: u16 = 128;
pub const InitialNetworkMinLockCost: u64 = 100_000_000_000;
pub const InitialSubnetOwnerCut: u16 = 0; // 0%. 100% of rewards go to validators + miners.
Expand Down
206 changes: 159 additions & 47 deletions pallets/subtensor/src/coinbase/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use frame_support::storage::IterableStorageDoubleMap;
use frame_support::weights::Weight;
use safe_math::*;
use sp_core::Get;
use substrate_fixed::types::I64F64;
use substrate_fixed::types::{I64F64, U96F32};
use subtensor_runtime_common::NetUid;

impl<T: Config> Pallet<T> {
Expand Down Expand Up @@ -383,51 +383,28 @@ impl<T: Config> Pallet<T> {
/// * 'SubNetworkDoesNotExist': If the specified network does not exist.
/// * 'NotSubnetOwner': If the caller does not own the specified subnet.
///
pub fn user_remove_network(coldkey: T::AccountId, netuid: NetUid) -> dispatch::DispatchResult {
// --- 1. Ensure this subnet exists.
pub fn do_dissolve_network(netuid: NetUid) -> dispatch::DispatchResult {
// --- Perform the dtTao-compatible cleanup before removing the network.
Self::destroy_alpha_in_out_stakes(netuid)?;

// --- Finally, remove the network entirely.
ensure!(
Self::if_subnet_exist(netuid),
Error::<T>::SubNetworkDoesNotExist
);

// --- 2. Ensure the caller owns this subnet.
ensure!(
SubnetOwner::<T>::get(netuid) == coldkey,
Error::<T>::NotSubnetOwner
);

// --- 4. Remove the subnet identity if it exists.
if SubnetIdentitiesV3::<T>::take(netuid).is_some() {
Self::deposit_event(Event::SubnetIdentityRemoved(netuid));
}

// --- 5. Explicitly erase the network and all its parameters.
Self::remove_network(netuid);

// --- 6. Emit the NetworkRemoved event.
// --- Emit event.
log::debug!("NetworkRemoved( netuid:{:?} )", netuid);
Self::deposit_event(Event::NetworkRemoved(netuid));

// --- 7. Return success.
Ok(())
}

/// Removes a network (identified by netuid) and all associated parameters.
///
/// This function is responsible for cleaning up all the data associated with a network.
/// It ensures that all the storage values related to the network are removed, any
/// reserved balance is returned to the network owner, and the subnet identity is removed if it exists.
///
/// # Args:
/// * 'netuid': ('u16'): The unique identifier of the network to be removed.
///
/// # Note:
/// This function does not emit any events, nor does it raise any errors. It silently
/// returns if any internal checks fail.
pub fn remove_network(netuid: NetUid) {
// --- 1. Return balance to subnet owner.
// --- 1. Get the owner and remove from SubnetOwner.
let owner_coldkey: T::AccountId = SubnetOwner::<T>::get(netuid);
let reserved_amount: u64 = Self::get_subnet_locked_balance(netuid);
SubnetOwner::<T>::remove(netuid);

// --- 2. Remove network count.
SubnetworkN::<T>::remove(netuid);
Expand All @@ -450,28 +427,26 @@ impl<T: Config> Pallet<T> {
let _ = Keys::<T>::clear_prefix(netuid, u32::MAX, None);
let _ = Bonds::<T>::clear_prefix(netuid, u32::MAX, None);

// --- 8. Removes the weights for this subnet (do not remove).
// --- 8. Remove the weights for this subnet itself.
let _ = Weights::<T>::clear_prefix(netuid, u32::MAX, None);

// --- 9. Iterate over stored weights and fill the matrix.
// --- 9. Also zero out any weights *in the root network* that point to this netuid.
for (uid_i, weights_i) in
<Weights<T> as IterableStorageDoubleMap<NetUid, u16, Vec<(u16, u16)>>>::iter_prefix(
NetUid::ROOT,
)
{
// Create a new vector to hold modified weights.
let mut modified_weights = weights_i.clone();
// Iterate over each weight entry to potentially update it.
for (subnet_id, weight) in modified_weights.iter_mut() {
// If the root network had a weight pointing to this netuid, set it to 0
if subnet_id == &u16::from(netuid) {
// If the condition matches, modify the weight
*weight = 0; // Set weight to 0 for the matching subnet_id.
*weight = 0;
}
}
Weights::<T>::insert(NetUid::ROOT, uid_i, modified_weights);
}

// --- 10. Remove various network-related parameters.
// --- 10. Remove network-related parameters and data.
Rank::<T>::remove(netuid);
Trust::<T>::remove(netuid);
Active::<T>::remove(netuid);
Expand All @@ -487,8 +462,6 @@ impl<T: Config> Pallet<T> {
for (_uid, key) in keys {
IsNetworkMember::<T>::remove(key, netuid);
}

// --- 11. Erase network parameters.
Tempo::<T>::remove(netuid);
Kappa::<T>::remove(netuid);
Difficulty::<T>::remove(netuid);
Expand All @@ -500,17 +473,24 @@ impl<T: Config> Pallet<T> {
RegistrationsThisInterval::<T>::remove(netuid);
POWRegistrationsThisInterval::<T>::remove(netuid);
BurnRegistrationsThisInterval::<T>::remove(netuid);
SubnetTAO::<T>::remove(netuid);
SubnetAlphaInEmission::<T>::remove(netuid);
SubnetAlphaOutEmission::<T>::remove(netuid);
SubnetTaoInEmission::<T>::remove(netuid);
SubnetVolume::<T>::remove(netuid);
SubnetMovingPrice::<T>::remove(netuid);

// --- 12. 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::<T>::remove(netuid);

// --- 13. Remove subnet identity if it exists.
if SubnetIdentitiesV3::<T>::contains_key(netuid) {
SubnetIdentitiesV3::<T>::remove(netuid);
Self::deposit_event(Event::SubnetIdentityRemoved(netuid));
}

// --- Log final removal.
log::debug!(
"remove_network: netuid={}, owner={:?} removed successfully",
netuid,
owner_coldkey
);
}

#[allow(clippy::arithmetic_side_effects)]
Expand Down Expand Up @@ -614,4 +594,136 @@ impl<T: Config> Pallet<T> {
pub fn set_rate_limited_last_block(rate_limit_key: &RateLimitKey, block: u64) {
LastRateLimitedBlock::<T>::set(rate_limit_key, block);
}

pub fn destroy_alpha_in_out_stakes(netuid: NetUid) -> DispatchResult {
// 1. Ensure the subnet exists.
ensure!(
Self::if_subnet_exist(netuid),
Error::<T>::SubNetworkDoesNotExist
);

// 2. Basic info.
let owner_coldkey: T::AccountId = SubnetOwner::<T>::get(netuid);
let lock_cost: u64 = Self::get_subnet_locked_balance(netuid);

// Owner-cut already received from emissions.
let total_emission: u64 = Emission::<T>::get(netuid).iter().sum();
let owner_fraction = Self::get_float_subnet_owner_cut();
let owner_received_emission = U96F32::from_num(total_emission)
.saturating_mul(owner_fraction)
.floor()
.saturating_to_num::<u64>();

// 3. Gather α-out stakers.
let mut total_alpha_out: u128 = 0;
let mut stakers: Vec<(T::AccountId, T::AccountId, u128)> = Vec::new();

for ((hot, cold, this_netuid), alpha) in Alpha::<T>::iter() {
if this_netuid == netuid {
let a = alpha.saturating_to_num::<u128>();
total_alpha_out = total_alpha_out.saturating_add(a);
stakers.push((hot, cold, a));
}
}

// 4. Pro-rata distribution – TAO restaked to ROOT.
let subnet_tao: u128 = SubnetTAO::<T>::get(netuid) as u128;
let root_netuid = NetUid::ROOT;

if total_alpha_out > 0 && subnet_tao > 0 && !stakers.is_empty() {
struct Portion<A, C> {
hot: A,
cold: C,
share: u64,
rem: u128,
}
let mut portions: Vec<Portion<_, _>> = Vec::with_capacity(stakers.len());
let mut distributed: u128 = 0;

for (hot, cold, a) in &stakers {
let prod = subnet_tao.saturating_mul(*a);
let share_u128 = prod.checked_div(total_alpha_out).unwrap_or_default();
let share_u64 = share_u128.min(u64::MAX as u128) as u64;
distributed = distributed.saturating_add(share_u64 as u128);

let rem = prod.checked_rem(total_alpha_out).unwrap_or_default();
portions.push(Portion {
hot: hot.clone(),
cold: cold.clone(),
share: share_u64,
rem,
});
}

// Handle leftover (< stakers.len()).
let leftover = subnet_tao.saturating_sub(distributed);
if leftover > 0 {
portions.sort_by(|a, b| b.rem.cmp(&a.rem));
for p in portions.iter_mut().take(leftover as usize) {
p.share = p.share.saturating_add(1);
}
}

// Restake into root and clean α records.
for p in portions {
if p.share > 0 {
// Zero-fee restake of TAO into the root network.
Self::stake_into_subnet(&p.hot, &p.cold, root_netuid, p.share, 0u64)?;
}
Alpha::<T>::remove((&p.hot, &p.cold, netuid));
}
} else {
// No α-out or no TAO – just clear α records.
for (hot, cold, _) in &stakers {
Alpha::<T>::remove((hot.clone(), cold.clone(), netuid));
}
}

// 5. Reset α in/out counters.
SubnetAlphaIn::<T>::insert(netuid, 0);
SubnetAlphaOut::<T>::insert(netuid, 0);

// 6. Refund remaining lock to subnet owner.
let refund = lock_cost.saturating_sub(owner_received_emission);
Self::set_subnet_locked_balance(netuid, 0);
if refund > 0 {
Self::add_balance_to_coldkey_account(&owner_coldkey, refund);
}

Ok(())
}

pub fn get_network_to_prune() -> Option<NetUid> {
let current_block: u64 = Self::get_current_block_as_u64();
let total_networks: u16 = TotalNetworks::<T>::get();

let mut candidate_netuid: Option<NetUid> = None;
let mut candidate_emission = u64::MAX;
let mut candidate_timestamp = u64::MAX;

for net in 1..=total_networks {
let netuid: NetUid = net.into();
let registered_at = NetworkRegisteredAt::<T>::get(netuid);

// Skip immune networks
if current_block < registered_at.saturating_add(Self::get_network_immunity_period()) {
continue;
}

// We want total emission across all UIDs in this subnet:
let emission_vec = Emission::<T>::get(netuid);
let total_emission = emission_vec.iter().sum::<u64>();

// If tie on total_emission, earliest registration wins
if total_emission < candidate_emission
|| (total_emission == candidate_emission && registered_at < candidate_timestamp)
{
candidate_netuid = Some(netuid);
candidate_emission = total_emission;
candidate_timestamp = registered_at;
}
}

candidate_netuid
}
}
9 changes: 9 additions & 0 deletions pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,12 @@ pub mod pallet {
50400
}

#[pallet::type_value]
/// Default value for subnet limit.
pub fn DefaultSubnetLimit<T: Config>() -> u16 {
256

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

256 or 128?

}

#[pallet::storage]
pub type MinActivityCutoff<T: Config> =
StorageValue<_, u16, ValueQuery, DefaultMinActivityCutoff<T>>;
Expand Down Expand Up @@ -1027,6 +1033,9 @@ pub mod pallet {
///
/// Eventually, Bittensor should migrate to using Holds afterwhich time we will not require this
/// separate accounting.
#[pallet::storage] // --- ITEM ( maximum_number_of_networks )
pub type SubnetLimit<T> = StorageValue<_, u16, ValueQuery, DefaultSubnetLimit<T>>;
#[pallet::storage] // --- ITEM ( total_issuance )
pub type TotalIssuance<T> = StorageValue<_, u64, ValueQuery, DefaultTotalIssuance<T>>;
#[pallet::storage] // --- ITEM ( total_stake )
Expand Down
19 changes: 15 additions & 4 deletions pallets/subtensor/src/macros/dispatches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,7 @@ mod dispatches {
/// User register a new subnetwork
#[pallet::call_index(59)]
#[pallet::weight((Weight::from_parts(260_500_000, 0)
.saturating_add(T::DbWeight::get().reads(33))
.saturating_add(T::DbWeight::get().reads(34))
.saturating_add(T::DbWeight::get().writes(51)), DispatchClass::Operational, Pays::No))]
pub fn register_network(origin: OriginFor<T>, hotkey: T::AccountId) -> DispatchResult {
Self::do_register_network(origin, &hotkey, 1, None)
Expand Down Expand Up @@ -1228,11 +1228,11 @@ mod dispatches {
.saturating_add(T::DbWeight::get().writes(31)), DispatchClass::Operational, Pays::No))]
pub fn dissolve_network(
origin: OriginFor<T>,
coldkey: T::AccountId,
_coldkey: T::AccountId,
netuid: NetUid,
) -> DispatchResult {
ensure_root(origin)?;
Self::user_remove_network(coldkey, netuid)
Self::do_dissolve_network(netuid)
}

/// Set a single child for a given hotkey on a specified network.
Expand Down Expand Up @@ -1539,7 +1539,7 @@ mod dispatches {
/// User register a new subnetwork
#[pallet::call_index(79)]
#[pallet::weight((Weight::from_parts(239_700_000, 0)
.saturating_add(T::DbWeight::get().reads(32))
.saturating_add(T::DbWeight::get().reads(33))
.saturating_add(T::DbWeight::get().writes(50)), DispatchClass::Operational, Pays::No))]
pub fn register_network_with_identity(
origin: OriginFor<T>,
Expand Down Expand Up @@ -2069,5 +2069,16 @@ mod dispatches {
PendingChildKeyCooldown::<T>::put(cooldown);
Ok(())
}

/// Remove a user's subnetwork
/// The caller must be root
#[pallet::call_index(110)]
#[pallet::weight((Weight::from_parts(119_000_000, 0)
.saturating_add(T::DbWeight::get().reads(6))
.saturating_add(T::DbWeight::get().writes(31)), DispatchClass::Operational, Pays::No))]
pub fn root_dissolve_network(origin: OriginFor<T>, netuid: NetUid) -> DispatchResult {
ensure_root(origin)?;
Self::do_dissolve_network(netuid)
}
}
}
2 changes: 2 additions & 0 deletions pallets/subtensor/src/macros/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,5 +218,7 @@ mod errors {
SameNetuid,
/// The caller does not have enough balance for the operation.
InsufficientBalance,
/// Subnet limit reached & no eligible subnet to prune
SubnetLimitReached,
}
}
2 changes: 1 addition & 1 deletion pallets/subtensor/src/macros/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ mod events {
/// the network minimum locking cost is set.
NetworkMinLockCostSet(u64),
/// the maximum number of subnets is set
// SubnetLimitSet(u16),
SubnetLimitSet(u16),
/// the lock cost reduction is set
NetworkLockCostReductionIntervalSet(u64),
/// the take for a delegate is decreased.
Expand Down
Loading
Loading