Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions pallets/admin-utils/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,12 @@ mod benchmarks {
_(RawOrigin::Root, U64F64::from_num(0.61));
}

#[benchmark]
fn sudo_set_emission_bar_rank() {
#[extrinsic_call]
_(RawOrigin::Root, 64u16);
}

#[benchmark]
fn sudo_set_emission_gate_exponent() {
#[extrinsic_call]
Expand Down
16 changes: 16 additions & 0 deletions pallets/admin-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2036,6 +2036,22 @@ pub mod pallet {
Ok(())
}

/// Sets the emission bar rank (N): when non-zero, the emission gate bar
/// (theta) is pinned to the Nth-largest demand share instead of the
/// q-mass quantile, so the eligible set tracks rank N as the demand
/// distribution shifts. Setting 0 restores quantile mode. Also forces a
/// bar recompute on the next block so the change takes effect
/// immediately.
#[pallet::call_index(102)]
#[pallet::weight(<T as Config>::WeightInfo::sudo_set_emission_bar_rank())]
pub fn sudo_set_emission_bar_rank(origin: OriginFor<T>, rank: u16) -> DispatchResult {
ensure_root(origin)?;

pallet_subtensor::Pallet::<T>::set_emission_bar_rank(rank);
log::debug!("set_emission_bar_rank( {rank:?} ) ");
Ok(())
}

/// Sets the emission gate Hill exponent (h): cliff sharpness at the bar.
#[pallet::call_index(101)]
#[pallet::weight(<T as Config>::WeightInfo::sudo_set_emission_gate_exponent())]
Expand Down
25 changes: 25 additions & 0 deletions pallets/admin-utils/src/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ pub trait WeightInfo {
fn sudo_set_tao_flow_cutoff() -> Weight;
fn sudo_set_tao_flow_normalization_exponent() -> Weight;
fn sudo_set_emission_bar_quantile() -> Weight;
fn sudo_set_emission_bar_rank() -> Weight;
fn sudo_set_emission_gate_exponent() -> Weight;
fn sudo_set_tao_flow_smoothing_factor() -> Weight;
fn sudo_set_net_tao_flow_enabled() -> Weight;
Expand Down Expand Up @@ -1332,6 +1333,18 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
Weight::from_parts(2_000_000, 0)
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: `SubtensorModule::EmissionGateBar` (r:0 w:1)
/// Proof: `SubtensorModule::EmissionGateBar` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `SubtensorModule::EmissionBarRank` (r:0 w:1)
/// Proof: `SubtensorModule::EmissionBarRank` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
fn sudo_set_emission_bar_rank() -> Weight {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `0`
// Minimum execution time: 1_000_000 picoseconds.
Weight::from_parts(2_000_000, 0)
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: `SubtensorModule::EmissionGateExponent` (r:0 w:1)
/// Proof: `SubtensorModule::EmissionGateExponent` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
fn sudo_set_emission_gate_exponent() -> Weight {
Expand Down Expand Up @@ -2705,6 +2718,18 @@ impl WeightInfo for () {
Weight::from_parts(2_000_000, 0)
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: `SubtensorModule::EmissionGateBar` (r:0 w:1)
/// Proof: `SubtensorModule::EmissionGateBar` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `SubtensorModule::EmissionBarRank` (r:0 w:1)
/// Proof: `SubtensorModule::EmissionBarRank` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
fn sudo_set_emission_bar_rank() -> Weight {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `0`
// Minimum execution time: 1_000_000 picoseconds.
Weight::from_parts(2_000_000, 0)
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: `SubtensorModule::EmissionGateExponent` (r:0 w:1)
/// Proof: `SubtensorModule::EmissionGateExponent` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
fn sudo_set_emission_gate_exponent() -> Weight {
Expand Down
49 changes: 34 additions & 15 deletions pallets/subtensor/src/coinbase/subnet_emissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,11 +400,18 @@ impl<T: Config> Pallet<T> {

/// Recomputes the emission gate bar (theta) when due.
///
/// Theta is the q-mass bar: sort demand shares descending and accumulate
/// until the running total crosses `EmissionBarQuantile` (q); the share at
/// the crossing is the bar. Subnets above the bar collectively carry q of
/// demand. Because theta is a property of the demand distribution (not the
/// slot count), registering empty subnets does not move it.
/// Two selection modes, both properties of the demand distribution over
/// *positive* shares, so registering empty subnets does not move the bar:
///
/// * Rank mode (`EmissionBarRank` N > 0): theta is the Nth-largest demand
/// share, so exactly the top N subnets sit at or above the gate midpoint
/// regardless of how the distribution shifts. If fewer than N subnets
/// have demand, theta is the smallest positive share (everyone passes).
///
/// * q-mass mode (N == 0): sort demand shares descending and accumulate
/// until the running total crosses `EmissionBarQuantile` (q); the share
/// at the crossing is the bar. Subnets above the bar collectively carry
/// q of demand.
fn maybe_update_emission_gate_bar(shares: &BTreeMap<NetUid, U64F64>) {
let zero = U64F64::saturating_from_num(0);
let current_bar = EmissionGateBar::<T>::get();
Expand All @@ -417,23 +424,35 @@ impl<T: Config> Pallet<T> {
return;
}

let q = EmissionBarQuantile::<T>::get();
let mut sorted: Vec<U64F64> = shares.values().copied().collect();
sorted.sort_unstable_by(|a, b| b.cmp(a));

let mut cumulative = zero;
let mut theta = zero;
for share in sorted {
cumulative = cumulative.saturating_add(share);
theta = share;
if cumulative >= q {
break;
let rank = EmissionBarRank::<T>::get();
let theta = if rank > 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] Add tests for the new economic rank path

This introduces a new emission-selection formula without dedicated tests. Existing gate tests only exercise the unchanged quantile path. Add coverage proving the selected theta for rank 1 and an interior rank, the fallback when rank exceeds the number of positive shares, zero-share handling, ties at the boundary, rank 0 compatibility, and that the root setter stores the rank and invalidates EmissionGateBar. Economic logic requires explicit boundary coverage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] Add tests for the new economic rank path

This remains untested: the added test changes all explicitly set EmissionBarRank to 0, so none execute this branch even though rank 64 is now the runtime default. Add focused coverage proving selection at rank 1 and an interior rank, fallback when rank exceeds the positive-share count, exclusion of zero shares, and recomputation after sudo_set_emission_bar_rank clears the cached bar. This changes emission allocation at upgrade and requires boundary tests.

sorted
.iter()
.filter(|share| **share > zero)
.nth(usize::from(rank).saturating_sub(1))
.or_else(|| sorted.iter().filter(|share| **share > zero).next_back())
.copied()
.unwrap_or(zero)
} else {
let q = EmissionBarQuantile::<T>::get();
let mut cumulative = zero;
let mut crossing = zero;
for share in sorted {
cumulative = cumulative.saturating_add(share);
crossing = share;
if cumulative >= q {
break;
}
}
}
crossing
};

if theta > zero {
EmissionGateBar::<T>::put(theta);
log::debug!("Emission gate bar updated: theta = {theta:?} (q = {q:?})");
log::debug!("Emission gate bar updated: theta = {theta:?} (rank = {rank:?})");
}
}

Expand Down
13 changes: 13 additions & 0 deletions pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,19 @@ pub mod pallet {
pub type EmissionBarQuantile<T: Config> =
StorageValue<_, U64F64, ValueQuery, DefaultEmissionBarQuantile<T>>;

#[pallet::type_value]
/// Default emission bar rank (N). N > 0 pins theta to the Nth-largest
/// demand share, so the eligible set tracks rank N as the distribution
/// shifts instead of drifting with a fixed q. 0 disables rank mode and
/// the bar falls back to the q-mass quantile.
pub fn DefaultEmissionBarRank<T: Config>() -> u16 {
64
}
#[pallet::storage]
/// ITEM --> Emission Bar Rank (N). When non-zero, overrides the quantile.
pub type EmissionBarRank<T: Config> =
StorageValue<_, u16, ValueQuery, DefaultEmissionBarRank<T>>;

#[pallet::type_value]
/// Default emission gate Hill exponent (h). Controls cliff sharpness at the bar.
pub fn DefaultEmissionGateExponent<T: Config>() -> U64F64 {
Expand Down
6 changes: 5 additions & 1 deletion pallets/subtensor/src/macros/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,11 @@ mod hooks {
// Remove orphan SubnetIdentitiesV3 entries left for recycled netuids.
.saturating_add(migrations::migrate_clear_orphan_subnet_identities_v3::migrate_clear_orphan_subnet_identities_v3::<T>())
// Backfill ColdkeyCollateralHotkeys from standing MinerCollateral rows.
.saturating_add(migrations::migrate_coldkey_collateral_hotkeys::migrate_coldkey_collateral_hotkeys::<T>());
.saturating_add(migrations::migrate_coldkey_collateral_hotkeys::migrate_coldkey_collateral_hotkeys::<T>())
// Kill the stale quantile-derived emission gate bar so the
// rank-64 bar (DefaultEmissionBarRank) applies from the first
// recompute after the upgrade instead of the next cadence boundary.
.saturating_add(migrations::migrate_reset_emission_gate_bar::migrate_reset_emission_gate_bar::<T>());
weight
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use super::*;
use frame_support::{traits::Get, weights::Weight};
use log;
use scale_info::prelude::string::String;

/// Kills the emission gate bar so it is recomputed on the first block after
/// the upgrade. Without this, the stale quantile-derived theta would keep
/// gating emissions for up to EMISSION_BAR_UPDATE_INTERVAL blocks before the
/// new rank-64 bar (DefaultEmissionBarRank) takes effect.
pub fn migrate_reset_emission_gate_bar<T: Config>() -> Weight {
let mig_name: Vec<u8> = b"reset_emission_gate_bar_rank_64".to_vec();

// 1 read: HasMigrationRun flag
let mut total_weight = T::DbWeight::get().reads(1);

// Run once guard
if HasMigrationRun::<T>::get(&mig_name) {
log::info!(
"Migration '{}' already executed - skipping",
String::from_utf8_lossy(&mig_name)
);
return total_weight;
}
log::info!("Running migration '{}'", String::from_utf8_lossy(&mig_name));

EmissionGateBar::<T>::kill();
total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1));

// Mark as done
HasMigrationRun::<T>::insert(&mig_name, true);
total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1));

log::info!(
"Migration '{}' completed",
String::from_utf8_lossy(&mig_name)
);
total_weight
}
1 change: 1 addition & 0 deletions pallets/subtensor/src/migrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub mod migrate_remove_unknown_neuron_axon_cert_prom;
pub mod migrate_remove_unused_maps_and_values;
pub mod migrate_remove_zero_total_hotkey_alpha;
pub mod migrate_reset_bonds_moving_average;
pub mod migrate_reset_emission_gate_bar;
pub mod migrate_reset_max_burn;
pub mod migrate_reset_tnet_conviction_locks;
pub mod migrate_reset_unactive_sn;
Expand Down
4 changes: 4 additions & 0 deletions pallets/subtensor/src/tests/coinbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ fn test_coinbase_tao_issuance_different_prices() {
// so it should receive twice the TAO emission.
SubnetMovingPrice::<Test>::insert(netuid1, I96F32::from_num(0.1));
SubnetMovingPrice::<Test>::insert(netuid2, I96F32::from_num(0.2));
// Pin to q-mass (quantile) mode; this test asserts quantile gate math.
EmissionBarRank::<Test>::set(0);
// Keep root_proportion ~1 so the injection cap does not bind.
set_full_injection_root_stake();

Expand Down Expand Up @@ -670,6 +672,8 @@ fn test_coinbase_alpha_issuance_different() {
// Price-based shares with prices 1 and 2 (1:2 ratio).
SubnetMovingPrice::<Test>::insert(netuid1, I96F32::from_num(1));
SubnetMovingPrice::<Test>::insert(netuid2, I96F32::from_num(2));
// Pin to q-mass (quantile) mode; this test asserts quantile gate math.
EmissionBarRank::<Test>::set(0);
// Keep root_proportion ~1 so the injection cap does not bind.
set_full_injection_root_stake();
// Run coinbase
Expand Down
30 changes: 30 additions & 0 deletions pallets/subtensor/src/tests/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5165,3 +5165,33 @@ fn test_migrate_dynamic_tempo_idempotent() {
);
});
}

#[test]
fn test_migrate_reset_emission_gate_bar() {
new_test_ext(1).execute_with(|| {
const MIG_NAME: &[u8] = b"reset_emission_gate_bar_rank_64";

// Pre-state: a stale quantile-derived bar is in place.
EmissionGateBar::<Test>::put(U64F64::from_num(0.009));
assert!(
!HasMigrationRun::<Test>::get(MIG_NAME.to_vec()),
"migration flag should be false before run"
);

let w = crate::migrations::migrate_reset_emission_gate_bar::migrate_reset_emission_gate_bar::<Test>();
assert!(!w.is_zero(), "weight must be non-zero");

// The stale bar is killed so the first recompute after the upgrade
// rebuilds it under the rank-64 default.
assert_eq!(EmissionGateBar::<Test>::get(), U64F64::from_num(0));
assert!(
HasMigrationRun::<Test>::get(MIG_NAME.to_vec()),
"migration flag not set"
);

// Second run is a no-op: a freshly recomputed bar survives.
EmissionGateBar::<Test>::put(U64F64::from_num(0.003));
crate::migrations::migrate_reset_emission_gate_bar::migrate_reset_emission_gate_bar::<Test>();
assert_eq!(EmissionGateBar::<Test>::get(), U64F64::from_num(0.003));
});
}
Loading
Loading