diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index e058041936..1d0f49c9fc 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -31,7 +31,7 @@ pub mod pallet { utils::rate_limiting::{Hyperparameter, TransactionType}, }; use sp_runtime::BoundedVec; - use substrate_fixed::types::I96F32; + use substrate_fixed::types::{I64F64, I96F32, U64F64}; use subtensor_runtime_common::{MechId, NetUid, TaoCurrency}; /// The main data structure of the module. @@ -115,6 +115,8 @@ pub mod pallet { MaxAllowedUidsLessThanMinAllowedUids, /// The maximum allowed UIDs must be less than the default maximum allowed UIDs. MaxAllowedUidsGreaterThanDefaultMaxAllowedUids, + /// Bad parameter value + InvalidValue, } /// Enum for specifying the type of precompile operation. #[derive( @@ -2102,6 +2104,71 @@ pub mod pallet { ); Ok(()) } + + /// Sets TAO flow cutoff value (A) + #[pallet::call_index(81)] + #[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_tao_flow_cutoff( + origin: OriginFor, + flow_cutoff: I64F64, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_tao_flow_cutoff(flow_cutoff); + log::debug!("set_tao_flow_cutoff( {flow_cutoff:?} ) "); + Ok(()) + } + + /// Sets TAO flow normalization exponent (p) + #[pallet::call_index(82)] + #[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_tao_flow_normalization_exponent( + origin: OriginFor, + exponent: U64F64, + ) -> DispatchResult { + ensure_root(origin)?; + + let one = U64F64::saturating_from_num(1); + let two = U64F64::saturating_from_num(2); + ensure!( + (one <= exponent) && (exponent <= two), + Error::::InvalidValue + ); + + pallet_subtensor::Pallet::::set_tao_flow_normalization_exponent(exponent); + log::debug!("set_tao_flow_normalization_exponent( {exponent:?} ) "); + Ok(()) + } + + /// Sets TAO flow smoothing factor (alpha) + #[pallet::call_index(83)] + #[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_tao_flow_smoothing_factor( + origin: OriginFor, + smoothing_factor: u64, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_tao_flow_smoothing_factor(smoothing_factor); + log::debug!("set_tao_flow_smoothing_factor( {smoothing_factor:?} ) "); + Ok(()) + } } } diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 4dac3347e6..8006feadd2 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -1,7 +1,9 @@ use super::*; use crate::alloc::borrow::ToOwned; use alloc::collections::BTreeMap; -use substrate_fixed::types::U96F32; +use safe_math::FixedExt; +use substrate_fixed::transcendental::{exp, ln}; +use substrate_fixed::types::{I32F32, I64F64, U64F64, U96F32}; impl Pallet { pub fn get_subnet_block_emissions( @@ -17,29 +19,275 @@ impl Pallet { .collect(); log::debug!("Subnets to emit to: {subnets_to_emit_to:?}"); + // Get subnet TAO emissions. + let shares = Self::get_shares(&subnets_to_emit_to); + log::debug!("Subnet emission shares = {shares:?}"); + + shares + .into_iter() + .map(|(netuid, share)| { + let emission = U64F64::saturating_from_num(block_emission).saturating_mul(share); + (netuid, U96F32::saturating_from_num(emission)) + }) + .collect::>() + } + + pub fn record_tao_inflow(netuid: NetUid, tao: TaoCurrency) { + SubnetTaoFlow::::mutate(netuid, |flow| { + *flow = flow.saturating_add(u64::from(tao) as i64); + }); + } + + pub fn record_tao_outflow(netuid: NetUid, tao: TaoCurrency) { + SubnetTaoFlow::::mutate(netuid, |flow| { + *flow = flow.saturating_sub(u64::from(tao) as i64) + }); + } + + pub fn reset_tao_outflow(netuid: NetUid) { + SubnetTaoFlow::::remove(netuid); + } + + // Update SubnetEmaTaoFlow if needed and return its value for + // the current block + fn get_ema_flow(netuid: NetUid) -> I64F64 { + let current_block: u64 = Self::get_current_block_as_u64(); + + // Calculate net ema flow for the next block + let block_flow = I64F64::saturating_from_num(SubnetTaoFlow::::get(netuid)); + if let Some((last_block, last_block_ema)) = SubnetEmaTaoFlow::::get(netuid) { + // EMA flow already initialized + if last_block != current_block { + let flow_alpha = I64F64::saturating_from_num(FlowEmaSmoothingFactor::::get()) + .safe_div(I64F64::saturating_from_num(u16::MAX)); + let one = I64F64::saturating_from_num(1); + let ema_flow = (one.saturating_sub(flow_alpha)) + .saturating_mul(last_block_ema) + .saturating_add(flow_alpha.saturating_mul(block_flow)); + SubnetEmaTaoFlow::::insert(netuid, (current_block, ema_flow)); + + // Drop the accumulated flow in the last block + Self::reset_tao_outflow(netuid); + ema_flow + } else { + last_block_ema + } + } else { + // Initialize EMA flow, set S(current_block) = 0 + let ema_flow = I64F64::saturating_from_num(0); + SubnetEmaTaoFlow::::insert(netuid, (current_block, ema_flow)); + ema_flow + } + } + + // Either the minimal EMA flow L = min{Si}, or an artificial + // cut off at some higher value A (TaoFlowCutoff) + // L = max {A, min{min{S[i], 0}}} + fn get_lower_limit(ema_flows: &BTreeMap) -> I64F64 { + let zero = I64F64::saturating_from_num(0); + let min_flow = ema_flows + .values() + .map(|flow| flow.min(&zero)) + .min() + .unwrap_or(&zero); + let flow_cutoff = TaoFlowCutoff::::get(); + flow_cutoff.max(*min_flow) + } + + // Estimate the upper value of pow with hardcoded p = 2 + fn pow_estimate(val: U64F64) -> U64F64 { + val.saturating_mul(val) + } + + fn safe_pow(val: U64F64, p: U64F64) -> U64F64 { + // If val is too low so that ln(val) doesn't fit I32F32::MIN, + // return 0 from the function + let zero = U64F64::saturating_from_num(0); + let i32f32_max = I32F32::saturating_from_num(i32::MAX); + if let Ok(val_ln) = ln(I32F32::saturating_from_num(val)) { + // If exp doesn't fit, do the best we can - max out on I32F32::MAX + U64F64::saturating_from_num(I32F32::saturating_from_num( + exp(I32F32::saturating_from_num(p).saturating_mul(val_ln)).unwrap_or(i32f32_max), + )) + } else { + zero + } + } + + fn inplace_scale(offset_flows: &mut BTreeMap) { + let zero = U64F64::saturating_from_num(0); + let flow_max = offset_flows.values().copied().max().unwrap_or(zero); + + // Calculate scale factor so that max becomes 1.0 + let flow_factor = U64F64::saturating_from_num(1).safe_div(flow_max); + + // Upscale/downscale in-place + for flow in offset_flows.values_mut() { + *flow = flow_factor.saturating_mul(*flow); + } + } + + pub(crate) fn inplace_pow_normalize(offset_flows: &mut BTreeMap, p: U64F64) { + // Scale offset flows so that that are no overflows and underflows when we use safe_pow: + // flow_factor * subnet_count * (flow_max ^ p) <= I32F32::MAX + let zero = U64F64::saturating_from_num(0); + let subnet_count = offset_flows.len(); + + // Pre-scale to max 1.0 + Self::inplace_scale(offset_flows); + + // Scale to maximize precision + let flow_max = offset_flows.values().copied().max().unwrap_or(zero); + log::debug!("Offset flow max: {flow_max:?}"); + let flow_max_pow_est = Self::pow_estimate(flow_max); + log::debug!("flow_max_pow_est: {flow_max_pow_est:?}"); + + let max_times_count = + U64F64::saturating_from_num(subnet_count).saturating_mul(flow_max_pow_est); + let i32f32_max = U64F64::saturating_from_num(i32::MAX); + let precision_min = i32f32_max.safe_div(U64F64::saturating_from_num(u64::MAX)); + + // If max_times_count < precision_min, all flow values are too low to fit I32F32. + if max_times_count >= precision_min { + let epsilon = + U64F64::saturating_from_num(1).safe_div(U64F64::saturating_from_num(1_000)); + let flow_factor = i32f32_max + .safe_div(max_times_count) + .checked_sqrt(epsilon) + .unwrap_or(zero); + + // Calculate sum + let sum = offset_flows + .clone() + .into_values() + .map(|flow| flow_factor.saturating_mul(flow)) + .map(|scaled_flow| Self::safe_pow(scaled_flow, p)) + .sum(); + log::debug!("Scaled offset flow sum: {sum:?}"); + + // Normalize in-place + for flow in offset_flows.values_mut() { + let scaled_flow = flow_factor.saturating_mul(*flow); + *flow = Self::safe_pow(scaled_flow, p).safe_div(sum); + } + } + } + + // Implementation of shares that uses TAO flow + fn get_shares_flow(subnets_to_emit_to: &[NetUid]) -> BTreeMap { + // Get raw flows + let ema_flows = subnets_to_emit_to + .iter() + .map(|netuid| (*netuid, Self::get_ema_flow(*netuid))) + .collect(); + log::debug!("EMA flows: {ema_flows:?}"); + + // Clip the EMA flow with lower limit L + // z[i] = max{S[i] − L, 0} + let lower_limit = Self::get_lower_limit(&ema_flows); + log::debug!("Lower flow limit: {lower_limit:?}"); + let mut offset_flows = ema_flows + .iter() + .map(|(netuid, flow)| { + ( + *netuid, + if *flow > lower_limit { + U64F64::saturating_from_num(flow.saturating_sub(lower_limit)) + } else { + U64F64::saturating_from_num(0) + }, + ) + }) + .collect::>(); + + // Normalize the set {z[i]}, using an exponent parameter (p ≥ 1) + let p = FlowNormExponent::::get(); + Self::inplace_pow_normalize(&mut offset_flows, p); + offset_flows + } + + // DEPRECATED: Implementation of shares that uses EMA prices will be gradually deprecated + fn get_shares_price_ema(subnets_to_emit_to: &[NetUid]) -> BTreeMap { // Get sum of alpha moving prices let total_moving_prices = subnets_to_emit_to .iter() - .map(|netuid| Self::get_moving_alpha_price(*netuid)) - .fold(U96F32::saturating_from_num(0.0), |acc, ema| { + .map(|netuid| U64F64::saturating_from_num(Self::get_moving_alpha_price(*netuid))) + .fold(U64F64::saturating_from_num(0.0), |acc, ema| { acc.saturating_add(ema) }); log::debug!("total_moving_prices: {total_moving_prices:?}"); - // Get subnet TAO emissions. + // Calculate shares. subnets_to_emit_to - .into_iter() + .iter() .map(|netuid| { - let moving_price = Self::get_moving_alpha_price(netuid); + let moving_price = + U64F64::saturating_from_num(Self::get_moving_alpha_price(*netuid)); log::debug!("moving_price_i: {moving_price:?}"); - let share = block_emission - .saturating_mul(moving_price) + let share = moving_price .checked_div(total_moving_prices) - .unwrap_or(U96F32::from_num(0)); + .unwrap_or(U64F64::saturating_from_num(0)); - (netuid, share) + (*netuid, share) }) - .collect::>() + .collect::>() + } + + // Combines ema price method and tao flow method linearly over FlowHalfLife blocks + pub(crate) fn get_shares(subnets_to_emit_to: &[NetUid]) -> BTreeMap { + let current_block: u64 = Self::get_current_block_as_u64(); + + // Weight of tao flow method + let period = FlowHalfLife::::get(); + let one = U64F64::saturating_from_num(1); + let zero = U64F64::saturating_from_num(0); + let tao_flow_weight = if let Some(start_block) = FlowFirstBlock::::get() { + if (current_block > start_block) && (current_block < start_block.saturating_add(period)) + { + // Combination period in progress + let start_fixed = U64F64::saturating_from_num(start_block); + let current_fixed = U64F64::saturating_from_num(current_block); + let period_fixed = U64F64::saturating_from_num(period); + current_fixed + .saturating_sub(start_fixed) + .safe_div(period_fixed) + } else if current_block >= start_block.saturating_add(period) { + // Over combination period + one + } else { + // Not yet in combination period + zero + } + } else { + zero + }; + + // Get shares for each method as needed + let shares_flow = if tao_flow_weight > zero { + Self::get_shares_flow(subnets_to_emit_to) + } else { + BTreeMap::new() + }; + + let shares_prices = if tao_flow_weight < one { + Self::get_shares_price_ema(subnets_to_emit_to) + } else { + BTreeMap::new() + }; + + // Combine + let mut shares_combined = BTreeMap::new(); + for netuid in subnets_to_emit_to.iter() { + let share_flow = shares_flow.get(netuid).unwrap_or(&zero); + let share_prices = shares_prices.get(netuid).unwrap_or(&zero); + shares_combined.insert( + *netuid, + share_flow.saturating_mul(tao_flow_weight).saturating_add( + share_prices.saturating_mul(one.saturating_sub(tao_flow_weight)), + ), + ); + } + shares_combined } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index f52e1e078b..5dbdcddb9d 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -88,6 +88,7 @@ pub mod pallet { use frame_system::pallet_prelude::*; use pallet_drand::types::RoundNumber; use runtime_common::prod_or_fast; + use safe_math::FixedExt; use sp_core::{ConstU32, H160, H256}; use sp_runtime::traits::{Dispatchable, TrailingZeroInput}; use sp_std::collections::btree_map::BTreeMap; @@ -95,7 +96,7 @@ pub mod pallet { use sp_std::collections::vec_deque::VecDeque; use sp_std::vec; use sp_std::vec::Vec; - use substrate_fixed::types::{I96F32, U64F64}; + use substrate_fixed::types::{I64F64, I96F32, U64F64}; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{ AlphaCurrency, Currency, MechId, NetUid, NetUidStorageIndex, TaoCurrency, @@ -376,6 +377,11 @@ pub mod pallet { pub fn DefaultZeroU64() -> u64 { 0 } + #[pallet::type_value] + /// Default value for zero. + pub fn DefaultZeroI64() -> i64 { + 0 + } /// Default value for Alpha currency. #[pallet::type_value] pub fn DefaultZeroAlpha() -> AlphaCurrency { @@ -1269,6 +1275,51 @@ pub mod pallet { pub type TokenSymbol = StorageMap<_, Identity, NetUid, Vec, ValueQuery, DefaultUnicodeVecU8>; + #[pallet::storage] // --- MAP ( netuid ) --> subnet_tao_flow | Returns the TAO inflow-outflow balance. + pub type SubnetTaoFlow = + StorageMap<_, Identity, NetUid, i64, ValueQuery, DefaultZeroI64>; + #[pallet::storage] // --- MAP ( netuid ) --> subnet_ema_tao_flow | Returns the EMA of TAO inflow-outflow balance. + pub type SubnetEmaTaoFlow = + StorageMap<_, Identity, NetUid, (u64, I64F64), OptionQuery>; + #[pallet::type_value] + /// Default value for flow cutoff. + pub fn DefaultFlowCutoff() -> I64F64 { + I64F64::saturating_from_num(0) + } + #[pallet::storage] + /// --- ITEM --> TAO Flow Cutoff + pub type TaoFlowCutoff = StorageValue<_, I64F64, ValueQuery, DefaultFlowCutoff>; + #[pallet::type_value] + /// Default value for flow normalization exponent. + pub fn DefaultFlowNormExponent() -> U64F64 { + U64F64::saturating_from_num(15).safe_div(U64F64::saturating_from_num(10)) + } + #[pallet::storage] + /// --- ITEM --> Flow Normalization Exponent (p) + pub type FlowNormExponent = + StorageValue<_, U64F64, ValueQuery, DefaultFlowNormExponent>; + #[pallet::type_value] + /// Default value for flow EMA smoothing. + pub fn DefaultFlowEmaSmoothingFactor() -> u64 { + // Example values: + // half-life factor value u64 normalized + // 216000 (1 month) --> 0.000003209009576 ( 59_195_778_378_555) + // 50400 (1 week) --> 0.000013752825678 (253_694_855_576_670) + 59_195_778_378_555 + } + #[pallet::type_value] + /// Flow EMA smoothing half-life. + pub fn FlowHalfLife() -> u64 { + 216_000 + } + #[pallet::storage] + /// --- ITEM --> Flow EMA smoothing factor (flow alpha), u64 normalized + pub type FlowEmaSmoothingFactor = + StorageValue<_, u64, ValueQuery, DefaultFlowEmaSmoothingFactor>; + #[pallet::storage] + /// --- ITEM --> Block when TAO flow calculation starts(ed) + pub type FlowFirstBlock = StorageValue<_, u64, OptionQuery>; + /// ============================ /// ==== Global Parameters ===== /// ============================ diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 87a87e911c..a4cdbfbe94 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -158,6 +158,8 @@ mod hooks { .saturating_add(migrations::migrate_auto_stake_destination::migrate_auto_stake_destination::()) // Migrate Kappa to default (0.5) .saturating_add(migrations::migrate_kappa_map_to_default::migrate_kappa_map_to_default::()) + // Set the first block of tao flow + .saturating_add(migrations::migrate_set_first_tao_flow_block::migrate_set_first_tao_flow_block::()) // Remove obsolete map entries .saturating_add(migrations::migrate_remove_tao_dividends::migrate_remove_tao_dividends::()); weight diff --git a/pallets/subtensor/src/migrations/migrate_set_first_tao_flow_block.rs b/pallets/subtensor/src/migrations/migrate_set_first_tao_flow_block.rs new file mode 100644 index 0000000000..99c10b99ba --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_set_first_tao_flow_block.rs @@ -0,0 +1,38 @@ +use super::*; +use crate::HasMigrationRun; +use frame_support::{traits::Get, weights::Weight}; +use scale_info::prelude::string::String; + +pub fn migrate_set_first_tao_flow_block() -> Weight { + let migration_name = b"migrate_set_first_tao_flow_block".to_vec(); + + let mut weight = T::DbWeight::get().reads(1); + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + "Running migration '{:?}'", + String::from_utf8_lossy(&migration_name) + ); + + // Actual migration + let current_block = Pallet::::get_current_block_as_u64(); + FlowFirstBlock::::set(Some(current_block)); + weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); + + // Mark Migration as Completed + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().reads(2)); + + log::info!( + "Migration '{:?}' completed successfully.", + String::from_utf8_lossy(&migration_name) + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index d95e4c7bac..2313870244 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -40,6 +40,7 @@ pub mod migrate_remove_zero_total_hotkey_alpha; pub mod migrate_reset_bonds_moving_average; pub mod migrate_reset_max_burn; pub mod migrate_set_first_emission_block_number; +pub mod migrate_set_first_tao_flow_block; pub mod migrate_set_min_burn; pub mod migrate_set_min_difficulty; pub mod migrate_set_nominator_min_stake; diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 671632f320..21ff1a57d5 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -721,6 +721,9 @@ impl Pallet { // }); // } + // Record TAO outflow + Self::record_tao_outflow(netuid, swap_result.amount_paid_out.into()); + LastColdkeyHotkeyStakeBlock::::insert(coldkey, hotkey, Self::get_current_block_as_u64()); // Deposit and log the unstaking event. @@ -795,6 +798,9 @@ impl Pallet { StakingHotkeys::::insert(coldkey, staking_hotkeys.clone()); } + // Record TAO inflow + Self::record_tao_inflow(netuid, swap_result.amount_paid_in.into()); + LastColdkeyHotkeyStakeBlock::::insert(coldkey, hotkey, Self::get_current_block_as_u64()); if set_limit { diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 19ce6da28d..75d4cc3ce9 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -8,7 +8,7 @@ use approx::assert_abs_diff_eq; use frame_support::assert_ok; use pallet_subtensor_swap::position::PositionId; use sp_core::U256; -use substrate_fixed::types::{I64F64, I96F32, U96F32}; +use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32}; use subtensor_runtime_common::{AlphaCurrency, NetUidStorageIndex}; use subtensor_swap_interface::{SwapEngine, SwapHandler}; @@ -80,21 +80,22 @@ fn test_coinbase_basecase() { // Test the emission distribution for a single subnet. // This test verifies that: -// - A single subnet receives the full emission amount -// - The emission is correctly reflected in SubnetTAO -// - Total issuance and total stake are updated appropriately +// - Single subnet gets cutoff by lower flow limit, so nothing is distributed // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_tao_issuance_base --exact --show-output --nocapture #[test] fn test_coinbase_tao_issuance_base() { new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); let emission = TaoCurrency::from(1_234_567); - add_network(netuid, 1, 0); - assert_eq!(SubnetTAO::::get(netuid), TaoCurrency::ZERO); + let subnet_owner_ck = U256::from(1001); + let subnet_owner_hk = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + SubnetMovingPrice::::insert(netuid, I96F32::from(3141) / I96F32::from(1000)); + let tao_in_before = SubnetTAO::::get(netuid); + let total_stake_before = TotalStake::::get(); SubtensorModule::run_coinbase(U96F32::from_num(emission)); - assert_eq!(SubnetTAO::::get(netuid), emission); + assert_eq!(SubnetTAO::::get(netuid), tao_in_before + emission); assert_eq!(TotalIssuance::::get(), emission); - assert_eq!(TotalStake::::get(), emission); + assert_eq!(TotalStake::::get(), total_stake_before + emission); }); } @@ -113,6 +114,31 @@ fn test_coinbase_tao_issuance_base_low() { }); } +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_tao_issuance_base_low_flow --exact --show-output --nocapture +#[test] +fn test_coinbase_tao_issuance_base_low_flow() { + new_test_ext(1).execute_with(|| { + let emission = TaoCurrency::from(1_234_567); + let subnet_owner_ck = U256::from(1001); + let subnet_owner_hk = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + let emission = TaoCurrency::from(1); + + // 100% tao flow method + let block_num = FlowHalfLife::::get(); + SubnetEmaTaoFlow::::insert(netuid, (block_num, I64F64::from_num(1_000_000_000))); + FlowFirstBlock::::set(Some(0_u64)); + System::set_block_number(block_num); + + let tao_in_before = SubnetTAO::::get(netuid); + let total_stake_before = TotalStake::::get(); + SubtensorModule::run_coinbase(U96F32::from_num(emission)); + assert_eq!(SubnetTAO::::get(netuid), tao_in_before + emission); + assert_eq!(TotalIssuance::::get(), emission); + assert_eq!(TotalStake::::get(), total_stake_before + emission); + }); +} + // Test emission distribution across multiple subnets. // This test verifies that: // - Multiple subnets receive equal portions of the total emission @@ -134,11 +160,23 @@ fn test_coinbase_tao_issuance_multiple() { assert_eq!(SubnetTAO::::get(netuid2), TaoCurrency::ZERO); assert_eq!(SubnetTAO::::get(netuid3), TaoCurrency::ZERO); SubtensorModule::run_coinbase(U96F32::from_num(emission)); - assert_eq!(SubnetTAO::::get(netuid1), emission / 3.into()); - assert_eq!(SubnetTAO::::get(netuid2), emission / 3.into()); - assert_eq!(SubnetTAO::::get(netuid3), emission / 3.into()); - assert_eq!(TotalIssuance::::get(), emission); - assert_eq!(TotalStake::::get(), emission); + assert_abs_diff_eq!( + SubnetTAO::::get(netuid1), + emission / 3.into(), + epsilon = 1.into(), + ); + assert_abs_diff_eq!( + SubnetTAO::::get(netuid2), + emission / 3.into(), + epsilon = 1.into(), + ); + assert_abs_diff_eq!( + SubnetTAO::::get(netuid3), + emission / 3.into(), + epsilon = 1.into(), + ); + assert_abs_diff_eq!(TotalIssuance::::get(), emission, epsilon = 3.into(),); + assert_abs_diff_eq!(TotalStake::::get(), emission, epsilon = 3.into(),); }); } @@ -222,6 +260,89 @@ fn test_coinbase_tao_issuance_different_prices() { }); } +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_tao_issuance_different_flows --exact --show-output --nocapture +#[test] +fn test_coinbase_tao_issuance_different_flows() { + new_test_ext(1).execute_with(|| { + let subnet_owner_ck = U256::from(1001); + let subnet_owner_hk = U256::from(1002); + let netuid1 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + let netuid2 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + let emission = 100_000_000; + + // Setup prices 0.1 and 0.2 + let initial_tao: u64 = 100_000_u64; + let initial_alpha1: u64 = initial_tao * 10; + let initial_alpha2: u64 = initial_tao * 5; + mock::setup_reserves(netuid1, initial_tao.into(), initial_alpha1.into()); + mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha2.into()); + + // Force the swap to initialize + SubtensorModule::swap_tao_for_alpha( + netuid1, + TaoCurrency::ZERO, + 1_000_000_000_000.into(), + false, + ) + .unwrap(); + SubtensorModule::swap_tao_for_alpha( + netuid2, + TaoCurrency::ZERO, + 1_000_000_000_000.into(), + false, + ) + .unwrap(); + + // Set subnet prices to reversed proportion to ensure they don't affect emissions. + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(2)); + SubnetMovingPrice::::insert(netuid2, I96F32::from_num(1)); + + // Set subnet tao flow ema. + // 100% tao flow method + let block_num = FlowHalfLife::::get(); + SubnetEmaTaoFlow::::insert(netuid1, (block_num, I64F64::from_num(1))); + SubnetEmaTaoFlow::::insert(netuid2, (block_num, I64F64::from_num(2))); + FlowFirstBlock::::set(Some(0_u64)); + System::set_block_number(block_num); + + // Set normalization exponent to 1 for simplicity + FlowNormExponent::::set(U64F64::from(1_u64)); + + // Assert initial TAO reserves. + assert_eq!(SubnetTAO::::get(netuid1), initial_tao.into()); + assert_eq!(SubnetTAO::::get(netuid2), initial_tao.into()); + let total_stake_before = TotalStake::::get(); + + // Run the coinbase with the emission amount. + SubtensorModule::run_coinbase(U96F32::from_num(emission)); + + // Assert tao emission is split evenly. + assert_abs_diff_eq!( + SubnetTAO::::get(netuid1), + TaoCurrency::from(initial_tao + emission / 3), + epsilon = 10.into(), + ); + assert_abs_diff_eq!( + SubnetTAO::::get(netuid2), + TaoCurrency::from(initial_tao + 2 * emission / 3), + epsilon = 10.into(), + ); + + // Prices are low => we limit tao issued (buy alpha with it) + let tao_issued = TaoCurrency::from(((0.1 + 0.2) * emission as f64) as u64); + assert_abs_diff_eq!( + TotalIssuance::::get(), + tao_issued, + epsilon = 10.into() + ); + assert_abs_diff_eq!( + TotalStake::::get(), + total_stake_before + emission.into(), + epsilon = 10.into() + ); + }); +} + // Test moving price updates with different alpha values. // This test verifies that: // - Moving price stays constant when alpha is 1.0 @@ -352,6 +473,8 @@ fn test_coinbase_alpha_issuance_base() { SubnetAlphaIn::::insert(netuid1, AlphaCurrency::from(initial)); SubnetTAO::::insert(netuid2, TaoCurrency::from(initial)); SubnetAlphaIn::::insert(netuid2, AlphaCurrency::from(initial)); + SubnetMovingPrice::::insert(netuid1, I96F32::from(1)); + SubnetMovingPrice::::insert(netuid2, I96F32::from(1)); // Check initial SubtensorModule::run_coinbase(U96F32::from_num(emission)); // tao_in = 500_000 @@ -393,6 +516,9 @@ fn test_coinbase_alpha_issuance_different() { // Set subnet prices. SubnetMovingPrice::::insert(netuid1, I96F32::from_num(1)); SubnetMovingPrice::::insert(netuid2, I96F32::from_num(2)); + // Set tao flow + SubnetEmaTaoFlow::::insert(netuid1, (1u64, I64F64::from_num(1))); + SubnetEmaTaoFlow::::insert(netuid2, (1u64, I64F64::from_num(2))); // Run coinbase SubtensorModule::run_coinbase(U96F32::from_num(emission)); // tao_in = 333_333 @@ -1843,7 +1969,7 @@ fn test_calculate_dividend_distribution_totals() { let pending_validator_alpha = AlphaCurrency::from(183_123_567_452); let pending_root_alpha = AlphaCurrency::from(837_120_949_872); - let tao_weight: U96F32 = U96F32::saturating_from_num(0.18); // 18% + let tao_weight: U96F32 = U96F32::from_num(0.18); // 18% let hotkeys = [U256::from(0), U256::from(1)]; @@ -1867,12 +1993,12 @@ fn test_calculate_dividend_distribution_totals() { let total_root_alpha_dividends = root_alpha_dividends.values().sum::(); assert_abs_diff_eq!( - total_alpha_dividends.saturating_to_num::(), + total_alpha_dividends.to_num::(), u64::from(pending_validator_alpha), epsilon = 1_000 ); assert_abs_diff_eq!( - total_root_alpha_dividends.saturating_to_num::(), + total_root_alpha_dividends.to_num::(), pending_root_alpha.to_u64(), epsilon = 1_000 ); @@ -1887,7 +2013,7 @@ fn test_calculate_dividend_distribution_total_only_tao() { let pending_validator_alpha = AlphaCurrency::ZERO; let pending_root_alpha = AlphaCurrency::from(837_120_949_872); - let tao_weight: U96F32 = U96F32::saturating_from_num(0.18); // 18% + let tao_weight: U96F32 = U96F32::from_num(0.18); // 18% let hotkeys = [U256::from(0), U256::from(1)]; @@ -1911,12 +2037,12 @@ fn test_calculate_dividend_distribution_total_only_tao() { let total_root_alpha_dividends = root_alpha_dividends.values().sum::(); assert_abs_diff_eq!( - total_alpha_dividends.saturating_to_num::(), + total_alpha_dividends.to_num::(), u64::from(pending_validator_alpha), epsilon = 1_000 ); assert_abs_diff_eq!( - total_root_alpha_dividends.saturating_to_num::(), + total_root_alpha_dividends.to_num::(), pending_root_alpha.to_u64(), epsilon = 1_000 ); @@ -1931,7 +2057,7 @@ fn test_calculate_dividend_distribution_total_no_tao_weight() { let pending_validator_alpha = AlphaCurrency::from(183_123_567_452); let pending_tao = TaoCurrency::ZERO; // If tao weight is 0, then only alpha dividends should be input. - let tao_weight: U96F32 = U96F32::saturating_from_num(0.0); // 0% + let tao_weight: U96F32 = U96F32::from_num(0.0); // 0% let hotkeys = [U256::from(0), U256::from(1)]; @@ -1955,12 +2081,12 @@ fn test_calculate_dividend_distribution_total_no_tao_weight() { let total_tao_dividends = tao_dividends.values().sum::(); assert_abs_diff_eq!( - total_alpha_dividends.saturating_to_num::(), + total_alpha_dividends.to_num::(), u64::from(pending_validator_alpha), epsilon = 1_000 ); assert_abs_diff_eq!( - total_tao_dividends.saturating_to_num::(), + total_tao_dividends.to_num::(), pending_tao.to_u64(), epsilon = 1_000 ); @@ -1975,7 +2101,7 @@ fn test_calculate_dividend_distribution_total_only_alpha() { let pending_validator_alpha = AlphaCurrency::from(183_123_567_452); let pending_tao = TaoCurrency::ZERO; - let tao_weight: U96F32 = U96F32::saturating_from_num(0.18); // 18% + let tao_weight: U96F32 = U96F32::from_num(0.18); // 18% let hotkeys = [U256::from(0), U256::from(1)]; @@ -1999,12 +2125,12 @@ fn test_calculate_dividend_distribution_total_only_alpha() { let total_tao_dividends = tao_dividends.values().sum::(); assert_abs_diff_eq!( - total_alpha_dividends.saturating_to_num::(), + total_alpha_dividends.to_num::(), u64::from(pending_validator_alpha), epsilon = 1_000 ); assert_abs_diff_eq!( - total_tao_dividends.saturating_to_num::(), + total_tao_dividends.to_num::(), pending_tao.to_u64(), epsilon = 1_000 ); @@ -2034,7 +2160,7 @@ fn test_calculate_dividend_and_incentive_distribution() { let pending_validator_alpha = pending_alpha / 2.into(); // Pay half to validators. let pending_tao = TaoCurrency::ZERO; let pending_swapped = 0; // Only alpha output. - let tao_weight: U96F32 = U96F32::saturating_from_num(0.0); // 0% + let tao_weight: U96F32 = U96F32::from_num(0.0); // 0% // Hotkey, Incentive, Dividend let hotkey_emission = vec![(hotkey, pending_alpha / 2.into(), pending_alpha / 2.into())]; @@ -2050,13 +2176,10 @@ fn test_calculate_dividend_and_incentive_distribution() { ); let incentives_total = incentives.values().copied().map(u64::from).sum::(); - let dividends_total = alpha_dividends - .values() - .sum::() - .saturating_to_num::(); + let dividends_total = alpha_dividends.values().sum::().to_num::(); assert_abs_diff_eq!( - dividends_total.saturating_add(incentives_total), + dividends_total + incentives_total, u64::from(pending_alpha), epsilon = 2 ); @@ -2085,7 +2208,7 @@ fn test_calculate_dividend_and_incentive_distribution_all_to_validators() { let pending_alpha = AlphaCurrency::from(123_456_789); let pending_validator_alpha = pending_alpha; // Pay all to validators. let pending_tao = TaoCurrency::ZERO; - let tao_weight: U96F32 = U96F32::saturating_from_num(0.0); // 0% + let tao_weight: U96F32 = U96F32::from_num(0.0); // 0% // Hotkey, Incentive, Dividend let hotkey_emission = vec![(hotkey, 0.into(), pending_alpha)]; @@ -2101,13 +2224,10 @@ fn test_calculate_dividend_and_incentive_distribution_all_to_validators() { ); let incentives_total = incentives.values().copied().map(u64::from).sum::(); - let dividends_total = alpha_dividends - .values() - .sum::() - .saturating_to_num::(); + let dividends_total = alpha_dividends.values().sum::().to_num::(); assert_eq!( - AlphaCurrency::from(dividends_total.saturating_add(incentives_total)), + AlphaCurrency::from(dividends_total + incentives_total), pending_alpha ); }); @@ -2134,7 +2254,7 @@ fn test_calculate_dividends_and_incentives() { let divdends = AlphaCurrency::from(123_456_789); let incentive = AlphaCurrency::from(683_051_923); - let total_emission = divdends.saturating_add(incentive); + let total_emission = divdends + incentive; // Hotkey, Incentive, Dividend let hotkey_emission = vec![(hotkey, incentive, divdends)]; @@ -2146,17 +2266,10 @@ fn test_calculate_dividends_and_incentives() { .values() .copied() .fold(AlphaCurrency::ZERO, |acc, x| acc + x); - let dividends_total = AlphaCurrency::from( - dividends - .values() - .sum::() - .saturating_to_num::(), - ); + let dividends_total = + AlphaCurrency::from(dividends.values().sum::().to_num::()); - assert_eq!( - dividends_total.saturating_add(incentives_total), - total_emission - ); + assert_eq!(dividends_total + incentives_total, total_emission); }); } @@ -2192,12 +2305,8 @@ fn test_calculate_dividends_and_incentives_only_validators() { .values() .copied() .fold(AlphaCurrency::ZERO, |acc, x| acc + x); - let dividends_total = AlphaCurrency::from( - dividends - .values() - .sum::() - .saturating_to_num::(), - ); + let dividends_total = + AlphaCurrency::from(dividends.values().sum::().to_num::()); assert_eq!(dividends_total, divdends); assert_eq!(incentives_total, AlphaCurrency::ZERO); @@ -2236,12 +2345,8 @@ fn test_calculate_dividends_and_incentives_only_miners() { .values() .copied() .fold(AlphaCurrency::ZERO, |acc, x| acc + x); - let dividends_total = AlphaCurrency::from( - dividends - .values() - .sum::() - .saturating_to_num::(), - ); + let dividends_total = + AlphaCurrency::from(dividends.values().sum::().to_num::()); assert_eq!(incentives_total, incentive); assert_eq!(dividends_total, divdends); @@ -2287,7 +2392,7 @@ fn test_drain_pending_emission_no_miners_all_drained() { // Slight epsilon due to rounding (hotkey_take). assert_abs_diff_eq!( new_stake, - u64::from(emission.saturating_add(init_stake.into())).into(), + u64::from(emission + init_stake.into()).into(), epsilon = 1.into() ); }); @@ -2442,7 +2547,7 @@ fn test_run_coinbase_not_started() { assert!(SubtensorModule::should_run_epoch(netuid, current_block)); // Run coinbase with emission. - SubtensorModule::run_coinbase(U96F32::saturating_from_num(100_000_000)); + SubtensorModule::run_coinbase(U96F32::from_num(100_000_000)); // We expect that the epoch ran. assert_eq!(BlocksSinceLastStep::::get(netuid), 0); @@ -2533,7 +2638,7 @@ fn test_run_coinbase_not_started_start_after() { assert!(SubtensorModule::should_run_epoch(netuid, current_block)); // Run coinbase with emission. - SubtensorModule::run_coinbase(U96F32::saturating_from_num(100_000_000)); + SubtensorModule::run_coinbase(U96F32::from_num(100_000_000)); // We expect that the epoch ran. assert_eq!(BlocksSinceLastStep::::get(netuid), 0); @@ -2553,7 +2658,7 @@ fn test_run_coinbase_not_started_start_after() { ); // Run coinbase with emission. - SubtensorModule::run_coinbase(U96F32::saturating_from_num(100_000_000)); + SubtensorModule::run_coinbase(U96F32::from_num(100_000_000)); // We expect that the epoch ran. assert_eq!(BlocksSinceLastStep::::get(netuid), 0); @@ -2781,3 +2886,31 @@ fn test_incentive_goes_to_hotkey_when_no_autostake_destination() { ); }); } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_zero_shares_zero_emission --exact --show-output --nocapture +#[test] +fn test_zero_shares_zero_emission() { + new_test_ext(1).execute_with(|| { + let subnet_owner_ck = U256::from(0); + let subnet_owner_hk = U256::from(1); + let netuid1 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + let netuid2 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + let emission: u64 = 1_000_000; + // Setup prices 1 and 1 + let initial: u64 = 1_000_000; + SubnetTAO::::insert(netuid1, TaoCurrency::from(initial)); + SubnetAlphaIn::::insert(netuid1, AlphaCurrency::from(initial)); + SubnetTAO::::insert(netuid2, TaoCurrency::from(initial)); + SubnetAlphaIn::::insert(netuid2, AlphaCurrency::from(initial)); + // Set subnet prices so that both are + // - cut off by lower limit for tao flow method + // - zeroed out for price ema method + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0)); + SubnetMovingPrice::::insert(netuid2, I96F32::from_num(0)); + // Run coinbase + SubtensorModule::run_coinbase(U96F32::from_num(emission)); + // Netuid 1 is cut off by lower limit, all emission goes to netuid2 + assert_eq!(SubnetAlphaIn::::get(netuid1), initial.into()); + assert_eq!(SubnetAlphaIn::::get(netuid2), initial.into()); + }); +} diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index ebe7defcda..bbaf25af58 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -25,6 +25,7 @@ mod serving; mod staking; mod staking2; mod subnet; +mod subnet_emissions; mod swap_coldkey; mod swap_hotkey; mod swap_hotkey_with_subnet; diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 31826b6810..27c5b5c16d 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -5576,3 +5576,74 @@ fn test_remove_root_updates_counters() { ); }); } + +// cargo test --package pallet-subtensor --lib -- tests::staking::test_staking_records_flow --exact --show-output +#[test] +fn test_staking_records_flow() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let hotkey = U256::from(3); + let coldkey = U256::from(4); + let amount = 100_000_000; + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Forse-set alpha in and tao reserve to make price equal 0.01 + let tao_reserve = TaoCurrency::from(100_000_000_000); + let alpha_in = AlphaCurrency::from(1_000_000_000_000); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + + // Initialize swap v3 + let order = GetAlphaForTao::::with_amount(0); + assert_ok!(::SwapInterface::swap( + netuid.into(), + order, + TaoCurrency::MAX, + false, + true + )); + + // Add stake with slippage safety and check if the result is ok + assert_ok!(SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey, + netuid, + amount.into(), + TaoCurrency::MAX, + false, + false, + )); + let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 + / u16::MAX as f64; + let expected_flow = (amount as f64) * (1. - fee_rate); + + // Check that flow has been recorded (less unstaking fees) + assert_abs_diff_eq!( + SubnetTaoFlow::::get(netuid), + expected_flow as i64, + epsilon = 1_i64 + ); + + // Remove stake + let alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_ok!(SubtensorModule::unstake_from_subnet( + &hotkey, + &coldkey, + netuid, + alpha, + TaoCurrency::ZERO, + false, + )); + + // Check that outflow has been recorded (less unstaking fees) + let expected_unstake_fee = expected_flow * fee_rate; + assert_abs_diff_eq!( + SubnetTaoFlow::::get(netuid), + expected_unstake_fee as i64, + epsilon = (expected_unstake_fee / 100.0) as i64 + ); + }); +} diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs new file mode 100644 index 0000000000..aeece09c2e --- /dev/null +++ b/pallets/subtensor/src/tests/subnet_emissions.rs @@ -0,0 +1,590 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] +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; + +fn u64f64(x: f64) -> U64F64 { + U64F64::from_num(x) +} + +fn i64f64(x: f64) -> I64F64 { + I64F64::from_num(x) +} + +fn i96f32(x: f64) -> I96F32 { + I96F32::from_num(x) +} + +#[test] +fn inplace_pow_normalize_all_zero_inputs_no_panic_and_unchanged() { + let mut m: BTreeMap = BTreeMap::new(); + m.insert(NetUid::from(1), u64f64(0.0)); + m.insert(NetUid::from(2), u64f64(0.0)); + m.insert(NetUid::from(3), u64f64(0.0)); + + let before = m.clone(); + // p = 1.0 (doesn't matter here) + SubtensorModule::inplace_pow_normalize(&mut m, u64f64(1.0)); + + // Expect unchanged (sum becomes 0 → safe_div handles, or branch skips) + for (k, v_before) in before { + let v_after = m.get(&k).copied().unwrap(); + assert_abs_diff_eq!( + v_after.to_num::(), + v_before.to_num::(), + epsilon = 1e-18 + ); + } +} + +#[test] +fn inplace_pow_normalize_tiny_values_no_panic() { + use alloc::collections::BTreeMap; + + // Very small inputs so that scaling branch is skipped in inplace_pow_normalize + let mut m: BTreeMap = BTreeMap::new(); + m.insert(NetUid::from(10), u64f64(1e-9)); + m.insert(NetUid::from(11), u64f64(2e-9)); + m.insert(NetUid::from(12), u64f64(3e-9)); + + let before = m.clone(); + SubtensorModule::inplace_pow_normalize(&mut m, u64f64(2.0)); // p = 2 + + let sum = (1 + 4 + 9) as f64; + for (k, v_before) in before { + let v_after = m.get(&k).copied().unwrap(); + let mut expected = v_before.to_num::(); + expected *= 1e18 * expected / sum; + assert_abs_diff_eq!( + v_after.to_num::(), + expected, + epsilon = expected / 100.0 + ); + } +} + +#[test] +fn inplace_pow_normalize_large_values_no_overflow_and_sum_to_one() { + use alloc::collections::BTreeMap; + + let mut m: BTreeMap = BTreeMap::new(); + m.insert(NetUid::from(1), u64f64(1e9)); + m.insert(NetUid::from(2), u64f64(5e9)); + m.insert(NetUid::from(3), u64f64(1e10)); + + SubtensorModule::inplace_pow_normalize(&mut m, u64f64(2.0)); // p = 2 + + // Sum ≈ 1 + let sum: f64 = m.values().map(|v| v.to_num::()).sum(); + assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-9); + + // Each value is finite and within [0, 1] + for (k, v) in &m { + let f = v.to_num::(); + assert!(f.is_finite(), "value for {k:?} not finite"); + assert!( + (0.0..=1.0).contains(&f), + "value for {k:?} out of [0,1]: {f}" + ); + } +} + +#[test] +fn inplace_pow_normalize_regular_case_relative_proportions_preserved() { + use alloc::collections::BTreeMap; + + // With p = 1, normalization should yield roughly same proportions + let mut m: BTreeMap = BTreeMap::new(); + m.insert(NetUid::from(7), u64f64(2.0)); + m.insert(NetUid::from(8), u64f64(3.0)); + m.insert(NetUid::from(9), u64f64(5.0)); + + SubtensorModule::inplace_pow_normalize(&mut m, u64f64(1.0)); // p = 1 + + let a = m.get(&NetUid::from(7)).copied().unwrap().to_num::(); + let b = m.get(&NetUid::from(8)).copied().unwrap().to_num::(); + let c = m.get(&NetUid::from(9)).copied().unwrap().to_num::(); + + assert_abs_diff_eq!(a, 0.2_f64, epsilon = 0.001); + assert_abs_diff_eq!(b, 0.3_f64, epsilon = 0.001); + assert_abs_diff_eq!(c, 0.5_f64, epsilon = 0.001); + + // The sum of shares is 1.0 with good precision + let sum = a + b + c; + assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-12); +} + +#[test] +fn inplace_pow_normalize_fractional_exponent() { + use alloc::collections::BTreeMap; + + [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0] + .into_iter() + .for_each(|p| { + let mut m: BTreeMap = BTreeMap::new(); + m.insert(NetUid::from(7), u64f64(2.0)); + m.insert(NetUid::from(8), u64f64(3.0)); + m.insert(NetUid::from(9), u64f64(5.0)); + + SubtensorModule::inplace_pow_normalize(&mut m, u64f64(p)); + + let a = m.get(&NetUid::from(7)).copied().unwrap().to_num::(); + let b = m.get(&NetUid::from(8)).copied().unwrap().to_num::(); + let c = m.get(&NetUid::from(9)).copied().unwrap().to_num::(); + + let sum = (2.0_f64).powf(p) + (3.0_f64).powf(p) + (5.0_f64).powf(p); + let expected_a = (2.0_f64).powf(p) / sum; + let expected_b = (3.0_f64).powf(p) / sum; + let expected_c = (5.0_f64).powf(p) / sum; + + assert_abs_diff_eq!(a, expected_a, epsilon = expected_a / 100.0); + assert_abs_diff_eq!(b, expected_b, epsilon = expected_b / 100.0); + assert_abs_diff_eq!(c, expected_c, epsilon = expected_c / 100.0); + + // The sum of shares is 1.0 with good precision + let sum = a + b + c; + assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-12); + }) +} + +/// 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); + + // 100% tao flow method + let block_num = FlowHalfLife::::get(); + FlowFirstBlock::::set(Some(0_u64)); + 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); + + // 100% tao flow method + let block_num = FlowHalfLife::::get(); + FlowFirstBlock::::set(Some(0_u64)); + 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); + + // 100% tao flow method + let block_num = FlowHalfLife::::get(); + FlowFirstBlock::::set(Some(0_u64)); + 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}" + ); + }); +} + +/// 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) { + let now = frame_system::Pallet::::block_number(); + SubnetMovingPrice::::insert(n1, i96f32(price1)); + SubnetMovingPrice::::insert(n2, i96f32(price2)); + SubnetEmaTaoFlow::::insert(n1, (now, i64f64(flow1))); + SubnetEmaTaoFlow::::insert(n2, (now, i64f64(flow2))); +} + +#[test] +fn get_shares_price_flow_blend_1v3_price_and_3v1_flow() { + new_test_ext(1).execute_with(|| { + // two subnets + let owner_hotkey = U256::from(42); + let owner_coldkey = U256::from(43); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // define "window" length half_life blocks and set first block to 0 + let half_life: u64 = FlowHalfLife::::get(); + FlowFirstBlock::::set(Some(0_u64)); + FlowNormExponent::::set(u64f64(1.0)); + + // t = 0: expect (0.25, 0.75) + frame_system::Pallet::::set_block_number(0); + seed_price_and_flow(n1, n2, /*price*/ 1.0, 3.0, /*flow*/ 3.0, 1.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_abs_diff_eq!(s1 + s2, 1.0_f64, epsilon = 1e-9); + assert_abs_diff_eq!(s1, 0.25_f64, epsilon = 1e-6); + assert_abs_diff_eq!(s2, 0.75_f64, epsilon = 1e-6); + + // t = half_life/2: expect (0.5, 0.5) + frame_system::Pallet::::set_block_number(half_life / 2); + seed_price_and_flow(n1, n2, 1.0, 3.0, 3.0, 1.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_abs_diff_eq!(s1 + s2, 1.0_f64, epsilon = 1e-9); + assert_abs_diff_eq!(s1, 0.5_f64, epsilon = 1e-6); + assert_abs_diff_eq!(s2, 0.5_f64, epsilon = 1e-6); + + // t = half_life: expect (0.75, 0.25) + frame_system::Pallet::::set_block_number(half_life); + seed_price_and_flow(n1, n2, 1.0, 3.0, 3.0, 1.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_abs_diff_eq!(s1 + s2, 1.0_f64, epsilon = 1e-9); + assert_abs_diff_eq!(s1, 0.75_f64, epsilon = 1e-6); + assert_abs_diff_eq!(s2, 0.25_f64, epsilon = 1e-6); + }); +} + +#[test] +fn get_shares_price_flow_blend_3v1_price_and_1v3_flow() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(50); + let owner_coldkey = U256::from(51); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // window half_life and anchor at 0 + let half_life: u64 = FlowHalfLife::::get(); + FlowFirstBlock::::set(Some(0_u64)); + FlowNormExponent::::set(u64f64(1.0)); + + // t = 0: prices dominate → (0.75, 0.25) + frame_system::Pallet::::set_block_number(0); + seed_price_and_flow(n1, n2, /*price*/ 3.0, 1.0, /*flow*/ 1.0, 3.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_abs_diff_eq!(s1 + s2, 1.0_f64, epsilon = 1e-9); + assert_abs_diff_eq!(s1, 0.75_f64, epsilon = 1e-6); + assert_abs_diff_eq!(s2, 0.25_f64, epsilon = 1e-6); + + // t = half_life/2: equal → (0.5, 0.5) + frame_system::Pallet::::set_block_number(half_life / 2); + seed_price_and_flow(n1, n2, 3.0, 1.0, 1.0, 3.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_abs_diff_eq!(s1, 0.5_f64, epsilon = 1e-6); + assert_abs_diff_eq!(s2, 0.5_f64, epsilon = 1e-6); + + // t = half_life: flows dominate → (0.25, 0.75) + frame_system::Pallet::::set_block_number(half_life); + seed_price_and_flow(n1, n2, 3.0, 1.0, 1.0, 3.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_abs_diff_eq!(s1, 0.25_f64, epsilon = 1e-6); + assert_abs_diff_eq!(s2, 0.75_f64, epsilon = 1e-6); + }); +} + +/// 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(); + FlowFirstBlock::::set(Some(0_u64)); + 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(); + FlowFirstBlock::::set(Some(0_u64)); + 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(); + FlowFirstBlock::::set(Some(0_u64)); + 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(); + FlowFirstBlock::::set(Some(0_u64)); + 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(); + FlowFirstBlock::::set(Some(0_u64)); + 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); + }); +} diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index ee16b9aa01..0ba3df1103 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -7,7 +7,7 @@ use safe_math::*; use sp_core::Get; use sp_core::U256; use sp_runtime::Saturating; -use substrate_fixed::types::{I32F32, U96F32}; +use substrate_fixed::types::{I32F32, I64F64, U64F64, U96F32}; use subtensor_runtime_common::{AlphaCurrency, NetUid, NetUidStorageIndex, TaoCurrency}; impl Pallet { @@ -951,4 +951,19 @@ impl Pallet { SubnetLimit::::put(limit); Self::deposit_event(Event::SubnetLimitSet(limit)); } + + /// Sets TAO flow cutoff value (A) + pub fn set_tao_flow_cutoff(flow_cutoff: I64F64) { + TaoFlowCutoff::::set(flow_cutoff); + } + + /// Sets TAO flow normalization exponent (p) + pub fn set_tao_flow_normalization_exponent(exponent: U64F64) { + FlowNormExponent::::set(exponent); + } + + /// Sets TAO flow smoothing factor (alpha) + pub fn set_tao_flow_smoothing_factor(smoothing_factor: u64) { + FlowEmaSmoothingFactor::::set(smoothing_factor); + } }