diff --git a/Cargo.lock b/Cargo.lock index 7bdb24a9d3..7a13718145 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16349,9 +16349,18 @@ dependencies = [ name = "share-pool" version = "0.1.0" dependencies = [ + "approx", + "log", + "num-traits", + "parity-scale-codec", + "rand 0.8.5", + "rayon", "safe-math", + "scale-info", + "sp-core", "sp-std", "substrate-fixed", + "subtensor-macros", ] [[package]] @@ -18259,6 +18268,7 @@ dependencies = [ name = "subtensor-transaction-fee" version = "0.1.0" dependencies = [ + "approx", "frame-executive", "frame-support", "frame-system", diff --git a/Cargo.toml b/Cargo.toml index 0d95b9a054..2a76ef639d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,7 @@ hex = { version = "0.4", default-features = false } hex-literal = "0.4.1" jsonrpsee = { version = "0.24.9", default-features = false } libsecp256k1 = { version = "0.7.2", default-features = false } +lencode = "0.1.6" log = { version = "0.4.21", default-features = false } memmap2 = "0.9.8" ndarray = { version = "0.16.1", default-features = false } @@ -315,3 +316,4 @@ pow-faucet = [] [patch.crates-io] w3f-bls = { git = "https://github.com/opentensor/bls", branch = "fix-no-std" } + diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 46ea71fc39..5c9729250f 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -435,8 +435,8 @@ impl pallet_subtensor_swap::Config for Test { type SubnetInfo = SubtensorModule; type BalanceOps = SubtensorModule; type ProtocolId = SwapProtocolId; - type TaoReserve = TaoCurrencyReserve; - type AlphaReserve = AlphaCurrencyReserve; + type TaoReserve = TaoBalanceReserve; + type AlphaReserve = AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; diff --git a/common/src/lib.rs b/common/src/lib.rs index a143c27824..70fa42c32b 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -259,7 +259,7 @@ pub trait BalanceOps { hotkey: &AccountId, netuid: NetUid, alpha: AlphaBalance, - ) -> Result; + ) -> Result<(), DispatchError>; } /// Allows to query the current block author diff --git a/docs/rust-setup.md b/docs/rust-setup.md index a3e4a952d7..a8f5cf46fb 100644 --- a/docs/rust-setup.md +++ b/docs/rust-setup.md @@ -67,7 +67,21 @@ Open the Terminal application and execute the following commands: # Make sure Homebrew is up-to-date, install protobuf and openssl brew update -brew install protobuf openssl +brew install protobuf openssl llvm@16 +``` + +Also, add the following lines at the end of your ~/.zshrc: + +``` +# LLVM 16 from Homebrew +export PATH="/opt/homebrew/opt/llvm@16/bin:$PATH" + +export CC="/opt/homebrew/opt/llvm@16/bin/clang" +export CXX="/opt/homebrew/opt/llvm@16/bin/clang++" +export LIBCLANG_PATH="/opt/homebrew/opt/llvm@16/lib/libclang.dylib" + +export LDFLAGS="-L/opt/homebrew/opt/llvm@16/lib" +export CPPFLAGS="-I/opt/homebrew/opt/llvm@16/include" ``` ### Windows diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 7f882fcf9c..a0c094e564 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -1149,7 +1149,7 @@ pub mod pallet { /// The extrinsic will call the Subtensor pallet to set the minimum stake required for nominators. #[pallet::call_index(43)] #[pallet::weight(Weight::from_parts(28_050_000, 6792) - .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)))] pub fn sudo_set_nominator_min_required_stake( origin: OriginFor, @@ -1595,7 +1595,7 @@ pub mod pallet { /// # Weight /// Weight is handled by the `#[pallet::weight]` attribute. #[pallet::call_index(65)] - #[pallet::weight(Weight::from_parts(6_201_000, 0) + #[pallet::weight(Weight::from_parts(3_415_000, 0) .saturating_add(T::DbWeight::get().reads(0_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)))] pub fn sudo_set_ema_price_halving_period( diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 35f8f6f784..f744a4ea39 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -340,8 +340,8 @@ impl pallet_subtensor_swap::Config for Test { type SubnetInfo = SubtensorModule; type BalanceOps = SubtensorModule; type ProtocolId = SwapProtocolId; - type TaoReserve = pallet_subtensor::TaoCurrencyReserve; - type AlphaReserve = pallet_subtensor::AlphaCurrencyReserve; + type TaoReserve = pallet_subtensor::TaoBalanceReserve; + type AlphaReserve = pallet_subtensor::AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; diff --git a/pallets/subtensor/src/coinbase/block_step.rs b/pallets/subtensor/src/coinbase/block_step.rs index a030607995..a7d41b7f3a 100644 --- a/pallets/subtensor/src/coinbase/block_step.rs +++ b/pallets/subtensor/src/coinbase/block_step.rs @@ -33,6 +33,7 @@ impl Pallet { Self::run_auto_claim_root_divs(last_block_hash); // --- 9. Populate root coldkey maps. Self::populate_root_coldkey_staking_maps(); + Self::populate_root_coldkey_staking_maps_v2(); // Return ok. Ok(()) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index d25f7ce170..460a754d45 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -515,7 +515,7 @@ impl Pallet { log::debug!( "owner_hotkey: {owner_hotkey:?} owner_coldkey: {owner_coldkey:?}, owner_cut: {owner_cut:?}" ); - let real_owner_cut = Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( &owner_hotkey, &owner_coldkey, netuid, @@ -523,7 +523,7 @@ impl Pallet { ); // If the subnet is leased, notify the lease logic that owner cut has been distributed. if let Some(lease_id) = SubnetUidToLeaseId::::get(netuid) { - Self::distribute_leased_network_dividends(lease_id, real_owner_cut); + Self::distribute_leased_network_dividends(lease_id, owner_cut); } } @@ -617,7 +617,7 @@ impl Pallet { root_alpha = root_alpha.saturating_sub(alpha_take); // Give the validator their take. log::debug!("hotkey: {hotkey:?} alpha_take: {alpha_take:?}"); - let _validator_stake = Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &Owner::::get(hotkey.clone()), netuid, diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index f27019989a..8dc031fbc4 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -92,6 +92,7 @@ pub mod pallet { use frame_system::pallet_prelude::*; use pallet_drand::types::RoundNumber; use runtime_common::prod_or_fast; + use share_pool::SafeFloat; use sp_core::{ConstU32, H160, H256}; use sp_runtime::traits::{Dispatchable, TrailingZeroInput}; use sp_std::collections::btree_map::BTreeMap; @@ -1433,11 +1434,41 @@ pub mod pallet { ValueQuery, >; + /// DMAP ( hot, netuid ) --> total_alpha_shares | Returns the number of alpha shares for a hotkey on a subnet, stores SafeFloat. + #[pallet::storage] + pub type TotalHotkeySharesV2 = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, // hot + Identity, + NetUid, // subnet + SafeFloat, // Hotkey shares in unlimited precision + ValueQuery, + >; + + /// --- NMAP ( hot, cold, netuid ) --> alpha | Returns the alpha shares for a hotkey, coldkey, netuid triplet, stores SafeFloat. + #[pallet::storage] + pub type AlphaV2 = StorageNMap< + _, + ( + NMapKey, // hot + NMapKey, // cold + NMapKey, // subnet + ), + SafeFloat, // Shares in unlimited precision + ValueQuery, + >; + /// Contains last Alpha storage map key to iterate (check first) #[pallet::storage] pub type AlphaMapLastKey = StorageValue<_, Option>, ValueQuery, DefaultAlphaIterationLastKey>; + /// Contains last AlphaV2 storage map key to iterate (check first) + #[pallet::storage] + pub type AlphaV2MapLastKey = + StorageValue<_, Option>, ValueQuery, DefaultAlphaIterationLastKey>; + /// --- MAP ( netuid ) --> token_symbol | Returns the token symbol for a subnet. #[pallet::storage] pub type TokenSymbol = @@ -2493,9 +2524,9 @@ use sp_std::vec::Vec; use subtensor_macros::freeze_struct; #[derive(Clone)] -pub struct TaoCurrencyReserve(PhantomData); +pub struct TaoBalanceReserve(PhantomData); -impl TokenReserve for TaoCurrencyReserve { +impl TokenReserve for TaoBalanceReserve { #![deny(clippy::expect_used)] fn reserve(netuid: NetUid) -> TaoBalance { SubnetTAO::::get(netuid).saturating_add(SubnetTaoProvided::::get(netuid)) @@ -2511,9 +2542,9 @@ impl TokenReserve for TaoCurrencyReserve { } #[derive(Clone)] -pub struct AlphaCurrencyReserve(PhantomData); +pub struct AlphaBalanceReserve(PhantomData); -impl TokenReserve for AlphaCurrencyReserve { +impl TokenReserve for AlphaBalanceReserve { #![deny(clippy::expect_used)] fn reserve(netuid: NetUid) -> AlphaBalance { SubnetAlphaIn::::get(netuid).saturating_add(SubnetAlphaInProvided::::get(netuid)) @@ -2529,9 +2560,9 @@ impl TokenReserve for AlphaCurrencyReserve { } pub type GetAlphaForTao = - subtensor_swap_interface::GetAlphaForTao, AlphaCurrencyReserve>; + subtensor_swap_interface::GetAlphaForTao, AlphaBalanceReserve>; pub type GetTaoForAlpha = - subtensor_swap_interface::GetTaoForAlpha, TaoCurrencyReserve>; + subtensor_swap_interface::GetTaoForAlpha, TaoBalanceReserve>; impl> subtensor_runtime_common::SubnetInfo for Pallet @@ -2619,20 +2650,25 @@ impl> hotkey: &T::AccountId, netuid: NetUid, alpha: AlphaBalance, - ) -> Result { + ) -> Result<(), DispatchError> { ensure!( Self::hotkey_account_exists(hotkey), Error::::HotKeyAccountNotExists ); + ensure!( + Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid) >= alpha, + Error::::InsufficientBalance + ); + // Decrese alpha out counter SubnetAlphaOut::::mutate(netuid, |total| { *total = total.saturating_sub(alpha); }); - Ok(Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( - hotkey, coldkey, netuid, alpha, - )) + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, alpha); + + Ok(()) } } diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 19a8f635dc..f84c8b0121 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -711,7 +711,7 @@ mod dispatches { /// #[pallet::call_index(2)] #[pallet::weight((Weight::from_parts(340_800_000, 0) - .saturating_add(T::DbWeight::get().reads(25_u64)) + .saturating_add(T::DbWeight::get().reads(27_u64)) .saturating_add(T::DbWeight::get().writes(15_u64)), DispatchClass::Normal, Pays::Yes))] pub fn add_stake( origin: OriginFor, @@ -1064,8 +1064,8 @@ mod dispatches { )] #[pallet::call_index(70)] #[pallet::weight((Weight::from_parts(275_300_000, 0) - .saturating_add(T::DbWeight::get().reads(52_u64)) - .saturating_add(T::DbWeight::get().writes(35_u64)), DispatchClass::Normal, Pays::No))] + .saturating_add(T::DbWeight::get().reads(57_u64)) + .saturating_add(T::DbWeight::get().writes(39_u64)), DispatchClass::Normal, Pays::No))] pub fn swap_hotkey( origin: OriginFor, hotkey: T::AccountId, @@ -1526,7 +1526,7 @@ mod dispatches { /// - Thrown if key has hit transaction rate limit #[pallet::call_index(84)] #[pallet::weight((Weight::from_parts(358_500_000, 0) - .saturating_add(T::DbWeight::get().reads(40_u64)) + .saturating_add(T::DbWeight::get().reads(44_u64)) .saturating_add(T::DbWeight::get().writes(24_u64)), DispatchClass::Normal, Pays::Yes))] pub fn unstake_all_alpha(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_unstake_all_alpha(origin, hotkey) @@ -1555,7 +1555,7 @@ mod dispatches { /// #[pallet::call_index(85)] #[pallet::weight((Weight::from_parts(164_300_000, 0) - .saturating_add(T::DbWeight::get().reads(15_u64)) + .saturating_add(T::DbWeight::get().reads(19_u64)) .saturating_add(T::DbWeight::get().writes(7_u64)), DispatchClass::Normal, Pays::Yes))] pub fn move_stake( origin: T::RuntimeOrigin, @@ -1598,7 +1598,7 @@ mod dispatches { /// May emit a `StakeTransferred` event on success. #[pallet::call_index(86)] #[pallet::weight((Weight::from_parts(160_300_000, 0) - .saturating_add(T::DbWeight::get().reads(13_u64)) + .saturating_add(T::DbWeight::get().reads(16_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)), DispatchClass::Normal, Pays::Yes))] pub fn transfer_stake( origin: T::RuntimeOrigin, @@ -1640,7 +1640,7 @@ mod dispatches { #[pallet::call_index(87)] #[pallet::weight(( Weight::from_parts(351_300_000, 0) - .saturating_add(T::DbWeight::get().reads(36_u64)) + .saturating_add(T::DbWeight::get().reads(40_u64)) .saturating_add(T::DbWeight::get().writes(22_u64)), DispatchClass::Normal, Pays::Yes @@ -1705,7 +1705,7 @@ mod dispatches { /// #[pallet::call_index(88)] #[pallet::weight((Weight::from_parts(402_900_000, 0) - .saturating_add(T::DbWeight::get().reads(25_u64)) + .saturating_add(T::DbWeight::get().reads(27_u64)) .saturating_add(T::DbWeight::get().writes(15_u64)), DispatchClass::Normal, Pays::Yes))] pub fn add_stake_limit( origin: OriginFor, @@ -1770,7 +1770,7 @@ mod dispatches { /// #[pallet::call_index(89)] #[pallet::weight((Weight::from_parts(377_400_000, 0) - .saturating_add(T::DbWeight::get().reads(28_u64)) + .saturating_add(T::DbWeight::get().reads(30_u64)) .saturating_add(T::DbWeight::get().writes(13_u64)), DispatchClass::Normal, Pays::Yes))] pub fn remove_stake_limit( origin: OriginFor, @@ -1814,7 +1814,7 @@ mod dispatches { #[pallet::call_index(90)] #[pallet::weight(( Weight::from_parts(411_500_000, 0) - .saturating_add(T::DbWeight::get().reads(36_u64)) + .saturating_add(T::DbWeight::get().reads(40_u64)) .saturating_add(T::DbWeight::get().writes(22_u64)), DispatchClass::Normal, Pays::Yes @@ -1936,7 +1936,7 @@ mod dispatches { /// Emits a `TokensRecycled` event on success. #[pallet::call_index(101)] #[pallet::weight(( - Weight::from_parts(113_400_000, 0).saturating_add(T::DbWeight::get().reads_writes(7, 4)), + Weight::from_parts(113_400_000, 0).saturating_add(T::DbWeight::get().reads_writes(9, 4)), DispatchClass::Normal, Pays::Yes ))] @@ -1961,7 +1961,7 @@ mod dispatches { /// Emits a `TokensBurned` event on success. #[pallet::call_index(102)] #[pallet::weight(( - Weight::from_parts(112_200_000, 0).saturating_add(T::DbWeight::get().reads_writes(7, 3)), + Weight::from_parts(112_200_000, 0).saturating_add(T::DbWeight::get().reads_writes(9, 3)), DispatchClass::Normal, Pays::Yes ))] @@ -1992,7 +1992,7 @@ mod dispatches { /// Without limit_price it remove all the stake similar to `remove_stake` extrinsic #[pallet::call_index(103)] #[pallet::weight((Weight::from_parts(395_300_000, 10142) - .saturating_add(T::DbWeight::get().reads(28_u64)) + .saturating_add(T::DbWeight::get().reads(30_u64)) .saturating_add(T::DbWeight::get().writes(13_u64)), DispatchClass::Normal, Pays::Yes))] pub fn remove_stake_full_limit( origin: T::RuntimeOrigin, @@ -2268,7 +2268,7 @@ mod dispatches { #[pallet::call_index(121)] #[pallet::weight(( Weight::from_parts(117_000_000, 7767) - .saturating_add(T::DbWeight::get().reads(12_u64)) + .saturating_add(T::DbWeight::get().reads(16_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)), DispatchClass::Normal, Pays::Yes @@ -2590,7 +2590,7 @@ mod dispatches { #[pallet::call_index(132)] #[pallet::weight(( Weight::from_parts(368_000_000, 8556) - .saturating_add(T::DbWeight::get().reads(28_u64)) + .saturating_add(T::DbWeight::get().reads(30_u64)) .saturating_add(T::DbWeight::get().writes(16_u64)), DispatchClass::Normal, Pays::Yes diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index e6c3aa5e78..78e8becb12 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -276,8 +276,6 @@ mod errors { VotingPowerTrackingNotEnabled, /// Invalid voting power EMA alpha value (must be <= 10^18). InvalidVotingPowerEmaAlpha, - /// Unintended precision loss when unstaking alpha - PrecisionLoss, /// Deprecated call. Deprecated, /// "Add stake and burn" exceeded the operation rate limit diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 4363eb3f35..ad62ec7c46 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -528,5 +528,18 @@ mod events { /// Alpha burned alpha: AlphaBalance, }, + + /// Transaction fee was paid in Alpha. + /// + /// Emitted in addition to `TransactionFeePaid` when the fee payment path is Alpha. + /// `alpha_fee` is the exact Alpha amount deducted. + TransactionFeePaidWithAlpha { + /// Account that paid the transaction fee. + who: T::AccountId, + /// Exact fee deducted in Alpha units. + alpha_fee: AlphaBalance, + /// Resulting swapped TAO amount + tao_amount: TaoBalance, + }, } } diff --git a/pallets/subtensor/src/macros/genesis.rs b/pallets/subtensor/src/macros/genesis.rs index d4e269391e..a074bdcef1 100644 --- a/pallets/subtensor/src/macros/genesis.rs +++ b/pallets/subtensor/src/macros/genesis.rs @@ -91,20 +91,20 @@ mod genesis { SubnetOwner::::insert(netuid, hotkey.clone()); SubnetLocked::::insert(netuid, TaoBalance::from(1)); LargestLocked::::insert(netuid, 1); - Alpha::::insert( + AlphaV2::::insert( // Lock the initial funds making this key the owner. (hotkey.clone(), hotkey.clone(), netuid), - U64F64::saturating_from_num(1_000_000_000), + SafeFloat::from(1_000_000_000), ); TotalHotkeyAlpha::::insert( hotkey.clone(), netuid, AlphaBalance::from(1_000_000_000), ); - TotalHotkeyShares::::insert( + TotalHotkeySharesV2::::insert( hotkey.clone(), netuid, - U64F64::saturating_from_num(1_000_000_000), + SafeFloat::from(1_000_000_000), ); SubnetAlphaOut::::insert(netuid, AlphaBalance::from(1_000_000_000)); let mut staking_hotkeys = StakingHotkeys::::get(hotkey.clone()); diff --git a/pallets/subtensor/src/rpc_info/delegate_info.rs b/pallets/subtensor/src/rpc_info/delegate_info.rs index 61c0286e7a..b69ea6f778 100644 --- a/pallets/subtensor/src/rpc_info/delegate_info.rs +++ b/pallets/subtensor/src/rpc_info/delegate_info.rs @@ -65,8 +65,8 @@ impl Pallet { alpha_share_pools.push(alpha_share_pool); } - for ((nominator, netuid), alpha_stake) in Alpha::::iter_prefix((delegate.clone(),)) { - if alpha_stake == 0 { + for (nominator, netuid, alpha_stake) in Self::alpha_iter_single_prefix(&delegate) { + if alpha_stake.is_zero() { continue; } @@ -166,7 +166,7 @@ impl Pallet { )> = Vec::new(); for delegate in as IterableStorageMap>::iter_keys() { // Staked to this delegate, so add to list - for (netuid, _) in Alpha::::iter_prefix((delegate.clone(), delegatee.clone())) { + for (netuid, _) in Self::alpha_iter_prefix((&delegate, &delegatee)) { let delegate_info = Self::get_delegate_by_existing_account(delegate.clone(), true); delegates.push(( delegate_info, diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index bfe34b2ab0..5c785f199b 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -1,3 +1,4 @@ +use alloc::collections::BTreeMap; use frame_support::traits::{ Imbalance, tokens::{ @@ -6,6 +7,7 @@ use frame_support::traits::{ }, }; use safe_math::*; +use share_pool::SafeFloat; use substrate_fixed::types::U96F32; use subtensor_runtime_common::{NetUid, TaoBalance}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -68,7 +70,7 @@ impl Pallet { hotkeys .iter() .map(|hotkey| { - Alpha::::iter_prefix((hotkey, coldkey)) + Self::alpha_iter_prefix((hotkey, coldkey)) .map(|(netuid, _)| { let alpha_stake = Self::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, netuid, @@ -101,7 +103,7 @@ impl Pallet { hotkeys .iter() .map(|hotkey| { - Alpha::::iter_prefix((hotkey, coldkey)) + Self::alpha_iter_prefix((hotkey, coldkey)) .map(|(netuid_on_storage, _)| { if netuid_on_storage == netuid { let alpha_stake = Self::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -261,7 +263,7 @@ impl Pallet { /// used with caution. pub fn clear_small_nominations() { // Loop through all staking accounts to identify and clear nominations below the minimum stake. - for ((hotkey, coldkey, netuid), _) in Alpha::::iter() { + for ((hotkey, coldkey, netuid), _) in Self::alpha_iter() { Self::clear_small_nomination_if_required(&hotkey, &coldkey, netuid); } } @@ -413,7 +415,149 @@ impl Pallet { } } + // Same thing as populate_root_coldkey_staking_maps, but for AlphaV2 + // TODO: Remove this function and AlphaV2MapLastKey when slow migration is finished + pub fn populate_root_coldkey_staking_maps_v2() { + // Get starting key for the batch. Get the first key if we restart the process. + let mut new_starting_raw_key = AlphaV2MapLastKey::::get(); + let mut starting_key = None; + if new_starting_raw_key.is_none() { + starting_key = AlphaV2::::iter_keys().next(); + new_starting_raw_key = starting_key.as_ref().map(AlphaV2::::hashed_key_for); + } + + if let Some(starting_raw_key) = new_starting_raw_key { + // Get the key batch + let mut keys = AlphaV2::::iter_keys_from(starting_raw_key) + .take(ALPHA_MAP_BATCH_SIZE) + .collect::>(); + + // New iteration: insert the starting key in the batch if it's a new iteration + // iter_keys_from() skips the starting key + if let Some(starting_key) = starting_key { + if keys.len() == ALPHA_MAP_BATCH_SIZE { + keys.remove(keys.len().saturating_sub(1)); + } + keys.insert(0, starting_key); + } + + let mut new_starting_key = None; + let new_iteration = keys.len() < ALPHA_MAP_BATCH_SIZE; + + // Check and remove alphas if necessary + for key in keys { + let (_, coldkey, netuid) = key.clone(); + + if netuid == NetUid::ROOT { + Self::maybe_add_coldkey_index(&coldkey); + } + + new_starting_key = Some(AlphaV2::::hashed_key_for(key)); + } + + // Restart the process if it's the last batch + if new_iteration { + new_starting_key = None; + } + + AlphaV2MapLastKey::::put(new_starting_key); + } + } + pub fn burn_subnet_alpha(_netuid: NetUid, _amount: AlphaBalance) { // Do nothing; TODO: record burned alpha in a tracker } + + /// Several alpha iteration helpers that merge key space from Alpha and AlphaV2 maps + pub fn alpha_iter() -> impl Iterator { + // Old Alpha shares format: U64F64 -> SafeFloat + let legacy = Alpha::::iter().map(|(key, val_u64f64)| { + let sf: SafeFloat = val_u64f64.into(); + (key, sf) + }); + + // New Alpha shares format + let v2 = AlphaV2::::iter(); + + // Merge and prefer v2 on duplicates + let merged: BTreeMap<_, SafeFloat> = + legacy + .chain(v2) + .fold(BTreeMap::new(), |mut acc, (key, val)| { + acc.entry(key) + .and_modify(|existing| { + *existing = val.clone(); + }) + .or_insert(val); + acc + }); + + merged.into_iter() + } + + pub fn alpha_iter_prefix( + prefix: (&T::AccountId, &T::AccountId), + ) -> impl Iterator + where + T::AccountId: Clone, + { + // Old Alpha shares format: U64F64 -> SafeFloat + let legacy = Alpha::::iter_prefix(prefix).map(|(netuid, val_u64f64)| { + let sf: SafeFloat = val_u64f64.into(); + (netuid, sf) + }); + + // New Alpha shares format + let v2 = AlphaV2::::iter_prefix(prefix); + + // Merge by netuid and sum SafeFloat values + let merged: BTreeMap = + legacy + .chain(v2) + .fold(BTreeMap::new(), |mut acc, (netuid, sf)| { + acc.entry(netuid) + .and_modify(|existing| { + *existing = sf.clone(); + }) + .or_insert(sf); + acc + }); + + merged + .into_iter() + .filter(|(_, alpha_share)| !alpha_share.is_zero()) + } + + pub fn alpha_iter_single_prefix( + prefix: &T::AccountId, + ) -> impl Iterator + where + T::AccountId: Clone, + { + // Old Alpha shares format: U64F64 -> SafeFloat + let legacy = + Alpha::::iter_prefix((prefix.clone(),)).map(|((coldkey, netuid), val_u64f64)| { + let sf: SafeFloat = val_u64f64.into(); + ((coldkey, netuid), sf) + }); + + // New Alpha shares format + let v2 = AlphaV2::::iter_prefix((prefix,)); + + let merged: BTreeMap<(T::AccountId, NetUid), SafeFloat> = + legacy + .chain(v2) + .fold(BTreeMap::new(), |mut acc, (key, sf)| { + acc.entry(key) + .and_modify(|existing| { + *existing = sf.clone(); + }) + .or_insert(sf); + acc + }); + + merged + .into_iter() + .map(|((coldkey, netuid), sf)| (coldkey, netuid, sf)) + } } diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index 08627274aa..cd7d92e67e 100644 --- a/pallets/subtensor/src/staking/recycle_alpha.rs +++ b/pallets/subtensor/src/staking/recycle_alpha.rs @@ -51,21 +51,12 @@ impl Pallet { ); // Deduct from the coldkey's stake. - let actual_alpha_decrease = Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, amount, - ); - - ensure!(actual_alpha_decrease <= amount, Error::::PrecisionLoss); + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid, amount); // Recycle means we should decrease the alpha issuance tracker. - Self::recycle_subnet_alpha(netuid, actual_alpha_decrease); + Self::recycle_subnet_alpha(netuid, amount); - Self::deposit_event(Event::AlphaRecycled( - coldkey, - hotkey, - actual_alpha_decrease, - netuid, - )); + Self::deposit_event(Event::AlphaRecycled(coldkey, hotkey, amount, netuid)); Ok(()) } @@ -118,21 +109,12 @@ impl Pallet { ); // Deduct from the coldkey's stake. - let actual_alpha_decrease = Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, amount, - ); - - ensure!(actual_alpha_decrease <= amount, Error::::PrecisionLoss); + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid, amount); - Self::burn_subnet_alpha(netuid, actual_alpha_decrease); + Self::burn_subnet_alpha(netuid, amount); // Deposit event - Self::deposit_event(Event::AlphaBurned( - coldkey, - hotkey, - actual_alpha_decrease, - netuid, - )); + Self::deposit_event(Event::AlphaBurned(coldkey, hotkey, amount, netuid)); Ok(()) } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 1a5238aeb3..75733aeb18 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -483,13 +483,13 @@ impl Pallet { let mut stakers: Vec<(T::AccountId, T::AccountId, u128)> = Vec::new(); let mut total_alpha_value_u128: u128 = 0; - let hotkeys_in_subnet: Vec = TotalHotkeyAlpha::::iter() - .filter(|(_, this_netuid, _)| *this_netuid == netuid) - .map(|(hot, _, _)| hot.clone()) + let hotkeys_in_subnet: Vec = TotalHotkeyAlpha::::iter_keys() + .filter(|(_, this_netuid)| *this_netuid == netuid) + .map(|(hot, _)| hot.clone()) .collect::>(); for hot in hotkeys_in_subnet.iter() { - for ((cold, this_netuid), share_u64f64) in Alpha::::iter_prefix((hot,)) { + for (cold, this_netuid, share_u64f64) in Self::alpha_iter_single_prefix(hot) { if this_netuid != netuid { continue; } @@ -501,7 +501,7 @@ impl Pallet { // Fallback: if pool uninitialized, treat raw Alpha share as value. let val_u64 = if actual_val_u64 == 0 { - share_u64f64.saturating_to_num::() + u64::from(share_u64f64) } else { actual_val_u64 }; @@ -572,12 +572,14 @@ impl Pallet { // 7) Destroy all α-in/α-out state for this subnet. // 7.a) Remove every (hot, cold, netuid) α entry. for (hot, cold) in keys_to_remove { - Alpha::::remove((hot, cold, netuid)); + Alpha::::remove((hot.clone(), cold.clone(), netuid)); + AlphaV2::::remove((hot, cold, netuid)); } // 7.b) Clear share‑pool totals for each hotkey on this subnet. for hot in hotkeys_in_subnet { TotalHotkeyAlpha::::remove(&hot, netuid); TotalHotkeyShares::::remove(&hot, netuid); + TotalHotkeySharesV2::::remove(&hot, netuid); } // 7.c) Remove α‑in/α‑out counters (fully destroyed). SubnetAlphaIn::::remove(netuid); diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index a4987bbd74..cb2d46cef5 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -1,8 +1,8 @@ use super::*; use safe_math::*; -use share_pool::{SharePool, SharePoolDataOperations}; +use share_pool::{SafeFloat, SharePool, SharePoolDataOperations}; use sp_std::ops::Neg; -use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32}; +use substrate_fixed::types::{I64F64, I96F32, U96F32}; use subtensor_runtime_common::{AlphaBalance, AuthorshipInfo, NetUid, TaoBalance, Token}; use subtensor_swap_interface::{Order, SwapHandler, SwapResult}; @@ -508,7 +508,7 @@ impl Pallet { coldkey: &T::AccountId, netuid: NetUid, amount: AlphaBalance, - ) -> AlphaBalance { + ) { if !amount.is_zero() { let mut staking_hotkeys = StakingHotkeys::::get(coldkey); if !staking_hotkeys.contains(hotkey) { @@ -520,11 +520,7 @@ impl Pallet { let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); // We expect to add a positive amount here. let amount = amount.to_u64() as i64; - let actual_alpha = alpha_share_pool.update_value_for_one(coldkey, amount); - - // We should return a positive amount, or 0 if the operation failed. - // e.g. the stake was removed due to precision issues. - actual_alpha.max(0).unsigned_abs().into() + alpha_share_pool.update_value_for_one(coldkey, amount); } pub fn try_increase_stake_for_hotkey_and_coldkey_on_subnet( @@ -552,22 +548,16 @@ impl Pallet { coldkey: &T::AccountId, netuid: NetUid, amount: AlphaBalance, - ) -> AlphaBalance { + ) { let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); let amount = amount.to_u64(); // We expect a negative value here - let mut actual_alpha = 0; if let Ok(value) = alpha_share_pool.try_get_value(coldkey) && value >= amount { - actual_alpha = alpha_share_pool.update_value_for_one(coldkey, (amount as i64).neg()); + alpha_share_pool.update_value_for_one(coldkey, (amount as i64).neg()); } - - // Get the negation of the removed alpha, and clamp at 0. - // This ensures we return a positive value, but only if - // `actual_alpha` was negative (i.e. a decrease in stake). - actual_alpha.neg().max(0).unsigned_abs().into() } /// Swaps TAO for the alpha token on the subnet. @@ -687,15 +677,13 @@ impl Pallet { drop_fees: bool, ) -> Result { // Decrease alpha on subnet - let actual_alpha_decrease = - Self::decrease_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, alpha); + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, alpha); // Swap the alpha for TAO. - let swap_result = - Self::swap_alpha_for_tao(netuid, actual_alpha_decrease, price_limit, drop_fees)?; + let swap_result = Self::swap_alpha_for_tao(netuid, alpha, price_limit, drop_fees)?; // Refund the unused alpha (in case if limit price is hit) - let refund = actual_alpha_decrease.saturating_sub( + let refund = alpha.saturating_sub( swap_result .amount_paid_in .saturating_add(swap_result.fee_paid) @@ -754,7 +742,7 @@ impl Pallet { coldkey.clone(), hotkey.clone(), swap_result.amount_paid_out.into(), - actual_alpha_decrease, + swap_result.amount_paid_in.into(), netuid, swap_result.fee_paid.to_u64(), )); @@ -764,7 +752,7 @@ impl Pallet { coldkey.clone(), hotkey.clone(), swap_result.amount_paid_out, - actual_alpha_decrease, + swap_result.amount_paid_in, netuid, swap_result.fee_paid ); @@ -802,17 +790,12 @@ impl Pallet { ); // Increase the alpha on the hotkey account. - if Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, netuid, swap_result.amount_paid_out.into(), - ) - .is_zero() - || swap_result.amount_paid_out.is_zero() - { - return Ok(AlphaBalance::ZERO); - } + ); // Step 4: Update the list of hotkeys staking for this coldkey let mut staking_hotkeys = StakingHotkeys::::get(coldkey); @@ -889,7 +872,7 @@ impl Pallet { alpha: AlphaBalance, ) -> Result { // Decrease alpha on origin keys - let actual_alpha_decrease = Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( origin_hotkey, origin_coldkey, netuid, @@ -904,17 +887,17 @@ impl Pallet { } // Increase alpha on destination keys - let actual_alpha_moved = Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( destination_hotkey, destination_coldkey, netuid, - actual_alpha_decrease, + alpha, ); if netuid == NetUid::ROOT { Self::add_stake_adjust_root_claimed_for_hotkey_and_coldkey( destination_hotkey, destination_coldkey, - actual_alpha_decrease.into(), + u64::from(alpha).into(), ); } @@ -923,7 +906,7 @@ impl Pallet { let current_price = ::SwapInterface::current_alpha_price(netuid.into()); let tao_equivalent: TaoBalance = current_price - .saturating_mul(U96F32::saturating_from_num(actual_alpha_moved)) + .saturating_mul(U96F32::saturating_from_num(alpha)) .saturating_to_num::() .into(); @@ -952,7 +935,7 @@ impl Pallet { origin_coldkey.clone(), origin_hotkey.clone(), tao_equivalent, - actual_alpha_decrease, + alpha, netuid, 0_u64, // 0 fee )); @@ -960,7 +943,7 @@ impl Pallet { destination_coldkey.clone(), destination_hotkey.clone(), tao_equivalent, - actual_alpha_moved, + alpha, netuid, 0_u64, // 0 fee )); @@ -1331,47 +1314,80 @@ type AlphaShareKey = ::AccountId; impl SharePoolDataOperations> for HotkeyAlphaSharePoolDataOperations { - fn get_shared_value(&self) -> U64F64 { - U64F64::saturating_from_num(crate::TotalHotkeyAlpha::::get(&self.hotkey, self.netuid)) + fn get_shared_value(&self) -> u64 { + u64::from(TotalHotkeyAlpha::::get(&self.hotkey, self.netuid)) } - fn get_share(&self, key: &AlphaShareKey) -> U64F64 { - crate::Alpha::::get((&(self.hotkey), key, self.netuid)) + fn get_share(&self, key: &AlphaShareKey) -> SafeFloat { + // Read the deprecated Alpha map first and, if value is not available, try new AlphaV2 + let maybe_share_v1 = Alpha::::try_get((&(self.hotkey), key, self.netuid)); + if let Ok(share_v1) = maybe_share_v1 { + return SafeFloat::from(share_v1); + } + + AlphaV2::::get((&(self.hotkey), key, self.netuid)) } - fn try_get_share(&self, key: &AlphaShareKey) -> Result { - crate::Alpha::::try_get((&(self.hotkey), key, self.netuid)) + fn try_get_share(&self, key: &AlphaShareKey) -> Result { + // Read the deprecated Alpha map first and, if value is not available, try new AlphaV2 + let maybe_share_v1 = Alpha::::try_get((&(self.hotkey), key, self.netuid)); + if let Ok(share_v1) = maybe_share_v1 { + return Ok(SafeFloat::from(share_v1)); + } + + let maybe_share = AlphaV2::::try_get((&(self.hotkey), key, self.netuid)); + if let Ok(share) = maybe_share { + Ok(share) + } else { + Err(()) + } } - fn get_denominator(&self) -> U64F64 { - crate::TotalHotkeyShares::::get(&(self.hotkey), self.netuid) + fn get_denominator(&self) -> SafeFloat { + // Read the deprecated TotalHotkeyShares map first and, if value is not available, try new TotalHotkeySharesV2 + let maybe_denomnator_v1 = TotalHotkeyShares::::try_get(&(self.hotkey), self.netuid); + if let Ok(denomnator_v1) = maybe_denomnator_v1 { + return SafeFloat::from(denomnator_v1); + } + + TotalHotkeySharesV2::::get(&(self.hotkey), self.netuid) } - fn set_shared_value(&mut self, value: U64F64) { + fn set_shared_value(&mut self, value: u64) { if value != 0 { - crate::TotalHotkeyAlpha::::insert( - &(self.hotkey), - self.netuid, - AlphaBalance::from(value.saturating_to_num::()), - ); + TotalHotkeyAlpha::::insert(&(self.hotkey), self.netuid, AlphaBalance::from(value)); } else { - crate::TotalHotkeyAlpha::::remove(&(self.hotkey), self.netuid); + TotalHotkeyAlpha::::remove(&(self.hotkey), self.netuid); } } - fn set_share(&mut self, key: &AlphaShareKey, share: U64F64) { - if share != 0 { - crate::Alpha::::insert((&self.hotkey, key, self.netuid), share); + fn set_share(&mut self, key: &AlphaShareKey, share: SafeFloat) { + // Lazy Alpha -> AlphaV2 migration happens right here + // Delete the Alpha entry, insert into AlphaV2 + let maybe_share_v1 = Alpha::::try_get((&(self.hotkey), key, self.netuid)); + if maybe_share_v1.is_ok() { + Alpha::::remove((&self.hotkey, key, self.netuid)); + } + + if !share.is_zero() { + AlphaV2::::insert((&self.hotkey, key, self.netuid), share); } else { - crate::Alpha::::remove((&self.hotkey, key, self.netuid)); + AlphaV2::::remove((&self.hotkey, key, self.netuid)); } } - fn set_denominator(&mut self, update: U64F64) { - if update != 0 { - crate::TotalHotkeyShares::::insert(&self.hotkey, self.netuid, update); + fn set_denominator(&mut self, update: SafeFloat) { + // Lazy TotalHotkeyShares -> TotalHotkeySharesV2 migration happens right here + // Delete the TotalHotkeyShares entry, insert into TotalHotkeySharesV2 + let maybe_denominator_v1 = TotalHotkeyShares::::try_get(&(self.hotkey), self.netuid); + if maybe_denominator_v1.is_ok() { + TotalHotkeyShares::::remove(&self.hotkey, self.netuid); + } + + if !update.is_zero() { + TotalHotkeySharesV2::::insert(&self.hotkey, self.netuid, update); } else { - crate::TotalHotkeyShares::::remove(&self.hotkey, self.netuid); + TotalHotkeySharesV2::::remove(&self.hotkey, self.netuid); } } } diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index 401b5989ec..27fef995b2 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -1,5 +1,5 @@ use super::*; -use substrate_fixed::types::U64F64; +use share_pool::SafeFloat; impl Pallet { /// Transfer all assets, stakes, subnet ownerships, and hotkey associations from `old_coldkey` to @@ -98,19 +98,40 @@ impl Pallet { new_coldkey: &T::AccountId, ) { for hotkey in StakingHotkeys::::get(old_coldkey) { - // Get the stake on the old (hot,coldkey) account. - let old_alpha: U64F64 = Alpha::::get((&hotkey, old_coldkey, netuid)); - // Get the stake on the new (hot,coldkey) account. - let new_alpha: U64F64 = Alpha::::get((&hotkey, new_coldkey, netuid)); - // Add the stake to new account. - Alpha::::insert( - (&hotkey, new_coldkey, netuid), - new_alpha.saturating_add(old_alpha), - ); - // Remove the value from the old account. + // Swap and lazy-migrate Alpha to AlphaV2 + // TotalHotkeyShares does not have to be migrated here, these migrations can be independent + + // Get the v1 alpha shares on the old (hot,coldkey) account. + let orig_alpha_v1: SafeFloat = + SafeFloat::from(Alpha::::get((&hotkey, old_coldkey, netuid))); + // Get the v1 alpha shares on the new (hot,coldkey) account. + let dest_alpha_v1: SafeFloat = + SafeFloat::from(Alpha::::get((&hotkey, new_coldkey, netuid))); + // Get the v2 alpha shares on the old (hot,coldkey) account. + let orig_alpha_v2: SafeFloat = AlphaV2::::get((&hotkey, old_coldkey, netuid)); + // Get the v2 alpha shares on the new (hot,coldkey) account. + let dest_alpha_v2: SafeFloat = AlphaV2::::get((&hotkey, new_coldkey, netuid)); + + // Calculate and save new alpha shares on the destination new_coldkey + let new_dest_alpha = orig_alpha_v1 + .add(&dest_alpha_v1) + .unwrap_or_default() + .add(&orig_alpha_v2) + .unwrap_or_default() + .add(&dest_alpha_v2) + .unwrap_or_default(); + if !new_dest_alpha.is_zero() { + AlphaV2::::insert((&hotkey, new_coldkey, netuid), new_dest_alpha.clone()); + } + + // Remove shares on the origin old_coldkey in both Alpha and AlphaV2 maps Alpha::::remove((&hotkey, old_coldkey, netuid)); + AlphaV2::::remove((&hotkey, old_coldkey, netuid)); + + // Remove shares on the destination new_coldkey in Alpha map + Alpha::::remove((&hotkey, new_coldkey, netuid)); - if new_alpha.saturating_add(old_alpha) > U64F64::from(0u64) { + if !new_dest_alpha.is_zero() { Self::transfer_root_claimed_for_new_keys( netuid, &hotkey, diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 488778e33e..8cb12a974a 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -1,5 +1,6 @@ use super::*; use frame_support::weights::Weight; +use share_pool::SafeFloat; use sp_core::Get; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{MechId, NetUid, Token}; @@ -184,8 +185,8 @@ impl Pallet { keep_stake: bool, ) -> DispatchResult { // 1. keep the old hotkey alpha values for the case where hotkey staked by multiple coldkeys. - let old_alpha_values: Vec<((T::AccountId, NetUid), U64F64)> = - Alpha::::iter_prefix((old_hotkey,)).collect(); + let old_alpha_values: Vec<(T::AccountId, NetUid, SafeFloat)> = + Self::alpha_iter_single_prefix(old_hotkey).collect(); weight.saturating_accrue(T::DbWeight::get().reads(old_alpha_values.len() as u64)); // 2. Swap owner. @@ -239,25 +240,26 @@ impl Pallet { weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); } - // 10. Alpha already update in perform_hotkey_swap_on_one_subnet + // 10. Alphas already update in perform_hotkey_swap_on_one_subnet // Update the StakingHotkeys for the case where hotkey staked by multiple coldkeys. if !keep_stake { - for ((coldkey, _netuid), _alpha) in old_alpha_values { + for (coldkey, _netuid, alpha_share) in old_alpha_values { // Swap StakingHotkeys. // StakingHotkeys( coldkey ) --> Vec -- the hotkeys that the coldkey stakes. - let mut staking_hotkeys = StakingHotkeys::::get(&coldkey); - weight.saturating_accrue(T::DbWeight::get().reads(1)); - if staking_hotkeys.contains(old_hotkey) { - staking_hotkeys.retain(|hk| *hk != *old_hotkey && *hk != *new_hotkey); - if !staking_hotkeys.contains(new_hotkey) { - staking_hotkeys.push(new_hotkey.clone()); + if !alpha_share.is_zero() { + let mut staking_hotkeys = StakingHotkeys::::get(&coldkey); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + if staking_hotkeys.contains(old_hotkey) { + staking_hotkeys.retain(|hk| *hk != *old_hotkey && *hk != *new_hotkey); + if !staking_hotkeys.contains(new_hotkey) { + staking_hotkeys.push(new_hotkey.clone()); + } + StakingHotkeys::::insert(&coldkey, staking_hotkeys); + weight.saturating_accrue(T::DbWeight::get().writes(1)); } - StakingHotkeys::::insert(&coldkey, staking_hotkeys); - weight.saturating_accrue(T::DbWeight::get().writes(1)); } } } - // Return successful after swapping all the relevant terms. Ok(()) } @@ -358,10 +360,9 @@ impl Pallet { netuid: NetUid, keep_stake: bool, ) -> DispatchResult { - // 1. Swap total hotkey alpha for all subnets it exists on. - // TotalHotkeyAlpha( hotkey, netuid ) -> alpha -- the total alpha that the hotkey has on a specific subnet. - // Only transfer stake when keep_stake is false. if !keep_stake { + // 1. Swap total hotkey alpha for all subnets it exists on. + // TotalHotkeyAlpha( hotkey, netuid ) -> alpha -- the total alpha that the hotkey has on a specific subnet. let alpha = TotalHotkeyAlpha::::take(old_hotkey, netuid); TotalHotkeyAlpha::::mutate(new_hotkey, netuid, |value| { @@ -370,12 +371,24 @@ impl Pallet { weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); // 2. Swap total hotkey shares on all subnets it exists on. - // TotalHotkeyShares( hotkey, netuid ) -> alpha -- the total alpha that the hotkey has on a specific subnet. - let share = TotalHotkeyShares::::take(old_hotkey, netuid); - TotalHotkeyShares::::mutate(new_hotkey, netuid, |value| { - *value = value.saturating_add(share) - }); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + // TotalHotkeyShares( hotkey, netuid ) -> share pool denominator for this hotkey on this subnet. + // Merge v1 and v2 TotalHotkeyShares because TotalHotkeyShares v1 is deprecated + weight.saturating_accrue(T::DbWeight::get().reads(4)); + let old_share_v1 = SafeFloat::from(TotalHotkeyShares::::take(old_hotkey, netuid)); + let old_share_v2 = TotalHotkeySharesV2::::take(old_hotkey, netuid); + let total_old_shares = old_share_v1.add(&old_share_v2).unwrap_or_default(); + + let new_share_v1 = SafeFloat::from(TotalHotkeyShares::::take(new_hotkey, netuid)); + let new_share_v2 = TotalHotkeySharesV2::::take(new_hotkey, netuid); + let total_new_shares = new_share_v1.add(&new_share_v2).unwrap_or_default(); + + TotalHotkeyShares::::remove(old_hotkey, netuid); + TotalHotkeyShares::::remove(new_hotkey, netuid); + + let total_old_plus_new_shares = + total_new_shares.add(&total_old_shares).unwrap_or_default(); + TotalHotkeySharesV2::::insert(new_hotkey, netuid, total_old_plus_new_shares); + weight.saturating_accrue(T::DbWeight::get().writes(3)); } // 3. Swap all subnet specific info. @@ -543,6 +556,11 @@ impl Pallet { weight.saturating_accrue(T::DbWeight::get().reads(old_alpha_values.len() as u64)); weight.saturating_accrue(T::DbWeight::get().writes(old_alpha_values.len() as u64)); + let old_alpha_values_v2: Vec<((T::AccountId, NetUid), SafeFloat)> = + AlphaV2::::iter_prefix((old_hotkey,)).collect(); + weight.saturating_accrue(T::DbWeight::get().reads(old_alpha_values_v2.len() as u64)); + weight.saturating_accrue(T::DbWeight::get().writes(old_alpha_values_v2.len() as u64)); + // 9.1. Transfer root claimable Self::transfer_root_claimable_for_new_hotkey(old_hotkey, new_hotkey); @@ -555,9 +573,38 @@ impl Pallet { let new_alpha = Alpha::::take((new_hotkey, &coldkey, netuid)); Alpha::::remove((old_hotkey, &coldkey, netuid)); - Alpha::::insert( + + // Insert into AlphaV2 because Alpha is deprecated + AlphaV2::::insert( + (new_hotkey, &coldkey, netuid), + SafeFloat::from(alpha.saturating_add(new_alpha)), + ); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); + + // Swap StakingHotkeys. + // StakingHotkeys( coldkey ) --> Vec -- the hotkeys that the coldkey stakes. + let mut staking_hotkeys = StakingHotkeys::::get(&coldkey); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + if staking_hotkeys.contains(old_hotkey) && !staking_hotkeys.contains(new_hotkey) + { + staking_hotkeys.push(new_hotkey.clone()); + StakingHotkeys::::insert(&coldkey, staking_hotkeys); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + } + } + } + + for ((coldkey, netuid_alpha), alpha) in old_alpha_values_v2 { + if netuid == netuid_alpha { + Self::transfer_root_claimed_for_new_keys( + netuid, old_hotkey, new_hotkey, &coldkey, &coldkey, + ); + + let new_alpha_v2 = AlphaV2::::take((new_hotkey, &coldkey, netuid)); + AlphaV2::::remove((old_hotkey, &coldkey, netuid)); + AlphaV2::::insert( (new_hotkey, &coldkey, netuid), - alpha.saturating_add(new_alpha), + alpha.add(&new_alpha_v2).unwrap_or_default(), ); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index deb4cd7fc5..772909638b 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -20,6 +20,7 @@ use frame_system as system; use frame_system::{EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_utility as pallet_utility; +use share_pool::SafeFloat; use sp_core::{ConstU64, Get, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; use sp_runtime::{ @@ -331,8 +332,8 @@ impl pallet_subtensor_swap::Config for Test { type SubnetInfo = SubtensorModule; type BalanceOps = SubtensorModule; type ProtocolId = SwapProtocolId; - type TaoReserve = TaoCurrencyReserve; - type AlphaReserve = AlphaCurrencyReserve; + type TaoReserve = TaoBalanceReserve; + type AlphaReserve = AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; @@ -1023,3 +1024,14 @@ pub fn commit_dummy(who: U256, netuid: NetUid) { hash )); } + +#[allow(dead_code)] +pub fn sf_to_u128(sf: &SafeFloat) -> u128 { + let alpha_f64: f64 = sf.into(); + alpha_f64 as u128 +} + +#[allow(dead_code)] +pub fn sf_from_u64(val: u64) -> SafeFloat { + SafeFloat::from(val) +} diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index f6a50cf4ff..859c325c1f 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -1,4 +1,4 @@ -#![allow(clippy::expect_used)] +#![allow(clippy::expect_used, clippy::indexing_slicing)] use super::mock::*; use crate::migrations::migrate_network_immunity_period; @@ -7,7 +7,7 @@ use frame_support::{assert_err, assert_ok}; use frame_system::Config; use sp_core::U256; use sp_std::collections::{btree_map::BTreeMap, vec_deque::VecDeque}; -use substrate_fixed::types::{I96F32, U64F64, U96F32}; +use substrate_fixed::types::{I96F32, U96F32}; use subtensor_runtime_common::{MechId, NetUidStorageIndex, TaoBalance}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -100,7 +100,7 @@ fn dissolve_single_alpha_out_staker_gets_all_tao() { // 2. Single α-out staker let (s_hot, s_cold) = (U256::from(100), U256::from(200)); - Alpha::::insert((s_hot, s_cold, net), U64F64::from_num(5_000u128)); + AlphaV2::::insert((s_hot, s_cold, net), sf_from_u64(5_000u64)); // Entire TAO pot should be paid to staker's cold-key let pot: u64 = 99_999; @@ -119,7 +119,7 @@ fn dissolve_single_alpha_out_staker_gets_all_tao() { assert_eq!(after, before + pot.into()); // No α entries left for dissolved subnet - assert!(Alpha::::iter().all(|((_h, _c, n), _)| n != net)); + assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != net)); assert!(!SubnetTAO::::contains_key(net)); }); } @@ -137,14 +137,14 @@ fn dissolve_two_stakers_pro_rata_distribution() { let reg_at = NetworkRegisteredAt::::get(net); NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); - let (s1_hot, s1_cold, a1) = (U256::from(201), U256::from(301), 300u128); - let (s2_hot, s2_cold, a2) = (U256::from(202), U256::from(302), 700u128); + let (s1_hot, s1_cold, a1) = (U256::from(201), U256::from(301), 300u64); + let (s2_hot, s2_cold, a2) = (U256::from(202), U256::from(302), 700u64); - Alpha::::insert((s1_hot, s1_cold, net), U64F64::from_num(a1)); - Alpha::::insert((s2_hot, s2_cold, net), U64F64::from_num(a2)); + AlphaV2::::insert((s1_hot, s1_cold, net), sf_from_u64(a1)); + AlphaV2::::insert((s2_hot, s2_cold, net), sf_from_u64(a2)); - TotalHotkeyAlpha::::insert(s1_hot, net, AlphaBalance::from(a1 as u64)); - TotalHotkeyAlpha::::insert(s2_hot, net, AlphaBalance::from(a2 as u64)); + TotalHotkeyAlpha::::insert(s1_hot, net, AlphaBalance::from(a1)); + TotalHotkeyAlpha::::insert(s2_hot, net, AlphaBalance::from(a2)); let pot: u64 = 10_000; SubnetTAO::::insert(net, TaoBalance::from(pot)); @@ -156,9 +156,9 @@ fn dissolve_two_stakers_pro_rata_distribution() { let owner_before = SubtensorModule::get_coldkey_balance(&oc); // Expected τ shares with largest remainder - let total = a1 + a2; - let prod1 = a1 * (pot as u128); - let prod2 = a2 * (pot as u128); + let total = (a1 + a2) as u128; + let prod1 = (a1 as u128) * (pot as u128); + let prod2 = (a2 as u128) * (pot as u128); let share1 = (prod1 / total) as u64; let share2 = (prod2 / total) as u64; let mut distributed = share1 + share2; @@ -202,7 +202,7 @@ fn dissolve_two_stakers_pro_rata_distribution() { ); // α entries for dissolved subnet gone - assert!(Alpha::::iter().all(|((_h, _c, n), _)| n != net)); + assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != net)); }); } @@ -635,7 +635,7 @@ fn dissolve_alpha_out_but_zero_tao_no_rewards() { let sh = U256::from(23); let sc = U256::from(24); - Alpha::::insert((sh, sc, net), U64F64::from_num(1_000u64)); + AlphaV2::::insert((sh, sc, net), sf_from_u64(1_000u64)); SubnetTAO::::insert(net, TaoBalance::from(0)); // zero TAO SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(0)); Emission::::insert(net, Vec::::new()); @@ -647,7 +647,7 @@ fn dissolve_alpha_out_but_zero_tao_no_rewards() { // No reward distributed, α-out cleared. assert_eq!(after, before); - assert!(Alpha::::iter().next().is_none()); + assert!(AlphaV2::::iter().next().is_none()); }); } @@ -679,8 +679,8 @@ fn dissolve_rounding_remainder_distribution() { let (s1h, s1c) = (U256::from(63), U256::from(64)); let (s2h, s2c) = (U256::from(65), U256::from(66)); - Alpha::::insert((s1h, s1c, net), U64F64::from_num(3u128)); - Alpha::::insert((s2h, s2c, net), U64F64::from_num(2u128)); + AlphaV2::::insert((s1h, s1c, net), sf_from_u64(3u64)); + AlphaV2::::insert((s2h, s2c, net), sf_from_u64(2u64)); SubnetTAO::::insert(net, TaoBalance::from(1)); // TAO pot = 1 SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(0)); @@ -703,7 +703,7 @@ fn dissolve_rounding_remainder_distribution() { assert_eq!(c2_after, c2_before); // α records for subnet gone; TAO key gone - assert!(Alpha::::iter().all(|((_h, _c, n), _)| n != net)); + assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != net)); assert!(!SubnetTAO::::contains_key(net)); }); } @@ -748,8 +748,8 @@ fn destroy_alpha_out_multiple_stakers_pro_rata() { )); // 4. α-out snapshot - let a1: u128 = Alpha::::get((h1, c1, netuid)).saturating_to_num(); - let a2: u128 = Alpha::::get((h2, c2, netuid)).saturating_to_num(); + let a1: u128 = sf_to_u128(&AlphaV2::::get((h1, c1, netuid))); + let a2: u128 = sf_to_u128(&AlphaV2::::get((h2, c2, netuid))); let atotal = a1 + a2; // 5. TAO pot & lock @@ -799,8 +799,8 @@ fn destroy_alpha_out_multiple_stakers_pro_rata() { ); // 11. α entries cleared for the subnet - assert!(!Alpha::::contains_key((h1, c1, netuid))); - assert!(!Alpha::::contains_key((h2, c2, netuid))); + assert!(!AlphaV2::::contains_key((h1, c1, netuid))); + assert!(!AlphaV2::::contains_key((h2, c2, netuid))); }); } @@ -856,7 +856,7 @@ fn destroy_alpha_out_many_stakers_complex_distribution() { let mut alpha = [0u128; N]; let mut alpha_sum: u128 = 0; for i in 0..N { - alpha[i] = Alpha::::get((hot[i], cold[i], netuid)).saturating_to_num(); + alpha[i] = sf_to_u128(&AlphaV2::::get((hot[i], cold[i], netuid))); alpha_sum += alpha[i]; } @@ -937,7 +937,7 @@ fn destroy_alpha_out_many_stakers_complex_distribution() { ); // α cleared for dissolved subnet & related counters reset - assert!(Alpha::::iter().all(|((_h, _c, n), _)| n != netuid)); + assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != netuid)); assert_eq!(SubnetAlphaIn::::get(netuid), 0.into()); assert_eq!(SubnetAlphaOut::::get(netuid), 0.into()); assert_eq!(SubtensorModule::get_subnet_locked_balance(netuid), 0.into()); @@ -1778,7 +1778,6 @@ fn test_tempo_greater_than_weight_set_rate_limit() { }) } -#[allow(clippy::indexing_slicing)] #[test] fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state() { new_test_ext(0).execute_with(|| { @@ -1816,7 +1815,7 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( ]; // ──────────────────────────────────────────────────────────────────── - // 1) Create many subnets, enable V3, fix price at tick=0 (sqrt≈1) + // 1) Create many subnets, fix price at tick=0 // ──────────────────────────────────────────────────────────────────── let mut nets: Vec = Vec::new(); for i in 0..NUM_NETS { @@ -1828,20 +1827,6 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( Emission::::insert(net, Vec::::new()); SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(0)); - assert_ok!( - pallet_subtensor_swap::Pallet::::toggle_user_liquidity( - RuntimeOrigin::root(), - net, - true - ) - ); - - // Price/tick pinned so LP math stays stable (sqrt(1)). - let ct0 = pallet_subtensor_swap::tick::TickIndex::new_unchecked(0); - let sqrt1 = ct0.try_to_sqrt_price().expect("sqrt(1) price"); - pallet_subtensor_swap::CurrentTick::::set(net, ct0); - pallet_subtensor_swap::AlphaSqrtPrice::::set(net, sqrt1); - nets.push(net); } @@ -1922,10 +1907,10 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( } // Capture **pair‑level** α snapshot per net (pre‑LP). - for ((hot, cold, net), amt) in Alpha::::iter() { + for ((hot, cold, net), amt) in AlphaV2::::iter() { if let Some(&ni) = net_index.get(&net) && lp_sets_per_net[ni].contains(&cold) { - let a: u128 = amt.saturating_to_num(); + let a: u128 = sf_to_u128(&amt); if a > 0 { alpha_pairs_per_net .entry(net) @@ -2050,53 +2035,17 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( // For each dissolved net, check α ledgers gone, network removed, and swap state clean. for &net in nets.iter() { assert!( - Alpha::::iter().all(|((_h, _c, n), _)| n != net), + AlphaV2::::iter().all(|((_h, _c, n), _)| n != net), "alpha ledger not fully cleared for net {net:?}" ); assert!( !SubtensorModule::if_subnet_exist(net), "subnet {net:?} still exists" ); - assert!( - pallet_subtensor_swap::Ticks::::iter_prefix(net) - .next() - .is_none(), - "ticks not cleared for net {net:?}" - ); - assert!( - !pallet_subtensor_swap::Positions::::iter() - .any(|((n, _owner, _pid), _)| n == net), - "swap positions not fully cleared for net {net:?}" - ); - assert_eq!( - pallet_subtensor_swap::FeeGlobalTao::::get(net).saturating_to_num::(), - 0, - "FeeGlobalTao nonzero for net {net:?}" - ); - assert_eq!( - pallet_subtensor_swap::FeeGlobalAlpha::::get(net).saturating_to_num::(), - 0, - "FeeGlobalAlpha nonzero for net {net:?}" - ); - assert_eq!( - pallet_subtensor_swap::CurrentLiquidity::::get(net), - 0, - "CurrentLiquidity not zero for net {net:?}" - ); assert!( !pallet_subtensor_swap::SwapV3Initialized::::get(net), "SwapV3Initialized still set" ); - assert!( - !pallet_subtensor_swap::EnabledUserLiquidity::::get(net), - "EnabledUserLiquidity still set" - ); - assert!( - pallet_subtensor_swap::TickIndexBitmapWords::::iter_prefix((net,)) - .next() - .is_none(), - "TickIndexBitmapWords not cleared for net {net:?}" - ); } // ──────────────────────────────────────────────────────────────────── @@ -2111,18 +2060,6 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( Emission::::insert(net_new, Vec::::new()); SubtensorModule::set_subnet_locked_balance(net_new, TaoBalance::from(0)); - assert_ok!( - pallet_subtensor_swap::Pallet::::toggle_user_liquidity( - RuntimeOrigin::root(), - net_new, - true - ) - ); - let ct0 = pallet_subtensor_swap::tick::TickIndex::new_unchecked(0); - let sqrt1 = ct0.try_to_sqrt_price().expect("sqrt(1)"); - pallet_subtensor_swap::CurrentTick::::set(net_new, ct0); - pallet_subtensor_swap::AlphaSqrtPrice::::set(net_new, sqrt1); - // Compute the exact min stake per the pallet rule: DefaultMinStake + fee(DefaultMinStake). let min_stake = DefaultMinStake::::get(); let order = GetAlphaForTao::::with_amount(min_stake); @@ -2142,7 +2079,7 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( register_ok_neuron(net_new, hot1, cold, 7777); let before_tao = SubtensorModule::get_coldkey_balance(&cold); - let a_prev: u64 = Alpha::::get((hot1, cold, net_new)).saturating_to_num(); + let a_prev: u64 = sf_to_u128(&AlphaV2::::get((hot1, cold, net_new))) as u64; // Expected α for this exact τ, using the same sim path as the pallet. let order = GetAlphaForTao::::with_amount(min_amount_required); @@ -2161,7 +2098,7 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( )); let after_tao = SubtensorModule::get_coldkey_balance(&cold); - let a_new: u64 = Alpha::::get((hot1, cold, net_new)).saturating_to_num(); + let a_new: u64 = sf_to_u128(&AlphaV2::::get((hot1, cold, net_new))) as u64; let a_delta = a_new.saturating_sub(a_prev); // τ decreased by exactly the amount we sent. diff --git a/pallets/subtensor/src/tests/recycle_alpha.rs b/pallets/subtensor/src/tests/recycle_alpha.rs index 9ae3975744..404967dc74 100644 --- a/pallets/subtensor/src/tests/recycle_alpha.rs +++ b/pallets/subtensor/src/tests/recycle_alpha.rs @@ -3,8 +3,9 @@ use super::mock::*; use crate::*; use approx::assert_abs_diff_eq; use frame_support::{assert_noop, assert_ok, traits::Currency}; +use share_pool::SafeFloat; use sp_core::U256; -use substrate_fixed::types::{U64F64, U96F32}; +use substrate_fixed::types::U96F32; use subtensor_runtime_common::{AlphaBalance, Token}; use subtensor_swap_interface::SwapHandler; @@ -546,75 +547,93 @@ fn test_burn_errors() { } #[test] -fn test_recycle_precision_loss() { +fn test_recycle_precision() { new_test_ext(1).execute_with(|| { let coldkey = U256::from(1); let hotkey = U256::from(2); let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + let tao_reserve = TaoBalance::from(1_000_000_000_u64); + let alpha_reserve = AlphaBalance::from(1_000_000_000_u64); + SubnetAlphaIn::::insert(netuid, alpha_reserve); + SubnetTAO::::insert(netuid, tao_reserve); Balances::make_free_balance_be(&coldkey, 1_000_000_000.into()); // sanity check assert!(SubtensorModule::if_subnet_exist(netuid)); // add stake to coldkey-hotkey pair so we can recycle it - let stake = 200_000; increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); // amount to recycle let recycle_amount = AlphaBalance::from(stake / 2); - // Modify the alpha pool denominator so it's low-precision - let denominator = U64F64::from_num(0.00000001); - TotalHotkeyShares::::insert(hotkey, netuid, denominator); - Alpha::::insert((&hotkey, &coldkey, netuid), denominator); + // Modify the alpha pool denominator so it's low-precision (denominator = share = 1e-9) + let denominator = SafeFloat::from(1) + .div(&SafeFloat::from(1_000_000_000)) + .unwrap_or_default(); + TotalHotkeySharesV2::::insert(hotkey, netuid, denominator.clone()); + AlphaV2::::insert((&hotkey, &coldkey, netuid), denominator); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); // recycle, expect error due to precision loss - assert_noop!( - SubtensorModule::recycle_alpha( - RuntimeOrigin::signed(coldkey), - hotkey, - recycle_amount, - netuid - ), - Error::::PrecisionLoss + assert_ok!(SubtensorModule::recycle_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + recycle_amount, + netuid + )); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaBalance::from(stake / 2) ); }); } #[test] -fn test_burn_precision_loss() { +fn test_burn_precision() { new_test_ext(1).execute_with(|| { let coldkey = U256::from(1); let hotkey = U256::from(2); let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + let tao_reserve = TaoBalance::from(1_000_000_000_u64); + let alpha_reserve = AlphaBalance::from(1_000_000_000_u64); + SubnetAlphaIn::::insert(netuid, alpha_reserve); + SubnetTAO::::insert(netuid, tao_reserve); Balances::make_free_balance_be(&coldkey, 1_000_000_000.into()); // sanity check assert!(SubtensorModule::if_subnet_exist(netuid)); // add stake to coldkey-hotkey pair so we can recycle it - let stake = 200_000; increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); // amount to recycle let burn_amount = AlphaBalance::from(stake / 2); - // Modify the alpha pool denominator so it's low-precision - let denominator = U64F64::from_num(0.00000001); - TotalHotkeyShares::::insert(hotkey, netuid, denominator); - Alpha::::insert((&hotkey, &coldkey, netuid), denominator); + // Modify the alpha pool denominator so it's low-precision (denominator = share = 1e-9) + let denominator = SafeFloat::from(1) + .div(&SafeFloat::from(1_000_000_000)) + .unwrap_or_default(); + TotalHotkeySharesV2::::insert(hotkey, netuid, denominator.clone()); + AlphaV2::::insert((&hotkey, &coldkey, netuid), denominator); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); // burn, expect error due to precision loss - assert_noop!( - SubtensorModule::burn_alpha( - RuntimeOrigin::signed(coldkey), - hotkey, - burn_amount, - netuid - ), - Error::::PrecisionLoss + assert_ok!(SubtensorModule::burn_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + burn_amount, + netuid + )); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaBalance::from(stake / 2) ); }); } diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 0b82fa27eb..28d5353ce7 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -8,6 +8,7 @@ use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; use frame_system::RawOrigin; use pallet_subtensor_swap::tick::TickIndex; use safe_math::FixedExt; +use share_pool::SafeFloat; use sp_core::{Get, H256, U256}; use substrate_fixed::traits::FromFixed; use substrate_fixed::types::{I96F32, I110F18, U64F64, U96F32}; @@ -4123,9 +4124,9 @@ fn test_add_stake_specific_stake_into_subnet_fail() { }); } -// cargo test --package pallet-subtensor --lib -- tests::staking::test_remove_99_999_per_cent_stake_removes_all --exact --show-output +// cargo test --package pallet-subtensor --lib -- tests::staking::test_remove_99_999_per_cent_stake_works_precisely --exact --show-output #[test] -fn test_remove_99_9991_per_cent_stake_removes_all() { +fn test_remove_99_9991_per_cent_stake_works_precisely() { new_test_ext(1).execute_with(|| { let subnet_owner_coldkey = U256::from(1); let subnet_owner_hotkey = U256::from(2); @@ -4161,7 +4162,7 @@ fn test_remove_99_9991_per_cent_stake_removes_all() { (U64F64::from_num(alpha) * U64F64::from_num(0.999991)).to_num::(), ); // we expected the entire stake to be returned - let (expected_balance, _) = mock::swap_alpha_to_tao(netuid, alpha); + let (expected_balance, _) = mock::swap_alpha_to_tao(netuid, remove_amount); assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -4175,16 +4176,13 @@ fn test_remove_99_9991_per_cent_stake_removes_all() { expected_balance, epsilon = 10.into(), ); - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), - TaoBalance::ZERO - ); + assert!(!SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id).is_zero()); let new_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey_account_id, &coldkey_account_id, netuid, ); - assert!(new_alpha.is_zero()); + assert_eq!(new_alpha, alpha - remove_amount); }); } @@ -5653,3 +5651,447 @@ fn test_staking_records_flow() { ); }); } + +// cargo test --package pallet-subtensor --lib -- tests::staking::test_lazy_sharepool_migration_get_stake_reads_from_deprecated_alpha_map --exact --nocapture +#[test] +fn test_lazy_sharepool_migration_get_stake_reads_from_deprecated_alpha_map() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha map + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaBalance::from(stake) + ); + }); +} + +#[test] +fn test_lazy_sharepool_migration_get_stake_reads_from_alpha_v2_map() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to AlphaV2 map + AlphaV2::::insert((hotkey, coldkey, netuid), SafeFloat::from(1_u64)); + TotalHotkeySharesV2::::insert(hotkey, netuid, SafeFloat::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaBalance::from(stake) + ); + }); +} + +#[test] +fn test_lazy_sharepool_migration_get_stake_reads_from_cross_alpha_maps() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to Alpha map + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + // but total shares are in TotalHotkeySharesV2 map (already migrated) + TotalHotkeySharesV2::::insert(hotkey, netuid, SafeFloat::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaBalance::from(stake) + ); + }); +} + +#[test] +fn test_lazy_sharepool_migration_staking_causes_migration() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha map + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Stake more via stake_into_subnet + increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); + + // Verify that deprecated v1 map values are gone + assert!(Alpha::::try_get((&hotkey, &coldkey, netuid)).is_err()); + assert!(TotalHotkeyShares::::try_get(hotkey, netuid).is_err()); + + // Verify that v2 map values are present + let migrated_share = AlphaV2::::get((&hotkey, &coldkey, netuid)); + let migrated_denominator = TotalHotkeySharesV2::::get(hotkey, netuid); + + assert_abs_diff_eq!( + f64::from((migrated_share.div(&migrated_denominator)).unwrap()), + 1.0, + epsilon = 0.000000000000001 + ); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha map + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to AlphaV2 map + let share = sf_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_mixed_v1_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha map and new THS v2 map + let share = sf_from_u64(1_u64); + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_mixed_v2_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to new AlphaV2 map and deprecated THS map + let share = sf_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_from_shares_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated THS map + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value_from_shares + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let current_share = SafeFloat::from(U64F64::from(1_u64)); + let actual_value = share_pool.get_value_from_shares(current_share); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_from_shares_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to new THS v2 map + let share = sf_from_u64(1_u64); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value_from_shares + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let current_share = SafeFloat::from(U64F64::from(1_u64)); + let actual_value = share_pool.get_value_from_shares(current_share); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_update_value_for_all() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to new AlphaV2 map + let share = sf_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and call update_value_for_all + let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + share_pool.update_value_for_all(stake as i64); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake * 2); + + share_pool.update_value_for_all(-(stake as i64)); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_update_value_for_one_v1_with_migration() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha and THS maps + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and call update_value_for_one + let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + share_pool.update_value_for_one(&coldkey, stake as i64); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake * 2); + + // Verify deletion from deprecated + assert!(!Alpha::::contains_key((hotkey, coldkey, netuid))); + assert!(!TotalHotkeyShares::::contains_key(hotkey, netuid)); + }); +} + +#[test] +fn test_sharepool_dataops_update_value_for_one_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to new AlphaV2 and THS maps + let share = sf_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and call update_value_for_one + let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + share_pool.update_value_for_one(&coldkey, stake as i64); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake * 2); + }); +} + +#[test] +fn test_sharepool_dataops_update_value_for_one_mixed_v1_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha and new THS v2 maps + let share = sf_from_u64(1_u64); + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and call update_value_for_one + let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + share_pool.update_value_for_one(&coldkey, stake as i64); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake * 2); + + // Verify deletion from deprecated + assert!(!Alpha::::contains_key((hotkey, coldkey, netuid))); + }); +} + +#[test] +fn test_sharepool_dataops_update_value_for_one_mixed_v2_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to new AlphaV2 and deprecated THS maps + let share = sf_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and call update_value_for_one + let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + share_pool.update_value_for_one(&coldkey, stake as i64); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake * 2); + + // Verify deletion from deprecated + assert!(!TotalHotkeyShares::::contains_key(hotkey, netuid)); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_returns_zero_on_non_existing_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add to deprecated THS map, but no value in Alpha map + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, 0_u64); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_returns_zero_on_non_existing_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add to THSV2 map, but no value in AlphaV2 map + let share = sf_from_u64(1_u64); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, 0_u64); + }); +} + +#[test] +fn test_sharepool_dataops_try_get_value_returns_err_on_non_existing_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add to deprecated THS map, but no value in Alpha map + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let maybe_actual_value = share_pool.try_get_value(&coldkey); + assert!(maybe_actual_value.is_err()); + }); +} + +#[test] +fn test_sharepool_dataops_try_get_value_returns_err_on_non_existing_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add to THSV2 map, but no value in AlphaV2 map + let share = sf_from_u64(1_u64); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let maybe_actual_value = share_pool.try_get_value(&coldkey); + assert!(maybe_actual_value.is_err()); + }); +} diff --git a/pallets/subtensor/src/tests/staking2.rs b/pallets/subtensor/src/tests/staking2.rs index 7de304e80e..536a14579a 100644 --- a/pallets/subtensor/src/tests/staking2.rs +++ b/pallets/subtensor/src/tests/staking2.rs @@ -429,12 +429,9 @@ fn test_share_based_staking_denominator_precision() { netuid, stake_amount, ); - assert_eq!( - stake_amount, - Alpha::::get((hotkey1, coldkey1, netuid)) - .to_num::() - .into(), - ); + + let actual_stake: f64 = AlphaV2::::get((hotkey1, coldkey1, netuid)).into(); + assert_eq!(stake_amount, (actual_stake as u64).into(),); SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( &hotkey1, &coldkey1, @@ -445,15 +442,7 @@ fn test_share_based_staking_denominator_precision() { let stake1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey1, &coldkey1, netuid, ); - let expected_remaining_stake = if (stake_amount.to_u64() as f64 - - unstake_amount.to_u64() as f64) - / (stake_amount.to_u64() as f64) - <= 0.00001 - { - AlphaBalance::ZERO - } else { - stake_amount - unstake_amount - }; + let expected_remaining_stake = stake_amount - unstake_amount; assert_eq!(stake1, expected_remaining_stake); }); }); diff --git a/pallets/subtensor/src/tests/swap_coldkey.rs b/pallets/subtensor/src/tests/swap_coldkey.rs index 23488f3370..67362ab614 100644 --- a/pallets/subtensor/src/tests/swap_coldkey.rs +++ b/pallets/subtensor/src/tests/swap_coldkey.rs @@ -16,6 +16,7 @@ use frame_support::traits::schedule::DispatchTime; use frame_support::traits::schedule::v3::Named as ScheduleNamed; use frame_support::{assert_err, assert_noop, assert_ok}; use frame_system::{Config, RawOrigin}; +use share_pool::SafeFloat; use sp_core::{Get, H256, U256}; use sp_runtime::traits::Hash; use sp_runtime::traits::{DispatchInfoOf, DispatchTransaction, TransactionExtension}; @@ -1355,21 +1356,15 @@ fn test_do_swap_coldkey_effect_on_delegations() { approx_total_stake, epsilon = approx_total_stake / 100.into() ); - assert_eq!( - expected_stake, - Alpha::::get((delegate, new_coldkey, netuid)) - .to_num::() - .into(), - ); - assert_eq!(Alpha::::get((delegate, coldkey, netuid)), 0); + let actual_stake_new: u64 = AlphaV2::::get((delegate, new_coldkey, netuid)).into(); + assert_eq!(expected_stake, actual_stake_new.into()); + let actual_stake_old: u64 = AlphaV2::::get((delegate, coldkey, netuid)).into(); + assert_eq!(actual_stake_old, 0u64); - assert_eq!( - expected_stake, - Alpha::::get((delegate, new_coldkey, netuid2)) - .to_num::() - .into() - ); - assert_eq!(Alpha::::get((delegate, coldkey, netuid2)), 0); + let actual_stake_new_2: u64 = AlphaV2::::get((delegate, new_coldkey, netuid2)).into(); + assert_eq!(expected_stake, actual_stake_new_2.into()); + let actual_stake_old_2: u64 = AlphaV2::::get((delegate, coldkey, netuid2)).into(); + assert_eq!(actual_stake_old_2, 0u64); }); } diff --git a/pallets/subtensor/src/tests/swap_hotkey.rs b/pallets/subtensor/src/tests/swap_hotkey.rs index d7f5a1274f..d0a1de3526 100644 --- a/pallets/subtensor/src/tests/swap_hotkey.rs +++ b/pallets/subtensor/src/tests/swap_hotkey.rs @@ -5,6 +5,7 @@ use codec::Encode; use frame_support::weights::Weight; use frame_support::{assert_err, assert_noop, assert_ok}; use frame_system::{Config, RawOrigin}; +use share_pool::SafeFloat; use sp_core::{Get, H160, H256, U256}; use sp_runtime::SaturatedConversion; use substrate_fixed::types::U64F64; @@ -877,7 +878,7 @@ fn test_swap_owner_new_hotkey_already_exists() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey -- test_swap_stake_success --exact --nocapture +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey::test_swap_stake_success --exact --nocapture #[test] fn test_swap_stake_success() { new_test_ext(1).execute_with(|| { @@ -934,7 +935,11 @@ fn test_swap_stake_success() { ); assert_eq!( TotalHotkeyShares::::get(new_hotkey, netuid), - U64F64::from_num(shares) + U64F64::from_num(0) + ); + assert_eq!( + TotalHotkeySharesV2::::get(new_hotkey, netuid), + shares.into() ); assert_eq!( Alpha::::get((old_hotkey, coldkey, netuid)), @@ -942,7 +947,92 @@ fn test_swap_stake_success() { ); assert_eq!( Alpha::::get((new_hotkey, coldkey, netuid)), - U64F64::from_num(amount) + U64F64::from_num(0) + ); + assert_eq!( + f64::from(AlphaV2::::get((new_hotkey, coldkey, netuid))), + amount as f64 + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, old_hotkey), + AlphaBalance::ZERO + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, new_hotkey), + amount.into() + ); + }); +} + +#[test] +fn test_swap_stake_v2_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let amount = 10_000; + let shares = U64F64::from_num(123456); + let mut weight = Weight::zero(); + + // Initialize staking variables for old_hotkey + TotalHotkeyAlpha::::insert(old_hotkey, netuid, AlphaBalance::from(amount)); + TotalHotkeyAlphaLastEpoch::::insert( + old_hotkey, + netuid, + AlphaBalance::from(amount * 2), + ); + TotalHotkeySharesV2::::insert(old_hotkey, netuid, SafeFloat::from(shares)); + AlphaV2::::insert( + (old_hotkey, coldkey, netuid), + SafeFloat::from(U64F64::from_num(amount)), + ); + AlphaDividendsPerSubnet::::insert(netuid, old_hotkey, AlphaBalance::from(amount)); + + // Perform the swap + SubtensorModule::perform_hotkey_swap_on_all_subnets( + &old_hotkey, + &new_hotkey, + &coldkey, + &mut weight, + false, + ); + + // Verify the swap + assert_eq!( + TotalHotkeyAlpha::::get(old_hotkey, netuid), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlpha::::get(new_hotkey, netuid), + amount.into() + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(old_hotkey, netuid), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(new_hotkey, netuid), + AlphaBalance::from(amount * 2) + ); + assert_eq!( + f64::from(TotalHotkeySharesV2::::get(old_hotkey, netuid)), + 0_f64 + ); + assert_abs_diff_eq!( + f64::from(TotalHotkeySharesV2::::get(new_hotkey, netuid)), + shares.to_num::(), + epsilon = 0.0000000001 + ); + assert_eq!( + f64::from(AlphaV2::::get((old_hotkey, coldkey, netuid))), + 0_f64 + ); + assert_eq!( + f64::from(AlphaV2::::get((new_hotkey, coldkey, netuid))), + amount as f64 ); assert_eq!( AlphaDividendsPerSubnet::::get(netuid, old_hotkey), @@ -985,8 +1075,8 @@ fn test_swap_stake_old_hotkey_not_exist() { false, ); - // Verify that new_hotkey has the stake and old_hotkey does not - assert!(Alpha::::contains_key((new_hotkey, coldkey, netuid))); + // Verify that new_hotkey has the stake (in new AlphaV2 map) and old_hotkey does not + assert!(AlphaV2::::contains_key((new_hotkey, coldkey, netuid))); assert!(!Alpha::::contains_key((old_hotkey, coldkey, netuid))); }); } diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index 6f43d46fde..eb310d1202 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -9,6 +9,7 @@ use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance, Tok use super::mock::*; use crate::*; +use share_pool::SafeFloat; use sp_core::{Get, H160, H256, U256}; use sp_runtime::SaturatedConversion; use std::collections::BTreeSet; @@ -933,7 +934,7 @@ fn test_swap_owner_new_hotkey_already_exists() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_stake_success --exact --nocapture +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_stake_success --exact --nocapture #[test] fn test_swap_stake_success() { new_test_ext(1).execute_with(|| { @@ -991,7 +992,12 @@ fn test_swap_stake_success() { ); assert_eq!( TotalHotkeyShares::::get(new_hotkey, netuid), - U64F64::from_num(shares) + U64F64::from_num(0) + ); + assert_abs_diff_eq!( + f64::from(TotalHotkeySharesV2::::get(new_hotkey, netuid)), + shares.to_num::(), + epsilon = 0.0000000001 ); assert_eq!( Alpha::::get((old_hotkey, coldkey, netuid)), @@ -999,7 +1005,93 @@ fn test_swap_stake_success() { ); assert_eq!( Alpha::::get((new_hotkey, coldkey, netuid)), - U64F64::from_num(amount) + U64F64::from_num(0) + ); + assert_eq!( + f64::from(AlphaV2::::get((new_hotkey, coldkey, netuid))), + amount as f64 + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, old_hotkey), + AlphaBalance::ZERO + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, new_hotkey), + AlphaBalance::from(amount) + ); + }); +} + +#[test] +fn test_swap_stake_v2_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + SubtensorModule::add_balance_to_coldkey_account(&coldkey, u64::MAX.into()); + let amount = 10_000; + let shares = U64F64::from_num(123456); + + // Initialize staking variables for old_hotkey + TotalHotkeyAlpha::::insert(old_hotkey, netuid, AlphaBalance::from(amount)); + TotalHotkeyAlphaLastEpoch::::insert( + old_hotkey, + netuid, + AlphaBalance::from(amount * 2), + ); + TotalHotkeySharesV2::::insert(old_hotkey, netuid, SafeFloat::from(shares)); + AlphaV2::::insert( + (old_hotkey, coldkey, netuid), + SafeFloat::from(U64F64::from_num(amount)), + ); + AlphaDividendsPerSubnet::::insert(netuid, old_hotkey, AlphaBalance::from(amount)); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::do_swap_hotkey( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false, + ),); + + // Verify the swap + assert_eq!( + TotalHotkeyAlpha::::get(old_hotkey, netuid), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlpha::::get(new_hotkey, netuid), + AlphaBalance::from(amount) + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(old_hotkey, netuid), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(new_hotkey, netuid), + AlphaBalance::from(amount * 2) + ); + assert_eq!( + f64::from(TotalHotkeySharesV2::::get(old_hotkey, netuid)), + 0_f64 + ); + assert_abs_diff_eq!( + f64::from(TotalHotkeySharesV2::::get(new_hotkey, netuid)), + shares.to_num::(), + epsilon = 0.0000000001 + ); + assert_eq!( + f64::from(AlphaV2::::get((old_hotkey, coldkey, netuid))), + 0_f64 + ); + assert_eq!( + f64::from(AlphaV2::::get((new_hotkey, coldkey, netuid))), + amount as f64 ); assert_eq!( AlphaDividendsPerSubnet::::get(netuid, old_hotkey), @@ -2205,16 +2297,18 @@ fn test_revert_hotkey_swap_dividends() { ); assert_eq!( TotalHotkeyShares::::get(hk2, netuid), - U64F64::from_num(shares) + U64F64::from_num(0) ); + assert_eq!(TotalHotkeySharesV2::::get(hk2, netuid), shares.into()); assert_eq!( Alpha::::get((hk1, coldkey, netuid)), U64F64::from_num(0) ); assert_eq!( Alpha::::get((hk2, coldkey, netuid)), - U64F64::from_num(amount) + U64F64::from_num(0) ); + assert_eq!(AlphaV2::::get((hk2, coldkey, netuid)), amount.into()); assert_eq!( AlphaDividendsPerSubnet::::get(netuid, hk1), AlphaBalance::ZERO @@ -2261,8 +2355,13 @@ fn test_revert_hotkey_swap_dividends() { ); assert_eq!( TotalHotkeyShares::::get(hk1, netuid), - U64F64::from_num(shares), - "hk1 TotalHotkeyShares must be restored after revert" + U64F64::from_num(0), + "hk1 TotalHotkeyShares must be migrated to v2" + ); + assert_eq!( + TotalHotkeySharesV2::::get(hk1, netuid), + shares.into(), + "hk1 TotalHotkeyShares must be restored to v2 after revert" ); assert_eq!( Alpha::::get((hk2, coldkey, netuid)), @@ -2271,8 +2370,13 @@ fn test_revert_hotkey_swap_dividends() { ); assert_eq!( Alpha::::get((hk1, coldkey, netuid)), - U64F64::from_num(amount), - "hk1 Alpha must be restored after revert" + U64F64::from_num(0), + "hk1 Alpha must be migrated to v2" + ); + assert_eq!( + AlphaV2::::get((hk1, coldkey, netuid)), + amount.into(), + "hk1 Alpha must be restored to v2 after revert" ); assert_eq!( AlphaDividendsPerSubnet::::get(netuid, hk2), diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs index 9fbeefd3b6..1a1cd0156e 100644 --- a/pallets/swap-interface/src/lib.rs +++ b/pallets/swap-interface/src/lib.rs @@ -47,6 +47,7 @@ pub trait SwapHandler { fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult; fn toggle_user_liquidity(netuid: NetUid, enabled: bool); fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult; + fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoBalance) -> AlphaBalance; } pub trait DefaultPriceLimit diff --git a/pallets/swap/src/mock.rs b/pallets/swap/src/mock.rs index b12fd4d567..5063972082 100644 --- a/pallets/swap/src/mock.rs +++ b/pallets/swap/src/mock.rs @@ -289,9 +289,9 @@ impl BalanceOps for MockBalanceOps { _coldkey: &AccountId, _hotkey: &AccountId, _netuid: NetUid, - alpha: AlphaBalance, - ) -> Result { - Ok(alpha) + _alpha: AlphaBalance, + ) -> Result<(), DispatchError> { + Ok(()) } } diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index cc309216bb..0eda907567 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1157,4 +1157,23 @@ impl SwapHandler for Pallet { fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { Self::do_clear_protocol_liquidity(netuid) } + + /// Get the amount of Alpha that needs to be sold to get a given amount of Tao + fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoBalance) -> AlphaBalance { + match T::SubnetInfo::mechanism(netuid.into()) { + 1 => { + // For uniswap v3: Use no-slippage method. Amount is supposed to be small, + // hence we can neglect slippage and return slightly lower amount. + let alpha_price = Self::current_price(netuid.into()); + AlphaBalance::from( + U96F32::from(u64::from(tao_amount)) + .safe_div(alpha_price) + .saturating_to_num::(), + ) + } + + // Static subnet, alpha == tao + _ => u64::from(tao_amount).into(), + } + } } diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index 281467277e..f096c80210 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -533,12 +533,7 @@ mod pallet { let tao_provided = T::BalanceOps::decrease_balance(&coldkey, result.tao)?; ensure!(tao_provided == result.tao, Error::::InsufficientBalance); - let alpha_provided = - T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), result.alpha)?; - ensure!( - alpha_provided == result.alpha, - Error::::InsufficientBalance - ); + T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), result.alpha)?; // Emit an event Self::deposit_event(Event::LiquidityModified { diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index 211020cbba..e2b53c5142 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -2230,15 +2230,10 @@ fn liquidate_v3_refunds_user_funds_and_clears_state() { // Mirror extrinsic bookkeeping: withdraw funds & bump provided‑reserve counters. let tao_taken = ::BalanceOps::decrease_balance(&cold, need_tao.into()) .expect("decrease TAO"); - let alpha_taken = ::BalanceOps::decrease_stake( - &cold, - &hot, - netuid.into(), - need_alpha.into(), - ) - .expect("decrease ALPHA"); + ::BalanceOps::decrease_stake(&cold, &hot, netuid.into(), need_alpha.into()) + .expect("decrease ALPHA"); TaoReserve::increase_provided(netuid.into(), tao_taken); - AlphaReserve::increase_provided(netuid.into(), alpha_taken); + AlphaReserve::increase_provided(netuid.into(), need_alpha.into()); // Users‑only liquidation. assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2297,14 +2292,14 @@ fn refund_alpha_single_provider_exact() { let alpha_before_total = alpha_before_hot + alpha_before_owner; // --- Mimic extrinsic bookkeeping: withdraw α and record provided reserve. - let alpha_taken = ::BalanceOps::decrease_stake( + ::BalanceOps::decrease_stake( &cold, &hot, netuid.into(), alpha_needed.into(), ) .expect("decrease ALPHA"); - AlphaReserve::increase_provided(netuid.into(), alpha_taken); + AlphaReserve::increase_provided(netuid.into(), alpha_needed.into()); // --- Act: users‑only dissolve. assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2371,16 +2366,14 @@ fn refund_alpha_multiple_providers_proportional_to_principal() { let a2_before_owner = ::BalanceOps::alpha_balance(netuid.into(), &c2, &c2); let a2_before = a2_before_hot + a2_before_owner; - // Withdraw α and account reserves for each provider. - let a1_taken = - ::BalanceOps::decrease_stake(&c1, &h1, netuid.into(), a1.into()) - .expect("decrease α #1"); - AlphaReserve::increase_provided(netuid.into(), a1_taken); + // Withdraw alpha and account reserves for each provider. + ::BalanceOps::decrease_stake(&c1, &h1, netuid.into(), a1.into()) + .expect("decrease alpha #1"); + AlphaReserve::increase_provided(netuid.into(), a1.into()); - let a2_taken = - ::BalanceOps::decrease_stake(&c2, &h2, netuid.into(), a2.into()) - .expect("decrease α #2"); - AlphaReserve::increase_provided(netuid.into(), a2_taken); + ::BalanceOps::decrease_stake(&c2, &h2, netuid.into(), a2.into()) + .expect("decrease alpha #2"); + AlphaReserve::increase_provided(netuid.into(), a2.into()); // Act assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2433,16 +2426,14 @@ fn refund_alpha_same_cold_multiple_hotkeys_conserved_to_owner() { let before_owner = ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); let before_total = before_hot1 + before_hot2 + before_owner; - // Withdraw α from both hotkeys; track provided‑reserve. - let t1 = - ::BalanceOps::decrease_stake(&cold, &hot1, netuid.into(), a1.into()) - .expect("decr α #hot1"); - AlphaReserve::increase_provided(netuid.into(), t1); + // Withdraw alpha from both hotkeys; track provided‑reserve. + ::BalanceOps::decrease_stake(&cold, &hot1, netuid.into(), a1.into()) + .expect("decr alpha #hot1"); + AlphaReserve::increase_provided(netuid.into(), a1.into()); - let t2 = - ::BalanceOps::decrease_stake(&cold, &hot2, netuid.into(), a2.into()) - .expect("decr α #hot2"); - AlphaReserve::increase_provided(netuid.into(), t2); + ::BalanceOps::decrease_stake(&cold, &hot2, netuid.into(), a2.into()) + .expect("decr alpha #hot2"); + AlphaReserve::increase_provided(netuid.into(), a2.into()); // Act assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2524,7 +2515,7 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { // --- Mirror extrinsic bookkeeping: withdraw τ & α; bump provided reserves --- let tao_taken = ::BalanceOps::decrease_balance(&cold, tao_needed.into()) .expect("decrease TAO"); - let alpha_taken = ::BalanceOps::decrease_stake( + ::BalanceOps::decrease_stake( &cold, &hot, netuid.into(), @@ -2533,7 +2524,7 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { .expect("decrease ALPHA"); TaoReserve::increase_provided(netuid.into(), tao_taken); - AlphaReserve::increase_provided(netuid.into(), alpha_taken); + AlphaReserve::increase_provided(netuid.into(), alpha_needed.into()); // --- Act: dissolve (GREEN PATH: permitted validators exist) --- assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); diff --git a/pallets/transaction-fee/Cargo.toml b/pallets/transaction-fee/Cargo.toml index 2180732677..d5a5c2f418 100644 --- a/pallets/transaction-fee/Cargo.toml +++ b/pallets/transaction-fee/Cargo.toml @@ -25,6 +25,7 @@ pallet-preimage = { workspace = true, default-features = false, optional = true pallet-scheduler = { workspace = true, default-features = false, optional = true } [dev-dependencies] +approx.workspace = true frame-executive.workspace = true pallet-evm-chain-id.workspace = true scale-info.workspace = true diff --git a/pallets/transaction-fee/src/lib.rs b/pallets/transaction-fee/src/lib.rs index 53e6cb816b..ec9eadf773 100644 --- a/pallets/transaction-fee/src/lib.rs +++ b/pallets/transaction-fee/src/lib.rs @@ -29,9 +29,9 @@ use subtensor_swap_interface::SwapHandler; // Misc use core::marker::PhantomData; use smallvec::smallvec; +use sp_runtime::traits::SaturatedConversion; use sp_std::vec::Vec; -use substrate_fixed::types::U96F32; -use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoBalance, Token}; +use subtensor_runtime_common::{AlphaBalance, AuthorshipInfo, NetUid, TaoBalance}; // Tests #[cfg(test)] @@ -67,7 +67,7 @@ pub trait AlphaFeeHandler { coldkey: &AccountIdOf, alpha_vec: &[(AccountIdOf, NetUid)], tao_amount: TaoBalance, - ); + ) -> (AlphaBalance, TaoBalance); fn get_all_netuids_for_coldkey_and_hotkey( coldkey: &AccountIdOf, hotkey: &AccountIdOf, @@ -99,12 +99,7 @@ where { fn on_nonzero_unbalanced(imbalance: BalancesImbalanceOf) { if let Some(author) = T::author() { - // Pay block author instead of burning. - // One of these is the right call depending on your exact fungible API: - // let _ = pallet_balances::Pallet::::resolve(&author, imbalance); - // or: let _ = pallet_balances::Pallet::::deposit(&author, imbalance.peek(), Precision::BestEffort); - // - // Prefer "resolve" (moves the actual imbalance) if available: + // Pay block author let _ = as Balanced<_>>::resolve(&author, imbalance); } else { // Fallback: if no author, burn (or just drop). @@ -121,9 +116,9 @@ where T: pallet_subtensor_swap::Config, { /// This function checks if tao_amount fee can be withdraw in Alpha currency - /// by converting Alpha to TAO at the current price and ignoring slippage. + /// by converting Alpha to TAO using the current pool conditions. /// - /// If this function returns true, the transaction will be included in the block + /// If this function returns true, the transaction will be added to the mempool /// and Alpha will be withdraw from the account, no matter whether transaction /// is successful or not. /// @@ -135,64 +130,67 @@ where alpha_vec: &[(AccountIdOf, NetUid)], tao_amount: TaoBalance, ) -> bool { - if alpha_vec.is_empty() { - // Alpha vector is empty, nothing to withdraw + if alpha_vec.len() != 1 { + // Multi-subnet alpha fee deduction is prohibited. return false; } - // Divide tao_amount among all alpha entries - let tao_per_entry = tao_amount - .checked_div(&TaoBalance::from(alpha_vec.len())) - .unwrap_or(TaoBalance::ZERO); - - // The rule here is that we should be able to withdraw at least from one entry. - // This is not ideal because it may not pay all fees, but UX is the priority - // and this approach still provides spam protection. - alpha_vec.iter().any(|(hotkey, netuid)| { - let alpha_balance = U96F32::saturating_from_num( + if let Some((hotkey, netuid)) = alpha_vec.first() { + let alpha_balance = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, *netuid, - ), + ); + let alpha_fee = pallet_subtensor_swap::Pallet::::get_alpha_amount_for_tao( + *netuid, + tao_amount.into(), ); - let alpha_price = pallet_subtensor_swap::Pallet::::current_alpha_price(*netuid); - alpha_price.saturating_mul(alpha_balance) >= u64::from(tao_per_entry) - }) + alpha_balance >= alpha_fee + } else { + false + } } fn withdraw_in_alpha( coldkey: &AccountIdOf, alpha_vec: &[(AccountIdOf, NetUid)], tao_amount: TaoBalance, - ) { - if alpha_vec.is_empty() { - return; + ) -> (AlphaBalance, TaoBalance) { + if alpha_vec.len() != 1 { + return (0.into(), 0.into()); } - let tao_per_entry = tao_amount - .checked_div(&TaoBalance::from(alpha_vec.len())) - .unwrap_or(TaoBalance::ZERO); - - alpha_vec.iter().for_each(|(hotkey, netuid)| { - // Divide tao_amount evenly among all alpha entries - let alpha_balance = U96F32::saturating_from_num( + if let Some((hotkey, netuid)) = alpha_vec.first() { + let alpha_balance = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, *netuid, - ), + ); + let mut alpha_equivalent = pallet_subtensor_swap::Pallet::::get_alpha_amount_for_tao( + *netuid, + tao_amount.into(), ); - let alpha_price = pallet_subtensor_swap::Pallet::::current_alpha_price(*netuid); - let alpha_fee = U96F32::saturating_from_num(tao_per_entry) - .checked_div(alpha_price) - .unwrap_or(alpha_balance) - .min(alpha_balance) - .saturating_to_num::(); - - pallet_subtensor::Pallet::::decrease_stake_for_hotkey_and_coldkey_on_subnet( + if alpha_equivalent.is_zero() { + alpha_equivalent = alpha_balance; + } + let alpha_fee = alpha_equivalent.min(alpha_balance); + + // Sell alpha_fee and burn received tao (ignore unstake_from_subnet return). + let swap_result = pallet_subtensor::Pallet::::unstake_from_subnet( hotkey, coldkey, *netuid, - alpha_fee.into(), + alpha_fee, + 0.into(), + true, ); - }); + + if let Ok(tao_amount) = swap_result { + (alpha_fee, tao_amount) + } else { + (0.into(), 0.into()) + } + } else { + (0.into(), 0.into()) + } } fn get_all_netuids_for_coldkey_and_hotkey( @@ -211,11 +209,13 @@ where } } -/// Enum that describes either a withdrawn amount of transaction fee in TAO or the -/// fact that fee was charged in Alpha (without an amount because it is not needed) +/// Enum that describes either a withdrawn amount of transaction fee in TAO or +/// the exact charged Alpha amount. pub enum WithdrawnFee>> { + // Contains withdrawn TAO amount Tao(Credit, F>), - Alpha, + // Contains withdrawn Alpha amount and resulting swapped TAO + Alpha((AlphaBalance, TaoBalance)), } /// Custom OnChargeTransaction implementation based on standard FungibleAdapter from transaction_payment @@ -231,7 +231,7 @@ impl SubtensorTxFeeHandler { /// distributed evenly between subnets in case of multiple subnets. pub fn fees_in_alpha(who: &AccountIdOf, call: &CallOf) -> Vec<(AccountIdOf, NetUid)> where - T: frame_system::Config + pallet_subtensor::Config, + T: frame_system::Config + pallet_subtensor::Config + AuthorshipInfo>, CallOf: IsSubType>, OU: AlphaFeeHandler, { @@ -303,18 +303,18 @@ impl SubtensorTxFeeHandler { impl OnChargeTransaction for SubtensorTxFeeHandler where - T: PTPConfig + pallet_subtensor::Config, + T: PTPConfig + pallet_subtensor::Config + AuthorshipInfo>, CallOf: IsSubType>, F: Balanced, OU: OnUnbalanced> + AlphaFeeHandler, - >>::Balance: Into, + >>::Balance: Into + From, { type LiquidityInfo = Option>; type Balance = ::AccountId>>::Balance; fn withdraw_fee( who: &AccountIdOf, - _call: &CallOf, + call: &CallOf, _dispatch_info: &DispatchInfoOf>, fee: Self::Balance, _tip: Self::Balance, @@ -333,12 +333,13 @@ where ) { Ok(imbalance) => Ok(Some(WithdrawnFee::Tao(imbalance))), Err(_) => { - // let alpha_vec = Self::fees_in_alpha::(who, call); - // if !alpha_vec.is_empty() { - // let fee_u64: u64 = fee.into(); - // OU::withdraw_in_alpha(who, &alpha_vec, fee_u64); - // return Ok(Some(WithdrawnFee::Alpha)); - // } + let alpha_vec = Self::fees_in_alpha::(who, call); + if !alpha_vec.is_empty() { + let fee_u64: u64 = fee.saturated_into::(); + let (alpha_fee, tao_amount) = + OU::withdraw_in_alpha(who, &alpha_vec, fee_u64.into()); + return Ok(Some(WithdrawnFee::Alpha((alpha_fee, tao_amount)))); + } Err(InvalidTransaction::Payment.into()) } } @@ -346,7 +347,7 @@ where fn can_withdraw_fee( who: &AccountIdOf, - _call: &CallOf, + call: &CallOf, _dispatch_info: &DispatchInfoOf>, fee: Self::Balance, _tip: Self::Balance, @@ -359,14 +360,14 @@ where match F::can_withdraw(who, fee) { WithdrawConsequence::Success => Ok(()), _ => { - // // Fallback to fees in Alpha if possible - // let alpha_vec = Self::fees_in_alpha::(who, call); - // if !alpha_vec.is_empty() { - // let fee_u64: u64 = fee.into(); - // if OU::can_withdraw_in_alpha(who, &alpha_vec, fee_u64) { - // return Ok(()); - // } - // } + // Fallback to fees in Alpha if possible + let alpha_vec = Self::fees_in_alpha::(who, call); + if !alpha_vec.is_empty() { + let fee_u64: u64 = fee.saturated_into::(); + if OU::can_withdraw_in_alpha(who, &alpha_vec, fee_u64.into()) { + return Ok(()); + } + } Err(InvalidTransaction::Payment.into()) } } @@ -404,7 +405,21 @@ where let (tip, fee) = adjusted_paid.split(tip); OU::on_unbalanceds(Some(fee).into_iter().chain(Some(tip))); } - WithdrawnFee::Alpha => { + WithdrawnFee::Alpha((alpha_fee, tao_amount)) => { + if let Some(author) = T::author() { + // Pay block author + let _ = F::deposit(&author, tao_amount.into(), Precision::BestEffort) + .unwrap_or_else(|_| Debt::::zero()); + } else { + // Fallback: no author => do nothing + } + frame_system::Pallet::::deposit_event( + pallet_subtensor::Event::::TransactionFeePaidWithAlpha { + who: who.clone(), + alpha_fee, + tao_amount, + }, + ); // Subtensor does not refund Alpha fees, charges are final } } diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index f0ad323168..1b4eac0706 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -413,8 +413,8 @@ impl pallet_subtensor_swap::Config for Test { type SubnetInfo = SubtensorModule; type BalanceOps = SubtensorModule; type ProtocolId = SwapProtocolId; - type TaoReserve = pallet_subtensor::TaoCurrencyReserve; - type AlphaReserve = pallet_subtensor::AlphaCurrencyReserve; + type TaoReserve = pallet_subtensor::TaoBalanceReserve; + type AlphaReserve = pallet_subtensor::AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; diff --git a/pallets/transaction-fee/src/tests/mod.rs b/pallets/transaction-fee/src/tests/mod.rs index 5dd353dcde..c7c39eb030 100644 --- a/pallets/transaction-fee/src/tests/mod.rs +++ b/pallets/transaction-fee/src/tests/mod.rs @@ -1,8 +1,9 @@ -#![allow(clippy::indexing_slicing, clippy::unwrap_used)] -use crate::TransactionSource; -use frame_support::assert_ok; +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +use crate::{AlphaFeeHandler, SubtensorTxFeeHandler, TransactionFeeHandler, TransactionSource}; +use approx::assert_abs_diff_eq; use frame_support::dispatch::GetDispatchInfo; use frame_support::pallet_prelude::Zero; +use frame_support::{assert_err, assert_ok}; use pallet_subtensor_swap::AlphaSqrtPrice; use sp_runtime::{ traits::{DispatchTransaction, TransactionExtension, TxBaseImplication}, @@ -71,12 +72,99 @@ fn test_remove_stake_fees_tao() { // Remove stake extrinsic should pay fees in TAO because ck has sufficient TAO balance assert!(actual_tao_fee > 0.into()); assert_eq!(actual_alpha_fee, AlphaBalance::from(0)); + + let events = System::events(); + assert!(events.iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::TransactionPayment( + pallet_transaction_payment::Event::TransactionFeePaid { .. } + ) + ) + })); + assert!(!events.iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::SubtensorModule(SubtensorEvent::TransactionFeePaidWithAlpha { .. }) + ) + })); + }); +} + +// cargo test --package subtensor-transaction-fee --lib -- tests::test_rejects_multi_subnet_alpha_fee_deduction --exact --show-output +#[test] +fn test_rejects_multi_subnet_alpha_fee_deduction() { + new_test_ext().execute_with(|| { + let sn = setup_subnets(2, 1); + let stake_amount = TAO; + setup_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + setup_stake( + sn.subnets[1].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + let alpha_before_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let alpha_before_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[1].netuid, + ); + + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::unstake_all { + hotkey: sn.hotkeys[0], + }); + let alpha_vec = + SubtensorTxFeeHandler::>::fees_in_alpha::( + &sn.coldkey, + &call, + ); + assert_eq!(alpha_vec.len(), 2); + + assert!( + ! as AlphaFeeHandler>::can_withdraw_in_alpha( + &sn.coldkey, + &alpha_vec, + 1.into(), + ) + ); + assert_eq!( + as AlphaFeeHandler>::withdraw_in_alpha( + &sn.coldkey, + &alpha_vec, + 1.into(), + ), + (0.into(), 0.into()) + ); + + let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let alpha_after_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[1].netuid, + ); + + assert_eq!(alpha_before_0, alpha_after_0); + assert_eq!(alpha_before_1, alpha_after_1); }); } // cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_fees_alpha --exact --show-output #[test] -#[ignore] fn test_remove_stake_fees_alpha() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -90,7 +178,7 @@ fn test_remove_stake_fees_alpha() { ); // Simulate stake removal to get how much TAO should we get for unstaked Alpha - let (expected_unstaked_tao, _swap_fee) = + let (expected_unstaked_tao, swap_fee) = mock::swap_alpha_to_tao(sn.subnets[0].netuid, unstake_amount); // Forse-set signer balance to ED @@ -100,6 +188,10 @@ fn test_remove_stake_fees_alpha() { current_balance - ExistentialDeposit::get(), ); + // Get the block builder balance + let block_builder = U256::from(MOCK_BLOCK_BUILDER); + let block_builder_balance_before = Balances::free_balance(block_builder); + // Remove stake let balance_before = Balances::free_balance(sn.coldkey); let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -113,6 +205,8 @@ fn test_remove_stake_fees_alpha() { amount_unstaked: unstake_amount, }); + System::reset_events(); + // Dispatch the extrinsic with ChargeTransactionPayment extension let info = call.get_dispatch_info(); let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); @@ -136,8 +230,51 @@ fn test_remove_stake_fees_alpha() { let actual_alpha_fee = alpha_before - alpha_after - unstake_amount; // Remove stake extrinsic should pay fees in Alpha - assert_eq!(actual_tao_fee, 0.into()); + assert_abs_diff_eq!(actual_tao_fee, 0.into(), epsilon = 10.into()); assert!(actual_alpha_fee > 0.into()); + + // Assert that swapped TAO from alpha fee goes to block author + let block_builder_fee_portion = 1.; + let expected_block_builder_swap_reward = swap_fee as f64 * block_builder_fee_portion; + let expected_tx_fee = 14000.; // Use very low value (0.000014) for less test flakiness, value before we 10x tx fees + let block_builder_balance_after = Balances::free_balance(block_builder); + let actual_block_builder_reward = + block_builder_balance_after - block_builder_balance_before; + assert!( + u64::from(actual_block_builder_reward) as f64 + >= expected_block_builder_swap_reward + expected_tx_fee + ); + + let events = System::events(); + let alpha_event = events + .iter() + .position(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::SubtensorModule(SubtensorEvent::TransactionFeePaidWithAlpha { + who, + alpha_fee, + tao_amount: _, + }) if who == &sn.coldkey && *alpha_fee == actual_alpha_fee + ) + }) + .expect("expected TransactionFeePaidWithAlpha event"); + let tao_event = events + .iter() + .position(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::TransactionPayment( + pallet_transaction_payment::Event::TransactionFeePaid { who, .. } + ) if who == &sn.coldkey + ) + }) + .expect("expected TransactionFeePaid event"); + + assert!( + alpha_event < tao_event, + "expected TransactionFeePaidWithAlpha before TransactionFeePaid" + ); }); } @@ -146,7 +283,6 @@ fn test_remove_stake_fees_alpha() { // // cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_root --exact --show-output #[test] -#[ignore] fn test_remove_stake_root() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -205,7 +341,6 @@ fn test_remove_stake_root() { // // cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_completely_root --exact --show-output #[test] -#[ignore] fn test_remove_stake_completely_root() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -257,7 +392,6 @@ fn test_remove_stake_completely_root() { // cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_completely_fees_alpha --exact --show-output #[test] -#[ignore] fn test_remove_stake_completely_fees_alpha() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -388,7 +522,6 @@ fn test_remove_stake_not_enough_balance_for_fees() { // // cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_edge_alpha --exact --show-output #[test] -#[ignore] fn test_remove_stake_edge_alpha() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -523,11 +656,11 @@ fn test_remove_stake_failing_transaction_tao_fees() { }); } -// Validation passes, but transaction fails => Alpha fees are paid +// Validation passes, but transaction fails (artificially disable subtoken) => +// Alpha fees are still paid // // cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_failing_transaction_alpha_fees --exact --show-output #[test] -#[ignore] fn test_remove_stake_failing_transaction_alpha_fees() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -540,8 +673,11 @@ fn test_remove_stake_failing_transaction_alpha_fees() { stake_amount, ); - // Make unstaking fail by reducing liquidity to critical - SubnetTAO::::insert(sn.subnets[0].netuid, TaoBalance::from(1)); + // Provide adequate TAO reserve so that sim swap works ok in validation + SubnetTAO::::insert(sn.subnets[0].netuid, TaoBalance::from(1_000_000_000_u64)); + + // Provide Alpha reserve so that price is about 1.0 + SubnetAlphaIn::::insert(sn.subnets[0].netuid, AlphaBalance::from(1_000_000_000_u64)); // Forse-set signer balance to ED let current_balance = Balances::free_balance(sn.coldkey); @@ -550,6 +686,9 @@ fn test_remove_stake_failing_transaction_alpha_fees() { current_balance - ExistentialDeposit::get(), ); + // Disable subtoken so that removing stake tx fails (still allows the validation to pass) + pallet_subtensor::SubtokenEnabled::::insert(sn.subnets[0].netuid, false); + // Remove stake let balance_before = Balances::free_balance(sn.coldkey); let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -560,7 +699,7 @@ fn test_remove_stake_failing_transaction_alpha_fees() { let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { hotkey: sn.hotkeys[0], netuid: sn.subnets[0].netuid, - amount_unstaked: unstake_amount, + amount_unstaked: alpha_before, }); // Dispatch the extrinsic with ChargeTransactionPayment extension @@ -593,7 +732,6 @@ fn test_remove_stake_failing_transaction_alpha_fees() { // cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_limit_fees_alpha --exact --show-output #[test] -#[ignore] fn test_remove_stake_limit_fees_alpha() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -607,8 +745,12 @@ fn test_remove_stake_limit_fees_alpha() { ); // Simulate stake removal to get how much TAO should we get for unstaked Alpha - let (expected_unstaked_tao, _swap_fee) = - mock::swap_alpha_to_tao(sn.subnets[0].netuid, unstake_amount); + let alpha_fee = AlphaBalance::from(24229); // This is measured alpha fee that matches the withdrawn tx fee + let (expected_burned_tao_fees, _swap_fee) = + mock::swap_alpha_to_tao(sn.subnets[0].netuid, alpha_fee); + let (expected_unstaked_tao_plus_fees, _swap_fee) = + mock::swap_alpha_to_tao(sn.subnets[0].netuid, unstake_amount + alpha_fee); + let expected_unstaked_tao = expected_unstaked_tao_plus_fees - expected_burned_tao_fees; // Forse-set signer balance to ED let current_balance = Balances::free_balance(sn.coldkey); @@ -654,14 +796,13 @@ fn test_remove_stake_limit_fees_alpha() { let actual_alpha_fee = alpha_before - alpha_after - unstake_amount; // Remove stake extrinsic should pay fees in Alpha - assert_eq!(actual_tao_fee, 0.into()); + assert_abs_diff_eq!(actual_tao_fee, 0.into(), epsilon = 100.into()); assert!(actual_alpha_fee > 0.into()); }); } // cargo test --package subtensor-transaction-fee --lib -- tests::test_unstake_all_fees_alpha --exact --show-output #[test] -#[ignore] fn test_unstake_all_fees_alpha() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -703,8 +844,22 @@ fn test_unstake_all_fees_alpha() { }); // Dispatch the extrinsic with ChargeTransactionPayment extension + // Get invalid payment because we cannot pay fees in multiple alphas let info = call.get_dispatch_info(); let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_err!( + ext.clone().dispatch_transaction( + RuntimeOrigin::signed(coldkey).into(), + call.clone(), + &info, + 0, + 0, + ), + TransactionValidityError::Invalid(InvalidTransaction::Payment), + ); + + // Give the coldkey TAO balance - now should unstake ok + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1_000_000_000_u64.into()); assert_ok!(ext.dispatch_transaction( RuntimeOrigin::signed(coldkey).into(), call, @@ -734,7 +889,6 @@ fn test_unstake_all_fees_alpha() { // cargo test --package subtensor-transaction-fee --lib -- tests::test_unstake_all_alpha_fees_alpha --exact --show-output #[test] -#[ignore] fn test_unstake_all_alpha_fees_alpha() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -771,8 +925,22 @@ fn test_unstake_all_alpha_fees_alpha() { }); // Dispatch the extrinsic with ChargeTransactionPayment extension + // Get invalid payment because we cannot pay fees in multiple alphas let info = call.get_dispatch_info(); let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_err!( + ext.clone().dispatch_transaction( + RuntimeOrigin::signed(coldkey).into(), + call.clone(), + &info, + 0, + 0, + ), + TransactionValidityError::Invalid(InvalidTransaction::Payment), + ); + + // Give the coldkey TAO balance - now should unstake ok + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1_000_000_000_u64.into()); assert_ok!(ext.dispatch_transaction( RuntimeOrigin::signed(coldkey).into(), call, @@ -802,7 +970,6 @@ fn test_unstake_all_alpha_fees_alpha() { // cargo test --package subtensor-transaction-fee --lib -- tests::test_move_stake_fees_alpha --exact --show-output #[test] -#[ignore] fn test_move_stake_fees_alpha() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -874,7 +1041,6 @@ fn test_move_stake_fees_alpha() { // cargo test --package subtensor-transaction-fee --lib -- tests::test_transfer_stake_fees_alpha --exact --show-output #[test] -#[ignore] fn test_transfer_stake_fees_alpha() { new_test_ext().execute_with(|| { let destination_coldkey = U256::from(100000); @@ -947,7 +1113,6 @@ fn test_transfer_stake_fees_alpha() { // cargo test --package subtensor-transaction-fee --lib -- tests::test_swap_stake_fees_alpha --exact --show-output #[test] -#[ignore] fn test_swap_stake_fees_alpha() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -1018,7 +1183,6 @@ fn test_swap_stake_fees_alpha() { // cargo test --package subtensor-transaction-fee --lib -- tests::test_swap_stake_limit_fees_alpha --exact --show-output #[test] -#[ignore] fn test_swap_stake_limit_fees_alpha() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -1091,7 +1255,6 @@ fn test_swap_stake_limit_fees_alpha() { // cargo test --package subtensor-transaction-fee --lib -- tests::test_burn_alpha_fees_alpha --exact --show-output #[test] -#[ignore] fn test_burn_alpha_fees_alpha() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -1153,7 +1316,6 @@ fn test_burn_alpha_fees_alpha() { // cargo test --package subtensor-transaction-fee --lib -- tests::test_recycle_alpha_fees_alpha --exact --show-output #[test] -#[ignore] fn test_recycle_alpha_fees_alpha() { new_test_ext().execute_with(|| { let stake_amount = TAO; @@ -1258,7 +1420,7 @@ fn test_add_stake_fees_go_to_block_builder() { // Expect that block builder balance has increased by both the swap fee and the transaction fee let expected_block_builder_swap_reward = swap_fee as f64 * block_builder_fee_portion; - let expected_tx_fee = 0.000136; // Use very low value for less test flakiness + let expected_tx_fee = 14000.; // Use very low value (0.000014) for less test flakiness, value before we 10x tx fees let block_builder_balance_after = Balances::free_balance(block_builder); let actual_reward = block_builder_balance_after - block_builder_balance_before; assert!( diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index 93bc9be3a7..8ec79b4092 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -315,7 +315,8 @@ where let hotkey = R::AccountId::from(hotkey.0); let mut coldkeys: Vec = vec![]; let netuid = NetUid::from(try_u16_from_u256(netuid)?); - for ((coldkey, netuid_in_alpha), _) in pallet_subtensor::Alpha::::iter_prefix((hotkey,)) + for (coldkey, netuid_in_alpha, _) in + pallet_subtensor::Pallet::::alpha_iter_single_prefix(&hotkey) { if netuid == netuid_in_alpha { let key: [u8; 32] = coldkey.into(); diff --git a/primitives/share-pool/Cargo.toml b/primitives/share-pool/Cargo.toml index ba42b0d77d..9282e3cfa6 100644 --- a/primitives/share-pool/Cargo.toml +++ b/primitives/share-pool/Cargo.toml @@ -4,13 +4,34 @@ version = "0.1.0" edition.workspace = true [dependencies] +approx.workspace = true +codec.workspace = true +log.workspace = true +scale-info.workspace = true substrate-fixed.workspace = true +subtensor-macros.workspace = true +sp-core.workspace = true sp-std.workspace = true +num-traits.workspace = true safe-math.workspace = true +[dev-dependencies] +rand = { version = "0.8", features = ["std", "std_rng"] } +rayon = "1.10" + [lints] workspace = true [features] default = ["std"] -std = ["substrate-fixed/std", "sp-std/std", "safe-math/std"] +std = [ + "codec/std", + "log/std", + "rand/std", + "scale-info/std", + "substrate-fixed/std", + "sp-core/std", + "sp-std/std", + "num-traits/std", + "safe-math/std", +] diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index d43f36259c..848ee64448 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -1,26 +1,397 @@ #![cfg_attr(not(feature = "std"), no_std)] -#![allow(clippy::result_unit_err)] +#![allow(clippy::result_unit_err, clippy::indexing_slicing)] -use safe_math::*; +use codec::{Decode, Encode}; +#[cfg(not(feature = "std"))] +use num_traits::float::FloatCore as _; +use scale_info::TypeInfo; +use sp_core::U256; use sp_std::marker; use sp_std::ops::Neg; -use substrate_fixed::types::{I64F64, U64F64}; +use substrate_fixed::types::U64F64; +use subtensor_macros::freeze_struct; + +// Maximum mantissa that can be used with SafeFloat +pub const SAFE_FLOAT_MAX: u128 = 1_000_000_000_000_000_000_000_u128; +pub const SAFE_FLOAT_MAX_EXP: i64 = 21_i64; + +/// Controlled precision floating point number with efficient storage +/// +/// Precision is controlled in a way that keeps enough mantissa digits so +/// that updating hotkey stake by 1 rao makes difference in the resulting shared +/// pool variables (both coldkey share and share pool denominator), but also +/// precision should be limited so that updating by 0.1 rao does not make the +/// difference (because there's no such thing as 0.1 rao, rao is integer). +#[freeze_struct("9a55fbe2d60efb41")] +#[derive(Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct SafeFloat { + mantissa: u128, + exponent: i64, +} + +/// Capped power of 10 in U256 +/// Cap at 10^SAFE_FLOAT_MAX_EXP+1, we don't need greater powers here +fn cappow10(e: u64) -> U256 { + if e > (SAFE_FLOAT_MAX_EXP as u64).saturating_add(1) { + return U256::from(SAFE_FLOAT_MAX.saturating_mul(10)); + } + if e == 0 { + return U256::from(1); + } + U256::from(10) + .checked_pow(U256::from(e)) + .unwrap_or_default() +} + +impl SafeFloat { + pub fn zero() -> Self { + SafeFloat { + mantissa: 0_u128, + exponent: 0_i64, + } + } + + pub fn new(mantissa: u128, exponent: i64) -> Option { + // Cap mantissa at SAFE_FLOAT_MAX + if mantissa > SAFE_FLOAT_MAX { + return None; + } + + let mut safe_float = SafeFloat::zero(); + + if safe_float.normalize(&U256::from(mantissa), exponent) { + Some(safe_float) + } else { + None + } + } + + /// Sets the new mantissa and exponent adjustsing mantissa and exponent so that + /// SAFE_FLOAT_MAX / 10 < mantissa <= SAFE_FLOAT_MAX + /// + /// Returns true in case of success or false if exponent over- or underflows + pub(crate) fn normalize(&mut self, new_mantissa: &U256, new_exponent: i64) -> bool { + if new_mantissa.is_zero() { + self.mantissa = 0; + self.exponent = 0; + return true; + } + + let ten = U256::from(10); + let max_mantissa = U256::from(SAFE_FLOAT_MAX); + let min_mantissa = U256::from(SAFE_FLOAT_MAX) + .checked_div(ten) + .unwrap_or_default(); + + // Loops are safe because they are bounded by U256 size and result + // in no more than 78 iterations together + let mut normalized_mantissa = *new_mantissa; + let mut normalized_exponent = new_exponent; + + while normalized_mantissa > max_mantissa { + let Some(next_mantissa) = normalized_mantissa.checked_div(ten) else { + return false; + }; + let Some(next_exponent) = normalized_exponent.checked_add(1) else { + return false; + }; + + normalized_mantissa = next_mantissa; + normalized_exponent = next_exponent; + } + + while normalized_mantissa <= min_mantissa { + let Some(next_mantissa) = normalized_mantissa.checked_mul(ten) else { + return false; + }; + let Some(next_exponent) = normalized_exponent.checked_sub(1) else { + return false; + }; + + normalized_mantissa = next_mantissa; + normalized_exponent = next_exponent; + } + + self.mantissa = normalized_mantissa.low_u128(); + self.exponent = normalized_exponent; + + true + } + + /// Divide current value by a preserving precision (SAFE_FLOAT_MAX digits in mantissa) + /// result = m1 * 10^e1 / m2 * 10^e2 + pub fn div(&self, a: &SafeFloat) -> Option { + // - In m1 / m2 division we need enough digits for a u128. + // This can be calculated in a lossless way in U256 as m1 * MAX_MANTISSA / m2 + // - The new exponent is e1 - e2 - SAFE_FLOAT_MAX_EXP + let maybe_m1_scaled_u256 = + U256::from(self.mantissa).checked_mul(U256::from(SAFE_FLOAT_MAX)); + let m2_u256 = U256::from(a.mantissa); + + // Calculate new exponent + let new_exponent_i128 = (self.exponent as i128) + .saturating_sub(a.exponent as i128) + .saturating_sub(SAFE_FLOAT_MAX_EXP as i128); + if (new_exponent_i128 > i64::MAX as i128) || (new_exponent_i128 < i64::MIN as i128) { + return None; + } + let new_exponent = new_exponent_i128 as i64; + + // Calcuate new mantissa, normalize, and return result + if let Some(m1_scaled_u256) = maybe_m1_scaled_u256 { + let maybe_new_mantissa_u256 = m1_scaled_u256.checked_div(m2_u256); + if let Some(new_mantissa_u256) = maybe_new_mantissa_u256 { + let mut safe_float = SafeFloat::zero(); + if safe_float.normalize(&new_mantissa_u256, new_exponent) { + Some(safe_float) + } else { + None + } + } else { + None + } + } else { + None + } + } + + pub fn add(&self, a: &SafeFloat) -> Option { + if self.is_zero() { + return Some(a.clone()); + } + if a.is_zero() { + return Some(self.clone()); + } + + let (new_mantissa, new_exponent) = if self.exponent >= a.exponent { + let exp_diff = self.exponent.saturating_sub(a.exponent); + let m1 = U256::from(self.mantissa); + let m2 = U256::from(a.mantissa) + .checked_div(cappow10(exp_diff as u64)) + .unwrap_or_default(); + (m1.saturating_add(m2), self.exponent) + } else { + let exp_diff = a.exponent.saturating_sub(self.exponent); + let m1 = U256::from(self.mantissa) + .checked_div(cappow10(exp_diff as u64)) + .unwrap_or_default(); + let m2 = U256::from(a.mantissa); + (m1.saturating_add(m2), a.exponent) + }; + + let mut safe_float = SafeFloat::zero(); + if safe_float.normalize(&new_mantissa, new_exponent) { + Some(safe_float) + } else { + None + } + } + + pub fn sub(&self, a: &SafeFloat) -> Option { + if self.is_zero() && a.is_zero() { + return Some(Self::zero()); + } else if self.is_zero() { + return None; + } + if a.is_zero() { + return Some(self.clone()); + } + + let (new_mantissa, new_exponent) = if self.exponent >= a.exponent { + let exp_diff = self.exponent.saturating_sub(a.exponent); + let m1 = U256::from(self.mantissa); + let m2 = U256::from(a.mantissa) + .checked_div(cappow10(exp_diff as u64)) + .unwrap_or_default(); + (m1.saturating_sub(m2), self.exponent) + } else { + let exp_diff = a.exponent.saturating_sub(self.exponent); + let m1 = U256::from(self.mantissa) + .checked_div(cappow10(exp_diff as u64)) + .unwrap_or_default(); + let m2 = U256::from(a.mantissa); + (m1.saturating_sub(m2), a.exponent) + }; + + let mut safe_float = SafeFloat::zero(); + if safe_float.normalize(&new_mantissa, new_exponent) { + Some(safe_float) + } else { + None + } + } + + /// Calculate self * a / b without loss of precision + pub fn mul_div(&self, a: &SafeFloat, b: &SafeFloat) -> Option { + if b.mantissa == 0_u128 { + return None; + } + + // No overflows here, just unwrap or default + let self_a_mantissa_u256 = U256::from(self.mantissa) + .checked_mul(U256::from(a.mantissa)) + .unwrap_or_default(); + let maybe_self_a_exponent = self.exponent.checked_add(a.exponent); + + if let Some(self_a_exponent) = maybe_self_a_exponent { + // Divide by b in U256 + let maybe_new_exponent = self_a_exponent.checked_sub(b.exponent); + if let Some(new_exponent) = maybe_new_exponent { + let new_mantissa = self_a_mantissa_u256 + .checked_div(U256::from(b.mantissa)) + .unwrap_or_default(); + let mut result = SafeFloat::zero(); + if result.normalize(&new_mantissa, new_exponent) { + Some(result) + } else { + None + } + } else { + None + } + } else { + None + } + } + + pub fn is_zero(&self) -> bool { + self.mantissa == 0u128 + } + + /// Returns true if self > a + /// Both values should be normalized + pub fn gt(&self, a: &SafeFloat) -> bool { + let ten = U256::from(10); + + if self.exponent == a.exponent { + self.mantissa > a.mantissa + } else if self.exponent > a.exponent { + let exp_diff = self.exponent.saturating_sub(a.exponent); + if exp_diff > 1_i64 { + true + } else { + ten.saturating_mul(U256::from(self.mantissa)) > U256::from(a.mantissa) + } + } else { + let exp_diff = a.exponent.saturating_sub(self.exponent); + if exp_diff > 1_i64 { + false + } else { + U256::from(self.mantissa) > ten.saturating_mul(U256::from(a.mantissa)) + } + } + } +} + +// Saturating conversion: negatives -> 0, overflow -> u64::MAX +impl From<&SafeFloat> for u64 { + fn from(value: &SafeFloat) -> Self { + // If exponent is zero, it's just an integer mantissa + if value.exponent == 0 { + return u64::try_from(value.mantissa).unwrap_or(u64::MAX); + } + + // scale = 10^exponent + let scale = cappow10(value.exponent.unsigned_abs()); + + // mantissa * 10^exponent + let q: U256 = if value.exponent > 0 { + U256::from(value.mantissa).saturating_mul(scale) + } else { + U256::from(value.mantissa) + .checked_div(scale) + .unwrap_or_default() + }; + + // Convert quotient to u64, saturating on overflow + if q.is_zero() { + 0 + } else { + q.try_into().unwrap_or(u64::MAX) + } + } +} + +// Convenience impl for owning values +impl From for u64 { + fn from(value: SafeFloat) -> Self { + u64::from(&value) + } +} + +impl From for SafeFloat { + fn from(value: u64) -> Self { + SafeFloat::new(value as u128, 0).unwrap_or_default() + } +} + +impl From for SafeFloat { + fn from(value: U64F64) -> Self { + let bits = value.to_bits(); + // High 64 bits = integer part + let int = (bits >> 64) as u64; + // Low 64 bits = fractional part + let frac = (bits & 0xFFFF_FFFF_FFFF_FFFF) as u64; + + // If strictly zero, shortcut + if bits == 0 { + return SafeFloat::zero(); + } + + // SafeFloat for integer part: int * 10^0 + let safe_int = SafeFloat::new(int as u128, 0).unwrap_or_default(); + + // Numerator of fractional part: frac * 10^0 + let safe_frac_num = SafeFloat::new(frac as u128, 0).unwrap_or_default(); + + // Denominator = 2^64 as an integer SafeFloat: (2^64) * 10^0 + let two64: u128 = 1u128 << 64; + let safe_two64 = SafeFloat::new(two64, 0).unwrap_or_default(); + + // frac_part = frac / 2^64 + let safe_frac = safe_frac_num.div(&safe_two64).unwrap_or_default(); + + // int + frac/2^64, with all mantissa/exponent normalization + safe_int.add(&safe_frac).unwrap_or_default() + } +} + +impl From<&SafeFloat> for f64 { + #[allow( + clippy::arithmetic_side_effects, + reason = "This code is only used in tests" + )] + fn from(value: &SafeFloat) -> Self { + let mant = value.mantissa as f64; + + // powi takes i32, so clamp i64 exponent into i32 range (test-only). + let e = value.exponent.clamp(i32::MIN as i64, i32::MAX as i64) as i32; + + mant * 10_f64.powi(e) + } +} + +impl From for f64 { + fn from(value: SafeFloat) -> Self { + f64::from(&value) + } +} pub trait SharePoolDataOperations { - /// Gets shared value - fn get_shared_value(&self) -> U64F64; + /// Gets shared value (always "the real thing" measured in rao, not fractional) + fn get_shared_value(&self) -> u64; /// Gets single share for a given key - fn get_share(&self, key: &Key) -> U64F64; + fn get_share(&self, key: &Key) -> SafeFloat; // Tries to get a single share for a given key, as a result. - fn try_get_share(&self, key: &Key) -> Result; + fn try_get_share(&self, key: &Key) -> Result; /// Gets share pool denominator - fn get_denominator(&self) -> U64F64; + fn get_denominator(&self) -> SafeFloat; /// Updates shared value by provided signed value - fn set_shared_value(&mut self, value: U64F64); + fn set_shared_value(&mut self, value: u64); /// Update single share for a given key by provided signed value - fn set_share(&mut self, key: &Key, share: U64F64); + fn set_share(&mut self, key: &Key, share: SafeFloat); /// Update share pool denominator by provided signed value - fn set_denominator(&mut self, update: U64F64); + fn set_denominator(&mut self, update: SafeFloat); } /// SharePool struct that depends on the Key type and uses the SharePoolDataOperations @@ -47,36 +418,24 @@ where } pub fn get_value(&self, key: &K) -> u64 { - let shared_value: U64F64 = self.state_ops.get_shared_value(); - let current_share: U64F64 = self.state_ops.get_share(key); - let denominator: U64F64 = self.state_ops.get_denominator(); - - let maybe_value_per_share = shared_value.checked_div(denominator); - (if let Some(value_per_share) = maybe_value_per_share { - value_per_share.saturating_mul(current_share) - } else { - shared_value - .saturating_mul(current_share) - .checked_div(denominator) - .unwrap_or(U64F64::saturating_from_num(0)) - }) - .saturating_to_num::() + let shared_value: SafeFloat = + SafeFloat::new(self.state_ops.get_shared_value() as u128, 0).unwrap_or_default(); + let current_share: SafeFloat = self.state_ops.get_share(key); + let denominator: SafeFloat = self.state_ops.get_denominator(); + shared_value + .mul_div(¤t_share, &denominator) + .unwrap_or_default() + .into() } - pub fn get_value_from_shares(&self, current_share: U64F64) -> u64 { - let shared_value: U64F64 = self.state_ops.get_shared_value(); - let denominator: U64F64 = self.state_ops.get_denominator(); - - let maybe_value_per_share = shared_value.checked_div(denominator); - (if let Some(value_per_share) = maybe_value_per_share { - value_per_share.saturating_mul(current_share) - } else { - shared_value - .saturating_mul(current_share) - .checked_div(denominator) - .unwrap_or(U64F64::saturating_from_num(0)) - }) - .saturating_to_num::() + pub fn get_value_from_shares(&self, current_share: SafeFloat) -> u64 { + let shared_value: SafeFloat = + SafeFloat::new(self.state_ops.get_shared_value() as u128, 0).unwrap_or_default(); + let denominator: SafeFloat = self.state_ops.get_denominator(); + shared_value + .mul_div(¤t_share, &denominator) + .unwrap_or_default() + .into() } pub fn try_get_value(&self, key: &K) -> Result { @@ -89,164 +448,184 @@ where /// Update the total shared value. /// Every key's associated value effectively updates with this operation pub fn update_value_for_all(&mut self, update: i64) { - let shared_value: U64F64 = self.state_ops.get_shared_value(); + let shared_value: u64 = self.state_ops.get_shared_value(); self.state_ops.set_shared_value(if update >= 0 { - shared_value.saturating_add(U64F64::saturating_from_num(update)) + shared_value.saturating_add(update as u64) } else { - shared_value.saturating_sub(U64F64::saturating_from_num(update.neg())) + shared_value.saturating_sub(update.neg() as u64) }); } pub fn sim_update_value_for_one(&mut self, update: i64) -> bool { - let shared_value: U64F64 = self.state_ops.get_shared_value(); - let denominator: U64F64 = self.state_ops.get_denominator(); + let shared_value: u64 = self.state_ops.get_shared_value(); + let denominator: SafeFloat = self.state_ops.get_denominator(); // Then, update this key's share - if denominator == 0 { + if denominator.mantissa == 0 { true } else { // There are already keys in the pool, set or update this key - let shares_per_update: I64F64 = - self.get_shares_per_update(update, &shared_value, &denominator); + let shares_per_update = self.get_shares_per_update(update, shared_value, &denominator); - shares_per_update != 0 + !shares_per_update.is_zero() } } fn get_shares_per_update( &self, update: i64, - shared_value: &U64F64, - denominator: &U64F64, - ) -> I64F64 { - let maybe_value_per_share = shared_value.checked_div(*denominator); - if let Some(value_per_share) = maybe_value_per_share { - I64F64::saturating_from_num(update) - .checked_div(I64F64::saturating_from_num(value_per_share)) - .unwrap_or(I64F64::saturating_from_num(0)) - } else { - I64F64::saturating_from_num(update) - .checked_div(I64F64::saturating_from_num(*shared_value)) - .unwrap_or(I64F64::saturating_from_num(0)) - .saturating_mul(I64F64::saturating_from_num(*denominator)) - } + shared_value: u64, + denominator: &SafeFloat, + ) -> SafeFloat { + let shared_value: SafeFloat = SafeFloat::new(shared_value as u128, 0).unwrap_or_default(); + let update_sf: SafeFloat = + SafeFloat::new(update.unsigned_abs() as u128, 0).unwrap_or_default(); + update_sf + .mul_div(denominator, &shared_value) + .unwrap_or_default() } /// Update the value associated with an item identified by the Key /// Returns actual update /// - pub fn update_value_for_one(&mut self, key: &K, update: i64) -> i64 { - let shared_value: U64F64 = self.state_ops.get_shared_value(); - let current_share: U64F64 = self.state_ops.get_share(key); - let denominator: U64F64 = self.state_ops.get_denominator(); - let initial_value: i64 = self.get_value(key) as i64; - let mut actual_update: i64 = update; + pub fn update_value_for_one(&mut self, key: &K, update: i64) { + let shared_value: u64 = self.state_ops.get_shared_value(); + let current_share: SafeFloat = self.state_ops.get_share(key); + let denominator: SafeFloat = self.state_ops.get_denominator(); // Then, update this key's share - if denominator == 0 { + if denominator.is_zero() { // Initialize the pool. The first key gets all. - let update_fixed: U64F64 = U64F64::saturating_from_num(update); - self.state_ops.set_denominator(update_fixed); - self.state_ops.set_share(key, update_fixed); + let update_float: SafeFloat = + SafeFloat::new(update.unsigned_abs() as u128, 0).unwrap_or_default(); + self.state_ops.set_denominator(update_float.clone()); + self.state_ops.set_share(key, update_float); } else { - let shares_per_update: I64F64 = - self.get_shares_per_update(update, &shared_value, &denominator); - - if shares_per_update >= 0 { - self.state_ops.set_denominator( - denominator.saturating_add(U64F64::saturating_from_num(shares_per_update)), - ); - self.state_ops.set_share( - key, - current_share.saturating_add(U64F64::saturating_from_num(shares_per_update)), - ); + let new_denominator; + let new_current_share; + + let shares_per_update: SafeFloat = + self.get_shares_per_update(update, shared_value, &denominator); + + // Handle SafeFloat overflows quietly here because this overflow of i64 exponent + // is extremely hypothetical and should never happen in practice. + if update > 0 { + new_denominator = match denominator.add(&shares_per_update) { + Some(new_denominator) => new_denominator, + None => { + log::error!( + "SafeFloat::add overflow when adding {:?} to {:?}; keeping old denominator", + shares_per_update, + denominator, + ); + // Return the value as it was before the failed addition + denominator + } + }; + + new_current_share = match current_share.add(&shares_per_update) { + Some(new_current_share) => new_current_share, + None => { + log::error!( + "SafeFloat::add overflow when adding {:?} to {:?}; keeping old current_share", + shares_per_update, + current_share, + ); + // Return the value as it was before the failed addition + current_share + } + }; } else { - // Check if this entry is about to break precision - let mut new_denominator = denominator - .saturating_sub(U64F64::saturating_from_num(shares_per_update.neg())); - let mut new_share = current_share - .saturating_sub(U64F64::saturating_from_num(shares_per_update.neg())); - - // The condition here is either the share remainder is too little OR - // the new_denominator is too low compared to what shared_value + year worth of emissions would be - if (new_share.safe_div(current_share) < U64F64::saturating_from_num(0.00001)) - || shared_value - .saturating_add(U64F64::saturating_from_num(2_628_000_000_000_000_u64)) - .checked_div(new_denominator) - .is_none() - { - // yes, precision is low, just remove all - new_share = U64F64::saturating_from_num(0); - new_denominator = denominator.saturating_sub(current_share); - actual_update = initial_value.neg(); - } - - self.state_ops.set_denominator(new_denominator); - self.state_ops.set_share(key, new_share); + new_denominator = match denominator.sub(&shares_per_update) { + Some(new_denominator) => new_denominator, + None => { + log::error!( + "SafeFloat::add overflow when adding {:?} to {:?}; keeping old denominator", + shares_per_update, + denominator, + ); + // Return the value as it was before the failed addition + denominator + } + }; + + new_current_share = match current_share.sub(&shares_per_update) { + Some(new_current_share) => new_current_share, + None => { + log::error!( + "SafeFloat::add overflow when adding {:?} to {:?}; keeping old current_share", + shares_per_update, + current_share, + ); + // Return the value as it was before the failed addition + current_share + } + }; } + + self.state_ops.set_denominator(new_denominator); + self.state_ops.set_share(key, new_current_share); } // Update shared value - self.update_value_for_all(actual_update); - - // Return actual udate - actual_update + self.update_value_for_all(update); } } +// cargo test --package share-pool --lib -- tests --nocapture #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use super::*; + use approx::assert_abs_diff_eq; use std::collections::BTreeMap; + use substrate_fixed::types::U64F64; struct MockSharePoolDataOperations { - shared_value: U64F64, - share: BTreeMap, - denominator: U64F64, + shared_value: u64, + share: BTreeMap, + denominator: SafeFloat, } impl MockSharePoolDataOperations { fn new() -> Self { MockSharePoolDataOperations { - shared_value: U64F64::saturating_from_num(0), + shared_value: 0u64, share: BTreeMap::new(), - denominator: U64F64::saturating_from_num(0), + denominator: SafeFloat::zero(), } } } impl SharePoolDataOperations for MockSharePoolDataOperations { - fn get_shared_value(&self) -> U64F64 { + fn get_shared_value(&self) -> u64 { self.shared_value } - fn get_share(&self, key: &u16) -> U64F64 { - *self - .share - .get(key) - .unwrap_or(&U64F64::saturating_from_num(0)) + fn get_share(&self, key: &u16) -> SafeFloat { + self.share.get(key).cloned().unwrap_or_else(SafeFloat::zero) } - fn try_get_share(&self, key: &u16) -> Result { - match self.share.get(key) { - Some(&value) => Ok(value), + fn try_get_share(&self, key: &u16) -> Result { + match self.share.get(key).cloned() { + Some(value) => Ok(value), None => Err(()), } } - fn get_denominator(&self) -> U64F64 { - self.denominator + fn get_denominator(&self) -> SafeFloat { + self.denominator.clone() } - fn set_shared_value(&mut self, value: U64F64) { + fn set_shared_value(&mut self, value: u64) { self.shared_value = value; } - fn set_share(&mut self, key: &u16, share: U64F64) { + fn set_share(&mut self, key: &u16, share: SafeFloat) { self.share.insert(*key, share); } - fn set_denominator(&mut self, update: U64F64) { + fn set_denominator(&mut self, update: SafeFloat) { self.denominator = update; } } @@ -254,10 +633,10 @@ mod tests { #[test] fn test_get_value() { let mut mock_ops = MockSharePoolDataOperations::new(); - mock_ops.set_denominator(U64F64::saturating_from_num(10)); - mock_ops.set_share(&1_u16, U64F64::saturating_from_num(3)); - mock_ops.set_share(&2_u16, U64F64::saturating_from_num(7)); - mock_ops.set_shared_value(U64F64::saturating_from_num(100)); + mock_ops.set_denominator(10u64.into()); + mock_ops.set_share(&1_u16, 3u64.into()); + mock_ops.set_share(&2_u16, 7u64.into()); + mock_ops.set_shared_value(100u64.into()); let share_pool = SharePool::new(mock_ops); let result1 = share_pool.get_value(&1); let result2 = share_pool.get_value(&2); @@ -268,7 +647,7 @@ mod tests { #[test] fn test_division_by_zero() { let mut mock_ops = MockSharePoolDataOperations::new(); - mock_ops.set_denominator(U64F64::saturating_from_num(0)); // Zero denominator + mock_ops.set_denominator(SafeFloat::zero()); // Zero denominator let pool = SharePool::::new(mock_ops); let value = pool.get_value(&1); @@ -278,10 +657,10 @@ mod tests { #[test] fn test_max_shared_value() { let mut mock_ops = MockSharePoolDataOperations::new(); - mock_ops.set_shared_value(U64F64::saturating_from_num(u64::MAX)); - mock_ops.set_share(&1, U64F64::saturating_from_num(3)); // Use a neutral value for share - mock_ops.set_share(&2, U64F64::saturating_from_num(7)); // Use a neutral value for share - mock_ops.set_denominator(U64F64::saturating_from_num(10)); // Neutral value to see max effect + mock_ops.set_shared_value(u64::MAX.into()); + mock_ops.set_share(&1, 3u64.into()); // Use a neutral value for share + mock_ops.set_share(&2, 7u64.into()); // Use a neutral value for share + mock_ops.set_denominator(10u64.into()); // Neutral value to see max effect let pool = SharePool::::new(mock_ops); let max_value = pool.get_value(&1) + pool.get_value(&2); @@ -291,16 +670,16 @@ mod tests { #[test] fn test_max_share_value() { let mut mock_ops = MockSharePoolDataOperations::new(); - mock_ops.set_shared_value(U64F64::saturating_from_num(1_000_000_000)); // Use a neutral value for shared value - mock_ops.set_share(&1, U64F64::saturating_from_num(u64::MAX / 2)); - mock_ops.set_share(&2, U64F64::saturating_from_num(u64::MAX / 2)); - mock_ops.set_denominator(U64F64::saturating_from_num(u64::MAX)); + mock_ops.set_shared_value(1_000_000_000u64); // Use a neutral value for shared value + mock_ops.set_share(&1, (u64::MAX / 2).into()); + mock_ops.set_share(&2, (u64::MAX / 2).into()); + mock_ops.set_denominator((u64::MAX).into()); let pool = SharePool::::new(mock_ops); let value1 = pool.get_value(&1) as i128; let value2 = pool.get_value(&2) as i128; - assert!((value1 - 500_000_000).abs() <= 1); + assert_abs_diff_eq!(value1 as f64, 500_000_000_f64, epsilon = 1.); assert!((value2 - 500_000_000).abs() <= 1); } @@ -331,26 +710,30 @@ mod tests { let mock_ops = MockSharePoolDataOperations::new(); let mut pool = SharePool::::new(mock_ops); + // 50%/50% stakes consisting of 1 rao each pool.update_value_for_one(&1, 1); pool.update_value_for_one(&2, 1); + // Huge emission resulting in 1M Alpha + // Both stakers should have 500k Alpha each pool.update_value_for_all(999_999_999_999_998); + // Everyone unstakes almost everything, leaving 10 rao in the stake pool.update_value_for_one(&1, -499_999_999_999_990); pool.update_value_for_one(&2, -499_999_999_999_990); + // Huge emission resulting in 1M Alpha + // Both stakers should have 500k Alpha each pool.update_value_for_all(999_999_999_999_980); + // Stakers add 1k Alpha each pool.update_value_for_one(&1, 1_000_000_000_000); pool.update_value_for_one(&2, 1_000_000_000_000); - let value1 = pool.get_value(&1) as i128; - let value2 = pool.get_value(&2) as i128; - - // First to stake gets all accumulated emission if there are no other stakers - // (which is artificial situation because there will be no emissions if there is no stake) - assert!((value1 - 1_001_000_000_000_000).abs() < 100); - assert!((value2 - 1_000_000_000_000).abs() < 100); + let value1 = pool.get_value(&1) as f64; + let value2 = pool.get_value(&2) as f64; + assert_abs_diff_eq!(value1, 501_000_000_000_000_f64, epsilon = 1.); + assert_abs_diff_eq!(value2, 501_000_000_000_000_f64, epsilon = 1.); } // cargo test --package share-pool --lib -- tests::test_denom_high_precision_many_small_unstakes --exact --show-output @@ -359,26 +742,37 @@ mod tests { let mock_ops = MockSharePoolDataOperations::new(); let mut pool = SharePool::::new(mock_ops); + // 50%/50% stakes consisting of 1 rao each pool.update_value_for_one(&1, 1); pool.update_value_for_one(&2, 1); + // Huge emission resulting in 1M Alpha + // Both stakers should have 500k Alpha + 1 rao each pool.update_value_for_all(1_000_000_000_000_000); - for _ in 0..1_000_000 { - pool.update_value_for_one(&1, -500_000_000); - pool.update_value_for_one(&2, -500_000_000); + // Run X number of small unstake transactions + let tx_count = 1000; + let unstake_amount = -500_000_000; + for _ in 0..tx_count { + pool.update_value_for_one(&1, unstake_amount); + pool.update_value_for_one(&2, unstake_amount); } + // Emit 1M - each gets 500k Alpha pool.update_value_for_all(1_000_000_000_000_000); + // Each adds 1k Alpha pool.update_value_for_one(&1, 1_000_000_000_000); pool.update_value_for_one(&2, 1_000_000_000_000); + // Result, each should get + // (500k+1) + tx_count * unstake_amount + 500k + 1k let value1 = pool.get_value(&1) as i128; let value2 = pool.get_value(&2) as i128; + let expected = 1_001_000_000_000_000 + tx_count * unstake_amount; - assert!((value1 - 1_001_000_000_000_000).abs() < 10); - assert!((value2 - 1_000_000_000_000).abs() < 10); + assert_abs_diff_eq!(value1 as f64, expected as f64, epsilon = 1.); + assert_abs_diff_eq!(value2 as f64, expected as f64, epsilon = 1.); } #[test] @@ -407,46 +801,846 @@ mod tests { // cargo test --package share-pool --lib -- tests::test_get_shares_per_update --exact --show-output #[test] fn test_get_shares_per_update() { + // Test case (update, shared_value, denominator_mantissa, denominator_exponent) + [ + (1_i64, 1_u64, 1_u64, 0_i64), + (1, 1_000_000_000_000_000_000, 1, 0), + (1, 21_000_000_000_000_000, 1, 5), + (1, 21_000_000_000_000_000, 1, -1_000_000), + (1, 21_000_000_000_000_000, 1, -1_000_000_000), + (1, 21_000_000_000_000_000, 1, -1_000_000_001), + (1_000, 21_000_000_000_000_000, 1, 5), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, 5), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, -5), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, -100), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, 100), + (210_000_000_000_000_000, 21_000_000_000_000_000, 1, 5), + (1_000, 1_000, 21_000_000_000_000_000, 0), + (1_000, 1_000, 21_000_000_000_000_000, -1), + ] + .into_iter() + .for_each( + |(update, shared_value, denominator_mantissa, denominator_exponent)| { + let mock_ops = MockSharePoolDataOperations::new(); + let pool = SharePool::::new(mock_ops); + + let denominator_float = + SafeFloat::new(denominator_mantissa as u128, denominator_exponent) + .unwrap_or_default(); + let denominator_f64: f64 = denominator_float.clone().into(); + let spu: f64 = pool + .get_shares_per_update(update, shared_value, &denominator_float) + .into(); + let expected = update as f64 * denominator_f64 / shared_value as f64; + let precision = 1000.; + assert_abs_diff_eq!(expected, spu, epsilon = expected / precision); + }, + ); + } + + #[test] + fn test_safefloat_normalize() { + // Test case: mantissa, exponent, expected mantissa, expected exponent + [ + (1_u128, 0, 1_000_000_000_000_000_000_000_u128, -21_i64), + (0, 0, 0, 0), + (10_u128, 0, 1_000_000_000_000_000_000_000_u128, -20), + (1_000_u128, 0, 1_000_000_000_000_000_000_000_u128, -18), + ( + 100_000_000_000_000_000_000_u128, + 0, + 1_000_000_000_000_000_000_000_u128, + -1, + ), + (SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX, 0), + ] + .into_iter() + .for_each(|(m, e, expected_m, expected_e)| { + let a = SafeFloat::new(m, e).unwrap(); + assert_eq!(a.mantissa, expected_m); + assert_eq!(a.exponent, expected_e); + }); + } + + #[test] + fn test_safefloat_add() { + // Test case: man_a, exp_a, man_b, exp_b, expected mantissa of a+b, expected exponent of a+b [ - (1_i64, 1_u64, 1.0, 1.0), + // 1 + 1 = 2 + ( + 1_u128, + 0, + 1_u128, + 0, + 200_000_000_000_000_000_000_u128, + -20_i64, + ), + // 0 + 1 = 1 + (0, 0, 1, 0, 1_000_000_000_000_000_000_000_u128, -21_i64), + // 0 + 0.1 = 0.1 + (0, 0, 1, -1, 1_000_000_000_000_000_000_000_u128, -22_i64), + // 1e-1000 + 0.1 = 0.1 + (1, -1000, 1, -1, 1_000_000_000_000_000_000_000_u128, -22_i64), + // SAFE_FLOAT_MAX + SAFE_FLOAT_MAX + ( + SAFE_FLOAT_MAX, + 0, + SAFE_FLOAT_MAX, + 0, + SAFE_FLOAT_MAX * 2 / 10, + 1_i64, + ), + // Expected loss of precision: tiny + huge + ( + 1_u128, + 0, + 1_000_000_000_000_000_000_000_u128, + 1, + 1_000_000_000_000_000_000_000_u128, + 1_i64, + ), + ( + 1_u128, + 0, + 1_u128, + 22, + 1_000_000_000_000_000_000_000_u128, + 1_i64, + ), + ( + 1_u128, + 0, + 1_u128, + 23, + 1_000_000_000_000_000_000_000_u128, + 2_i64, + ), + ( + 123_u128, + 0, + 1_u128, + 23, + 1_000_000_000_000_000_000_000_u128, + 2_i64, + ), + ( + 123_u128, + 1, + 1_u128, + 23, + 100_000_000_000_000_000_001_u128, + 3_i64, + ), + // Small-ish + very large (10^22 + 42) + // 42 * 10^0 + 1 * 10^22 ≈ 1e22 + 42 + // Normalized ≈ (1e21 + 4) * 10^1 + ( + 42_u128, + 0, + 1_u128, + 22, + 1_000_000_000_000_000_000_000_u128, + 1_i64, + ), + // "Almost 10^21" + 10^22 + // (10^21 - 1) + 10^22 → floor((10^22 + 10^21 - 1) / 100) * 10^2 + ( + 999_999_999_999_999_999_999_u128, + 0, + 1_u128, + 22, + 109_999_999_999_999_999_999_u128, + 2_i64, + ), + // Small-ish + 10^23 where the small part is completely lost + // 42 + 10^23 -> floor((10^23 + 42)/100) * 10^2 ≈ 1e21 * 10^2 ( - 1_000, - 21_000_000_000_000_000, - 0.00001, - 0.00000000000000000043, + 42_u128, + 0, + 1_u128, + 23, + 1_000_000_000_000_000_000_000_u128, + 2_i64, ), + // Small-ish + 10^23 where tiny part slightly affects mantissa + // 4200 + 10^23 -> floor((10^23 + 4200)/100) * 10^2 = (1e21 + 42) * 10^2 ( - 21_000_000_000_000_000, - 21_000_000_000_000_000, - 0.00001, - 0.00001, + 4_200_u128, + 0, + 1_u128, + 23, + 100_000_000_000_000_000_004_u128, + 3_i64, ), + // (10^21 - 1) + 10^23 + // -> floor((10^23 + 10^21 - 1)/100) = 1e21 + 1e19 - 1 ( - 210_000_000_000_000_000, - 21_000_000_000_000_000, - 0.00001, - 0.0001, + 999_999_999_999_999_999_999_u128, + 0, + 1_u128, + 23, + 100_999_999_999_999_999_999_u128, + 3_i64, ), + // Medium + 10^23 with exponent 1 on the smaller term + // 999_999 * 10^1 + 1 * 10^23 -> (10^22 + 999_999) * 10^1 + // Normalized ≈ (1e21 + 99_999) * 10^2 ( - 1_000, - 1_000, - 21_000_000_000_000_000_f64, - 21_000_000_000_000_000_f64, + 999_999_u128, + 1, + 1_u128, + 23, + 100_000_000_000_000_009_999_u128, + 3_i64, + ), + // Check behaviour with exponent 24, tiny second term + // 1 * 10^24 + 1 -> floor((10^24 + 1)/1000) * 10^3 ≈ 1e21 * 10^3 + ( + 1_u128, + 24, + 1_u128, + 0, + 1_000_000_000_000_000_000_000_u128, + 3_i64, + ), + // 1 * 10^24 + a non-trivial small mantissa + // 1e24 + 123456789012345678901 -> floor(/1000) = 1e21 + 123456789012345678 + ( + 1_u128, + 24, + 123_456_789_012_345_678_901_u128, + 0, + 100_012_345_678_901_234_567_u128, + 4_i64, + ), + // 10^22 and 10^23 combined: + // 1 * 10^22 + 1 * 10^23 = 11 * 10^22 = (1.1 * 10^23) + // Normalized → (1.1e20) * 10^3 + ( + 1_u128, + 22, + 1_u128, + 23, + 110_000_000_000_000_000_000_u128, + 3_i64, + ), + // Both operands already aligned at a huge scale: + // (10^21 - 1) * 10^22 + 1 * 10^22 = 10^21 * 10^22 = 10^43 + // Canonical form: (1e21) * 10^22 + ( + 999_999_999_999_999_999_999_u128, + 22, + 1_u128, + 22, + 1_000_000_000_000_000_000_000_u128, + 22_i64, + ), + ] + .into_iter() + .for_each(|(m_a, e_a, m_b, e_b, expected_m, expected_e)| { + let a = SafeFloat::new(m_a, e_a).unwrap(); + let b = SafeFloat::new(m_b, e_b).unwrap(); + + let a_plus_b = a.add(&b).unwrap(); + let b_plus_a = b.add(&a).unwrap(); + + assert_eq!(a_plus_b.mantissa, expected_m); + assert_eq!(a_plus_b.exponent, expected_e); + assert_eq!(b_plus_a.mantissa, expected_m); + assert_eq!(b_plus_a.exponent, expected_e); + }); + } + + #[test] + fn test_safefloat_div_by_zero_is_none() { + let a = SafeFloat::new(1u128, 0).unwrap(); + assert!(a.div(&SafeFloat::zero()).is_none()); + } + + #[test] + fn test_safefloat_div() { + // Test case: man_a, exp_a, man_b, exp_b + [ + (1_u128, 0_i64, 100_000_000_000_000_000_000_u128, -20_i64), + (1_u128, 0, 1_u128, 0), + (1_u128, 1, 1_u128, 0), + (1_u128, 7, 1_u128, 0), + (1_u128, 50, 1_u128, 0), + (1_u128, 100, 1_u128, 0), + (1_u128, 0, 7_u128, 0), + (1_u128, 1, 7_u128, 0), + (1_u128, 7, 7_u128, 0), + (1_u128, 50, 7_u128, 0), + (1_u128, 100, 7_u128, 0), + (1_u128, 0, 3_u128, 0), + (1_u128, 1, 3_u128, 0), + (1_u128, 7, 3_u128, 0), + (1_u128, 50, 3_u128, 0), + (1_u128, 100, 3_u128, 0), + (2_u128, 0, 3_u128, 0), + (2_u128, 1, 3_u128, 0), + (2_u128, 7, 3_u128, 0), + (2_u128, 50, 3_u128, 0), + (2_u128, 100, 3_u128, 0), + (5_u128, 0, 3_u128, 0), + (5_u128, 1, 3_u128, 0), + (5_u128, 7, 3_u128, 0), + (5_u128, 50, 3_u128, 0), + (5_u128, 100, 3_u128, 0), + (10_u128, 0, 100_000_000_000_000_000_000_u128, -19), + (1_000_u128, 0, 100_000_000_000_000_000_000_u128, -17), + ( + 100_000_000_000_000_000_000_u128, + 0, + 1_000_000_000_000_000_000_000_u128, + -1, + ), + (SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX, 0), + (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX, -100), + (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX - 1, -100), + (SAFE_FLOAT_MAX - 1, 100, SAFE_FLOAT_MAX, -100), + (SAFE_FLOAT_MAX - 2, 100, SAFE_FLOAT_MAX, -100), + (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX / 2 - 1, -100), + (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX / 2 - 1, 100), + (1_u128, 0, 100_000_000_000_000_000_000_u128, -20_i64), + ( + 123_456_789_123_456_789_123_u128, + 20_i64, + 87_654_321_987_654_321_987_u128, + -20_i64, + ), + ( + 123_456_789_123_456_789_123_u128, + 100_i64, + 87_654_321_987_654_321_987_u128, + -100_i64, + ), + ( + 123_456_789_123_456_789_123_u128, + -100_i64, + 87_654_321_987_654_321_987_u128, + 100_i64, + ), + ( + 123_456_789_123_456_789_123_u128, + -99_i64, + 87_654_321_987_654_321_987_u128, + 99_i64, + ), + ( + 123_456_789_123_456_789_123_u128, + 123_i64, + 87_654_321_987_654_321_987_u128, + -32_i64, + ), + ( + 123_456_789_123_456_789_123_u128, + -123_i64, + 87_654_321_987_654_321_987_u128, + 32_i64, ), ] - .iter() - .for_each(|(update, shared_value, denominator, expected)| { - let mock_ops = MockSharePoolDataOperations::new(); - let pool = SharePool::::new(mock_ops); - - let shared_fixed = U64F64::from_num(*shared_value); - let denominator_fixed = U64F64::from_num(*denominator); - let expected_fixed = I64F64::from_num(*expected); - - let spu: I64F64 = - pool.get_shares_per_update(*update, &shared_fixed, &denominator_fixed); - let precision: I64F64 = I64F64::from_num(1000.); - assert!((spu - expected_fixed).abs() <= expected_fixed / precision,); + .into_iter() + .for_each(|(ma, ea, mb, eb)| { + let a = SafeFloat::new(ma, ea).unwrap(); + let b = SafeFloat::new(mb, eb).unwrap(); + + let actual: f64 = a.div(&b).unwrap().into(); + let expected = + ma as f64 * (10_f64).powi(ea as i32) / (mb as f64 * (10_f64).powi(eb as i32)); + + assert_abs_diff_eq!(actual, expected, epsilon = actual / 100_000_000_000_000_f64); }); } + + #[test] + fn test_safefloat_mul_div() { + // result = a * b / c + // should not lose precision gained in a * b + // Test case: man_a, exp_a, man_b, exp_b, man_c, exp_c + [ + (1_u128, -20_i64, 1_u128, -20_i64, 1_u128, -20_i64), + (123_u128, 20_i64, 123_u128, -20_i64, 321_u128, 0_i64), + ( + 123_123_123_123_123_123_u128, + 20_i64, + 321_321_321_321_321_321_u128, + -20_i64, + 777_777_777_777_777_777_u128, + 0_i64, + ), + ( + 11_111_111_111_111_111_111_u128, + 20_i64, + 99_321_321_321_321_321_321_u128, + -20_i64, + 77_777_777_777_777_777_777_u128, + 0_i64, + ), + ] + .into_iter() + .for_each(|(ma, ea, mb, eb, mc, ec)| { + let a = SafeFloat::new(ma, ea).unwrap(); + let b = SafeFloat::new(mb, eb).unwrap(); + let c = SafeFloat::new(mc, ec).unwrap(); + + let actual: f64 = a.mul_div(&b, &c).unwrap().into(); + let expected = (ma as f64 * (10_f64).powi(ea as i32)) + * (mb as f64 * (10_f64).powi(eb as i32)) + / (mc as f64 * (10_f64).powi(ec as i32)); + + assert_abs_diff_eq!(actual, expected, epsilon = actual / 100_000_000_000_000_f64); + }); + } + + #[test] + fn test_safefloat_from_u64f64() { + [ + // U64F64::from_num(1000.0), + // U64F64::from_num(10.0), + // U64F64::from_num(1.0), + U64F64::from_num(0.1), + // U64F64::from_num(0.00000001), + // U64F64::from_num(123_456_789_123_456u128), + // // Exact zero + // U64F64::from_num(0.0), + // // Very small positive value (well above Q64.64 resolution) + // U64F64::from_num(1e-18), + // // Value just below 1 + // U64F64::from_num(0.999_999_999_999_999_f64), + // // Value just above 1 + // U64F64::from_num(1.000_000_000_000_001_f64), + // // "Random-looking" fractional with many digits + // U64F64::from_num(1.234_567_890_123_45_f64), + // // Large integer, but smaller than the max integer part of U64F64 + // U64F64::from_num(999_999_999_999_999_999u128), + // // Very large integer near the upper bound of integer range + // U64F64::from_num(u64::MAX as u128), + // // Large number with fractional part + // U64F64::from_num(123_456_789_123_456.78_f64), + // // Medium-large with tiny fractional part to test precision on tail digits + // U64F64::from_num(1_000_000_000_000.000_001_f64), + // // Smallish with long fractional part + // U64F64::from_num(0.123_456_789_012_345_f64), + ] + .into_iter() + .for_each(|f| { + let safe_float: SafeFloat = f.into(); + let actual: f64 = safe_float.into(); + let expected = f.to_num::(); + + // Relative epsilon ~1e-14 of the magnitude + let epsilon = if actual == 0.0 { + 0.0 + } else { + actual.abs() / 100_000_000_000_000_f64 + }; + + assert_abs_diff_eq!(actual, expected, epsilon = epsilon); + }); + } + + /// This is a real-life scenario test when someone lost 7 TAO on Chutes (SN64) + /// when paying fees in Alpha. The scenario occured because the update of share value + /// of one coldkey (update_value_for_one) hit the scenario of full unstake. + /// + /// Specifically, the following condition was triggered: + /// + /// `(shared_value + 2_628_000_000_000_000_u64).checked_div(new_denominator)` + /// + /// returned None because new_denominator was too low and division of + /// `shared_value + 2_628_000_000_000_000_u64` by new_denominator has overflown U64F64. + /// + /// This test fails on the old version of share pool (with much lower tolerances). + /// + /// cargo test --package share-pool --lib -- tests::test_loss_due_to_precision --exact --nocapture + #[test] + fn test_loss_due_to_precision() { + let mock_ops = MockSharePoolDataOperations::new(); + let mut pool = SharePool::::new(mock_ops); + + // Setup pool so that initial coldkey's alpha is 10% of 1e12 = 1e11 rao. + let low_denominator = SafeFloat::new(1u128, -14).unwrap(); + let low_share = SafeFloat::new(1u128, -15).unwrap(); + pool.state_ops.set_denominator(low_denominator); + pool.state_ops.set_shared_value(1_000_000_000_000_u64); + pool.state_ops.set_share(&1, low_share); + + let value_before = pool.get_value(&1) as i128; + assert_abs_diff_eq!(value_before as f64, 100_000_000_000., epsilon = 0.1); + + // Remove a little stake + let unstake_amount = 1000i64; + pool.update_value_for_one(&1, unstake_amount.neg()); + + let value_after = pool.get_value(&1) as i128; + assert_abs_diff_eq!( + (value_before - value_after) as f64, + unstake_amount as f64, + epsilon = unstake_amount as f64 / 1_000_000_000. + ); + } + + fn rel_err(a: f64, b: f64) -> f64 { + let denom = a.abs().max(b.abs()).max(1.0); + (a - b).abs() / denom + } + + fn push_unique(v: &mut Vec, x: u128) { + if x != 0 && !v.contains(&x) { + v.push(x); + } + } + + // cargo test --package share-pool --lib -- tests::test_safefloat_mul_div_wide_range --exact --include-ignored --show-output + #[test] + #[ignore = "long-running sweep test; run explicitly when needed"] + fn test_safefloat_mul_div_wide_range() { + use rayon::prelude::*; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + // Build mantissa corpus + let mut mantissas = Vec::::new(); + + let linear_steps: u128 = 200; + let linear_step = (SAFE_FLOAT_MAX / linear_steps).max(1); + let mut m = 1u128; + while m <= SAFE_FLOAT_MAX { + push_unique(&mut mantissas, m); + match m.checked_add(linear_step) { + Some(next) if next > m => m = next, + _ => break, + } + } + push_unique(&mut mantissas, SAFE_FLOAT_MAX); + + let mut p = 1u128; + while p <= SAFE_FLOAT_MAX { + push_unique(&mut mantissas, p); + if p > 1 { + push_unique(&mut mantissas, p - 1); + } + if let Some(next) = p.checked_add(1) + && next <= SAFE_FLOAT_MAX + { + push_unique(&mut mantissas, next); + } + + match p.checked_mul(10) { + Some(next) if next > p && next <= SAFE_FLOAT_MAX => p = next, + _ => break, + } + } + + for delta in [ + 0u128, 1, 2, 3, 7, 9, 10, 11, 99, 100, 101, 999, 1_000, 10_000, + ] { + if SAFE_FLOAT_MAX > delta { + push_unique(&mut mantissas, SAFE_FLOAT_MAX - delta); + } + } + + mantissas.sort_unstable(); + mantissas.dedup(); + + let exp_min: i64 = -120; + let exp_max: i64 = 120; + let exp_step: usize = 5; + let exponents: Vec = (exp_min..=exp_max).step_by(exp_step).collect(); + + // Precompute all (a, b) pairs as outer work items. + // Each Rayon task will then iterate all c's sequentially. + let mut outer_cases: Vec<(u128, i64, u128, i64)> = Vec::new(); + + for &ma in &mantissas { + for &ea in &exponents { + for &mb in &mantissas { + for &eb in &exponents { + outer_cases.push((ma, ea, mb, eb)); + } + } + } + } + + let checked = Arc::new(AtomicUsize::new(0)); + let skipped_non_finite = Arc::new(AtomicUsize::new(0)); + let skipped_invalid_sf = Arc::new(AtomicUsize::new(0)); + + let progress_step = 10_000usize; + let total_outer = outer_cases.len(); + + outer_cases.into_par_iter().for_each(|(ma, ea, mb, eb)| { + let a = match SafeFloat::new(ma, ea) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + return; + } + }; + + let b = match SafeFloat::new(mb, eb) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + return; + } + }; + + for &mc in &mantissas { + for &ec in &exponents { + let c = match SafeFloat::new(mc, ec) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + continue; + } + }; + + let actual_sf = a.mul_div(&b, &c).unwrap(); + let actual: f64 = actual_sf.into(); + + let expected = + (ma as f64 * 10_f64.powi(ea as i32)) + * (mb as f64 * 10_f64.powi(eb as i32)) + / (mc as f64 * 10_f64.powi(ec as i32)); + + if !expected.is_finite() || !actual.is_finite() { + skipped_non_finite.fetch_add(1, Ordering::Relaxed); + continue; + } + + let err = rel_err(actual, expected); + + assert!( + err <= 1e-12, + concat!( + "mul_div mismatch:\n", + " a = {}e{}\n", + " b = {}e{}\n", + " c = {}e{}\n", + " actual = {:.20e}\n", + " expected = {:.20e}\n", + " rel_err = {:.20e}" + ), + ma, ea, mb, eb, mc, ec, actual, expected, err + ); + + checked.fetch_add(1, Ordering::Relaxed); + } + } + + let done_outer = checked.load(Ordering::Relaxed); + if done_outer % progress_step == 0 { + let invalid = skipped_invalid_sf.load(Ordering::Relaxed); + let non_finite = skipped_non_finite.load(Ordering::Relaxed); + log::debug!( + "progress: checked={}, skipped_invalid_sf={}, skipped_non_finite={}, outer_total={}", + done_outer, + invalid, + non_finite, + total_outer, + ); + } + }); + + let checked = checked.load(Ordering::Relaxed); + let skipped_non_finite = skipped_non_finite.load(Ordering::Relaxed); + let skipped_invalid_sf = skipped_invalid_sf.load(Ordering::Relaxed); + + println!( + "checked={}, skipped_non_finite={}, skipped_invalid_sf={}, mantissas={}, exponents={}, outer_cases={}", + checked, + skipped_non_finite, + skipped_invalid_sf, + mantissas.len(), + exponents.len(), + total_outer, + ); + + assert!(checked > 0, "test did not validate any finite cases"); + } + + #[test] + #[ignore = "long-running broad-range test; run explicitly when needed"] + fn test_safefloat_div_wide_range() { + use rayon::prelude::*; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + fn rel_err(a: f64, b: f64) -> f64 { + let denom = a.abs().max(b.abs()).max(1.0); + (a - b).abs() / denom + } + + fn push_unique(v: &mut Vec, x: u128) { + if x != 0 && !v.contains(&x) { + v.push(x); + } + } + + // Build a broad mantissa corpus: + // - coarse linear sweep + // - powers of 10 and neighbors + // - values near SAFE_FLOAT_MAX + let mut mantissas = Vec::::new(); + + let linear_steps: u128 = 200; + let linear_step = (SAFE_FLOAT_MAX / linear_steps).max(1); + let mut m = 1u128; + while m <= SAFE_FLOAT_MAX { + push_unique(&mut mantissas, m); + match m.checked_add(linear_step) { + Some(next) if next > m => m = next, + _ => break, + } + } + push_unique(&mut mantissas, SAFE_FLOAT_MAX); + + let mut p = 1u128; + while p <= SAFE_FLOAT_MAX { + push_unique(&mut mantissas, p); + if p > 1 { + push_unique(&mut mantissas, p - 1); + } + if let Some(next) = p.checked_add(1) + && next <= SAFE_FLOAT_MAX + { + push_unique(&mut mantissas, next); + } + + match p.checked_mul(10) { + Some(next) if next > p && next <= SAFE_FLOAT_MAX => p = next, + _ => break, + } + } + + for delta in [ + 0u128, 1, 2, 3, 7, 9, 10, 11, 99, 100, 101, 999, 1_000, 10_000, + ] { + if SAFE_FLOAT_MAX > delta { + push_unique(&mut mantissas, SAFE_FLOAT_MAX - delta); + } + } + + mantissas.sort_unstable(); + mantissas.dedup(); + + // Exponent sweep. + // Keep it large enough to stress normalization / exponent math, + // but still practical for f64 reference calculations. + let exp_min: i64 = -120; + let exp_max: i64 = 120; + let exp_step: usize = 5; + let exponents: Vec = (exp_min..=exp_max).step_by(exp_step).collect(); + + let m_len = mantissas.len(); + let e_len = exponents.len(); + let total_cases = m_len * e_len * m_len * e_len; + + let checked = Arc::new(AtomicUsize::new(0)); + let skipped_non_finite = Arc::new(AtomicUsize::new(0)); + let skipped_invalid_sf = Arc::new(AtomicUsize::new(0)); + let done_counter = Arc::new(AtomicUsize::new(0)); + + (0..total_cases).into_par_iter().for_each(|idx| { + let mut rem = idx; + + let eb_idx = rem % e_len; + rem /= e_len; + + let mb_idx = rem % m_len; + rem /= m_len; + + let ea_idx = rem % e_len; + rem /= e_len; + + let ma_idx = rem % m_len; + + let ma = mantissas[ma_idx]; + let ea = exponents[ea_idx]; + let mb = mantissas[mb_idx]; + let eb = exponents[eb_idx]; + + let a = match SafeFloat::new(ma, ea) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + done_counter.fetch_add(1, Ordering::Relaxed); + return; + } + }; + + let b = match SafeFloat::new(mb, eb) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + done_counter.fetch_add(1, Ordering::Relaxed); + return; + } + }; + + let actual_sf = match a.div(&b) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + done_counter.fetch_add(1, Ordering::Relaxed); + return; + } + }; + + let actual: f64 = actual_sf.into(); + let expected = + (ma as f64 * 10_f64.powi(ea as i32)) / (mb as f64 * 10_f64.powi(eb as i32)); + + if !actual.is_finite() || !expected.is_finite() { + skipped_non_finite.fetch_add(1, Ordering::Relaxed); + } else { + let err = rel_err(actual, expected); + + assert!( + err <= 1e-12, + concat!( + "div mismatch:\n", + " a = {}e{}\n", + " b = {}e{}\n", + " actual = {:.20e}\n", + " expected = {:.20e}\n", + " rel_err = {:.20e}" + ), + ma, + ea, + mb, + eb, + actual, + expected, + err + ); + + checked.fetch_add(1, Ordering::Relaxed); + } + + let done = done_counter.fetch_add(1, Ordering::Relaxed) + 1; + if done % 10_000 == 0 { + let progress = done as f64 / total_cases as f64 * 100.0; + log::debug!("div progress = {progress:.4}%"); + } + }); + + let checked = checked.load(Ordering::Relaxed); + let skipped_non_finite = skipped_non_finite.load(Ordering::Relaxed); + let skipped_invalid_sf = skipped_invalid_sf.load(Ordering::Relaxed); + + println!( + "div checked={}, skipped_non_finite={}, skipped_invalid_sf={}, mantissas={}, exponents={}, total_cases={}", + checked, + skipped_non_finite, + skipped_invalid_sf, + mantissas.len(), + exponents.len(), + total_cases, + ); + + assert!(checked > 0, "div test did not validate any finite cases"); + } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5e3436a78c..9c9697bed6 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -268,7 +268,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 390, + spec_version: 391, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -1210,8 +1210,8 @@ impl pallet_subtensor_swap::Config for Runtime { type SubnetInfo = SubtensorModule; type BalanceOps = SubtensorModule; type ProtocolId = SwapProtocolId; - type TaoReserve = pallet_subtensor::TaoCurrencyReserve; - type AlphaReserve = pallet_subtensor::AlphaCurrencyReserve; + type TaoReserve = pallet_subtensor::TaoBalanceReserve; + type AlphaReserve = pallet_subtensor::AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity;