diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 4688b1f22f..ae63af8f11 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -110,6 +110,14 @@ pub mod pallet { /// The new burn increase multiplier. burn_increase_mult: U64F64, }, + + /// Pool-side subnet emission injections and chain buys were enabled or disabled. + SubnetEmissionEnabledSet { + /// The network identifier. + netuid: NetUid, + /// Whether pool-side emission injections and chain buys are enabled. + enabled: bool, + }, } // Errors inform users that something went wrong. @@ -2141,6 +2149,51 @@ pub mod pallet { Ok(()) } + + /// Enables or disables subnet pool-side emission for a subnet. + /// + /// This does not remove the subnet from emission share calculation and does not + /// change `alpha_out`, owner cut, root proportion, pending server emission, or + /// pending validator emission. It only zeros the pool-side `alpha_in`, `tao_in`, + /// and `excess_tao` chain-buy paths. + #[pallet::call_index(92)] + #[pallet::weight(( + Weight::from_parts(25_000_000, 0) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)), + DispatchClass::Operational, + Pays::Yes, + ))] + pub fn sudo_set_subnet_emission_enabled( + origin: OriginFor, + netuid: NetUid, + enabled: bool, + ) -> DispatchResult { + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[Hyperparameter::SubnetEmissionEnabled.into()], + )?; + pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; + + ensure!( + pallet_subtensor::Pallet::::if_subnet_exist(netuid), + Error::::SubnetDoesNotExist + ); + ensure!(!netuid.is_root(), Error::::NotPermittedOnRootSubnet); + + pallet_subtensor::SubnetEmissionEnabled::::insert(netuid, enabled); + Self::deposit_event(Event::SubnetEmissionEnabledSet { netuid, enabled }); + log::debug!("SubnetEmissionEnabledSet( netuid: {netuid:?}, enabled: {enabled:?} )"); + + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[Hyperparameter::SubnetEmissionEnabled.into()], + ); + + Ok(()) + } } } diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 2854777abc..922b3f5592 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -85,6 +85,11 @@ impl Pallet { let tao_to_swap_with: TaoBalance = tou64!(excess_tao.get(netuid_i).unwrap_or(&asfloat!(0))).into(); + // Clear per-block pool-side emission counters up front so a subnet + // disabled this block does not display stale values from an earlier block. + SubnetExcessTao::::insert(*netuid_i, TaoBalance::ZERO); + SubnetTaoInEmission::::insert(*netuid_i, TaoBalance::ZERO); + T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i); if tao_to_swap_with > TaoBalance::ZERO { diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 485e2cf662..c8ba872e38 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -22,13 +22,33 @@ impl Pallet { subnets_to_emit_to: &[NetUid], block_emission: U96F32, ) -> BTreeMap { - // Get subnet TAO emissions. + // Get subnet TAO emission shares for all emission-eligible subnets first. This keeps + // disabled subnets in the returned map so they still continue through alpha_out/root-prop + // accounting, while their TAO-side emission is redistributed to enabled subnets. let shares = Self::get_shares(subnets_to_emit_to); log::debug!("Subnet emission shares = {shares:?}"); + let zero = U64F64::saturating_from_num(0.0); + let has_disabled_subnets = shares + .keys() + .any(|netuid| !SubnetEmissionEnabled::::get(*netuid)); + let enabled_share_sum: U64F64 = shares + .iter() + .filter(|(netuid, _)| SubnetEmissionEnabled::::get(**netuid)) + .fold(zero, |acc, (_, share)| acc.saturating_add(*share)); + shares .into_iter() .map(|(netuid, share)| { + let share = if has_disabled_subnets { + if SubnetEmissionEnabled::::get(netuid) && enabled_share_sum > zero { + share.safe_div(enabled_share_sum) + } else { + zero + } + } else { + share + }; let emission = U64F64::saturating_from_num(block_emission).saturating_mul(share); (netuid, U96F32::saturating_from_num(emission)) }) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 75735c7471..98ee408fec 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1334,6 +1334,18 @@ pub mod pallet { pub type SubnetAlphaInEmission = StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha>; + /// --- MAP ( netuid ) --> subnet_emission_enabled + /// + /// When false, subnet pool-side emission is disabled for this subnet: + /// `alpha_in`, `tao_in`, and `excess_tao` chain buys are all treated as zero. + /// `alpha_out`, owner cut, root proportion, pending server emission, and pending + /// validator emission are intentionally left unchanged. + /// + /// Defaults to true so existing subnets keep current behavior. + #[pallet::storage] + pub type SubnetEmissionEnabled = + StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultTrue>; + /// --- MAP ( netuid ) --> alpha_out_emission | Returns the amount of alpha out emission into the network per block. #[pallet::storage] pub type SubnetAlphaOutEmission = diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index e1aa5eb744..1c5422c964 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -302,6 +302,10 @@ impl Pallet { Self::set_yuma3_enabled(netuid, true); Self::set_burn(netuid, DefaultNeuronBurnCost::::get()); + // New subnets should never inherit a prior subnet owner's disabled state + // when a netuid is reused after pruning/dissolve. + SubnetEmissionEnabled::::insert(netuid, true); + // Make network parameters explicit. if !Tempo::::contains_key(netuid) { Tempo::::insert(netuid, Tempo::::get(netuid)); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 6199aa9952..0039889cab 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -174,6 +174,169 @@ fn test_coinbase_tao_issuance_multiple() { }); } +#[test] +fn test_coinbase_disabled_subnet_emission_redistributes_tao_to_enabled_subnets() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let netuid3 = NetUid::from(3); + let emission = TaoBalance::from(3_333_333); + + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + add_network(netuid3, 1, 0); + + SubnetEmissionEnabled::::insert(netuid2, false); + + SubnetTaoFlow::::insert(netuid1, 100_000_000_i64); + SubnetTaoFlow::::insert(netuid2, 100_000_000_i64); + SubnetTaoFlow::::insert(netuid3, 100_000_000_i64); + + let subnet_emissions = SubtensorModule::get_subnet_block_emissions( + &[netuid1, netuid2, netuid3], + U96F32::saturating_from_num(emission.to_u64()), + ); + + assert_abs_diff_eq!( + subnet_emissions[&netuid1].to_num::(), + (emission.to_u64() / 2) as f64, + epsilon = 2.0, + ); + assert_abs_diff_eq!( + subnet_emissions[&netuid2].to_num::(), + 0.0, + epsilon = 1.0 + ); + assert_abs_diff_eq!( + subnet_emissions[&netuid3].to_num::(), + (emission.to_u64() / 2) as f64, + epsilon = 2.0, + ); + + let (_tao_in, alpha_in, alpha_out, excess_tao) = + SubtensorModule::get_subnet_terms(&subnet_emissions); + assert_eq!(alpha_in[&netuid2], U96F32::from_num(0.0)); + assert_eq!(excess_tao[&netuid2], U96F32::from_num(0.0)); + assert!(alpha_out[&netuid2] > U96F32::from_num(0.0)); + + let total_issuance_before = TotalIssuance::::get(); + let total_stake_before = TotalStake::::get(); + let emission_credit = SubtensorModule::mint_tao(emission); + SubtensorModule::run_coinbase(emission_credit); + + assert_abs_diff_eq!( + SubnetTAO::::get(netuid1), + emission / 2.into(), + epsilon = 2.into(), + ); + assert_eq!(SubnetTAO::::get(netuid2), TaoBalance::ZERO); + assert_abs_diff_eq!( + SubnetTAO::::get(netuid3), + emission / 2.into(), + epsilon = 2.into(), + ); + assert_abs_diff_eq!( + TotalIssuance::::get(), + total_issuance_before + emission, + epsilon = 2.into(), + ); + assert_abs_diff_eq!( + TotalStake::::get(), + total_stake_before + emission, + epsilon = 2.into(), + ); + }); +} + +#[test] +fn test_sudo_set_subnet_emission_enabled_multiple_subnets_multiple_toggles() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let netuid3 = NetUid::from(3); + let emission = TaoBalance::from(3_000_000); + + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + add_network(netuid3, 1, 0); + + SubnetTaoFlow::::insert(netuid1, 100_000_000_i64); + SubnetTaoFlow::::insert(netuid2, 100_000_000_i64); + SubnetTaoFlow::::insert(netuid3, 100_000_000_i64); + + let assert_emission_storage = |expected1: u64, expected2: u64, expected3: u64| { + assert_abs_diff_eq!( + SubnetTaoInEmission::::get(netuid1), + TaoBalance::from(expected1), + epsilon = 2.into(), + ); + assert_abs_diff_eq!( + SubnetTaoInEmission::::get(netuid2), + TaoBalance::from(expected2), + epsilon = 2.into(), + ); + assert_abs_diff_eq!( + SubnetTaoInEmission::::get(netuid3), + TaoBalance::from(expected3), + epsilon = 2.into(), + ); + + assert_eq!( + SubnetAlphaInEmission::::get(netuid1) == AlphaBalance::from(0), + expected1 == 0 + ); + assert_eq!( + SubnetAlphaInEmission::::get(netuid2) == AlphaBalance::from(0), + expected2 == 0 + ); + assert_eq!( + SubnetAlphaInEmission::::get(netuid3) == AlphaBalance::from(0), + expected3 == 0 + ); + + assert!(SubnetAlphaOutEmission::::get(netuid1) > AlphaBalance::from(0)); + assert!(SubnetAlphaOutEmission::::get(netuid2) > AlphaBalance::from(0)); + assert!(SubnetAlphaOutEmission::::get(netuid3) > AlphaBalance::from(0)); + }; + + let run_coinbase = || { + let emission_credit = SubtensorModule::mint_tao(emission); + SubtensorModule::run_coinbase(emission_credit); + }; + + // All enabled: split TAO-side emission equally across all three subnets. + run_coinbase(); + assert_emission_storage(1_000_000, 1_000_000, 1_000_000); + + // Seed stale values and then disable netuid2. The next coinbase run must clear + // netuid2's per-block TAO-side emission storage while preserving alpha_out. + SubnetTaoInEmission::::insert(netuid2, TaoBalance::from(123)); + SubnetAlphaInEmission::::insert(netuid2, AlphaBalance::from(123)); + SubnetExcessTao::::insert(netuid2, TaoBalance::from(123)); + SubnetEmissionEnabled::::insert(netuid2, false); + run_coinbase(); + assert_emission_storage(1_500_000, 0, 1_500_000); + assert_eq!(SubnetExcessTao::::get(netuid2), TaoBalance::from(0)); + + // Toggle a different subnet off and netuid2 back on. + SubnetTaoInEmission::::insert(netuid1, TaoBalance::from(456)); + SubnetAlphaInEmission::::insert(netuid1, AlphaBalance::from(456)); + SubnetExcessTao::::insert(netuid1, TaoBalance::from(456)); + SubnetEmissionEnabled::::insert(netuid1, false); + SubnetEmissionEnabled::::insert(netuid2, true); + run_coinbase(); + assert_emission_storage(0, 1_500_000, 1_500_000); + assert_eq!(SubnetExcessTao::::get(netuid1), TaoBalance::from(0)); + + // Toggle everything back on: TAO-side emission should return to an even split. + SubnetEmissionEnabled::::insert(netuid1, true); + SubnetEmissionEnabled::::insert(netuid2, true); + SubnetEmissionEnabled::::insert(netuid3, true); + run_coinbase(); + assert_emission_storage(1_000_000, 1_000_000, 1_000_000); + }); +} + // Test emission distribution with different subnet prices. // This test verifies that: // - Subnets with different prices receive proportional emission shares diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index f0c9243aa8..602600e6cc 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -204,6 +204,7 @@ pub enum Hyperparameter { MaxAllowedUids = 25, BurnHalfLife = 26, BurnIncreaseMult = 27, + SubnetEmissionEnabled = 28, } impl Pallet { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 00d3839fa7..15d8a43bef 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -272,7 +272,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: 406, + spec_version: 407, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -812,6 +812,9 @@ impl InstanceFilter for ProxyType { | RuntimeCall::AdminUtils( pallet_admin_utils::Call::sudo_set_toggle_transfer { .. } ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_subnet_emission_enabled { .. } + ) ), ProxyType::RootClaim => matches!( c,