diff --git a/Cargo.lock b/Cargo.lock index c9ba8aede2..b9c92f94b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,17 @@ dependencies = [ "subtle 2.6.1", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -497,7 +508,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" dependencies = [ - "ahash", + "ahash 0.8.12", "ark-ff 0.5.0", "ark-poly 0.5.0", "ark-serialize 0.5.0", @@ -732,7 +743,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" dependencies = [ - "ahash", + "ahash 0.8.12", "ark-ff 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", @@ -1669,6 +1680,29 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases 0.2.1", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "bounded-collections" version = "0.1.9" @@ -1944,6 +1978,28 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.24.0" @@ -4053,6 +4109,16 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "endian-cast" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f7a506e5de77a3db9e56fdbed17fa6f3b8d27ede81545dde96107c3d6a1d2" +dependencies = [ + "generic-array 1.3.5", + "typenum", +] + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -5492,6 +5558,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic-array" +version = "1.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" +dependencies = [ + "rustversion", + "typenum", +] + [[package]] name = "gethostname" version = "0.2.3" @@ -5691,6 +5767,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -5698,7 +5777,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -5707,7 +5786,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.12", "allocator-api2", "serde", ] @@ -5729,6 +5808,8 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.2.0", "serde", ] @@ -6850,6 +6931,33 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lencode" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83dc280ed78264020f986b2539e6a44e0720f98f66c99a48a2f52e4a441e99d8" +dependencies = [ + "endian-cast", + "generic-array 1.3.5", + "hashbrown 0.16.0", + "lencode-macros", + "newt-hype", + "ruint", + "zstd-safe 7.2.4", +] + +[[package]] +name = "lencode-macros" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c57df14b9005d1e4e8e56436e922e2c046ad0be55d7cfb062a303714857508" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "libc" version = "0.2.176" @@ -8151,6 +8259,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "newt-hype" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8b7b69b0eafaa88ec8dc9fe7c3860af0a147517e5207cfbd0ecd21cd7cde18" + [[package]] name = "nix" version = "0.26.4" @@ -13472,6 +13586,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quanta" version = "0.12.6" @@ -13580,6 +13714,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoth" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d9da82a5dc3ff2fb2eee43d2b434fb197a9bf6a2a243850505b61584f888d2" +dependencies = [ + "quoth-macros", + "regex", + "rust_decimal", + "safe-string", +] + +[[package]] +name = "quoth-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58547202bec9896e773db7ef04b4d47c444f9c97bc4386f36e55718c347db440" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "r-efi" version = "5.3.0" @@ -13858,6 +14015,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "resolv-conf" version = "0.7.5" @@ -13912,6 +14078,35 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rlp" version = "0.5.2" @@ -14147,6 +14342,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec 0.7.6", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -14402,6 +14613,18 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "safe-bigmath" +version = "0.4.1" +source = "git+https://github.com/sam0x17/safe-bigmath?rev=013c499#013c49984910e1c9a23289e8c85e7a856e263a02" +dependencies = [ + "lencode", + "num-bigint", + "num-integer", + "num-traits", + "quoth", +] + [[package]] name = "safe-math" version = "0.1.0" @@ -14421,6 +14644,12 @@ dependencies = [ "rustc_version 0.2.3", ] +[[package]] +name = "safe-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fc51f1e562058dee569383bfdb5a58752bfeb7fa7f0823f5c07c4c45381b5a" + [[package]] name = "safe_arch" version = "0.7.4" @@ -14842,7 +15071,7 @@ name = "sc-consensus-grandpa" version = "0.36.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=81fa2c54e94f824eba7dabe9dffd063481cb2d80#81fa2c54e94f824eba7dabe9dffd063481cb2d80" dependencies = [ - "ahash", + "ahash 0.8.12", "array-bytes 6.2.3", "async-trait", "dyn-clone", @@ -15145,7 +15374,7 @@ name = "sc-network-gossip" version = "0.51.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=81fa2c54e94f824eba7dabe9dffd063481cb2d80#81fa2c54e94f824eba7dabe9dffd063481cb2d80" dependencies = [ - "ahash", + "ahash 0.8.12", "futures", "futures-timer", "log", @@ -15858,7 +16087,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" dependencies = [ - "ahash", + "ahash 0.8.12", "cfg-if", "hashbrown 0.13.2", ] @@ -15922,6 +16151,12 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.7.3" @@ -16310,7 +16545,12 @@ dependencies = [ name = "share-pool" version = "0.1.0" dependencies = [ + "approx", + "lencode", + "parity-scale-codec", + "safe-bigmath", "safe-math", + "scale-info", "sp-std", "substrate-fixed", ] @@ -16353,6 +16593,12 @@ dependencies = [ "wide", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simple-dns" version = "0.9.3" @@ -17433,7 +17679,7 @@ name = "sp-trie" version = "40.0.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=81fa2c54e94f824eba7dabe9dffd063481cb2d80#81fa2c54e94f824eba7dabe9dffd063481cb2d80" dependencies = [ - "ahash", + "ahash 0.8.12", "foldhash 0.1.5", "hash-db", "hashbrown 0.15.5", @@ -18081,7 +18327,7 @@ dependencies = [ name = "subtensor-macros" version = "0.1.0" dependencies = [ - "ahash", + "ahash 0.8.12", "proc-macro2", "quote", "syn 2.0.106", @@ -19675,7 +19921,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c128c039340ffd50d4195c3f8ce31aac357f06804cfc494c8b9508d4b30dca4" dependencies = [ - "ahash", + "ahash 0.8.12", "hashbrown 0.14.5", "string-interner", ] @@ -20885,6 +21131,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.16+zstd.1.5.7" diff --git a/Cargo.toml b/Cargo.toml index 1d65a3cd5e..65904b6c68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ pallet-subtensor-swap-runtime-api = { path = "pallets/swap/runtime-api", default pallet-subtensor-swap-rpc = { path = "pallets/swap/rpc", default-features = false } procedural-fork = { path = "support/procedural-fork", default-features = false } safe-math = { path = "primitives/safe-math", default-features = false } +safe-bigmath = { rev = "013c499", package = "safe-bigmath", default-features = false, git = "https://github.com/sam0x17/safe-bigmath" } share-pool = { path = "primitives/share-pool", default-features = false } subtensor-macros = { path = "support/macros", default-features = false } subtensor-custom-rpc = { default-features = false, path = "pallets/subtensor/rpc" } @@ -82,6 +83,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 } diff --git a/common/src/lib.rs b/common/src/lib.rs index 658f8b2e01..7bd3884b7a 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -257,7 +257,7 @@ pub trait BalanceOps { hotkey: &AccountId, netuid: NetUid, alpha: AlphaCurrency, - ) -> Result; + ) -> Result<(), DispatchError>; } pub mod time { diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 2091946598..bddbaed60e 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -516,7 +516,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, @@ -524,7 +524,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); } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 6ae43ac384..ecad1eeba7 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -88,6 +88,7 @@ pub mod pallet { use frame_system::pallet_prelude::*; use pallet_drand::types::RoundNumber; use runtime_common::prod_or_fast; + use share_pool::SafeFloatSerializable; use sp_core::{ConstU32, H160, H256}; use sp_runtime::traits::{Dispatchable, TrailingZeroInput}; use sp_std::collections::btree_map::BTreeMap; @@ -1437,6 +1438,31 @@ pub mod pallet { ValueQuery, >; + /// DMAP ( hot, netuid ) --> total_alpha_shares | Returns the number of alpha shares for a hotkey on a subnet, stores bigmath vector. + #[pallet::storage] + pub type TotalHotkeySharesV2 = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, // hot + Identity, + NetUid, // subnet + SafeFloatSerializable, // Hotkey shares in unlimited precision + ValueQuery, + >; + + /// --- NMAP ( hot, cold, netuid ) --> alpha | Returns the alpha shares for a hotkey, coldkey, netuid triplet, stores bigmath vector. + #[pallet::storage] + pub type AlphaV2 = StorageNMap< + _, + ( + NMapKey, // hot + NMapKey, // cold + NMapKey, // subnet + ), + SafeFloatSerializable, // Shares in unlimited precision + ValueQuery, + >; + /// Contains last Alpha storage map key to iterate (check first) #[pallet::storage] pub type AlphaMapLastKey = @@ -2635,12 +2661,17 @@ impl> hotkey: &T::AccountId, netuid: NetUid, alpha: AlphaCurrency, - ) -> 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); diff --git a/pallets/subtensor/src/rpc_info/delegate_info.rs b/pallets/subtensor/src/rpc_info/delegate_info.rs index bf6dafd332..223402df39 100644 --- a/pallets/subtensor/src/rpc_info/delegate_info.rs +++ b/pallets/subtensor/src/rpc_info/delegate_info.rs @@ -6,6 +6,7 @@ use substrate_fixed::types::U64F64; extern crate alloc; use alloc::collections::BTreeMap; use codec::Compact; +use share_pool::SafeFloat; use subtensor_runtime_common::{AlphaCurrency, NetUid}; #[freeze_struct("1fafc4fcf28cba7a")] @@ -65,8 +66,12 @@ 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_float_serializable) in + AlphaV2::::iter_prefix((delegate.clone(),)) + { + let alpha_stake = SafeFloat::from(&alpha_stake_float_serializable); + + if alpha_stake.is_zero() { continue; } diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index 5229971ed0..47fc75bd63 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/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 1aeeacc33c..26be257168 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, SafeFloatSerializable, 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::{AlphaCurrency, Currency, NetUid, TaoCurrency}; use subtensor_swap_interface::{Order, SwapHandler, SwapResult}; @@ -514,7 +514,7 @@ impl Pallet { coldkey: &T::AccountId, netuid: NetUid, amount: AlphaCurrency, - ) -> AlphaCurrency { + ) { if !amount.is_zero() { let mut staking_hotkeys = StakingHotkeys::::get(coldkey); if !staking_hotkeys.contains(hotkey) { @@ -526,11 +526,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( @@ -558,22 +554,16 @@ impl Pallet { coldkey: &T::AccountId, netuid: NetUid, amount: AlphaCurrency, - ) -> AlphaCurrency { + ) { 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. @@ -694,15 +684,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) @@ -736,7 +724,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(), )); @@ -746,7 +734,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 ); @@ -784,17 +772,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(AlphaCurrency::ZERO); - } + ); // Step 4: Update the list of hotkeys staking for this coldkey let mut staking_hotkeys = StakingHotkeys::::get(coldkey); @@ -856,7 +839,7 @@ impl Pallet { alpha: AlphaCurrency, ) -> 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, @@ -871,17 +854,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(), ); } @@ -890,7 +873,7 @@ impl Pallet { let current_price = ::SwapInterface::current_alpha_price(netuid.into()); let tao_equivalent: TaoCurrency = current_price - .saturating_mul(U96F32::saturating_from_num(actual_alpha_moved)) + .saturating_mul(U96F32::saturating_from_num(alpha)) .saturating_to_num::() .into(); @@ -919,7 +902,7 @@ impl Pallet { origin_coldkey.clone(), origin_hotkey.clone(), tao_equivalent, - actual_alpha_decrease, + alpha, netuid, 0_u64, // 0 fee )); @@ -927,7 +910,7 @@ impl Pallet { destination_coldkey.clone(), destination_hotkey.clone(), tao_equivalent, - actual_alpha_moved, + alpha, netuid, 0_u64, // 0 fee )); @@ -1306,47 +1289,52 @@ 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 { + let share_serializable = AlphaV2::::get((&(self.hotkey), key, self.netuid)); + SafeFloat::from(&share_serializable) } - 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 { + let maybe_share_serializable = AlphaV2::::try_get((&(self.hotkey), key, self.netuid)); + if let Ok(share_serializable) = maybe_share_serializable { + Ok(SafeFloat::from(&share_serializable)) + } else { + Err(()) + } } - fn get_denominator(&self) -> U64F64 { - crate::TotalHotkeyShares::::get(&(self.hotkey), self.netuid) + fn get_denominator(&self) -> SafeFloat { + let denominator_serializable = TotalHotkeySharesV2::::get(&(self.hotkey), self.netuid); + SafeFloat::from(&denominator_serializable) } - 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, - AlphaCurrency::from(value.saturating_to_num::()), - ); + TotalHotkeyAlpha::::insert(&(self.hotkey), self.netuid, AlphaCurrency::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) { + if !share.is_zero() { + let float_serializable = SafeFloatSerializable::from(&share); + AlphaV2::::insert((&self.hotkey, key, self.netuid), float_serializable); } 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) { + if !update.is_zero() { + let float_serializable = SafeFloatSerializable::from(&update); + TotalHotkeySharesV2::::insert(&self.hotkey, self.netuid, float_serializable); } else { - crate::TotalHotkeyShares::::remove(&self.hotkey, self.netuid); + TotalHotkeySharesV2::::remove(&self.hotkey, self.netuid); } } } diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs index 19af1303c1..d624798c29 100644 --- a/pallets/swap-interface/src/lib.rs +++ b/pallets/swap-interface/src/lib.rs @@ -51,6 +51,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: TaoCurrency) -> AlphaCurrency; } pub trait DefaultPriceLimit diff --git a/pallets/swap/src/mock.rs b/pallets/swap/src/mock.rs index aacdf90835..c7adb4d08e 100644 --- a/pallets/swap/src/mock.rs +++ b/pallets/swap/src/mock.rs @@ -264,9 +264,9 @@ impl BalanceOps for MockBalanceOps { _coldkey: &AccountId, _hotkey: &AccountId, _netuid: NetUid, - alpha: AlphaCurrency, - ) -> Result { - Ok(alpha) + _alpha: AlphaCurrency, + ) -> Result<(), DispatchError> { + Ok(()) } } diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 6ec02879bf..2b99f02a98 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1160,4 +1160,16 @@ 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: TaoCurrency) -> AlphaCurrency { + // This is a mock implementation, waiting to merge balancer + // TODO: When balancer is merged, simulate with slippage + let alpha_price = Self::current_price(netuid.into()); + AlphaCurrency::from( + U96F32::from(u64::from(tao_amount)) + .safe_div(alpha_price) + .saturating_to_num::(), + ) + } } diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index 97a25ec242..93a0fe627c 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -405,13 +405,11 @@ mod pallet { let tao_provided = T::BalanceOps::decrease_balance(&coldkey, tao)?; ensure!(tao_provided == tao, Error::::InsufficientBalance); - let alpha_provided = - T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), alpha)?; - ensure!(alpha_provided == alpha, Error::::InsufficientBalance); + T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), alpha)?; // Add provided liquidity to user-provided reserves T::TaoReserve::increase_provided(netuid.into(), tao_provided); - T::AlphaReserve::increase_provided(netuid.into(), alpha_provided); + T::AlphaReserve::increase_provided(netuid.into(), alpha); // Emit an event Self::deposit_event(Event::LiquidityAdded { @@ -527,12 +525,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 4013248abb..38bb9453e8 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -2268,15 +2268,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(), AlphaCurrency::from(need_alpha)); // Users‑only liquidation. assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2335,14 +2330,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(), AlphaCurrency::from(alpha_needed)); // --- Act: users‑only dissolve. assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2410,15 +2405,13 @@ fn refund_alpha_multiple_providers_proportional_to_principal() { 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); + ::BalanceOps::decrease_stake(&c1, &h1, netuid.into(), a1.into()) + .expect("decrease α #1"); + AlphaReserve::increase_provided(netuid.into(), AlphaCurrency::from(a1)); - 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 α #2"); + AlphaReserve::increase_provided(netuid.into(), AlphaCurrency::from(a2)); // Act assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2472,15 +2465,13 @@ fn refund_alpha_same_cold_multiple_hotkeys_conserved_to_owner() { 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); + ::BalanceOps::decrease_stake(&cold, &hot1, netuid.into(), a1.into()) + .expect("decr α #hot1"); + AlphaReserve::increase_provided(netuid.into(), AlphaCurrency::from(a1)); - 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 α #hot2"); + AlphaReserve::increase_provided(netuid.into(), AlphaCurrency::from(a2)); // Act assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2562,7 +2553,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(), @@ -2571,7 +2562,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(), AlphaCurrency::from(alpha_needed)); // --- Act: dissolve (GREEN PATH: permitted validators exist) --- assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); diff --git a/pallets/transaction-fee/src/lib.rs b/pallets/transaction-fee/src/lib.rs index fc2a16a409..53fa613630 100644 --- a/pallets/transaction-fee/src/lib.rs +++ b/pallets/transaction-fee/src/lib.rs @@ -30,7 +30,6 @@ use subtensor_swap_interface::SwapHandler; use core::marker::PhantomData; use smallvec::smallvec; use sp_std::vec::Vec; -use substrate_fixed::types::U96F32; use subtensor_runtime_common::{Balance, Currency, NetUid}; // Tests @@ -119,9 +118,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. /// @@ -145,13 +144,15 @@ where // 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( + let alpha_balance = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, *netuid, - ), + ); + let alpha_per_entry = pallet_subtensor_swap::Pallet::::get_alpha_amount_for_tao( + *netuid, + tao_per_entry.into(), ); - let alpha_price = pallet_subtensor_swap::Pallet::::current_alpha_price(*netuid); - alpha_price.saturating_mul(alpha_balance) >= tao_per_entry + alpha_balance >= alpha_per_entry }) } @@ -165,28 +166,34 @@ where } let tao_per_entry = tao_amount.checked_div(alpha_vec.len() as u64).unwrap_or(0); - - alpha_vec.iter().for_each(|(hotkey, netuid)| { - // Divide tao_amount evenly among all alpha entries - let alpha_balance = U96F32::saturating_from_num( - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - hotkey, coldkey, *netuid, - ), - ); - 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( - hotkey, - coldkey, - *netuid, - alpha_fee.into(), - ); - }); + if !tao_per_entry.is_zero() { + alpha_vec.iter().for_each(|(hotkey, netuid)| { + // Divide tao_amount evenly among all alpha entries + 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_per_entry.into(), + ); + 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 _ = pallet_subtensor::Pallet::::unstake_from_subnet( + hotkey, + coldkey, + *netuid, + alpha_fee, + 0.into(), + true, + ); + }); + } } fn get_all_netuids_for_coldkey_and_hotkey( @@ -308,7 +315,7 @@ where fn withdraw_fee( who: &AccountIdOf, - _call: &CallOf, + call: &CallOf, _dispatch_info: &DispatchInfoOf>, fee: Self::Balance, _tip: Self::Balance, @@ -327,12 +334,12 @@ 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.into(); + OU::withdraw_in_alpha(who, &alpha_vec, fee_u64); + return Ok(Some(WithdrawnFee::Alpha)); + } Err(InvalidTransaction::Payment.into()) } } @@ -340,7 +347,7 @@ where fn can_withdraw_fee( who: &AccountIdOf, - _call: &CallOf, + call: &CallOf, _dispatch_info: &DispatchInfoOf>, fee: Self::Balance, _tip: Self::Balance, @@ -353,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.into(); + if OU::can_withdraw_in_alpha(who, &alpha_vec, fee_u64) { + return Ok(()); + } + } Err(InvalidTransaction::Payment.into()) } } diff --git a/pallets/transaction-fee/src/tests/mod.rs b/pallets/transaction-fee/src/tests/mod.rs index b6697e87f0..558b4f6677 100644 --- a/pallets/transaction-fee/src/tests/mod.rs +++ b/pallets/transaction-fee/src/tests/mod.rs @@ -74,7 +74,6 @@ fn test_remove_stake_fees_tao() { // 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; @@ -143,7 +142,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; @@ -202,7 +200,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; @@ -254,7 +251,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; @@ -385,7 +381,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; @@ -524,7 +519,6 @@ fn test_remove_stake_failing_transaction_tao_fees() { // // 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; @@ -590,7 +584,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; @@ -604,8 +597,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 = AlphaCurrency::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); @@ -658,7 +655,6 @@ fn test_remove_stake_limit_fees_alpha() { // 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; @@ -731,7 +727,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; @@ -799,7 +794,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; @@ -871,7 +865,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); @@ -944,7 +937,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; @@ -1015,7 +1007,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; @@ -1088,7 +1079,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; @@ -1150,7 +1140,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; diff --git a/primitives/share-pool/Cargo.toml b/primitives/share-pool/Cargo.toml index ba42b0d77d..b5da06cff6 100644 --- a/primitives/share-pool/Cargo.toml +++ b/primitives/share-pool/Cargo.toml @@ -4,8 +4,13 @@ version = "0.1.0" edition.workspace = true [dependencies] +approx.workspace = true +codec.workspace = true +lencode.workspace = true +scale-info.workspace = true substrate-fixed.workspace = true sp-std.workspace = true +safe-bigmath.workspace = true safe-math.workspace = true [lints] @@ -13,4 +18,12 @@ workspace = true [features] default = ["std"] -std = ["substrate-fixed/std", "sp-std/std", "safe-math/std"] +std = [ + "codec/std", + "lencode/std", + "scale-info/std", + "substrate-fixed/std", + "sp-std/std", + "safe-math/std", + "safe-bigmath/std" +] diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index d43f36259c..f56498c383 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -1,26 +1,329 @@ #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::result_unit_err)] -use safe_math::*; +use codec::{Decode, Encode}; +use lencode::io::Cursor; +use lencode::{Decode as LenDecode, Encode as LenEncode}; +use safe_bigmath::*; +use scale_info::TypeInfo; use sp_std::marker; use sp_std::ops::Neg; -use substrate_fixed::types::{I64F64, U64F64}; + +// Maximum value that can be represented 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). +#[derive(Clone, Debug)] +pub struct SafeFloat { + mantissa: SafeInt, + exponent: i64, +} + +#[derive(Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct SafeFloatSerializable { + mantissa: Vec, + exponent: i64, +} + +/// Power of 10 in SafeInt +/// Uses SafeInt pow function that accepts u32 argument +/// and the formula: 10^(a*b) = (10^a)^b +fn pow10(e: u64) -> SafeInt { + if e == 0 { + return SafeInt::one(); + } + let exp_high = ((e & 0xFFFFFFFF00000000) >> 32) as u32; + let exp_low = (e & 0xFFFFFFFF) as u32; + let ten_exp_low = SafeInt::from(10u32).pow(exp_low); + let ten_exp_high = SafeInt::from(10u32).pow(exp_high); + let two_exp_16 = 1u32 << 16; + + ten_exp_high.pow(two_exp_16).pow(two_exp_16) * ten_exp_low +} + +fn intlog10(a: &SafeInt) -> u64 { + let scale = SafeInt::from(1_000_000_000_000_000_000i128); + let precision = 256u32; + let max_iters = Some(4096); + (a.log10(&scale, precision, max_iters)) + .unwrap_or_default() + .to_u64() + .unwrap_or_default() +} + +impl SafeFloat { + pub fn zero() -> Self { + SafeFloat { + mantissa: SafeInt::zero(), + exponent: 0_i64, + } + } + + pub fn new(mantissa: SafeInt, exponent: i64) -> Option { + // Cap at SAFE_FLOAT_MAX + let max_value = SafeInt::from(SAFE_FLOAT_MAX) + SafeInt::one(); + if !(mantissa.clone() / max_value).unwrap_or_default().is_zero() { + return None; + } + + let mut safe_float = SafeFloat { mantissa, exponent }; + + safe_float.adjust_precision(); + Some(safe_float) + } + + /// Adjusts mantissa and exponent of this floating point number so that + /// SAFE_FLOAT_MAX <= mantissa < 10 * SAFE_FLOAT_MAX + pub(crate) fn adjust_precision(&mut self) { + let max_value = SafeInt::from(SAFE_FLOAT_MAX); + let max_value_div10 = SafeInt::from(SAFE_FLOAT_MAX.checked_div(10).unwrap_or_default()); + let mantissa_abs = self.mantissa.clone().abs(); + + let exponent_adjustment: i64 = if mantissa_abs.is_zero() { + 0i64 + } else if max_value_div10 >= mantissa_abs { + // Mantissa is too low, upscale mantissa + reduce exponent + let scale = (max_value_div10 / mantissa_abs).unwrap_or_default(); + -1 * (intlog10(&scale) + 1) as i64 + } else if max_value < mantissa_abs { + // Mantissa is too high, downscale mantissa + increase exponent + let scale = (mantissa_abs / max_value).unwrap_or_default(); + (intlog10(&scale) + 1) as i64 + } else { + 0i64 + }; + + self.exponent = self.exponent + exponent_adjustment; + + if exponent_adjustment > 0 { + let mantissa_adjustment = pow10(exponent_adjustment as u64); + self.mantissa = (self.mantissa.clone() / mantissa_adjustment).unwrap_or_default(); + } else { + let mantissa_adjustment = pow10(exponent_adjustment.neg() as u64); + self.mantissa = self.mantissa.clone() * mantissa_adjustment + } + } + + /// 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 { + // We need to offset exponent so that + // 1. e1 - e2 is non-negative + // 2. We have enough precision after division + let redundant_exponent = SAFE_FLOAT_MAX_EXP.saturating_mul(2); + + let maybe_new_mantissa = + self.mantissa.clone() * pow10(redundant_exponent as u64) / a.mantissa.clone(); + if let Some(new_mantissa) = maybe_new_mantissa { + let mut safe_float = SafeFloat { + mantissa: new_mantissa, + exponent: self + .exponent + .saturating_sub(a.exponent) + .saturating_sub(redundant_exponent), + }; + safe_float.adjust_precision(); + Some(safe_float) + } else { + None + } + } + + pub fn add(&self, a: &SafeFloat) -> Self { + // Multiply both operands by 10^exponent_offset so that both are above 1. + // (lowest exponent becomes 0) + let exponent_offset = self.exponent.min(a.exponent).neg(); + let unnormalized_mantissa = self.mantissa.clone() + * pow10(self.exponent.saturating_add(exponent_offset) as u64) + + a.mantissa.clone() * pow10(a.exponent.saturating_add(exponent_offset) as u64); + + let mut safe_float = SafeFloat { + mantissa: unnormalized_mantissa, + exponent: exponent_offset.neg(), + }; + safe_float.adjust_precision(); + safe_float + } + + /// Calculate self * a / b without loss of precision + pub fn mul_div(&self, a: &SafeFloat, b: &SafeFloat) -> Option { + let self_a_mantissa = self.mantissa.clone() * a.mantissa.clone(); + let self_a_exponent = self.exponent.saturating_add(a.exponent); + + // Divide by b without adjusting precision first (preserve higher precision + // of multiplication result) + SafeFloat { + mantissa: self_a_mantissa, + exponent: self_a_exponent, + } + .div(b) + } + + pub fn is_zero(&self) -> bool { + self.mantissa.is_zero() + } + + /// Returns true if self > a + pub fn gt(&self, a: &SafeFloat) -> bool { + // Shortcut: same exponent → compare mantissas directly + if self.exponent == a.exponent { + return self.mantissa > a.mantissa; + } + + // Bring both to the same exponent = max(exponents) + let max_e = self.exponent.max(a.exponent); + let k1 = max_e - self.exponent; + let k2 = max_e - a.exponent; + + let scale1 = pow10(k1 as u64); + let scale2 = pow10(k2 as u64); + + let lhs = &self.mantissa * &scale1; + let rhs = &a.mantissa * &scale2; + + lhs - rhs > 0 + } +} + +// Saturating conversion: negatives -> 0, overflow -> u64::MAX +impl From<&SafeFloat> for u64 { + fn from(value: &SafeFloat) -> Self { + // Negative values are clamped to 0 + if value.mantissa.is_negative() { + return 0; + } + + // If exponent is zero, it's just an integer mantissa + if value.exponent == 0 { + return value.mantissa.to_u64().unwrap_or(u64::MAX); + } + + // scale = 10^exponent + let scale = pow10(value.exponent.abs() as u64); + + // mantissa * 10^exponent + let q: SafeInt = if value.exponent > 0 { + &value.mantissa * &scale + } else { + (&value.mantissa / &scale).unwrap_or_else(SafeInt::zero) + }; + + // Convert quotient to u64, saturating on overflow + if q.is_zero() { + 0 + } else { + q.to_u64().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(SafeInt::from(value), 0).unwrap_or_default() + } +} + +impl From<&SafeFloat> for SafeFloatSerializable { + fn from(value: &SafeFloat) -> Self { + let mut mantissa_serializable = Vec::new(); + value + .mantissa + .encode(&mut mantissa_serializable) + .unwrap_or_default(); + + SafeFloatSerializable { + mantissa: mantissa_serializable, + exponent: value.exponent, + } + } +} + +impl From<&SafeFloatSerializable> for SafeFloat { + fn from(value: &SafeFloatSerializable) -> Self { + let decoded = SafeInt::decode(&mut Cursor::new(&value.mantissa)).unwrap_or_default(); + SafeFloat { + mantissa: decoded, + exponent: value.exponent, + } + } +} + +impl From<&SafeFloat> for f64 { + fn from(value: &SafeFloat) -> Self { + // Zero shortcut + if value.mantissa.is_zero() { + return 0.0; + } + + // If you ever allow negative mantissas, handle sign here. + // For now we assume mantissa >= 0 per your spec. + let mut mant = value.mantissa.clone(); + let mut exp_i32 = value.exponent as i32; + + let ten = SafeInt::from(10); + + // Max integer exactly representable in f64: 2^53 - 1 + let max_exact = SafeInt::from((1u64 << 53) - 1); + + // While mantissa is too large to be exactly represented, + // discard right decimal digits: mant /= 10, and adjust exponent + // so that mant * 10^exp stays the same value. + while mant > max_exact { + mant = (&mant / &ten).unwrap_or_default(); + exp_i32 += 1; // because value = mant * 10^exp, and we did mant /= 10 + } + + // Now mant <= max_exact, so we can convert mant to u64 then to f64 exactly. + let mant_u64 = mant.to_u64().unwrap_or_default(); + + let mant_f = mant_u64 as f64; + let scale = 10f64.powi(exp_i32); + + mant_f * scale + } +} + +impl From for f64 { + fn from(value: SafeFloat) -> Self { + f64::from(&value) + } +} + +impl Default for SafeFloat { + fn default() -> Self { + SafeFloat::zero() + } +} 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 +350,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(SafeInt::from(self.state_ops.get_shared_value()), 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(SafeFloat::zero()) + .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(SafeInt::from(self.state_ops.get_shared_value()), 0).unwrap_or_default(); + let denominator: SafeFloat = self.state_ops.get_denominator(); + shared_value + .mul_div(¤t_share, &denominator) + .unwrap_or(SafeFloat::zero()) + .into() } pub fn try_get_value(&self, key: &K) -> Result { @@ -89,164 +380,128 @@ 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(SafeInt::from(shared_value), 0).unwrap_or_default(); + let update: SafeFloat = SafeFloat::new(SafeInt::from(update), 0).unwrap_or_default(); + update + .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(SafeInt::from(update), 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)), - ); - } 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); - } + let shares_per_update: SafeFloat = + self.get_shares_per_update(update, shared_value, &denominator); + + self.state_ops + .set_denominator(denominator.add(&shares_per_update)); + self.state_ops + .set_share(key, current_share.add(&shares_per_update)); } // 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)] mod tests { use super::*; + use approx::assert_abs_diff_eq; + use lencode::io::Cursor; + use lencode::{Decode, Encode}; 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 +509,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 +523,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 +533,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 +546,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 as f64, epsilon = 1.); assert!((value2 - 500_000_000).abs() <= 1); } @@ -331,26 +586,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); - pool.update_value_for_all(999_999_999_999_998); + // // Huge emission resulting in 1M Alpha + // // Both stakers should have 500k Alpha each + // pool.update_value_for_all(999_999_999_999_998); - pool.update_value_for_one(&1, -499_999_999_999_990); - pool.update_value_for_one(&2, -499_999_999_999_990); + // // 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); - pool.update_value_for_all(999_999_999_999_980); + // // Huge emission resulting in 1M Alpha + // // Both stakers should have 500k Alpha each + // pool.update_value_for_all(999_999_999_999_980); - 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; + // // 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); - // 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 +618,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 +677,271 @@ 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.0, 1.0), + (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(SafeInt::from(denominator_mantissa), denominator_exponent) + .unwrap(); + 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_safeint_serialization() { + let safe_int = SafeInt::from(12345); + let mut buf = Vec::new(); + safe_int.encode(&mut buf).unwrap(); + + let decoded = SafeInt::decode(&mut Cursor::new(&buf)).unwrap(); + assert_eq!(decoded, safe_int); + } + + #[test] + fn test_safefloat_adjust_precision() { + // Test case: mantissa, exponent, expected mantissa, expected exponent + [ + (1_u128, 0, 100_000_000_000_000_000_000_u128, -20_i64), + (0, 0, 0, 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), ( - 1_000, - 21_000_000_000_000_000, - 0.00001, - 0.00000000000000000043, + 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(SafeInt::from(m), e).unwrap(); + assert_eq!(a.mantissa, SafeInt::from(expected_m)); + assert_eq!(a.exponent, SafeInt::from(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 + 1 = 2 + ( + 1_u128, + 0, + 1_u128, + 0, + 200_000_000_000_000_000_000_u128, + -20_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_001_u128, + 2_i64, + ), + ( + 123_u128, + 1, + 1_u128, + 23, + 1_000_000_000_000_000_000_012_u128, + 2_i64, + ), + // --- New tests start here --- + + // 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_004_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 + ( + 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, + 1_000_000_000_000_000_000_042_u128, + 2_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, + 1_009_999_999_999_999_999_999_u128, + 2_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, + 1_000_000_000_000_000_099_999_u128, + 2_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, + 1_000_123_456_789_012_345_678_u128, + 3_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, ), ] - .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(|(m_a, e_a, m_b, e_b, expected_m, expected_e)| { + let a = SafeFloat::new(SafeInt::from(m_a), e_a).unwrap(); + let b = SafeFloat::new(SafeInt::from(m_b), e_b).unwrap(); + + let a_plus_b = a.add(&b); + let b_plus_a = b.add(&a); + + assert_eq!(a_plus_b.mantissa, SafeInt::from(expected_m)); + assert_eq!(a_plus_b.exponent, SafeInt::from(expected_e)); + assert_eq!(b_plus_a.mantissa, SafeInt::from(expected_m)); + assert_eq!(b_plus_a.exponent, SafeInt::from(expected_e)); }); } + + #[test] + fn test_safefloat_div() { + todo!() + // Test with f64 + } + + #[test] + fn test_safefloat_mul_div() { + todo!() + // Test with f64 + } }