From 02e400b9746b79a0e09c9b92548140c1fefcf1ea Mon Sep 17 00:00:00 2001
From: Pawel Polewicz
Date: Sat, 31 Jan 2026 08:54:33 +0000
Subject: [PATCH 01/29] feat: add EffectiveRootProp storage and computation
Add EffectiveRootProp StorageMap (NetUid -> U64F64) computed during
distribute_dividends_and_incentives() as:
sum(RootAlphaDividendsPerSubnet[netuid]) /
(sum(AlphaDividendsPerSubnet[netuid]) + sum(RootAlphaDividendsPerSubnet[netuid]))
This measures the proportion of dividends on a subnet flowing to root
stakers, stored per-subnet for efficient single-read access during
emission preparation.
Co-Authored-By: Claude Opus 4.5
---
PLAN.md | 344 ++++++++++++++++++
.../subtensor/src/coinbase/run_coinbase.rs | 44 ++-
pallets/subtensor/src/lib.rs | 8 +
.../subtensor/src/tests/subnet_emissions.rs | 126 +++++++
4 files changed, 521 insertions(+), 1 deletion(-)
create mode 100644 PLAN.md
diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000000..8c6a0704b4
--- /dev/null
+++ b/PLAN.md
@@ -0,0 +1,344 @@
+# Implementation Plan: Emission Scaling and Subnet Limiting Hyperparameters
+
+## Overview
+
+Three new root-level hyperparameters for `get_subnet_block_emissions()` in `src/coinbase/subnet_emissions.rs`:
+
+1. **EffectiveRootPropEmissionScaling** (bool) - When enabled, multiply each subnet's emission share by its `EffectiveRootProp`
+2. **EmissionTopSubnetProportion** (u16, default 50% = 5000/10000) - Only top-K% of subnets by share receive emission
+3. **EmissionTopSubnetAbsoluteLimit** (u16, default 0 = disabled) - Hard cap on number of subnets receiving emission
+
+## Subfeature 1: EffectiveRootProp Storage & Computation
+
+### 1A. Storage declarations in `pallets/subtensor/src/lib.rs`
+
+Add near the existing flow-related storage items (~line 1489):
+
+```rust
+/// --- MAP ( netuid ) --> EffectiveRootProp for a subnet.
+/// Computed during epoch as:
+/// sum(RootAlphaDividendsPerSubnet[netuid]) /
+/// (sum(AlphaDividendsPerSubnet[netuid]) + sum(RootAlphaDividendsPerSubnet[netuid]))
+/// This measures the proportion of dividends on a subnet that go to root stakers.
+#[pallet::storage]
+pub type EffectiveRootProp =
+ StorageMap<_, Identity, NetUid, U64F64, ValueQuery>;
+```
+
+### 1B. Compute and store EffectiveRootProp during epoch
+
+In `pallets/subtensor/src/coinbase/run_coinbase.rs`, inside `distribute_dividends_and_incentives()` (after the alpha dividends and root alpha dividends loops at ~line 638), add computation:
+
+```rust
+// After distributing both alpha_divs and root_alpha_divs, compute EffectiveRootProp
+let total_root_alpha_divs: U64F64 = /* sum of root_alpha values from root_alpha_dividends map */;
+let total_alpha_divs: U64F64 = /* sum of alpha_divs values from alpha_dividends map */;
+let total = total_root_alpha_divs + total_alpha_divs;
+let effective_root_prop = if total > 0 { total_root_alpha_divs / total } else { 0 };
+EffectiveRootProp::::insert(netuid, effective_root_prop);
+```
+
+Create a helper function `compute_and_store_effective_root_prop()` that takes netuid and the two dividend maps, computes the ratio, stores it, and returns the value. This keeps `distribute_dividends_and_incentives` clean.
+
+### 1C. Tests for EffectiveRootProp computation
+
+In `pallets/subtensor/src/tests/subnet_emissions.rs`:
+- Test that `EffectiveRootProp` is 0.0 when there are no root alpha dividends
+- Test that `EffectiveRootProp` is 1.0 when there are no alpha dividends (all root)
+- Test that `EffectiveRootProp` is ~0.5 when root and alpha dividends are equal
+- Test that correct values are stored per-subnet after `distribute_dividends_and_incentives` runs
+
+## Subfeature 2: EffectiveRootPropEmissionScaling Hyperparameter
+
+### 2A. Storage declaration in `pallets/subtensor/src/lib.rs`
+
+Add near the flow-related storage items:
+
+```rust
+#[pallet::type_value]
+pub fn DefaultEffectiveRootPropEmissionScaling() -> bool {
+ false
+}
+#[pallet::storage]
+/// When enabled, multiply each subnet's emission share by its EffectiveRootProp
+pub type EffectiveRootPropEmissionScaling =
+ StorageValue<_, bool, ValueQuery, DefaultEffectiveRootPropEmissionScaling>;
+```
+
+### 2B. Setter in `pallets/subtensor/src/utils/misc.rs`
+
+Add setter function following the pattern of `set_tao_flow_cutoff`:
+```rust
+pub fn set_effective_root_prop_emission_scaling(enabled: bool) {
+ EffectiveRootPropEmissionScaling::::set(enabled);
+}
+```
+
+### 2C. Admin extrinsic in `pallets/admin-utils/src/lib.rs`
+
+Add `sudo_set_effective_root_prop_emission_scaling` with next available call_index (88):
+```rust
+#[pallet::call_index(88)]
+pub fn sudo_set_effective_root_prop_emission_scaling(
+ origin: OriginFor,
+ enabled: bool,
+) -> DispatchResult {
+ ensure_root(origin)?;
+ pallet_subtensor::Pallet::::set_effective_root_prop_emission_scaling(enabled);
+ Ok(())
+}
+```
+
+### 2D. Apply scaling in `get_subnet_block_emissions()`
+
+In `pallets/subtensor/src/coinbase/subnet_emissions.rs`, modify `get_subnet_block_emissions()`:
+
+After `get_shares()` returns shares, add a step:
+1. If `EffectiveRootPropEmissionScaling` is enabled, multiply each share by `EffectiveRootProp::::get(netuid)`
+2. Re-normalize shares so they sum to 1.0
+
+Extract this into a helper function `apply_effective_root_prop_scaling(shares: &mut BTreeMap)`.
+
+### 2E. Event for scaling application
+
+Add an event in `pallets/subtensor/src/macros/events.rs`:
+```rust
+/// Emission shares have been adjusted by EffectiveRootProp scaling.
+EffectiveRootPropEmissionScalingApplied {
+ /// Per-subnet shares after scaling (netuid, share)
+ shares: Vec<(NetUid, u64)>,
+},
+```
+
+Emit this event after applying the scaling so tests can verify it.
+
+### 2F. Tests
+
+- Test that with scaling disabled, shares are unchanged
+- Test that with scaling enabled and known EffectiveRootProp values, shares are correctly multiplied and re-normalized
+- Test that event is emitted when scaling is applied
+- Test edge case: all EffectiveRootProp values are 0 (should result in equal shares or all zeros)
+- Test edge case: single subnet
+
+## Subfeature 3: EmissionTopSubnetProportion
+
+### 3A. Storage declaration in `pallets/subtensor/src/lib.rs`
+
+```rust
+#[pallet::type_value]
+pub fn DefaultEmissionTopSubnetProportion() -> u16 {
+ 5000 // 50% = 5000/10000
+}
+#[pallet::storage]
+/// Proportion of subnets (by count, ranked by share) that receive emission.
+/// Value is in basis points: 5000 = 50%. Subnets outside top-K% get shares zeroed.
+/// Round up: ceil(count * proportion / 10000).
+pub type EmissionTopSubnetProportion =
+ StorageValue<_, u16, ValueQuery, DefaultEmissionTopSubnetProportion>;
+```
+
+### 3B. Setter in `pallets/subtensor/src/utils/misc.rs`
+
+```rust
+pub fn set_emission_top_subnet_proportion(proportion: u16) {
+ EmissionTopSubnetProportion::::set(proportion);
+}
+```
+
+### 3C. Admin extrinsic in `pallets/admin-utils/src/lib.rs`
+
+Add `sudo_set_emission_top_subnet_proportion` with call_index 89:
+```rust
+#[pallet::call_index(89)]
+pub fn sudo_set_emission_top_subnet_proportion(
+ origin: OriginFor,
+ proportion: u16,
+) -> DispatchResult {
+ ensure_root(origin)?;
+ ensure!(proportion <= 10000, Error::::InvalidValue);
+ ensure!(proportion > 0, Error::::InvalidValue);
+ pallet_subtensor::Pallet::::set_emission_top_subnet_proportion(proportion);
+ Ok(())
+}
+```
+
+### 3D. Apply in `get_subnet_block_emissions()`
+
+After getting shares (and after EffectiveRootProp scaling if enabled):
+
+1. Sort subnets by share descending
+2. Compute K = ceil(total_subnets * proportion / 10000)
+3. Zero out shares for subnets not in top-K
+4. Re-normalize remaining shares to sum to 1.0
+
+Extract into helper function `apply_top_subnet_proportion_filter(shares: &mut BTreeMap)`.
+
+### 3E. Event
+
+```rust
+/// Subnet emission shares zeroed for subnets outside top proportion.
+EmissionTopSubnetFilterApplied {
+ /// Number of subnets that kept emission
+ top_k: u16,
+ /// Total number of subnets considered
+ total: u16,
+},
+```
+
+### 3F. Tests
+
+- Test default 50%: 4 subnets -> top 2 get emission (ceil(4*0.5)=2)
+- Test default 50%: 1 subnet -> still gets emission (ceil(1*0.5)=1)
+- Test default 50%: 3 subnets -> top 2 get emission (ceil(3*0.5)=2)
+- Test 100%: all subnets get emission
+- Test that shares are re-normalized after filtering
+- Test that event is emitted with correct top_k and total
+- Test that zeroed subnets get zero emission in final output
+
+## Subfeature 4: EmissionTopSubnetAbsoluteLimit
+
+### 4A. Storage declaration in `pallets/subtensor/src/lib.rs`
+
+```rust
+#[pallet::type_value]
+pub fn DefaultEmissionTopSubnetAbsoluteLimit() -> u16 {
+ 0 // 0 means no limit
+}
+#[pallet::storage]
+/// Absolute maximum number of subnets that can receive emission.
+/// 0 means no limit (disabled). When set, only the top N subnets by share receive emission.
+pub type EmissionTopSubnetAbsoluteLimit =
+ StorageValue<_, u16, ValueQuery, DefaultEmissionTopSubnetAbsoluteLimit>;
+```
+
+### 4B. Setter in `pallets/subtensor/src/utils/misc.rs`
+
+```rust
+pub fn set_emission_top_subnet_absolute_limit(limit: u16) {
+ EmissionTopSubnetAbsoluteLimit::::set(limit);
+}
+```
+
+### 4C. Admin extrinsic in `pallets/admin-utils/src/lib.rs`
+
+Add `sudo_set_emission_top_subnet_absolute_limit` with call_index 90:
+```rust
+#[pallet::call_index(90)]
+pub fn sudo_set_emission_top_subnet_absolute_limit(
+ origin: OriginFor,
+ limit: u16,
+) -> DispatchResult {
+ ensure_root(origin)?;
+ pallet_subtensor::Pallet::::set_emission_top_subnet_absolute_limit(limit);
+ Ok(())
+}
+```
+
+### 4D. Apply in `get_subnet_block_emissions()`
+
+After proportion filter, apply absolute limit:
+
+1. If limit > 0 and limit < number of remaining nonzero subnets:
+ - Sort by share descending
+ - Zero out shares beyond position `limit`
+ - Re-normalize
+
+Extract into helper `apply_top_subnet_absolute_limit(shares: &mut BTreeMap)`.
+
+### 4E. Event
+
+```rust
+/// Subnet emission shares zeroed for subnets beyond absolute limit.
+EmissionAbsoluteLimitApplied {
+ /// The absolute limit applied
+ limit: u16,
+ /// Number of subnets that had nonzero shares before limiting
+ before_count: u16,
+},
+```
+
+### 4F. Tests
+
+- Test with limit=0: no filtering occurs
+- Test with limit=2 and 5 subnets: only top 2 get emission
+- Test with limit=10 and 3 subnets: all 3 get emission (limit > count)
+- Test interaction with proportion filter: both applied, stricter one wins
+- Test event emission
+
+## Execution Order in `get_subnet_block_emissions()`
+
+The final `get_subnet_block_emissions()` function should:
+
+```
+1. let shares = get_shares(subnets)
+2. apply_effective_root_prop_scaling(&mut shares) // if enabled
+3. apply_top_subnet_proportion_filter(&mut shares)
+4. apply_top_subnet_absolute_limit(&mut shares)
+5. convert shares to emissions
+```
+
+Each step is a separate function that is easy to test independently.
+
+## Shared helper: normalize_shares()
+
+Create a utility `normalize_shares(shares: &mut BTreeMap)` that normalizes values to sum to 1.0. Used by multiple scaling/filtering steps.
+
+## Shared helper: zero_and_redistribute_bottom_shares()
+
+Create `zero_and_redistribute_bottom_shares(shares: &mut BTreeMap, top_k: usize)` that:
+1. Sorts entries by value descending
+2. Zeros out entries beyond top_k
+3. Calls `normalize_shares`
+
+This is reused by both the proportion filter and the absolute limit.
+
+## Implementation Order (4 commits)
+
+### Commit 1: EffectiveRootProp storage + computation
+- Add `EffectiveRootProp` storage map to `lib.rs`
+- Add `compute_and_store_effective_root_prop()` helper in `run_coinbase.rs`
+- Call it from `distribute_dividends_and_incentives()`
+- Add tests for EffectiveRootProp computation
+- Run `scripts/fix_rust.sh`
+
+### Commit 2: EffectiveRootPropEmissionScaling hyperparameter
+- Add `EffectiveRootPropEmissionScaling` storage to `lib.rs`
+- Add setter in `utils/misc.rs`
+- Add admin extrinsic in `admin-utils/src/lib.rs`
+- Add `normalize_shares()` and `apply_effective_root_prop_scaling()` to `subnet_emissions.rs`
+- Add event + emit it
+- Wire into `get_subnet_block_emissions()`
+- Add tests
+- Run `scripts/fix_rust.sh`
+
+### Commit 3: EmissionTopSubnetProportion hyperparameter
+- Add `EmissionTopSubnetProportion` storage to `lib.rs`
+- Add setter in `utils/misc.rs`
+- Add admin extrinsic in `admin-utils/src/lib.rs`
+- Add `zero_and_redistribute_bottom_shares()` and `apply_top_subnet_proportion_filter()` to `subnet_emissions.rs`
+- Add event + emit it
+- Wire into `get_subnet_block_emissions()`
+- Add tests
+- Run `scripts/fix_rust.sh`
+
+### Commit 4: EmissionTopSubnetAbsoluteLimit hyperparameter
+- Add `EmissionTopSubnetAbsoluteLimit` storage to `lib.rs`
+- Add setter in `utils/misc.rs`
+- Add admin extrinsic in `admin-utils/src/lib.rs`
+- Add `apply_top_subnet_absolute_limit()` to `subnet_emissions.rs`
+- Add event + emit it
+- Wire into `get_subnet_block_emissions()`
+- Add tests (including interaction with proportion filter)
+- Run `scripts/fix_rust.sh`
+
+## Files Modified
+
+| File | Changes |
+|------|---------|
+| `pallets/subtensor/src/lib.rs` | 4 new storage items + 3 type_value defaults |
+| `pallets/subtensor/src/coinbase/run_coinbase.rs` | `compute_and_store_effective_root_prop()` helper + call site |
+| `pallets/subtensor/src/coinbase/subnet_emissions.rs` | `normalize_shares()`, `apply_effective_root_prop_scaling()`, `zero_and_redistribute_bottom_shares()`, `apply_top_subnet_proportion_filter()`, `apply_top_subnet_absolute_limit()`, modified `get_subnet_block_emissions()` |
+| `pallets/subtensor/src/utils/misc.rs` | 3 setter functions |
+| `pallets/admin-utils/src/lib.rs` | 3 new extrinsics (call_index 88, 89, 90) |
+| `pallets/subtensor/src/macros/events.rs` | 3 new events |
+| `pallets/subtensor/src/tests/subnet_emissions.rs` | ~15 new tests |
diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs
index 2091946598..de43fb89f2 100644
--- a/pallets/subtensor/src/coinbase/run_coinbase.rs
+++ b/pallets/subtensor/src/coinbase/run_coinbase.rs
@@ -1,7 +1,7 @@
use super::*;
use alloc::collections::BTreeMap;
use safe_math::*;
-use substrate_fixed::types::U96F32;
+use substrate_fixed::types::{U64F64, U96F32};
use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency};
use subtensor_swap_interface::SwapHandler;
@@ -508,6 +508,13 @@ impl Pallet {
alpha_dividends: BTreeMap,
root_alpha_dividends: BTreeMap,
) {
+ // Compute and store EffectiveRootProp before distributing (uses raw dividend values).
+ Self::compute_and_store_effective_root_prop(
+ netuid,
+ &alpha_dividends,
+ &root_alpha_dividends,
+ );
+
// Distribute the owner cut.
if let Ok(owner_coldkey) = SubnetOwner::::try_get(netuid)
&& let Ok(owner_hotkey) = SubnetOwnerHotkey::::try_get(netuid)
@@ -638,6 +645,41 @@ impl Pallet {
}
}
+ /// Computes and stores the EffectiveRootProp for a subnet.
+ ///
+ /// EffectiveRootProp = sum(root_alpha_dividends) / (sum(alpha_dividends) + sum(root_alpha_dividends))
+ ///
+ /// This represents the proportion of total dividends on this subnet that flow to root stakers.
+ pub fn compute_and_store_effective_root_prop(
+ netuid: NetUid,
+ alpha_dividends: &BTreeMap,
+ root_alpha_dividends: &BTreeMap,
+ ) {
+ let zero = U96F32::saturating_from_num(0);
+
+ let total_alpha_divs: U96F32 = alpha_dividends
+ .values()
+ .fold(zero, |acc, v| acc.saturating_add(*v));
+
+ let total_root_divs: U96F32 = root_alpha_dividends
+ .values()
+ .fold(zero, |acc, v| acc.saturating_add(*v));
+
+ let total = total_alpha_divs.saturating_add(total_root_divs);
+
+ let effective_root_prop = if total > zero {
+ U64F64::saturating_from_num(total_root_divs.checked_div(total).unwrap_or(zero))
+ } else {
+ U64F64::saturating_from_num(0)
+ };
+
+ log::debug!(
+ "EffectiveRootProp for netuid {netuid:?}: {effective_root_prop:?} (root_divs: {total_root_divs:?}, alpha_divs: {total_alpha_divs:?})"
+ );
+
+ EffectiveRootProp::::insert(netuid, effective_root_prop);
+ }
+
pub fn get_stake_map(
netuid: NetUid,
hotkeys: Vec<&T::AccountId>,
diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs
index c956c517de..be596e795a 100644
--- a/pallets/subtensor/src/lib.rs
+++ b/pallets/subtensor/src/lib.rs
@@ -1488,6 +1488,14 @@ pub mod pallet {
pub type FlowEmaSmoothingFactor =
StorageValue<_, u64, ValueQuery, DefaultFlowEmaSmoothingFactor>;
+ #[pallet::storage]
+ /// --- MAP ( netuid ) --> EffectiveRootProp for a subnet.
+ /// Computed during epoch in distribute_dividends_and_incentives() as:
+ /// sum(RootAlphaDividendsPerSubnet[netuid]) /
+ /// (sum(AlphaDividendsPerSubnet[netuid]) + sum(RootAlphaDividendsPerSubnet[netuid]))
+ /// This measures the proportion of dividends on a subnet that go to root stakers.
+ pub type EffectiveRootProp = StorageMap<_, Identity, NetUid, U64F64, ValueQuery>;
+
/// ============================
/// ==== Global Parameters =====
/// ============================
diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs
index 311a930647..71771aa6d5 100644
--- a/pallets/subtensor/src/tests/subnet_emissions.rs
+++ b/pallets/subtensor/src/tests/subnet_emissions.rs
@@ -488,3 +488,129 @@ 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);
// });
// }
+
+#[test]
+fn test_effective_root_prop_no_root_dividends() {
+ // When there are no root alpha dividends, EffectiveRootProp should be 0
+ new_test_ext(1).execute_with(|| {
+ let netuid = NetUid::from(1);
+ let hotkey1 = U256::from(100);
+ let hotkey2 = U256::from(101);
+
+ let mut alpha_dividends: BTreeMap = BTreeMap::new();
+ alpha_dividends.insert(hotkey1, U96F32::from_num(1000));
+ alpha_dividends.insert(hotkey2, U96F32::from_num(2000));
+
+ let root_alpha_dividends: BTreeMap = BTreeMap::new();
+
+ SubtensorModule::compute_and_store_effective_root_prop(
+ netuid,
+ &alpha_dividends,
+ &root_alpha_dividends,
+ );
+
+ let prop = EffectiveRootProp::::get(netuid);
+ assert_abs_diff_eq!(prop.to_num::(), 0.0, epsilon = 1e-12);
+ });
+}
+
+#[test]
+fn test_effective_root_prop_all_root_dividends() {
+ // When there are only root alpha dividends, EffectiveRootProp should be 1.0
+ new_test_ext(1).execute_with(|| {
+ let netuid = NetUid::from(1);
+ let hotkey1 = U256::from(100);
+ let hotkey2 = U256::from(101);
+
+ let alpha_dividends: BTreeMap = BTreeMap::new();
+
+ let mut root_alpha_dividends: BTreeMap = BTreeMap::new();
+ root_alpha_dividends.insert(hotkey1, U96F32::from_num(1000));
+ root_alpha_dividends.insert(hotkey2, U96F32::from_num(2000));
+
+ SubtensorModule::compute_and_store_effective_root_prop(
+ netuid,
+ &alpha_dividends,
+ &root_alpha_dividends,
+ );
+
+ let prop = EffectiveRootProp::::get(netuid);
+ assert_abs_diff_eq!(prop.to_num::(), 1.0, epsilon = 1e-12);
+ });
+}
+
+#[test]
+fn test_effective_root_prop_balanced() {
+ // When root and alpha dividends are equal, EffectiveRootProp should be ~0.5
+ new_test_ext(1).execute_with(|| {
+ let netuid = NetUid::from(1);
+ let hotkey1 = U256::from(100);
+
+ let mut alpha_dividends: BTreeMap = BTreeMap::new();
+ alpha_dividends.insert(hotkey1, U96F32::from_num(5000));
+
+ let mut root_alpha_dividends: BTreeMap = BTreeMap::new();
+ root_alpha_dividends.insert(hotkey1, U96F32::from_num(5000));
+
+ SubtensorModule::compute_and_store_effective_root_prop(
+ netuid,
+ &alpha_dividends,
+ &root_alpha_dividends,
+ );
+
+ let prop = EffectiveRootProp::::get(netuid);
+ assert_abs_diff_eq!(prop.to_num::(), 0.5, epsilon = 1e-9);
+ });
+}
+
+#[test]
+fn test_effective_root_prop_both_empty() {
+ // When both are empty, EffectiveRootProp should be 0
+ new_test_ext(1).execute_with(|| {
+ let netuid = NetUid::from(1);
+
+ let alpha_dividends: BTreeMap = BTreeMap::new();
+ let root_alpha_dividends: BTreeMap = BTreeMap::new();
+
+ SubtensorModule::compute_and_store_effective_root_prop(
+ netuid,
+ &alpha_dividends,
+ &root_alpha_dividends,
+ );
+
+ let prop = EffectiveRootProp::::get(netuid);
+ assert_abs_diff_eq!(prop.to_num::(), 0.0, epsilon = 1e-12);
+ });
+}
+
+#[test]
+fn test_effective_root_prop_different_subnets() {
+ // Test that different subnets get different EffectiveRootProp values
+ new_test_ext(1).execute_with(|| {
+ let netuid1 = NetUid::from(1);
+ let netuid2 = NetUid::from(2);
+ let hotkey1 = U256::from(100);
+
+ // Subnet 1: 25% root
+ let mut alpha_divs1: BTreeMap = BTreeMap::new();
+ alpha_divs1.insert(hotkey1, U96F32::from_num(3000));
+ let mut root_divs1: BTreeMap = BTreeMap::new();
+ root_divs1.insert(hotkey1, U96F32::from_num(1000));
+
+ SubtensorModule::compute_and_store_effective_root_prop(netuid1, &alpha_divs1, &root_divs1);
+
+ // Subnet 2: 75% root
+ let mut alpha_divs2: BTreeMap = BTreeMap::new();
+ alpha_divs2.insert(hotkey1, U96F32::from_num(1000));
+ let mut root_divs2: BTreeMap = BTreeMap::new();
+ root_divs2.insert(hotkey1, U96F32::from_num(3000));
+
+ SubtensorModule::compute_and_store_effective_root_prop(netuid2, &alpha_divs2, &root_divs2);
+
+ let prop1 = EffectiveRootProp::::get(netuid1);
+ let prop2 = EffectiveRootProp::::get(netuid2);
+
+ assert_abs_diff_eq!(prop1.to_num::(), 0.25, epsilon = 1e-9);
+ assert_abs_diff_eq!(prop2.to_num::(), 0.75, epsilon = 1e-9);
+ });
+}
From d09024ca704a9cbd896b3437d1088829490c3775 Mon Sep 17 00:00:00 2001
From: Pawel Polewicz
Date: Sat, 31 Jan 2026 09:32:43 +0000
Subject: [PATCH 02/29] feat: add EffectiveRootPropEmissionScaling root
hyperparameter
When enabled, multiplies each subnet's emission share by its stored
EffectiveRootProp value and re-normalizes. This allows root governance
to weight emission toward subnets with higher root staker participation.
- Storage: EffectiveRootPropEmissionScaling (bool, default false)
- Admin extrinsic: sudo_set_effective_root_prop_emission_scaling (call_index 88)
- Helper: normalize_shares(), apply_effective_root_prop_scaling()
- Event: EffectiveRootPropEmissionScalingApplied
Co-Authored-By: Claude Opus 4.5
---
pallets/admin-utils/src/lib.rs | 19 +++
.../src/coinbase/subnet_emissions.rs | 47 ++++-
pallets/subtensor/src/lib.rs | 11 ++
pallets/subtensor/src/macros/events.rs | 6 +
.../subtensor/src/tests/subnet_emissions.rs | 160 ++++++++++++++++++
pallets/subtensor/src/utils/misc.rs | 5 +
6 files changed, 246 insertions(+), 2 deletions(-)
diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs
index 445642d02f..03c742b7ad 100644
--- a/pallets/admin-utils/src/lib.rs
+++ b/pallets/admin-utils/src/lib.rs
@@ -2259,6 +2259,25 @@ pub mod pallet {
log::trace!("ColdkeySwapReannouncementDelaySet( duration: {duration:?} )");
Ok(())
}
+
+ /// Sets EffectiveRootProp emission scaling on/off
+ #[pallet::call_index(88)]
+ #[pallet::weight((
+ Weight::from_parts(7_343_000, 0)
+ .saturating_add(::DbWeight::get().reads(0))
+ .saturating_add(::DbWeight::get().writes(1)),
+ DispatchClass::Operational,
+ Pays::Yes
+ ))]
+ pub fn sudo_set_effective_root_prop_emission_scaling(
+ origin: OriginFor,
+ enabled: bool,
+ ) -> DispatchResult {
+ ensure_root(origin)?;
+ pallet_subtensor::Pallet::::set_effective_root_prop_emission_scaling(enabled);
+ log::debug!("set_effective_root_prop_emission_scaling( {enabled:?} )");
+ Ok(())
+ }
}
}
diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs
index 477a678864..5547add0c2 100644
--- a/pallets/subtensor/src/coinbase/subnet_emissions.rs
+++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs
@@ -1,6 +1,6 @@
use super::*;
use alloc::collections::BTreeMap;
-use safe_math::FixedExt;
+use safe_math::*;
use substrate_fixed::transcendental::{exp, ln};
use substrate_fixed::types::{I32F32, I64F64, U64F64, U96F32};
@@ -22,14 +22,57 @@ impl Pallet {
.collect()
}
+ /// Normalizes shares so they sum to 1.0. If all shares are zero, leaves them unchanged.
+ pub(crate) fn normalize_shares(shares: &mut BTreeMap) {
+ let sum: U64F64 = shares.values().copied().sum();
+ if sum > U64F64::saturating_from_num(0) {
+ for share in shares.values_mut() {
+ *share = share.safe_div(sum);
+ }
+ }
+ }
+
+ /// When EffectiveRootPropEmissionScaling is enabled, multiplies each subnet's share
+ /// by its stored EffectiveRootProp value, then re-normalizes shares to sum to 1.0.
+ pub(crate) fn apply_effective_root_prop_scaling(shares: &mut BTreeMap) {
+ if !EffectiveRootPropEmissionScaling::::get() {
+ return;
+ }
+
+ for (netuid, share) in shares.iter_mut() {
+ let effective_root_prop = EffectiveRootProp::::get(netuid);
+ *share = share.saturating_mul(effective_root_prop);
+ }
+
+ Self::normalize_shares(shares);
+
+ // Emit event with scaled shares
+ let shares_vec: Vec<(NetUid, u64)> = shares
+ .iter()
+ .map(|(netuid, share)| {
+ // Store as fixed-point u64 (multiply by 10^18 for precision)
+ let share_u64 = share
+ .saturating_mul(U64F64::saturating_from_num(1_000_000_000u64))
+ .saturating_to_num::();
+ (*netuid, share_u64)
+ })
+ .collect();
+ Self::deposit_event(Event::::EffectiveRootPropEmissionScalingApplied {
+ shares: shares_vec,
+ });
+ }
+
pub fn get_subnet_block_emissions(
subnets_to_emit_to: &[NetUid],
block_emission: U96F32,
) -> BTreeMap {
// Get subnet TAO emissions.
- let shares = Self::get_shares(subnets_to_emit_to);
+ let mut shares = Self::get_shares(subnets_to_emit_to);
log::debug!("Subnet emission shares = {shares:?}");
+ // Apply EffectiveRootProp scaling if enabled.
+ Self::apply_effective_root_prop_scaling(&mut shares);
+
shares
.into_iter()
.map(|(netuid, share)| {
diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs
index be596e795a..ea87f84a99 100644
--- a/pallets/subtensor/src/lib.rs
+++ b/pallets/subtensor/src/lib.rs
@@ -1496,6 +1496,17 @@ pub mod pallet {
/// This measures the proportion of dividends on a subnet that go to root stakers.
pub type EffectiveRootProp = StorageMap<_, Identity, NetUid, U64F64, ValueQuery>;
+ #[pallet::type_value]
+ /// Default: EffectiveRootPropEmissionScaling is disabled.
+ pub fn DefaultEffectiveRootPropEmissionScaling() -> bool {
+ false
+ }
+ #[pallet::storage]
+ /// When enabled, multiply each subnet's emission share by its EffectiveRootProp,
+ /// then re-normalize so shares sum to 1.0.
+ pub type EffectiveRootPropEmissionScaling =
+ StorageValue<_, bool, ValueQuery, DefaultEffectiveRootPropEmissionScaling>;
+
/// ============================
/// ==== Global Parameters =====
/// ============================
diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs
index 107bbf975c..20a0995ac6 100644
--- a/pallets/subtensor/src/macros/events.rs
+++ b/pallets/subtensor/src/macros/events.rs
@@ -516,5 +516,11 @@ mod events {
/// The amount of alpha distributed
alpha: AlphaCurrency,
},
+
+ /// Emission shares have been adjusted by EffectiveRootProp scaling.
+ EffectiveRootPropEmissionScalingApplied {
+ /// Per-subnet scaled shares as (netuid, share * 10^18) for precision
+ shares: Vec<(NetUid, u64)>,
+ },
}
}
diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs
index 71771aa6d5..e137eb3b17 100644
--- a/pallets/subtensor/src/tests/subnet_emissions.rs
+++ b/pallets/subtensor/src/tests/subnet_emissions.rs
@@ -614,3 +614,163 @@ fn test_effective_root_prop_different_subnets() {
assert_abs_diff_eq!(prop2.to_num::(), 0.75, epsilon = 1e-9);
});
}
+
+#[test]
+fn test_normalize_shares_basic() {
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(2.0));
+ shares.insert(NetUid::from(2), u64f64(3.0));
+ shares.insert(NetUid::from(3), u64f64(5.0));
+
+ SubtensorModule::normalize_shares(&mut shares);
+
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ let s2 = shares.get(&NetUid::from(2)).unwrap().to_num::();
+ let s3 = shares.get(&NetUid::from(3)).unwrap().to_num::();
+
+ assert_abs_diff_eq!(s1, 0.2, epsilon = 1e-9);
+ assert_abs_diff_eq!(s2, 0.3, epsilon = 1e-9);
+ assert_abs_diff_eq!(s3, 0.5, epsilon = 1e-9);
+}
+
+#[test]
+fn test_normalize_shares_all_zero() {
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.0));
+ shares.insert(NetUid::from(2), u64f64(0.0));
+
+ SubtensorModule::normalize_shares(&mut shares);
+
+ // Should remain zero when all are zero
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ let s2 = shares.get(&NetUid::from(2)).unwrap().to_num::();
+
+ assert_abs_diff_eq!(s1, 0.0, epsilon = 1e-12);
+ assert_abs_diff_eq!(s2, 0.0, epsilon = 1e-12);
+}
+
+#[test]
+fn test_apply_effective_root_prop_scaling_disabled() {
+ new_test_ext(1).execute_with(|| {
+ // Scaling is disabled by default
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.5));
+ shares.insert(NetUid::from(2), u64f64(0.5));
+
+ let shares_before = shares.clone();
+ SubtensorModule::apply_effective_root_prop_scaling(&mut shares);
+
+ // Shares should be unchanged when scaling is disabled
+ for (k, v) in shares_before {
+ assert_abs_diff_eq!(
+ shares.get(&k).unwrap().to_num::(),
+ v.to_num::(),
+ epsilon = 1e-12
+ );
+ }
+ });
+}
+
+#[test]
+fn test_apply_effective_root_prop_scaling_enabled() {
+ new_test_ext(1).execute_with(|| {
+ // Enable scaling
+ EffectiveRootPropEmissionScaling::::set(true);
+
+ // Set EffectiveRootProp for subnets
+ EffectiveRootProp::::insert(NetUid::from(1), u64f64(0.8));
+ EffectiveRootProp::::insert(NetUid::from(2), u64f64(0.2));
+
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.5));
+ shares.insert(NetUid::from(2), u64f64(0.5));
+
+ SubtensorModule::apply_effective_root_prop_scaling(&mut shares);
+
+ // After scaling: subnet1 = 0.5*0.8 = 0.4, subnet2 = 0.5*0.2 = 0.1
+ // After normalization: subnet1 = 0.4/0.5 = 0.8, subnet2 = 0.1/0.5 = 0.2
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ let s2 = shares.get(&NetUid::from(2)).unwrap().to_num::();
+
+ assert_abs_diff_eq!(s1, 0.8, epsilon = 1e-9);
+ assert_abs_diff_eq!(s2, 0.2, epsilon = 1e-9);
+ assert_abs_diff_eq!(s1 + s2, 1.0, epsilon = 1e-9);
+ });
+}
+
+#[test]
+fn test_apply_effective_root_prop_scaling_emits_event() {
+ new_test_ext(1).execute_with(|| {
+ // Enable scaling
+ EffectiveRootPropEmissionScaling::::set(true);
+
+ // Set EffectiveRootProp for subnets
+ EffectiveRootProp::::insert(NetUid::from(1), u64f64(0.5));
+ EffectiveRootProp::::insert(NetUid::from(2), u64f64(0.5));
+
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.6));
+ shares.insert(NetUid::from(2), u64f64(0.4));
+
+ // Clear events
+ System::reset_events();
+
+ SubtensorModule::apply_effective_root_prop_scaling(&mut shares);
+
+ // Check that the event was emitted
+ let events = System::events();
+ let found = events.iter().any(|e| {
+ matches!(
+ &e.event,
+ RuntimeEvent::SubtensorModule(
+ Event::EffectiveRootPropEmissionScalingApplied { .. }
+ )
+ )
+ });
+ assert!(
+ found,
+ "Expected EffectiveRootPropEmissionScalingApplied event"
+ );
+ });
+}
+
+#[test]
+fn test_apply_effective_root_prop_scaling_all_zero_props() {
+ new_test_ext(1).execute_with(|| {
+ // Enable scaling
+ EffectiveRootPropEmissionScaling::::set(true);
+
+ // EffectiveRootProp defaults to 0 for all subnets (ValueQuery default)
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.5));
+ shares.insert(NetUid::from(2), u64f64(0.5));
+
+ SubtensorModule::apply_effective_root_prop_scaling(&mut shares);
+
+ // All shares become 0 when all EffectiveRootProp are 0
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ let s2 = shares.get(&NetUid::from(2)).unwrap().to_num::();
+
+ assert_abs_diff_eq!(s1, 0.0, epsilon = 1e-12);
+ assert_abs_diff_eq!(s2, 0.0, epsilon = 1e-12);
+ });
+}
+
+#[test]
+fn test_apply_effective_root_prop_scaling_single_subnet() {
+ new_test_ext(1).execute_with(|| {
+ // Enable scaling
+ EffectiveRootPropEmissionScaling::::set(true);
+
+ EffectiveRootProp::::insert(NetUid::from(1), u64f64(0.3));
+
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(1.0));
+
+ SubtensorModule::apply_effective_root_prop_scaling(&mut shares);
+
+ // Single subnet should get normalized back to 1.0
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ assert_abs_diff_eq!(s1, 1.0, epsilon = 1e-9);
+ });
+}
diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs
index 609e43cf63..441af6736f 100644
--- a/pallets/subtensor/src/utils/misc.rs
+++ b/pallets/subtensor/src/utils/misc.rs
@@ -911,4 +911,9 @@ impl Pallet {
pub fn set_tao_flow_smoothing_factor(smoothing_factor: u64) {
FlowEmaSmoothingFactor::::set(smoothing_factor);
}
+
+ /// Sets whether EffectiveRootProp emission scaling is enabled.
+ pub fn set_effective_root_prop_emission_scaling(enabled: bool) {
+ EffectiveRootPropEmissionScaling::::set(enabled);
+ }
}
From a0e847ed04977478c0e756a3fdbf5ced183d2f42 Mon Sep 17 00:00:00 2001
From: Pawel Polewicz
Date: Sat, 31 Jan 2026 10:04:51 +0000
Subject: [PATCH 03/29] feat: add EmissionTopSubnetProportion root
hyperparameter
Only the top proportion of subnets (ranked by emission share) receive
emission. Default is 50% (5000 basis points). Remaining subnets have
shares zeroed and redistributed to top subnets.
Uses ceil(count * proportion / 10000) so a single subnet always
qualifies (ceil(1 * 0.5) = 1).
- Storage: EmissionTopSubnetProportion (u16, default 5000)
- Admin extrinsic: sudo_set_emission_top_subnet_proportion (call_index 89)
- Helpers: zero_and_redistribute_bottom_shares(), apply_top_subnet_proportion_filter()
- Event: EmissionTopSubnetFilterApplied
Co-Authored-By: Claude Opus 4.5
---
pallets/admin-utils/src/lib.rs | 23 +++
.../src/coinbase/subnet_emissions.rs | 66 +++++++
pallets/subtensor/src/lib.rs | 13 ++
pallets/subtensor/src/macros/events.rs | 7 +
.../subtensor/src/tests/subnet_emissions.rs | 181 ++++++++++++++++++
pallets/subtensor/src/utils/misc.rs | 5 +
6 files changed, 295 insertions(+)
diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs
index 03c742b7ad..9ba0bcd30b 100644
--- a/pallets/admin-utils/src/lib.rs
+++ b/pallets/admin-utils/src/lib.rs
@@ -2278,6 +2278,29 @@ pub mod pallet {
log::debug!("set_effective_root_prop_emission_scaling( {enabled:?} )");
Ok(())
}
+
+ /// Sets the proportion of top subnets that receive emission
+ #[pallet::call_index(89)]
+ #[pallet::weight((
+ Weight::from_parts(7_343_000, 0)
+ .saturating_add(::DbWeight::get().reads(0))
+ .saturating_add(::DbWeight::get().writes(1)),
+ DispatchClass::Operational,
+ Pays::Yes
+ ))]
+ pub fn sudo_set_emission_top_subnet_proportion(
+ origin: OriginFor,
+ proportion: u16,
+ ) -> DispatchResult {
+ ensure_root(origin)?;
+ ensure!(
+ proportion > 0 && proportion <= 10000,
+ Error::::InvalidValue
+ );
+ pallet_subtensor::Pallet::::set_emission_top_subnet_proportion(proportion);
+ log::debug!("set_emission_top_subnet_proportion( {proportion:?} )");
+ Ok(())
+ }
}
}
diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs
index 5547add0c2..de1d0dc1a5 100644
--- a/pallets/subtensor/src/coinbase/subnet_emissions.rs
+++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs
@@ -62,6 +62,69 @@ impl Pallet {
});
}
+ /// Zeros shares outside top_k (by descending share value) and re-normalizes the rest.
+ /// Subnets with equal shares at the boundary are included if they tie with the k-th position.
+ pub(crate) fn zero_and_redistribute_bottom_shares(
+ shares: &mut BTreeMap,
+ top_k: usize,
+ ) {
+ if top_k == 0 || shares.is_empty() {
+ // Zero everything
+ for share in shares.values_mut() {
+ *share = U64F64::saturating_from_num(0);
+ }
+ return;
+ }
+ if top_k >= shares.len() {
+ return; // Nothing to filter
+ }
+
+ // Sort netuids by share descending
+ let mut sorted: Vec<(NetUid, U64F64)> = shares.iter().map(|(k, v)| (*k, *v)).collect();
+ sorted.sort_by(|a, b| b.1.cmp(&a.1));
+
+ // Find the set of netuids to zero out (those beyond top_k)
+ let netuids_to_zero: Vec = sorted[top_k..].iter().map(|(k, _)| *k).collect();
+
+ for netuid in netuids_to_zero {
+ if let Some(share) = shares.get_mut(&netuid) {
+ *share = U64F64::saturating_from_num(0);
+ }
+ }
+
+ Self::normalize_shares(shares);
+ }
+
+ /// Filters subnets so only the top proportion (by share) receive emission.
+ /// Uses ceil(count * proportion / 10000) to determine how many subnets to keep.
+ /// A single subnet always counts as in top 50%.
+ pub(crate) fn apply_top_subnet_proportion_filter(shares: &mut BTreeMap) {
+ let proportion = EmissionTopSubnetProportion::::get();
+ if proportion >= 10000 {
+ return; // 100% means all subnets get emission
+ }
+
+ let total = shares.len() as u32;
+ if total == 0 {
+ return;
+ }
+
+ // ceil(total * proportion / 10000)
+ let top_k = ((total as u64) * (proportion as u64)).div_ceil(10000);
+ let top_k = top_k.max(1) as usize; // At least 1 subnet
+
+ log::debug!(
+ "EmissionTopSubnetProportion: keeping top {top_k} of {total} subnets (proportion: {proportion}/10000)"
+ );
+
+ Self::zero_and_redistribute_bottom_shares(shares, top_k);
+
+ Self::deposit_event(Event::::EmissionTopSubnetFilterApplied {
+ top_k: top_k as u16,
+ total: total as u16,
+ });
+ }
+
pub fn get_subnet_block_emissions(
subnets_to_emit_to: &[NetUid],
block_emission: U96F32,
@@ -73,6 +136,9 @@ impl Pallet {
// Apply EffectiveRootProp scaling if enabled.
Self::apply_effective_root_prop_scaling(&mut shares);
+ // Apply top subnet proportion filter.
+ Self::apply_top_subnet_proportion_filter(&mut shares);
+
shares
.into_iter()
.map(|(netuid, share)| {
diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs
index ea87f84a99..ca7eb62fd5 100644
--- a/pallets/subtensor/src/lib.rs
+++ b/pallets/subtensor/src/lib.rs
@@ -1507,6 +1507,19 @@ pub mod pallet {
pub type EffectiveRootPropEmissionScaling =
StorageValue<_, bool, ValueQuery, DefaultEffectiveRootPropEmissionScaling>;
+ #[pallet::type_value]
+ /// Default: top 50% of subnets (by emission share) receive emission.
+ pub fn DefaultEmissionTopSubnetProportion() -> u16 {
+ 5000 // 50% in basis points (out of 10000)
+ }
+ #[pallet::storage]
+ /// Proportion of subnets (ranked by share) that receive emission.
+ /// Value in basis points: 5000 = 50%, 10000 = 100%.
+ /// Only the top ceil(count * proportion / 10000) subnets get emission.
+ /// Remaining subnets have shares zeroed and redistributed.
+ pub type EmissionTopSubnetProportion =
+ StorageValue<_, u16, ValueQuery, DefaultEmissionTopSubnetProportion>;
+
/// ============================
/// ==== Global Parameters =====
/// ============================
diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs
index 20a0995ac6..1452e17367 100644
--- a/pallets/subtensor/src/macros/events.rs
+++ b/pallets/subtensor/src/macros/events.rs
@@ -522,5 +522,12 @@ mod events {
/// Per-subnet scaled shares as (netuid, share * 10^18) for precision
shares: Vec<(NetUid, u64)>,
},
+ /// Subnet emission shares zeroed for subnets outside top proportion.
+ EmissionTopSubnetFilterApplied {
+ /// Number of subnets that kept emission
+ top_k: u16,
+ /// Total number of subnets considered
+ total: u16,
+ },
}
}
diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs
index e137eb3b17..69312bd6ce 100644
--- a/pallets/subtensor/src/tests/subnet_emissions.rs
+++ b/pallets/subtensor/src/tests/subnet_emissions.rs
@@ -774,3 +774,184 @@ fn test_apply_effective_root_prop_scaling_single_subnet() {
assert_abs_diff_eq!(s1, 1.0, epsilon = 1e-9);
});
}
+
+#[test]
+fn test_zero_and_redistribute_bottom_shares_basic() {
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.1));
+ shares.insert(NetUid::from(2), u64f64(0.2));
+ shares.insert(NetUid::from(3), u64f64(0.3));
+ shares.insert(NetUid::from(4), u64f64(0.4));
+
+ SubtensorModule::zero_and_redistribute_bottom_shares(&mut shares, 2);
+
+ // Top 2 are netuid 4 (0.4) and netuid 3 (0.3)
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ let s2 = shares.get(&NetUid::from(2)).unwrap().to_num::();
+ let s3 = shares.get(&NetUid::from(3)).unwrap().to_num::();
+ let s4 = shares.get(&NetUid::from(4)).unwrap().to_num::();
+
+ assert_abs_diff_eq!(s1, 0.0, epsilon = 1e-12);
+ assert_abs_diff_eq!(s2, 0.0, epsilon = 1e-12);
+ // s3 and s4 should be renormalized: 0.3/0.7 and 0.4/0.7
+ assert_abs_diff_eq!(s3, 0.3 / 0.7, epsilon = 1e-9);
+ assert_abs_diff_eq!(s4, 0.4 / 0.7, epsilon = 1e-9);
+ assert_abs_diff_eq!(s3 + s4, 1.0, epsilon = 1e-9);
+}
+
+#[test]
+fn test_zero_and_redistribute_top_k_exceeds_count() {
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.5));
+ shares.insert(NetUid::from(2), u64f64(0.5));
+
+ SubtensorModule::zero_and_redistribute_bottom_shares(&mut shares, 10);
+
+ // Nothing should change since top_k > len
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ let s2 = shares.get(&NetUid::from(2)).unwrap().to_num::();
+ assert_abs_diff_eq!(s1, 0.5, epsilon = 1e-12);
+ assert_abs_diff_eq!(s2, 0.5, epsilon = 1e-12);
+}
+
+#[test]
+fn test_apply_top_subnet_proportion_filter_default_50_percent_4_subnets() {
+ new_test_ext(1).execute_with(|| {
+ // Default is 5000 (50%)
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.1));
+ shares.insert(NetUid::from(2), u64f64(0.2));
+ shares.insert(NetUid::from(3), u64f64(0.3));
+ shares.insert(NetUid::from(4), u64f64(0.4));
+
+ SubtensorModule::apply_top_subnet_proportion_filter(&mut shares);
+
+ // ceil(4 * 5000 / 10000) = ceil(2.0) = 2
+ // Top 2: netuid 4 (0.4) and netuid 3 (0.3)
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ let s2 = shares.get(&NetUid::from(2)).unwrap().to_num::();
+ let s3 = shares.get(&NetUid::from(3)).unwrap().to_num::();
+ let s4 = shares.get(&NetUid::from(4)).unwrap().to_num::();
+
+ assert_abs_diff_eq!(s1, 0.0, epsilon = 1e-12);
+ assert_abs_diff_eq!(s2, 0.0, epsilon = 1e-12);
+ assert!(s3 > 0.0);
+ assert!(s4 > 0.0);
+ assert_abs_diff_eq!(s3 + s4, 1.0, epsilon = 1e-9);
+ });
+}
+
+#[test]
+fn test_apply_top_subnet_proportion_filter_default_50_percent_1_subnet() {
+ new_test_ext(1).execute_with(|| {
+ // Default 50%, 1 subnet -> ceil(1 * 0.5) = 1
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(1.0));
+
+ SubtensorModule::apply_top_subnet_proportion_filter(&mut shares);
+
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ assert_abs_diff_eq!(s1, 1.0, epsilon = 1e-9);
+ });
+}
+
+#[test]
+fn test_apply_top_subnet_proportion_filter_default_50_percent_3_subnets() {
+ new_test_ext(1).execute_with(|| {
+ // Default 50%, 3 subnets -> ceil(3 * 5000 / 10000) = ceil(1.5) = 2
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.2));
+ shares.insert(NetUid::from(2), u64f64(0.3));
+ shares.insert(NetUid::from(3), u64f64(0.5));
+
+ SubtensorModule::apply_top_subnet_proportion_filter(&mut shares);
+
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ let s2 = shares.get(&NetUid::from(2)).unwrap().to_num::();
+ let s3 = shares.get(&NetUid::from(3)).unwrap().to_num::();
+
+ assert_abs_diff_eq!(s1, 0.0, epsilon = 1e-12);
+ assert!(s2 > 0.0);
+ assert!(s3 > 0.0);
+ assert_abs_diff_eq!(s2 + s3, 1.0, epsilon = 1e-9);
+ });
+}
+
+#[test]
+fn test_apply_top_subnet_proportion_filter_100_percent() {
+ new_test_ext(1).execute_with(|| {
+ EmissionTopSubnetProportion::::set(10000); // 100%
+
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.25));
+ shares.insert(NetUid::from(2), u64f64(0.75));
+
+ let shares_before = shares.clone();
+ SubtensorModule::apply_top_subnet_proportion_filter(&mut shares);
+
+ // All subnets should keep their shares
+ for (k, v) in shares_before {
+ assert_abs_diff_eq!(
+ shares.get(&k).unwrap().to_num::(),
+ v.to_num::(),
+ epsilon = 1e-12
+ );
+ }
+ });
+}
+
+#[test]
+fn test_apply_top_subnet_proportion_filter_emits_event() {
+ new_test_ext(1).execute_with(|| {
+ // Default 50%, 4 subnets
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.1));
+ shares.insert(NetUid::from(2), u64f64(0.2));
+ shares.insert(NetUid::from(3), u64f64(0.3));
+ shares.insert(NetUid::from(4), u64f64(0.4));
+
+ System::reset_events();
+ SubtensorModule::apply_top_subnet_proportion_filter(&mut shares);
+
+ let events = System::events();
+ let found = events.iter().any(|e| {
+ matches!(
+ &e.event,
+ RuntimeEvent::SubtensorModule(Event::EmissionTopSubnetFilterApplied {
+ top_k: 2,
+ total: 4,
+ })
+ )
+ });
+ assert!(
+ found,
+ "Expected EmissionTopSubnetFilterApplied event with top_k=2, total=4"
+ );
+ });
+}
+
+#[test]
+fn test_apply_top_subnet_proportion_filter_zeroed_get_no_emission() {
+ new_test_ext(1).execute_with(|| {
+ // Default 50%, 4 subnets
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.1));
+ shares.insert(NetUid::from(2), u64f64(0.2));
+ shares.insert(NetUid::from(3), u64f64(0.3));
+ shares.insert(NetUid::from(4), u64f64(0.4));
+
+ SubtensorModule::apply_top_subnet_proportion_filter(&mut shares);
+
+ // Verify zeroed subnets produce zero emission
+ let block_emission = U96F32::from_num(1_000_000);
+ for (netuid, share) in &shares {
+ let emission = U64F64::saturating_from_num(*share)
+ .saturating_mul(U64F64::saturating_from_num(block_emission));
+ if *netuid == NetUid::from(1) || *netuid == NetUid::from(2) {
+ assert_abs_diff_eq!(emission.to_num::(), 0.0, epsilon = 1e-6);
+ } else {
+ assert!(emission.to_num::() > 0.0);
+ }
+ }
+ });
+}
diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs
index 441af6736f..8fca29f071 100644
--- a/pallets/subtensor/src/utils/misc.rs
+++ b/pallets/subtensor/src/utils/misc.rs
@@ -916,4 +916,9 @@ impl Pallet {
pub fn set_effective_root_prop_emission_scaling(enabled: bool) {
EffectiveRootPropEmissionScaling::::set(enabled);
}
+
+ /// Sets the proportion of top subnets that receive emission (in basis points, max 10000).
+ pub fn set_emission_top_subnet_proportion(proportion: u16) {
+ EmissionTopSubnetProportion::::set(proportion);
+ }
}
From 48cbd70daa92f3c0a760a8b1bbcccf974db43e4d Mon Sep 17 00:00:00 2001
From: Pawel Polewicz
Date: Sat, 31 Jan 2026 10:49:49 +0000
Subject: [PATCH 04/29] feat: add EmissionTopSubnetAbsoluteLimit root
hyperparameter
Adds absolute cap on the number of subnets that can receive emission.
Default is 0 (disabled). When set to N > 0, only the top N subnets by
share receive emission; the rest are zeroed and redistributed.
Applied after proportion filter so the stricter constraint wins.
- Storage: EmissionTopSubnetAbsoluteLimit (u16, default 0)
- Admin extrinsic: sudo_set_emission_top_subnet_absolute_limit (call_index 90)
- Helper: apply_top_subnet_absolute_limit()
- Event: EmissionAbsoluteLimitApplied
- Fixed 5 existing coinbase tests to disable proportion filter
Co-Authored-By: Claude Opus 4.5
---
pallets/admin-utils/src/lib.rs | 19 ++
.../src/coinbase/subnet_emissions.rs | 34 +++
pallets/subtensor/src/lib.rs | 12 ++
pallets/subtensor/src/macros/events.rs | 7 +
pallets/subtensor/src/tests/coinbase.rs | 10 +
.../subtensor/src/tests/subnet_emissions.rs | 202 ++++++++++++++++++
pallets/subtensor/src/utils/misc.rs | 5 +
7 files changed, 289 insertions(+)
diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs
index 9ba0bcd30b..7b86fcff75 100644
--- a/pallets/admin-utils/src/lib.rs
+++ b/pallets/admin-utils/src/lib.rs
@@ -2301,6 +2301,25 @@ pub mod pallet {
log::debug!("set_emission_top_subnet_proportion( {proportion:?} )");
Ok(())
}
+
+ /// Sets the absolute limit on number of subnets receiving emission (0 = no limit)
+ #[pallet::call_index(90)]
+ #[pallet::weight((
+ Weight::from_parts(7_343_000, 0)
+ .saturating_add(::DbWeight::get().reads(0))
+ .saturating_add(::DbWeight::get().writes(1)),
+ DispatchClass::Operational,
+ Pays::Yes
+ ))]
+ pub fn sudo_set_emission_top_subnet_absolute_limit(
+ origin: OriginFor,
+ limit: u16,
+ ) -> DispatchResult {
+ ensure_root(origin)?;
+ pallet_subtensor::Pallet::::set_emission_top_subnet_absolute_limit(limit);
+ log::debug!("set_emission_top_subnet_absolute_limit( {limit:?} )");
+ Ok(())
+ }
}
}
diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs
index de1d0dc1a5..31e62f1398 100644
--- a/pallets/subtensor/src/coinbase/subnet_emissions.rs
+++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs
@@ -125,6 +125,37 @@ impl Pallet {
});
}
+ /// Limits the number of subnets receiving emission to an absolute number.
+ /// When limit is 0, no filtering occurs (disabled).
+ /// When limit > 0 and less than the number of subnets with nonzero shares,
+ /// zeros shares beyond the top `limit` subnets and re-normalizes.
+ pub(crate) fn apply_top_subnet_absolute_limit(shares: &mut BTreeMap) {
+ let limit = EmissionTopSubnetAbsoluteLimit::::get();
+ if limit == 0 {
+ return; // Disabled
+ }
+
+ let nonzero_count = shares
+ .values()
+ .filter(|v| **v > U64F64::saturating_from_num(0))
+ .count();
+
+ if nonzero_count <= limit as usize {
+ return; // Already within limit
+ }
+
+ log::debug!(
+ "EmissionTopSubnetAbsoluteLimit: limiting to top {limit} subnets (had {nonzero_count} nonzero)"
+ );
+
+ Self::zero_and_redistribute_bottom_shares(shares, limit as usize);
+
+ Self::deposit_event(Event::::EmissionAbsoluteLimitApplied {
+ limit,
+ before_count: nonzero_count as u16,
+ });
+ }
+
pub fn get_subnet_block_emissions(
subnets_to_emit_to: &[NetUid],
block_emission: U96F32,
@@ -139,6 +170,9 @@ impl Pallet {
// Apply top subnet proportion filter.
Self::apply_top_subnet_proportion_filter(&mut shares);
+ // Apply absolute subnet limit.
+ Self::apply_top_subnet_absolute_limit(&mut shares);
+
shares
.into_iter()
.map(|(netuid, share)| {
diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs
index ca7eb62fd5..c146a5df32 100644
--- a/pallets/subtensor/src/lib.rs
+++ b/pallets/subtensor/src/lib.rs
@@ -1520,6 +1520,18 @@ pub mod pallet {
pub type EmissionTopSubnetProportion =
StorageValue<_, u16, ValueQuery, DefaultEmissionTopSubnetProportion>;
+ #[pallet::type_value]
+ /// Default: no absolute limit on number of subnets receiving emission.
+ pub fn DefaultEmissionTopSubnetAbsoluteLimit() -> u16 {
+ 0 // 0 means no limit (disabled)
+ }
+ #[pallet::storage]
+ /// Absolute maximum number of subnets that can receive emission.
+ /// 0 means no limit (disabled). When set to N > 0, only the top N
+ /// subnets by share receive emission; the rest are zeroed and redistributed.
+ pub type EmissionTopSubnetAbsoluteLimit =
+ StorageValue<_, u16, ValueQuery, DefaultEmissionTopSubnetAbsoluteLimit>;
+
/// ============================
/// ==== Global Parameters =====
/// ============================
diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs
index 1452e17367..2704aece7a 100644
--- a/pallets/subtensor/src/macros/events.rs
+++ b/pallets/subtensor/src/macros/events.rs
@@ -529,5 +529,12 @@ mod events {
/// Total number of subnets considered
total: u16,
},
+ /// Subnet emission shares zeroed for subnets beyond absolute limit.
+ EmissionAbsoluteLimitApplied {
+ /// The absolute limit applied
+ limit: u16,
+ /// Number of subnets that had nonzero shares before limiting
+ before_count: u16,
+ },
}
}
diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs
index a79f4b713a..586dff8dc0 100644
--- a/pallets/subtensor/src/tests/coinbase.rs
+++ b/pallets/subtensor/src/tests/coinbase.rs
@@ -164,6 +164,8 @@ fn test_coinbase_tao_issuance_base_low() {
#[test]
fn test_coinbase_tao_issuance_multiple() {
new_test_ext(1).execute_with(|| {
+ // Disable top-subnet proportion filter so all 3 subnets receive emission.
+ EmissionTopSubnetProportion::::set(10000);
let netuid1 = NetUid::from(1);
let netuid2 = NetUid::from(2);
let netuid3 = NetUid::from(3);
@@ -208,6 +210,8 @@ fn test_coinbase_tao_issuance_multiple() {
#[test]
fn test_coinbase_tao_issuance_different_prices() {
new_test_ext(1).execute_with(|| {
+ // Disable top-subnet proportion filter so both subnets receive emission.
+ EmissionTopSubnetProportion::::set(10000);
let netuid1 = NetUid::from(1);
let netuid2 = NetUid::from(2);
let emission = 100_000_000;
@@ -480,6 +484,8 @@ fn test_update_moving_price_after_time() {
#[test]
fn test_coinbase_alpha_issuance_base() {
new_test_ext(1).execute_with(|| {
+ // Disable top-subnet proportion filter so both subnets receive emission.
+ EmissionTopSubnetProportion::::set(10000);
let netuid1 = NetUid::from(1);
let netuid2 = NetUid::from(2);
let emission: u64 = 1_000_000;
@@ -518,6 +524,8 @@ fn test_coinbase_alpha_issuance_base() {
#[test]
fn test_coinbase_alpha_issuance_different() {
new_test_ext(1).execute_with(|| {
+ // Disable top-subnet proportion filter so both subnets receive emission.
+ EmissionTopSubnetProportion::::set(10000);
let netuid1 = NetUid::from(1);
let netuid2 = NetUid::from(2);
let emission: u64 = 1_000_000;
@@ -592,6 +600,8 @@ fn test_coinbase_alpha_issuance_with_cap_trigger() {
#[test]
fn test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission() {
new_test_ext(1).execute_with(|| {
+ // Disable top-subnet proportion filter so both subnets receive emission.
+ EmissionTopSubnetProportion::::set(10000);
let netuid1 = NetUid::from(1);
let netuid2 = NetUid::from(2);
let emission: u64 = 1_000_000;
diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs
index 69312bd6ce..6ab0d9143c 100644
--- a/pallets/subtensor/src/tests/subnet_emissions.rs
+++ b/pallets/subtensor/src/tests/subnet_emissions.rs
@@ -955,3 +955,205 @@ fn test_apply_top_subnet_proportion_filter_zeroed_get_no_emission() {
}
});
}
+
+#[test]
+fn test_apply_top_subnet_absolute_limit_disabled() {
+ new_test_ext(1).execute_with(|| {
+ // Default limit is 0 (disabled)
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.25));
+ shares.insert(NetUid::from(2), u64f64(0.25));
+ shares.insert(NetUid::from(3), u64f64(0.25));
+ shares.insert(NetUid::from(4), u64f64(0.25));
+
+ let shares_before = shares.clone();
+ SubtensorModule::apply_top_subnet_absolute_limit(&mut shares);
+
+ // No change when disabled
+ for (k, v) in shares_before {
+ assert_abs_diff_eq!(
+ shares.get(&k).unwrap().to_num::(),
+ v.to_num::(),
+ epsilon = 1e-12
+ );
+ }
+ });
+}
+
+#[test]
+fn test_apply_top_subnet_absolute_limit_two_of_five() {
+ new_test_ext(1).execute_with(|| {
+ EmissionTopSubnetAbsoluteLimit::::set(2);
+
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.05));
+ shares.insert(NetUid::from(2), u64f64(0.10));
+ shares.insert(NetUid::from(3), u64f64(0.15));
+ shares.insert(NetUid::from(4), u64f64(0.30));
+ shares.insert(NetUid::from(5), u64f64(0.40));
+
+ SubtensorModule::apply_top_subnet_absolute_limit(&mut shares);
+
+ // Only top 2 (netuid 5 and 4) should have nonzero shares
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ let s2 = shares.get(&NetUid::from(2)).unwrap().to_num::();
+ let s3 = shares.get(&NetUid::from(3)).unwrap().to_num::();
+ let s4 = shares.get(&NetUid::from(4)).unwrap().to_num::();
+ let s5 = shares.get(&NetUid::from(5)).unwrap().to_num::();
+
+ assert_abs_diff_eq!(s1, 0.0, epsilon = 1e-12);
+ assert_abs_diff_eq!(s2, 0.0, epsilon = 1e-12);
+ assert_abs_diff_eq!(s3, 0.0, epsilon = 1e-12);
+ assert!(s4 > 0.0);
+ assert!(s5 > 0.0);
+ assert_abs_diff_eq!(s4 + s5, 1.0, epsilon = 1e-9);
+ // 0.30/0.70 and 0.40/0.70
+ assert_abs_diff_eq!(s4, 0.30 / 0.70, epsilon = 1e-9);
+ assert_abs_diff_eq!(s5, 0.40 / 0.70, epsilon = 1e-9);
+ });
+}
+
+#[test]
+fn test_apply_top_subnet_absolute_limit_exceeds_count() {
+ new_test_ext(1).execute_with(|| {
+ EmissionTopSubnetAbsoluteLimit::::set(10);
+
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.5));
+ shares.insert(NetUid::from(2), u64f64(0.3));
+ shares.insert(NetUid::from(3), u64f64(0.2));
+
+ let shares_before = shares.clone();
+ SubtensorModule::apply_top_subnet_absolute_limit(&mut shares);
+
+ // All keep their shares when limit > count
+ for (k, v) in shares_before {
+ assert_abs_diff_eq!(
+ shares.get(&k).unwrap().to_num::(),
+ v.to_num::(),
+ epsilon = 1e-12
+ );
+ }
+ });
+}
+
+#[test]
+fn test_apply_top_subnet_absolute_limit_emits_event() {
+ new_test_ext(1).execute_with(|| {
+ EmissionTopSubnetAbsoluteLimit::::set(2);
+
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.1));
+ shares.insert(NetUid::from(2), u64f64(0.2));
+ shares.insert(NetUid::from(3), u64f64(0.3));
+ shares.insert(NetUid::from(4), u64f64(0.4));
+
+ System::reset_events();
+ SubtensorModule::apply_top_subnet_absolute_limit(&mut shares);
+
+ let events = System::events();
+ let found = events.iter().any(|e| {
+ matches!(
+ &e.event,
+ RuntimeEvent::SubtensorModule(Event::EmissionAbsoluteLimitApplied {
+ limit: 2,
+ before_count: 4,
+ })
+ )
+ });
+ assert!(
+ found,
+ "Expected EmissionAbsoluteLimitApplied event with limit=2, before_count=4"
+ );
+ });
+}
+
+#[test]
+fn test_apply_top_subnet_absolute_limit_no_event_when_within_limit() {
+ new_test_ext(1).execute_with(|| {
+ EmissionTopSubnetAbsoluteLimit::::set(5);
+
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.5));
+ shares.insert(NetUid::from(2), u64f64(0.5));
+
+ System::reset_events();
+ SubtensorModule::apply_top_subnet_absolute_limit(&mut shares);
+
+ // No event should be emitted when already within limit
+ let events = System::events();
+ let found = events.iter().any(|e| {
+ matches!(
+ &e.event,
+ RuntimeEvent::SubtensorModule(Event::EmissionAbsoluteLimitApplied { .. })
+ )
+ });
+ assert!(!found, "Should not emit event when within limit");
+ });
+}
+
+#[test]
+fn test_interaction_proportion_and_absolute_limit() {
+ new_test_ext(1).execute_with(|| {
+ // 50% proportion with 6 subnets -> ceil(6*0.5) = 3 subnets
+ // Absolute limit = 2 -> further reduces to 2 subnets
+ EmissionTopSubnetAbsoluteLimit::::set(2);
+ // EmissionTopSubnetProportion defaults to 5000 (50%)
+
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.05));
+ shares.insert(NetUid::from(2), u64f64(0.10));
+ shares.insert(NetUid::from(3), u64f64(0.15));
+ shares.insert(NetUid::from(4), u64f64(0.20));
+ shares.insert(NetUid::from(5), u64f64(0.25));
+ shares.insert(NetUid::from(6), u64f64(0.25));
+
+ // Apply proportion filter first (as in get_subnet_block_emissions)
+ SubtensorModule::apply_top_subnet_proportion_filter(&mut shares);
+
+ // After 50% filter: top 3 subnets (6, 5, 4) keep their shares
+ let nonzero_after_proportion = shares.values().filter(|v| v.to_num::() > 0.0).count();
+ assert_eq!(nonzero_after_proportion, 3, "50% of 6 subnets = top 3");
+
+ // Apply absolute limit
+ SubtensorModule::apply_top_subnet_absolute_limit(&mut shares);
+
+ // After absolute limit: only top 2 subnets
+ let nonzero_after_limit = shares.values().filter(|v| v.to_num::() > 0.0).count();
+ assert_eq!(
+ nonzero_after_limit, 2,
+ "Absolute limit of 2 should leave 2 subnets"
+ );
+
+ // Sum should be 1.0
+ let sum: f64 = shares.values().map(|v| v.to_num::()).sum();
+ assert_abs_diff_eq!(sum, 1.0, epsilon = 1e-9);
+ });
+}
+
+#[test]
+fn test_interaction_absolute_limit_stricter_than_proportion() {
+ new_test_ext(1).execute_with(|| {
+ // proportion = 100% (all subnets), absolute limit = 1
+ EmissionTopSubnetProportion::::set(10000);
+ EmissionTopSubnetAbsoluteLimit::::set(1);
+
+ let mut shares: BTreeMap = BTreeMap::new();
+ shares.insert(NetUid::from(1), u64f64(0.3));
+ shares.insert(NetUid::from(2), u64f64(0.3));
+ shares.insert(NetUid::from(3), u64f64(0.4));
+
+ // Apply both filters
+ SubtensorModule::apply_top_subnet_proportion_filter(&mut shares);
+ SubtensorModule::apply_top_subnet_absolute_limit(&mut shares);
+
+ // Only subnet 3 should survive (highest share)
+ let s1 = shares.get(&NetUid::from(1)).unwrap().to_num::();
+ let s2 = shares.get(&NetUid::from(2)).unwrap().to_num::();
+ let s3 = shares.get(&NetUid::from(3)).unwrap().to_num::();
+
+ assert_abs_diff_eq!(s1, 0.0, epsilon = 1e-12);
+ assert_abs_diff_eq!(s2, 0.0, epsilon = 1e-12);
+ assert_abs_diff_eq!(s3, 1.0, epsilon = 1e-9);
+ });
+}
diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs
index 8fca29f071..ce8b81dc3e 100644
--- a/pallets/subtensor/src/utils/misc.rs
+++ b/pallets/subtensor/src/utils/misc.rs
@@ -921,4 +921,9 @@ impl Pallet {
pub fn set_emission_top_subnet_proportion(proportion: u16) {
EmissionTopSubnetProportion::::set(proportion);
}
+
+ /// Sets the absolute maximum number of subnets that receive emission (0 = no limit).
+ pub fn set_emission_top_subnet_absolute_limit(limit: u16) {
+ EmissionTopSubnetAbsoluteLimit::::set(limit);
+ }
}
From 6f1c0244a60d81477f61427346279e21e753c37d Mon Sep 17 00:00:00 2001
From: Pawel Polewicz
Date: Sat, 31 Jan 2026 15:01:59 +0000
Subject: [PATCH 05/29] remove temporary plan.md file
---
PLAN.md | 344 --------------------------------------------------------
1 file changed, 344 deletions(-)
delete mode 100644 PLAN.md
diff --git a/PLAN.md b/PLAN.md
deleted file mode 100644
index 8c6a0704b4..0000000000
--- a/PLAN.md
+++ /dev/null
@@ -1,344 +0,0 @@
-# Implementation Plan: Emission Scaling and Subnet Limiting Hyperparameters
-
-## Overview
-
-Three new root-level hyperparameters for `get_subnet_block_emissions()` in `src/coinbase/subnet_emissions.rs`:
-
-1. **EffectiveRootPropEmissionScaling** (bool) - When enabled, multiply each subnet's emission share by its `EffectiveRootProp`
-2. **EmissionTopSubnetProportion** (u16, default 50% = 5000/10000) - Only top-K% of subnets by share receive emission
-3. **EmissionTopSubnetAbsoluteLimit** (u16, default 0 = disabled) - Hard cap on number of subnets receiving emission
-
-## Subfeature 1: EffectiveRootProp Storage & Computation
-
-### 1A. Storage declarations in `pallets/subtensor/src/lib.rs`
-
-Add near the existing flow-related storage items (~line 1489):
-
-```rust
-/// --- MAP ( netuid ) --> EffectiveRootProp for a subnet.
-/// Computed during epoch as:
-/// sum(RootAlphaDividendsPerSubnet[netuid]) /
-/// (sum(AlphaDividendsPerSubnet[netuid]) + sum(RootAlphaDividendsPerSubnet[netuid]))
-/// This measures the proportion of dividends on a subnet that go to root stakers.
-#[pallet::storage]
-pub type EffectiveRootProp =
- StorageMap<_, Identity, NetUid, U64F64, ValueQuery>;
-```
-
-### 1B. Compute and store EffectiveRootProp during epoch
-
-In `pallets/subtensor/src/coinbase/run_coinbase.rs`, inside `distribute_dividends_and_incentives()` (after the alpha dividends and root alpha dividends loops at ~line 638), add computation:
-
-```rust
-// After distributing both alpha_divs and root_alpha_divs, compute EffectiveRootProp
-let total_root_alpha_divs: U64F64 = /* sum of root_alpha values from root_alpha_dividends map */;
-let total_alpha_divs: U64F64 = /* sum of alpha_divs values from alpha_dividends map */;
-let total = total_root_alpha_divs + total_alpha_divs;
-let effective_root_prop = if total > 0 { total_root_alpha_divs / total } else { 0 };
-EffectiveRootProp::::insert(netuid, effective_root_prop);
-```
-
-Create a helper function `compute_and_store_effective_root_prop()` that takes netuid and the two dividend maps, computes the ratio, stores it, and returns the value. This keeps `distribute_dividends_and_incentives` clean.
-
-### 1C. Tests for EffectiveRootProp computation
-
-In `pallets/subtensor/src/tests/subnet_emissions.rs`:
-- Test that `EffectiveRootProp` is 0.0 when there are no root alpha dividends
-- Test that `EffectiveRootProp` is 1.0 when there are no alpha dividends (all root)
-- Test that `EffectiveRootProp` is ~0.5 when root and alpha dividends are equal
-- Test that correct values are stored per-subnet after `distribute_dividends_and_incentives` runs
-
-## Subfeature 2: EffectiveRootPropEmissionScaling Hyperparameter
-
-### 2A. Storage declaration in `pallets/subtensor/src/lib.rs`
-
-Add near the flow-related storage items:
-
-```rust
-#[pallet::type_value]
-pub fn DefaultEffectiveRootPropEmissionScaling() -> bool {
- false
-}
-#[pallet::storage]
-/// When enabled, multiply each subnet's emission share by its EffectiveRootProp
-pub type EffectiveRootPropEmissionScaling =
- StorageValue<_, bool, ValueQuery, DefaultEffectiveRootPropEmissionScaling>;
-```
-
-### 2B. Setter in `pallets/subtensor/src/utils/misc.rs`
-
-Add setter function following the pattern of `set_tao_flow_cutoff`:
-```rust
-pub fn set_effective_root_prop_emission_scaling(enabled: bool) {
- EffectiveRootPropEmissionScaling::::set(enabled);
-}
-```
-
-### 2C. Admin extrinsic in `pallets/admin-utils/src/lib.rs`
-
-Add `sudo_set_effective_root_prop_emission_scaling` with next available call_index (88):
-```rust
-#[pallet::call_index(88)]
-pub fn sudo_set_effective_root_prop_emission_scaling(
- origin: OriginFor,
- enabled: bool,
-) -> DispatchResult {
- ensure_root(origin)?;
- pallet_subtensor::Pallet::::set_effective_root_prop_emission_scaling(enabled);
- Ok(())
-}
-```
-
-### 2D. Apply scaling in `get_subnet_block_emissions()`
-
-In `pallets/subtensor/src/coinbase/subnet_emissions.rs`, modify `get_subnet_block_emissions()`:
-
-After `get_shares()` returns shares, add a step:
-1. If `EffectiveRootPropEmissionScaling` is enabled, multiply each share by `EffectiveRootProp::::get(netuid)`
-2. Re-normalize shares so they sum to 1.0
-
-Extract this into a helper function `apply_effective_root_prop_scaling(shares: &mut BTreeMap)`.
-
-### 2E. Event for scaling application
-
-Add an event in `pallets/subtensor/src/macros/events.rs`:
-```rust
-/// Emission shares have been adjusted by EffectiveRootProp scaling.
-EffectiveRootPropEmissionScalingApplied {
- /// Per-subnet shares after scaling (netuid, share)
- shares: Vec<(NetUid, u64)>,
-},
-```
-
-Emit this event after applying the scaling so tests can verify it.
-
-### 2F. Tests
-
-- Test that with scaling disabled, shares are unchanged
-- Test that with scaling enabled and known EffectiveRootProp values, shares are correctly multiplied and re-normalized
-- Test that event is emitted when scaling is applied
-- Test edge case: all EffectiveRootProp values are 0 (should result in equal shares or all zeros)
-- Test edge case: single subnet
-
-## Subfeature 3: EmissionTopSubnetProportion
-
-### 3A. Storage declaration in `pallets/subtensor/src/lib.rs`
-
-```rust
-#[pallet::type_value]
-pub fn DefaultEmissionTopSubnetProportion() -> u16 {
- 5000 // 50% = 5000/10000
-}
-#[pallet::storage]
-/// Proportion of subnets (by count, ranked by share) that receive emission.
-/// Value is in basis points: 5000 = 50%. Subnets outside top-K% get shares zeroed.
-/// Round up: ceil(count * proportion / 10000).
-pub type EmissionTopSubnetProportion =
- StorageValue<_, u16, ValueQuery, DefaultEmissionTopSubnetProportion>;
-```
-
-### 3B. Setter in `pallets/subtensor/src/utils/misc.rs`
-
-```rust
-pub fn set_emission_top_subnet_proportion(proportion: u16) {
- EmissionTopSubnetProportion::::set(proportion);
-}
-```
-
-### 3C. Admin extrinsic in `pallets/admin-utils/src/lib.rs`
-
-Add `sudo_set_emission_top_subnet_proportion` with call_index 89:
-```rust
-#[pallet::call_index(89)]
-pub fn sudo_set_emission_top_subnet_proportion(
- origin: OriginFor,
- proportion: u16,
-) -> DispatchResult {
- ensure_root(origin)?;
- ensure!(proportion <= 10000, Error::::InvalidValue);
- ensure!(proportion > 0, Error::::InvalidValue);
- pallet_subtensor::Pallet::::set_emission_top_subnet_proportion(proportion);
- Ok(())
-}
-```
-
-### 3D. Apply in `get_subnet_block_emissions()`
-
-After getting shares (and after EffectiveRootProp scaling if enabled):
-
-1. Sort subnets by share descending
-2. Compute K = ceil(total_subnets * proportion / 10000)
-3. Zero out shares for subnets not in top-K
-4. Re-normalize remaining shares to sum to 1.0
-
-Extract into helper function `apply_top_subnet_proportion_filter(shares: &mut BTreeMap)`.
-
-### 3E. Event
-
-```rust
-/// Subnet emission shares zeroed for subnets outside top proportion.
-EmissionTopSubnetFilterApplied {
- /// Number of subnets that kept emission
- top_k: u16,
- /// Total number of subnets considered
- total: u16,
-},
-```
-
-### 3F. Tests
-
-- Test default 50%: 4 subnets -> top 2 get emission (ceil(4*0.5)=2)
-- Test default 50%: 1 subnet -> still gets emission (ceil(1*0.5)=1)
-- Test default 50%: 3 subnets -> top 2 get emission (ceil(3*0.5)=2)
-- Test 100%: all subnets get emission
-- Test that shares are re-normalized after filtering
-- Test that event is emitted with correct top_k and total
-- Test that zeroed subnets get zero emission in final output
-
-## Subfeature 4: EmissionTopSubnetAbsoluteLimit
-
-### 4A. Storage declaration in `pallets/subtensor/src/lib.rs`
-
-```rust
-#[pallet::type_value]
-pub fn DefaultEmissionTopSubnetAbsoluteLimit() -> u16 {
- 0 // 0 means no limit
-}
-#[pallet::storage]
-/// Absolute maximum number of subnets that can receive emission.
-/// 0 means no limit (disabled). When set, only the top N subnets by share receive emission.
-pub type EmissionTopSubnetAbsoluteLimit =
- StorageValue<_, u16, ValueQuery, DefaultEmissionTopSubnetAbsoluteLimit>;
-```
-
-### 4B. Setter in `pallets/subtensor/src/utils/misc.rs`
-
-```rust
-pub fn set_emission_top_subnet_absolute_limit(limit: u16) {
- EmissionTopSubnetAbsoluteLimit::::set(limit);
-}
-```
-
-### 4C. Admin extrinsic in `pallets/admin-utils/src/lib.rs`
-
-Add `sudo_set_emission_top_subnet_absolute_limit` with call_index 90:
-```rust
-#[pallet::call_index(90)]
-pub fn sudo_set_emission_top_subnet_absolute_limit(
- origin: OriginFor,
- limit: u16,
-) -> DispatchResult {
- ensure_root(origin)?;
- pallet_subtensor::Pallet::