diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 8d1d735052..8d6e736ec8 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -2279,6 +2279,69 @@ pub mod pallet { log::trace!("ColdkeySwapReannouncementDelaySet( duration: {duration:?} )"); Ok(()) } + + /// Sets EffectiveRootProp emission scaling on/off + #[pallet::call_index(91)] + #[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); + Ok(()) + } + + /// Sets the proportion of top subnets that receive emission. + /// `proportion_ppm` is in parts-per-million: 1_000_000 = 100%, 500_000 = 50%, etc. + /// Must be in range (0, 1_000_000]. + #[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_ppm: u64, + ) -> DispatchResult { + ensure_root(origin)?; + ensure!( + proportion_ppm > 0 && proportion_ppm <= 1_000_000, + Error::::InvalidValue + ); + let prop = U64F64::saturating_from_num(proportion_ppm) + .saturating_div(U64F64::saturating_from_num(1_000_000)); + pallet_subtensor::Pallet::::set_emission_top_subnet_proportion(prop); + Ok(()) + } + + /// Sets the absolute-limit cutoff for subnets receiving emission (None = no limit). + /// Ties at the cutoff are included, so the number of nonzero subnets may exceed N. + #[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: Option, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_emission_top_subnet_absolute_limit(limit); + Ok(()) + } } } diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 83567b6f57..6ffe1035dd 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -363,12 +363,18 @@ impl Pallet { StakeWeight::::remove(netuid); LoadedEmission::::remove(netuid); + // --- 18b. Root prop / utilization. + EffectiveRootProp::::remove(netuid); + RootProp::::remove(netuid); + RootClaimableThreshold::::remove(netuid); + // --- 19. DMAPs where netuid is the FIRST key: clear by prefix. let _ = BlockAtRegistration::::clear_prefix(netuid, u32::MAX, None); let _ = Axons::::clear_prefix(netuid, u32::MAX, None); let _ = NeuronCertificates::::clear_prefix(netuid, u32::MAX, None); let _ = Prometheus::::clear_prefix(netuid, u32::MAX, None); let _ = AlphaDividendsPerSubnet::::clear_prefix(netuid, u32::MAX, None); + let _ = RootAlphaDividendsPerSubnet::::clear_prefix(netuid, u32::MAX, None); let _ = PendingChildKeys::::clear_prefix(netuid, u32::MAX, None); let _ = AssociatedEvmAddress::::clear_prefix(netuid, u32::MAX, None); diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 2091946598..8e1e94c152 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -638,6 +638,183 @@ impl Pallet { } } + /// Computes and stores the EffectiveRootProp for a subnet. Returns the utilization value. + /// + /// EffectiveRootProp = raw_root_prop * utilization + /// + /// Where: + /// raw_root_prop = sum(root_alpha_dividends) / (sum(alpha_dividends) + sum(root_alpha_dividends)) + /// utilization = sum(root_stake_i * efficiency_i) / total_root_stake + /// efficiency_i = min(actual_share_i / expected_share_i, 1.0) + /// expected_share_i = root_stake_i / total_root_stake + /// actual_share_i = root_alpha_dividends[i] / sum(root_alpha_dividends) + /// + /// Only root stake of validators with UIDs on this subnet is counted. + /// TotalIssuance, unstaked TAO, and root stake on other subnets are irrelevant. + pub fn compute_and_store_effective_root_prop( + netuid: NetUid, + alpha_dividends: &BTreeMap, + root_alpha_dividends: &BTreeMap, + ) -> U96F32 { + let zero = U96F32::saturating_from_num(0); + let one = U96F32::saturating_from_num(1); + + 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 raw_root_prop = if total > zero { + total_root_divs.checked_div(total).unwrap_or(zero) + } else { + zero + }; + + // Compute dividend-efficiency-based utilization. + // For each root-staked validator registered on this subnet: + // expected_share = root_stake_i / total_root_stake + // actual_share = root_dividends_i / total_root_divs + // efficiency = min(actual_share / expected_share, 1.0) + // utilization = sum(root_stake_i * efficiency_i) / total_root_stake + let n = SubnetworkN::::get(netuid); + let mut total_root_stake = zero; + + // First pass: compute total root stake on this subnet + let mut hotkey_root_stakes: Vec<(T::AccountId, U96F32)> = Vec::new(); + for uid in 0..n { + if let Ok(hotkey) = Keys::::try_get(netuid, uid) { + let root_stake = Self::get_stake_for_hotkey_on_subnet(&hotkey, NetUid::ROOT); + let rs = U96F32::saturating_from_num(root_stake.to_u64()); + total_root_stake = total_root_stake.saturating_add(rs); + if rs > zero { + hotkey_root_stakes.push((hotkey, rs)); + } + } + } + + let utilization = if total_root_stake > zero && total_root_divs > zero { + // Second pass: compute weighted efficiency + let mut weighted_efficiency_sum = zero; + for (hotkey, rs) in &hotkey_root_stakes { + let expected_share = rs.checked_div(total_root_stake).unwrap_or(zero); + let actual_div = root_alpha_dividends.get(hotkey).copied().unwrap_or(zero); + let actual_share = actual_div.checked_div(total_root_divs).unwrap_or(zero); + let efficiency = if expected_share > zero { + let raw_eff = actual_share.checked_div(expected_share).unwrap_or(zero); + raw_eff.min(one) + } else { + zero + }; + weighted_efficiency_sum = + weighted_efficiency_sum.saturating_add(rs.saturating_mul(efficiency)); + } + weighted_efficiency_sum + .checked_div(total_root_stake) + .unwrap_or(zero) + } else if total_root_stake > zero { + // No root dividends at all → utilization = 0 + zero + } else { + zero + }; + + let effective_root_prop = raw_root_prop.saturating_mul(utilization); + + log::debug!( + "EffectiveRootProp for netuid {netuid:?}: {effective_root_prop:?} (raw: {raw_root_prop:?}, utilization: {utilization:?}, total_root_stake: {total_root_stake:?})" + ); + + EffectiveRootProp::::insert(netuid, effective_root_prop); + utilization + } + + /// Computes the fraction of a hotkey's dividends attributable to root stake. + /// + /// root_fraction = (root_stake * tao_weight) / (alpha_stake + root_stake * tao_weight) + /// + /// Returns 0 if the hotkey has no root stake or the total is zero. + pub fn get_root_dividend_fraction( + hotkey: &T::AccountId, + netuid: NetUid, + tao_weight: U96F32, + ) -> U96F32 { + let zero = U96F32::saturating_from_num(0); + let root_stake = Self::get_stake_for_hotkey_on_subnet(hotkey, NetUid::ROOT); + let root_stake_f = asfloat!(root_stake.to_u64()); + if root_stake_f <= zero { + return zero; + } + let root_alpha_weighted = root_stake_f.saturating_mul(tao_weight); + let alpha_stake = Self::get_stake_for_hotkey_on_subnet(hotkey, netuid); + let alpha_stake_f = asfloat!(alpha_stake.to_u64()); + let total_stake = alpha_stake_f.saturating_add(root_alpha_weighted); + if total_stake <= zero { + return zero; + } + root_alpha_weighted.checked_div(total_stake).unwrap_or(zero) + } + + /// Applies utilization-based hard cap to root dividend maps. + /// + /// - utilization >= 0.5: no scaling, returns 0 recycled + /// - utilization < 0.5: hard cap applied; zeroes all root dividends, recycles everything, + /// and sets EffectiveRootProp to 0 + /// + /// Also adjusts the root-staked portion of alpha_dividends accordingly. + /// Returns the total amount recycled. + pub fn apply_utilization_scaling( + netuid: NetUid, + utilization: U96F32, + alpha_dividends: &mut BTreeMap, + root_alpha_dividends: &mut BTreeMap, + tao_weight: U96F32, + ) -> U96F32 { + let half = U96F32::saturating_from_num(0.5); + let zero = U96F32::saturating_from_num(0); + + let has_root_dividends = + !root_alpha_dividends.is_empty() && root_alpha_dividends.values().any(|v| *v > zero); + + if !has_root_dividends || utilization >= half { + return zero; + } + + let mut total_recycled = zero; + + // Hard cap: utilization < 0.5 → recycle ALL root alpha dividends + let total_root: U96F32 = root_alpha_dividends + .values() + .fold(zero, |acc, v| acc.saturating_add(*v)); + Self::recycle_subnet_alpha(netuid, AlphaCurrency::from(tou64!(total_root))); + total_recycled = total_recycled.saturating_add(total_root); + root_alpha_dividends.clear(); + + // Zero root-staked portion of alpha_dividends + for (hotkey, alpha_div) in alpha_dividends.iter_mut() { + let root_fraction = Self::get_root_dividend_fraction(hotkey, netuid, tao_weight); + if root_fraction > zero { + let recycle_amount = (*alpha_div).saturating_mul(root_fraction); + *alpha_div = (*alpha_div).saturating_sub(recycle_amount); + Self::recycle_subnet_alpha(netuid, AlphaCurrency::from(tou64!(recycle_amount))); + total_recycled = total_recycled.saturating_add(recycle_amount); + } + } + + // Overwrite EffectiveRootProp to 0 + EffectiveRootProp::::insert(netuid, U96F32::saturating_from_num(0)); + + log::debug!( + "Hard cap triggered for netuid {netuid:?}: utilization {utilization:?} < 0.5, all root dividends recycled" + ); + + total_recycled + } + pub fn get_stake_map( netuid: NetUid, hotkeys: Vec<&T::AccountId>, @@ -724,7 +901,7 @@ impl Pallet { let root_alpha = pending_root_alpha; let owner_cut = pending_owner_cut; - let (incentives, (alpha_dividends, root_alpha_dividends)) = + let (incentives, (mut alpha_dividends, mut root_alpha_dividends)) = Self::calculate_dividend_and_incentive_distribution( netuid, root_alpha, @@ -733,6 +910,22 @@ impl Pallet { tao_weight, ); + // Compute and store EffectiveRootProp, getting back utilization for scaling. + let utilization = Self::compute_and_store_effective_root_prop( + netuid, + &alpha_dividends, + &root_alpha_dividends, + ); + + // Apply utilization-based scaling or hard cap to root dividends. + Self::apply_utilization_scaling( + netuid, + utilization, + &mut alpha_dividends, + &mut root_alpha_dividends, + tao_weight, + ); + Self::distribute_dividends_and_incentives( netuid, owner_cut, diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 477a678864..8d35bc6c05 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,147 @@ 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 min(EffectiveRootProp, RootProp) and re-normalizes shares to sum to 1.0. + /// Using the minimum of the two prevents exploitation by disabling alpha validators + /// to artificially inflate EffectiveRootProp above the configured RootProp. + 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 = + U64F64::saturating_from_num(EffectiveRootProp::::get(netuid)); + let root_prop = U64F64::saturating_from_num(RootProp::::get(netuid)); + *share = share.saturating_mul(effective_root_prop.min(root_prop)); + } + + Self::normalize_shares(shares); + } + + /// 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, + ) { + let zero = U64F64::saturating_from_num(0); + if shares.is_empty() { + return; + } + if top_k == 0 { + for share in shares.values_mut() { + *share = zero; + } + 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)); + + // The threshold is the share value at the k-th position (0-indexed: top_k - 1). + // All entries with share >= threshold are kept (ties at the boundary are included). + let threshold = sorted + .get(top_k.saturating_sub(1)) + .map(|(_, v)| *v) + .unwrap_or(zero); + + for share in shares.values_mut() { + if *share < threshold { + *share = zero; + } + } + + Self::normalize_shares(shares); + } + + /// Filters subnets so only the top proportion (by share) receive emission. + /// Uses ceil(count * proportion) to determine how many subnets to keep. + /// A single subnet always counts as in top 50%. + /// Ties at the cutoff are included, so the number kept can exceed ceil(count * proportion). + pub(crate) fn apply_top_subnet_proportion_filter(shares: &mut BTreeMap) { + let proportion = EmissionTopSubnetProportion::::get(); + let one = U64F64::saturating_from_num(1); + if proportion >= one { + return; // 100% means all subnets get emission + } + + let total = shares.len(); + if total == 0 { + return; + } + + // ceil(total * proportion): multiply total by proportion and round up + let top_k_f = U64F64::saturating_from_num(total).saturating_mul(proportion); + let top_k = + usize::try_from(top_k_f.ceil().saturating_to_num::().max(1)).unwrap_or(usize::MAX); + + log::debug!( + "EmissionTopSubnetProportion: keeping top {top_k} of {total} subnets (proportion: {proportion:?})" + ); + + Self::zero_and_redistribute_bottom_shares(shares, top_k); + } + + /// Applies the absolute-limit feature for subnets receiving emission. + /// When limit is None, no filtering occurs (disabled). + /// When limit is Some(N) and less than the number of subnets with nonzero shares, + /// subnets strictly below the N-th share are zeroed and the rest are re-normalized. + /// Ties at the cutoff are included, so more than N subnets may remain nonzero. + pub(crate) fn apply_top_subnet_absolute_limit(shares: &mut BTreeMap) { + let limit = match EmissionTopSubnetAbsoluteLimit::::get() { + Some(limit) => limit, + None => 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: applying cutoff at N={limit} with tie inclusion (had {nonzero_count} nonzero)" + ); + + Self::zero_and_redistribute_bottom_shares(shares, limit as usize); + } + 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); + + // 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)| { @@ -41,13 +174,13 @@ impl Pallet { pub fn record_tao_inflow(netuid: NetUid, tao: TaoCurrency) { SubnetTaoFlow::::mutate(netuid, |flow| { - *flow = flow.saturating_add(u64::from(tao) as i64); + *flow = flow.saturating_add(i64::try_from(u64::from(tao)).unwrap_or(i64::MAX)); }); } pub fn record_tao_outflow(netuid: NetUid, tao: TaoCurrency) { SubnetTaoFlow::::mutate(netuid, |flow| { - *flow = flow.saturating_sub(u64::from(tao) as i64) + *flow = flow.saturating_sub(i64::try_from(u64::from(tao)).unwrap_or(i64::MAX)); }); } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index ba7e3dcbf6..5da0a52a8d 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1274,7 +1274,10 @@ pub mod pallet { pub type SubnetMovingPrice = StorageMap<_, Identity, NetUid, I96F32, ValueQuery, DefaultMovingPrice>; - /// --- MAP ( netuid ) --> root_prop | The subnet root proportion. + /// --- MAP ( netuid ) --> root_prop | The subnet root proportion (global measure). + /// Computed as: tao_weight * root_tao / (tao_weight * root_tao + alpha_issuance). + /// This represents the proportion of the subnet's value from root TAO. + /// Note: This is distinct from EffectiveRootProp, which accounts for dividend-efficiency utilization. #[pallet::storage] pub type RootProp = StorageMap<_, Identity, NetUid, U96F32, ValueQuery, DefaultRootProp>; @@ -1488,6 +1491,44 @@ pub mod pallet { pub type FlowEmaSmoothingFactor = StorageValue<_, u64, ValueQuery, DefaultFlowEmaSmoothingFactor>; + #[pallet::storage] + /// --- MAP ( netuid ) --> EffectiveRootProp for a subnet (per-epoch measure). + /// Computed as: raw_root_prop * utilization, where raw_root_prop = root_dividends / (alpha_dividends + root_dividends) + /// and utilization is the dividend-efficiency metric. This accounts for actual dividend distribution efficiency. + /// Note: This is distinct from RootProp, which is a global measure of value proportion without efficiency adjustment. + pub type EffectiveRootProp = StorageMap<_, Identity, NetUid, U96F32, 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>; + + #[pallet::type_value] + /// Default: all subnets receive emission (1.0 = 100%). + pub fn DefaultEmissionTopSubnetProportion() -> U64F64 { + U64F64::saturating_from_num(1) + } + #[pallet::storage] + /// Proportion of subnets (ranked by share) that receive emission. + /// Value in range [0.0, 1.0] where 0.5 = 50%, 1.0 = 100%. + /// Subnets strictly below the ceil(count * proportion)-th share are zeroed and redistributed. + /// Ties at the cutoff are included, so the number of nonzero subnets can exceed ceil(count * proportion). + pub type EmissionTopSubnetProportion = + StorageValue<_, U64F64, ValueQuery, DefaultEmissionTopSubnetProportion>; + + #[pallet::storage] + /// Absolute maximum number of subnets that can receive emission. + /// None means no limit (disabled). When set to Some(N), subnets with share + /// strictly below the N-th position are zeroed and redistributed. + /// Ties at the cutoff are included, so the number of nonzero subnets can exceed N. + pub type EmissionTopSubnetAbsoluteLimit = StorageValue<_, u16, OptionQuery>; + /// ============================ /// ==== Global Parameters ===== /// ============================ diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 88b6a3f0ec..a26ac5911c 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2295,6 +2295,10 @@ mod dispatches { if let RootClaimTypeEnum::KeepSubnets { subnets } = &new_root_claim_type { ensure!(!subnets.is_empty(), Error::::InvalidSubnetNumber); + ensure!( + subnets.len() <= MAX_SUBNET_CLAIMS, + Error::::InvalidSubnetNumber + ); } Self::maybe_add_coldkey_index(&coldkey); @@ -2342,7 +2346,7 @@ mod dispatches { Self::ensure_subnet_owner_or_root(origin, netuid)?; ensure!( - new_value <= I96F32::from(MAX_ROOT_CLAIM_THRESHOLD), + new_value <= MAX_ROOT_CLAIM_THRESHOLD, Error::::InvalidRootClaimThreshold ); diff --git a/pallets/subtensor/src/staking/claim_root.rs b/pallets/subtensor/src/staking/claim_root.rs index 24a26d154c..64d68168a0 100644 --- a/pallets/subtensor/src/staking/claim_root.rs +++ b/pallets/subtensor/src/staking/claim_root.rs @@ -19,7 +19,8 @@ impl Pallet { ); let mut last_idx = start_index; for i in 0..k { - let bh_idx: usize = ((i.saturating_mul(8)) % 32) as usize; + let bh_idx: usize = + usize::try_from(i.saturating_mul(8).checked_rem(32).unwrap_or(0)).unwrap_or(0); let idx_step = u64::from_be_bytes( block_hash_bytes .get(bh_idx..(bh_idx.saturating_add(8))) @@ -276,7 +277,7 @@ impl Pallet { // Iterate over all the subnets this hotkey is staked on for root. let root_claimable = RootClaimable::::get(hotkey); for (netuid, claimable_rate) in root_claimable.iter() { - if *netuid == NetUid::ROOT.into() { + if *netuid == NetUid::ROOT { continue; // Skip the root netuid. } @@ -340,8 +341,6 @@ impl Pallet { if let Ok(coldkey) = StakingColdkeysByIndex::::try_get(i) { weight.saturating_accrue(Self::do_root_claim(coldkey.clone(), None)); } - - continue; } weight diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index 8f07572e25..69d0b7eac3 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -32,3 +32,4 @@ mod swap_hotkey_with_subnet; mod uids; mod voting_power; mod weights; +mod wide_scope_dividend; diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs index 311a930647..6ee6f9dc9b 100644 --- a/pallets/subtensor/src/tests/subnet_emissions.rs +++ b/pallets/subtensor/src/tests/subnet_emissions.rs @@ -1,11 +1,17 @@ -#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] +#![allow( + unused, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::arithmetic_side_effects +)] use super::mock::*; use crate::*; use alloc::collections::BTreeMap; use approx::assert_abs_diff_eq; use sp_core::U256; use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32}; -use subtensor_runtime_common::NetUid; +use subtensor_runtime_common::{AlphaCurrency, MechId, NetUid, TaoCurrency}; fn u64f64(x: f64) -> U64F64 { U64F64::from_num(x) @@ -151,137 +157,165 @@ fn inplace_pow_normalize_fractional_exponent() { }) } -// /// Normal (moderate, non-zero) EMA flows across 3 subnets. -// /// Expect: shares sum to ~1 and are monotonic with flows. -// #[test] -// fn get_shares_normal_flows_three_subnets() { -// new_test_ext(1).execute_with(|| { -// let owner_hotkey = U256::from(10); -// let owner_coldkey = U256::from(20); - -// let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); -// let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); -// let n3 = add_dynamic_network(&owner_hotkey, &owner_coldkey); - -// let block_num = FlowHalfLife::::get(); -// System::set_block_number(block_num); - -// // Set (block_number, flow) with reasonable positive flows -// SubnetEmaTaoFlow::::insert(n1, (block_num, i64f64(1_000.0))); -// SubnetEmaTaoFlow::::insert(n2, (block_num, i64f64(3_000.0))); -// SubnetEmaTaoFlow::::insert(n3, (block_num, i64f64(6_000.0))); - -// let subnets = vec![n1, n2, n3]; -// let shares = SubtensorModule::get_shares(&subnets); - -// // Sum ≈ 1 -// let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); -// assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-9); - -// // Each share in [0,1] and finite -// for (k, v) in &shares { -// let f = v.to_num::(); -// assert!(f.is_finite(), "share for {k:?} not finite"); -// assert!( -// (0.0..=1.0).contains(&f), -// "share for {k:?} out of [0,1]: {f}" -// ); -// } - -// // Monotonicity with the flows: share(n3) > share(n2) > share(n1) -// let s1 = shares.get(&n1).unwrap().to_num::(); -// let s2 = shares.get(&n2).unwrap().to_num::(); -// let s3 = shares.get(&n3).unwrap().to_num::(); -// assert!( -// s3 > s2 && s2 > s1, -// "expected s3 > s2 > s1; got {s1}, {s2}, {s3}" -// ); -// }); -// } - -// /// Very low (but non-zero) EMA flows across 2 subnets. -// /// Expect: shares sum to ~1 and higher-flow subnet gets higher share. -// #[test] -// fn get_shares_low_flows_sum_one_and_ordering() { -// new_test_ext(1).execute_with(|| { -// let owner_hotkey = U256::from(11); -// let owner_coldkey = U256::from(21); - -// let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); -// let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); - -// let block_num = FlowHalfLife::::get(); -// System::set_block_number(block_num); - -// // Tiny flows to exercise precision/scaling path -// SubnetEmaTaoFlow::::insert(n1, (block_num, i64f64(1e-9))); -// SubnetEmaTaoFlow::::insert(n2, (block_num, i64f64(2e-9))); - -// let subnets = vec![n1, n2]; -// let shares = SubtensorModule::get_shares(&subnets); - -// let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); -// assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-8); - -// for (k, v) in &shares { -// let f = v.to_num::(); -// assert!(f.is_finite(), "share for {k:?} not finite"); -// assert!( -// (0.0..=1.0).contains(&f), -// "share for {k:?} out of [0,1]: {f}" -// ); -// } - -// let s1 = shares.get(&n1).unwrap().to_num::(); -// let s2 = shares.get(&n2).unwrap().to_num::(); -// assert!( -// s2 > s1, -// "expected s2 > s1 with higher flow; got s1={s1}, s2={s2}" -// ); -// }); -// } - -// /// High EMA flows across 2 subnets. -// /// Expect: no overflow, shares sum to ~1, and ordering follows flows. -// #[test] -// fn get_shares_high_flows_sum_one_and_ordering() { -// new_test_ext(1).execute_with(|| { -// let owner_hotkey = U256::from(12); -// let owner_coldkey = U256::from(22); - -// let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); -// let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); - -// let block_num = FlowHalfLife::::get(); -// System::set_block_number(block_num); - -// // Large but safe flows for I64F64 -// SubnetEmaTaoFlow::::insert(n1, (block_num, i64f64(9.0e11))); -// SubnetEmaTaoFlow::::insert(n2, (block_num, i64f64(1.8e12))); - -// let subnets = vec![n1, n2]; -// let shares = SubtensorModule::get_shares(&subnets); - -// let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); -// assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-9); - -// for (k, v) in &shares { -// let f = v.to_num::(); -// assert!(f.is_finite(), "share for {k:?} not finite"); -// assert!( -// (0.0..=1.0).contains(&f), -// "share for {k:?} out of [0,1]: {f}" -// ); -// } - -// let s1 = shares.get(&n1).unwrap().to_num::(); -// let s2 = shares.get(&n2).unwrap().to_num::(); -// assert!( -// s2 > s1, -// "expected s2 > s1 with higher flow; got s1={s1}, s2={s2}" -// ); -// }); -// } +/// Normal (moderate, non-zero) EMA flows across 3 subnets. +/// Expect: shares sum to ~1 and are monotonic with flows. +#[test] +fn get_shares_normal_flows_three_subnets() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(10); + let owner_coldkey = U256::from(20); + + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n3 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + let block_num = FlowHalfLife::::get(); + System::set_block_number(block_num); + + // Set (block_number, flow) with reasonable positive flows + SubnetEmaTaoFlow::::insert(n1, (block_num, i64f64(1_000.0))); + SubnetEmaTaoFlow::::insert(n2, (block_num, i64f64(3_000.0))); + SubnetEmaTaoFlow::::insert(n3, (block_num, i64f64(6_000.0))); + + let subnets = vec![n1, n2, n3]; + let shares = SubtensorModule::get_shares(&subnets); + + // Sum ≈ 1 + let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); + assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-9); + + // Each share in [0,1] and finite + for (k, v) in &shares { + let f = v.to_num::(); + assert!(f.is_finite(), "share for {k:?} not finite"); + assert!( + (0.0..=1.0).contains(&f), + "share for {k:?} out of [0,1]: {f}" + ); + } + + // Monotonicity with the flows: share(n3) > share(n2) > share(n1) + let s1 = shares.get(&n1).unwrap().to_num::(); + let s2 = shares.get(&n2).unwrap().to_num::(); + let s3 = shares.get(&n3).unwrap().to_num::(); + assert!( + s3 > s2 && s2 > s1, + "expected s3 > s2 > s1; got {s1}, {s2}, {s3}" + ); + }); +} + +/// Very low (but non-zero) EMA flows across 2 subnets. +/// Expect: shares sum to ~1 and higher-flow subnet gets higher share. +#[test] +fn get_shares_low_flows_sum_one_and_ordering() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(11); + let owner_coldkey = U256::from(21); + + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + let block_num = FlowHalfLife::::get(); + System::set_block_number(block_num); + + // Tiny flows to exercise precision/scaling path + SubnetEmaTaoFlow::::insert(n1, (block_num, i64f64(1e-9))); + SubnetEmaTaoFlow::::insert(n2, (block_num, i64f64(2e-9))); + + let subnets = vec![n1, n2]; + let shares = SubtensorModule::get_shares(&subnets); + + let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); + assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-8); + + for (k, v) in &shares { + let f = v.to_num::(); + assert!(f.is_finite(), "share for {k:?} not finite"); + assert!( + (0.0..=1.0).contains(&f), + "share for {k:?} out of [0,1]: {f}" + ); + } + + let s1 = shares.get(&n1).unwrap().to_num::(); + let s2 = shares.get(&n2).unwrap().to_num::(); + assert!( + s2 > s1, + "expected s2 > s1 with higher flow; got s1={s1}, s2={s2}" + ); + }); +} + +/// High EMA flows across 2 subnets. +/// Expect: no overflow, shares sum to ~1, and ordering follows flows. +#[test] +fn get_shares_high_flows_sum_one_and_ordering() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(12); + let owner_coldkey = U256::from(22); + + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + let block_num = FlowHalfLife::::get(); + System::set_block_number(block_num); + + // Large but safe flows for I64F64 + SubnetEmaTaoFlow::::insert(n1, (block_num, i64f64(9.0e11))); + SubnetEmaTaoFlow::::insert(n2, (block_num, i64f64(1.8e12))); + + let subnets = vec![n1, n2]; + let shares = SubtensorModule::get_shares(&subnets); + + let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); + assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-9); + + for (k, v) in &shares { + let f = v.to_num::(); + assert!(f.is_finite(), "share for {k:?} not finite"); + assert!( + (0.0..=1.0).contains(&f), + "share for {k:?} out of [0,1]: {f}" + ); + } + + let s1 = shares.get(&n1).unwrap().to_num::(); + let s2 = shares.get(&n2).unwrap().to_num::(); + assert!( + s2 > s1, + "expected s2 > s1 with higher flow; got s1={s1}, s2={s2}" + ); + }); +} + +/// Single subnet should receive 100% of the share (1.0). +/// Expect: shares contain exactly 1 entry with value 1.0. +#[test] +fn test_get_shares_single_subnet() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(13); + let owner_coldkey = U256::from(23); + + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + let block_num = FlowHalfLife::::get(); + System::set_block_number(block_num); + + // Set (block_number, flow) with a positive flow + SubnetEmaTaoFlow::::insert(n1, (block_num, i64f64(1_000.0))); + + let subnets = vec![n1]; + let shares = SubtensorModule::get_shares(&subnets); + + // Should have exactly 1 entry + assert_eq!(shares.len(), 1, "expected exactly 1 entry in shares"); + + // The single subnet should get share of 1.0 + let s1 = shares.get(&n1).unwrap().to_num::(); + assert_abs_diff_eq!(s1, 1.0_f64, epsilon = 1e-9); + }); +} /// Helper to (re)seed EMA price & flow at the *current* block. fn seed_price_and_flow(n1: NetUid, n2: NetUid, price1: f64, price2: f64, flow1: f64, flow2: f64) { @@ -292,199 +326,2550 @@ fn seed_price_and_flow(n1: NetUid, n2: NetUid, price1: f64, price2: f64, flow1: SubnetEmaTaoFlow::::insert(n2, (now, i64f64(flow2))); } -// /// If one subnet has a negative EMA flow and the other positive, -// /// the negative one should contribute no weight (treated as zero), -// /// so the positive-flow subnet gets the full share. -// #[test] -// fn get_shares_negative_vs_positive_flow() { -// new_test_ext(1).execute_with(|| { -// // 2 subnets -// let owner_hotkey = U256::from(60); -// let owner_coldkey = U256::from(61); -// let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); -// let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); - -// // Configure blending window and current block -// let half_life: u64 = FlowHalfLife::::get(); -// FlowNormExponent::::set(u64f64(1.0)); -// frame_system::Pallet::::set_block_number(half_life); -// TaoFlowCutoff::::set(I64F64::from_num(0)); - -// // Equal EMA prices so price side doesn't bias -// SubnetMovingPrice::::insert(n1, i96f32(1.0)); -// SubnetMovingPrice::::insert(n2, i96f32(1.0)); - -// // Set flows: n1 negative, n2 positive -// let now = frame_system::Pallet::::block_number(); -// SubnetEmaTaoFlow::::insert(n1, (now, i64f64(-100.0))); -// SubnetEmaTaoFlow::::insert(n2, (now, i64f64(500.0))); - -// let shares = SubtensorModule::get_shares(&[n1, n2]); -// let s1 = shares.get(&n1).unwrap().to_num::(); -// let s2 = shares.get(&n2).unwrap().to_num::(); - -// // Sum ~ 1 -// assert_abs_diff_eq!(s1 + s2, 1.0_f64, epsilon = 1e-9); -// // Negative flow subnet should not get weight from flow; with equal prices mid-window, -// // positive-flow subnet should dominate and get all the allocation. -// assert!( -// s2 > 0.999_999 && s1 < 1e-6, -// "expected s2≈1, s1≈0; got s1={s1}, s2={s2}" -// ); -// }); -// } - -// /// If both subnets have negative EMA flows, flows should contribute zero weight -// #[test] -// fn get_shares_both_negative_flows_zero_emission() { -// new_test_ext(1).execute_with(|| { -// // 2 subnets -// let owner_hotkey = U256::from(60); -// let owner_coldkey = U256::from(61); -// let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); -// let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); - -// // Configure blending window and current block -// let half_life: u64 = FlowHalfLife::::get(); -// FlowNormExponent::::set(u64f64(1.0)); -// frame_system::Pallet::::set_block_number(half_life); -// TaoFlowCutoff::::set(I64F64::from_num(0)); - -// // Equal EMA prices so price side doesn't bias -// SubnetMovingPrice::::insert(n1, i96f32(1.0)); -// SubnetMovingPrice::::insert(n2, i96f32(1.0)); - -// // Set flows -// let now = frame_system::Pallet::::block_number(); -// SubnetEmaTaoFlow::::insert(n1, (now, i64f64(-100.0))); -// SubnetEmaTaoFlow::::insert(n2, (now, i64f64(-200.0))); - -// let shares = SubtensorModule::get_shares(&[n1, n2]); -// let s1 = shares.get(&n1).unwrap().to_num::(); -// let s2 = shares.get(&n2).unwrap().to_num::(); - -// assert!( -// s1 < 1e-20 && s2 < 1e-20, -// "expected s2≈0, s1≈0; got s1={s1}, s2={s2}" -// ); -// }); -// } - -// /// If both subnets have positive EMA flows lower than or equal to cutoff, flows should contribute zero weight -// #[test] -// fn get_shares_both_below_cutoff_zero_emission() { -// new_test_ext(1).execute_with(|| { -// // 2 subnets -// let owner_hotkey = U256::from(60); -// let owner_coldkey = U256::from(61); -// let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); -// let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); - -// // Configure blending window and current block -// let half_life: u64 = FlowHalfLife::::get(); -// FlowNormExponent::::set(u64f64(1.0)); -// frame_system::Pallet::::set_block_number(half_life); -// TaoFlowCutoff::::set(I64F64::from_num(2_000)); - -// // Equal EMA prices so price side doesn't bias -// SubnetMovingPrice::::insert(n1, i96f32(1.0)); -// SubnetMovingPrice::::insert(n2, i96f32(1.0)); - -// // Set flows -// let now = frame_system::Pallet::::block_number(); -// SubnetEmaTaoFlow::::insert(n1, (now, i64f64(1000.0))); -// SubnetEmaTaoFlow::::insert(n2, (now, i64f64(2000.0))); - -// let shares = SubtensorModule::get_shares(&[n1, n2]); -// let s1 = shares.get(&n1).unwrap().to_num::(); -// let s2 = shares.get(&n2).unwrap().to_num::(); - -// assert!( -// s1 < 1e-20 && s2 < 1e-20, -// "expected s2≈0, s1≈0; got s1={s1}, s2={s2}" -// ); -// }); -// } - -// /// If one subnet has positive EMA flow lower than cutoff, the other gets full emission -// #[test] -// fn get_shares_one_below_cutoff_other_full_emission() { -// new_test_ext(1).execute_with(|| { -// [(1000.0, 2000.00001), (1000.0, 2000.001), (1000.0, 5000.0)] -// .into_iter() -// .for_each(|(flow1, flow2)| { -// // 2 subnets -// let owner_hotkey = U256::from(60); -// let owner_coldkey = U256::from(61); -// let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); -// let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); - -// // Configure blending window and current block -// let half_life: u64 = FlowHalfLife::::get(); -// FlowNormExponent::::set(u64f64(1.0)); -// frame_system::Pallet::::set_block_number(half_life); -// TaoFlowCutoff::::set(I64F64::from_num(2_000)); - -// // Equal EMA prices (price side doesn't bias) -// SubnetMovingPrice::::insert(n1, i96f32(1.0)); -// SubnetMovingPrice::::insert(n2, i96f32(1.0)); - -// // Set flows -// let now = frame_system::Pallet::::block_number(); -// SubnetEmaTaoFlow::::insert(n1, (now, i64f64(flow1))); -// SubnetEmaTaoFlow::::insert(n2, (now, i64f64(flow2))); - -// let shares = SubtensorModule::get_shares(&[n1, n2]); -// let s1 = shares.get(&n1).unwrap().to_num::(); -// let s2 = shares.get(&n2).unwrap().to_num::(); - -// // Sum ~ 1 -// assert_abs_diff_eq!(s1 + s2, 1.0_f64, epsilon = 1e-9); -// assert!( -// s2 > 0.999_999 && s1 < 1e-6, -// "expected s2≈1, s1≈0; got s1={s1}, s2={s2}" -// ); -// }); -// }); -// } - -// /// If subnets have negative EMA flows, but they are above the cut-off, emissions are proportional -// /// for all except the bottom one, which gets nothing -// #[test] -// fn get_shares_both_negative_above_cutoff() { -// new_test_ext(1).execute_with(|| { -// // 2 subnets -// let owner_hotkey = U256::from(60); -// let owner_coldkey = U256::from(61); -// let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); -// let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); -// let n3 = add_dynamic_network(&owner_hotkey, &owner_coldkey); - -// // Configure blending window and current block -// let half_life: u64 = FlowHalfLife::::get(); -// FlowNormExponent::::set(u64f64(1.0)); -// frame_system::Pallet::::set_block_number(half_life); -// TaoFlowCutoff::::set(I64F64::from_num(-1000.0)); - -// // Equal EMA prices so price side doesn't bias -// SubnetMovingPrice::::insert(n1, i96f32(1.0)); -// SubnetMovingPrice::::insert(n2, i96f32(1.0)); -// SubnetMovingPrice::::insert(n3, i96f32(1.0)); - -// // Set flows -// let now = frame_system::Pallet::::block_number(); -// SubnetEmaTaoFlow::::insert(n1, (now, i64f64(-100.0))); -// SubnetEmaTaoFlow::::insert(n2, (now, i64f64(-300.0))); -// SubnetEmaTaoFlow::::insert(n3, (now, i64f64(-400.0))); - -// let shares = SubtensorModule::get_shares(&[n1, n2, n3]); -// let s1 = shares.get(&n1).unwrap().to_num::(); -// let s2 = shares.get(&n2).unwrap().to_num::(); -// let s3 = shares.get(&n3).unwrap().to_num::(); - -// assert_abs_diff_eq!(s1, 0.75, epsilon = s1 / 100.0); -// assert_abs_diff_eq!(s2, 0.25, epsilon = s2 / 100.0); -// assert_abs_diff_eq!(s3, 0.0, epsilon = 1e-9); -// assert_abs_diff_eq!(s1 + s2 + s3, 1.0, epsilon = 1e-9); -// }); -// } +/// If one subnet has a negative EMA flow and the other positive, +/// the negative one should contribute no weight (treated as zero), +/// so the positive-flow subnet gets the full share. +#[test] +fn get_shares_negative_vs_positive_flow() { + new_test_ext(1).execute_with(|| { + // 2 subnets + let owner_hotkey = U256::from(60); + let owner_coldkey = U256::from(61); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Configure blending window and current block + let half_life: u64 = FlowHalfLife::::get(); + FlowNormExponent::::set(u64f64(1.0)); + frame_system::Pallet::::set_block_number(half_life); + TaoFlowCutoff::::set(I64F64::from_num(0)); + + // Set flows: n1 negative, n2 positive + let now = frame_system::Pallet::::block_number(); + SubnetEmaTaoFlow::::insert(n1, (now, i64f64(-100.0))); + SubnetEmaTaoFlow::::insert(n2, (now, i64f64(500.0))); + + let shares = SubtensorModule::get_shares(&[n1, n2]); + let s1 = shares.get(&n1).unwrap().to_num::(); + let s2 = shares.get(&n2).unwrap().to_num::(); + + // Sum ~ 1 + assert_abs_diff_eq!(s1 + s2, 1.0_f64, epsilon = 1e-9); + // Negative flow subnet should not get weight from flow; + // positive-flow subnet should get all the allocation. + assert!( + s2 > 0.999_999 && s1 < 1e-6, + "expected s2≈1, s1≈0; got s1={s1}, s2={s2}" + ); + }); +} + +/// If both subnets have negative EMA flows, flows should contribute zero weight +#[test] +fn get_shares_both_negative_flows_zero_emission() { + new_test_ext(1).execute_with(|| { + // 2 subnets + let owner_hotkey = U256::from(60); + let owner_coldkey = U256::from(61); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Configure blending window and current block + let half_life: u64 = FlowHalfLife::::get(); + FlowNormExponent::::set(u64f64(1.0)); + frame_system::Pallet::::set_block_number(half_life); + TaoFlowCutoff::::set(I64F64::from_num(0)); + + // Set flows + let now = frame_system::Pallet::::block_number(); + SubnetEmaTaoFlow::::insert(n1, (now, i64f64(-100.0))); + SubnetEmaTaoFlow::::insert(n2, (now, i64f64(-200.0))); + + let shares = SubtensorModule::get_shares(&[n1, n2]); + let s1 = shares.get(&n1).unwrap().to_num::(); + let s2 = shares.get(&n2).unwrap().to_num::(); + + assert!( + s1 < 1e-20 && s2 < 1e-20, + "expected s2≈0, s1≈0; got s1={s1}, s2={s2}" + ); + }); +} + +/// If both subnets have positive EMA flows lower than or equal to cutoff, flows should contribute zero weight +#[test] +fn get_shares_both_below_cutoff_zero_emission() { + new_test_ext(1).execute_with(|| { + // 2 subnets + let owner_hotkey = U256::from(60); + let owner_coldkey = U256::from(61); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Configure blending window and current block + let half_life: u64 = FlowHalfLife::::get(); + FlowNormExponent::::set(u64f64(1.0)); + frame_system::Pallet::::set_block_number(half_life); + TaoFlowCutoff::::set(I64F64::from_num(2_000)); + + // Set flows + let now = frame_system::Pallet::::block_number(); + SubnetEmaTaoFlow::::insert(n1, (now, i64f64(1000.0))); + SubnetEmaTaoFlow::::insert(n2, (now, i64f64(2000.0))); + + let shares = SubtensorModule::get_shares(&[n1, n2]); + let s1 = shares.get(&n1).unwrap().to_num::(); + let s2 = shares.get(&n2).unwrap().to_num::(); + + assert!( + s1 < 1e-20 && s2 < 1e-20, + "expected s2≈0, s1≈0; got s1={s1}, s2={s2}" + ); + }); +} + +/// If one subnet has positive EMA flow lower than cutoff, the other gets full emission +#[test] +fn get_shares_one_below_cutoff_other_full_emission() { + new_test_ext(1).execute_with(|| { + [(1000.0, 2000.00001), (1000.0, 2000.001), (1000.0, 5000.0)] + .into_iter() + .for_each(|(flow1, flow2)| { + // 2 subnets + let owner_hotkey = U256::from(60); + let owner_coldkey = U256::from(61); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Configure blending window and current block + let half_life: u64 = FlowHalfLife::::get(); + FlowNormExponent::::set(u64f64(1.0)); + frame_system::Pallet::::set_block_number(half_life); + TaoFlowCutoff::::set(I64F64::from_num(2_000)); + + // Set flows + let now = frame_system::Pallet::::block_number(); + SubnetEmaTaoFlow::::insert(n1, (now, i64f64(flow1))); + SubnetEmaTaoFlow::::insert(n2, (now, i64f64(flow2))); + + let shares = SubtensorModule::get_shares(&[n1, n2]); + let s1 = shares.get(&n1).unwrap().to_num::(); + let s2 = shares.get(&n2).unwrap().to_num::(); + + // Sum ~ 1 + assert_abs_diff_eq!(s1 + s2, 1.0_f64, epsilon = 1e-9); + assert!( + s2 > 0.999_999 && s1 < 1e-6, + "expected s2≈1, s1≈0; got s1={s1}, s2={s2}" + ); + }); + }); +} + +/// If subnets have negative EMA flows, but they are above the cut-off, emissions are proportional +/// for all except the bottom one, which gets nothing +#[test] +fn get_shares_both_negative_above_cutoff() { + new_test_ext(1).execute_with(|| { + // 3 subnets + let owner_hotkey = U256::from(60); + let owner_coldkey = U256::from(61); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n3 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Configure blending window and current block + let half_life: u64 = FlowHalfLife::::get(); + FlowNormExponent::::set(u64f64(1.0)); + frame_system::Pallet::::set_block_number(half_life); + TaoFlowCutoff::::set(I64F64::from_num(-1000)); + + // Set flows + let now = frame_system::Pallet::::block_number(); + SubnetEmaTaoFlow::::insert(n1, (now, i64f64(-100.0))); + SubnetEmaTaoFlow::::insert(n2, (now, i64f64(-300.0))); + SubnetEmaTaoFlow::::insert(n3, (now, i64f64(-400.0))); + + let shares = SubtensorModule::get_shares(&[n1, n2, n3]); + let s1 = shares.get(&n1).unwrap().to_num::(); + let s2 = shares.get(&n2).unwrap().to_num::(); + let s3 = shares.get(&n3).unwrap().to_num::(); + + assert_abs_diff_eq!(s1, 0.75, epsilon = s1 / 100.0); + assert_abs_diff_eq!(s2, 0.25, epsilon = s2 / 100.0); + assert_abs_diff_eq!(s3, 0.0, epsilon = 1e-9); + assert_abs_diff_eq!(s1 + s2 + s3, 1.0, epsilon = 1e-9); + }); +} + +/// Test that get_shares is idempotent within the same block. +/// When called multiple times in the same block, it should return the same values. +#[test] +fn test_get_shares_idempotent_within_same_block() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(100); + let owner_coldkey = U256::from(200); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + let block_num = FlowHalfLife::::get(); + System::set_block_number(block_num); + + SubnetEmaTaoFlow::::insert(n1, (block_num, i64f64(1000.0))); + SubnetEmaTaoFlow::::insert(n2, (block_num, i64f64(3000.0))); + + // Call get_shares twice in the same block + let shares1 = SubtensorModule::get_shares(&[n1, n2]); + let shares2 = SubtensorModule::get_shares(&[n1, n2]); + + // Both calls should return the same values + let s1a = shares1.get(&n1).unwrap().to_num::(); + let s1b = shares2.get(&n1).unwrap().to_num::(); + let s2a = shares1.get(&n2).unwrap().to_num::(); + let s2b = shares2.get(&n2).unwrap().to_num::(); + + assert_abs_diff_eq!(s1a, s1b, epsilon = 1e-18); + assert_abs_diff_eq!(s2a, s2b, epsilon = 1e-18); + }); +} + +#[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_root_stake_but_no_root_dividends() { + // When validators have root stake registered on the subnet but there are NO root dividends, + // utilization should be 0 (the else if total_root_stake > zero branch). + // This is different from test_effective_root_prop_no_root_dividends which has no registered + // validators with root stake. + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey1 = U256::from(100); + let coldkey1 = U256::from(200); + let hotkey2 = U256::from(101); + let coldkey2 = U256::from(201); + + // Register hotkeys on subnet + Keys::::insert(netuid, 0u16, hotkey1); + Keys::::insert(netuid, 1u16, hotkey2); + SubnetworkN::::insert(netuid, 2u16); + + // Give the hotkeys root stake + increase_stake_on_coldkey_hotkey_account(&coldkey1, &hotkey1, 1000u64.into(), NetUid::ROOT); + increase_stake_on_coldkey_hotkey_account(&coldkey2, &hotkey2, 1000u64.into(), NetUid::ROOT); + + // Create non-empty alpha dividends (so raw_root_prop denominator is non-zero) + let mut alpha_dividends: BTreeMap = BTreeMap::new(); + alpha_dividends.insert(hotkey1, U96F32::from_num(1000)); + alpha_dividends.insert(hotkey2, U96F32::from_num(2000)); + + // Create EMPTY root_alpha_dividends (so total_root_divs = 0) + let root_alpha_dividends: BTreeMap = BTreeMap::new(); + + let utilization = SubtensorModule::compute_and_store_effective_root_prop( + netuid, + &alpha_dividends, + &root_alpha_dividends, + ); + + // Assert utilization is 0 because no root dividends despite having root stake + assert_abs_diff_eq!(utilization.to_num::(), 0.0, epsilon = 1e-12); + + // Assert EffectiveRootProp is also 0 + 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 with equal root stakes but unequal dividends, + // efficiency-based utilization < 1.0 because the validator with less dividends than expected + // has efficiency < 1.0. + // hotkey1: expected_share=0.5, actual_share=1/3 → efficiency=2/3 + // hotkey2: expected_share=0.5, actual_share=2/3 → efficiency=1.0 (capped) + // utilization = (1000*2/3 + 1000*1.0) / 2000 ≈ 0.8333 + // raw_root_prop = 1.0 (all root divs), so ERP ≈ 0.8333 + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey1 = U256::from(100); + let coldkey1 = U256::from(200); + let hotkey2 = U256::from(101); + let coldkey2 = U256::from(201); + + // Register hotkeys on subnet and give them root stake so utilization = 1.0 + Keys::::insert(netuid, 0u16, hotkey1); + Keys::::insert(netuid, 1u16, hotkey2); + SubnetworkN::::insert(netuid, 2u16); + increase_stake_on_coldkey_hotkey_account(&coldkey1, &hotkey1, 1000u64.into(), NetUid::ROOT); + increase_stake_on_coldkey_hotkey_account(&coldkey2, &hotkey2, 1000u64.into(), NetUid::ROOT); + + 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)); + + let utilization = SubtensorModule::compute_and_store_effective_root_prop( + netuid, + &alpha_dividends, + &root_alpha_dividends, + ); + + assert_abs_diff_eq!(utilization.to_num::(), 0.8333, epsilon = 1e-3); + + let prop = EffectiveRootProp::::get(netuid); + // raw_root_prop = 1.0, utilization ≈ 0.8333 + assert_abs_diff_eq!(prop.to_num::(), 0.8333, epsilon = 1e-3); + }); +} + +#[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 coldkey1 = U256::from(200); + + // Register hotkey on subnet and give root stake so utilization = 1.0 + Keys::::insert(netuid, 0u16, hotkey1); + SubnetworkN::::insert(netuid, 1u16); + increase_stake_on_coldkey_hotkey_account(&coldkey1, &hotkey1, 1000u64.into(), NetUid::ROOT); + + 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); + let coldkey1 = U256::from(200); + + // Register hotkey on both subnets and give root stake so utilization = 1.0 + Keys::::insert(netuid1, 0u16, hotkey1); + SubnetworkN::::insert(netuid1, 1u16); + Keys::::insert(netuid2, 0u16, hotkey1); + SubnetworkN::::insert(netuid2, 1u16); + increase_stake_on_coldkey_hotkey_account(&coldkey1, &hotkey1, 1000u64.into(), NetUid::ROOT); + + // 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); + }); +} + +#[test] +fn test_effective_root_prop_single_validator_always_full_utilization() { + // A single validator should always achieve 100% utilization because it gets + // 100% of both expected and actual share (efficiency = 1.0). + // With equal alpha and root dividends, raw_root_prop = 0.5. + // EffectiveRootProp = raw_root_prop * utilization = 0.5 * 1.0 = 0.5 + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(100); + let coldkey = U256::from(200); + + // Register ONE hotkey on subnet + Keys::::insert(netuid, 0u16, hotkey); + SubnetworkN::::insert(netuid, 1u16); + + // Give it root stake (1M) + increase_stake_on_coldkey_hotkey_account( + &coldkey, + &hotkey, + 1_000_000u64.into(), + NetUid::ROOT, + ); + + // Create alpha_dividends with hotkey: 5000 + let mut alpha_dividends: BTreeMap = BTreeMap::new(); + alpha_dividends.insert(hotkey, U96F32::from_num(5000)); + + // Create root_alpha_dividends with hotkey: 5000 + let mut root_alpha_dividends: BTreeMap = BTreeMap::new(); + root_alpha_dividends.insert(hotkey, U96F32::from_num(5000)); + + let utilization = SubtensorModule::compute_and_store_effective_root_prop( + netuid, + &alpha_dividends, + &root_alpha_dividends, + ); + + // Single validator always gets 100% utilization + assert_abs_diff_eq!(utilization.to_num::(), 1.0, epsilon = 1e-12); + + let prop = EffectiveRootProp::::get(netuid); + // raw_root_prop = 5000/(5000+5000) = 0.5 + // EffectiveRootProp = 0.5 * 1.0 = 0.5 + assert_abs_diff_eq!(prop.to_num::(), 0.5, 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 and RootProp for subnets. + // RootProp >= EffectiveRootProp, so min() uses EffectiveRootProp. + EffectiveRootProp::::insert(NetUid::from(1), U96F32::from_num(0.8)); + EffectiveRootProp::::insert(NetUid::from(2), U96F32::from_num(0.2)); + RootProp::::insert(NetUid::from(1), U96F32::from_num(0.9)); + RootProp::::insert(NetUid::from(2), U96F32::from_num(0.9)); + + 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_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), U96F32::from_num(0.3)); + RootProp::::insert(NetUid::from(1), U96F32::from_num(0.5)); + + 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); + }); +} + +#[test] +fn test_apply_effective_root_prop_scaling_capped_by_root_prop() { + new_test_ext(1).execute_with(|| { + // Enable scaling + EffectiveRootPropEmissionScaling::::set(true); + + // Simulate exploitation: EffectiveRootProp inflated above RootProp + // by disabling alpha validators. Scaling should use min(ERP, RP). + EffectiveRootProp::::insert(NetUid::from(1), U96F32::from_num(0.9)); // inflated + EffectiveRootProp::::insert(NetUid::from(2), U96F32::from_num(0.2)); // normal + RootProp::::insert(NetUid::from(1), U96F32::from_num(0.3)); // actual root prop + RootProp::::insert(NetUid::from(2), U96F32::from_num(0.5)); // actual root prop + + 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); + + // min(0.9, 0.3) = 0.3 for subnet1, min(0.2, 0.5) = 0.2 for subnet2 + // After scaling: subnet1 = 0.5*0.3 = 0.15, subnet2 = 0.5*0.2 = 0.10 + // After normalization: subnet1 = 0.15/0.25 = 0.6, subnet2 = 0.10/0.25 = 0.4 + 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.6, epsilon = 1e-9); + assert_abs_diff_eq!(s2, 0.4, epsilon = 1e-9); + assert_abs_diff_eq!(s1 + s2, 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_bottom_shares_tie_at_boundary() { + // A:0.4, B:0.3, C:0.3 with top_k=2 — B and C tie at the boundary. + // Both should be kept (tie inclusion), so all 3 remain. + let mut shares: BTreeMap = BTreeMap::new(); + shares.insert(NetUid::from(1), u64f64(0.4)); + shares.insert(NetUid::from(2), u64f64(0.3)); + shares.insert(NetUid::from(3), u64f64(0.3)); + + SubtensorModule::zero_and_redistribute_bottom_shares(&mut shares, 2); + + 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::(); + + // All three should be nonzero since B and C tie at the k-th position + assert!(s1 > 0.0, "A should be kept"); + assert!(s2 > 0.0, "B should be kept (tie at boundary)"); + assert!(s3 > 0.0, "C should be kept (tie at boundary)"); + assert_abs_diff_eq!(s1 + s2 + s3, 1.0, epsilon = 1e-9); + // Normalized: 0.4/1.0, 0.3/1.0, 0.3/1.0 + assert_abs_diff_eq!(s1, 0.4, epsilon = 1e-9); + assert_abs_diff_eq!(s2, 0.3, epsilon = 1e-9); + assert_abs_diff_eq!(s3, 0.3, epsilon = 1e-9); +} + +#[test] +fn test_zero_and_redistribute_bottom_shares_no_tie() { + // A:0.5, B:0.3, C:0.2 with top_k=2 — no tie at boundary, C is strictly below. + 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)); + + SubtensorModule::zero_and_redistribute_bottom_shares(&mut shares, 2); + + 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!(s1 > 0.0); + assert!(s2 > 0.0); + assert_abs_diff_eq!(s3, 0.0, epsilon = 1e-12); + assert_abs_diff_eq!(s1 + s2, 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(|| { + EmissionTopSubnetProportion::::set(U64F64::saturating_from_num(0.5)); // 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(|| { + EmissionTopSubnetProportion::::set(U64F64::saturating_from_num(0.5)); // 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(|| { + EmissionTopSubnetProportion::::set(U64F64::saturating_from_num(0.5)); // 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(U64F64::saturating_from_num(1)); // 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_zeroed_get_no_emission() { + new_test_ext(1).execute_with(|| { + EmissionTopSubnetProportion::::set(U64F64::saturating_from_num(0.5)); // 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); + + // 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); + } + } + }); +} + +#[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(Some(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(Some(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_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 + EmissionTopSubnetProportion::::set(U64F64::saturating_from_num(0.5)); + EmissionTopSubnetAbsoluteLimit::::set(Some(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.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(U64F64::saturating_from_num(1)); + EmissionTopSubnetAbsoluteLimit::::set(Some(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); + }); +} + +// =========================================================================== +// Tests for full filter chain composition (ERP scaling -> proportion -> absolute) +// =========================================================================== + +#[test] +fn test_full_filter_chain_erp_zeroes_shares_then_proportion_sees_fewer_nonzero() { + // Full filter chain: apply_effective_root_prop_scaling -> apply_top_subnet_proportion_filter + // -> apply_top_subnet_absolute_limit compose correctly. + // + // Setup: 4 subnets. After ERP scaling, two subnets are effectively zeroed (ERP = 0), + // leaving only 2 nonzero. The proportion filter at 50% of 4 would normally keep + // ceil(4 * 0.5) = 2, but since only 2 are nonzero, both survive. The absolute limit + // of 3 is not binding. Result: exactly 2 nonzero subnets. + new_test_ext(1).execute_with(|| { + EffectiveRootPropEmissionScaling::::set(true); + EmissionTopSubnetProportion::::set(U64F64::saturating_from_num(0.5)); // 50% + EmissionTopSubnetAbsoluteLimit::::set(Some(3)); + + // Subnets 1 and 2 have zero ERP -> their shares will be zeroed by ERP scaling + EffectiveRootProp::::insert(NetUid::from(1), U96F32::from_num(0)); + EffectiveRootProp::::insert(NetUid::from(2), U96F32::from_num(0)); + EffectiveRootProp::::insert(NetUid::from(3), U96F32::from_num(0.5)); + EffectiveRootProp::::insert(NetUid::from(4), U96F32::from_num(0.8)); + + RootProp::::insert(NetUid::from(1), U96F32::from_num(0.5)); + RootProp::::insert(NetUid::from(2), U96F32::from_num(0.5)); + RootProp::::insert(NetUid::from(3), U96F32::from_num(0.5)); + RootProp::::insert(NetUid::from(4), U96F32::from_num(0.8)); + + 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)); + + // Step 1: ERP scaling zeros subnets 1 and 2 + SubtensorModule::apply_effective_root_prop_scaling(&mut shares); + + 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); + // Subnets 3 and 4 are the only nonzero ones + let nonzero_after_erp = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!(nonzero_after_erp, 2); + + // Step 2: Proportion filter (50% of 4 = ceil(2) = 2) + // The top 2 by share are subnets 3 and 4, and subnets 1,2 are already zero. + // Threshold is set by 2nd-highest share. Subnets 1,2 are below it -> stay zero. + SubtensorModule::apply_top_subnet_proportion_filter(&mut shares); + + let nonzero_after_prop = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!(nonzero_after_prop, 2); + + // Step 3: Absolute limit of 3 is not binding since only 2 nonzero + SubtensorModule::apply_top_subnet_absolute_limit(&mut shares); + + let nonzero_final = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!(nonzero_final, 2); + + let s3 = shares.get(&NetUid::from(3)).unwrap().to_num::(); + let s4 = shares.get(&NetUid::from(4)).unwrap().to_num::(); + assert!(s3 > 0.0); + assert!(s4 > 0.0); + assert_abs_diff_eq!(s3 + s4, 1.0, epsilon = 1e-9); + }); +} + +#[test] +fn test_full_filter_chain_erp_reduces_then_absolute_limit_binds() { + // After ERP scaling, 3 of 5 subnets remain nonzero. + // Proportion filter at 100% does nothing. + // Absolute limit = 2 then trims to top 2. + new_test_ext(1).execute_with(|| { + EffectiveRootPropEmissionScaling::::set(true); + EmissionTopSubnetProportion::::set(U64F64::saturating_from_num(1.0)); // 100% + EmissionTopSubnetAbsoluteLimit::::set(Some(2)); + + EffectiveRootProp::::insert(NetUid::from(1), U96F32::from_num(0)); + EffectiveRootProp::::insert(NetUid::from(2), U96F32::from_num(0)); + EffectiveRootProp::::insert(NetUid::from(3), U96F32::from_num(0.3)); + EffectiveRootProp::::insert(NetUid::from(4), U96F32::from_num(0.5)); + EffectiveRootProp::::insert(NetUid::from(5), U96F32::from_num(0.7)); + + RootProp::::insert(NetUid::from(1), U96F32::from_num(0.5)); + RootProp::::insert(NetUid::from(2), U96F32::from_num(0.5)); + RootProp::::insert(NetUid::from(3), U96F32::from_num(0.5)); + RootProp::::insert(NetUid::from(4), U96F32::from_num(0.5)); + RootProp::::insert(NetUid::from(5), U96F32::from_num(0.7)); + + let mut shares: BTreeMap = BTreeMap::new(); + shares.insert(NetUid::from(1), u64f64(0.2)); + shares.insert(NetUid::from(2), u64f64(0.2)); + shares.insert(NetUid::from(3), u64f64(0.2)); + shares.insert(NetUid::from(4), u64f64(0.2)); + shares.insert(NetUid::from(5), u64f64(0.2)); + + // Step 1: ERP scaling zeros subnets 1 and 2 (ERP=0), leaves 3,4,5 + SubtensorModule::apply_effective_root_prop_scaling(&mut shares); + let nonzero_after_erp = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!(nonzero_after_erp, 3); + + // Step 2: Proportion at 100% keeps all + SubtensorModule::apply_top_subnet_proportion_filter(&mut shares); + let nonzero_after_prop = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!(nonzero_after_prop, 3); + + // Step 3: Absolute limit of 2 trims to top 2 nonzero by share + SubtensorModule::apply_top_subnet_absolute_limit(&mut shares); + let nonzero_final = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!(nonzero_final, 2); + + // Subnets 1 and 2 were zeroed by ERP, subnet 3 zeroed by absolute limit + 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, 0.0, epsilon = 1e-12); + + let s4 = shares.get(&NetUid::from(4)).unwrap().to_num::(); + let s5 = shares.get(&NetUid::from(5)).unwrap().to_num::(); + assert!(s4 > 0.0); + assert!(s5 > 0.0); + assert_abs_diff_eq!(s4 + s5, 1.0, epsilon = 1e-9); + }); +} + +#[test] +fn test_full_filter_chain_all_three_filters_active_and_binding() { + // ERP scaling differentiates shares, proportion filter trims further, + // absolute limit trims even further. Each stage reduces nonzero count. + new_test_ext(1).execute_with(|| { + EffectiveRootPropEmissionScaling::::set(true); + EmissionTopSubnetProportion::::set(U64F64::saturating_from_num(0.5)); // 50% + EmissionTopSubnetAbsoluteLimit::::set(Some(2)); + + // 6 subnets, all start equal. After ERP scaling, subnet 6 has the highest + // effective share because it has the highest min(ERP, RP). + EffectiveRootProp::::insert(NetUid::from(1), U96F32::from_num(0.1)); + EffectiveRootProp::::insert(NetUid::from(2), U96F32::from_num(0.2)); + EffectiveRootProp::::insert(NetUid::from(3), U96F32::from_num(0.3)); + EffectiveRootProp::::insert(NetUid::from(4), U96F32::from_num(0.4)); + EffectiveRootProp::::insert(NetUid::from(5), U96F32::from_num(0.5)); + EffectiveRootProp::::insert(NetUid::from(6), U96F32::from_num(0.6)); + + // RootProp >= ERP for all, so min(ERP, RP) = ERP + for i in 1u16..=6 { + RootProp::::insert(NetUid::from(i), U96F32::from_num(1.0)); + } + + let mut shares: BTreeMap = BTreeMap::new(); + for i in 1u16..=6 { + shares.insert(NetUid::from(i), u64f64(1.0 / 6.0)); + } + + // Step 1: ERP scaling. Each share *= its ERP, then re-normalize. + // After: shares proportional to [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] + SubtensorModule::apply_effective_root_prop_scaling(&mut shares); + let nonzero_after_erp = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!(nonzero_after_erp, 6); + + // Step 2: Proportion filter at 50% of 6 = ceil(3) = 3. Keep top 3 by share. + // That's subnets 4, 5, 6. + SubtensorModule::apply_top_subnet_proportion_filter(&mut shares); + let nonzero_after_prop = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!(nonzero_after_prop, 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::(); + 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); + + // Step 3: Absolute limit of 2 trims to top 2. Subnets 5 and 6. + SubtensorModule::apply_top_subnet_absolute_limit(&mut shares); + let nonzero_final = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!(nonzero_final, 2); + + let s4 = shares.get(&NetUid::from(4)).unwrap().to_num::(); + let s5 = shares.get(&NetUid::from(5)).unwrap().to_num::(); + let s6 = shares.get(&NetUid::from(6)).unwrap().to_num::(); + assert_abs_diff_eq!(s4, 0.0, epsilon = 1e-12); + assert!(s5 > 0.0); + assert!(s6 > 0.0); + assert_abs_diff_eq!(s5 + s6, 1.0, epsilon = 1e-9); + }); +} + +// =========================================================================== +// Tie-inclusion tests for zero_and_redistribute_bottom_shares +// =========================================================================== + +#[test] +fn test_zero_and_redistribute_bottom_shares_multiple_ties_at_cutoff_all_kept() { + // 5 subnets: A=0.4, B=0.2, C=0.2, D=0.2, E=0.0 with top_k=2. + // Top 1 is A (0.4). The 2nd position threshold is 0.2. + // B, C, D all tie at 0.2 (the cutoff), so all must be included. + // Result: 4 nonzero subnets (exceeding top_k=2). + let mut shares: BTreeMap = BTreeMap::new(); + shares.insert(NetUid::from(1), u64f64(0.4)); + shares.insert(NetUid::from(2), u64f64(0.2)); + shares.insert(NetUid::from(3), u64f64(0.2)); + shares.insert(NetUid::from(4), u64f64(0.2)); + shares.insert(NetUid::from(5), u64f64(0.0)); + + SubtensorModule::zero_and_redistribute_bottom_shares(&mut shares, 2); + + 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::(); + + // A and all three tied subnets should be kept (4 nonzero, exceeding top_k=2) + assert!(s1 > 0.0, "Subnet 1 (highest) should be kept"); + assert!(s2 > 0.0, "Subnet 2 should be kept (tie at cutoff)"); + assert!(s3 > 0.0, "Subnet 3 should be kept (tie at cutoff)"); + assert!(s4 > 0.0, "Subnet 4 should be kept (tie at cutoff)"); + assert_abs_diff_eq!(s5, 0.0, epsilon = 1e-12); // Subnet 5 (zero) should stay zero + + let nonzero_count = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!( + nonzero_count, 4, + "Tie inclusion should allow more than top_k nonzero subnets" + ); + assert_abs_diff_eq!(s1 + s2 + s3 + s4, 1.0, epsilon = 1e-9); +} + +#[test] +fn test_zero_and_redistribute_bottom_shares_all_equal_top_k_less_than_total() { + // When all subnets have equal shares and top_k < total, all should be kept + // because they all tie at the cutoff value. + let mut shares: BTreeMap = BTreeMap::new(); + shares.insert(NetUid::from(1), u64f64(0.2)); + shares.insert(NetUid::from(2), u64f64(0.2)); + shares.insert(NetUid::from(3), u64f64(0.2)); + shares.insert(NetUid::from(4), u64f64(0.2)); + shares.insert(NetUid::from(5), u64f64(0.2)); + + SubtensorModule::zero_and_redistribute_bottom_shares(&mut shares, 1); + + // All 5 subnets should be kept because they all tie at the threshold + for i in 1u16..=5 { + let s = shares.get(&NetUid::from(i)).unwrap().to_num::(); + assert!( + s > 0.0, + "Subnet {i} should be kept (all tied at cutoff with top_k=1)" + ); + } + + let nonzero_count = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!( + nonzero_count, 5, + "All subnets should survive when they all tie" + ); + + let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); + assert_abs_diff_eq!(sum, 1.0, epsilon = 1e-9); +} + +#[test] +fn test_zero_and_redistribute_bottom_shares_large_tie_group_exceeds_top_k() { + // 6 subnets: top 1 distinct, then 5 tied at the cutoff. top_k=3. + // Threshold = value at position 2 (0-indexed). Positions 0-4 have >= threshold. + // So 6 nonzero (all tied subnets kept), exceeding top_k=3. + let mut shares: BTreeMap = BTreeMap::new(); + shares.insert(NetUid::from(1), u64f64(0.5)); + shares.insert(NetUid::from(2), u64f64(0.1)); + shares.insert(NetUid::from(3), u64f64(0.1)); + shares.insert(NetUid::from(4), u64f64(0.1)); + shares.insert(NetUid::from(5), u64f64(0.1)); + shares.insert(NetUid::from(6), u64f64(0.1)); + + SubtensorModule::zero_and_redistribute_bottom_shares(&mut shares, 3); + + // The threshold is set at position 2 (top_k-1=2), which has value 0.1. + // All 5 subnets with 0.1 tie at the cutoff + subnet 1 at 0.5. + // All 6 should be kept. + let nonzero_count = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!( + nonzero_count, 6, + "All 6 subnets kept: 1 above threshold + 5 tied at threshold" + ); + + let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); + assert_abs_diff_eq!(sum, 1.0, epsilon = 1e-9); +} + +// =========================================================================== +// Tie-inclusion test for apply_top_subnet_proportion_filter +// =========================================================================== + +#[test] +fn test_apply_top_subnet_proportion_filter_ties_at_boundary_included() { + // 5 subnets with shares: A=0.4, B=0.2, C=0.2, D=0.1, E=0.1 + // Proportion = 40% -> ceil(5 * 0.4) = 2 -> top_k=2. + // Top by share: A=0.4 (1st), B=0.2 (2nd-tied), C=0.2 (2nd-tied). + // Threshold = 0.2 (value at position 1). B and C tie at boundary, + // both should be included -> 3 nonzero (exceeding top_k=2). + new_test_ext(1).execute_with(|| { + EmissionTopSubnetProportion::::set(U64F64::saturating_from_num(0.4)); // 40% + + let mut shares: BTreeMap = BTreeMap::new(); + shares.insert(NetUid::from(1), u64f64(0.4)); + shares.insert(NetUid::from(2), u64f64(0.2)); + shares.insert(NetUid::from(3), u64f64(0.2)); + shares.insert(NetUid::from(4), u64f64(0.1)); + shares.insert(NetUid::from(5), u64f64(0.1)); + + 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::(); + let s4 = shares.get(&NetUid::from(4)).unwrap().to_num::(); + let s5 = shares.get(&NetUid::from(5)).unwrap().to_num::(); + + assert!(s1 > 0.0, "Subnet 1 (highest) should be kept"); + assert!( + s2 > 0.0, + "Subnet 2 should be kept (tie at proportion boundary)" + ); + assert!( + s3 > 0.0, + "Subnet 3 should be kept (tie at proportion boundary)" + ); + assert_abs_diff_eq!(s4, 0.0, epsilon = 1e-12); // Subnet 4 should be zeroed + assert_abs_diff_eq!(s5, 0.0, epsilon = 1e-12); // Subnet 5 should be zeroed + + let nonzero_count = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!( + nonzero_count, 3, + "Tie inclusion means 3 subnets kept, exceeding ceil(5*0.4)=2" + ); + + assert_abs_diff_eq!(s1 + s2 + s3, 1.0, epsilon = 1e-9); + }); +} + +#[test] +fn test_apply_top_subnet_proportion_filter_all_equal_shares() { + // When all subnets have equal shares and proportion < 1.0, + // all tie at the cutoff -> all should be kept. + new_test_ext(1).execute_with(|| { + EmissionTopSubnetProportion::::set(U64F64::saturating_from_num(0.25)); // 25% + + 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)); + + SubtensorModule::apply_top_subnet_proportion_filter(&mut shares); + + // All tie -> all should be kept + let nonzero_count = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!( + nonzero_count, 4, + "All 4 subnets kept because they all tie at the cutoff" + ); + + let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); + assert_abs_diff_eq!(sum, 1.0, epsilon = 1e-9); + }); +} + +// =========================================================================== +// Tie-inclusion test for apply_top_subnet_absolute_limit +// =========================================================================== + +#[test] +fn test_apply_top_subnet_absolute_limit_ties_at_boundary_included() { + // 5 subnets with shares: A=0.4, B=0.2, C=0.2, D=0.1, E=0.1 + // Absolute limit = 2. Top 2 by share: A=0.4 (1st), B=0.2 (2nd-tied), C=0.2 (2nd-tied). + // Both B and C tie at boundary -> 3 nonzero (exceeding limit=2). + new_test_ext(1).execute_with(|| { + EmissionTopSubnetAbsoluteLimit::::set(Some(2)); + + let mut shares: BTreeMap = BTreeMap::new(); + shares.insert(NetUid::from(1), u64f64(0.4)); + shares.insert(NetUid::from(2), u64f64(0.2)); + shares.insert(NetUid::from(3), u64f64(0.2)); + shares.insert(NetUid::from(4), u64f64(0.1)); + shares.insert(NetUid::from(5), u64f64(0.1)); + + SubtensorModule::apply_top_subnet_absolute_limit(&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::(); + let s4 = shares.get(&NetUid::from(4)).unwrap().to_num::(); + let s5 = shares.get(&NetUid::from(5)).unwrap().to_num::(); + + assert!(s1 > 0.0, "Subnet 1 (highest) should be kept"); + assert!( + s2 > 0.0, + "Subnet 2 should be kept (tie at absolute limit boundary)" + ); + assert!( + s3 > 0.0, + "Subnet 3 should be kept (tie at absolute limit boundary)" + ); + assert_abs_diff_eq!(s4, 0.0, epsilon = 1e-12); // Subnet 4 should be zeroed + assert_abs_diff_eq!(s5, 0.0, epsilon = 1e-12); // Subnet 5 should be zeroed + + let nonzero_count = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!( + nonzero_count, 3, + "Tie inclusion means 3 subnets kept, exceeding limit=2" + ); + + assert_abs_diff_eq!(s1 + s2 + s3, 1.0, epsilon = 1e-9); + // Verify normalization: 0.4/0.8 = 0.5, 0.2/0.8 = 0.25 each + assert_abs_diff_eq!(s1, 0.4 / 0.8, epsilon = 1e-9); + assert_abs_diff_eq!(s2, 0.2 / 0.8, epsilon = 1e-9); + assert_abs_diff_eq!(s3, 0.2 / 0.8, epsilon = 1e-9); + }); +} + +#[test] +fn test_apply_top_subnet_absolute_limit_all_equal_shares() { + // When all subnets have equal shares and limit < total nonzero, + // all tie at the cutoff -> all should be kept. + new_test_ext(1).execute_with(|| { + EmissionTopSubnetAbsoluteLimit::::set(Some(1)); + + let mut shares: BTreeMap = BTreeMap::new(); + shares.insert(NetUid::from(1), u64f64(0.2)); + shares.insert(NetUid::from(2), u64f64(0.2)); + shares.insert(NetUid::from(3), u64f64(0.2)); + shares.insert(NetUid::from(4), u64f64(0.2)); + shares.insert(NetUid::from(5), u64f64(0.2)); + + SubtensorModule::apply_top_subnet_absolute_limit(&mut shares); + + // All tie -> all should be kept despite limit=1 + let nonzero_count = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!( + nonzero_count, 5, + "All 5 subnets kept because they all tie at the cutoff (limit=1)" + ); + + let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); + assert_abs_diff_eq!(sum, 1.0, epsilon = 1e-9); + }); +} + +#[test] +fn test_apply_top_subnet_absolute_limit_ties_with_large_tie_group() { + // 7 subnets: one at 0.3, six tied at ~0.116667. Limit=3. + // Threshold at position 2 = ~0.116667. All 6 tied subnets >= threshold. + // So all 7 should be kept. + new_test_ext(1).execute_with(|| { + EmissionTopSubnetAbsoluteLimit::::set(Some(3)); + + let tied_share = 0.7 / 6.0; // ~0.116667 + let mut shares: BTreeMap = BTreeMap::new(); + shares.insert(NetUid::from(1), u64f64(0.3)); + for i in 2u16..=7 { + shares.insert(NetUid::from(i), u64f64(tied_share)); + } + + SubtensorModule::apply_top_subnet_absolute_limit(&mut shares); + + let nonzero_count = shares.values().filter(|v| v.to_num::() > 0.0).count(); + assert_eq!( + nonzero_count, 7, + "All 7 kept: 1 above threshold + 6 tied at threshold, exceeding limit=3" + ); + + let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); + assert_abs_diff_eq!(sum, 1.0, epsilon = 1e-9); + }); +} + +// =========================================================================== +// Tests for get_root_dividend_fraction +// =========================================================================== + +#[test] +fn test_root_dividend_fraction_no_root_stake() { + // Hotkey with 0 root stake → fraction = 0 + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(100); + let coldkey = U256::from(200); + let tao_weight = U96F32::from_num(0.18); + + // Only alpha stake, no root stake + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + AlphaCurrency::from(1_000_000u64), + ); + + let frac = SubtensorModule::get_root_dividend_fraction(&hotkey, netuid, tao_weight); + assert_abs_diff_eq!(frac.to_num::(), 0.0, epsilon = 1e-12); + }); +} + +#[test] +fn test_root_dividend_fraction_no_alpha_stake() { + // Hotkey with only root stake → fraction = 1.0 + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(100); + let coldkey = U256::from(200); + let tao_weight = U96F32::from_num(0.18); + + // Only root stake, no alpha + increase_stake_on_coldkey_hotkey_account( + &coldkey, + &hotkey, + 1_000_000u64.into(), + NetUid::ROOT, + ); + + let frac = SubtensorModule::get_root_dividend_fraction(&hotkey, netuid, tao_weight); + assert_abs_diff_eq!(frac.to_num::(), 1.0, epsilon = 1e-9); + }); +} + +#[test] +fn test_root_dividend_fraction_mixed_stake() { + // Hotkey with both root and alpha stake + // root_alpha_weighted = 1_000_000 * 0.18 = 180_000 + // alpha_stake = 820_000 + // fraction = 180_000 / (820_000 + 180_000) = 0.18 + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(100); + let coldkey = U256::from(200); + let tao_weight = U96F32::from_num(0.18); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + AlphaCurrency::from(820_000u64), + ); + increase_stake_on_coldkey_hotkey_account( + &coldkey, + &hotkey, + 1_000_000u64.into(), + NetUid::ROOT, + ); + + let frac = SubtensorModule::get_root_dividend_fraction(&hotkey, netuid, tao_weight); + assert_abs_diff_eq!(frac.to_num::(), 0.18, epsilon = 1e-6); + }); +} + +#[test] +fn test_root_dividend_fraction_high_tao_weight() { + // With high tao_weight, root fraction approaches 1.0 + // root_alpha_weighted = 100 * 10.0 = 1000 + // alpha_stake = 100 + // fraction = 1000 / (100 + 1000) ≈ 0.909 + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(100); + let coldkey = U256::from(200); + let tao_weight = U96F32::from_num(10); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + AlphaCurrency::from(100u64), + ); + increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, 100u64.into(), NetUid::ROOT); + + let frac = SubtensorModule::get_root_dividend_fraction(&hotkey, netuid, tao_weight); + assert_abs_diff_eq!(frac.to_num::(), 10.0 / 11.0, epsilon = 1e-6); + }); +} + +#[test] +fn test_get_root_dividend_fraction_zero_tao_weight() { + // With tao_weight = 0, root_alpha_weighted = root_stake * 0 = 0 + // fraction = 0 / (alpha_stake + 0) = 0 + new_test_ext(1).execute_with(|| { + let (netuid, hotkey1, _hotkey2) = setup_scaling_test(); + let tao_weight = U96F32::from_num(0); + + let frac = SubtensorModule::get_root_dividend_fraction(&hotkey1, netuid, tao_weight); + assert_abs_diff_eq!(frac.to_num::(), 0.0, epsilon = 1e-12); + }); +} + +#[test] +fn test_get_root_dividend_fraction_no_stake_at_all() { + // Hotkey with no stake anywhere (no root, no alpha) → fraction = 0 + // Early return when root_stake_f <= 0 + new_test_ext(1).execute_with(|| { + let (netuid, _hotkey1, _hotkey2) = setup_scaling_test(); + let hotkey_no_stake = U256::from(999); + let tao_weight = U96F32::from_num(0.18); + + let frac = + SubtensorModule::get_root_dividend_fraction(&hotkey_no_stake, netuid, tao_weight); + assert_abs_diff_eq!(frac.to_num::(), 0.0, epsilon = 1e-12); + }); +} + +#[test] +fn test_root_proportion_computation() { + // Test the root_proportion() function that computes: + // root_proportion = (tao_weight * SubnetTAO(ROOT)) / ((tao_weight * SubnetTAO(ROOT)) + alpha_issuance) + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + + // Scenario 1: Zero root TAO → root_proportion should be 0 + { + // Set SubnetTAO for ROOT to 0 + SubnetTAO::::insert(NetUid::ROOT, TaoCurrency::from(0u64)); + + // Set alpha issuance components (only SubnetAlphaOut for simplicity) + SubnetAlphaOut::::insert(netuid, AlphaCurrency::from(1_000_000u64)); + + // Set TaoWeight to 0.18 (18% of u64::MAX) + TaoWeight::::set(u64::MAX / 100 * 18); + + let root_prop = SubtensorModule::root_proportion(netuid); + assert_abs_diff_eq!(root_prop.to_num::(), 0.0, epsilon = 1e-12); + } + + // Scenario 2: Zero alpha issuance, nonzero root → should return close to 1.0 + { + // Set SubnetTAO for ROOT to a nonzero value + SubnetTAO::::insert(NetUid::ROOT, TaoCurrency::from(1_000_000u64)); + + // Set alpha issuance to 0 (clear all components) + SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(0u64)); + SubnetAlphaInProvided::::insert(netuid, AlphaCurrency::from(0u64)); + SubnetAlphaOut::::insert(netuid, AlphaCurrency::from(0u64)); + + // Set TaoWeight to 0.18 + TaoWeight::::set(u64::MAX / 100 * 18); + + let root_prop = SubtensorModule::root_proportion(netuid); + // With zero alpha issuance, denominator = tao_weight * root_tao + 0 + // So root_prop = tao_weight * root_tao / tao_weight * root_tao = 1.0 + assert_abs_diff_eq!(root_prop.to_num::(), 1.0, epsilon = 1e-9); + } + + // Scenario 3: Balanced - equal weighted root and alpha → should be ~0.5 + { + // Set SubnetTAO for ROOT + SubnetTAO::::insert(NetUid::ROOT, TaoCurrency::from(1_000_000u64)); + + // Set TaoWeight to 0.5 (50% of u64::MAX) + TaoWeight::::set(u64::MAX / 2); + + // Set alpha issuance such that alpha_issuance = tao_weight * root_tao + // tao_weight * root_tao = 0.5 * 1_000_000 = 500_000 + // So we need alpha_issuance = 500_000 + SubnetAlphaOut::::insert(netuid, AlphaCurrency::from(500_000u64)); + SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(0u64)); + SubnetAlphaInProvided::::insert(netuid, AlphaCurrency::from(0u64)); + + let root_prop = SubtensorModule::root_proportion(netuid); + // root_prop = 500_000 / (500_000 + 500_000) = 0.5 + assert_abs_diff_eq!(root_prop.to_num::(), 0.5, epsilon = 1e-9); + } + }); +} + +// =========================================================================== +// Tests for apply_utilization_scaling +// =========================================================================== + +/// Helper: set up a subnet with hotkeys that have root + alpha stakes. +/// Returns (netuid, hotkey1, hotkey2). +fn setup_scaling_test() -> (NetUid, U256, U256) { + let netuid = NetUid::from(1); + let hotkey1 = U256::from(100); + let coldkey1 = U256::from(200); + let hotkey2 = U256::from(101); + let coldkey2 = U256::from(201); + + // hotkey1: 900k alpha, 1M root + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &coldkey1, + netuid, + AlphaCurrency::from(900_000u64), + ); + increase_stake_on_coldkey_hotkey_account( + &coldkey1, + &hotkey1, + 1_000_000u64.into(), + NetUid::ROOT, + ); + + // hotkey2: 500k alpha, 500k root + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey2, + netuid, + AlphaCurrency::from(500_000u64), + ); + increase_stake_on_coldkey_hotkey_account(&coldkey2, &hotkey2, 500_000u64.into(), NetUid::ROOT); + + // Need SubnetAlphaOut for recycling to work + SubnetAlphaOut::::insert(netuid, AlphaCurrency::from(10_000_000u64)); + + (netuid, hotkey1, hotkey2) +} + +#[test] +fn test_apply_utilization_scaling_full_utilization() { + // utilization = 1.0 → no scaling, returns 0 recycled + new_test_ext(1).execute_with(|| { + let (netuid, hotkey1, hotkey2) = setup_scaling_test(); + let tao_weight = U96F32::from_num(0.18); + let utilization = U96F32::from_num(1); + + let mut alpha_divs: BTreeMap = BTreeMap::new(); + alpha_divs.insert(hotkey1, U96F32::from_num(10000)); + alpha_divs.insert(hotkey2, U96F32::from_num(5000)); + + let mut root_divs: BTreeMap = BTreeMap::new(); + root_divs.insert(hotkey1, U96F32::from_num(2000)); + root_divs.insert(hotkey2, U96F32::from_num(1000)); + + let alpha_divs_before = alpha_divs.clone(); + let root_divs_before = root_divs.clone(); + + let recycled = SubtensorModule::apply_utilization_scaling( + netuid, + utilization, + &mut alpha_divs, + &mut root_divs, + tao_weight, + ); + + assert_abs_diff_eq!(recycled.to_num::(), 0.0, epsilon = 1e-12); + // Maps unchanged + assert_eq!(alpha_divs, alpha_divs_before); + assert_eq!(root_divs, root_divs_before); + }); +} + +#[test] +fn test_apply_utilization_scaling_no_root_dividends() { + // Empty root dividends → no scaling regardless of utilization, returns 0 + new_test_ext(1).execute_with(|| { + let (netuid, hotkey1, _hotkey2) = setup_scaling_test(); + let tao_weight = U96F32::from_num(0.18); + let utilization = U96F32::from_num(0); // Would normally trigger hard cap + + let mut alpha_divs: BTreeMap = BTreeMap::new(); + alpha_divs.insert(hotkey1, U96F32::from_num(10000)); + + let mut root_divs: BTreeMap = BTreeMap::new(); // empty + + let alpha_divs_before = alpha_divs.clone(); + + let recycled = SubtensorModule::apply_utilization_scaling( + netuid, + utilization, + &mut alpha_divs, + &mut root_divs, + tao_weight, + ); + + assert_abs_diff_eq!(recycled.to_num::(), 0.0, epsilon = 1e-12); + // Alpha divs unchanged (no root dividends to trigger scaling) + assert_eq!(alpha_divs, alpha_divs_before); + }); +} + +#[test] +fn test_apply_utilization_scaling_partial() { + // utilization = 0.7 >= 0.5 → full dividends, no scaling, no recycling + new_test_ext(1).execute_with(|| { + let (netuid, hotkey1, hotkey2) = setup_scaling_test(); + let tao_weight = U96F32::from_num(0.18); + let utilization = U96F32::from_num(0.7); + + let mut alpha_divs: BTreeMap = BTreeMap::new(); + alpha_divs.insert(hotkey1, U96F32::from_num(10000)); + alpha_divs.insert(hotkey2, U96F32::from_num(5000)); + + let mut root_divs: BTreeMap = BTreeMap::new(); + root_divs.insert(hotkey1, U96F32::from_num(2000)); + root_divs.insert(hotkey2, U96F32::from_num(1000)); + + let recycled = SubtensorModule::apply_utilization_scaling( + netuid, + utilization, + &mut alpha_divs, + &mut root_divs, + tao_weight, + ); + + // Root dividends should be unchanged (no scaling when util >= 0.5) + assert_abs_diff_eq!( + root_divs.get(&hotkey1).unwrap().to_num::(), + 2000.0, + epsilon = 1.0 + ); + assert_abs_diff_eq!( + root_divs.get(&hotkey2).unwrap().to_num::(), + 1000.0, + epsilon = 1.0 + ); + + // Alpha divs should be unchanged + assert_abs_diff_eq!( + alpha_divs.get(&hotkey1).unwrap().to_num::(), + 10000.0, + epsilon = 1.0 + ); + assert_abs_diff_eq!( + alpha_divs.get(&hotkey2).unwrap().to_num::(), + 5000.0, + epsilon = 1.0 + ); + + // Nothing recycled + assert_abs_diff_eq!(recycled.to_num::(), 0.0, epsilon = 1e-12); + }); +} + +#[test] +fn test_apply_utilization_scaling_hard_cap() { + // utilization = 0.3 < 0.5 → hard cap: recycle ALL root dividends, set ERP = 0 + new_test_ext(1).execute_with(|| { + let (netuid, hotkey1, hotkey2) = setup_scaling_test(); + let tao_weight = U96F32::from_num(0.18); + let utilization = U96F32::from_num(0.3); + + // Set a non-zero ERP so we can verify it gets zeroed + EffectiveRootProp::::insert(netuid, U96F32::from_num(0.5)); + + let mut alpha_divs: BTreeMap = BTreeMap::new(); + alpha_divs.insert(hotkey1, U96F32::from_num(10000)); + alpha_divs.insert(hotkey2, U96F32::from_num(5000)); + + let mut root_divs: BTreeMap = BTreeMap::new(); + root_divs.insert(hotkey1, U96F32::from_num(2000)); + root_divs.insert(hotkey2, U96F32::from_num(1000)); + + let recycled = SubtensorModule::apply_utilization_scaling( + netuid, + utilization, + &mut alpha_divs, + &mut root_divs, + tao_weight, + ); + + // Root dividends should be completely cleared + assert!( + root_divs.is_empty(), + "Root divs should be empty after hard cap" + ); + + // Alpha divs should be reduced by their root fraction + let alpha1 = alpha_divs.get(&hotkey1).unwrap().to_num::(); + assert!(alpha1 < 10000.0, "Alpha divs should be reduced: {alpha1}"); + // hotkey1 root_fraction ≈ 0.1666, so alpha1 ≈ 10000 * (1 - 0.1666) ≈ 8334 + assert_abs_diff_eq!(alpha1, 8334.0, epsilon = 100.0); + + // Total recycled should account for all root divs + root fraction of alpha divs + assert!( + recycled.to_num::() > 3000.0, + "Should recycle at least the 3000 root divs" + ); + + // EffectiveRootProp should be 0 + let erp = EffectiveRootProp::::get(netuid); + assert_abs_diff_eq!(erp.to_num::(), 0.0, epsilon = 1e-12); + }); +} + +#[test] +fn test_apply_utilization_scaling_at_boundary() { + // utilization = 0.5 exactly → full dividends, NOT hard cap + new_test_ext(1).execute_with(|| { + let (netuid, hotkey1, _hotkey2) = setup_scaling_test(); + let tao_weight = U96F32::from_num(0.18); + let utilization = U96F32::from_num(0.5); + + let mut alpha_divs: BTreeMap = BTreeMap::new(); + alpha_divs.insert(hotkey1, U96F32::from_num(10000)); + + let mut root_divs: BTreeMap = BTreeMap::new(); + root_divs.insert(hotkey1, U96F32::from_num(2000)); + + let recycled = SubtensorModule::apply_utilization_scaling( + netuid, + utilization, + &mut alpha_divs, + &mut root_divs, + tao_weight, + ); + + // Root dividends should be unchanged (full dividends at boundary) + assert!( + !root_divs.is_empty(), + "Root divs should NOT be empty at boundary 0.5" + ); + assert_abs_diff_eq!( + root_divs.get(&hotkey1).unwrap().to_num::(), + 2000.0, + epsilon = 1.0 + ); + + // Alpha divs should be unchanged + assert_abs_diff_eq!( + alpha_divs.get(&hotkey1).unwrap().to_num::(), + 10000.0, + epsilon = 1.0 + ); + + // Nothing recycled + assert_abs_diff_eq!(recycled.to_num::(), 0.0, epsilon = 1e-12); + }); +} + +#[test] +fn test_apply_utilization_scaling_just_below_boundary() { + // utilization = 0.4999 → hard cap triggers + new_test_ext(1).execute_with(|| { + let (netuid, hotkey1, _hotkey2) = setup_scaling_test(); + let tao_weight = U96F32::from_num(0.18); + let utilization = U96F32::from_num(0.4999); + + EffectiveRootProp::::insert(netuid, U96F32::from_num(0.5)); + + let mut alpha_divs: BTreeMap = BTreeMap::new(); + alpha_divs.insert(hotkey1, U96F32::from_num(10000)); + + let mut root_divs: BTreeMap = BTreeMap::new(); + root_divs.insert(hotkey1, U96F32::from_num(2000)); + + SubtensorModule::apply_utilization_scaling( + netuid, + utilization, + &mut alpha_divs, + &mut root_divs, + tao_weight, + ); + + // Hard cap: root divs cleared, ERP = 0 + assert!(root_divs.is_empty(), "Root divs should be empty below 0.5"); + assert_abs_diff_eq!( + EffectiveRootProp::::get(netuid).to_num::(), + 0.0, + epsilon = 1e-12 + ); + }); +} + +#[test] +fn test_apply_utilization_scaling_no_root_stake_at_all() { + // Verify early exit when root_alpha_dividends is empty (pure alpha-only subnet) + // Even with low utilization, nothing should happen since there are no root dividends + new_test_ext(1).execute_with(|| { + let (netuid, hotkey1, hotkey2) = setup_scaling_test(); + let tao_weight = U96F32::from_num(0.18); + let utilization = U96F32::from_num(0); // Low utilization - doesn't matter + + // Create alpha_dividends with entries + let mut alpha_divs: BTreeMap = BTreeMap::new(); + alpha_divs.insert(hotkey1, U96F32::from_num(10000)); + alpha_divs.insert(hotkey2, U96F32::from_num(5000)); + + // Empty root_alpha_dividends (pure alpha-only subnet with no root stake) + let mut root_divs: BTreeMap = BTreeMap::new(); + + let alpha_divs_before = alpha_divs.clone(); + + let recycled = SubtensorModule::apply_utilization_scaling( + netuid, + utilization, + &mut alpha_divs, + &mut root_divs, + tao_weight, + ); + + // Should return 0 recycled (early exit on !has_root_dividends) + assert_abs_diff_eq!(recycled.to_num::(), 0.0, epsilon = 1e-12); + + // alpha_dividends should be unchanged + assert_eq!(alpha_divs, alpha_divs_before); + assert_abs_diff_eq!( + alpha_divs.get(&hotkey1).unwrap().to_num::(), + 10000.0, + epsilon = 1.0 + ); + assert_abs_diff_eq!( + alpha_divs.get(&hotkey2).unwrap().to_num::(), + 5000.0, + epsilon = 1.0 + ); + + // root_alpha_dividends should still be empty + assert!(root_divs.is_empty(), "Root divs should remain empty"); + }); +} + +#[test] +fn test_apply_utilization_scaling_zero_utilization() { + // utilization = 0 → hard cap: recycle ALL root dividends, set ERP = 0 + new_test_ext(1).execute_with(|| { + let (netuid, hotkey1, hotkey2) = setup_scaling_test(); + let tao_weight = U96F32::from_num(0.18); + let utilization = U96F32::from_num(0); + + // Set a non-zero ERP so we can verify it gets zeroed + EffectiveRootProp::::insert(netuid, U96F32::from_num(0.5)); + + let mut alpha_divs: BTreeMap = BTreeMap::new(); + alpha_divs.insert(hotkey1, U96F32::from_num(10000)); + alpha_divs.insert(hotkey2, U96F32::from_num(5000)); + + let mut root_divs: BTreeMap = BTreeMap::new(); + root_divs.insert(hotkey1, U96F32::from_num(2000)); + root_divs.insert(hotkey2, U96F32::from_num(1000)); + + let recycled = SubtensorModule::apply_utilization_scaling( + netuid, + utilization, + &mut alpha_divs, + &mut root_divs, + tao_weight, + ); + + // Root dividends should be completely cleared + assert!( + root_divs.is_empty(), + "Root divs should be empty after hard cap" + ); + + // Alpha divs should be reduced by their root fraction + let alpha1 = alpha_divs.get(&hotkey1).unwrap().to_num::(); + assert!(alpha1 < 10000.0, "Alpha divs should be reduced: {alpha1}"); + // hotkey1 root_fraction ≈ 0.1666, so alpha1 ≈ 10000 * (1 - 0.1666) ≈ 8334 + assert_abs_diff_eq!(alpha1, 8334.0, epsilon = 100.0); + + // Total recycled should account for all root divs + root fraction of alpha divs + assert!( + recycled.to_num::() > 3000.0, + "Should recycle at least the 3000 root divs" + ); + + // EffectiveRootProp should be 0 + let erp = EffectiveRootProp::::get(netuid); + assert_abs_diff_eq!(erp.to_num::(), 0.0, epsilon = 1e-12); + }); +} + +#[test] +fn test_apply_utilization_scaling_hotkey_only_root_stake() { + // Test case: hotkey with ONLY root stake (no alpha stake on subnet) + // Should have root_fraction = 1.0, causing all alpha dividends to be recycled + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey1 = U256::from(100); + let coldkey1 = U256::from(200); + let hotkey2 = U256::from(101); + let coldkey2 = U256::from(201); + + // hotkey1: 0 alpha stake, 1M root stake (only root stake) + increase_stake_on_coldkey_hotkey_account( + &coldkey1, + &hotkey1, + 1_000_000u64.into(), + NetUid::ROOT, + ); + + // hotkey2: 500k alpha stake, 0 root stake (for comparison) + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey2, + netuid, + AlphaCurrency::from(500_000u64), + ); + + // Need SubnetAlphaOut for recycling to work + SubnetAlphaOut::::insert(netuid, AlphaCurrency::from(10_000_000u64)); + + let tao_weight = U96F32::from_num(0.18); + let utilization = U96F32::from_num(0.3); // Triggers hard cap + + // Set a non-zero ERP so we can verify it gets zeroed + EffectiveRootProp::::insert(netuid, U96F32::from_num(0.5)); + + let mut alpha_divs: BTreeMap = BTreeMap::new(); + alpha_divs.insert(hotkey1, U96F32::from_num(8000)); + alpha_divs.insert(hotkey2, U96F32::from_num(5000)); + + let mut root_divs: BTreeMap = BTreeMap::new(); + root_divs.insert(hotkey1, U96F32::from_num(2000)); + + let recycled = SubtensorModule::apply_utilization_scaling( + netuid, + utilization, + &mut alpha_divs, + &mut root_divs, + tao_weight, + ); + + // Root dividends should be completely cleared + assert!( + root_divs.is_empty(), + "Root divs should be empty after hard cap" + ); + + // hotkey1 has only root stake (root_fraction = 1.0), so ALL alpha dividends should be recycled + let alpha1 = alpha_divs.get(&hotkey1).unwrap().to_num::(); + assert_abs_diff_eq!(alpha1, 0.0, epsilon = 1.0); + + // hotkey2 has no root stake (root_fraction = 0.0), so alpha dividends should be unchanged + let alpha2 = alpha_divs.get(&hotkey2).unwrap().to_num::(); + assert_abs_diff_eq!(alpha2, 5000.0, epsilon = 1.0); + + // Total recycled should include all root divs (2000) + all of hotkey1's alpha divs (8000) + let recycled_val = recycled.to_num::(); + assert_abs_diff_eq!(recycled_val, 10000.0, epsilon = 100.0); + + // EffectiveRootProp should be 0 + let erp = EffectiveRootProp::::get(netuid); + assert_abs_diff_eq!(erp.to_num::(), 0.0, epsilon = 1e-12); + }); +} + +#[test] +fn test_remove_network_cleans_root_alpha_dividends_per_subnet() { + new_test_ext(1).execute_with(|| { + // Setup: Create a subnet + let owner_hotkey = U256::from(100); + let owner_coldkey = U256::from(200); + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Create some hotkeys to test cleanup + let hotkey1 = U256::from(101); + let hotkey2 = U256::from(102); + + // Insert entries into RootAlphaDividendsPerSubnet + RootAlphaDividendsPerSubnet::::insert(netuid, hotkey1, AlphaCurrency::from(1000u64)); + RootAlphaDividendsPerSubnet::::insert(netuid, hotkey2, AlphaCurrency::from(2000u64)); + + // Insert entries into EffectiveRootProp, RootProp, and RootClaimableThreshold + EffectiveRootProp::::insert(netuid, U96F32::from_num(0.5)); + RootProp::::insert(netuid, U96F32::from_num(0.3)); + RootClaimableThreshold::::insert(netuid, I96F32::from_num(100)); + + // Verify that the data exists before removal + assert_eq!( + RootAlphaDividendsPerSubnet::::get(netuid, hotkey1), + AlphaCurrency::from(1000u64) + ); + assert_eq!( + RootAlphaDividendsPerSubnet::::get(netuid, hotkey2), + AlphaCurrency::from(2000u64) + ); + assert_eq!(EffectiveRootProp::::get(netuid).to_num::(), 0.5); + assert_abs_diff_eq!( + RootProp::::get(netuid).to_num::(), + 0.3, + epsilon = 1e-6 + ); + assert_abs_diff_eq!( + RootClaimableThreshold::::get(netuid).to_num::(), + 100.0, + epsilon = 1e-6 + ); + + // Action: Remove the network + SubtensorModule::remove_network(netuid); + + // Assert: Verify that RootAlphaDividendsPerSubnet entries are cleaned up + assert_eq!( + RootAlphaDividendsPerSubnet::::get(netuid, hotkey1), + AlphaCurrency::from(0u64), + "RootAlphaDividendsPerSubnet for hotkey1 should be zero after remove_network" + ); + assert_eq!( + RootAlphaDividendsPerSubnet::::get(netuid, hotkey2), + AlphaCurrency::from(0u64), + "RootAlphaDividendsPerSubnet for hotkey2 should be zero after remove_network" + ); + + // Assert: Verify that EffectiveRootProp is cleaned up + assert_eq!( + EffectiveRootProp::::get(netuid).to_num::(), + 0.0, + "EffectiveRootProp should be zero after remove_network" + ); + + // Assert: Verify that RootProp is cleaned up + assert_eq!( + RootProp::::get(netuid).to_num::(), + 0.0, + "RootProp should be zero after remove_network" + ); + + // Assert: Verify that RootClaimableThreshold is cleaned up (returns default 500_000) + assert!( + !RootClaimableThreshold::::contains_key(netuid), + "RootClaimableThreshold key should be removed after remove_network" + ); + }); +} + +#[test] +fn test_finalize_all_subnet_root_dividends_cleanup() { + new_test_ext(1).execute_with(|| { + // Setup: Create a subnet (netuid=1) + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + add_network(netuid1, 1, 0); + + // Create hotkeys + let hotkey1 = U256::from(101); + let hotkey2 = U256::from(102); + let coldkey1 = U256::from(201); + let coldkey2 = U256::from(202); + + // Set up RootClaimable for both hotkeys with entries for netuid=1 and netuid=2 + let mut claimable_map1 = BTreeMap::new(); + claimable_map1.insert(netuid1, I96F32::from_num(1000.0)); + claimable_map1.insert(netuid2, I96F32::from_num(2000.0)); + RootClaimable::::insert(hotkey1, claimable_map1); + + let mut claimable_map2 = BTreeMap::new(); + claimable_map2.insert(netuid1, I96F32::from_num(1500.0)); + claimable_map2.insert(netuid2, I96F32::from_num(2500.0)); + RootClaimable::::insert(hotkey2, claimable_map2); + + // Set up RootClaimed entries for (netuid, hotkey, coldkey) pairs + // For netuid=1 + RootClaimed::::insert((netuid1, &hotkey1, &coldkey1), 500u128); + RootClaimed::::insert((netuid1, &hotkey2, &coldkey2), 600u128); + + // For netuid=2 (these should NOT be cleaned) + RootClaimed::::insert((netuid2, &hotkey1, &coldkey1), 700u128); + RootClaimed::::insert((netuid2, &hotkey2, &coldkey2), 800u128); + + // Verify setup + assert!(RootClaimable::::get(hotkey1).contains_key(&netuid1)); + assert!(RootClaimable::::get(hotkey1).contains_key(&netuid2)); + assert!(RootClaimable::::get(hotkey2).contains_key(&netuid1)); + assert!(RootClaimable::::get(hotkey2).contains_key(&netuid2)); + + assert_eq!( + RootClaimed::::get((netuid1, &hotkey1, &coldkey1)), + 500u128 + ); + assert_eq!( + RootClaimed::::get((netuid1, &hotkey2, &coldkey2)), + 600u128 + ); + assert_eq!( + RootClaimed::::get((netuid2, &hotkey1, &coldkey1)), + 700u128 + ); + assert_eq!( + RootClaimed::::get((netuid2, &hotkey2, &coldkey2)), + 800u128 + ); + + // Action: Call finalize_all_subnet_root_dividends for netuid=1 + SubtensorModule::finalize_all_subnet_root_dividends(NetUid::from(netuid1)); + + // Assert: RootClaimable for hotkey1 no longer contains netuid=1 (but still contains netuid=2) + assert!( + !RootClaimable::::get(hotkey1).contains_key(&netuid1), + "RootClaimable for hotkey1 should not contain netuid=1 after cleanup" + ); + assert!( + RootClaimable::::get(hotkey1).contains_key(&netuid2), + "RootClaimable for hotkey1 should still contain netuid=2" + ); + assert_eq!( + RootClaimable::::get(hotkey1) + .get(&netuid2) + .unwrap() + .to_num::(), + 2000.0, + "RootClaimable for hotkey1 netuid=2 should be unchanged" + ); + + // Assert: RootClaimable for hotkey2 no longer contains netuid=1 (but still contains netuid=2) + assert!( + !RootClaimable::::get(hotkey2).contains_key(&netuid1), + "RootClaimable for hotkey2 should not contain netuid=1 after cleanup" + ); + assert!( + RootClaimable::::get(hotkey2).contains_key(&netuid2), + "RootClaimable for hotkey2 should still contain netuid=2" + ); + assert_eq!( + RootClaimable::::get(hotkey2) + .get(&netuid2) + .unwrap() + .to_num::(), + 2500.0, + "RootClaimable for hotkey2 netuid=2 should be unchanged" + ); + + // Assert: RootClaimed for (netuid=1, hotkey, coldkey) is gone + assert_eq!( + RootClaimed::::get((netuid1, &hotkey1, &coldkey1)), + 0u128, + "RootClaimed for (netuid1, hotkey1, coldkey1) should be zero after cleanup" + ); + assert_eq!( + RootClaimed::::get((netuid1, &hotkey2, &coldkey2)), + 0u128, + "RootClaimed for (netuid1, hotkey2, coldkey2) should be zero after cleanup" + ); + + // Assert: RootClaimed for (netuid=2, hotkey, coldkey) is still present + assert_eq!( + RootClaimed::::get((netuid2, &hotkey1, &coldkey1)), + 700u128, + "RootClaimed for (netuid2, hotkey1, coldkey1) should remain unchanged" + ); + assert_eq!( + RootClaimed::::get((netuid2, &hotkey2, &coldkey2)), + 800u128, + "RootClaimed for (netuid2, hotkey2, coldkey2) should remain unchanged" + ); + }); +} + +#[test] +fn test_root_sell_flag_boundary() { + new_test_ext(1).execute_with(|| { + // Setup: Create owner for subnets + let owner_hotkey = U256::from(100); + let owner_coldkey = U256::from(200); + + // Create 2 subnets + let netuid1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let netuid2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Scenario 1: Total exactly at 1.0 (both at 0.5) + // Should return false (not strictly above 1.0) + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.5)); + SubnetMovingPrice::::insert(netuid2, I96F32::from_num(0.5)); + + let subnets = vec![netuid1, netuid2]; + let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&subnets); + assert!( + !root_sell_flag, + "Root sell flag should be false when total moving price equals 1.0" + ); + + // Scenario 2: Total just above 1.0 (e.g., 0.500001 and 0.500001) + // Should return true (strictly above 1.0) + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.500001)); + SubnetMovingPrice::::insert(netuid2, I96F32::from_num(0.500001)); + + let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&subnets); + assert!( + root_sell_flag, + "Root sell flag should be true when total moving price is above 1.0" + ); + + // Scenario 3: Total below 1.0 (both at 0.4) + // Should return false + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.4)); + SubnetMovingPrice::::insert(netuid2, I96F32::from_num(0.4)); + + let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&subnets); + assert!( + !root_sell_flag, + "Root sell flag should be false when total moving price is below 1.0" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::subnet_emissions::test_distribute_emission_zero_incentive_sum --exact --show-output --nocapture +#[test] +fn test_distribute_emission_zero_incentive_sum() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + + // Register 3 validator hotkeys + let validator1 = U256::from(1); + let validator2 = U256::from(2); + let validator3 = U256::from(3); + let coldkey1 = U256::from(11); + let coldkey2 = U256::from(12); + let coldkey3 = U256::from(13); + + // Register all validators + register_ok_neuron(netuid, validator1, coldkey1, 0); + register_ok_neuron(netuid, validator2, coldkey2, 0); + register_ok_neuron(netuid, validator3, coldkey3, 0); + + // Give them some stake so they can receive dividends + let stake_amount = AlphaCurrency::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &validator1, + &coldkey1, + netuid, + stake_amount, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &validator2, + &coldkey2, + netuid, + stake_amount, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &validator3, + &coldkey3, + netuid, + stake_amount, + ); + + // Mark them as validators so they can set weights + ValidatorPermit::::insert(netuid, vec![true, true, true]); + + // Set up weights so validators are voting for each other (simulating all validators, no miners) + let idx = SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(0)); + Weights::::insert( + idx, + 0, + vec![(0u16, 0xFFFF / 3), (1u16, 0xFFFF / 3), (2u16, 0xFFFF / 3)], + ); + Weights::::insert( + idx, + 1, + vec![(0u16, 0xFFFF / 3), (1u16, 0xFFFF / 3), (2u16, 0xFFFF / 3)], + ); + Weights::::insert( + idx, + 2, + vec![(0u16, 0xFFFF / 3), (1u16, 0xFFFF / 3), (2u16, 0xFFFF / 3)], + ); + + // Manually set all incentives to ZERO (this simulates no miners getting any incentive) + // This will cause incentive_sum to be zero in distribute_emission + Incentive::::insert(idx, vec![0u16, 0u16, 0u16]); + + // Record initial stakes + let initial_stake1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &validator1, + &coldkey1, + netuid, + ); + let initial_stake2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &validator2, + &coldkey2, + netuid, + ); + let initial_stake3 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &validator3, + &coldkey3, + netuid, + ); + + // Set up emission amounts + let pending_server_alpha = AlphaCurrency::from(500_000_000); // 500M for miners + let pending_validator_alpha = AlphaCurrency::from(500_000_000); // 500M for validators + let pending_root_alpha = AlphaCurrency::ZERO; + let pending_owner_cut = AlphaCurrency::ZERO; + + // Call distribute_emission + // When incentive_sum == 0, validators should get BOTH server and validator alpha + SubtensorModule::distribute_emission( + netuid, + pending_server_alpha, + pending_validator_alpha, + pending_root_alpha, + pending_owner_cut, + ); + + // Check final stakes + let final_stake1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &validator1, + &coldkey1, + netuid, + ); + let final_stake2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &validator2, + &coldkey2, + netuid, + ); + let final_stake3 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &validator3, + &coldkey3, + netuid, + ); + + // Calculate the increase in stake + let increase1 = final_stake1.saturating_sub(initial_stake1); + let increase2 = final_stake2.saturating_sub(initial_stake2); + let increase3 = final_stake3.saturating_sub(initial_stake3); + let total_increase = increase1 + .saturating_add(increase2) + .saturating_add(increase3); + + // The total increase should be close to server_alpha + validator_alpha = 1B + // (minus any rounding or take amounts) + let expected_total = pending_server_alpha.saturating_add(pending_validator_alpha); + + // Allow 10% margin for rounding, take, etc. + let tolerance = expected_total.to_u64() / 10; + + assert!( + (total_increase.to_u64() as i128 - expected_total.to_u64() as i128).abs() + < tolerance as i128, + "When incentive_sum == 0, validators should receive both server and validator alpha. \ + Expected: {}, Got: {}, Difference: {}", + expected_total.to_u64(), + total_increase.to_u64(), + (total_increase.to_u64() as i128 - expected_total.to_u64() as i128).abs() + ); + + // Verify that each validator got some emission (roughly equal since they have equal stake) + assert!( + increase1 > AlphaCurrency::ZERO, + "Validator 1 should receive emission" + ); + assert!( + increase2 > AlphaCurrency::ZERO, + "Validator 2 should receive emission" + ); + assert!( + increase3 > AlphaCurrency::ZERO, + "Validator 3 should receive emission" + ); + }); +} + +#[test] +fn test_get_subnets_to_emit_to_excludes_registration_disabled() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(100); + let owner_coldkey = U256::from(200); + + // Create 3 subnets with registration enabled by default + let subnet1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let subnet2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let subnet3 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Subnet 1: keep registration enabled (default from add_dynamic_network) + // Subnet 2: disable both NetworkRegistrationAllowed and NetworkPowRegistrationAllowed + NetworkRegistrationAllowed::::insert(subnet2, false); + NetworkPowRegistrationAllowed::::insert(subnet2, false); + // Subnet 3: keep registration enabled (default from add_dynamic_network) + + // Get all subnets + let all_subnets = vec![subnet1, subnet2, subnet3]; + + // Call get_subnets_to_emit_to + let subnets_to_emit = SubtensorModule::get_subnets_to_emit_to(&all_subnets); + + // Assert subnet2 is excluded (registration disabled) + assert!( + !subnets_to_emit.contains(&subnet2), + "Subnet with disabled registration should be excluded from emission" + ); + + // Assert subnet1 and subnet3 are included (registration enabled) + assert!( + subnets_to_emit.contains(&subnet1), + "Subnet 1 with enabled registration should be included in emission" + ); + assert!( + subnets_to_emit.contains(&subnet3), + "Subnet 3 with enabled registration should be included in emission" + ); + + // Verify exactly 2 subnets are in the result + assert_eq!( + subnets_to_emit.len(), + 2, + "Expected 2 subnets to be eligible for emission" + ); + }); +} diff --git a/pallets/subtensor/src/tests/wide_scope_dividend.rs b/pallets/subtensor/src/tests/wide_scope_dividend.rs new file mode 100644 index 0000000000..a8865a2c3c --- /dev/null +++ b/pallets/subtensor/src/tests/wide_scope_dividend.rs @@ -0,0 +1,1175 @@ +#![allow( + unused, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used, + clippy::arithmetic_side_effects +)] + +use super::mock::*; +use crate::*; +use frame_support::assert_ok; +use sp_core::U256; +use substrate_fixed::types::{I64F64, I96F32, U96F32}; +use subtensor_runtime_common::{AlphaCurrency, MechId, NetUid, TaoCurrency}; + +/// Asserts that `value` is within `eps` of `target` (absolute difference). +fn close(value: u64, target: u64, eps: u64) { + assert!( + (value as i128 - target as i128).unsigned_abs() < eps as u128, + "close assertion failed: value = {value}, target = {target}, eps = {eps}, diff = {}", + (value as i128 - target as i128).abs() + ); +} + +// =========================== +// Neuron identity constants +// =========================== + +// Subnet 1 owner +const OWNER1_HK: u64 = 10; +const OWNER1_CK: u64 = 110; + +// Subnet 2 owner +const OWNER2_HK: u64 = 20; +const OWNER2_CK: u64 = 120; + +// Root validators (registered in both subnets) +const MAJOR_ROOT_HK: u64 = 1; +const MAJOR_ROOT_CK: u64 = 101; +const MINOR_ROOT_HK: u64 = 2; +const MINOR_ROOT_CK: u64 = 102; + +// Subnet 1 validators and miner +const MAJOR_SN1_HK: u64 = 11; +const MAJOR_SN1_CK: u64 = 111; +const MINOR_SN1_HK: u64 = 12; +const MINOR_SN1_CK: u64 = 112; +const MINER1_HK: u64 = 13; +const MINER1_CK: u64 = 113; + +// Subnet 2 validators and miner +const MAJOR_SN2_HK: u64 = 21; +const MAJOR_SN2_CK: u64 = 121; +const MINOR_SN2_HK: u64 = 22; +const MINOR_SN2_CK: u64 = 122; +const MINER2_HK: u64 = 23; +const MINER2_CK: u64 = 123; + +// Stake amounts +const OWNER_ALPHA: u64 = 1_000; +const MAJOR_SUBNET_ALPHA: u64 = 999_000; +const MINOR_SUBNET_ALPHA: u64 = 1_000; +const MAJOR_ROOT_TAO: u64 = 5_550_000; +const MINOR_ROOT_TAO: u64 = 5_556; + +// Test setup result +struct TestSetup { + netuid1: NetUid, + netuid2: NetUid, +} + +/// Creates 2 subnets and registers all neurons with the specified stakes. +/// SN1 has tempo=1 (epochs at blocks 3, 5, 7...), SN2 has tempo=0 (never fires). +/// BlockAtRegistration is set to 0 for all SN1 neurons so weights set at block 1 +/// are not masked by the epoch's weight masking logic. +/// +/// Per SN1 UIDs: +/// 0 = subnet owner validator (1,000 alpha, does NOT set weights) +/// 1 = major root validator (0 alpha here, 5,550,000 TAO on root) +/// 2 = minor root validator (0 alpha here, 5,556 TAO on root) +/// 3 = major subnet validator (999,000 alpha) +/// 4 = minor subnet validator (1,000 alpha) +/// 5 = miner (0 stake) +fn setup_test() -> TestSetup { + // ----------- Create two subnets ----------- + let netuid1 = + add_dynamic_network_disable_commit_reveal(&U256::from(OWNER1_HK), &U256::from(OWNER1_CK)); + let netuid2 = + add_dynamic_network_disable_commit_reveal(&U256::from(OWNER2_HK), &U256::from(OWNER2_CK)); + log::info!( + "Created subnets: netuid1={:?}, netuid2={:?}", + netuid1, + netuid2 + ); + + // ----------- Subnet parameters ----------- + // SN1: tempo=1, epochs fire at blocks 3, 5, 7... for netuid=1 + SubtensorModule::set_tempo(netuid1, 1); + // SN2: tempo=0 means it never fires epochs; it only exists for flow share + SubtensorModule::set_tempo(netuid2, 0); + + for &netuid in &[netuid1, netuid2] { + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_min_allowed_weights(netuid, 1); + SubtensorModule::set_max_allowed_validators(netuid, 5); + SubtensorModule::set_activity_cutoff(netuid, 5000); + SubtensorModule::set_max_registrations_per_block(netuid, 100); + SubtensorModule::set_target_registrations_per_interval(netuid, 100); + } + + // ----------- Subnet reserves for price 0.5 (default, tests can override) ----------- + let tao_reserve = TaoCurrency::from(500_000u64); + let alpha_reserve = AlphaCurrency::from(1_000_000u64); + setup_reserves(netuid1, tao_reserve, alpha_reserve); + setup_reserves(netuid2, tao_reserve, alpha_reserve); + + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.5)); + SubnetMovingPrice::::insert(netuid2, I96F32::from_num(0.5)); + + // ----------- Subnet flow EMA = 0.001 ----------- + let now = SubtensorModule::get_current_block_as_u64(); + SubnetEmaTaoFlow::::insert(netuid1, (now, I64F64::from_num(0.001))); + SubnetEmaTaoFlow::::insert(netuid2, (now, I64F64::from_num(0.001))); + + // ----------- TaoWeight ≈ 0.18 ----------- + TaoWeight::::set(u64::MAX / 100 * 18); + + // ----------- Subnet owner cut = 18% ----------- + SubtensorModule::set_subnet_owner_cut(u16::MAX / 100 * 18); + + // ----------- Enable EffectiveRootPropEmissionScaling ----------- + EffectiveRootPropEmissionScaling::::set(true); + + // ----------- Register neurons ----------- + // SN1 + register_ok_neuron( + netuid1, + U256::from(MAJOR_ROOT_HK), + U256::from(MAJOR_ROOT_CK), + 0, + ); + register_ok_neuron( + netuid1, + U256::from(MINOR_ROOT_HK), + U256::from(MINOR_ROOT_CK), + 10, + ); + register_ok_neuron( + netuid1, + U256::from(MAJOR_SN1_HK), + U256::from(MAJOR_SN1_CK), + 20, + ); + register_ok_neuron( + netuid1, + U256::from(MINOR_SN1_HK), + U256::from(MINOR_SN1_CK), + 30, + ); + register_ok_neuron(netuid1, U256::from(MINER1_HK), U256::from(MINER1_CK), 40); + + // SN2 + register_ok_neuron( + netuid2, + U256::from(MAJOR_ROOT_HK), + U256::from(MAJOR_ROOT_CK), + 50, + ); + register_ok_neuron( + netuid2, + U256::from(MINOR_ROOT_HK), + U256::from(MINOR_ROOT_CK), + 60, + ); + register_ok_neuron( + netuid2, + U256::from(MAJOR_SN2_HK), + U256::from(MAJOR_SN2_CK), + 70, + ); + register_ok_neuron( + netuid2, + U256::from(MINOR_SN2_HK), + U256::from(MINOR_SN2_CK), + 80, + ); + register_ok_neuron(netuid2, U256::from(MINER2_HK), U256::from(MINER2_CK), 90); + + // ----------- Fix BlockAtRegistration for SN1 ----------- + // Set to 0 so weights at block 1 have last_update=1 > 0=block_at_registration + // and are NOT masked by epoch's vec_mask_sparse_matrix. + for uid in 0..6u16 { + BlockAtRegistration::::insert(netuid1, uid, 0u64); + } + + // ----------- Add alpha stakes ----------- + // SN1 + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(OWNER1_HK), + &U256::from(OWNER1_CK), + netuid1, + AlphaCurrency::from(OWNER_ALPHA), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(MAJOR_SN1_HK), + &U256::from(MAJOR_SN1_CK), + netuid1, + AlphaCurrency::from(MAJOR_SUBNET_ALPHA), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(MINOR_SN1_HK), + &U256::from(MINOR_SN1_CK), + netuid1, + AlphaCurrency::from(MINOR_SUBNET_ALPHA), + ); + + // SN2 + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(OWNER2_HK), + &U256::from(OWNER2_CK), + netuid2, + AlphaCurrency::from(OWNER_ALPHA), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(MAJOR_SN2_HK), + &U256::from(MAJOR_SN2_CK), + netuid2, + AlphaCurrency::from(MAJOR_SUBNET_ALPHA), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(MINOR_SN2_HK), + &U256::from(MINOR_SN2_CK), + netuid2, + AlphaCurrency::from(MINOR_SUBNET_ALPHA), + ); + + // Track SubnetAlphaOut + let total_subnet_alpha = + AlphaCurrency::from(OWNER_ALPHA + MAJOR_SUBNET_ALPHA + MINOR_SUBNET_ALPHA); + SubnetAlphaOut::::mutate(netuid1, |total| { + *total = total.saturating_add(total_subnet_alpha); + }); + SubnetAlphaOut::::mutate(netuid2, |total| { + *total = total.saturating_add(total_subnet_alpha); + }); + + // ----------- Root stakes ----------- + SubtensorModule::add_balance_to_coldkey_account(&U256::from(MAJOR_ROOT_CK), MAJOR_ROOT_TAO); + SubtensorModule::add_balance_to_coldkey_account(&U256::from(MINOR_ROOT_CK), MINOR_ROOT_TAO); + TotalIssuance::::mutate(|total| { + *total = total.saturating_add(TaoCurrency::from(MAJOR_ROOT_TAO + MINOR_ROOT_TAO)); + }); + increase_stake_on_coldkey_hotkey_account( + &U256::from(MAJOR_ROOT_CK), + &U256::from(MAJOR_ROOT_HK), + TaoCurrency::from(MAJOR_ROOT_TAO), + NetUid::ROOT, + ); + increase_stake_on_coldkey_hotkey_account( + &U256::from(MINOR_ROOT_CK), + &U256::from(MINOR_ROOT_HK), + TaoCurrency::from(MINOR_ROOT_TAO), + NetUid::ROOT, + ); + + // ----------- Unstaked TAO (10% of MAJOR_ROOT_TAO) ----------- + // This TAO exists in TotalIssuance but is not staked anywhere. + // It should have zero effect on utilization. + TotalIssuance::::mutate(|total| { + *total = total.saturating_add(TaoCurrency::from(MAJOR_ROOT_TAO / 10)); + }); + + // ----------- Validator permits (manual) ----------- + ValidatorPermit::::insert(netuid1, vec![true, true, true, true, true, false]); + ValidatorPermit::::insert(netuid2, vec![true, true, true, true, true, false]); + + // ----------- Log initial state ----------- + log::info!("=== Initial State ==="); + log::info!(" SN1 SubnetTAO: {:?}", SubnetTAO::::get(netuid1)); + log::info!( + " SN1 SubnetAlphaIn: {:?}", + SubnetAlphaIn::::get(netuid1) + ); + log::info!( + " SN1 SubnetAlphaOut: {:?}", + SubnetAlphaOut::::get(netuid1) + ); + log::info!( + " SN1 Moving price: {:?}", + SubnetMovingPrice::::get(netuid1) + ); + log::info!( + " SN1 EMA flow: {:?}", + SubnetEmaTaoFlow::::get(netuid1) + ); + log::info!( + " BlockEmission: {:?}", + SubtensorModule::get_block_emission() + ); + log::info!(" TaoWeight: {:?}", TaoWeight::::get()); + log::info!( + " SubnetOwnerCut: {:?}", + SubtensorModule::get_subnet_owner_cut() + ); + + TestSetup { netuid1, netuid2 } +} + +/// Logs detailed per-neuron state for a subnet +fn log_neuron_state(label: &str, netuid: NetUid, neurons: &[(&str, u64, u64)]) { + log::info!("=== {} (subnet {:?}) ===", label, netuid); + for &(name, hk_id, _ck_id) in neurons { + let hotkey = U256::from(hk_id); + let stake = SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey, netuid); + let alpha_divs = AlphaDividendsPerSubnet::::get(netuid, hotkey); + let root_divs = RootAlphaDividendsPerSubnet::::get(netuid, hotkey); + let root_stake = SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey, NetUid::ROOT); + log::info!( + " {} (hk={}): stake={:?}, alpha_divs={:?}, root_divs={:?}, root_stake={:?}", + name, + hk_id, + stake, + alpha_divs, + root_divs, + root_stake + ); + } +} + +/// Logs subnet-level state including per-UID epoch vectors +fn log_subnet_state(label: &str, netuid: NetUid) { + log::info!("=== {} (subnet {:?}) ===", label, netuid); + log::info!(" SubnetTAO: {:?}", SubnetTAO::::get(netuid)); + log::info!(" SubnetAlphaIn: {:?}", SubnetAlphaIn::::get(netuid)); + log::info!( + " SubnetAlphaOut: {:?}", + SubnetAlphaOut::::get(netuid) + ); + log::info!( + " PendingServerEmission: {:?}", + PendingServerEmission::::get(netuid) + ); + log::info!( + " PendingValidatorEmission: {:?}", + PendingValidatorEmission::::get(netuid) + ); + log::info!( + " PendingRootAlphaDivs: {:?}", + PendingRootAlphaDivs::::get(netuid) + ); + log::info!( + " EffectiveRootProp: {:?}", + EffectiveRootProp::::get(netuid) + ); + log::info!(" RootProp: {:?}", RootProp::::get(netuid)); + let mech_idx = SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(0u8)); + let incentive_vec = Incentive::::get(mech_idx); + let dividends_vec = Dividends::::get(netuid); + let emission_vec = Emission::::get(netuid); + log::info!(" Incentive (per UID): {:?}", incentive_vec); + log::info!(" Dividends (per UID): {:?}", dividends_vec); + log::info!(" Emission (per UID): {:?}", emission_vec); +} + +/// Standard neuron list for SN1 +fn sn1_neurons() -> Vec<(&'static str, u64, u64)> { + vec![ + ("owner1", OWNER1_HK, OWNER1_CK), + ("major_root", MAJOR_ROOT_HK, MAJOR_ROOT_CK), + ("minor_root", MINOR_ROOT_HK, MINOR_ROOT_CK), + ("major_sn1", MAJOR_SN1_HK, MAJOR_SN1_CK), + ("minor_sn1", MINOR_SN1_HK, MINOR_SN1_CK), + ("miner1", MINER1_HK, MINER1_CK), + ] +} + +/// Helper closures for reading stake/dividends +fn stake_of(hk: u64, netuid: NetUid) -> u64 { + u64::from(SubtensorModule::get_stake_for_hotkey_on_subnet( + &U256::from(hk), + netuid, + )) +} + +fn alpha_divs_of(hk: u64, netuid: NetUid) -> u64 { + u64::from(AlphaDividendsPerSubnet::::get(netuid, U256::from(hk))) +} + +fn root_divs_of(hk: u64, netuid: NetUid) -> u64 { + u64::from(RootAlphaDividendsPerSubnet::::get( + netuid, + U256::from(hk), + )) +} + +/// 1% tolerance +fn eps(val: u64) -> u64 { + val / 100 + 1 +} + +// =========================================================================== +// Test 1: Basic case - all validators set weights to miner (price=0.6) +// +// With price=0.6, total_ema_price = 1.2 > 1.0, so root_sell_flag = true +// and root validators earn dividends. +// +// Block structure (5 total): +// Block 1: setup + set weights +// Blocks 2-4: coinbase accumulates pending +// Block 3: 1st epoch + drain (bonds form, dividends still 0 for miners) +// Block 5: 2nd epoch + drain (bonds active, miners earn incentive) +// +// Run: +// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::wide_scope_dividend::test_basic_all_validators_set_weights_to_miners --exact --show-output --nocapture +// =========================================================================== +#[test] +fn test_basic_all_validators_set_weights_to_miners() { + new_test_ext(1).execute_with(|| { + let setup = setup_test(); + let netuid1 = setup.netuid1; + + // Override prices to 0.6 (root_sell_flag = true: 2*0.6=1.2 > 1.0) + let tao_reserve = TaoCurrency::from(600_000u64); + let alpha_reserve = AlphaCurrency::from(1_000_000u64); + setup_reserves(netuid1, tao_reserve, alpha_reserve); + setup_reserves(setup.netuid2, tao_reserve, alpha_reserve); + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.6)); + SubnetMovingPrice::::insert(setup.netuid2, I96F32::from_num(0.6)); + + // Get miner UID + let miner1_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid1, &U256::from(MINER1_HK)).unwrap(); + + // Set weights: all validators (except owner) -> miner (block 1) + for hk_id in [MAJOR_ROOT_HK, MINOR_ROOT_HK, MAJOR_SN1_HK, MINOR_SN1_HK] { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(hk_id)), + netuid1, + vec![miner1_uid], + vec![u16::MAX], + 0 + )); + } + log::info!( + "Weights set at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + // Step 4 blocks: block 1→5. Epochs fire at blocks 3 and 5 for netuid=1, tempo=1. + let neurons = sn1_neurons(); + for block in 2..=5 { + step_block(1); + log::info!( + "--- Block {} ---", + SubtensorModule::get_current_block_as_u64() + ); + log_subnet_state("SN1", netuid1); + log_neuron_state("SN1 neurons", netuid1, &neurons); + } + + // ======================================================================== + // SUBNET 1 assertions + // ======================================================================== + + // 1. Miner earned incentive from server emission + let miner1_stake = stake_of(MINER1_HK, netuid1); + log::info!("miner1_stake = {}", miner1_stake); + close(miner1_stake, 1_640_192_260, eps(1_640_192_260)); + + // 2. Major subnet validator earned more dividends than minor + let major_sn1_divs = alpha_divs_of(MAJOR_SN1_HK, netuid1); + let minor_sn1_divs = alpha_divs_of(MINOR_SN1_HK, netuid1); + log::info!( + "major_sn1_divs = {}, minor_sn1_divs = {}", + major_sn1_divs, + minor_sn1_divs + ); + close(major_sn1_divs, 622_577_642, eps(622_577_642)); + close(minor_sn1_divs, 618_529, eps(618_529)); + assert!(major_sn1_divs > minor_sn1_divs); + + // 3. Major subnet validator stake + close( + stake_of(MAJOR_SN1_HK, netuid1), + 1_578_898_899, + eps(1_578_898_899), + ); + + // 4. Root validators earn nonzero (root_sell_flag=true, price=0.6*2=1.2>1.0) + close( + stake_of(MAJOR_ROOT_HK, netuid1), + 60_006_436, + eps(60_006_436), + ); + close( + alpha_divs_of(MAJOR_ROOT_HK, netuid1), + 49_088_509, + eps(49_088_509), + ); + close(root_divs_of(MAJOR_ROOT_HK, netuid1), 147_661, eps(147_661)); + close(stake_of(MINOR_ROOT_HK, netuid1), 61_228, eps(61_228)); + close(alpha_divs_of(MINOR_ROOT_HK, netuid1), 50_091, eps(50_091)); + close(root_divs_of(MINOR_ROOT_HK, netuid1), 146, eps(146) + 2); + assert!(stake_of(MAJOR_ROOT_HK, netuid1) > stake_of(MINOR_ROOT_HK, netuid1)); + + // 5. Owner earned owner cut (18% of emissions), no dividends + close(stake_of(OWNER1_HK, netuid1), 719_616_472, eps(719_616_472)); + assert_eq!(alpha_divs_of(OWNER1_HK, netuid1), 0); + + // 6. Miner has 0 dividends + assert_eq!(alpha_divs_of(MINER1_HK, netuid1), 0); + assert_eq!(root_divs_of(MINER1_HK, netuid1), 0); + + // 7. Incentive vector: miner (UID 5) has 100% of incentive + let mech_idx = SubtensorModule::get_mechanism_storage_index(netuid1, MechId::from(0u8)); + let incentive_vec = Incentive::::get(mech_idx); + assert_eq!(incentive_vec.get(5).copied().unwrap_or(0), u16::MAX); + for uid in 0..5 { + assert_eq!(incentive_vec.get(uid).copied().unwrap_or(0), 0); + } + + // 8. Root stakes increase due to root dividends being converted to root claimable + close( + stake_of(MAJOR_ROOT_HK, NetUid::ROOT), + 5_750_691, + eps(5_750_691), + ); + close( + stake_of(MINOR_ROOT_HK, NetUid::ROOT), + MINOR_ROOT_TAO, + eps(MINOR_ROOT_TAO) + 200, + ); + + // 9. EffectiveRootProp is close to RootProp (all root stake is active, utilization ≈ 1.0) + let erp = EffectiveRootProp::::get(netuid1); + let rp = RootProp::::get(netuid1); + log::info!( + "EffectiveRootProp = {:?}, RootProp = {:?}", + erp, + rp + ); + // EffectiveRootProp should be within 2x of RootProp + assert!( + erp >= rp, + "EffectiveRootProp ({erp:?}) should be >= RootProp ({rp:?}) when all root validators set weights" + ); + }); +} + +// =========================================================================== +// Test 2: No root sell - all validators set weights (price=0.5) +// +// With price=0.5, total_ema_price = 1.0, root_sell_flag = false. +// Root validators earn 0 dividends. +// =========================================================================== +#[test] +fn test_no_root_sell_all_validators_set_weights_to_miners() { + new_test_ext(1).execute_with(|| { + let setup = setup_test(); + let netuid1 = setup.netuid1; + + // Prices stay at 0.5 from setup (root_sell_flag = false: 2*0.5=1.0) + + let miner1_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid1, &U256::from(MINER1_HK)).unwrap(); + + // Set weights: all validators (except owner) -> miner (block 1) + for hk_id in [MAJOR_ROOT_HK, MINOR_ROOT_HK, MAJOR_SN1_HK, MINOR_SN1_HK] { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(hk_id)), + netuid1, + vec![miner1_uid], + vec![u16::MAX], + 0 + )); + } + + let neurons = sn1_neurons(); + for _ in 2..=5 { + step_block(1); + } + log::info!( + "--- Final state (block {}) ---", + SubtensorModule::get_current_block_as_u64() + ); + log_subnet_state("SN1", netuid1); + log_neuron_state("SN1 neurons", netuid1, &neurons); + + // 1. Miner earned incentive + close( + stake_of(MINER1_HK, netuid1), + 1_639_765_956, + eps(1_639_765_956), + ); + + // 2. Major SN1 validator earned more than minor + let major_sn1_divs = alpha_divs_of(MAJOR_SN1_HK, netuid1); + let minor_sn1_divs = alpha_divs_of(MINOR_SN1_HK, netuid1); + close(major_sn1_divs, 671_619_324, eps(671_619_324)); + close(minor_sn1_divs, 667_252, eps(667_252)); + assert!(major_sn1_divs > minor_sn1_divs); + + // 3. Root validators earn 0 (root_sell_flag=false, total_ema_price=1.0) + assert_eq!(alpha_divs_of(MAJOR_ROOT_HK, netuid1), 0); + assert_eq!(root_divs_of(MAJOR_ROOT_HK, netuid1), 0); + assert_eq!(stake_of(MAJOR_ROOT_HK, netuid1), 0); + assert_eq!(alpha_divs_of(MINOR_ROOT_HK, netuid1), 0); + assert_eq!(root_divs_of(MINOR_ROOT_HK, netuid1), 0); + assert_eq!(stake_of(MINOR_ROOT_HK, netuid1), 0); + + // 4. Owner earned owner cut + close(stake_of(OWNER1_HK, netuid1), 719_616_472, eps(719_616_472)); + assert_eq!(alpha_divs_of(OWNER1_HK, netuid1), 0); + + // 5. Root stakes unchanged + assert_eq!(stake_of(MAJOR_ROOT_HK, NetUid::ROOT), MAJOR_ROOT_TAO); + assert_eq!(stake_of(MINOR_ROOT_HK, NetUid::ROOT), MINOR_ROOT_TAO); + }); +} + +// =========================================================================== +// Test 3: Major root validator does NOT set weights on SN1 +// +// Price=0.6 (root_sell_flag=true), but major root (5.55M TAO) doesn't +// set weights on SN1. Only minor root, major_sn1, minor_sn1 set weights. +// Expected: major root earns 0 dividends (no bonds), minor root still earns. +// =========================================================================== +#[test] +fn test_basic_major_root_no_weights() { + new_test_ext(1).execute_with(|| { + let setup = setup_test(); + let netuid1 = setup.netuid1; + + // Override prices to 0.6 + let tao_reserve = TaoCurrency::from(600_000u64); + let alpha_reserve = AlphaCurrency::from(1_000_000u64); + setup_reserves(netuid1, tao_reserve, alpha_reserve); + setup_reserves(setup.netuid2, tao_reserve, alpha_reserve); + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.6)); + SubnetMovingPrice::::insert(setup.netuid2, I96F32::from_num(0.6)); + + let miner1_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid1, &U256::from(MINER1_HK)).unwrap(); + + // Set weights: only minor_root, major_sn1, minor_sn1 -> miner + // MAJOR_ROOT does NOT set weights + for hk_id in [MINOR_ROOT_HK, MAJOR_SN1_HK, MINOR_SN1_HK] { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(hk_id)), + netuid1, + vec![miner1_uid], + vec![u16::MAX], + 0 + )); + } + + let neurons = sn1_neurons(); + for _ in 2..=5 { + step_block(1); + } + log::info!( + "--- Final state (block {}) ---", + SubtensorModule::get_current_block_as_u64() + ); + log_subnet_state("SN1", netuid1); + log_neuron_state("SN1 neurons", netuid1, &neurons); + + // 1. Miner earned incentive + close( + stake_of(MINER1_HK, netuid1), + 1_640_192_260, + eps(1_640_192_260), + ); + + // 2. Major root earns 0 (didn't set weights, no bonds develop) + assert_eq!(stake_of(MAJOR_ROOT_HK, netuid1), 0); + assert_eq!(alpha_divs_of(MAJOR_ROOT_HK, netuid1), 0); + assert_eq!(root_divs_of(MAJOR_ROOT_HK, netuid1), 0); + + // 3. Minor root: hard cap triggered (utilization ≈ 0.001 < 0.5), all root dividends recycled. + // Minor root loses its root_alpha_dividends and root-staked portion of alpha_dividends. + assert_eq!(root_divs_of(MINOR_ROOT_HK, netuid1), 0); + // Minor root may still have some alpha dividends from its alpha-stake portion + // (since hard cap only zeroes the root-staked fraction) + + // 4. Subnet validators (alpha-only validators unaffected by hard cap) + assert!(alpha_divs_of(MAJOR_SN1_HK, netuid1) > 0); + assert!(alpha_divs_of(MINOR_SN1_HK, netuid1) > 0); + + // 5. Root stakes unchanged (no root dividends converted) + assert_eq!(stake_of(MAJOR_ROOT_HK, NetUid::ROOT), MAJOR_ROOT_TAO); + assert_eq!(stake_of(MINOR_ROOT_HK, NetUid::ROOT), MINOR_ROOT_TAO); + + // 6. EffectiveRootProp = 0 (hard cap triggered, utilization < 0.5) + let erp = EffectiveRootProp::::get(netuid1); + log::info!("EffectiveRootProp = {:?}", erp); + assert_eq!( + erp, + U96F32::from_num(0), + "EffectiveRootProp should be 0 when hard cap triggers (utilization < 0.5)" + ); + }); +} + +// =========================================================================== +// Test 4: Unstaked TAO doesn't affect utilization +// +// Same setup as basic test (price=0.6, all validators set weights to miner), +// but with a massive amount of extra unstaked TAO added to TotalIssuance. +// Proves that utilization denominator = root stake on subnet, not TotalIssuance. +// +// Run: +// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::wide_scope_dividend::test_unstaked_tao_does_not_affect_utilization --exact --show-output --nocapture +// =========================================================================== +#[test] +fn test_unstaked_tao_does_not_affect_utilization() { + new_test_ext(1).execute_with(|| { + let setup = setup_test(); + let netuid1 = setup.netuid1; + + // Override prices to 0.6 (root_sell_flag = true) + let tao_reserve = TaoCurrency::from(600_000u64); + let alpha_reserve = AlphaCurrency::from(1_000_000u64); + setup_reserves(netuid1, tao_reserve, alpha_reserve); + setup_reserves(setup.netuid2, tao_reserve, alpha_reserve); + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.6)); + SubnetMovingPrice::::insert(setup.netuid2, I96F32::from_num(0.6)); + + // Add a MASSIVE amount of unstaked TAO (100x MAJOR_ROOT_TAO) + TotalIssuance::::mutate(|total| { + *total = total.saturating_add(TaoCurrency::from(MAJOR_ROOT_TAO * 100)); + }); + + let miner1_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid1, &U256::from(MINER1_HK)).unwrap(); + + // Set weights: all validators -> miner (same as basic test) + for hk_id in [MAJOR_ROOT_HK, MINOR_ROOT_HK, MAJOR_SN1_HK, MINOR_SN1_HK] { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(hk_id)), + netuid1, + vec![miner1_uid], + vec![u16::MAX], + 0 + )); + } + + let neurons = sn1_neurons(); + for _ in 2..=5 { + step_block(1); + } + log::info!( + "--- Final state (block {}) ---", + SubtensorModule::get_current_block_as_u64() + ); + log_subnet_state("SN1", netuid1); + log_neuron_state("SN1 neurons", netuid1, &neurons); + + // 1. Root validators earn nonzero dividends (utilization = 1.0, no scaling) + assert!( + root_divs_of(MAJOR_ROOT_HK, netuid1) > 0, + "Major root should earn root dividends" + ); + assert!( + alpha_divs_of(MAJOR_ROOT_HK, netuid1) > 0, + "Major root should earn alpha dividends" + ); + + // 2. EffectiveRootProp should be >= RootProp (utilization = 1.0, no scaling) + let erp = EffectiveRootProp::::get(netuid1); + let rp = RootProp::::get(netuid1); + log::info!("EffectiveRootProp = {:?}, RootProp = {:?}", erp, rp); + assert!( + erp >= rp, + "EffectiveRootProp ({erp:?}) should be >= RootProp ({rp:?}) with full utilization" + ); + + // 3. Root stakes increase (root dividends converted to root claimable) + assert!( + stake_of(MAJOR_ROOT_HK, NetUid::ROOT) > MAJOR_ROOT_TAO, + "Major root stake should increase from root dividends" + ); + + // 4. Unstaked TAO only affects block emission rate, not utilization + // The key invariant: utilization denominator = root stake on subnet, not TotalIssuance + log::info!("TotalIssuance = {:?}", TotalIssuance::::get()); + }); +} + +// =========================================================================== +// Test 5: Half-weights test - major root sets half weights to validator +// +// Big root sets half weights to miner, half to minor_root_validator. +// Small root (minor_root) DOES set full weights to miner. +// Utilization stays above 50% so dividends are scaled by utilization, not hard-capped. +// +// Run: +// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::wide_scope_dividend::test_basic_major_root_half_weights_to_validator --exact --show-output --nocapture +// =========================================================================== +#[test] +fn test_basic_major_root_half_weights_to_validator() { + new_test_ext(1).execute_with(|| { + let setup = setup_test(); + let netuid1 = setup.netuid1; + + // Override prices to 0.6 + let tao_reserve = TaoCurrency::from(600_000u64); + let alpha_reserve = AlphaCurrency::from(1_000_000u64); + setup_reserves(netuid1, tao_reserve, alpha_reserve); + setup_reserves(setup.netuid2, tao_reserve, alpha_reserve); + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.6)); + SubnetMovingPrice::::insert(setup.netuid2, I96F32::from_num(0.6)); + + let miner1_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid1, &U256::from(MINER1_HK)).unwrap(); + let minor_root_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid1, &U256::from(MINOR_ROOT_HK)) + .unwrap(); + + // Major root sets HALF weights to miner, HALF to minor_root (validator) + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(MAJOR_ROOT_HK)), + netuid1, + vec![miner1_uid, minor_root_uid], + vec![u16::MAX / 2, u16::MAX / 2], + 0 + )); + + // Minor root sets FULL weights to miner + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(MINOR_ROOT_HK)), + netuid1, + vec![miner1_uid], + vec![u16::MAX], + 0 + )); + + // Subnet validators set weights to miner + for hk_id in [MAJOR_SN1_HK, MINOR_SN1_HK] { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(hk_id)), + netuid1, + vec![miner1_uid], + vec![u16::MAX], + 0 + )); + } + + let neurons = sn1_neurons(); + for _ in 2..=5 { + step_block(1); + } + log::info!( + "--- Final state (block {}) ---", + SubtensorModule::get_current_block_as_u64() + ); + log_subnet_state("SN1", netuid1); + log_neuron_state("SN1 neurons", netuid1, &neurons); + + // 1. EffectiveRootProp should be > 0 (utilization is high, not hard-capped) + // Even with half weights to a validator, the major root still earns its expected + // share of root dividends because consensus clips the wasted weight and dividends + // flow through bond formation with miners. Minor root also earns, so utilization ≈ 1.0. + let erp = EffectiveRootProp::::get(netuid1); + let rp = RootProp::::get(netuid1); + log::info!("EffectiveRootProp = {:?}, RootProp = {:?}", erp, rp); + assert!( + erp > U96F32::from_num(0), + "EffectiveRootProp should be > 0 (utilization > 0.5)" + ); + + // 2. Both root validators earn dividends (both set weights, both have bonds) + let major_root_divs = root_divs_of(MAJOR_ROOT_HK, netuid1); + let minor_root_divs = root_divs_of(MINOR_ROOT_HK, netuid1); + log::info!( + "major_root_divs = {}, minor_root_divs = {}", + major_root_divs, + minor_root_divs + ); + assert!( + major_root_divs > 0, + "Major root should earn root dividends" + ); + assert!( + minor_root_divs > 0, + "Minor root should earn root dividends" + ); + + // 3. Utilization is high enough that EffectiveRootProp >= RootProp + assert!( + erp >= rp, + "EffectiveRootProp ({erp:?}) should be >= RootProp ({rp:?}) when all root validators set weights" + ); + }); +} + +// =========================================================================== +// Test 6: Half-weights, minor root doesn't set weights +// +// Big root sets half weights to miner, half to minor_root_validator. +// Small root does NOT set weights at all. +// Since major root (99.9% of root stake) still earns its expected share of +// root dividends, utilization remains high (~0.999). Only minor root (0.1%) +// is inactive. Hard cap does NOT trigger. +// +// Run: +// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::wide_scope_dividend::test_basic_major_root_half_weights_no_minor_root --exact --show-output --nocapture +// =========================================================================== +#[test] +fn test_basic_major_root_half_weights_no_minor_root() { + new_test_ext(1).execute_with(|| { + let setup = setup_test(); + let netuid1 = setup.netuid1; + + // Override prices to 0.6 + let tao_reserve = TaoCurrency::from(600_000u64); + let alpha_reserve = AlphaCurrency::from(1_000_000u64); + setup_reserves(netuid1, tao_reserve, alpha_reserve); + setup_reserves(setup.netuid2, tao_reserve, alpha_reserve); + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.6)); + SubnetMovingPrice::::insert(setup.netuid2, I96F32::from_num(0.6)); + + let miner1_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid1, &U256::from(MINER1_HK)).unwrap(); + let minor_root_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid1, &U256::from(MINOR_ROOT_HK)) + .unwrap(); + + // Major root sets HALF weights to miner, HALF to minor_root (validator) + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(MAJOR_ROOT_HK)), + netuid1, + vec![miner1_uid, minor_root_uid], + vec![u16::MAX / 2, u16::MAX / 2], + 0 + )); + + // Minor root does NOT set weights (this is the key difference from test 5) + + // Subnet validators set weights to miner + for hk_id in [MAJOR_SN1_HK, MINOR_SN1_HK] { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(hk_id)), + netuid1, + vec![miner1_uid], + vec![u16::MAX], + 0 + )); + } + + let neurons = sn1_neurons(); + for _ in 2..=5 { + step_block(1); + } + log::info!( + "--- Final state (block {}) ---", + SubtensorModule::get_current_block_as_u64() + ); + log_subnet_state("SN1", netuid1); + log_neuron_state("SN1 neurons", netuid1, &neurons); + + // 1. EffectiveRootProp > 0: utilization is high (~0.999) because major root + // (99.9% of root stake) earns its expected share. Only minor root (0.1%) is idle. + let erp = EffectiveRootProp::::get(netuid1); + let rp = RootProp::::get(netuid1); + log::info!("EffectiveRootProp = {:?}, RootProp = {:?}", erp, rp); + assert!( + erp > U96F32::from_num(0), + "EffectiveRootProp should be > 0 (major root active, utilization > 0.5)" + ); + + // 2. Major root earns root dividends (set weights, has bonds) + assert!( + root_divs_of(MAJOR_ROOT_HK, netuid1) > 0, + "Major root should earn root dividends" + ); + + // 3. Minor root earns 0 (didn't set weights, no bonds) + assert_eq!( + root_divs_of(MINOR_ROOT_HK, netuid1), + 0, + "Minor root should earn 0 root dividends (no weights set)" + ); + + // 4. Miner earns incentive + assert!( + stake_of(MINER1_HK, netuid1) > 0, + "Miner should earn incentive" + ); + + // 5. Utilization is slightly below 1.0 due to minor root being inactive, + // so ERP should be very close to RootProp but may be slightly scaled + assert!( + erp >= rp, + "EffectiveRootProp should be close to RootProp with near-full utilization" + ); + }); +} + +// =========================================================================== +// Test 7: Root validators abandon, then return +// +// Phase 1: Root validators don't set weights on SN1. Only subnet validators +// set weights. Hard cap triggers (utilization < 0.5), ERP = 0, +// root dividends recycled. +// Phase 2: Root validators set weights to miner and we advance epochs. +// Subnet recovers — ERP > 0, root dividends flow again. +// +// This proves the hard cap is not permanent: subnets can recover once root +// validators resume validating. +// +// Run: +// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::wide_scope_dividend::test_root_validators_abandon_then_return --exact --show-output --nocapture +// =========================================================================== +#[test] +fn test_root_validators_abandon_then_return() { + new_test_ext(1).execute_with(|| { + let setup = setup_test(); + let netuid1 = setup.netuid1; + + // Override prices to 0.6 (root_sell_flag = true: 2*0.6=1.2 > 1.0) + let tao_reserve = TaoCurrency::from(600_000u64); + let alpha_reserve = AlphaCurrency::from(1_000_000u64); + setup_reserves(netuid1, tao_reserve, alpha_reserve); + setup_reserves(setup.netuid2, tao_reserve, alpha_reserve); + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.6)); + SubnetMovingPrice::::insert(setup.netuid2, I96F32::from_num(0.6)); + + let miner1_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid1, &U256::from(MINER1_HK)).unwrap(); + + // ==================================================================== + // PHASE 1: Root validators ABANDON the subnet (don't set weights) + // Only subnet validators set weights to miner. + // ==================================================================== + for hk_id in [MAJOR_SN1_HK, MINOR_SN1_HK] { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(hk_id)), + netuid1, + vec![miner1_uid], + vec![u16::MAX], + 0 + )); + } + log::info!( + "Phase 1: Only subnet validators set weights at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + // Step 4 blocks: block 1→5. Epochs fire at blocks 3 and 5 for netuid=1, tempo=1. + let neurons = sn1_neurons(); + for _ in 2..=5 { + step_block(1); + } + log::info!( + "--- Phase 1 final state (block {}) ---", + SubtensorModule::get_current_block_as_u64() + ); + log_subnet_state("SN1 Phase1", netuid1); + log_neuron_state("SN1 neurons Phase1", netuid1, &neurons); + + // Phase 1 assertions: hard cap triggered + let erp_phase1 = EffectiveRootProp::::get(netuid1); + log::info!("Phase 1 EffectiveRootProp = {:?}", erp_phase1); + assert_eq!( + erp_phase1, + U96F32::from_num(0), + "Phase 1: EffectiveRootProp should be 0 (hard cap triggered, root validators abandoned)" + ); + + // Root validators earned 0 dividends + assert_eq!( + root_divs_of(MAJOR_ROOT_HK, netuid1), + 0, + "Phase 1: Major root should earn 0 root dividends" + ); + assert_eq!( + root_divs_of(MINOR_ROOT_HK, netuid1), + 0, + "Phase 1: Minor root should earn 0 root dividends" + ); + + // Record root stakes at end of phase 1 as baseline. + // Root stakes may have increased from block emission distribution (not from SN1 root dividends, + // which were recycled), so we use these as the baseline for phase 2 comparison. + let major_root_stake_phase1 = stake_of(MAJOR_ROOT_HK, NetUid::ROOT); + let minor_root_stake_phase1 = stake_of(MINOR_ROOT_HK, NetUid::ROOT); + log::info!( + "Phase 1 root stakes: major={}, minor={}", + major_root_stake_phase1, + minor_root_stake_phase1 + ); + + // Miner earned incentive (subnet still functions, just no root dividends) + assert!( + stake_of(MINER1_HK, netuid1) > 0, + "Phase 1: Miner should still earn incentive" + ); + + // Subnet validators earned dividends (alpha-only, unaffected by root hard cap) + assert!( + alpha_divs_of(MAJOR_SN1_HK, netuid1) > 0, + "Phase 1: Major SN1 should still earn alpha dividends" + ); + + // ==================================================================== + // PHASE 2: Root validators RETURN — set weights to miner + // ==================================================================== + for hk_id in [MAJOR_ROOT_HK, MINOR_ROOT_HK] { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(hk_id)), + netuid1, + vec![miner1_uid], + vec![u16::MAX], + 0 + )); + } + log::info!( + "Phase 2: Root validators set weights at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + // Step several more blocks so bonds form and dividends flow. + // Need at least 2 epochs for bonds to develop: epochs at blocks 7, 9, 11, 13. + for _ in 6..=13 { + step_block(1); + } + log::info!( + "--- Phase 2 final state (block {}) ---", + SubtensorModule::get_current_block_as_u64() + ); + log_subnet_state("SN1 Phase2", netuid1); + log_neuron_state("SN1 neurons Phase2", netuid1, &neurons); + + // Phase 2 assertions: subnet has recovered + let erp_phase2 = EffectiveRootProp::::get(netuid1); + let rp_phase2 = RootProp::::get(netuid1); + log::info!( + "Phase 2 EffectiveRootProp = {:?}, RootProp = {:?}", + erp_phase2, + rp_phase2 + ); + assert!( + erp_phase2 > U96F32::from_num(0), + "Phase 2: EffectiveRootProp should be > 0 (root validators returned, utilization > 0.5)" + ); + + // Root validators now earn dividends + assert!( + root_divs_of(MAJOR_ROOT_HK, netuid1) > 0, + "Phase 2: Major root should earn root dividends after returning" + ); + assert!( + alpha_divs_of(MAJOR_ROOT_HK, netuid1) > 0, + "Phase 2: Major root should earn alpha dividends after returning" + ); + + // Miner continues earning + assert!( + stake_of(MINER1_HK, netuid1) > 0, + "Phase 2: Miner should continue earning incentive" + ); + + // Utilization is above 50% (otherwise hard cap would have zeroed ERP). + // Since the minor root just started validating and bonds are still forming, + // utilization may be ~52%, so ERP < RootProp (scaling applied). That's fine — + // the key invariant is that ERP recovered from 0 to a positive value. + log::info!( + "Test passed: subnet recovered from root validator abandonment. ERP went from 0 to {:?} (RootProp={:?})", + erp_phase2, + rp_phase2 + ); + }); +} diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 609e43cf63..66007e052d 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -911,4 +911,23 @@ 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); + } + + /// Sets the proportion of top subnets that receive emission (0.0-1.0). + pub fn set_emission_top_subnet_proportion(proportion: U64F64) { + EmissionTopSubnetProportion::::set(proportion); + } + + /// Sets the absolute-limit cutoff for subnets that receive emission (None = no limit). + /// Ties at the cutoff are included, so the number of nonzero subnets may exceed N. + pub fn set_emission_top_subnet_absolute_limit(limit: Option) { + match limit { + Some(l) => EmissionTopSubnetAbsoluteLimit::::put(l), + None => EmissionTopSubnetAbsoluteLimit::::kill(), + } + } } diff --git a/scripts/utilization_analysis.py b/scripts/utilization_analysis.py new file mode 100644 index 0000000000..8f2e56dc95 --- /dev/null +++ b/scripts/utilization_analysis.py @@ -0,0 +1,485 @@ +""" +Utilization Analysis Script + +Reads on-chain state to compute dividend-efficiency-based utilization +per subnet, then applies hard cap (< 0.5 -> zero) and scaling (< 1.0 -> +multiply by utilization) to root alpha dividends. + +Implements the same logic as compute_and_store_effective_root_prop() and +the utilization scaling in distribute_emission() from run_coinbase.rs. + +Usage: + python utilization_analysis.py # Normal analysis + python utilization_analysis.py --debug # Debug mode for a single subnet + python utilization_analysis.py --debug 104 # Debug mode for subnet 104 +""" + +import argparse +import sys + +import matplotlib + +matplotlib.use("Agg") + +import matplotlib.pyplot as plt +from substrateinterface import SubstrateInterface + +plt.style.use("ggplot") + +ROOT_NETUID = 0 +HARD_CAP_THRESHOLD = 0.5 +NANO = 1e9 + + +def iter_storage_map(node, storage_name): + return node.query_map("SubtensorModule", storage_name, []) + + +def nano_to_float(x) -> float: + return x / NANO + + +def as_bits(x) -> int: + if isinstance(x, int): + return x + if isinstance(x, dict) and "bits" in x: + return int(x["bits"]) + if hasattr(x, "value"): + return as_bits(x.value) + return int(x) + + +def extract_key(k): + """Extract (netuid, hotkey) from a storage map key.""" + if hasattr(k, "value"): + k = k.value + if not isinstance(k, (list, tuple)) or len(k) < 2: + return None, None + netuid_obj, hotkey_obj = k[0], k[1] + netuid = int(netuid_obj.value) if hasattr(netuid_obj, "value") else int(netuid_obj) + hotkey = str(hotkey_obj.value) if hasattr(hotkey_obj, "value") else str(hotkey_obj) + return netuid, hotkey + + +def extract_value(v) -> float: + raw = v.value if hasattr(v, "value") else v + return nano_to_float(as_bits(raw)) + + +def get_dividends_per_hotkey( + node, storage_name: str, netuids: list[int] +) -> dict[int, dict[str, float]]: + """Read per-hotkey dividends from a (netuid, hotkey) -> amount storage map.""" + wanted = set(netuids) + result: dict[int, dict[str, float]] = {n: {} for n in netuids} + for key, value in iter_storage_map(node, storage_name): + netuid, hotkey = extract_key( + key + if isinstance(key, (list, tuple)) + else key.value + if hasattr(key, "value") + else key + ) + if netuid is None or netuid not in wanted: + continue + amount = extract_value(value) + if amount > 0: + result[netuid][hotkey] = amount + return result + + +def get_root_stakes(node) -> dict[str, float]: + """Read root stake (TotalHotkeyAlpha on root netuid) for all hotkeys.""" + root_stakes: dict[str, float] = {} + for key, value in iter_storage_map(node, "TotalHotkeyAlpha"): + k = key if isinstance(key, (list, tuple)) else ( + key.value if hasattr(key, "value") else key + ) + if not isinstance(k, (list, tuple)) or len(k) < 2: + continue + # TotalHotkeyAlpha key order: (hotkey, netuid) + hotkey_obj, netuid_obj = k[0], k[1] + netuid = int(netuid_obj.value) if hasattr(netuid_obj, "value") else int(netuid_obj) + if netuid != ROOT_NETUID: + continue + hotkey = str(hotkey_obj.value) if hasattr(hotkey_obj, "value") else str(hotkey_obj) + root_stakes[hotkey] = extract_value(value) + return root_stakes + + +def get_subnet_hotkeys(node, netuids: list[int]) -> dict[int, set[str]]: + """Read Keys storage to find hotkeys registered on each subnet.""" + wanted = set(netuids) + result: dict[int, set[str]] = {n: set() for n in netuids} + for key, value in iter_storage_map(node, "Keys"): + k = key if isinstance(key, (list, tuple)) else ( + key.value if hasattr(key, "value") else key + ) + if not isinstance(k, (list, tuple)) or len(k) < 1: + continue + netuid_obj = k[0] + netuid = int(netuid_obj.value) if hasattr(netuid_obj, "value") else int(netuid_obj) + if netuid not in wanted: + continue + hotkey = str(value.value) if hasattr(value, "value") else str(value) + result[netuid].add(hotkey) + return result + + +def compute_utilization( + root_alpha_divs: dict[str, float], + subnet_hotkeys: set[str], + root_stakes: dict[str, float], +) -> float: + """ + Compute dividend-efficiency-based utilization for a subnet. + + For each root-staked validator registered on the subnet: + expected_share = root_stake_i / total_root_stake + actual_share = root_dividends_i / total_root_divs + efficiency = min(actual_share / expected_share, 1.0) + utilization = sum(root_stake_i * efficiency_i) / total_root_stake + + Only root stake of validators with UIDs on the subnet is counted. + + IMPORTANT: RootAlphaDividendsPerSubnet on chain contains post-delegation + amounts (dividends flowed to parent hotkeys not registered on the subnet). + The Rust utilization code uses the pre-delegation map which only contains + registered hotkeys. We must filter to only registered hotkeys here too. + """ + hotkey_root_stakes: list[tuple[str, float]] = [] + total_root_stake = 0.0 + for hotkey in subnet_hotkeys: + rs = root_stakes.get(hotkey, 0.0) + if rs > 0: + hotkey_root_stakes.append((hotkey, rs)) + total_root_stake += rs + + if total_root_stake == 0: + return 0.0 + + # Only count root dividends for hotkeys registered on the subnet (pre-delegation). + # Chain storage includes delegated amounts to parent hotkeys not on the subnet. + total_root_divs = sum(root_alpha_divs.get(hk, 0.0) for hk in subnet_hotkeys) + if total_root_divs == 0: + return 0.0 + + weighted_efficiency_sum = 0.0 + for hotkey, rs in hotkey_root_stakes: + expected_share = rs / total_root_stake + actual_div = root_alpha_divs.get(hotkey, 0.0) + actual_share = actual_div / total_root_divs + if expected_share > 0: + efficiency = min(actual_share / expected_share, 1.0) + else: + efficiency = 0.0 + weighted_efficiency_sum += rs * efficiency + + return weighted_efficiency_sum / total_root_stake + + +def analyze_subnets( + root_alpha_divs_per_subnet: dict[int, dict[str, float]], + alpha_divs_per_subnet: dict[int, dict[str, float]], + subnet_hotkeys: dict[int, set[str]], + root_stakes: dict[str, float], + netuids: list[int], +) -> tuple[dict[int, float], dict[int, float], dict[int, float], dict[int, float]]: + """ + Compute utilization, raw/scaled root dividends, and effective root prop per subnet. + + Returns (utilizations, old_sums, new_sums, effective_root_props). + """ + utilizations: dict[int, float] = {} + old_sums: dict[int, float] = {} + new_sums: dict[int, float] = {} + effective_root_props: dict[int, float] = {} + + for netuid in netuids: + root_divs = root_alpha_divs_per_subnet.get(netuid, {}) + alpha_divs = alpha_divs_per_subnet.get(netuid, {}) + old_total = sum(root_divs.values()) + alpha_total = sum(alpha_divs.values()) + old_sums[netuid] = old_total + + hotkeys = subnet_hotkeys.get(netuid, set()) + util = compute_utilization(root_divs, hotkeys, root_stakes) + utilizations[netuid] = util + + # Apply hard cap: util < 0.5 → withhold all; util >= 0.5 → full dividends + if old_total == 0: + new_sums[netuid] = 0.0 + elif util < HARD_CAP_THRESHOLD: + new_sums[netuid] = 0.0 + else: + new_sums[netuid] = old_total + + # Compute effective root prop + denom = alpha_total + old_total + raw_root_prop = old_total / denom if denom > 0 else 0.0 + if old_total > 0 and util < HARD_CAP_THRESHOLD: + effective_root_props[netuid] = 0.0 + else: + effective_root_props[netuid] = raw_root_prop + + return utilizations, old_sums, new_sums, effective_root_props + + +def plot_results( + old_sums: dict[int, float], + new_sums: dict[int, float], + utilizations: dict[int, float], + netuids: list[int], + output_path: str = "utilization_analysis.png", +): + """Generate a two-panel plot: root dividends comparison and utilization bars.""" + active = [ + n for n in netuids if old_sums.get(n, 0) > 0 or new_sums.get(n, 0) > 0 + ] + if not active: + print("No active subnets to plot.") + return + + old_vals = [old_sums.get(n, 0) for n in active] + new_vals = [new_sums.get(n, 0) for n in active] + utils = [utilizations.get(n, 0) for n in active] + + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10)) + + # Panel 1: root dividends comparison + x = range(len(active)) + width = 0.35 + ax1.bar(x, old_vals, width, label="Raw Root Dividends") + ax1.bar([i + width for i in x], new_vals, width, label="After Utilization Scaling") + ax1.set_xlabel("Netuid") + ax1.set_ylabel("Root Alpha Dividends (ALPHA)") + ax1.set_title("Root Alpha Dividends: Before vs After Utilization Scaling + Hard Cap") + ax1.set_xticks([i + width / 2 for i in x]) + ax1.set_xticklabels(active, rotation=90, fontsize=6) + ax1.legend() + + # Panel 2: utilization per subnet + colors = [ + "red" if u < HARD_CAP_THRESHOLD else "orange" if u < 1.0 else "green" + for u in utils + ] + ax2.bar(range(len(active)), utils, color=colors) + ax2.axhline( + y=HARD_CAP_THRESHOLD, + color="red", + linestyle="--", + label=f"Hard Cap ({HARD_CAP_THRESHOLD})", + ) + ax2.axhline(y=1.0, color="green", linestyle="--", label="Full Utilization") + ax2.set_xlabel("Netuid") + ax2.set_ylabel("Utilization") + ax2.set_title("Dividend-Efficiency Utilization per Subnet") + ax2.set_xticks(range(len(active))) + ax2.set_xticklabels(active, rotation=90, fontsize=6) + ax2.legend() + + plt.tight_layout() + plt.savefig(output_path, dpi=150) + print(f"Plot saved to {output_path}") + + +# ========================================================================= +# Chain data reader (shared between normal and debug modes) +# ========================================================================= + +def read_chain_data(node, netuids): + """Read all chain state needed for analysis. Returns a dict of data.""" + print("Reading chain state...") + + print(" Reading root alpha dividends per hotkey...") + root_alpha_divs = get_dividends_per_hotkey( + node, "RootAlphaDividendsPerSubnet", netuids + ) + + print(" Reading alpha dividends per hotkey...") + alpha_divs = get_dividends_per_hotkey( + node, "AlphaDividendsPerSubnet", netuids + ) + + print(" Reading root stakes...") + root_stakes = get_root_stakes(node) + + print(" Reading subnet hotkeys...") + subnet_hotkeys = get_subnet_hotkeys(node, netuids) + + return { + "root_alpha_divs": root_alpha_divs, + "alpha_divs": alpha_divs, + "root_stakes": root_stakes, + "subnet_hotkeys": subnet_hotkeys, + } + + +# ========================================================================= +# Debug mode: per-hotkey breakdown for a single subnet +# ========================================================================= + +def run_debug(node, netuids, target_netuid): + """Debug mode: show per-hotkey detail and overlap analysis for one subnet.""" + data = read_chain_data(node, netuids) + root_alpha_divs = data["root_alpha_divs"] + root_stakes = data["root_stakes"] + subnet_hotkeys = data["subnet_hotkeys"] + + root_divs = root_alpha_divs.get(target_netuid, {}) + hotkeys = subnet_hotkeys.get(target_netuid, set()) + + div_set = set(root_divs.keys()) + stake_set = set(root_stakes.keys()) + + sep = "=" * 90 + print(f"\n{sep}") + print(f"DEBUG: Subnet {target_netuid}") + print(f"{sep}\n") + + print(f"Hotkeys with root dividends (post-delegation): {len(div_set)}") + print(f"Hotkeys registered on subnet (Keys): {len(hotkeys)}") + print(f"Hotkeys with root stake (global): {len(stake_set)}") + + print(f"\nRoot div hotkeys in root_stakes: {len(div_set & stake_set)} / {len(div_set)}") + print(f"Root div hotkeys in subnet Keys: {len(div_set & hotkeys)} / {len(div_set)}") + print(f"Subnet hotkeys with root stake: {len(hotkeys & stake_set)} / {len(hotkeys)}") + + if div_set and not (div_set & stake_set): + print("\n*** WARNING: No overlap between root_div hotkeys and root_stakes! ***") + + # Find root-staked validators on this subnet + hotkey_rs: list[tuple[str, float]] = [] + total_root_stake = 0.0 + for hk in hotkeys: + rs = root_stakes.get(hk, 0.0) + if rs > 0: + hotkey_rs.append((hk, rs)) + total_root_stake += rs + + # Only count registered-hotkey root divs (pre-delegation) + registered_root_divs = sum(root_divs.get(hk, 0.0) for hk in hotkeys) + total_root_divs_all = sum(root_divs.values()) + + print(f"\nRoot-staked validators on subnet: {len(hotkey_rs)}") + print(f"Total root stake on subnet: {total_root_stake:.2f}") + print(f"Root divs (registered only): {registered_root_divs:.6f}") + print(f"Root divs (all, post-delegation): {total_root_divs_all:.6f}") + + if total_root_stake > 0 and registered_root_divs > 0: + print(f"\n{'Hotkey':>20} {'Root Stake':>12} {'Expected':>10} " + f"{'Actual Div':>12} {'Actual Share':>12} {'Efficiency':>12}") + print("-" * 82) + + weighted_eff_sum = 0.0 + for hk, rs in sorted(hotkey_rs, key=lambda x: -x[1]): + expected = rs / total_root_stake + actual_div = root_divs.get(hk, 0.0) + actual = actual_div / registered_root_divs if registered_root_divs > 0 else 0.0 + eff = min(actual / expected, 1.0) if expected > 0 else 0.0 + weighted_eff_sum += rs * eff + print( + f"{hk[:20]:>20} {rs:>12.2f} {expected:>10.6f} " + f"{actual_div:>12.6f} {actual:>12.6f} {eff:>12.6f}" + ) + + util = weighted_eff_sum / total_root_stake + status = "HARD-CAP" if util < HARD_CAP_THRESHOLD else "ACTIVE" + print(f"\nUtilization: {util:.6f} ({status})") + else: + print("\nUtilization: 0.000000 (no root stake or no root divs)") + + +# ========================================================================= +# Normal mode: full analysis across all subnets +# ========================================================================= + +def run_analysis(node, netuids): + """Normal mode: analyze all subnets, print table, generate plot.""" + data = read_chain_data(node, netuids) + + print("Computing utilization and scaling...") + utilizations, old_sums, new_sums, effective_root_props = analyze_subnets( + data["root_alpha_divs"], + data["alpha_divs"], + data["subnet_hotkeys"], + data["root_stakes"], + netuids, + ) + + # Categorize subnets + hard_capped = [ + n for n in netuids + if utilizations.get(n, 0) < HARD_CAP_THRESHOLD and old_sums.get(n, 0) > 0 + ] + active = [ + n for n in netuids + if utilizations.get(n, 0) >= HARD_CAP_THRESHOLD and old_sums.get(n, 0) > 0 + ] + + sep = "=" * 90 + print(f"\n{sep}") + print("UTILIZATION ANALYSIS") + print(f"{sep}\n") + + print( + f"Hard-capped subnets (util < {HARD_CAP_THRESHOLD}, all root divs recycled): " + f"{hard_capped}" + ) + print(f"Active subnets (util >= {HARD_CAP_THRESHOLD}, full dividends): {active}") + + header = ( + f"{'Netuid':>8} {'Utilization':>12} {'Raw Root Divs':>15} " + f"{'Effective Root Divs':>20} {'ERP':>12} {'Status':>12}" + ) + print(f"\n{header}") + print("-" * 90) + for netuid in netuids: + util = utilizations.get(netuid, 0) + old = old_sums.get(netuid, 0) + new = new_sums.get(netuid, 0) + erp = effective_root_props.get(netuid, 0) + if old > 0 or new > 0: + status = "HARD-CAP" if util < HARD_CAP_THRESHOLD and old > 0 else "ACTIVE" + print( + f"{netuid:>8} {util:>12.6f} {old:>15.2f} " + f"{new:>20.2f} {erp:>12.8f} {status:>12}" + ) + + total_old = sum(old_sums.values()) + total_new = sum(new_sums.values()) + recycled = total_old - total_new + print(f"\nTotal raw root dividends: {total_old:.2f}") + print(f"Total after scaling: {total_new:.2f}") + if total_old > 0: + pct = recycled / total_old * 100 + print(f"Total recycled: {recycled:.2f} ({pct:.1f}%)") + + plot_results(old_sums, new_sums, utilizations, netuids) + + +def main(): + parser = argparse.ArgumentParser( + description="Analyze dividend-efficiency utilization per subnet" + ) + parser.add_argument( + "--debug", + nargs="?", + const=1, + type=int, + metavar="NETUID", + help="Debug mode: show per-hotkey breakdown for a single subnet (default: 1)", + ) + args = parser.parse_args() + + node = SubstrateInterface(url="wss://entrypoint-finney.opentensor.ai:443") + netuids = list(range(1, 129)) + + if args.debug is not None: + run_debug(node, netuids, args.debug) + else: + run_analysis(node, netuids) + + +if __name__ == "__main__": + main()