From 28730589f40741873f69eaee83c960b824b03407 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 13 Mar 2026 15:35:14 -0400 Subject: [PATCH 1/2] Only top subnets receive emissions --- pallets/admin-utils/src/lib.rs | 13 +++++ .../src/coinbase/subnet_emissions.rs | 56 ++++++++++++++++++- pallets/subtensor/src/lib.rs | 10 ++++ .../subtensor/src/tests/subnet_emissions.rs | 52 +++++++++++++++++ 4 files changed, 128 insertions(+), 3 deletions(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 7f882fcf9c..d96a84b7c9 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -2134,6 +2134,19 @@ pub mod pallet { log::trace!("ColdkeySwapReannouncementDelaySet( duration: {duration:?} )"); Ok(()) } + + /// Set the number of top subnets that will receive emission + /// If the subnet emission is not within this number, it will receive no emission + #[pallet::call_index(89)] + #[pallet::weight(Weight::from_parts(5_420_000, 0) + .saturating_add(::DbWeight::get().reads(0_u64)) + .saturating_add(::DbWeight::get().writes(1_u64)))] + pub fn sudo_set_subnet_emission_cap(origin: OriginFor, cap: u16) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_subnet_emission_cap(cap); + log::debug!("SubnetEmissionCap set to {}", cap); + Ok(()) + } } } diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 17a40e820b..9d6cbc0122 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -212,10 +212,56 @@ impl Pallet { offset_flows } - // Combines ema price method and tao flow method linearly over FlowHalfLife blocks + // Return emission shares pub(crate) fn get_shares(subnets_to_emit_to: &[NetUid]) -> BTreeMap { - Self::get_shares_flow(subnets_to_emit_to) - // Self::get_shares_price_ema(subnets_to_emit_to) + let topk = SubnetEmissionCap::::get() as usize; + let shares = Self::get_shares_flow(subnets_to_emit_to); + + // Start with all requested subnets present in the output and assigned + // a zero share. + // + // This guarantees that: + // 1. every input NetUid is present in the returned map + // 2. non-selected subnets remain at zero + // 3. if normalization is impossible, all shares stay at zero + let mut normalized: BTreeMap = subnets_to_emit_to + .iter() + .map(|netuid| (*netuid, U64F64::from_num(0))) + .collect(); + + // If there is no capacity or no computed shares, return the all-zero map. + if topk == 0 || shares.is_empty() { + return normalized; + } + + // Collect into a vector so we can sort by share descending. + let mut top_shares: Vec<(NetUid, U64F64)> = shares.into_iter().collect(); + + // Sort by: + // 1. larger share first + // 2. smaller NetUid first as a deterministic tie-breaker + top_shares.sort_unstable_by(|(netuid_a, share_a), (netuid_b, share_b)| { + share_b.cmp(share_a).then_with(|| netuid_a.cmp(netuid_b)) + }); + + // Keep only the top-k shares. If topk is larger than the number of shares, + // all shares are kept. + top_shares.truncate(topk); + + // Sum the selected shares so we can re-normalize them to add up to 1.0. + let total_selected_share: U64F64 = top_shares.iter().map(|(_, share)| *share).sum(); + + // If normalization is possible, write normalized values for the selected + // top-k subnets. All other subnets remain at zero. + // + // If normalization is not possible (sum == 0), return the all-zero map. + if total_selected_share != U64F64::from_num(0) { + for (netuid, share) in top_shares { + normalized.insert(netuid, share.safe_div(total_selected_share)); + } + } + + normalized } // DEPRECATED: Implementation of shares that uses EMA prices will be gradually deprecated @@ -246,4 +292,8 @@ impl Pallet { }) .collect::>() } + + pub fn set_subnet_emission_cap(cap: u16) { + SubnetEmissionCap::::set(cap); + } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index f27019989a..88c926d5aa 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -678,6 +678,12 @@ pub mod pallet { 0.into() } + /// Default maximum childkey take. + #[pallet::type_value] + pub fn DefaultSubnetEmissionCap() -> u16 { + 256_u16 + } + /// Default value for blocks since last step. #[pallet::type_value] pub fn DefaultBlocksSinceLastStep() -> u64 { @@ -1634,6 +1640,10 @@ pub mod pallet { pub type PendingServerEmission = StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha>; + /// --- ITEM ( subnet_emission_cap ) + #[pallet::storage] + pub type SubnetEmissionCap = StorageValue<_, u16, ValueQuery, DefaultSubnetEmissionCap>; + /// --- MAP ( netuid ) --> pending_validator_emission #[pallet::storage] pub type PendingValidatorEmission = diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs index 311a930647..ed532f699e 100644 --- a/pallets/subtensor/src/tests/subnet_emissions.rs +++ b/pallets/subtensor/src/tests/subnet_emissions.rs @@ -488,3 +488,55 @@ fn seed_price_and_flow(n1: NetUid, n2: NetUid, price1: f64, price2: f64, flow1: // assert_abs_diff_eq!(s1 + s2 + s3, 1.0, epsilon = 1e-9); // }); // } + +// cargo test --package pallet-subtensor --lib -- tests::subnet_emissions::get_shares_respects_subnet_emission_cap_and_keeps_zero_entries --exact --nocapture +#[test] +fn get_shares_respects_subnet_emission_cap_and_keeps_zero_entries() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(60); + let owner_coldkey = U256::from(61); + + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n3 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Keep the math simple and deterministic. + FlowNormExponent::::set(u64f64(1.0)); + let block_num = FlowHalfLife::::get(); + System::set_block_number(block_num); + + // Only the top 2 subnets should receive non-zero emission. + SubnetEmissionCap::::set(2); + + // Neutral prices so ordering comes from flows. + SubnetMovingPrice::::insert(n1, i96f32(1.0)); + SubnetMovingPrice::::insert(n2, i96f32(1.0)); + SubnetMovingPrice::::insert(n3, i96f32(1.0)); + + // Positive, strictly ordered flows. + SubnetEmaTaoFlow::::insert(n1, (block_num, i64f64(1_000.0))); + SubnetEmaTaoFlow::::insert(n2, (block_num, i64f64(3_000.0))); + SubnetEmaTaoFlow::::insert(n3, (block_num, i64f64(6_000.0))); + + let shares = SubtensorModule::get_shares(&[n1, n2, n3]); + + // All requested subnets must be present in the returned map. + assert_eq!(shares.len(), 3); + assert!(shares.contains_key(&n1)); + assert!(shares.contains_key(&n2)); + assert!(shares.contains_key(&n3)); + + let s1 = shares.get(&n1).unwrap().to_num::(); + let s2 = shares.get(&n2).unwrap().to_num::(); + let s3 = shares.get(&n3).unwrap().to_num::(); + + // The lowest-share subnet should be kept in the map but receive zero. + assert_abs_diff_eq!(s1, 0.0, epsilon = 1e-18); + + // The top-2 subnets should be re-normalized to sum to 1. + assert!(s2 > 0.0, "expected n2 to receive non-zero share, got {s2}"); + assert!(s3 > 0.0, "expected n3 to receive non-zero share, got {s3}"); + assert!(s3 > s2, "expected n3 > n2; got s2={s2}, s3={s3}"); + assert_abs_diff_eq!(s1 + s2 + s3, 1.0_f64, epsilon = 1e-9); + }); +} From 21799012e5d08c43731863e72e671b6a3e7b427c Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 13 Mar 2026 15:41:05 -0400 Subject: [PATCH 2/2] Update comment --- pallets/admin-utils/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index d96a84b7c9..3307ad761b 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -2136,7 +2136,8 @@ pub mod pallet { } /// Set the number of top subnets that will receive emission - /// If the subnet emission is not within this number, it will receive no emission + /// If the subnet is not within this number in the list of subnets sorted by emission, + /// it will receive no emission #[pallet::call_index(89)] #[pallet::weight(Weight::from_parts(5_420_000, 0) .saturating_add(::DbWeight::get().reads(0_u64))