diff --git a/pallets/admin-utils/src/benchmarking.rs b/pallets/admin-utils/src/benchmarking.rs index 8832654a75..5ae8d3a51a 100644 --- a/pallets/admin-utils/src/benchmarking.rs +++ b/pallets/admin-utils/src/benchmarking.rs @@ -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] diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 25877b7820..5d9f9ffb42 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -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(::WeightInfo::sudo_set_emission_bar_rank())] + pub fn sudo_set_emission_bar_rank(origin: OriginFor, rank: u16) -> DispatchResult { + ensure_root(origin)?; + + pallet_subtensor::Pallet::::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(::WeightInfo::sudo_set_emission_gate_exponent())] diff --git a/pallets/admin-utils/src/weights.rs b/pallets/admin-utils/src/weights.rs index 28e4a61c13..15497f848d 100644 --- a/pallets/admin-utils/src/weights.rs +++ b/pallets/admin-utils/src/weights.rs @@ -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; @@ -1332,6 +1333,18 @@ impl WeightInfo for SubstrateWeight { 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 { @@ -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 { diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 02b01a45a5..16cf4c60b2 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -400,11 +400,18 @@ impl Pallet { /// 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) { let zero = U64F64::saturating_from_num(0); let current_bar = EmissionGateBar::::get(); @@ -417,23 +424,35 @@ impl Pallet { return; } - let q = EmissionBarQuantile::::get(); let mut sorted: Vec = 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::::get(); + let theta = if rank > 0 { + 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::::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::::put(theta); - log::debug!("Emission gate bar updated: theta = {theta:?} (q = {q:?})"); + log::debug!("Emission gate bar updated: theta = {theta:?} (rank = {rank:?})"); } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 3cf4318e9d..90addf952a 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1859,6 +1859,19 @@ pub mod pallet { pub type EmissionBarQuantile = StorageValue<_, U64F64, ValueQuery, DefaultEmissionBarQuantile>; + #[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() -> u16 { + 64 + } + #[pallet::storage] + /// ITEM --> Emission Bar Rank (N). When non-zero, overrides the quantile. + pub type EmissionBarRank = + StorageValue<_, u16, ValueQuery, DefaultEmissionBarRank>; + #[pallet::type_value] /// Default emission gate Hill exponent (h). Controls cliff sharpness at the bar. pub fn DefaultEmissionGateExponent() -> U64F64 { diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 6d3692d9a2..cd57922bfd 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -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::()) // Backfill ColdkeyCollateralHotkeys from standing MinerCollateral rows. - .saturating_add(migrations::migrate_coldkey_collateral_hotkeys::migrate_coldkey_collateral_hotkeys::()); + .saturating_add(migrations::migrate_coldkey_collateral_hotkeys::migrate_coldkey_collateral_hotkeys::()) + // 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::()); weight } diff --git a/pallets/subtensor/src/migrations/migrate_reset_emission_gate_bar.rs b/pallets/subtensor/src/migrations/migrate_reset_emission_gate_bar.rs new file mode 100644 index 0000000000..6b3e9d1db2 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_reset_emission_gate_bar.rs @@ -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() -> Weight { + let mig_name: Vec = 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::::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::::kill(); + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + + // Mark as done + HasMigrationRun::::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 +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index 63a7ec4439..d54071cfa5 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -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; diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 4a54157b63..7d17aa35db 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -386,6 +386,8 @@ fn test_coinbase_tao_issuance_different_prices() { // so it should receive twice the TAO emission. SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.1)); SubnetMovingPrice::::insert(netuid2, I96F32::from_num(0.2)); + // Pin to q-mass (quantile) mode; this test asserts quantile gate math. + EmissionBarRank::::set(0); // Keep root_proportion ~1 so the injection cap does not bind. set_full_injection_root_stake(); @@ -670,6 +672,8 @@ fn test_coinbase_alpha_issuance_different() { // Price-based shares with prices 1 and 2 (1:2 ratio). SubnetMovingPrice::::insert(netuid1, I96F32::from_num(1)); SubnetMovingPrice::::insert(netuid2, I96F32::from_num(2)); + // Pin to q-mass (quantile) mode; this test asserts quantile gate math. + EmissionBarRank::::set(0); // Keep root_proportion ~1 so the injection cap does not bind. set_full_injection_root_stake(); // Run coinbase diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index c9d8b51b9b..e35fcd4685 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -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::::put(U64F64::from_num(0.009)); + assert!( + !HasMigrationRun::::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::(); + 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::::get(), U64F64::from_num(0)); + assert!( + HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag not set" + ); + + // Second run is a no-op: a freshly recomputed bar survives. + EmissionGateBar::::put(U64F64::from_num(0.003)); + crate::migrations::migrate_reset_emission_gate_bar::migrate_reset_emission_gate_bar::(); + assert_eq!(EmissionGateBar::::get(), U64F64::from_num(0.003)); + }); +} diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs index 846fa78def..69e48b4b15 100644 --- a/pallets/subtensor/src/tests/subnet_emissions.rs +++ b/pallets/subtensor/src/tests/subnet_emissions.rs @@ -244,6 +244,9 @@ fn emission_gate_concentrates_1_to_2_price_split() { let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + // Pin to q-mass (quantile) mode; this test asserts quantile bar selection. + EmissionBarRank::::set(0); + System::set_block_number(0); SubnetMovingPrice::::insert(n1, i96f32(1.0)); SubnetMovingPrice::::insert(n2, i96f32(2.0)); @@ -307,6 +310,9 @@ fn emission_gate_bar_update_cadence() { let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); let n3 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + // Pin to q-mass (quantile) mode; this test asserts quantile bar selection. + EmissionBarRank::::set(0); + System::set_block_number(0); // Shares 0.7, 0.2, 0.1. q = 0.61 crosses on the first → theta = 0.7. SubnetMovingPrice::::insert(n1, i96f32(7.0)); @@ -344,6 +350,188 @@ fn emission_gate_bar_update_cadence() { }); } +/// The upgrade-active default: rank mode at N = 64. +#[test] +fn emission_bar_rank_default_is_64() { + new_test_ext(1).execute_with(|| { + assert_eq!(EmissionBarRank::::get(), 64); + }); +} + +/// Rank mode pins theta to the Nth-largest share: five subnets with distinct +/// prices 5:4:3:2:1 and rank 3 → theta is the 3rd-largest share (3/15). +#[test] +fn emission_bar_rank_selects_nth_largest_share() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(86); + let owner_coldkey = U256::from(87); + let nets: Vec = (0..5) + .map(|_| add_dynamic_network(&owner_hotkey, &owner_coldkey)) + .collect(); + + EmissionBarRank::::set(3); + + System::set_block_number(0); + for (i, n) in nets.iter().enumerate() { + SubnetMovingPrice::::insert(n, i96f32((5 - i) as f64)); + MinerBurned::::insert(n, U96F32::saturating_from_num(0.0)); + } + + let _ = SubtensorModule::get_shares(&nets); + assert_abs_diff_eq!( + EmissionGateBar::::get().to_num::(), + 3.0 / 15.0, + epsilon = 1e-9 + ); + }); +} + +/// Zero-demand subnets are excluded from rank selection: prices 3:2:0:1 with +/// rank 3 → theta is the 3rd-largest positive share (1/6), not the zero. +#[test] +fn emission_bar_rank_ignores_zero_shares() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(88); + let owner_coldkey = U256::from(89); + let nets: Vec = (0..4) + .map(|_| add_dynamic_network(&owner_hotkey, &owner_coldkey)) + .collect(); + + EmissionBarRank::::set(3); + + System::set_block_number(0); + for (n, price) in nets.iter().zip([3.0, 2.0, 0.0, 1.0]) { + SubnetMovingPrice::::insert(n, i96f32(price)); + MinerBurned::::insert(n, U96F32::saturating_from_num(0.0)); + } + + let _ = SubtensorModule::get_shares(&nets); + assert_abs_diff_eq!( + EmissionGateBar::::get().to_num::(), + 1.0 / 6.0, + epsilon = 1e-9 + ); + }); +} + +/// Ties at the bar: prices 3:2:2:1 with rank 3 → theta = 2/8; both tied +/// subnets sit exactly at the gate midpoint. +#[test] +fn emission_bar_rank_ties_at_bar() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(90); + let owner_coldkey = U256::from(91); + let nets: Vec = (0..4) + .map(|_| add_dynamic_network(&owner_hotkey, &owner_coldkey)) + .collect(); + + EmissionBarRank::::set(3); + + System::set_block_number(0); + for (n, price) in nets.iter().zip([3.0, 2.0, 2.0, 1.0]) { + SubnetMovingPrice::::insert(n, i96f32(price)); + MinerBurned::::insert(n, U96F32::saturating_from_num(0.0)); + } + + let _ = SubtensorModule::get_shares(&nets); + assert_abs_diff_eq!( + EmissionGateBar::::get().to_num::(), + 2.0 / 8.0, + epsilon = 1e-9 + ); + }); +} + +/// The mainnet-default path: rank 64 with fewer than 64 subnets falls back to +/// the smallest positive share, so every subnet passes at or above the gate +/// midpoint and no emission is stranded. +#[test] +fn emission_bar_rank_fewer_subnets_than_rank_falls_back_to_smallest() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(92); + let owner_coldkey = U256::from(93); + let nets: Vec = (0..3) + .map(|_| add_dynamic_network(&owner_hotkey, &owner_coldkey)) + .collect(); + + // Deliberately no EmissionBarRank override: exercise the default (64). + assert_eq!(EmissionBarRank::::get(), 64); + + System::set_block_number(0); + for (n, price) in nets.iter().zip([5.0, 3.0, 2.0]) { + SubnetMovingPrice::::insert(n, i96f32(price)); + MinerBurned::::insert(n, U96F32::saturating_from_num(0.0)); + } + + let shares = SubtensorModule::get_shares(&nets); + + // theta = smallest positive share (2/10). + assert_abs_diff_eq!( + EmissionGateBar::::get().to_num::(), + 2.0 / 10.0, + epsilon = 1e-9 + ); + + // Everyone passes: each post-gate share is at least half its pre-gate + // linear share (gate ≥ 1/2 for s ≥ theta), and mass is fully allocated. + let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); + assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-9); + for (n, linear) in nets.iter().zip([0.5, 0.3, 0.2]) { + let gated = shares.get(n).copied().unwrap().to_num::(); + assert!( + gated >= linear * 0.5 / (0.5 + 0.3 + 0.2), + "subnet {n:?} gated share {gated} fell below the everyone-passes floor" + ); + } + }); +} + +/// The setter stores the new rank and kills the bar, forcing a recompute on +/// the next shares evaluation even mid-cadence-interval. +#[test] +fn set_emission_bar_rank_updates_and_forces_recompute() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(94); + let owner_coldkey = U256::from(95); + let nets: Vec = (0..3) + .map(|_| add_dynamic_network(&owner_hotkey, &owner_coldkey)) + .collect(); + + System::set_block_number(0); + for (n, price) in nets.iter().zip([5.0, 3.0, 2.0]) { + SubnetMovingPrice::::insert(n, i96f32(price)); + MinerBurned::::insert(n, U96F32::saturating_from_num(0.0)); + } + + // Establish a bar under the default rank (64 → smallest share, 0.2). + let _ = SubtensorModule::get_shares(&nets); + assert_abs_diff_eq!( + EmissionGateBar::::get().to_num::(), + 0.2_f64, + epsilon = 1e-9 + ); + + // Change the rank mid-interval: the bar is killed immediately... + SubtensorModule::set_emission_bar_rank(1); + assert_eq!(EmissionBarRank::::get(), 1); + assert_abs_diff_eq!( + EmissionGateBar::::get().to_num::(), + 0.0_f64, + epsilon = 1e-18 + ); + + // ...and the next evaluation recomputes with the new rank even though + // block 179 is not a cadence boundary (rank 1 → largest share, 0.5). + System::set_block_number(179); + let _ = SubtensorModule::get_shares(&nets); + assert_abs_diff_eq!( + EmissionGateBar::::get().to_num::(), + 0.5_f64, + epsilon = 1e-9 + ); + }); +} + // /// Normal (moderate, non-zero) EMA flows across 3 subnets. // /// Expect: shares sum to ~1 and are monotonic with flows. // #[test] diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index cc60a933ea..e7e947485c 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -995,6 +995,13 @@ impl Pallet { EmissionGateBar::::kill(); } + /// Sets the emission bar rank (N). 0 restores q-mass (quantile) mode. + pub fn set_emission_bar_rank(rank: u16) { + EmissionBarRank::::set(rank); + // Force a bar recompute with the new rank on the next block. + EmissionGateBar::::kill(); + } + /// Sets the emission gate Hill exponent (h) pub fn set_emission_gate_exponent(exponent: U64F64) { EmissionGateExponent::::set(exponent); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 95eaca16ee..521906bb58 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -235,7 +235,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 440, + spec_version: 441, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/src/proxy_filters/call_groups.rs b/runtime/src/proxy_filters/call_groups.rs index 213b9be332..24bcf0ada6 100644 --- a/runtime/src/proxy_filters/call_groups.rs +++ b/runtime/src/proxy_filters/call_groups.rs @@ -581,6 +581,9 @@ call_filter_group!( RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_min_non_immune_uids), RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_tao_flow_cutoff), RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_tao_flow_normalization_exponent), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_emission_bar_quantile), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_emission_bar_rank), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_emission_gate_exponent), RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_tao_flow_smoothing_factor), RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_net_tao_flow_enabled), RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_max_mechanism_count), diff --git a/sdk/python/bittensor/_generated/calls.py b/sdk/python/bittensor/_generated/calls.py index 7a70fa3db2..02cb5c684a 100644 --- a/sdk/python/bittensor/_generated/calls.py +++ b/sdk/python/bittensor/_generated/calls.py @@ -1,7 +1,7 @@ """Generated from runtime metadata by codegen. DO NOT EDIT BY HAND. Regenerate with: python -m codegen -Spec version: 440 +Spec version: 441 """ from typing import Any, NamedTuple @@ -1002,6 +1002,11 @@ def sudo_set_emission_bar_quantile(quantile: 'FixedU128') -> Call: 'Sets the emission bar quantile (q): the fraction of demand carried by subnets above the emission gate bar. Also forces a bar recompute on the next block so the new quantile takes effect immediately.' return Call('AdminUtils', 'sudo_set_emission_bar_quantile', {'quantile': quantile}) + @staticmethod + def sudo_set_emission_bar_rank(rank: 'u16') -> Call: + '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.' + return Call('AdminUtils', 'sudo_set_emission_bar_rank', {'rank': rank}) + @staticmethod def sudo_set_emission_gate_exponent(exponent: 'FixedU128') -> Call: 'Sets the emission gate Hill exponent (h): cliff sharpness at the bar.' diff --git a/sdk/python/bittensor/_generated/constants.py b/sdk/python/bittensor/_generated/constants.py index 6b90f809d8..2676148cfa 100644 --- a/sdk/python/bittensor/_generated/constants.py +++ b/sdk/python/bittensor/_generated/constants.py @@ -1,7 +1,7 @@ """Generated from runtime metadata by codegen. DO NOT EDIT BY HAND. Regenerate with: python -m codegen -Spec version: 440 +Spec version: 441 Pallet constant descriptors: unpack into substrate.constant. """ diff --git a/sdk/python/bittensor/_generated/errors.py b/sdk/python/bittensor/_generated/errors.py index fabd28868f..57914a9782 100644 --- a/sdk/python/bittensor/_generated/errors.py +++ b/sdk/python/bittensor/_generated/errors.py @@ -1,7 +1,7 @@ """Generated from runtime metadata by codegen. DO NOT EDIT BY HAND. Regenerate with: python -m codegen -Spec version: 440 +Spec version: 441 """ from dataclasses import dataclass diff --git a/sdk/python/bittensor/_generated/runtime_apis.py b/sdk/python/bittensor/_generated/runtime_apis.py index c132a56001..f3c1c5acbe 100644 --- a/sdk/python/bittensor/_generated/runtime_apis.py +++ b/sdk/python/bittensor/_generated/runtime_apis.py @@ -1,7 +1,7 @@ """Generated from runtime metadata by codegen. DO NOT EDIT BY HAND. Regenerate with: python -m codegen -Spec version: 440 +Spec version: 441 Runtime API method descriptors: unpack into substrate.runtime_call. """ diff --git a/sdk/python/bittensor/_generated/storage.py b/sdk/python/bittensor/_generated/storage.py index 5b7807db67..da4e743336 100644 --- a/sdk/python/bittensor/_generated/storage.py +++ b/sdk/python/bittensor/_generated/storage.py @@ -1,7 +1,7 @@ """Generated from runtime metadata by codegen. DO NOT EDIT BY HAND. Regenerate with: python -m codegen -Spec version: 440 +Spec version: 441 Storage item descriptors: unpack into substrate.query/query_map. Each carries its VALUE's type identity (value_type_ident) so normalization can key on the runtime's own type names without a node round-trip. """ @@ -156,6 +156,7 @@ class SubtensorModule: TaoFlowCutoff = Item('SubtensorModule', 'TaoFlowCutoff', 'FixedI128') FlowNormExponent = Item('SubtensorModule', 'FlowNormExponent', 'FixedU128') EmissionBarQuantile = Item('SubtensorModule', 'EmissionBarQuantile', 'FixedU128') + EmissionBarRank = Item('SubtensorModule', 'EmissionBarRank', 'u16') EmissionGateExponent = Item('SubtensorModule', 'EmissionGateExponent', 'FixedU128') EmissionGateBar = Item('SubtensorModule', 'EmissionGateBar', 'FixedU128') FlowEmaSmoothingFactor = Item('SubtensorModule', 'FlowEmaSmoothingFactor', 'u64') diff --git a/sdk/python/codegen/check.py b/sdk/python/codegen/check.py index cdf1de5e6b..e0dd9818ea 100644 --- a/sdk/python/codegen/check.py +++ b/sdk/python/codegen/check.py @@ -231,6 +231,10 @@ def check_drift(endpoint: str) -> int: "sudo_set_difficulty", "sudo_set_dissolve_network_schedule_duration", "sudo_set_ema_price_halving_period", + # emission gate tuning (v440/v441) — root-only, no semantic wrapper + "sudo_set_emission_bar_quantile", + "sudo_set_emission_bar_rank", + "sudo_set_emission_gate_exponent", "sudo_set_evm_chain_id", "sudo_set_kappa", "sudo_set_lock_reduction_interval",