diff --git a/Cargo.lock b/Cargo.lock index 980b8dede8..9a680bd41f 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" @@ -13476,6 +13590,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" @@ -13584,6 +13718,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" @@ -13862,6 +14019,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" @@ -13916,6 +14082,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" @@ -14151,6 +14346,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" @@ -14406,6 +14617,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" @@ -14425,6 +14648,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" @@ -14846,7 +15075,7 @@ name = "sc-consensus-grandpa" version = "0.36.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=df2f9b531e05ab2fa58a25113627c02d6fe96aaa#df2f9b531e05ab2fa58a25113627c02d6fe96aaa" dependencies = [ - "ahash", + "ahash 0.8.12", "array-bytes 6.2.3", "async-trait", "dyn-clone", @@ -15149,7 +15378,7 @@ name = "sc-network-gossip" version = "0.51.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=df2f9b531e05ab2fa58a25113627c02d6fe96aaa#df2f9b531e05ab2fa58a25113627c02d6fe96aaa" dependencies = [ - "ahash", + "ahash 0.8.12", "futures", "futures-timer", "log", @@ -15862,7 +16091,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", ] @@ -15926,6 +16155,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" @@ -16314,7 +16549,14 @@ dependencies = [ name = "share-pool" version = "0.1.0" dependencies = [ + "approx", + "lencode", + "log", + "num-traits", + "parity-scale-codec", + "safe-bigmath", "safe-math", + "scale-info", "sp-std", "substrate-fixed", ] @@ -16357,6 +16599,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" @@ -17437,7 +17685,7 @@ name = "sp-trie" version = "40.0.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=df2f9b531e05ab2fa58a25113627c02d6fe96aaa#df2f9b531e05ab2fa58a25113627c02d6fe96aaa" dependencies = [ - "ahash", + "ahash 0.8.12", "foldhash 0.1.5", "hash-db", "hashbrown 0.15.5", @@ -18085,7 +18333,7 @@ dependencies = [ name = "subtensor-macros" version = "0.1.0" dependencies = [ - "ahash", + "ahash 0.8.12", "proc-macro2", "quote", "syn 2.0.106", @@ -19680,7 +19928,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", ] @@ -20890,11 +21138,18 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "git+https://github.com/gztensor/zstd-safe#42cc34ef6abe5d35d982f6afefb5d7e4e69f5f18" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +source = "git+https://github.com/gztensor/zstd-sys#01e299b6ce8d08af5a3429f7ceb956f8355cf1aa" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 475ef831e5..e898f05305 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 } @@ -306,6 +308,8 @@ pow-faucet = [] [patch.crates-io] w3f-bls = { git = "https://github.com/opentensor/bls", branch = "fix-no-std" } +zstd-sys = { git = "https://github.com/gztensor/zstd-sys" } +zstd-safe = { git = "https://github.com/gztensor/zstd-safe" } # Patches automatically generated with `diener`: # `diener patch --target https://github.com/paritytech/polkadot-sdk --point-to-git https://github.com/opentensor/polkadot-sdk.git --point-to-git-commit 81fa2c54e94f824eba7dabe9dffd063481cb2d80 --crates-to-patch ../polkadot-sdk --ignore-unused` 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/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/subtensor/src/coinbase/block_step.rs b/pallets/subtensor/src/coinbase/block_step.rs index 6081edad19..27bede5797 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 2091946598..b335241bc6 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); } } @@ -618,7 +618,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 ba7e3dcbf6..fc811ed948 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -91,6 +91,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; @@ -1432,11 +1433,41 @@ 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 = 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 = @@ -2683,20 +2714,25 @@ 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); }); - 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/errors.rs b/pallets/subtensor/src/macros/errors.rs index f74a7657d8..701774632a 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, /// Subnet buyback exceeded the operation rate limit diff --git a/pallets/subtensor/src/macros/genesis.rs b/pallets/subtensor/src/macros/genesis.rs index f16f2f7a3a..84ccdb90d2 100644 --- a/pallets/subtensor/src/macros/genesis.rs +++ b/pallets/subtensor/src/macros/genesis.rs @@ -4,6 +4,7 @@ use frame_support::pallet_macros::pallet_section; /// This can later be imported into the pallet using [`import_section`]. #[pallet_section] mod genesis { + use share_pool::SafeFloat; use sp_core::crypto::Pair; use sp_core::sr25519::Pair as Sr25519Pair; @@ -91,20 +92,20 @@ mod genesis { SubnetOwner::::insert(netuid, hotkey.clone()); SubnetLocked::::insert(netuid, TaoCurrency::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), + SafeFloatSerializable::from(&SafeFloat::from(1_000_000_000)), ); TotalHotkeyAlpha::::insert( hotkey.clone(), netuid, AlphaCurrency::from(1_000_000_000), ); - TotalHotkeyShares::::insert( + TotalHotkeySharesV2::::insert( hotkey.clone(), netuid, - U64F64::saturating_from_num(1_000_000_000), + SafeFloatSerializable::from(&SafeFloat::from(1_000_000_000)), ); SubnetAlphaOut::::insert(netuid, AlphaCurrency::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 bf6dafd332..c72b19beb5 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 099f8e26b6..42951053d1 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, TaoCurrency}; 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,158 @@ 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: AlphaCurrency) { // 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: SafeFloatSerializable -> SafeFloat + let v2 = AlphaV2::::iter().map(|(key, val_sf_ser)| { + let sf: SafeFloat = SafeFloat::from(&val_sf_ser); + (key, sf) + }); + + // 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: SafeFloatSerializable -> SafeFloat + let v2 = AlphaV2::::iter_prefix(prefix).map(|(netuid, val_sf_ser)| { + let sf: SafeFloat = SafeFloat::from(&val_sf_ser); + (netuid, sf) + }); + + // 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: SafeFloatSerializable -> SafeFloat + let v2 = AlphaV2::::iter_prefix((prefix,)).map(|((coldkey, netuid), val_sf_ser)| { + let sf: SafeFloat = SafeFloat::from(&val_sf_ser); + ((coldkey, netuid), sf) + }); + + 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 4dd362028e..944971247b 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 74a6bf34a6..23196a2cd4 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 1aeeacc33c..7e141fcb57 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,84 @@ 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); + } + + 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 { + // 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_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 { + // 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); + } + + 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) { + // 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() { + 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) { + // 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() { + 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/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index 54b07d9dbf..8325e9d9e6 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, SafeFloatSerializable}; impl Pallet { /// Transfer all assets, stakes, subnet ownerships, and hotkey associations from `old_coldkey` to @@ -98,19 +98,45 @@ 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 = + SafeFloat::from(&AlphaV2::::get((&hotkey, old_coldkey, netuid))); + // Get the v2 alpha shares on the new (hot,coldkey) account. + let dest_alpha_v2: SafeFloat = + SafeFloat::from(&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), + SafeFloatSerializable::from(&new_dest_alpha), + ); + } + + // 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 a54a02a750..dec1624489 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, SafeFloatSerializable}; use sp_core::Get; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{Currency, MechId, NetUid}; @@ -162,8 +163,8 @@ impl Pallet { weight: &mut Weight, ) -> 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. @@ -217,21 +218,22 @@ impl Pallet { // 10. Alpha already update in perform_hotkey_swap_on_one_subnet // Update the StakingHotkeys for the case where hotkey staked by multiple coldkeys. - 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(()) } @@ -334,13 +336,27 @@ 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); - // TotalHotkeyAlpha::::remove(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 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 = SafeFloat::from(&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 = SafeFloat::from(&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, + SafeFloatSerializable::from(&total_old_plus_new_shares), + ); + weight.saturating_accrue(T::DbWeight::get().writes(3)); // 3. Swap all subnet specific info. @@ -505,6 +521,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), SafeFloatSerializable)> = + 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); @@ -518,9 +539,42 @@ 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), + SafeFloatSerializable::from(&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 = + SafeFloat::from(&AlphaV2::::take((new_hotkey, &coldkey, netuid))); + AlphaV2::::remove((old_hotkey, &coldkey, netuid)); + AlphaV2::::insert( (new_hotkey, &coldkey, netuid), - alpha.saturating_add(new_alpha), + SafeFloatSerializable::from( + &SafeFloat::from(&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 fca61172dc..5ca2a71fac 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, SafeFloatSerializable}; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{ConstU64, Get, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; @@ -1044,3 +1045,14 @@ pub fn commit_dummy(who: U256, netuid: NetUid) { hash )); } + +#[allow(dead_code)] +pub fn sfser_to_u128(sf_ser: &SafeFloatSerializable) -> u128 { + let alpha_f64: f64 = SafeFloat::from(sf_ser).into(); + alpha_f64 as u128 +} + +#[allow(dead_code)] +pub fn sfser_from_u64(val: u64) -> SafeFloatSerializable { + SafeFloatSerializable::from(&SafeFloat::from(val)) +} diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 4605ac8bef..da5d3a4064 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -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, TaoCurrency}; 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), sfser_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); // 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), sfser_from_u64(a1)); + AlphaV2::::insert((s2_hot, s2_cold, net), sfser_from_u64(a2)); - TotalHotkeyAlpha::::insert(s1_hot, net, AlphaCurrency::from(a1 as u64)); - TotalHotkeyAlpha::::insert(s2_hot, net, AlphaCurrency::from(a2 as u64)); + TotalHotkeyAlpha::::insert(s1_hot, net, AlphaCurrency::from(a1)); + TotalHotkeyAlpha::::insert(s2_hot, net, AlphaCurrency::from(a2)); let pot: u64 = 10_000; SubnetTAO::::insert(net, TaoCurrency::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), sfser_from_u64(1_000u64)); SubnetTAO::::insert(net, TaoCurrency::from(0)); // zero TAO SubtensorModule::set_subnet_locked_balance(net, TaoCurrency::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), sfser_from_u64(3u64)); + AlphaV2::::insert((s2h, s2c, net), sfser_from_u64(2u64)); SubnetTAO::::insert(net, TaoCurrency::from(1)); // TAO pot = 1 SubtensorModule::set_subnet_locked_balance(net, TaoCurrency::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 = sfser_to_u128(&AlphaV2::::get((h1, c1, netuid))); + let a2: u128 = sfser_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))); }); } @@ -855,7 +855,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] = sfser_to_u128(&AlphaV2::::get((hot[i], cold[i], netuid))); alpha_sum += alpha[i]; } @@ -936,7 +936,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()); @@ -1939,10 +1939,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 = sfser_to_u128(&amt); if a > 0 { alpha_pairs_per_net .entry(net) @@ -2085,7 +2085,7 @@ 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!( @@ -2177,7 +2177,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 = sfser_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); @@ -2196,7 +2196,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 = sfser_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 a49a029805..445692fbda 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, SafeFloatSerializable}; use sp_core::U256; -use substrate_fixed::types::{U64F64, U96F32}; +use substrate_fixed::types::U96F32; use subtensor_runtime_common::{AlphaCurrency, Currency as CurrencyT}; use subtensor_swap_interface::SwapHandler; @@ -546,75 +547,107 @@ 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 = TaoCurrency::from(1_000_000_000_u64); + let alpha_reserve = AlphaCurrency::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); // 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 = AlphaCurrency::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, + SafeFloatSerializable::from(&denominator), + ); + AlphaV2::::insert( + (&hotkey, &coldkey, netuid), + SafeFloatSerializable::from(&denominator), + ); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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), + AlphaCurrency::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 = TaoCurrency::from(1_000_000_000_u64); + let alpha_reserve = AlphaCurrency::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); // 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 = AlphaCurrency::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, + SafeFloatSerializable::from(&denominator), + ); + AlphaV2::::insert( + (&hotkey, &coldkey, netuid), + SafeFloatSerializable::from(&denominator), + ); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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), + AlphaCurrency::from(stake / 2) ); }); } diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 6c7e18b707..94931f8972 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, SafeFloatSerializable}; use sp_core::{Get, H256, U256}; use substrate_fixed::traits::FromFixed; use substrate_fixed::types::{I96F32, I110F18, U64F64, U96F32}; @@ -4079,9 +4080,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); @@ -4113,7 +4114,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, @@ -4127,16 +4128,13 @@ fn test_remove_99_9991_per_cent_stake_removes_all() { expected_balance.to_u64(), epsilon = 10, ); - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), - TaoCurrency::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); }); } @@ -5646,3 +5644,458 @@ 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, AlphaCurrency::from(stake)); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaCurrency::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), + SafeFloatSerializable::from(&SafeFloat::from(1_u64)), + ); + TotalHotkeySharesV2::::insert( + hotkey, + netuid, + SafeFloatSerializable::from(&SafeFloat::from(1_u64)), + ); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::from(stake)); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaCurrency::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, + SafeFloatSerializable::from(&SafeFloat::from(1_u64)), + ); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::from(stake)); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaCurrency::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, AlphaCurrency::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 = SafeFloat::from(&AlphaV2::::get((&hotkey, &coldkey, netuid))); + let migrated_denominator = + SafeFloat::from(&TotalHotkeySharesV2::::get(hotkey, netuid)); + + assert_eq!( + f64::from((migrated_share.div(&migrated_denominator)).unwrap()), + 1.0 + ); + }); +} + +#[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, AlphaCurrency::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 = sfser_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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 = sfser_from_u64(1_u64); + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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 = sfser_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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, AlphaCurrency::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 = sfser_from_u64(1_u64); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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 = sfser_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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, AlphaCurrency::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 = sfser_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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 = sfser_from_u64(1_u64); + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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 = sfser_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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, AlphaCurrency::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 = sfser_from_u64(1_u64); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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, AlphaCurrency::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 = sfser_from_u64(1_u64); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaCurrency::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 7d6cc6ad3c..46f8f6f629 100644 --- a/pallets/subtensor/src/tests/staking2.rs +++ b/pallets/subtensor/src/tests/staking2.rs @@ -5,6 +5,7 @@ use frame_support::{ dispatch::{GetDispatchInfo, Pays}, weights::Weight, }; +use share_pool::SafeFloat; use sp_core::U256; use subtensor_runtime_common::{AlphaCurrency, Currency, TaoCurrency}; use subtensor_swap_interface::SwapHandler; @@ -429,12 +430,10 @@ 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 = + SafeFloat::from(&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 +444,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 - { - AlphaCurrency::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 36d083344c..834218e1b1 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}; @@ -1359,21 +1360,19 @@ 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); - - 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: u64 = + SafeFloat::from(&AlphaV2::::get((delegate, new_coldkey, netuid))).into(); + assert_eq!(expected_stake, actual_stake_new.into()); + let actual_stake_old: u64 = + SafeFloat::from(&AlphaV2::::get((delegate, coldkey, netuid))).into(); + assert_eq!(actual_stake_old, 0u64); + + let actual_stake_new_2: u64 = + SafeFloat::from(&AlphaV2::::get((delegate, new_coldkey, netuid2))).into(); + assert_eq!(expected_stake, actual_stake_new_2.into()); + let actual_stake_old_2: u64 = + SafeFloat::from(&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 71191d1951..f8ea8da42a 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, SafeFloatSerializable}; use sp_core::{Get, H160, H256, U256}; use sp_runtime::SaturatedConversion; use substrate_fixed::types::U64F64; @@ -852,7 +853,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(|| { @@ -908,7 +909,13 @@ fn test_swap_stake_success() { ); assert_eq!( TotalHotkeyShares::::get(new_hotkey, netuid), - U64F64::from_num(shares) + U64F64::from_num(0) + ); + assert_eq!( + f64::from(SafeFloat::from(&TotalHotkeySharesV2::::get( + new_hotkey, netuid + ))), + shares.to_num::() ); assert_eq!( Alpha::::get((old_hotkey, coldkey, netuid)), @@ -916,7 +923,104 @@ 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(SafeFloat::from(&AlphaV2::::get(( + new_hotkey, coldkey, netuid + )))), + amount as f64 + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, old_hotkey), + AlphaCurrency::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, AlphaCurrency::from(amount)); + TotalHotkeyAlphaLastEpoch::::insert( + old_hotkey, + netuid, + AlphaCurrency::from(amount * 2), + ); + TotalHotkeySharesV2::::insert( + old_hotkey, + netuid, + SafeFloatSerializable::from(&SafeFloat::from(shares)), + ); + AlphaV2::::insert( + (old_hotkey, coldkey, netuid), + SafeFloatSerializable::from(&SafeFloat::from(U64F64::from_num(amount))), + ); + AlphaDividendsPerSubnet::::insert(netuid, old_hotkey, AlphaCurrency::from(amount)); + + // Perform the swap + SubtensorModule::perform_hotkey_swap_on_all_subnets( + &old_hotkey, + &new_hotkey, + &coldkey, + &mut weight, + ); + + // Verify the swap + assert_eq!( + TotalHotkeyAlpha::::get(old_hotkey, netuid), + AlphaCurrency::ZERO + ); + assert_eq!( + TotalHotkeyAlpha::::get(new_hotkey, netuid), + amount.into() + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(old_hotkey, netuid), + AlphaCurrency::ZERO + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(new_hotkey, netuid), + AlphaCurrency::from(amount * 2) + ); + assert_eq!( + f64::from(SafeFloat::from(&TotalHotkeySharesV2::::get( + old_hotkey, netuid + ))), + 0_f64 + ); + assert_eq!( + f64::from(SafeFloat::from(&TotalHotkeySharesV2::::get( + new_hotkey, netuid + ))), + shares.to_num::() + ); + assert_eq!( + f64::from(SafeFloat::from(&AlphaV2::::get(( + old_hotkey, coldkey, netuid + )))), + 0_f64 + ); + assert_eq!( + f64::from(SafeFloat::from(&AlphaV2::::get(( + new_hotkey, coldkey, netuid + )))), + amount as f64 ); assert_eq!( AlphaDividendsPerSubnet::::get(netuid, old_hotkey), @@ -958,8 +1062,8 @@ fn test_swap_stake_old_hotkey_not_exist() { &mut weight, ); - // 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 6e423c1269..3a2dd8b71d 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::{AlphaCurrency, Currency, NetUidStorageIndex, TaoC use super::mock::*; use crate::*; +use share_pool::{SafeFloat, SafeFloatSerializable}; use sp_core::{Get, H160, H256, U256}; use sp_runtime::SaturatedConversion; use substrate_fixed::types::U64F64; @@ -906,7 +907,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(|| { @@ -963,7 +964,13 @@ fn test_swap_stake_success() { ); assert_eq!( TotalHotkeyShares::::get(new_hotkey, netuid), - U64F64::from_num(shares) + U64F64::from_num(0) + ); + assert_eq!( + f64::from(SafeFloat::from(&TotalHotkeySharesV2::::get( + new_hotkey, netuid + ))), + shares.to_num::() ); assert_eq!( Alpha::::get((old_hotkey, coldkey, netuid)), @@ -971,7 +978,105 @@ 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(SafeFloat::from(&AlphaV2::::get(( + new_hotkey, coldkey, netuid + )))), + amount as f64 + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, old_hotkey), + AlphaCurrency::ZERO + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, new_hotkey), + AlphaCurrency::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); + let amount = 10_000; + let shares = U64F64::from_num(123456); + + // Initialize staking variables for old_hotkey + TotalHotkeyAlpha::::insert(old_hotkey, netuid, AlphaCurrency::from(amount)); + TotalHotkeyAlphaLastEpoch::::insert( + old_hotkey, + netuid, + AlphaCurrency::from(amount * 2), + ); + TotalHotkeySharesV2::::insert( + old_hotkey, + netuid, + SafeFloatSerializable::from(&SafeFloat::from(shares)), + ); + AlphaV2::::insert( + (old_hotkey, coldkey, netuid), + SafeFloatSerializable::from(&SafeFloat::from(U64F64::from_num(amount))), + ); + AlphaDividendsPerSubnet::::insert(netuid, old_hotkey, AlphaCurrency::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) + ),); + + // Verify the swap + assert_eq!( + TotalHotkeyAlpha::::get(old_hotkey, netuid), + AlphaCurrency::ZERO + ); + assert_eq!( + TotalHotkeyAlpha::::get(new_hotkey, netuid), + AlphaCurrency::from(amount) + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(old_hotkey, netuid), + AlphaCurrency::ZERO + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(new_hotkey, netuid), + AlphaCurrency::from(amount * 2) + ); + assert_eq!( + f64::from(SafeFloat::from(&TotalHotkeySharesV2::::get( + old_hotkey, netuid + ))), + 0_f64 + ); + assert_eq!( + f64::from(SafeFloat::from(&TotalHotkeySharesV2::::get( + new_hotkey, netuid + ))), + shares.to_num::() + ); + assert_eq!( + f64::from(SafeFloat::from(&AlphaV2::::get(( + old_hotkey, coldkey, netuid + )))), + 0_f64 + ); + assert_eq!( + f64::from(SafeFloat::from(&AlphaV2::::get(( + new_hotkey, coldkey, netuid + )))), + amount as f64 ); assert_eq!( AlphaDividendsPerSubnet::::get(netuid, old_hotkey), 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..e2add9b71e 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1160,4 +1160,45 @@ 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 (commented fn below) + let alpha_price = Self::current_price(netuid.into()); + AlphaCurrency::from( + U96F32::from(u64::from(tao_amount)) + .safe_div(alpha_price) + .saturating_to_num::(), + ) + } + + // fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoCurrency) -> AlphaCurrency { + // if !PalSwapInitialized::::get(netuid) { + // // If swap is uninitialized, fallback to no-slippage method + // let alpha_price = Self::current_price(netuid.into()); + // AlphaCurrency::from( + // U64F64::from(u64::from(tao_amount)) + // .safe_div(alpha_price) + // .saturating_to_num::(), + // ) + // } else { + // // Use the swap simulation (with slippage) + // let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + // let tao_reserve = T::TaoReserve::reserve(netuid.into()); + + // AlphaCurrency::from( + // if u64::from(tao_reserve) > u64::from(T::MinimumReserve::get()) { + // let balancer = SwapBalancer::::get(netuid); + // balancer.get_base_needed_for_quote( + // tao_reserve.into(), + // alpha_reserve.into(), + // tao_amount.into(), + // ) + // } else { + // u64::MAX + // }, + // ) + // } + // } } diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index b55df77fee..1ced165c44 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -399,16 +399,11 @@ mod pallet { // let tao = TaoCurrency::from(tao); // // Remove TAO and Alpha balances or fail transaction if they can't be removed exactly - // 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 { @@ -526,12 +521,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 cd18e665cd..1d8dba165e 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -2261,15 +2261,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)); @@ -2328,14 +2323,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)); @@ -2403,15 +2398,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)); @@ -2465,15 +2458,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)); @@ -2555,7 +2546,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(), @@ -2564,7 +2555,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/precompiles/src/staking.rs b/precompiles/src/staking.rs index 2ed8891a5c..9ba22f9409 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..021decbbf0 100644 --- a/primitives/share-pool/Cargo.toml +++ b/primitives/share-pool/Cargo.toml @@ -4,8 +4,15 @@ version = "0.1.0" edition.workspace = true [dependencies] +approx.workspace = true +codec.workspace = true +lencode.workspace = true +log.workspace = true +scale-info.workspace = true substrate-fixed.workspace = true sp-std.workspace = true +num-traits.workspace = true +safe-bigmath.workspace = true safe-math.workspace = true [lints] @@ -13,4 +20,14 @@ workspace = true [features] default = ["std"] -std = ["substrate-fixed/std", "sp-std/std", "safe-math/std"] +std = [ + "codec/std", + "lencode/std", + "log/std", + "scale-info/std", + "substrate-fixed/std", + "sp-std/std", + "num-traits/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..a7ad684965 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -1,26 +1,424 @@ #![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}; +#[cfg(not(feature = "std"))] +use num_traits::float::FloatCore as _; +use safe_bigmath::*; +use scale_info::TypeInfo; use sp_std::marker; use sp_std::ops::Neg; -use substrate_fixed::types::{I64F64, U64F64}; +use sp_std::vec::Vec; +use substrate_fixed::types::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 +#[allow( + clippy::arithmetic_side_effects, + reason = "SafeInt never overflows and never panics" +)] +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, + } + } + + #[allow( + clippy::arithmetic_side_effects, + reason = "SafeInt never overflows and never panics" + )] + 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 }; + + if safe_float.normalize() { + Some(safe_float) + } else { + None + } + } + + /// Adjusts mantissa and exponent of this floating point number so that + /// SAFE_FLOAT_MAX <= mantissa < 10 * SAFE_FLOAT_MAX + /// + /// Returns true in case of success or false if exponent over- or underflows + #[allow( + clippy::arithmetic_side_effects, + reason = "SafeInt never overflows and never panics" + )] + pub(crate) fn normalize(&mut self) -> bool { + 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(); + ((intlog10(&scale).saturating_add(1)) as i64).neg() + } 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).saturating_add(1)) as i64 + } else { + 0i64 + }; + + // Check exponent over- or underflows + let new_exponent_i128 = (self.exponent as i128).saturating_add(exponent_adjustment as i128); + if (i64::MIN as i128 <= new_exponent_i128) && (new_exponent_i128 <= i64::MAX as i128) { + self.exponent = new_exponent_i128 as i64; + } else { + return false; + } + + 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 + } + + // Check if adjusted mantissa turned into zero, in which case set exponent to 0. + if self.mantissa.is_zero() { + self.exponent = 0; + } + + true + } + + /// Divide current value by a preserving precision (SAFE_FLOAT_MAX digits in mantissa) + /// result = m1 * 10^e1 / m2 * 10^e2 + #[allow( + clippy::arithmetic_side_effects, + reason = "SafeInt never overflows and never panics" + )] + 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), + }; + if safe_float.normalize() { + Some(safe_float) + } else { + None + } + } else { + None + } + } + + #[allow( + clippy::arithmetic_side_effects, + reason = "SafeInt never overflows and never panics" + )] + pub fn add(&self, a: &SafeFloat) -> Option { + // 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(), + }; + if safe_float.normalize() { + Some(safe_float) + } else { + None + } + } + + /// Calculate self * a / b without loss of precision + #[allow( + clippy::arithmetic_side_effects, + reason = "SafeInt never overflows and never panics" + )] + 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 + #[allow( + clippy::arithmetic_side_effects, + reason = "SafeInt never overflows and never panics" + )] + 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 { + #[allow( + clippy::arithmetic_side_effects, + reason = "SafeInt never overflows and never panics" + )] + 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.unsigned_abs()); + + // 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 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(SafeInt::from(int), 0).unwrap_or_default(); + + // Numerator of fractional part: frac * 10^0 + let safe_frac_num = SafeFloat::new(SafeInt::from(frac), 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(SafeInt::from(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 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 { + #[allow( + clippy::arithmetic_side_effects, + reason = "This code is only used in tests" + )] + 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 +445,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_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(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_default() + .into() } pub fn try_get_value(&self, key: &K) -> Result { @@ -89,164 +475,155 @@ 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(); + 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. + let 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 + } + }; + + let 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 } + }; - self.state_ops.set_denominator(new_denominator); - self.state_ops.set_share(key, new_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 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 +631,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 +645,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 +655,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 +668,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 +708,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); - - 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_998); - pool.update_value_for_all(999_999_999_999_980); + // // 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_one(&1, 1_000_000_000_000); - pool.update_value_for_one(&2, 1_000_000_000_000); + // // Huge emission resulting in 1M Alpha + // // Both stakers should have 500k Alpha each + // pool.update_value_for_all(999_999_999_999_980); - 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 +740,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 +799,489 @@ 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(SafeInt::from(denominator_mantissa), 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_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_normalize() { + // 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), + ( + 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_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, + ), + // 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 + ( + 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 + ( + 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 + ( + 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_000, - 21_000_000_000_000_000, - 0.00001, - 0.00000000000000000043, + 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 ( - 21_000_000_000_000_000, - 21_000_000_000_000_000, - 0.00001, - 0.00001, + 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 ( - 210_000_000_000_000_000, - 21_000_000_000_000_000, - 0.00001, - 0.0001, + 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 ( - 1_000, - 1_000, - 21_000_000_000_000_000_f64, - 21_000_000_000_000_000_f64, + 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).unwrap(); + let b_plus_a = b.add(&a).unwrap(); + + 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_by_zero_is_none() { + let a = SafeFloat::new(SafeInt::from(1), 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, + ), + ] + .into_iter() + .for_each(|(ma, ea, mb, eb)| { + let a = SafeFloat::new(SafeInt::from(ma), ea).unwrap(); + let b = SafeFloat::new(SafeInt::from(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(SafeInt::from(ma), ea).unwrap(); + let b = SafeFloat::new(SafeInt::from(mb), eb).unwrap(); + let c = SafeFloat::new(SafeInt::from(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(SafeInt::from(1), -14).unwrap(); + let low_share = SafeFloat::new(SafeInt::from(1), -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. + ); + } }