From 68525d9dc4f4bd75d53e90c28af0601fa04795f6 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 8 Jan 2026 12:22:49 -0500 Subject: [PATCH 01/15] Unignore alpha fee tests --- pallets/transaction-fee/src/tests/mod.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/pallets/transaction-fee/src/tests/mod.rs b/pallets/transaction-fee/src/tests/mod.rs index b6697e87f0..14e138b1d4 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; @@ -658,7 +651,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 +723,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 +790,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 +861,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 +933,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 +1003,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 +1075,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 +1136,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; From 7481c9a41f68518b9d9f1d8334171790cdc3bfb5 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 8 Jan 2026 14:18:31 -0500 Subject: [PATCH 02/15] Real swap when withdrawing alpha tx fees --- pallets/swap-interface/src/lib.rs | 1 + pallets/swap/src/pallet/impls.rs | 12 ++++ pallets/transaction-fee/src/lib.rs | 89 ++++++++++++------------ pallets/transaction-fee/src/tests/mod.rs | 8 ++- 4 files changed, 63 insertions(+), 47 deletions(-) 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/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 6ec02879bf..2b99f02a98 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1160,4 +1160,16 @@ impl SwapHandler for Pallet { fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { Self::do_clear_protocol_liquidity(netuid) } + + /// Get the amount of Alpha that needs to be sold to get a given amount of Tao + fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoCurrency) -> AlphaCurrency { + // This is a mock implementation, waiting to merge balancer + // TODO: When balancer is merged, simulate with slippage + let alpha_price = Self::current_price(netuid.into()); + AlphaCurrency::from( + U96F32::from(u64::from(tao_amount)) + .safe_div(alpha_price) + .saturating_to_num::(), + ) + } } diff --git a/pallets/transaction-fee/src/lib.rs b/pallets/transaction-fee/src/lib.rs index fc2a16a409..47b4b85696 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,14 @@ 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( - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + 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 +165,27 @@ where } let tao_per_entry = tao_amount.checked_div(alpha_vec.len() as u64).unwrap_or(0); + 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); - 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(), - ); - }); + // 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 +307,7 @@ where fn withdraw_fee( who: &AccountIdOf, - _call: &CallOf, + call: &CallOf, _dispatch_info: &DispatchInfoOf>, fee: Self::Balance, _tip: Self::Balance, @@ -327,12 +326,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 +339,7 @@ where fn can_withdraw_fee( who: &AccountIdOf, - _call: &CallOf, + call: &CallOf, _dispatch_info: &DispatchInfoOf>, fee: Self::Balance, _tip: Self::Balance, @@ -353,14 +352,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 14e138b1d4..558b4f6677 100644 --- a/pallets/transaction-fee/src/tests/mod.rs +++ b/pallets/transaction-fee/src/tests/mod.rs @@ -597,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); From 01c8dff2e4019ad8db7b336bfeba48d3a5f7dc51 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 8 Jan 2026 14:28:07 -0500 Subject: [PATCH 03/15] fmt --- pallets/transaction-fee/src/lib.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pallets/transaction-fee/src/lib.rs b/pallets/transaction-fee/src/lib.rs index 47b4b85696..53fa613630 100644 --- a/pallets/transaction-fee/src/lib.rs +++ b/pallets/transaction-fee/src/lib.rs @@ -144,7 +144,8 @@ 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 = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + 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( @@ -168,7 +169,8 @@ where 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( + let alpha_balance = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, *netuid, ); let mut alpha_equivalent = @@ -179,11 +181,17 @@ where if alpha_equivalent.is_zero() { alpha_equivalent = alpha_balance; } - let alpha_fee = alpha_equivalent - .min(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); + let _ = pallet_subtensor::Pallet::::unstake_from_subnet( + hotkey, + coldkey, + *netuid, + alpha_fee, + 0.into(), + true, + ); }); } } From 136f007a858f178ef2ff15cd5ddfb3bffaec4a76 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 12 Jan 2026 13:51:02 -0500 Subject: [PATCH 04/15] Add drafty storage items for high precision alpha share pool --- pallets/subtensor/src/lib.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index b87d44ac10..67ed04bf39 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1437,6 +1437,31 @@ pub mod pallet { ValueQuery, >; + /// DMAP ( hot, netuid ) --> total_alpha_shares | Returns the number of alpha shares for a hotkey on a subnet, stores bigmath vector. + #[pallet::storage] + pub type TotalHotkeySharesV2 = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, // hot + Identity, + NetUid, // subnet + Vec, // 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 + ), + Vec, // Shares in unlimited precision + ValueQuery, + >; + /// Contains last Alpha storage map key to iterate (check first) #[pallet::storage] pub type AlphaMapLastKey = From 4c8cf2a9d23077bae7ab5ae2f3ad57e8a2df85f6 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Wed, 14 Jan 2026 19:24:36 -0500 Subject: [PATCH 05/15] Add bigmath for share pool --- Cargo.lock | 273 +++++++- Cargo.toml | 2 + common/src/lib.rs | 2 +- .../subtensor/src/coinbase/run_coinbase.rs | 4 +- pallets/subtensor/src/lib.rs | 12 +- .../subtensor/src/rpc_info/delegate_info.rs | 7 +- .../subtensor/src/staking/recycle_alpha.rs | 16 +- pallets/subtensor/src/staking/stake_utils.rs | 103 ++- pallets/swap/src/mock.rs | 6 +- pallets/swap/src/pallet/mod.rs | 13 +- pallets/swap/src/pallet/tests.rs | 40 +- primitives/share-pool/Cargo.toml | 15 +- primitives/share-pool/src/lib.rs | 634 +++++++++++++----- 13 files changed, 836 insertions(+), 291 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9ba8aede2..2d14d8debb 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", ] @@ -6850,6 +6929,33 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lencode" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7914750d49f4d7824709d4254b92d29f876e98ec33cdaa134fdbe4e4c566bee8" +dependencies = [ + "endian-cast", + "generic-array 1.3.5", + "hashbrown 0.12.3", + "lencode-macros", + "newt-hype", + "ruint", + "zstd-safe 7.2.4", +] + +[[package]] +name = "lencode-macros" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f382b3593c743195f02bbcc2f8631391d9cc7c954b87e8d0a28b6a2a9242b19" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "libc" version = "0.2.176" @@ -8151,6 +8257,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "newt-hype" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8b7b69b0eafaa88ec8dc9fe7c3860af0a147517e5207cfbd0ecd21cd7cde18" + [[package]] name = "nix" version = "0.26.4" @@ -13472,6 +13584,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quanta" version = "0.12.6" @@ -13580,6 +13712,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoth" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d9da82a5dc3ff2fb2eee43d2b434fb197a9bf6a2a243850505b61584f888d2" +dependencies = [ + "quoth-macros", + "regex", + "rust_decimal", + "safe-string", +] + +[[package]] +name = "quoth-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58547202bec9896e773db7ef04b4d47c444f9c97bc4386f36e55718c347db440" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "r-efi" version = "5.3.0" @@ -13858,6 +14013,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "resolv-conf" version = "0.7.5" @@ -13912,6 +14076,35 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rlp" version = "0.5.2" @@ -14147,6 +14340,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec 0.7.6", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -14402,6 +14611,18 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "safe-bigmath" +version = "0.4.0" +source = "git+https://github.com/sam0x17/safe-bigmath?rev=367b6c6#367b6c6b1f10ee8126b63877ff9a512bc4cf4cef" +dependencies = [ + "lencode", + "num-bigint", + "num-integer", + "num-traits", + "quoth", +] + [[package]] name = "safe-math" version = "0.1.0" @@ -14421,6 +14642,12 @@ dependencies = [ "rustc_version 0.2.3", ] +[[package]] +name = "safe-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fc51f1e562058dee569383bfdb5a58752bfeb7fa7f0823f5c07c4c45381b5a" + [[package]] name = "safe_arch" version = "0.7.4" @@ -14842,7 +15069,7 @@ name = "sc-consensus-grandpa" version = "0.36.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=81fa2c54e94f824eba7dabe9dffd063481cb2d80#81fa2c54e94f824eba7dabe9dffd063481cb2d80" dependencies = [ - "ahash", + "ahash 0.8.12", "array-bytes 6.2.3", "async-trait", "dyn-clone", @@ -15145,7 +15372,7 @@ name = "sc-network-gossip" version = "0.51.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=81fa2c54e94f824eba7dabe9dffd063481cb2d80#81fa2c54e94f824eba7dabe9dffd063481cb2d80" dependencies = [ - "ahash", + "ahash 0.8.12", "futures", "futures-timer", "log", @@ -15858,7 +16085,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" dependencies = [ - "ahash", + "ahash 0.8.12", "cfg-if", "hashbrown 0.13.2", ] @@ -15922,6 +16149,12 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.7.3" @@ -16310,7 +16543,12 @@ dependencies = [ name = "share-pool" version = "0.1.0" dependencies = [ + "approx", + "lencode", + "parity-scale-codec", + "safe-bigmath", "safe-math", + "scale-info", "sp-std", "substrate-fixed", ] @@ -16353,6 +16591,12 @@ dependencies = [ "wide", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simple-dns" version = "0.9.3" @@ -17433,7 +17677,7 @@ name = "sp-trie" version = "40.0.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=81fa2c54e94f824eba7dabe9dffd063481cb2d80#81fa2c54e94f824eba7dabe9dffd063481cb2d80" dependencies = [ - "ahash", + "ahash 0.8.12", "foldhash 0.1.5", "hash-db", "hashbrown 0.15.5", @@ -18081,7 +18325,7 @@ dependencies = [ name = "subtensor-macros" version = "0.1.0" dependencies = [ - "ahash", + "ahash 0.8.12", "proc-macro2", "quote", "syn 2.0.106", @@ -19675,7 +19919,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c128c039340ffd50d4195c3f8ce31aac357f06804cfc494c8b9508d4b30dca4" dependencies = [ - "ahash", + "ahash 0.8.12", "hashbrown 0.14.5", "string-interner", ] @@ -20885,6 +21129,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.16+zstd.1.5.7" diff --git a/Cargo.toml b/Cargo.toml index 1d65a3cd5e..c205bd05e7 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 = "367b6c6", 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" log = { version = "0.4.21", default-features = false } memmap2 = "0.9.8" ndarray = { version = "0.16.1", default-features = false } diff --git a/common/src/lib.rs b/common/src/lib.rs index 658f8b2e01..7bd3884b7a 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -257,7 +257,7 @@ pub trait BalanceOps { hotkey: &AccountId, netuid: NetUid, alpha: AlphaCurrency, - ) -> Result; + ) -> Result<(), DispatchError>; } pub mod time { diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 2091946598..bddbaed60e 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -516,7 +516,7 @@ impl Pallet { log::debug!( "owner_hotkey: {owner_hotkey:?} owner_coldkey: {owner_coldkey:?}, owner_cut: {owner_cut:?}" ); - let real_owner_cut = Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( &owner_hotkey, &owner_coldkey, netuid, @@ -524,7 +524,7 @@ impl Pallet { ); // If the subnet is leased, notify the lease logic that owner cut has been distributed. if let Some(lease_id) = SubnetUidToLeaseId::::get(netuid) { - Self::distribute_leased_network_dividends(lease_id, real_owner_cut); + Self::distribute_leased_network_dividends(lease_id, owner_cut); } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 67ed04bf39..4f105953d1 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -88,6 +88,7 @@ pub mod pallet { use frame_system::pallet_prelude::*; use pallet_drand::types::RoundNumber; use runtime_common::prod_or_fast; + use share_pool::SafeFloatSerializable; use sp_core::{ConstU32, H160, H256}; use sp_runtime::traits::{Dispatchable, TrailingZeroInput}; use sp_std::collections::btree_map::BTreeMap; @@ -1445,7 +1446,7 @@ pub mod pallet { T::AccountId, // hot Identity, NetUid, // subnet - Vec, // Hotkey shares in unlimited precision + SafeFloatSerializable, // Hotkey shares in unlimited precision ValueQuery, >; @@ -1458,7 +1459,7 @@ pub mod pallet { NMapKey, // cold NMapKey, // subnet ), - Vec, // Shares in unlimited precision + SafeFloatSerializable, // Shares in unlimited precision ValueQuery, >; @@ -2659,12 +2660,17 @@ impl> hotkey: &T::AccountId, netuid: NetUid, alpha: AlphaCurrency, - ) -> Result { + ) -> Result<(), DispatchError> { ensure!( Self::hotkey_account_exists(hotkey), Error::::HotKeyAccountNotExists ); + ensure!( + Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid) >= alpha, + Error::::InsufficientBalance + ); + // Decrese alpha out counter SubnetAlphaOut::::mutate(netuid, |total| { *total = total.saturating_sub(alpha); diff --git a/pallets/subtensor/src/rpc_info/delegate_info.rs b/pallets/subtensor/src/rpc_info/delegate_info.rs index bf6dafd332..34d5f3f53c 100644 --- a/pallets/subtensor/src/rpc_info/delegate_info.rs +++ b/pallets/subtensor/src/rpc_info/delegate_info.rs @@ -6,6 +6,7 @@ use substrate_fixed::types::U64F64; extern crate alloc; use alloc::collections::BTreeMap; use codec::Compact; +use share_pool::SafeFloat; use subtensor_runtime_common::{AlphaCurrency, NetUid}; #[freeze_struct("1fafc4fcf28cba7a")] @@ -65,8 +66,10 @@ impl Pallet { alpha_share_pools.push(alpha_share_pool); } - for ((nominator, netuid), alpha_stake) in Alpha::::iter_prefix((delegate.clone(),)) { - if alpha_stake == 0 { + for ((nominator, netuid), alpha_stake_float_serializable) in AlphaV2::::iter_prefix((delegate.clone(),)) { + let alpha_stake = SafeFloat::from(&alpha_stake_float_serializable); + + if alpha_stake.is_zero() { continue; } diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index 5229971ed0..a5b8d9894a 100644 --- a/pallets/subtensor/src/staking/recycle_alpha.rs +++ b/pallets/subtensor/src/staking/recycle_alpha.rs @@ -51,19 +51,17 @@ impl Pallet { ); // Deduct from the coldkey's stake. - let actual_alpha_decrease = Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &coldkey, netuid, amount, ); - ensure!(actual_alpha_decrease <= amount, Error::::PrecisionLoss); - // 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, + amount, netuid, )); @@ -118,19 +116,17 @@ impl Pallet { ); // Deduct from the coldkey's stake. - let actual_alpha_decrease = Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &coldkey, netuid, amount, ); - ensure!(actual_alpha_decrease <= amount, Error::::PrecisionLoss); - - 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, + amount, netuid, )); diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index f61a8a6ce2..f7f79d0340 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,14 @@ 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)?; + 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 +725,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 +735,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 +773,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 +840,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, @@ -864,11 +848,11 @@ 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, ); // Calculate TAO equivalent based on current price (it is accurate because @@ -876,7 +860,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(); @@ -905,7 +889,7 @@ impl Pallet { origin_coldkey.clone(), origin_hotkey.clone(), tao_equivalent, - actual_alpha_decrease, + alpha, netuid, 0_u64, // 0 fee )); @@ -913,7 +897,7 @@ impl Pallet { destination_coldkey.clone(), destination_hotkey.clone(), tao_equivalent, - actual_alpha_moved, + alpha, netuid, 0_u64, // 0 fee )); @@ -1292,47 +1276,56 @@ type AlphaShareKey = ::AccountId; impl SharePoolDataOperations> for HotkeyAlphaSharePoolDataOperations { - fn get_shared_value(&self) -> U64F64 { - U64F64::saturating_from_num(crate::TotalHotkeyAlpha::::get(&self.hotkey, self.netuid)) + fn get_shared_value(&self) -> u64 { + u64::from(TotalHotkeyAlpha::::get(&self.hotkey, self.netuid)) } - fn get_share(&self, key: &AlphaShareKey) -> U64F64 { - crate::Alpha::::get((&(self.hotkey), key, self.netuid)) + fn get_share(&self, key: &AlphaShareKey) -> SafeFloat { + let share_serializable = AlphaV2::::get((&(self.hotkey), key, self.netuid)); + SafeFloat::from(&share_serializable) } - fn try_get_share(&self, key: &AlphaShareKey) -> Result { - crate::Alpha::::try_get((&(self.hotkey), key, self.netuid)) + fn try_get_share(&self, key: &AlphaShareKey) -> Result { + let maybe_share_serializable = AlphaV2::::try_get((&(self.hotkey), key, self.netuid)); + if let Ok(share_serializable) = maybe_share_serializable { + Ok(SafeFloat::from(&share_serializable)) + } else { + Err(()) + } } - fn get_denominator(&self) -> U64F64 { - crate::TotalHotkeyShares::::get(&(self.hotkey), self.netuid) + fn get_denominator(&self) -> SafeFloat { + let denominator_serializable = TotalHotkeySharesV2::::get(&(self.hotkey), self.netuid); + SafeFloat::from(&denominator_serializable) } - fn set_shared_value(&mut self, value: U64F64) { + fn set_shared_value(&mut self, value: u64) { if value != 0 { - crate::TotalHotkeyAlpha::::insert( + TotalHotkeyAlpha::::insert( &(self.hotkey), self.netuid, - AlphaCurrency::from(value.saturating_to_num::()), + AlphaCurrency::from(value), ); } else { - crate::TotalHotkeyAlpha::::remove(&(self.hotkey), self.netuid); + TotalHotkeyAlpha::::remove(&(self.hotkey), self.netuid); } } - fn set_share(&mut self, key: &AlphaShareKey, share: U64F64) { - if share != 0 { - crate::Alpha::::insert((&self.hotkey, key, self.netuid), share); + fn set_share(&mut self, key: &AlphaShareKey, share: SafeFloat) { + if !share.is_zero() { + let float_serializable = SafeFloatSerializable::from(&share); + AlphaV2::::insert((&self.hotkey, key, self.netuid), float_serializable); } else { - crate::Alpha::::remove((&self.hotkey, key, self.netuid)); + AlphaV2::::remove((&self.hotkey, key, self.netuid)); } } - fn set_denominator(&mut self, update: U64F64) { - if update != 0 { - crate::TotalHotkeyShares::::insert(&self.hotkey, self.netuid, update); + fn set_denominator(&mut self, update: SafeFloat) { + if !update.is_zero() { + let float_serializable = SafeFloatSerializable::from(&update); + TotalHotkeySharesV2::::insert(&self.hotkey, self.netuid, float_serializable); } else { - crate::TotalHotkeyShares::::remove(&self.hotkey, self.netuid); + TotalHotkeySharesV2::::remove(&self.hotkey, self.netuid); } } } diff --git a/pallets/swap/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/mod.rs b/pallets/swap/src/pallet/mod.rs index 97a25ec242..93a0fe627c 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -405,13 +405,11 @@ mod pallet { let tao_provided = T::BalanceOps::decrease_balance(&coldkey, tao)?; ensure!(tao_provided == tao, Error::::InsufficientBalance); - let alpha_provided = - T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), alpha)?; - ensure!(alpha_provided == alpha, Error::::InsufficientBalance); + T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), alpha)?; // Add provided liquidity to user-provided reserves T::TaoReserve::increase_provided(netuid.into(), tao_provided); - T::AlphaReserve::increase_provided(netuid.into(), alpha_provided); + T::AlphaReserve::increase_provided(netuid.into(), alpha); // Emit an event Self::deposit_event(Event::LiquidityAdded { @@ -527,12 +525,7 @@ mod pallet { let tao_provided = T::BalanceOps::decrease_balance(&coldkey, result.tao)?; ensure!(tao_provided == result.tao, Error::::InsufficientBalance); - let alpha_provided = - T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), result.alpha)?; - ensure!( - alpha_provided == result.alpha, - Error::::InsufficientBalance - ); + T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), result.alpha)?; // Emit an event Self::deposit_event(Event::LiquidityModified { diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index 4013248abb..8a4827f796 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -2268,7 +2268,7 @@ 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( + ::BalanceOps::decrease_stake( &cold, &hot, netuid.into(), @@ -2276,7 +2276,7 @@ fn liquidate_v3_refunds_user_funds_and_clears_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(need_alpha)); // Users‑only liquidation. assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2335,14 +2335,14 @@ fn refund_alpha_single_provider_exact() { let alpha_before_total = alpha_before_hot + alpha_before_owner; // --- Mimic extrinsic bookkeeping: withdraw α and record provided reserve. - let alpha_taken = ::BalanceOps::decrease_stake( + ::BalanceOps::decrease_stake( &cold, &hot, netuid.into(), alpha_needed.into(), ) .expect("decrease ALPHA"); - AlphaReserve::increase_provided(netuid.into(), alpha_taken); + AlphaReserve::increase_provided(netuid.into(), AlphaCurrency::from(alpha_needed)); // --- Act: users‑only dissolve. assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2410,15 +2410,13 @@ fn refund_alpha_multiple_providers_proportional_to_principal() { let a2_before = a2_before_hot + a2_before_owner; // Withdraw α and account reserves for each provider. - let a1_taken = - ::BalanceOps::decrease_stake(&c1, &h1, netuid.into(), a1.into()) - .expect("decrease α #1"); - AlphaReserve::increase_provided(netuid.into(), a1_taken); + ::BalanceOps::decrease_stake(&c1, &h1, netuid.into(), a1.into()) + .expect("decrease α #1"); + AlphaReserve::increase_provided(netuid.into(), AlphaCurrency::from(a1)); - let a2_taken = - ::BalanceOps::decrease_stake(&c2, &h2, netuid.into(), a2.into()) - .expect("decrease α #2"); - AlphaReserve::increase_provided(netuid.into(), a2_taken); + ::BalanceOps::decrease_stake(&c2, &h2, netuid.into(), a2.into()) + .expect("decrease α #2"); + AlphaReserve::increase_provided(netuid.into(), AlphaCurrency::from(a2)); // Act assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2472,15 +2470,13 @@ fn refund_alpha_same_cold_multiple_hotkeys_conserved_to_owner() { let before_total = before_hot1 + before_hot2 + before_owner; // Withdraw α from both hotkeys; track provided‑reserve. - let t1 = - ::BalanceOps::decrease_stake(&cold, &hot1, netuid.into(), a1.into()) - .expect("decr α #hot1"); - AlphaReserve::increase_provided(netuid.into(), t1); + ::BalanceOps::decrease_stake(&cold, &hot1, netuid.into(), a1.into()) + .expect("decr α #hot1"); + AlphaReserve::increase_provided(netuid.into(), AlphaCurrency::from(a1)); - let t2 = - ::BalanceOps::decrease_stake(&cold, &hot2, netuid.into(), a2.into()) - .expect("decr α #hot2"); - AlphaReserve::increase_provided(netuid.into(), t2); + ::BalanceOps::decrease_stake(&cold, &hot2, netuid.into(), a2.into()) + .expect("decr α #hot2"); + AlphaReserve::increase_provided(netuid.into(), AlphaCurrency::from(a2)); // Act assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2562,7 +2558,7 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { // --- Mirror extrinsic bookkeeping: withdraw τ & α; bump provided reserves --- let tao_taken = ::BalanceOps::decrease_balance(&cold, tao_needed.into()) .expect("decrease TAO"); - let alpha_taken = ::BalanceOps::decrease_stake( + ::BalanceOps::decrease_stake( &cold, &hot, netuid.into(), @@ -2571,7 +2567,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/primitives/share-pool/Cargo.toml b/primitives/share-pool/Cargo.toml index ba42b0d77d..b5da06cff6 100644 --- a/primitives/share-pool/Cargo.toml +++ b/primitives/share-pool/Cargo.toml @@ -4,8 +4,13 @@ version = "0.1.0" edition.workspace = true [dependencies] +approx.workspace = true +codec.workspace = true +lencode.workspace = true +scale-info.workspace = true substrate-fixed.workspace = true sp-std.workspace = true +safe-bigmath.workspace = true safe-math.workspace = true [lints] @@ -13,4 +18,12 @@ workspace = true [features] default = ["std"] -std = ["substrate-fixed/std", "sp-std/std", "safe-math/std"] +std = [ + "codec/std", + "lencode/std", + "scale-info/std", + "substrate-fixed/std", + "sp-std/std", + "safe-math/std", + "safe-bigmath/std" +] diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index d43f36259c..cde2c08f74 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -1,26 +1,304 @@ #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::result_unit_err)] -use safe_math::*; +use codec::{Decode, Encode}; +use lencode::{Decode as LenDecode, Encode as LenEncode}; +use lencode::io::Cursor; +use safe_bigmath::*; +use scale_info::TypeInfo; use sp_std::marker; use sp_std::ops::Neg; -use substrate_fixed::types::{I64F64, U64F64}; + +// Maximum value that can be represented with SafeFloat +pub const SAFE_FLOAT_MAX: u128 = 1_000_000_000_000_000_000_000_u128; + +/// Controlled precision floating point number with efficient storage +/// The representation is mantissa / 10^-exponent +/// (exponent is used as negative number unlike conventional floats) +/// +/// 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: u32, +} + +#[derive(Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct SafeFloatSerializable { + mantissa: Vec, + exponent: u32, +} + +fn clip_to_u32(x: i32) -> u32 { + if x < 0 { + 0u32 + } else { + x as u32 + } +} + +impl SafeFloat { + pub fn zero() -> Self { + SafeFloat { + mantissa: SafeInt::zero(), + exponent: 0_u32, + } + } + + pub fn new(mantissa: SafeInt, exponent: u32) -> Option { + // Cap at SAFE_FLOAT_MAX + let max_value = SafeInt::from(SAFE_FLOAT_MAX) + SafeInt::one(); + if !(mantissa.clone() / max_value).unwrap_or_default().is_zero() { + return None; + } + + let mut safe_float = SafeFloat { + mantissa, + exponent, + }; + + safe_float.adjust_precision(); + Some(safe_float) + } + + 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() + } + + /// Adjusts mantissa and exponent of this floating point number so that + /// SAFE_FLOAT_MAX <= mantissa < 10 * SAFE_FLOAT_MAX + pub fn adjust_precision(&mut self) { + let max_value = SafeInt::from(SAFE_FLOAT_MAX); + let max_value_div10 = SafeInt::from(SAFE_FLOAT_MAX.checked_div(10).unwrap_or_default()); + let mantissa_abs = self.mantissa.clone().abs(); + let exponent_adjustment: i32 = if max_value_div10 > mantissa_abs { + let scale = (max_value_div10 / mantissa_abs).unwrap_or_default(); + (Self::intlog10(&scale) + 1) as i32 + } else if max_value < mantissa_abs { + let scale = (mantissa_abs / max_value).unwrap_or_default(); + -1 * ((Self::intlog10(&scale) + 1) as i32) + } else { + 0i32 + }; + + self.exponent = clip_to_u32(self.exponent as i32 + exponent_adjustment); + + if exponent_adjustment > 0 { + let mantissa_adjustment = SafeInt::from(10).pow(exponent_adjustment as u32); + self.mantissa = self.mantissa.clone() * mantissa_adjustment; + } else { + let mantissa_adjustment = SafeInt::from(10).pow((-1 * exponent_adjustment as i32) as u32); + self.mantissa = (self.mantissa.clone() / mantissa_adjustment).unwrap_or_default(); + } + } + + /// Divide current value by a preserving precision (SAFE_FLOAT_MAX digits in mantissa) + /// result = m1 * 10^e2 / m2 * 10^e1 + pub fn div(&self, a: &SafeFloat) -> Option { + let ten = SafeInt::from(10u32); + let redundant_exponent = self.exponent + a.exponent; + + let maybe_new_mantissa = self.mantissa.clone() * ten.pow(redundant_exponent as u32) / a.mantissa.clone(); + if let Some(new_mantissa) = maybe_new_mantissa { + let mut safe_float = SafeFloat { + mantissa: new_mantissa, + exponent: self.exponent.saturating_mul(2), + }; + safe_float.adjust_precision(); + Some(safe_float) + } else { + None + } + } + + pub fn add(&self, a: &SafeFloat) -> Self { + let ten = SafeInt::from(10u32); + let mut safe_float = SafeFloat { + mantissa: self.mantissa.clone() * ten.clone().pow(a.exponent) + a.mantissa.clone() * ten.pow(self.exponent), + exponent: self.exponent + a.exponent, + }; + safe_float.adjust_precision(); + safe_float + } + + /// Calculate self * a / b without loss of precision + pub fn mul_div(&self, a: &SafeFloat, b: &SafeFloat) -> Option { + let self_a_mantissa = self.mantissa.clone() * a.mantissa.clone(); + let self_a_exponent = self.exponent + a.exponent; + + // Divide by b without adjusting precision first (preserve higher precision + // of multiplication result) + SafeFloat{ + mantissa: self_a_mantissa, + exponent: self_a_exponent + }.div(b) + } + + pub fn is_zero(&self) -> bool { + self.mantissa.is_zero() + } + + /// Returns true if self > a + pub fn gt(&self, a: &SafeFloat) -> bool { + // Shortcut: same exponent → compare mantissas directly + if self.exponent == a.exponent { + return self.mantissa > a.mantissa; + } + + let ten = SafeInt::from(10); + + // 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 = ten.clone().pow(k1); + let scale2 = ten.pow(k2); + + let lhs = &self.mantissa * &scale1; + let rhs = &a.mantissa * &scale2; + + lhs - rhs > 0 + } +} + +// Saturating conversion: negatives -> 0, overflow -> u64::MAX +impl From<&SafeFloat> for u64 { + fn from(value: &SafeFloat) -> Self { + // Negative values are clamped to 0 + if value.mantissa.is_negative() { + return 0; + } + + // If exponent is zero, it's just an integer mantissa + if value.exponent == 0 { + return value.mantissa.to_u64().unwrap_or(u64::MAX); + } + + // scale = 10^exponent + let scale = SafeInt::from(10).pow(value.exponent); + + // mantissa / 10^exponent (integer division, truncating toward zero) + // SafeInt division is fallible; None only if divisor is zero (can't happen here) + let q: SafeInt = (&value.mantissa / &scale).unwrap_or_else(SafeInt::zero); + + // Convert quotient to u64, saturating on overflow + if q.is_zero() { + 0 + } else { + q.to_u64().unwrap_or(u64::MAX) + } + } +} + +// Convenience impl for owning values +impl From for u64 { + fn from(value: SafeFloat) -> Self { + u64::from(&value) + } +} + +impl From for SafeFloat { + fn from(value: u64) -> Self { + SafeFloat::new(SafeInt::from(value), 0).unwrap_or_default() + } +} + +impl From<&SafeFloat> for SafeFloatSerializable { + fn from(value: &SafeFloat) -> Self { + let mut mantissa_serializable = Vec::new(); + value.mantissa.encode(&mut mantissa_serializable).unwrap_or_default(); + + SafeFloatSerializable { + mantissa: mantissa_serializable, + exponent: value.exponent + } + } +} + +impl From<&SafeFloatSerializable> for SafeFloat { + fn from(value: &SafeFloatSerializable) -> Self { + let decoded = SafeInt::decode(&mut Cursor::new(&value.mantissa)).unwrap_or_default(); + SafeFloat { + mantissa: decoded, + exponent: value.exponent + } + } +} + +impl From<&SafeFloat> for f64 { + fn from(value: &SafeFloat) -> Self { + // Zero shortcut + if value.mantissa.is_zero() { + return 0.0; + } + + // If you ever allow negative mantissas, handle sign here. + // For now we assume mantissa >= 0 per your spec. + let mut mant = value.mantissa.clone(); + let mut exp_i32 = value.exponent as i32; + + let ten = SafeInt::from(10); + + // Max integer exactly representable in f64: 2^53 - 1 + let max_exact = SafeInt::from((1u64 << 53) - 1); + + // While mantissa is too large to be exactly represented, + // discard right decimal digits: mant /= 10, and adjust exponent + // so that mant * 10^-exp stays the same value. + while mant > max_exact { + mant = (&mant / &ten).expect("10 is non-zero; division must succeed"); + 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() + .expect("mant <= 2^53-1, must fit into u64"); + + 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 +325,20 @@ where } pub fn get_value(&self, key: &K) -> u64 { - let shared_value: U64F64 = self.state_ops.get_shared_value(); - let current_share: U64F64 = self.state_ops.get_share(key); - let denominator: U64F64 = self.state_ops.get_denominator(); - - let maybe_value_per_share = shared_value.checked_div(denominator); - (if let Some(value_per_share) = maybe_value_per_share { - value_per_share.saturating_mul(current_share) - } else { - shared_value - .saturating_mul(current_share) - .checked_div(denominator) - .unwrap_or(U64F64::saturating_from_num(0)) - }) - .saturating_to_num::() + let shared_value: SafeFloat = SafeFloat::new(SafeInt::from(self.state_ops.get_shared_value()), 0).unwrap_or_default(); + let current_share: SafeFloat = self.state_ops.get_share(key); + let denominator: SafeFloat = self.state_ops.get_denominator(); + shared_value.mul_div(¤t_share, &denominator) + .unwrap_or(SafeFloat::zero()) + .into() } - pub fn get_value_from_shares(&self, current_share: U64F64) -> u64 { - let shared_value: U64F64 = self.state_ops.get_shared_value(); - let denominator: U64F64 = self.state_ops.get_denominator(); - - let maybe_value_per_share = shared_value.checked_div(denominator); - (if let Some(value_per_share) = maybe_value_per_share { - value_per_share.saturating_mul(current_share) - } else { - shared_value - .saturating_mul(current_share) - .checked_div(denominator) - .unwrap_or(U64F64::saturating_from_num(0)) - }) - .saturating_to_num::() + pub fn get_value_from_shares(&self, current_share: SafeFloat) -> u64 { + let shared_value: SafeFloat = SafeFloat::new(SafeInt::from(self.state_ops.get_shared_value()), 0).unwrap_or_default(); + let denominator: SafeFloat = self.state_ops.get_denominator(); + shared_value.mul_div(¤t_share, &denominator) + .unwrap_or(SafeFloat::zero()) + .into() } pub fn try_get_value(&self, key: &K) -> Result { @@ -89,164 +351,132 @@ where /// Update the total shared value. /// Every key's associated value effectively updates with this operation pub fn update_value_for_all(&mut self, update: i64) { - let shared_value: U64F64 = self.state_ops.get_shared_value(); + let shared_value: u64 = self.state_ops.get_shared_value(); self.state_ops.set_shared_value(if update >= 0 { - shared_value.saturating_add(U64F64::saturating_from_num(update)) + shared_value.saturating_add(update as u64) } else { - shared_value.saturating_sub(U64F64::saturating_from_num(update.neg())) + shared_value.saturating_sub(update.neg() as u64) }); } pub fn sim_update_value_for_one(&mut self, update: i64) -> bool { - let shared_value: U64F64 = self.state_ops.get_shared_value(); - let denominator: U64F64 = self.state_ops.get_denominator(); + let shared_value: u64 = self.state_ops.get_shared_value(); + let denominator: SafeFloat = self.state_ops.get_denominator(); // Then, update this key's share - if denominator == 0 { + if denominator.mantissa == 0 { true } else { // There are already keys in the pool, set or update this key - let shares_per_update: I64F64 = - self.get_shares_per_update(update, &shared_value, &denominator); + let shares_per_update = + self.get_shares_per_update(update, shared_value, &denominator); - shares_per_update != 0 + !shares_per_update.is_zero() } } fn get_shares_per_update( &self, update: i64, - shared_value: &U64F64, - denominator: &U64F64, - ) -> I64F64 { - let maybe_value_per_share = shared_value.checked_div(*denominator); - if let Some(value_per_share) = maybe_value_per_share { - I64F64::saturating_from_num(update) - .checked_div(I64F64::saturating_from_num(value_per_share)) - .unwrap_or(I64F64::saturating_from_num(0)) - } else { - I64F64::saturating_from_num(update) - .checked_div(I64F64::saturating_from_num(*shared_value)) - .unwrap_or(I64F64::saturating_from_num(0)) - .saturating_mul(I64F64::saturating_from_num(*denominator)) - } + shared_value: u64, + denominator: &SafeFloat, + ) -> SafeFloat { + let shared_value: SafeFloat = SafeFloat::new(SafeInt::from(shared_value), 0).unwrap_or_default(); + let update: SafeFloat = SafeFloat::new(SafeInt::from(update), 0).unwrap_or_default(); + update.mul_div(denominator, &shared_value).unwrap_or_default() } /// Update the value associated with an item identified by the Key /// Returns actual update /// - pub fn update_value_for_one(&mut self, key: &K, update: i64) -> i64 { - let shared_value: U64F64 = self.state_ops.get_shared_value(); - let current_share: U64F64 = self.state_ops.get_share(key); - let denominator: U64F64 = self.state_ops.get_denominator(); - let initial_value: i64 = self.get_value(key) as i64; - let mut actual_update: i64 = update; + pub fn update_value_for_one(&mut self, key: &K, update: i64) { + let shared_value: u64 = self.state_ops.get_shared_value(); + let current_share: SafeFloat = self.state_ops.get_share(key); + let denominator: SafeFloat = self.state_ops.get_denominator(); // Then, update this key's share - if denominator == 0 { + if denominator.is_zero() { // Initialize the pool. The first key gets all. - let update_fixed: U64F64 = U64F64::saturating_from_num(update); - self.state_ops.set_denominator(update_fixed); - self.state_ops.set_share(key, update_fixed); + let update_float: SafeFloat = SafeFloat::new(SafeInt::from(update), 0).unwrap_or_default(); + self.state_ops.set_denominator(update_float.clone()); + self.state_ops.set_share(key, update_float); } else { - let shares_per_update: I64F64 = - self.get_shares_per_update(update, &shared_value, &denominator); - - if shares_per_update >= 0 { - self.state_ops.set_denominator( - denominator.saturating_add(U64F64::saturating_from_num(shares_per_update)), - ); - self.state_ops.set_share( - key, - current_share.saturating_add(U64F64::saturating_from_num(shares_per_update)), - ); - } else { - // Check if this entry is about to break precision - let mut new_denominator = denominator - .saturating_sub(U64F64::saturating_from_num(shares_per_update.neg())); - let mut new_share = current_share - .saturating_sub(U64F64::saturating_from_num(shares_per_update.neg())); - - // The condition here is either the share remainder is too little OR - // the new_denominator is too low compared to what shared_value + year worth of emissions would be - if (new_share.safe_div(current_share) < U64F64::saturating_from_num(0.00001)) - || shared_value - .saturating_add(U64F64::saturating_from_num(2_628_000_000_000_000_u64)) - .checked_div(new_denominator) - .is_none() - { - // yes, precision is low, just remove all - new_share = U64F64::saturating_from_num(0); - new_denominator = denominator.saturating_sub(current_share); - actual_update = initial_value.neg(); - } - - self.state_ops.set_denominator(new_denominator); - self.state_ops.set_share(key, new_share); - } + let shares_per_update: SafeFloat = + self.get_shares_per_update(update, shared_value, &denominator); + + self.state_ops.set_denominator( + denominator.add(&shares_per_update), + ); + self.state_ops.set_share( + key, + current_share.add(&shares_per_update), + ); } // Update shared value - self.update_value_for_all(actual_update); - - // Return actual udate - actual_update + self.update_value_for_all(update); } } +// cargo test --package share-pool --lib -- tests --nocapture #[cfg(test)] mod tests { use super::*; + use approx::assert_abs_diff_eq; + use lencode::{Decode, Encode}; + use lencode::io::Cursor; 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 + fn get_share(&self, key: &u16) -> SafeFloat { + self .share .get(key) - .unwrap_or(&U64F64::saturating_from_num(0)) + .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 +484,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 +498,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 +508,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 +521,20 @@ mod tests { #[test] fn test_max_share_value() { let mut mock_ops = MockSharePoolDataOperations::new(); - mock_ops.set_shared_value(U64F64::saturating_from_num(1_000_000_000)); // Use a neutral value for shared value - mock_ops.set_share(&1, U64F64::saturating_from_num(u64::MAX / 2)); - mock_ops.set_share(&2, U64F64::saturating_from_num(u64::MAX / 2)); - mock_ops.set_denominator(U64F64::saturating_from_num(u64::MAX)); + mock_ops.set_shared_value(1_000_000_000u64); // Use a neutral value for shared value + mock_ops.set_share(&1, (u64::MAX / 2).into()); + mock_ops.set_share(&2, (u64::MAX / 2).into()); + mock_ops.set_denominator((u64::MAX).into()); let pool = SharePool::::new(mock_ops); let value1 = pool.get_value(&1) as i128; let value2 = pool.get_value(&2) as i128; - assert!((value1 - 500_000_000).abs() <= 1); + assert_abs_diff_eq!( + value1 as f64, + 500_000_000 as f64, + epsilon = 1. + ); assert!((value2 - 500_000_000).abs() <= 1); } @@ -331,26 +565,30 @@ mod tests { let mock_ops = MockSharePoolDataOperations::new(); let mut pool = SharePool::::new(mock_ops); + // 50%/50% stakes consisting of 1 rao each pool.update_value_for_one(&1, 1); pool.update_value_for_one(&2, 1); + // Huge emission resulting in 1M Alpha + // Both stakers should have 500k Alpha each pool.update_value_for_all(999_999_999_999_998); + // Everyone unstakes almost everything, leaving 10 rao in the stake pool.update_value_for_one(&1, -499_999_999_999_990); pool.update_value_for_one(&2, -499_999_999_999_990); + // Huge emission resulting in 1M Alpha + // Both stakers should have 500k Alpha each pool.update_value_for_all(999_999_999_999_980); + // Stakers add 1k Alpha each pool.update_value_for_one(&1, 1_000_000_000_000); pool.update_value_for_one(&2, 1_000_000_000_000); - let value1 = pool.get_value(&1) as i128; - let value2 = pool.get_value(&2) as i128; - - // First to stake gets all accumulated emission if there are no other stakers - // (which is artificial situation because there will be no emissions if there is no stake) - assert!((value1 - 1_001_000_000_000_000).abs() < 100); - assert!((value2 - 1_000_000_000_000).abs() < 100); + let value1 = pool.get_value(&1) as f64; + let value2 = pool.get_value(&2) as f64; + assert_abs_diff_eq!(value1, 501_000_000_000_000_f64, epsilon = 1.); + assert_abs_diff_eq!(value2, 501_000_000_000_000_f64, epsilon = 1.); } // cargo test --package share-pool --lib -- tests::test_denom_high_precision_many_small_unstakes --exact --show-output @@ -359,26 +597,45 @@ 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 +664,79 @@ mod tests { // cargo test --package share-pool --lib -- tests::test_get_shares_per_update --exact --show-output #[test] fn test_get_shares_per_update() { + + // Test case (update, shared_value, denominator_mantissa, denominator_exponent) [ - (1_i64, 1_u64, 1.0, 1.0), + (1_i64, 1_u64, 1_u64, 0_u32), + ( + 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_000, 21_000_000_000_000_000, - 0.00001, - 0.00000000000000000043, + 1, + 5 ), ( 21_000_000_000_000_000, 21_000_000_000_000_000, - 0.00001, - 0.00001, + 1, + 5 ), ( 210_000_000_000_000_000, 21_000_000_000_000_000, - 0.00001, - 0.0001, + 1, + 5 ), ( 1_000, 1_000, - 21_000_000_000_000_000_f64, - 21_000_000_000_000_000_f64, + 21_000_000_000_000_000, + 0 ), ] - .iter() - .for_each(|(update, shared_value, denominator, expected)| { + .into_iter() + .for_each(|(update, shared_value, denominator_mantissa, denominator_exponent)| { 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,); + let denominator_float = SafeFloat::new(SafeInt::from(denominator_mantissa), denominator_exponent).unwrap(); + let denominator_f64: f64 = denominator_float.clone().into(); + let spu: f64 = + pool.get_shares_per_update(update, shared_value, &denominator_float).into(); + let expected = update as f64 * denominator_f64 / shared_value as f64; + let precision = 1000.; + assert_abs_diff_eq!( + expected, + spu, + epsilon = expected / precision + ); }); } + + #[test] + fn test_safeint_serialization() { + let safe_int = SafeInt::from(12345); + let mut buf = Vec::new(); + safe_int.encode(&mut buf).unwrap(); + + let decoded = SafeInt::decode(&mut Cursor::new(&buf)).unwrap(); + assert_eq!(decoded, safe_int); + } } From 49dc08be17148c54e9a2d612ca6f794f406e280d Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 15 Jan 2026 13:30:51 -0500 Subject: [PATCH 06/15] Make SafeFloat exponent work like m*10^e, allow negative exponents --- Cargo.lock | 4 +- Cargo.toml | 2 +- pallets/subtensor/src/lib.rs | 4 +- .../subtensor/src/rpc_info/delegate_info.rs | 4 +- .../subtensor/src/staking/recycle_alpha.rs | 22 +- pallets/subtensor/src/staking/stake_utils.rs | 11 +- pallets/swap/src/pallet/tests.rs | 9 +- primitives/share-pool/src/lib.rs | 378 +++++++++--------- 8 files changed, 208 insertions(+), 226 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d14d8debb..71af766313 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14613,8 +14613,8 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "safe-bigmath" -version = "0.4.0" -source = "git+https://github.com/sam0x17/safe-bigmath?rev=367b6c6#367b6c6b1f10ee8126b63877ff9a512bc4cf4cef" +version = "0.4.1" +source = "git+https://github.com/sam0x17/safe-bigmath?rev=013c499#013c49984910e1c9a23289e8c85e7a856e263a02" dependencies = [ "lencode", "num-bigint", diff --git a/Cargo.toml b/Cargo.toml index c205bd05e7..4bc3fa93bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +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 = "367b6c6", package = "safe-bigmath", default-features = false, git = "https://github.com/sam0x17/safe-bigmath" } +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" } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index a68d4f559c..ecad1eeba7 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1445,7 +1445,7 @@ pub mod pallet { Blake2_128Concat, T::AccountId, // hot Identity, - NetUid, // subnet + NetUid, // subnet SafeFloatSerializable, // Hotkey shares in unlimited precision ValueQuery, >; @@ -2670,7 +2670,7 @@ impl> ensure!( Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid) >= alpha, Error::::InsufficientBalance - ); + ); // Decrese alpha out counter SubnetAlphaOut::::mutate(netuid, |total| { diff --git a/pallets/subtensor/src/rpc_info/delegate_info.rs b/pallets/subtensor/src/rpc_info/delegate_info.rs index 34d5f3f53c..223402df39 100644 --- a/pallets/subtensor/src/rpc_info/delegate_info.rs +++ b/pallets/subtensor/src/rpc_info/delegate_info.rs @@ -66,7 +66,9 @@ impl Pallet { alpha_share_pools.push(alpha_share_pool); } - for ((nominator, netuid), alpha_stake_float_serializable) in AlphaV2::::iter_prefix((delegate.clone(),)) { + for ((nominator, netuid), alpha_stake_float_serializable) in + AlphaV2::::iter_prefix((delegate.clone(),)) + { let alpha_stake = SafeFloat::from(&alpha_stake_float_serializable); if alpha_stake.is_zero() { diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index a5b8d9894a..47fc75bd63 100644 --- a/pallets/subtensor/src/staking/recycle_alpha.rs +++ b/pallets/subtensor/src/staking/recycle_alpha.rs @@ -51,19 +51,12 @@ impl Pallet { ); // Deduct from the coldkey's stake. - Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, amount, - ); + 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, amount); - Self::deposit_event(Event::AlphaRecycled( - coldkey, - hotkey, - amount, - netuid, - )); + Self::deposit_event(Event::AlphaRecycled(coldkey, hotkey, amount, netuid)); Ok(()) } @@ -116,19 +109,12 @@ impl Pallet { ); // Deduct from the coldkey's stake. - Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, amount, - ); + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid, amount); Self::burn_subnet_alpha(netuid, amount); // Deposit event - Self::deposit_event(Event::AlphaBurned( - coldkey, - hotkey, - amount, - netuid, - )); + Self::deposit_event(Event::AlphaBurned(coldkey, hotkey, amount, netuid)); Ok(()) } diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index b3e27bdb92..26be257168 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -687,8 +687,7 @@ impl Pallet { 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, alpha, 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 = alpha.saturating_sub( @@ -865,7 +864,7 @@ impl Pallet { Self::add_stake_adjust_root_claimed_for_hotkey_and_coldkey( destination_hotkey, destination_coldkey, - actual_alpha_decrease.into(), + u64::from(alpha).into(), ); } @@ -1315,11 +1314,7 @@ impl SharePoolDataOperations> fn set_shared_value(&mut self, value: u64) { if value != 0 { - TotalHotkeyAlpha::::insert( - &(self.hotkey), - self.netuid, - AlphaCurrency::from(value), - ); + TotalHotkeyAlpha::::insert(&(self.hotkey), self.netuid, AlphaCurrency::from(value)); } else { TotalHotkeyAlpha::::remove(&(self.hotkey), self.netuid); } diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index 8a4827f796..38bb9453e8 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -2268,13 +2268,8 @@ 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"); - ::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(), AlphaCurrency::from(need_alpha)); diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index cde2c08f74..85f0e1c2c6 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -2,8 +2,8 @@ #![allow(clippy::result_unit_err)] use codec::{Decode, Encode}; -use lencode::{Decode as LenDecode, Encode as LenEncode}; use lencode::io::Cursor; +use lencode::{Decode as LenDecode, Encode as LenEncode}; use safe_bigmath::*; use scale_info::TypeInfo; use sp_std::marker; @@ -11,105 +11,121 @@ use sp_std::ops::Neg; // 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: u128 = 18_i64; /// Controlled precision floating point number with efficient storage -/// The representation is mantissa / 10^-exponent -/// (exponent is used as negative number unlike conventional floats) -/// -/// 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 +/// +/// 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: u32, + exponent: i64, } #[derive(Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug)] pub struct SafeFloatSerializable { mantissa: Vec, - exponent: u32, + exponent: i64, } -fn clip_to_u32(x: i32) -> u32 { - if x < 0 { - 0u32 - } else { - x as u32 +/// Power of 10 in SafeInt +/// Uses SafeInt pow function that accepts u32 argument +/// and the formula: 10^(a*b) = (10^a)^b +fn pow10(e: u64) -> SafeInt { + if e == 0 { + return SafeInt::one(); } + let exp_high = ((e & 0xFFFFFFFF00000000) >> 32) as u32; + let exp_low = (e & 0xFFFFFFFF) as u32; + let ten_exp_low = SafeInt::from(10u32).pow(exp_low); + let ten_exp_high = SafeInt::from(10u32).pow(exp_high); + let two_exp_16 = 1u32 << 16; + + ten_exp_high.pow(two_exp_16).pow(two_exp_16) * ten_exp_low +} + +fn intlog10(a: &SafeInt) -> u64 { + let scale = SafeInt::from(1_000_000_000_000_000_000i128); + let precision = 256u32; + let max_iters = Some(4096); + (a.log10(&scale, precision, max_iters)) + .unwrap_or_default() + .to_u64() + .unwrap_or_default() } impl SafeFloat { pub fn zero() -> Self { SafeFloat { mantissa: SafeInt::zero(), - exponent: 0_u32, + exponent: 0_i64, } } - pub fn new(mantissa: SafeInt, exponent: u32) -> Option { + 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, - }; + let mut safe_float = SafeFloat { mantissa, exponent }; safe_float.adjust_precision(); Some(safe_float) } - 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() - } - - /// Adjusts mantissa and exponent of this floating point number so that + /// Adjusts mantissa and exponent of this floating point number so that /// SAFE_FLOAT_MAX <= mantissa < 10 * SAFE_FLOAT_MAX pub fn adjust_precision(&mut self) { let max_value = SafeInt::from(SAFE_FLOAT_MAX); let max_value_div10 = SafeInt::from(SAFE_FLOAT_MAX.checked_div(10).unwrap_or_default()); let mantissa_abs = self.mantissa.clone().abs(); - let exponent_adjustment: i32 = if max_value_div10 > mantissa_abs { + + let exponent_adjustment: i64 = if max_value_div10 > mantissa_abs { + // Mantissa is too low, upscale mantissa + reduce exponent let scale = (max_value_div10 / mantissa_abs).unwrap_or_default(); - (Self::intlog10(&scale) + 1) as i32 + -1 * (intlog10(&scale) + 1) as i64 } else if max_value < mantissa_abs { + // Mantissa is too high, downscale mantissa + increase exponent let scale = (mantissa_abs / max_value).unwrap_or_default(); - -1 * ((Self::intlog10(&scale) + 1) as i32) + (intlog10(&scale) + 1) as i64 } else { - 0i32 + 0i64 }; - self.exponent = clip_to_u32(self.exponent as i32 + exponent_adjustment); + self.exponent = self.exponent + exponent_adjustment; if exponent_adjustment > 0 { - let mantissa_adjustment = SafeInt::from(10).pow(exponent_adjustment as u32); - self.mantissa = self.mantissa.clone() * mantissa_adjustment; - } else { - let mantissa_adjustment = SafeInt::from(10).pow((-1 * exponent_adjustment as i32) as u32); + let mantissa_adjustment = pow10(exponent_adjustment as u64); self.mantissa = (self.mantissa.clone() / mantissa_adjustment).unwrap_or_default(); + } else { + let mantissa_adjustment = pow10(exponent_adjustment.neg() as u64); + self.mantissa = self.mantissa.clone() * mantissa_adjustment } } /// Divide current value by a preserving precision (SAFE_FLOAT_MAX digits in mantissa) - /// result = m1 * 10^e2 / m2 * 10^e1 + /// result = m1 * 10^e1 / m2 * 10^e2 pub fn div(&self, a: &SafeFloat) -> Option { - let ten = SafeInt::from(10u32); - let redundant_exponent = self.exponent + a.exponent; - - let maybe_new_mantissa = self.mantissa.clone() * ten.pow(redundant_exponent as u32) / a.mantissa.clone(); - if let Some(new_mantissa) = maybe_new_mantissa { + // 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_mul(2), + exponent: self + .exponent + .saturating_sub(a.exponent) + .saturating_sub(redundant_exponent), }; safe_float.adjust_precision(); Some(safe_float) @@ -119,10 +135,16 @@ impl SafeFloat { } pub fn add(&self, a: &SafeFloat) -> Self { - let ten = SafeInt::from(10u32); + // 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(a.exponent.saturating_add(exponent_offset) as u64) + + a.mantissa.clone() * pow10(self.exponent.saturating_add(exponent_offset) as u64); + let mut safe_float = SafeFloat { - mantissa: self.mantissa.clone() * ten.clone().pow(a.exponent) + a.mantissa.clone() * ten.pow(self.exponent), - exponent: self.exponent + a.exponent, + mantissa: unnormalized_mantissa, + exponent: exponent_offset.neg(), }; safe_float.adjust_precision(); safe_float @@ -131,14 +153,15 @@ impl SafeFloat { /// Calculate self * a / b without loss of precision pub fn mul_div(&self, a: &SafeFloat, b: &SafeFloat) -> Option { let self_a_mantissa = self.mantissa.clone() * a.mantissa.clone(); - let self_a_exponent = self.exponent + a.exponent; + let self_a_exponent = self.exponent.saturating_add(a.exponent); - // Divide by b without adjusting precision first (preserve higher precision + // Divide by b without adjusting precision first (preserve higher precision // of multiplication result) - SafeFloat{ + SafeFloat { mantissa: self_a_mantissa, - exponent: self_a_exponent - }.div(b) + exponent: self_a_exponent, + } + .div(b) } pub fn is_zero(&self) -> bool { @@ -152,15 +175,13 @@ impl SafeFloat { return self.mantissa > a.mantissa; } - let ten = SafeInt::from(10); - // 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 = ten.clone().pow(k1); - let scale2 = ten.pow(k2); + let scale1 = pow10(k1 as u64); + let scale2 = pow10(k2 as u64); let lhs = &self.mantissa * &scale1; let rhs = &a.mantissa * &scale2; @@ -183,11 +204,14 @@ impl From<&SafeFloat> for u64 { } // scale = 10^exponent - let scale = SafeInt::from(10).pow(value.exponent); + let scale = pow10(value.exponent.abs() as u64); - // mantissa / 10^exponent (integer division, truncating toward zero) - // SafeInt division is fallible; None only if divisor is zero (can't happen here) - let q: SafeInt = (&value.mantissa / &scale).unwrap_or_else(SafeInt::zero); + // 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() { @@ -214,11 +238,14 @@ impl From for SafeFloat { 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(); + value + .mantissa + .encode(&mut mantissa_serializable) + .unwrap_or_default(); SafeFloatSerializable { mantissa: mantissa_serializable, - exponent: value.exponent + exponent: value.exponent, } } } @@ -228,7 +255,7 @@ impl From<&SafeFloatSerializable> for SafeFloat { let decoded = SafeInt::decode(&mut Cursor::new(&value.mantissa)).unwrap_or_default(); SafeFloat { mantissa: decoded, - exponent: value.exponent + exponent: value.exponent, } } } @@ -252,19 +279,17 @@ impl From<&SafeFloat> for f64 { // 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. + // so that mant * 10^exp stays the same value. while mant > max_exact { - mant = (&mant / &ten).expect("10 is non-zero; division must succeed"); - exp_i32 -= 1; // because value = mant * 10^-exp, and we did mant /= 10 + 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() - .expect("mant <= 2^53-1, must fit into u64"); + let mant_u64 = mant.to_u64().unwrap_or_default(); let mant_f = mant_u64 as f64; - let scale = 10f64.powi(-exp_i32); + let scale = 10f64.powi(exp_i32); mant_f * scale } @@ -282,8 +307,6 @@ impl Default for SafeFloat { } } - - pub trait SharePoolDataOperations { /// Gets shared value (always "the real thing" measured in rao, not fractional) fn get_shared_value(&self) -> u64; @@ -325,18 +348,22 @@ where } pub fn get_value(&self, key: &K) -> u64 { - let shared_value: SafeFloat = SafeFloat::new(SafeInt::from(self.state_ops.get_shared_value()), 0).unwrap_or_default(); + 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) + shared_value + .mul_div(¤t_share, &denominator) .unwrap_or(SafeFloat::zero()) .into() } 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 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) + shared_value + .mul_div(¤t_share, &denominator) .unwrap_or(SafeFloat::zero()) .into() } @@ -368,8 +395,7 @@ where true } else { // There are already keys in the pool, set or update this key - let shares_per_update = - 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.is_zero() } @@ -381,9 +407,12 @@ where shared_value: u64, denominator: &SafeFloat, ) -> SafeFloat { - let shared_value: SafeFloat = SafeFloat::new(SafeInt::from(shared_value), 0).unwrap_or_default(); + 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 + .mul_div(denominator, &shared_value) + .unwrap_or_default() } /// Update the value associated with an item identified by the Key @@ -397,20 +426,18 @@ where // Then, update this key's share if denominator.is_zero() { // Initialize the pool. The first key gets all. - let update_float: SafeFloat = SafeFloat::new(SafeInt::from(update), 0).unwrap_or_default(); + 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: SafeFloat = self.get_shares_per_update(update, shared_value, &denominator); - self.state_ops.set_denominator( - denominator.add(&shares_per_update), - ); - self.state_ops.set_share( - key, - current_share.add(&shares_per_update), - ); + self.state_ops + .set_denominator(denominator.add(&shares_per_update)); + self.state_ops + .set_share(key, current_share.add(&shares_per_update)); } // Update shared value @@ -418,13 +445,13 @@ where } } -// cargo test --package share-pool --lib -- tests --nocapture +// cargo test --package share-pool --lib -- tests --nocapture #[cfg(test)] mod tests { use super::*; use approx::assert_abs_diff_eq; - use lencode::{Decode, Encode}; use lencode::io::Cursor; + use lencode::{Decode, Encode}; use std::collections::BTreeMap; use substrate_fixed::types::U64F64; @@ -450,11 +477,7 @@ mod tests { } fn get_share(&self, key: &u16) -> SafeFloat { - self - .share - .get(key) - .cloned() - .unwrap_or_else(SafeFloat::zero) + self.share.get(key).cloned().unwrap_or_else(SafeFloat::zero) } fn try_get_share(&self, key: &u16) -> Result { @@ -530,11 +553,7 @@ mod tests { let value1 = pool.get_value(&1) as i128; let value2 = pool.get_value(&2) as i128; - assert_abs_diff_eq!( - value1 as f64, - 500_000_000 as f64, - epsilon = 1. - ); + assert_abs_diff_eq!(value1 as f64, 500_000_000 as f64, epsilon = 1.); assert!((value2 - 500_000_000).abs() <= 1); } @@ -569,26 +588,26 @@ mod tests { pool.update_value_for_one(&1, 1); pool.update_value_for_one(&2, 1); - // Huge emission resulting in 1M Alpha - // Both stakers should have 500k Alpha each - pool.update_value_for_all(999_999_999_999_998); + // // Huge emission resulting in 1M Alpha + // // Both stakers should have 500k Alpha each + // pool.update_value_for_all(999_999_999_999_998); - // Everyone unstakes almost everything, leaving 10 rao in the stake - pool.update_value_for_one(&1, -499_999_999_999_990); - pool.update_value_for_one(&2, -499_999_999_999_990); + // // Everyone unstakes almost everything, leaving 10 rao in the stake + // pool.update_value_for_one(&1, -499_999_999_999_990); + // pool.update_value_for_one(&2, -499_999_999_999_990); - // Huge emission resulting in 1M Alpha - // Both stakers should have 500k Alpha each - pool.update_value_for_all(999_999_999_999_980); + // // Huge emission resulting in 1M Alpha + // // Both stakers should have 500k Alpha each + // pool.update_value_for_all(999_999_999_999_980); - // Stakers add 1k Alpha each - pool.update_value_for_one(&1, 1_000_000_000_000); - pool.update_value_for_one(&2, 1_000_000_000_000); + // // Stakers add 1k Alpha each + // pool.update_value_for_one(&1, 1_000_000_000_000); + // pool.update_value_for_one(&2, 1_000_000_000_000); - let value1 = pool.get_value(&1) as 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.); + // 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 @@ -626,16 +645,8 @@ mod tests { let value2 = pool.get_value(&2) as i128; let expected = 1_001_000_000_000_000 + tx_count * unstake_amount; - assert_abs_diff_eq!( - value1 as f64, - expected as f64, - epsilon = 1. - ); - assert_abs_diff_eq!( - value2 as f64, - expected as f64, - epsilon = 1. - ); + 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] @@ -664,70 +675,41 @@ 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_u32), - ( - 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_000, - 21_000_000_000_000_000, - 1, - 5 - ), - ( - 21_000_000_000_000_000, - 21_000_000_000_000_000, - 1, - 5 - ), - ( - 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_i64, 1_u64, 1_u64, 0_i64), + (1, 1_000_000_000_000_000_000, 1, 0), + (1, 21_000_000_000_000_000, 1, 5), + (1, 21_000_000_000_000_000, 1, -1_000_000), + (1, 21_000_000_000_000_000, 1, -1_000_000_000), + (1, 21_000_000_000_000_000, 1, -1_000_000_001), + (1_000, 21_000_000_000_000_000, 1, 5), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, 5), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, -5), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, -100), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, 100), + (210_000_000_000_000_000, 21_000_000_000_000_000, 1, 5), + (1_000, 1_000, 21_000_000_000_000_000, 0), + (1_000, 1_000, 21_000_000_000_000_000, -1), ] .into_iter() - .for_each(|(update, shared_value, denominator_mantissa, denominator_exponent)| { - let mock_ops = MockSharePoolDataOperations::new(); - let pool = SharePool::::new(mock_ops); - - let denominator_float = SafeFloat::new(SafeInt::from(denominator_mantissa), denominator_exponent).unwrap(); - let denominator_f64: f64 = denominator_float.clone().into(); - let spu: f64 = - pool.get_shares_per_update(update, shared_value, &denominator_float).into(); - let expected = update as f64 * denominator_f64 / shared_value as f64; - let precision = 1000.; - assert_abs_diff_eq!( - expected, - spu, - epsilon = expected / precision - ); - }); + .for_each( + |(update, shared_value, denominator_mantissa, denominator_exponent)| { + let mock_ops = MockSharePoolDataOperations::new(); + let pool = SharePool::::new(mock_ops); + + let denominator_float = + SafeFloat::new(SafeInt::from(denominator_mantissa), denominator_exponent) + .unwrap(); + let denominator_f64: f64 = denominator_float.clone().into(); + let spu: f64 = pool + .get_shares_per_update(update, shared_value, &denominator_float) + .into(); + let expected = update as f64 * denominator_f64 / shared_value as f64; + let precision = 1000.; + assert_abs_diff_eq!(expected, spu, epsilon = expected / precision); + }, + ); } #[test] @@ -739,4 +721,26 @@ mod tests { let decoded = SafeInt::decode(&mut Cursor::new(&buf)).unwrap(); assert_eq!(decoded, safe_int); } + + #[test] + fn test_safefloat_adjust_precision() { + let a = SafeFloat::new(SafeInt::from(1), 0).unwrap_or_default(); + let b = SafeFloat::new(SafeInt::from(1_000_000_000_000_123_u64), 0).unwrap_or_default(); + let c = a.add(&b); + let d = b.add(&a); + let e = SafeFloat::new(SafeInt::from(SAFE_FLOAT_MAX * 2u128), 0).unwrap_or_default(); + let f = SafeFloat::new(SafeInt::from(SAFE_FLOAT_MAX), 0).unwrap_or_default(); + let g = SafeFloat::new(SafeInt::from(SAFE_FLOAT_MAX), 0).unwrap_or_default(); + let h = g.add(&f); + + println!("a = {:?}", a); + println!("b = {:?}", b); + println!("c = {:?}", c); + println!("d = {:?}", d); + println!("e = {:?}", e); + println!("g = {:?}", g); + println!("h = {:?}", h); + + // assert_eq!(decoded, safe_int); + } } From 3716b0cd0a0444248435c6e0608b43c55683b562 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 15 Jan 2026 17:00:20 -0500 Subject: [PATCH 07/15] Fix safe float max exp type --- primitives/share-pool/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index 85f0e1c2c6..542f3f0d1e 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -11,7 +11,7 @@ use sp_std::ops::Neg; // 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: u128 = 18_i64; +pub const SAFE_FLOAT_MAX_EXP: i64 = 18_i64; /// Controlled precision floating point number with efficient storage /// From 054abb155d7b558523d1289acee54654b66f4f67 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 15 Jan 2026 17:09:32 -0500 Subject: [PATCH 08/15] Update lencode --- Cargo.lock | 12 +++++++----- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71af766313..b9c92f94b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5808,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", ] @@ -6931,13 +6933,13 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lencode" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7914750d49f4d7824709d4254b92d29f876e98ec33cdaa134fdbe4e4c566bee8" +checksum = "83dc280ed78264020f986b2539e6a44e0720f98f66c99a48a2f52e4a441e99d8" dependencies = [ "endian-cast", "generic-array 1.3.5", - "hashbrown 0.12.3", + "hashbrown 0.16.0", "lencode-macros", "newt-hype", "ruint", @@ -6946,9 +6948,9 @@ dependencies = [ [[package]] name = "lencode-macros" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f382b3593c743195f02bbcc2f8631391d9cc7c954b87e8d0a28b6a2a9242b19" +checksum = "61c57df14b9005d1e4e8e56436e922e2c046ad0be55d7cfb062a303714857508" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 4bc3fa93bb..65904b6c68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +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" +lencode = "0.1.6" log = { version = "0.4.21", default-features = false } memmap2 = "0.9.8" ndarray = { version = "0.16.1", default-features = false } From b523ff77b2ebb52c34a550b7b1e3302dee093c4f Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 15 Jan 2026 17:58:52 -0500 Subject: [PATCH 09/15] Fix safefloat addition and mantissa normalization, add tests --- primitives/share-pool/src/lib.rs | 71 +++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index 542f3f0d1e..c9d1ea3c8e 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -11,7 +11,7 @@ use sp_std::ops::Neg; // 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 = 18_i64; +pub const SAFE_FLOAT_MAX_EXP: i64 = 21_i64; /// Controlled precision floating point number with efficient storage /// @@ -81,12 +81,14 @@ impl SafeFloat { /// Adjusts mantissa and exponent of this floating point number so that /// SAFE_FLOAT_MAX <= mantissa < 10 * SAFE_FLOAT_MAX - pub fn adjust_precision(&mut self) { + pub(crate) fn adjust_precision(&mut self) { let max_value = SafeInt::from(SAFE_FLOAT_MAX); let max_value_div10 = SafeInt::from(SAFE_FLOAT_MAX.checked_div(10).unwrap_or_default()); let mantissa_abs = self.mantissa.clone().abs(); - let exponent_adjustment: i64 = if max_value_div10 > mantissa_abs { + let exponent_adjustment: i64 = if mantissa_abs.is_zero() { + 0i64 + } else if max_value_div10 >= mantissa_abs { // Mantissa is too low, upscale mantissa + reduce exponent let scale = (max_value_div10 / mantissa_abs).unwrap_or_default(); -1 * (intlog10(&scale) + 1) as i64 @@ -139,8 +141,8 @@ impl SafeFloat { // (lowest exponent becomes 0) let exponent_offset = self.exponent.min(a.exponent).neg(); let unnormalized_mantissa = self.mantissa.clone() - * pow10(a.exponent.saturating_add(exponent_offset) as u64) - + a.mantissa.clone() * pow10(self.exponent.saturating_add(exponent_offset) as u64); + * 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, @@ -724,23 +726,46 @@ mod tests { #[test] fn test_safefloat_adjust_precision() { - let a = SafeFloat::new(SafeInt::from(1), 0).unwrap_or_default(); - let b = SafeFloat::new(SafeInt::from(1_000_000_000_000_123_u64), 0).unwrap_or_default(); - let c = a.add(&b); - let d = b.add(&a); - let e = SafeFloat::new(SafeInt::from(SAFE_FLOAT_MAX * 2u128), 0).unwrap_or_default(); - let f = SafeFloat::new(SafeInt::from(SAFE_FLOAT_MAX), 0).unwrap_or_default(); - let g = SafeFloat::new(SafeInt::from(SAFE_FLOAT_MAX), 0).unwrap_or_default(); - let h = g.add(&f); - - println!("a = {:?}", a); - println!("b = {:?}", b); - println!("c = {:?}", c); - println!("d = {:?}", d); - println!("e = {:?}", e); - println!("g = {:?}", g); - println!("h = {:?}", h); - - // assert_eq!(decoded, safe_int); + // 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_u128, 0, 1_u128, 0, 200_000_000_000_000_000_000_u128, -20_i64), + (SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX * 2 / 10, 1_i64), + // Expected loss of precision + (1_u128, 0, 1_000_000_000_000_000_000_000_u128, 1, 1_000_000_000_000_000_000_000_u128, 1_i64), + ] + .into_iter() + .for_each(|(m_a, e_a, m_b, e_b, expected_m, expected_e)| { + let a = SafeFloat::new(SafeInt::from(m_a), e_a).unwrap(); + let b = SafeFloat::new(SafeInt::from(m_b), e_b).unwrap(); + + let a_plus_b = a.add(&b); + let b_plus_a = b.add(&a); + + assert_eq!(a_plus_b.mantissa, SafeInt::from(expected_m)); + assert_eq!(a_plus_b.exponent, SafeInt::from(expected_e)); + assert_eq!(b_plus_a.mantissa, SafeInt::from(expected_m)); + assert_eq!(b_plus_a.exponent, SafeInt::from(expected_e)); + }); } + + } From 2151c1fe363ef20ca0379c3912aac5576362d538 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 15 Jan 2026 18:02:36 -0500 Subject: [PATCH 10/15] Add more test cases for safefloat add tests --- primitives/share-pool/src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index c9d1ea3c8e..ade2c37805 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -751,6 +751,10 @@ mod tests { (SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX * 2 / 10, 1_i64), // Expected loss of precision (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), ] .into_iter() .for_each(|(m_a, e_a, m_b, e_b, expected_m, expected_e)| { @@ -767,5 +771,14 @@ mod tests { }); } + #[test] + fn test_safefloat_div() { + todo!() + } + + #[test] + fn test_safefloat_mul_div() { + todo!() + } } From e138732fbd2a23bc155bcb466f9be31e24b33c35 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 15 Jan 2026 18:02:52 -0500 Subject: [PATCH 11/15] fmt --- primitives/share-pool/src/lib.rs | 71 ++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index ade2c37805..a94b562890 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -732,7 +732,12 @@ mod tests { (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), + ( + 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() @@ -747,14 +752,63 @@ mod tests { 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_u128, 0, 1_u128, 0, 200_000_000_000_000_000_000_u128, -20_i64), - (SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX * 2 / 10, 1_i64), + ( + 1_u128, + 0, + 1_u128, + 0, + 200_000_000_000_000_000_000_u128, + -20_i64, + ), + ( + SAFE_FLOAT_MAX, + 0, + SAFE_FLOAT_MAX, + 0, + SAFE_FLOAT_MAX * 2 / 10, + 1_i64, + ), // Expected loss of precision - (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), + ( + 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, + ), ] .into_iter() .for_each(|(m_a, e_a, m_b, e_b, expected_m, expected_e)| { @@ -780,5 +834,4 @@ mod tests { fn test_safefloat_mul_div() { todo!() } - } From f616e11a6a7867bedf46d5b1bf3d1ba9287be923 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 15 Jan 2026 21:58:37 -0500 Subject: [PATCH 12/15] Add more test cases for safefloat add --- primitives/share-pool/src/lib.rs | 112 ++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index a94b562890..f56498c383 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -752,6 +752,7 @@ mod tests { fn test_safefloat_add() { // Test case: man_a, exp_a, man_b, exp_b, expected mantissa of a+b, expected exponent of a+b [ + // 1 + 1 = 2 ( 1_u128, 0, @@ -760,6 +761,7 @@ mod tests { 200_000_000_000_000_000_000_u128, -20_i64, ), + // SAFE_FLOAT_MAX + SAFE_FLOAT_MAX ( SAFE_FLOAT_MAX, 0, @@ -768,7 +770,7 @@ mod tests { SAFE_FLOAT_MAX * 2 / 10, 1_i64, ), - // Expected loss of precision + // Expected loss of precision: tiny + huge ( 1_u128, 0, @@ -809,6 +811,112 @@ mod tests { 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_u128, + 24, + 1_u128, + 0, + 1_000_000_000_000_000_000_000_u128, + 3_i64, + ), + // 1 * 10^24 + a non-trivial small mantissa + // 1e24 + 123456789012345678901 -> floor(/1000) = 1e21 + 123456789012345678 + ( + 1_u128, + 24, + 123_456_789_012_345_678_901_u128, + 0, + 1_000_123_456_789_012_345_678_u128, + 3_i64, + ), + // 10^22 and 10^23 combined: + // 1 * 10^22 + 1 * 10^23 = 11 * 10^22 = (1.1 * 10^23) + // Normalized → (1.1e20) * 10^3 + ( + 1_u128, + 22, + 1_u128, + 23, + 110_000_000_000_000_000_000_u128, + 3_i64, + ), + // Both operands already aligned at a huge scale: + // (10^21 - 1) * 10^22 + 1 * 10^22 = 10^21 * 10^22 = 10^43 + // Canonical form: (1e21) * 10^22 + ( + 999_999_999_999_999_999_999_u128, + 22, + 1_u128, + 22, + 1_000_000_000_000_000_000_000_u128, + 22_i64, + ), ] .into_iter() .for_each(|(m_a, e_a, m_b, e_b, expected_m, expected_e)| { @@ -828,10 +936,12 @@ mod tests { #[test] fn test_safefloat_div() { todo!() + // Test with f64 } #[test] fn test_safefloat_mul_div() { todo!() + // Test with f64 } } From cbb41d1e029ade818c57c85d0cd16bfdcb758529 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 16 Jan 2026 10:21:09 -0500 Subject: [PATCH 13/15] Add safefloat tests for div and mul_div --- primitives/share-pool/src/lib.rs | 109 ++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index f56498c383..107056db95 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -75,13 +75,13 @@ impl SafeFloat { let mut safe_float = SafeFloat { mantissa, exponent }; - safe_float.adjust_precision(); + safe_float.normalize(); Some(safe_float) } /// Adjusts mantissa and exponent of this floating point number so that /// SAFE_FLOAT_MAX <= mantissa < 10 * SAFE_FLOAT_MAX - pub(crate) fn adjust_precision(&mut self) { + pub(crate) fn normalize(&mut self) { let max_value = SafeInt::from(SAFE_FLOAT_MAX); let max_value_div10 = SafeInt::from(SAFE_FLOAT_MAX.checked_div(10).unwrap_or_default()); let mantissa_abs = self.mantissa.clone().abs(); @@ -129,7 +129,7 @@ impl SafeFloat { .saturating_sub(a.exponent) .saturating_sub(redundant_exponent), }; - safe_float.adjust_precision(); + safe_float.normalize(); Some(safe_float) } else { None @@ -148,7 +148,7 @@ impl SafeFloat { mantissa: unnormalized_mantissa, exponent: exponent_offset.neg(), }; - safe_float.adjust_precision(); + safe_float.normalize(); safe_float } @@ -725,7 +725,7 @@ mod tests { } #[test] - fn test_safefloat_adjust_precision() { + 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), @@ -933,15 +933,106 @@ mod tests { }); } + #[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() { - todo!() - // Test with f64 + // 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() { - todo!() - // Test with f64 + // 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 + ); + }); } } From 60f715397c7f9e8f2d7b698cf3a82bf1cb470ae4 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 16 Jan 2026 10:55:38 -0500 Subject: [PATCH 14/15] Handle overflows in safefloat --- Cargo.lock | 1 + primitives/share-pool/Cargo.toml | 2 + primitives/share-pool/src/lib.rs | 82 ++++++++++++++++++++++++++------ 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9c92f94b5..6aa10abf07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16547,6 +16547,7 @@ version = "0.1.0" dependencies = [ "approx", "lencode", + "log", "parity-scale-codec", "safe-bigmath", "safe-math", diff --git a/primitives/share-pool/Cargo.toml b/primitives/share-pool/Cargo.toml index b5da06cff6..8da85e805f 100644 --- a/primitives/share-pool/Cargo.toml +++ b/primitives/share-pool/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true approx.workspace = true codec.workspace = true lencode.workspace = true +log.workspace = true scale-info.workspace = true substrate-fixed.workspace = true sp-std.workspace = true @@ -21,6 +22,7 @@ default = ["std"] std = [ "codec/std", "lencode/std", + "log/std", "scale-info/std", "substrate-fixed/std", "sp-std/std", diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index 107056db95..8dbe8ba693 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -75,13 +75,18 @@ impl SafeFloat { let mut safe_float = SafeFloat { mantissa, exponent }; - safe_float.normalize(); - Some(safe_float) + 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 - pub(crate) fn normalize(&mut self) { + /// + /// Returns true in case of success or false if exponent over- or underflows + 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(); @@ -91,16 +96,22 @@ impl SafeFloat { } else if max_value_div10 >= mantissa_abs { // Mantissa is too low, upscale mantissa + reduce exponent let scale = (max_value_div10 / mantissa_abs).unwrap_or_default(); - -1 * (intlog10(&scale) + 1) as i64 + ((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) + 1) as i64 + (intlog10(&scale).saturating_add(1)) as i64 } else { 0i64 }; - self.exponent = self.exponent + exponent_adjustment; + // 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); @@ -109,6 +120,13 @@ impl SafeFloat { 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) @@ -129,14 +147,17 @@ impl SafeFloat { .saturating_sub(a.exponent) .saturating_sub(redundant_exponent), }; - safe_float.normalize(); - Some(safe_float) + if safe_float.normalize() { + Some(safe_float) + } else { + None + } } else { None } } - pub fn add(&self, a: &SafeFloat) -> Self { + 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(); @@ -148,8 +169,11 @@ impl SafeFloat { mantissa: unnormalized_mantissa, exponent: exponent_offset.neg(), }; - safe_float.normalize(); - safe_float + if safe_float.normalize() { + Some(safe_float) + } else { + None + } } /// Calculate self * a / b without loss of precision @@ -436,10 +460,38 @@ where 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(denominator.add(&shares_per_update)); + .set_denominator(new_denominator); self.state_ops - .set_share(key, current_share.add(&shares_per_update)); + .set_share(key, new_current_share); } // Update shared value @@ -923,8 +975,8 @@ mod tests { let a = SafeFloat::new(SafeInt::from(m_a), e_a).unwrap(); let b = SafeFloat::new(SafeInt::from(m_b), e_b).unwrap(); - let a_plus_b = a.add(&b); - let b_plus_a = b.add(&a); + 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)); From 9360f920bbf8af6356cd97925500fb9675b5de8c Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 16 Jan 2026 10:55:53 -0500 Subject: [PATCH 15/15] fmt --- primitives/share-pool/src/lib.rs | 99 ++++++++++++++++++++++---------- 1 file changed, 68 insertions(+), 31 deletions(-) diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index 8dbe8ba693..b0a7e92316 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -84,7 +84,7 @@ impl SafeFloat { /// 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 pub(crate) fn normalize(&mut self) -> bool { let max_value = SafeInt::from(SAFE_FLOAT_MAX); @@ -460,7 +460,7 @@ where 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 + // 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, @@ -488,10 +488,8 @@ where } }; - self.state_ops - .set_denominator(new_denominator); - self.state_ops - .set_share(key, new_current_share); + self.state_ops.set_denominator(new_denominator); + self.state_ops.set_share(key, new_current_share); } // Update shared value @@ -1031,18 +1029,48 @@ mod tests { ), (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), + (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), + ( + 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)| { @@ -1050,13 +1078,10 @@ mod tests { 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)); + 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 - ); + assert_abs_diff_eq!(actual, expected, epsilon = actual / 100_000_000_000_000_f64); }); } @@ -1068,8 +1093,22 @@ mod tests { [ (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), + ( + 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)| { @@ -1078,13 +1117,11 @@ mod tests { 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)); + 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 - ); + assert_abs_diff_eq!(actual, expected, epsilon = actual / 100_000_000_000_000_f64); }); } }