From 0e27cb74d7604e80771cfa186abcd6a5a42c6cca Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 17 Apr 2026 13:53:27 -0400 Subject: [PATCH 01/13] Initial commit for convictions --- pallets/subtensor/src/lib.rs | 37 + pallets/subtensor/src/macros/dispatches.rs | 33 + pallets/subtensor/src/macros/errors.rs | 4 + pallets/subtensor/src/macros/events.rs | 12 + pallets/subtensor/src/staking/lock.rs | 212 +++ pallets/subtensor/src/staking/mod.rs | 1 + pallets/subtensor/src/staking/stake_utils.rs | 11 + pallets/subtensor/src/tests/locks.rs | 1537 ++++++++++++++++++ pallets/subtensor/src/tests/mod.rs | 1 + 9 files changed, 1848 insertions(+) create mode 100644 pallets/subtensor/src/staking/lock.rs create mode 100644 pallets/subtensor/src/tests/locks.rs diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 94a18d7d16..e0a07653a6 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1492,6 +1492,43 @@ pub mod pallet { ValueQuery, >; + /// Exponential lock state for a coldkey on a subnet. + #[derive( + Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, + )] + pub struct LockState { + /// The hotkey this stake is locked to. + pub hotkey: AccountId, + /// Exponentially decaying locked amount. + pub locked_mass: AlphaBalance, + /// Matured decaying score (integral of locked_mass over time). + pub conviction: U64F64, + /// Block number of last roll-forward. + pub last_update: u64, + } + + /// --- DMAP ( coldkey, netuid ) --> LockState | Exponential lock per coldkey per subnet. + #[pallet::storage] + pub type Lock = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, // coldkey + Identity, + NetUid, // subnet + LockState, + OptionQuery, + >; + + /// Default decay timescale: ~30 days at 12s blocks. + #[pallet::type_value] + pub fn DefaultTauBlocks() -> u64 { + 7200 * 30 + } + + /// --- ITEM( tau_blocks ) | Decay timescale in blocks for exponential lock. + #[pallet::storage] + pub type TauBlocks = StorageValue<_, u64, ValueQuery, DefaultTauBlocks>; + /// Contains last Alpha storage map key to iterate (check first) #[pallet::storage] pub type AlphaMapLastKey = diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index b098b58425..b3e9daa6e3 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2533,5 +2533,38 @@ mod dispatches { Self::deposit_event(Event::AutoParentDelegationEnabledSet { hotkey, enabled }); Ok(()) } + + /// Locks stake on a subnet to a specific hotkey, building conviction over time. + /// + /// If no lock exists for (coldkey, subnet), a new one is created. + /// If a lock exists, the destination hotkey must match the existing lock's hotkey. + /// Top-up adds to the locked amount after rolling the lock state forward. + /// + /// # Arguments + /// * `origin` - Must be signed by the coldkey. + /// * `hotkey` - The hotkey to lock stake to. + /// * `netuid` - The subnet on which to lock. + /// * `amount` - The alpha amount to lock. + #[pallet::call_index(136)] + #[pallet::weight((Weight::from_parts(46_000_000, 0) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(1)), + DispatchClass::Normal, + Pays::Yes + ))] + pub fn lock_stake( + origin: OriginFor, + hotkey: T::AccountId, + netuid: NetUid, + amount: AlphaBalance, + ) -> DispatchResult { + let coldkey = ensure_signed(origin)?; + Self::do_lock_stake( + &coldkey, + netuid, + &hotkey, + amount, + ) + } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index dda057bb07..87c4152795 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -293,5 +293,9 @@ mod errors { DisabledTemporarily, /// Registration Price Limit Exceeded RegistrationPriceLimitExceeded, + /// Lock hotkey mismatch: existing lock is for a different hotkey. + LockHotkeyMismatch, + /// Insufficient stake on subnet to cover the lock amount. + InsufficientStakeForLock, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index fe10bfec7a..f0da9a3d3d 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -570,5 +570,17 @@ mod events { /// Whether delegation is now enabled. enabled: bool, }, + + /// Stake has been locked to a hotkey on a subnet. + StakeLocked { + /// The coldkey that locked the stake. + coldkey: T::AccountId, + /// The hotkey the stake is locked to. + hotkey: T::AccountId, + /// The subnet the stake is locked on. + netuid: NetUid, + /// The alpha amount locked. + amount: AlphaBalance, + }, } } diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs new file mode 100644 index 0000000000..e496a41011 --- /dev/null +++ b/pallets/subtensor/src/staking/lock.rs @@ -0,0 +1,212 @@ +use super::*; +use substrate_fixed::transcendental::exp; +use substrate_fixed::types::{I64F64, U64F64}; +use subtensor_runtime_common::NetUid; + +const DUST_THRESHOLD: u64 = 100; + +impl Pallet { + /// Computes exp(-dt / tau) as a U64F64 decay factor. + pub fn exp_decay(dt: u64, tau: u64) -> U64F64 { + if tau == 0 || dt == 0 { + if dt == 0 { + return U64F64::saturating_from_num(1); + } + return U64F64::saturating_from_num(0); + } + let min_ratio = I64F64::saturating_from_num(-40); + let neg_ratio = + I64F64::saturating_from_num(-(dt as i128)).checked_div(I64F64::saturating_from_num(tau)).unwrap_or(min_ratio); + let clamped = neg_ratio.max(min_ratio); + let result: I64F64 = exp(clamped).unwrap_or(I64F64::saturating_from_num(0)); + if result < I64F64::saturating_from_num(0) { + U64F64::saturating_from_num(0) + } else { + U64F64::saturating_from_num(result) + } + } + + /// Rolls a LockState forward to `now` using exponential decay. + /// + /// X_new = decay * X_old + /// Y_new = decay * (Y_old + dt * X_old) + pub fn roll_forward_lock( + lock: LockState, + now: u64, + ) -> LockState { + if now <= lock.last_update { + return lock; + } + let dt = now.saturating_sub(lock.last_update); + let tau = TauBlocks::::get(); + let decay = Self::exp_decay(dt, tau); + + let dt_fixed = U64F64::saturating_from_num(dt); + let mass_fixed = U64F64::saturating_from_num(lock.locked_mass); + let new_locked_mass = decay.saturating_mul(mass_fixed).saturating_to_num::().into(); + let new_conviction = + decay.saturating_mul(lock.conviction.saturating_add(dt_fixed.saturating_mul(mass_fixed))); + + LockState { + hotkey: lock.hotkey, + locked_mass: new_locked_mass, + conviction: new_conviction, + last_update: now, + } + } + + /// Returns the sum of raw alpha shares for a coldkey across all hotkeys on a given subnet. + pub fn total_coldkey_alpha_on_subnet(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { + StakingHotkeys::::get(coldkey) + .into_iter() + .map(|hotkey| Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, coldkey, netuid)) + .fold(AlphaBalance::ZERO, |acc, stake| acc.saturating_add(stake)) + } + + /// Returns the current locked amount for a coldkey on a subnet (rolled forward to now). + pub fn get_current_locked(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { + let now = Self::get_current_block_as_u64(); + match Lock::::get(coldkey, netuid) { + Some(lock) => Self::roll_forward_lock(lock, now).locked_mass, + None => AlphaBalance::ZERO, + } + } + + /// Returns the current conviction for a coldkey on a subnet (rolled forward to now). + pub fn get_conviction(coldkey: &T::AccountId, netuid: NetUid) -> U64F64 { + let now = Self::get_current_block_as_u64(); + match Lock::::get(coldkey, netuid) { + Some(lock) => Self::roll_forward_lock(lock, now).conviction, + None => U64F64::saturating_from_num(0), + } + } + + /// Returns the alpha amount available to unstake for a coldkey on a subnet. + pub fn available_to_unstake(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { + let total = Self::total_coldkey_alpha_on_subnet(coldkey, netuid); + let locked = Self::get_current_locked(coldkey, netuid); + if total > locked { + total.saturating_sub(locked) + } else { + AlphaBalance::ZERO + } + } + + /// Locks stake for a coldkey on a subnet to a specific hotkey. + /// If no lock exists, creates one. If one exists, the hotkey must match. + /// Top-up adds to locked_mass after rolling forward. + pub fn do_lock_stake( + coldkey: &T::AccountId, + netuid: NetUid, + hotkey: &T::AccountId, + amount: AlphaBalance, + ) -> dispatch::DispatchResult { + ensure!( + !amount.is_zero(), + Error::::AmountTooLow + ); + + let total = Self::total_coldkey_alpha_on_subnet(coldkey, netuid); + let now = Self::get_current_block_as_u64(); + + match Lock::::get(coldkey, netuid) { + None => { + ensure!(total >= amount, Error::::InsufficientStakeForLock); + Lock::::insert( + coldkey, + netuid, + LockState { + hotkey: hotkey.clone(), + locked_mass: amount, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }, + ); + } + Some(existing) => { + ensure!( + *hotkey == existing.hotkey, + Error::::LockHotkeyMismatch + ); + let lock = Self::roll_forward_lock(existing, now); + let new_locked = lock.locked_mass.saturating_add(amount); + ensure!(total >= new_locked, Error::::InsufficientStakeForLock); + Lock::::insert( + coldkey, + netuid, + LockState { + hotkey: lock.hotkey, + locked_mass: new_locked, + conviction: lock.conviction, + last_update: now, + }, + ); + } + } + + Self::deposit_event(Event::StakeLocked { + coldkey: coldkey.clone(), + hotkey: hotkey.clone(), + netuid, + amount, + }); + + Ok(()) + } + + /// Clears the lock if both locked_mass and conviction have decayed below the dust threshold. + pub fn maybe_cleanup_lock(coldkey: &T::AccountId, netuid: NetUid) { + if let Some(existing) = Lock::::get(coldkey, netuid) { + let now = Self::get_current_block_as_u64(); + let lock = Self::roll_forward_lock(existing, now); + let dust = DUST_THRESHOLD.into(); + + if lock.locked_mass < dust && lock.conviction < U64F64::saturating_from_num(DUST_THRESHOLD) { + Lock::::remove(coldkey, netuid); + } else { + Lock::::insert(coldkey, netuid, lock); + } + } + } + + /// Returns the total conviction for a hotkey on a subnet, + /// summed over all coldkeys that have locked to this hotkey. + pub fn hotkey_conviction(hotkey: &T::AccountId, netuid: NetUid) -> U64F64 { + let now = Self::get_current_block_as_u64(); + let mut total = U64F64::saturating_from_num(0); + for (_coldkey, _subnet_id, lock) in Lock::::iter() { + if _subnet_id != netuid { + continue; + } + if *hotkey == lock.hotkey { + let rolled = Self::roll_forward_lock(lock, now); + total = total.saturating_add(rolled.conviction); + } + } + total + } + + /// Finds the hotkey with the highest conviction on a given subnet. + pub fn subnet_king(netuid: NetUid) -> Option { + let now = Self::get_current_block_as_u64(); + let mut scores: sp_std::collections::btree_map::BTreeMap, (T::AccountId, U64F64)> = + sp_std::collections::btree_map::BTreeMap::new(); + + for (_coldkey, subnet_id, lock) in Lock::::iter() { + if subnet_id != netuid { + continue; + } + let rolled = Self::roll_forward_lock(lock, now); + let key = rolled.hotkey.encode(); + let entry = scores + .entry(key) + .or_insert_with(|| (rolled.hotkey.clone(), U64F64::saturating_from_num(0))); + entry.1 = entry.1.saturating_add(rolled.conviction); + } + + scores + .into_values() + .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(sp_std::cmp::Ordering::Equal)) + .map(|(hotkey, _)| hotkey) + } +} diff --git a/pallets/subtensor/src/staking/mod.rs b/pallets/subtensor/src/staking/mod.rs index ad2b66189f..a10908eca3 100644 --- a/pallets/subtensor/src/staking/mod.rs +++ b/pallets/subtensor/src/staking/mod.rs @@ -5,6 +5,7 @@ mod claim_root; pub mod decrease_take; pub mod helpers; pub mod increase_take; +pub mod lock; pub mod move_stake; pub mod recycle_alpha; pub mod remove_stake; diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 5e22cc09ac..363f7d6276 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -1157,6 +1157,10 @@ impl Pallet { Error::::HotKeyAccountNotExists ); + // Ensure that unstaked amount is not greater than available to unstake (due to locks) + let alpha_available = Self::available_to_unstake(coldkey, netuid); + ensure!(alpha_available >= alpha_unstaked, Error::::CannotUnstakeLock); + Ok(()) } @@ -1302,6 +1306,13 @@ impl Pallet { } } + // Enforce lock invariant: if the operation reduces total coldkey alpha on origin subnet + // (cross-coldkey transfer or cross-subnet move), the remaining amount must cover the lock. + if origin_coldkey != destination_coldkey || origin_netuid != destination_netuid { + let alpha_available = Self::available_to_unstake(origin_coldkey, origin_netuid); + ensure!(alpha_available >= alpha_amount, Error::::CannotUnstakeLock); + } + Ok(()) } diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs new file mode 100644 index 0000000000..9de6abfa2a --- /dev/null +++ b/pallets/subtensor/src/tests/locks.rs @@ -0,0 +1,1537 @@ +#![allow(clippy::unwrap_used, clippy::arithmetic_side_effects)] + +use approx::assert_abs_diff_eq; +use frame_support::{assert_noop, assert_ok}; +use frame_support::weights::Weight; +use sp_core::U256; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AlphaBalance, TaoBalance}; +use subtensor_swap_interface::SwapHandler; + +use super::mock::*; +use crate::*; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn setup_subnet_with_stake( + coldkey: U256, + hotkey: U256, + stake_tao: u64, +) -> subtensor_runtime_common::NetUid { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let amount: TaoBalance = (stake_tao).into(); + setup_reserves( + netuid, + (stake_tao * 1_000_000).into(), + (stake_tao * 10_000_000).into(), + ); + + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + ) + .unwrap(); + + netuid +} + +fn get_alpha(hotkey: &U256, coldkey: &U256, netuid: subtensor_runtime_common::NetUid) -> AlphaBalance { + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid) +} + +// ========================================================================= +// GROUP 1: Green-path — basic lock creation +// ========================================================================= + +#[test] +fn test_lock_stake_creates_new_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let alpha = get_alpha(&hotkey, &coldkey, netuid); + let lock_amount = alpha.to_u64() / 2; + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount.into(), + )); + + let lock = Lock::::get(coldkey, netuid).expect("Lock should exist"); + assert_eq!(lock.hotkey, hotkey); + assert_eq!(lock.locked_mass, lock_amount.into()); + assert_eq!(lock.conviction, U64F64::saturating_from_num(0)); + assert_eq!(lock.last_update, SubtensorModule::get_current_block_as_u64()); + }); +} + +#[test] +fn test_lock_stake_emits_event() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let lock_amount: u64 = 1000; + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount.into(), + )); + + System::assert_last_event( + Event::StakeLocked { + coldkey, + hotkey, + netuid, + amount: lock_amount.into(), + } + .into(), + ); + }); +} + +#[test] +fn test_lock_stake_full_amount() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total_alpha = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert!(!total_alpha.is_zero()); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + total_alpha, + )); + + let lock = Lock::::get(coldkey, netuid).unwrap(); + assert_eq!(lock.locked_mass, total_alpha); + }); +} + +// ========================================================================= +// GROUP 2: Green-path — lock queries +// ========================================================================= + +#[test] +fn test_get_current_locked_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let netuid = subtensor_runtime_common::NetUid::from(1); + assert_eq!( + SubtensorModule::get_current_locked(&coldkey, netuid), + AlphaBalance::ZERO + ); + }); +} + +#[test] +fn test_get_conviction_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let netuid = subtensor_runtime_common::NetUid::from(1); + assert_eq!( + SubtensorModule::get_conviction(&coldkey, netuid), + U64F64::saturating_from_num(0) + ); + }); +} + +#[test] +fn test_available_to_unstake_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let available = SubtensorModule::available_to_unstake(&coldkey, netuid); + assert_eq!(available, total); + }); +} + +#[test] +fn test_available_to_unstake_with_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let lock_amount = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + let available = SubtensorModule::available_to_unstake(&coldkey, netuid); + assert_eq!(available, total - lock_amount); + }); +} + +#[test] +fn test_available_to_unstake_fully_locked() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + total, + )); + + let available = SubtensorModule::available_to_unstake(&coldkey, netuid); + assert_eq!(available, AlphaBalance::ZERO); + }); +} + +// ========================================================================= +// GROUP 3: Incremental locks (top-up) +// ========================================================================= + +#[test] +fn test_lock_stake_topup() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let first_lock = 1000u64; + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, first_lock.into())); + + step_block(100); + + let second_lock = 500u64; + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, second_lock.into())); + + let lock = Lock::::get(coldkey, netuid).unwrap(); + // locked_mass should be decayed(first_lock) + second_lock + // Since tau is large (216000), decay over 100 blocks is small; locked_mass ~ 1000 + 500 + assert!(lock.locked_mass > 1490.into()); + assert!(lock.locked_mass < 1501.into()); + // conviction should have grown from the time the first lock was active + assert!(lock.conviction > U64F64::saturating_from_num(0)); + assert_eq!(lock.last_update, SubtensorModule::get_current_block_as_u64()); + }); +} + +#[test] +fn test_lock_stake_topup_multiple_times() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let chunk = 500u64.into(); + + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, chunk)); + step_block(50); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, chunk)); + step_block(50); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, chunk)); + + let lock = Lock::::get(coldkey, netuid).unwrap(); + // After three top-ups with small decay, should be close to 1500 + assert!(lock.locked_mass > 1490.into()); + assert!(lock.locked_mass <= 1500.into()); + assert!(lock.conviction > U64F64::saturating_from_num(0)); + }); +} + +#[test] +fn test_lock_stake_topup_same_block() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let first = 1000u64.into(); + let second = 500u64.into(); + + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, first)); + // No block advancement — same block top-up + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, second)); + + let lock = Lock::::get(coldkey, netuid).unwrap(); + // dt=0 means no decay, simple addition + assert_eq!(lock.locked_mass, first + second); + assert_eq!(lock.conviction, U64F64::saturating_from_num(0)); + }); +} + +// ========================================================================= +// GROUP 4: Lock rejection cases +// ========================================================================= + +#[test] +fn test_lock_stake_zero_amount() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_noop!( + SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + AlphaBalance::ZERO, + ), + Error::::AmountTooLow + ); + }); +} + +#[test] +fn test_lock_stake_exceeds_total_alpha() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let too_much = total + 1.into(); + + assert_noop!( + SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, too_much), + Error::::InsufficientStakeForLock + ); + }); +} + +#[test] +fn test_lock_stake_wrong_hotkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + let netuid = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey_a, + 1000u64.into(), + )); + + assert_noop!( + SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey_b, + 500u64.into(), + ), + Error::::LockHotkeyMismatch + ); + }); +} + +#[test] +fn test_lock_stake_topup_exceeds_total() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + // Lock 80% initially + let initial = total * 8.into() / 10.into(); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, initial)); + + // Try to top up the remaining 30% (exceeds total by 10%) + let topup = total * 3.into() / 10.into(); + assert_noop!( + SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, topup), + Error::::InsufficientStakeForLock + ); + }); +} + +// ========================================================================= +// GROUP 5: Exponential decay math +// ========================================================================= + +#[test] +fn test_exp_decay_zero_dt() { + new_test_ext(1).execute_with(|| { + let result = SubtensorModule::exp_decay(0, 216000); + assert_eq!(result, U64F64::saturating_from_num(1)); + }); +} + +#[test] +fn test_exp_decay_zero_tau() { + new_test_ext(1).execute_with(|| { + let result = SubtensorModule::exp_decay(1000, 0); + assert_eq!(result, U64F64::saturating_from_num(0)); + }); +} + +#[test] +fn test_exp_decay_one_tau() { + new_test_ext(1).execute_with(|| { + let tau = 216000u64; + let result = SubtensorModule::exp_decay(tau, tau); + // exp(-1) ~= 0.36787944 + let expected = U64F64::saturating_from_num(0.36787944f64); + let diff = if result > expected { + result - expected + } else { + expected - result + }; + assert!(diff < U64F64::saturating_from_num(0.001)); + }); +} + +#[test] +fn test_roll_forward_locked_mass_decays() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let lock_amount = 10000u64; + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, lock_amount.into())); + + // Advance one full tau via direct block number jump (step_block overflows u16 for tau=216000) + let tau = TauBlocks::::get(); + let target = System::block_number() + tau; + System::set_block_number(target); + + let locked = SubtensorModule::get_current_locked(&coldkey, netuid); + // After one tau, locked should be ~36.8% of original + assert!(locked < lock_amount.into()); + let expected = lock_amount as f64 * 0.368; + assert_abs_diff_eq!( + u64::from(locked) as f64, + expected, + epsilon = lock_amount as f64 / 10. + ); + }); +} + +#[test] +fn test_roll_forward_conviction_grows_then_decays() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let lock_amount = 10000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, lock_amount)); + + // Conviction at t=0 is 0 + let c0 = SubtensorModule::get_conviction(&coldkey, netuid); + assert_eq!(c0, U64F64::saturating_from_num(0)); + + // After some time, conviction should have grown + step_block(1000); + let c1 = SubtensorModule::get_conviction(&coldkey, netuid); + assert!(c1 > U64F64::saturating_from_num(0)); + + // After more time, conviction should be even higher + step_block(1000); + let c2 = SubtensorModule::get_conviction(&coldkey, netuid); + assert!(c2 > c1); + + // After a very long time (many taus), conviction starts to decay back + // because locked_mass has mostly decayed away + let tau = TauBlocks::::get(); + let target = System::block_number() + tau * 10; + System::set_block_number(target); + let c_late = SubtensorModule::get_conviction(&coldkey, netuid); + assert!(c_late < c2); + }); +} + +#[test] +fn test_roll_forward_no_change_when_now_equals_last_update() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(2); + let lock = LockState { + hotkey, + locked_mass: 5000.into(), + conviction: U64F64::saturating_from_num(1234), + last_update: 100, + }; + let rolled = SubtensorModule::roll_forward_lock(lock.clone(), 100); + assert_eq!(rolled.locked_mass, lock.locked_mass); + assert_eq!(rolled.conviction, lock.conviction); + assert_eq!(rolled.last_update, 100); + }); +} + +// ========================================================================= +// GROUP 6: Unstake invariant enforcement +// ========================================================================= + +#[test] +fn test_unstake_allowed_when_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let alpha = get_alpha(&hotkey, &coldkey, netuid); + assert!(alpha > AlphaBalance::ZERO); + + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha, + )); + }); +} + +#[test] +fn test_unstake_allowed_up_to_available() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let lock_amount = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, lock_amount)); + + // Unstake the unlocked half + let alpha = get_alpha(&hotkey, &coldkey, netuid); + let available_alpha: u64 = (alpha.to_u64()) / 2; + // Need to step a block to pass rate limiter + step_block(1); + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + available_alpha.into(), + )); + }); +} + +#[test] +fn test_unstake_blocked_by_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + // Lock the entire amount + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); + + step_block(1); + + let alpha = get_alpha(&hotkey, &coldkey, netuid); + assert_noop!( + SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha, + ), + Error::::CannotUnstakeLock + ); + }); +} + +#[test] +fn test_unstake_allowed_after_decay() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); + + // Advance many taus so lock decays to near-zero (use set_block_number to avoid u16 overflow) + let tau = TauBlocks::::get(); + let target = System::block_number() + tau * 50; + System::set_block_number(target); + // Step one block to clear rate limiter state from on_finalize + step_block(1); + + // Lock should have decayed to near zero + let locked = SubtensorModule::get_current_locked(&coldkey, netuid); + assert!(locked.is_zero()); + + // Should now be able to unstake (subtract 1 to avoid U64F64/AlphaBalance rounding edge) + let alpha = get_alpha(&hotkey, &coldkey, netuid); + if alpha > 1.into() { + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha.saturating_sub(1.into()), + )); + } + }); +} + +#[test] +fn test_unstake_partial_after_partial_decay() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); + + // Advance one tau: lock ~ 37% of original + let tau = TauBlocks::::get(); + let target = System::block_number() + tau; + System::set_block_number(target); + + let locked_now = SubtensorModule::get_current_locked(&coldkey, netuid); + let total_now = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert!(total_now > locked_now); + + // Unstake up to the available amount + let available = total_now - locked_now; + let unstake_amount: u64 = u64::from(available); + if unstake_amount > 0 { + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + unstake_amount.into(), + )); + + // Verify remaining alpha is still >= locked + let remaining = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let locked_after = SubtensorModule::get_current_locked(&coldkey, netuid); + assert!(remaining >= locked_after); + } + }); +} + +// ========================================================================= +// GROUP 7: Move/transfer invariant enforcement +// ========================================================================= + +#[test] +fn test_move_stake_same_coldkey_same_subnet_allowed() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + let netuid = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); + + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey_b); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + // Lock the full amount to hotkey_a + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey_a, total)); + + // Move from hotkey_a to hotkey_b on same subnet — total coldkey alpha unchanged + let alpha = get_alpha(&hotkey_a, &coldkey, netuid); + let move_amount = alpha / 2.into(); + assert_ok!(SubtensorModule::do_move_stake( + RuntimeOrigin::signed(coldkey), + hotkey_a, + hotkey_b, + netuid, + netuid, + move_amount, + )); + }); +} + +#[test] +fn test_move_stake_cross_subnet_blocked_by_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid_a = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let subnet_owner2_ck = U256::from(2001); + let subnet_owner2_hk = U256::from(2002); + let netuid_b = add_dynamic_network(&subnet_owner2_hk, &subnet_owner2_ck); + setup_reserves( + netuid_b, + (100_000_000_000u64 * 1_000_000).into(), + (100_000_000_000u64 * 10_000_000).into(), + ); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid_a); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid_a, &hotkey, total)); + + step_block(1); + + let alpha = get_alpha(&hotkey, &coldkey, netuid_a); + assert_noop!( + SubtensorModule::do_move_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + hotkey, + netuid_a, + netuid_b, + alpha, + ), + Error::::CannotUnstakeLock + ); + }); +} + +#[test] +fn test_transfer_stake_cross_coldkey_blocked_by_lock() { + new_test_ext(1).execute_with(|| { + let coldkey_sender = U256::from(1); + let coldkey_receiver = U256::from(5); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey_sender, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_sender, + netuid, + &hotkey, + total, + )); + + step_block(1); + + let alpha = get_alpha(&hotkey, &coldkey_sender, netuid); + assert_noop!( + SubtensorModule::do_transfer_stake( + RuntimeOrigin::signed(coldkey_sender), + coldkey_receiver, + hotkey, + netuid, + netuid, + alpha, + ), + Error::::CannotUnstakeLock + ); + }); +} + +#[test] +fn test_transfer_stake_cross_coldkey_allowed_partial() { + new_test_ext(1).execute_with(|| { + let coldkey_sender = U256::from(1); + let coldkey_receiver = U256::from(5); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey_sender, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); + let lock_half = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_sender, + netuid, + &hotkey, + lock_half, + )); + + step_block(1); + + // Transfer the unlocked portion + let alpha = get_alpha(&hotkey, &coldkey_sender, netuid); + let transfer_amount = alpha / 4.into(); // well within the unlocked half + assert_ok!(SubtensorModule::do_transfer_stake( + RuntimeOrigin::signed(coldkey_sender), + coldkey_receiver, + hotkey, + netuid, + netuid, + transfer_amount, + )); + }); +} + +// ========================================================================= +// GROUP 8: Multi-subnet locks +// ========================================================================= + +#[test] +fn test_lock_on_multiple_subnets() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + + let netuid_a = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); + + let subnet_owner2_ck = U256::from(2001); + let subnet_owner2_hk = U256::from(2002); + let netuid_b = add_dynamic_network(&subnet_owner2_hk, &subnet_owner2_ck); + setup_reserves( + netuid_b, + (100_000_000_000u64 * 1_000_000).into(), + (100_000_000_000u64 * 10_000_000).into(), + ); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey_b); + SubtensorModule::stake_into_subnet( + &hotkey_b, + &coldkey, + netuid_b, + 100_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + false, + ) + .unwrap(); + + // Lock on subnet A to hotkey_a + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid_a, + &hotkey_a, + 1000u64.into(), + )); + + // Lock on subnet B to hotkey_b (different hotkey is fine — different subnet) + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid_b, + &hotkey_b, + 2000u64.into(), + )); + + let lock_a = Lock::::get(coldkey, netuid_a).unwrap(); + let lock_b = Lock::::get(coldkey, netuid_b).unwrap(); + assert_eq!(lock_a.hotkey, hotkey_a); + assert_eq!(lock_b.hotkey, hotkey_b); + assert_eq!(lock_a.locked_mass, 1000u64.into()); + assert_eq!(lock_b.locked_mass, 2000u64.into()); + }); +} + +#[test] +fn test_unstake_one_subnet_does_not_affect_other() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid_a = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + // Lock on subnet A + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid_a, + &hotkey, + 5000u64.into(), + )); + + // Subnet B — no lock, just stake + let subnet_owner2_ck = U256::from(2001); + let subnet_owner2_hk = U256::from(2002); + let netuid_b = add_dynamic_network(&subnet_owner2_hk, &subnet_owner2_ck); + setup_reserves( + netuid_b, + (100_000_000_000u64 * 1_000_000).into(), + (100_000_000_000u64 * 10_000_000).into(), + ); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey, + netuid_b, + 100_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + false, + ) + .unwrap(); + + step_block(1); + + // Unstake from subnet B — should succeed (no lock there) + let alpha_b = get_alpha(&hotkey, &coldkey, netuid_b); + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid_b, + alpha_b, + )); + + // Lock on subnet A unaffected + let lock_a = Lock::::get(coldkey, netuid_a).unwrap(); + assert_eq!(lock_a.locked_mass, 5000u64.into()); + }); +} + +// ========================================================================= +// GROUP 9: Hotkey conviction and subnet king +// ========================================================================= + +#[test] +fn test_hotkey_conviction_single_locker() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 5000u64.into(), + )); + + // Initially conviction is 0 (just created) + let c = SubtensorModule::hotkey_conviction(&hotkey, netuid); + assert_eq!(c, U64F64::saturating_from_num(0)); + + // After time, conviction grows + step_block(1000); + let c = SubtensorModule::hotkey_conviction(&hotkey, netuid); + assert!(c > U64F64::saturating_from_num(0)); + }); +} + +#[test] +fn test_hotkey_conviction_multiple_lockers() { + new_test_ext(1).execute_with(|| { + let coldkey1 = U256::from(1); + let coldkey2 = U256::from(5); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey1, hotkey, 100_000_000_000); + + // Also give coldkey2 stake on same hotkey + SubtensorModule::add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); + SubtensorModule::create_account_if_non_existent(&coldkey2, &hotkey); + SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey2, + netuid, + 50_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + false, + ) + .unwrap(); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey1, + netuid, + &hotkey, + 3000u64.into(), + )); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey2, + netuid, + &hotkey, + 2000u64.into(), + )); + + step_block(500); + + let total_conviction = SubtensorModule::hotkey_conviction(&hotkey, netuid); + let c1 = SubtensorModule::get_conviction(&coldkey1, netuid); + let c2 = SubtensorModule::get_conviction(&coldkey2, netuid); + + // Total conviction should be approximately sum of individual convictions + let diff = if total_conviction > (c1 + c2) { + total_conviction - (c1 + c2) + } else { + (c1 + c2) - total_conviction + }; + assert!(diff < U64F64::saturating_from_num(1)); + }); +} + +#[test] +fn test_subnet_king_single_hotkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 5000u64.into(), + )); + + step_block(100); + + let king = SubtensorModule::subnet_king(netuid); + assert_eq!(king, Some(hotkey)); + }); +} + +#[test] +fn test_subnet_king_highest_conviction_wins() { + new_test_ext(1).execute_with(|| { + let coldkey1 = U256::from(1); + let coldkey2 = U256::from(5); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + + let netuid = setup_subnet_with_stake(coldkey1, hotkey_a, 100_000_000_000); + + SubtensorModule::add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); + SubtensorModule::create_account_if_non_existent(&coldkey2, &hotkey_b); + SubtensorModule::stake_into_subnet( + &hotkey_b, + &coldkey2, + netuid, + 50_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + false, + ) + .unwrap(); + + // coldkey1 locks more to hotkey_a + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey1, + netuid, + &hotkey_a, + 8000u64.into(), + )); + // coldkey2 locks less to hotkey_b + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey2, + netuid, + &hotkey_b, + 2000u64.into(), + )); + + step_block(500); + + let king = SubtensorModule::subnet_king(netuid); + assert_eq!(king, Some(hotkey_a)); + }); +} + +#[test] +fn test_subnet_king_no_locks() { + new_test_ext(1).execute_with(|| { + let netuid = subtensor_runtime_common::NetUid::from(99); + let king = SubtensorModule::subnet_king(netuid); + assert_eq!(king, None); + }); +} + +// ========================================================================= +// GROUP 10: Lock cleanup +// ========================================================================= + +#[test] +fn test_maybe_cleanup_lock_removes_dust() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + // Lock a small amount + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 50u64.into(), + )); + + // Advance many taus so everything decays well below dust (100) + let tau = TauBlocks::::get(); + let target = System::block_number() + tau * 50; + System::set_block_number(target); + + SubtensorModule::maybe_cleanup_lock(&coldkey, netuid); + + assert!(Lock::::get(coldkey, netuid).is_none()); + }); +} + +#[test] +fn test_maybe_cleanup_lock_preserves_active_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 100_000u64.into(), + )); + + step_block(100); + + SubtensorModule::maybe_cleanup_lock(&coldkey, netuid); + + let lock = Lock::::get(coldkey, netuid); + assert!(lock.is_some()); + // last_update should be rolled forward to current block + assert_eq!( + lock.unwrap().last_update, + SubtensorModule::get_current_block_as_u64() + ); + }); +} + +#[test] +fn test_maybe_cleanup_lock_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let netuid = subtensor_runtime_common::NetUid::from(1); + // Should be a no-op, no panic + SubtensorModule::maybe_cleanup_lock(&coldkey, netuid); + assert!(Lock::::get(coldkey, netuid).is_none()); + }); +} + +// ========================================================================= +// GROUP 11: Coldkey swap interaction +// ========================================================================= + +#[test] +fn test_coldkey_swap_orphans_lock() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(10); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(old_coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &old_coldkey, + netuid, + &hotkey, + 5000u64.into(), + )); + + // Perform coldkey swap + assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey)); + + // Lock remains on old coldkey (orphaned) + assert!(Lock::::get(old_coldkey, netuid).is_some()); + // New coldkey has no lock + assert!(Lock::::get(new_coldkey, netuid).is_none()); + }); +} + +#[test] +fn test_coldkey_swap_lock_no_longer_blocks_unstake() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(10); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(old_coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&old_coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &old_coldkey, + netuid, + &hotkey, + total, + )); + + // Swap coldkey + assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey)); + + step_block(1); + + // New coldkey should be able to unstake freely — no lock on new_coldkey + let alpha = get_alpha(&hotkey, &new_coldkey, netuid); + if alpha > AlphaBalance::ZERO { + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(new_coldkey), + hotkey, + netuid, + alpha, + )); + } + }); +} + +// ========================================================================= +// GROUP 12: Hotkey swap interaction +// ========================================================================= + +#[test] +fn test_hotkey_swap_lock_becomes_stale() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let old_hotkey = U256::from(2); + let new_hotkey = U256::from(20); + let netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &old_hotkey, + 5000u64.into(), + )); + + // Perform hotkey swap + let mut weight = Weight::zero(); + assert_ok!(SubtensorModule::perform_hotkey_swap_on_all_subnets( + &old_hotkey, + &new_hotkey, + &coldkey, + &mut weight, + false + )); + + // Lock still references old_hotkey + let lock = Lock::::get(coldkey, netuid).unwrap(); + assert_eq!(lock.hotkey, old_hotkey); + + // Trying to top up to new_hotkey fails with mismatch + assert_noop!( + SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &new_hotkey, + 100u64.into(), + ), + Error::::LockHotkeyMismatch + ); + }); +} + +#[test] +fn test_hotkey_swap_conviction_not_migrated() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let old_hotkey = U256::from(2); + let new_hotkey = U256::from(20); + let netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &old_hotkey, + 5000u64.into(), + )); + + step_block(500); + let conviction_before = SubtensorModule::hotkey_conviction(&old_hotkey, netuid); + assert!(conviction_before > U64F64::saturating_from_num(0)); + + // Swap hotkey + let mut weight = Weight::zero(); + assert_ok!(SubtensorModule::perform_hotkey_swap_on_all_subnets( + &old_hotkey, + &new_hotkey, + &coldkey, + &mut weight, + false + )); + + // New hotkey has no conviction + let conviction_new = SubtensorModule::hotkey_conviction(&new_hotkey, netuid); + assert_eq!(conviction_new, U64F64::saturating_from_num(0)); + + // Old hotkey still has conviction (lock still points there) + let conviction_old = SubtensorModule::hotkey_conviction(&old_hotkey, netuid); + assert!(conviction_old > U64F64::saturating_from_num(0)); + }); +} + +// ========================================================================= +// GROUP 13: Lock extrinsic via dispatch +// ========================================================================= + +#[test] +fn test_lock_stake_extrinsic() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let lock_amount: u64 = 5000; + assert_ok!(SubtensorModule::lock_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + lock_amount.into(), + )); + + let lock = Lock::::get(coldkey, netuid).expect("Lock should exist"); + assert_eq!(lock.hotkey, hotkey); + assert_eq!(lock.locked_mass, lock_amount.into()); + assert_eq!(lock.conviction, U64F64::saturating_from_num(0)); + }); +} + +// ========================================================================= +// GROUP 14: Recycle/burn alpha bypass (BUG: bypasses lock) +// ========================================================================= + +#[test] +fn test_recycle_alpha_bypasses_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); + + step_block(1); + + // Unstake should be blocked + let alpha = get_alpha(&hotkey, &coldkey, netuid); + assert_noop!( + SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha, + ), + Error::::CannotUnstakeLock + ); + + // BUG: recycle_alpha bypasses lock — it succeeds despite full lock + let recycle_amount = alpha / 2.into(); + assert_ok!(SubtensorModule::do_recycle_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + recycle_amount, + netuid, + )); + + // Alpha is now below locked_mass — lock invariant violated + let total_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let locked = SubtensorModule::get_current_locked(&coldkey, netuid); + assert!(total_after < locked); + }); +} + +#[test] +fn test_burn_alpha_bypasses_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); + + step_block(1); + + // BUG: burn_alpha bypasses lock — it succeeds despite full lock + let alpha = get_alpha(&hotkey, &coldkey, netuid); + let burn_amount = alpha / 2.into(); + assert_ok!(SubtensorModule::do_burn_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + burn_amount, + netuid, + )); + + // Alpha is now below locked_mass — lock invariant violated + let total_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let locked = SubtensorModule::get_current_locked(&coldkey, netuid); + assert!(total_after < locked); + }); +} + +// ========================================================================= +// GROUP 15: Subnet dissolution +// ========================================================================= + +#[test] +fn test_subnet_dissolution_orphans_locks() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 5000u64.into(), + )); + assert!(Lock::::get(coldkey, netuid).is_some()); + + // Dissolve the subnet + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + + // All Alpha entries are gone + assert_eq!( + SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid), + AlphaBalance::ZERO + ); + + // BUG: Lock entry is orphaned — still present despite no alpha + assert!(Lock::::get(coldkey, netuid).is_some()); + }); +} + +#[test] +fn test_subnet_dissolution_and_netuid_reuse() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_old = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey_old, 100_000_000_000); + + // Lock on the old subnet + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey_old, + 5000u64.into(), + )); + + // Dissolve old subnet + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + + // The stale lock from old subnet remains + let stale_lock = Lock::::get(coldkey, netuid); + assert!(stale_lock.is_some()); + assert_eq!(stale_lock.unwrap().hotkey, hotkey_old); + }); +} + +// ========================================================================= +// GROUP 16: Clear small nomination bypass +// ========================================================================= + +#[test] +fn test_clear_small_nomination_bypasses_lock() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(100); + let owner_hotkey = U256::from(101); + let netuid = setup_subnet_with_stake(owner_coldkey, owner_hotkey, 100_000_000_000); + + // Set up a nominator (different coldkey, does NOT own the hotkey) + let nominator = U256::from(200); + SubtensorModule::add_balance_to_coldkey_account(&nominator, 100_000_000_000u64.into()); + SubtensorModule::create_account_if_non_existent(&nominator, &owner_hotkey); + SubtensorModule::stake_into_subnet( + &owner_hotkey, + &nominator, + netuid, + 50_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + false, + ) + .unwrap(); + + let nominator_alpha = get_alpha(&owner_hotkey, &nominator, netuid); + assert!(nominator_alpha > AlphaBalance::ZERO); + + // Nominator locks their full stake + let nominator_total = SubtensorModule::total_coldkey_alpha_on_subnet(&nominator, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &nominator, + netuid, + &owner_hotkey, + nominator_total, + )); + + // Set a high nominator min stake so the current stake is "small" + SubtensorModule::set_nominator_min_required_stake(u64::MAX); + + // BUG: clear_small_nomination bypasses the lock and removes alpha + SubtensorModule::clear_small_nomination_if_required(&owner_hotkey, &nominator, netuid); + + // Nominator alpha has been removed despite lock + let nominator_alpha_after = get_alpha(&owner_hotkey, &nominator, netuid); + assert_eq!(nominator_alpha_after, AlphaBalance::ZERO); + + // Lock entry still exists, now orphaned + assert!(Lock::::get(nominator, netuid).is_some()); + }); +} + +// ========================================================================= +// GROUP 17: Emission interaction +// ========================================================================= + +#[test] +fn test_emissions_do_not_break_lock_invariant() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total_alpha_before = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total_alpha_before)); + + // Simulate emission: directly increase alpha for the hotkey on subnet + // This increases the pool value for all share holders (including our coldkey) + let emission_amount: AlphaBalance = 10_000_000u64.into(); + SubtensorModule::increase_stake_for_hotkey_on_subnet(&hotkey, netuid, emission_amount); + + // After emission, total alpha should increase by emission_amount + let total_alpha_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_eq!(total_alpha_after, total_alpha_before + emission_amount); + + // Lock invariant still holds: total_alpha >= locked_mass + let locked = SubtensorModule::get_current_locked(&coldkey, netuid); + assert!(total_alpha_after >= locked); + + // Available becomes emission_amount + let available = SubtensorModule::available_to_unstake(&coldkey, netuid); + assert_eq!(available, emission_amount); + }); +} + +// ========================================================================= +// GROUP 18: Neuron replacement +// ========================================================================= + +#[test] +fn test_neuron_replacement_does_not_affect_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + // Register the hotkey as a neuron + register_ok_neuron(netuid, hotkey, coldkey, 0); + + let lock_amount = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, lock_amount)); + + let total_before = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let locked_before = SubtensorModule::get_current_locked(&coldkey, netuid); + + // Replace the neuron with a different hotkey + let new_hotkey = U256::from(99); + let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).unwrap(); + SubtensorModule::replace_neuron( + netuid, + uid, + &new_hotkey, + SubtensorModule::get_current_block_as_u64(), + ); + + // Alpha and lock should be unaffected by neuron replacement + let total_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let locked_after = SubtensorModule::get_current_locked(&coldkey, netuid); + + assert_eq!(total_after, total_before); + assert_eq!(locked_after, locked_before); + + // Lock still references original hotkey + let lock = Lock::::get(coldkey, netuid).unwrap(); + assert_eq!(lock.hotkey, hotkey); + }); +} diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index 7e0c477c56..6deff52b2e 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -11,6 +11,7 @@ mod epoch; mod epoch_logs; mod evm; mod leasing; +mod locks; mod math; mod mechanism; mod migration; From 463ebcff45456bfd2b70a65fe03e0321ddee40e5 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 17 Apr 2026 14:50:46 -0400 Subject: [PATCH 02/13] Fix subtensor benchmarks --- pallets/subtensor/src/weights.rs | 140 +++++++++++++++---------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/pallets/subtensor/src/weights.rs b/pallets/subtensor/src/weights.rs index d6c63175f0..18900ad336 100644 --- a/pallets/subtensor/src/weights.rs +++ b/pallets/subtensor/src/weights.rs @@ -192,8 +192,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `13600` // Minimum execution time: 348_026_000 picoseconds. Weight::from_parts(354_034_000, 13600) - .saturating_add(T::DbWeight::get().reads(46_u64)) - .saturating_add(T::DbWeight::get().writes(38_u64)) + .saturating_add(T::DbWeight::get().reads(47_u64)) + .saturating_add(T::DbWeight::get().writes(39_u64)) } /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) /// Proof: `SubtensorModule::CommitRevealWeightsEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -296,10 +296,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `2307` // Estimated: `8556` - // Minimum execution time: 338_691_000 picoseconds. - Weight::from_parts(346_814_000, 8556) - .saturating_add(T::DbWeight::get().reads(27_u64)) - .saturating_add(T::DbWeight::get().writes(15_u64)) + // Minimum execution time: 399_660_000 picoseconds. + Weight::from_parts(399_660_000, 8556) + .saturating_add(T::DbWeight::get().reads(28_u64)) + .saturating_add(T::DbWeight::get().writes(16_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -427,10 +427,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1639` // Estimated: `13600` - // Minimum execution time: 341_145_000 picoseconds. - Weight::from_parts(345_863_000, 13600) - .saturating_add(T::DbWeight::get().reads(46_u64)) - .saturating_add(T::DbWeight::get().writes(38_u64)) + // Minimum execution time: 385_433_000 picoseconds. + Weight::from_parts(385_433_000, 13600) + .saturating_add(T::DbWeight::get().reads(47_u64)) + .saturating_add(T::DbWeight::get().writes(39_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -611,8 +611,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `10091` // Minimum execution time: 289_917_000 picoseconds. Weight::from_parts(293_954_000, 10091) - .saturating_add(T::DbWeight::get().reads(45_u64)) - .saturating_add(T::DbWeight::get().writes(49_u64)) + .saturating_add(T::DbWeight::get().reads(41_u64)) + .saturating_add(T::DbWeight::get().writes(46_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1032,10 +1032,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `2307` // Estimated: `8556` - // Minimum execution time: 376_539_000 picoseconds. - Weight::from_parts(383_750_000, 8556) - .saturating_add(T::DbWeight::get().reads(27_u64)) - .saturating_add(T::DbWeight::get().writes(15_u64)) + // Minimum execution time: 444_193_000 picoseconds. + Weight::from_parts(444_193_000, 8556) + .saturating_add(T::DbWeight::get().reads(28_u64)) + .saturating_add(T::DbWeight::get().writes(16_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1130,8 +1130,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `10626` // Minimum execution time: 387_646_000 picoseconds. Weight::from_parts(403_169_000, 10626) - .saturating_add(T::DbWeight::get().reads(30_u64)) - .saturating_add(T::DbWeight::get().writes(13_u64)) + .saturating_add(T::DbWeight::get().reads(31_u64)) + .saturating_add(T::DbWeight::get().writes(14_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1191,8 +1191,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `8556` // Minimum execution time: 461_377_000 picoseconds. Weight::from_parts(477_951_000, 8556) - .saturating_add(T::DbWeight::get().reads(40_u64)) - .saturating_add(T::DbWeight::get().writes(22_u64)) + .saturating_add(T::DbWeight::get().reads(42_u64)) + .saturating_add(T::DbWeight::get().writes(23_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1291,8 +1291,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `8556` // Minimum execution time: 402_808_000 picoseconds. Weight::from_parts(420_035_000, 8556) - .saturating_add(T::DbWeight::get().reads(40_u64)) - .saturating_add(T::DbWeight::get().writes(22_u64)) + .saturating_add(T::DbWeight::get().reads(42_u64)) + .saturating_add(T::DbWeight::get().writes(23_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1526,8 +1526,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `9975` // Minimum execution time: 279_983_000 picoseconds. Weight::from_parts(284_690_000, 9975) - .saturating_add(T::DbWeight::get().reads(44_u64)) - .saturating_add(T::DbWeight::get().writes(48_u64)) + .saturating_add(T::DbWeight::get().reads(40_u64)) + .saturating_add(T::DbWeight::get().writes(45_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1640,7 +1640,7 @@ impl WeightInfo for SubstrateWeight { // Estimated: `28766` // Minimum execution time: 1_148_985_000 picoseconds. Weight::from_parts(1_154_584_000, 28766) - .saturating_add(T::DbWeight::get().reads(159_u64)) + .saturating_add(T::DbWeight::get().reads(161_u64)) .saturating_add(T::DbWeight::get().writes(95_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:1) @@ -1738,8 +1738,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `10787` // Minimum execution time: 414_015_000 picoseconds. Weight::from_parts(427_445_000, 10787) - .saturating_add(T::DbWeight::get().reads(44_u64)) - .saturating_add(T::DbWeight::get().writes(24_u64)) + .saturating_add(T::DbWeight::get().reads(47_u64)) + .saturating_add(T::DbWeight::get().writes(26_u64)) } /// Storage: `SubtensorModule::Alpha` (r:1 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1797,8 +1797,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `10626` // Minimum execution time: 412_223_000 picoseconds. Weight::from_parts(430_190_000, 10626) - .saturating_add(T::DbWeight::get().reads(30_u64)) - .saturating_add(T::DbWeight::get().writes(13_u64)) + .saturating_add(T::DbWeight::get().reads(31_u64)) + .saturating_add(T::DbWeight::get().writes(14_u64)) } /// Storage: `Crowdloan::CurrentCrowdloanId` (r:1 w:0) /// Proof: `Crowdloan::CurrentCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -1947,9 +1947,9 @@ impl WeightInfo for SubstrateWeight { Weight::from_parts(286_320_370, 10400) // Standard Error: 33_372 .saturating_add(Weight::from_parts(47_145_967, 0).saturating_mul(k.into())) - .saturating_add(T::DbWeight::get().reads(54_u64)) + .saturating_add(T::DbWeight::get().reads(50_u64)) .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(k.into()))) - .saturating_add(T::DbWeight::get().writes(54_u64)) + .saturating_add(T::DbWeight::get().writes(51_u64)) .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(k.into()))) .saturating_add(Weight::from_parts(0, 2579).saturating_mul(k.into())) } @@ -2177,10 +2177,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `2365` // Estimated: `8556` - // Minimum execution time: 471_702_000 picoseconds. - Weight::from_parts(484_481_000, 8556) - .saturating_add(T::DbWeight::get().reads(30_u64)) - .saturating_add(T::DbWeight::get().writes(16_u64)) + // Minimum execution time: 534_433_000 picoseconds. + Weight::from_parts(534_433_000, 8556) + .saturating_add(T::DbWeight::get().reads(31_u64)) + .saturating_add(T::DbWeight::get().writes(17_u64)) } /// Storage: `SubtensorModule::PendingChildKeyCooldown` (r:0 w:1) /// Proof: `SubtensorModule::PendingChildKeyCooldown` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) @@ -2310,8 +2310,8 @@ impl WeightInfo for () { // Estimated: `13600` // Minimum execution time: 348_026_000 picoseconds. Weight::from_parts(354_034_000, 13600) - .saturating_add(RocksDbWeight::get().reads(46_u64)) - .saturating_add(RocksDbWeight::get().writes(38_u64)) + .saturating_add(RocksDbWeight::get().reads(47_u64)) + .saturating_add(RocksDbWeight::get().writes(39_u64)) } /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) /// Proof: `SubtensorModule::CommitRevealWeightsEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -2414,10 +2414,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `2307` // Estimated: `8556` - // Minimum execution time: 338_691_000 picoseconds. - Weight::from_parts(346_814_000, 8556) - .saturating_add(RocksDbWeight::get().reads(27_u64)) - .saturating_add(RocksDbWeight::get().writes(15_u64)) + // Minimum execution time: 399_660_000 picoseconds. + Weight::from_parts(399_660_000, 8556) + .saturating_add(RocksDbWeight::get().reads(28_u64)) + .saturating_add(RocksDbWeight::get().writes(16_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -2545,10 +2545,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1639` // Estimated: `13600` - // Minimum execution time: 341_145_000 picoseconds. - Weight::from_parts(345_863_000, 13600) - .saturating_add(RocksDbWeight::get().reads(46_u64)) - .saturating_add(RocksDbWeight::get().writes(38_u64)) + // Minimum execution time: 385_433_000 picoseconds. + Weight::from_parts(385_433_000, 13600) + .saturating_add(RocksDbWeight::get().reads(47_u64)) + .saturating_add(RocksDbWeight::get().writes(39_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -2729,8 +2729,8 @@ impl WeightInfo for () { // Estimated: `10091` // Minimum execution time: 289_917_000 picoseconds. Weight::from_parts(293_954_000, 10091) - .saturating_add(RocksDbWeight::get().reads(45_u64)) - .saturating_add(RocksDbWeight::get().writes(49_u64)) + .saturating_add(RocksDbWeight::get().reads(41_u64)) + .saturating_add(RocksDbWeight::get().writes(46_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3150,10 +3150,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `2307` // Estimated: `8556` - // Minimum execution time: 376_539_000 picoseconds. - Weight::from_parts(383_750_000, 8556) - .saturating_add(RocksDbWeight::get().reads(27_u64)) - .saturating_add(RocksDbWeight::get().writes(15_u64)) + // Minimum execution time: 444_193_000 picoseconds. + Weight::from_parts(444_193_000, 8556) + .saturating_add(RocksDbWeight::get().reads(28_u64)) + .saturating_add(RocksDbWeight::get().writes(16_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3248,8 +3248,8 @@ impl WeightInfo for () { // Estimated: `10626` // Minimum execution time: 387_646_000 picoseconds. Weight::from_parts(403_169_000, 10626) - .saturating_add(RocksDbWeight::get().reads(30_u64)) - .saturating_add(RocksDbWeight::get().writes(13_u64)) + .saturating_add(RocksDbWeight::get().reads(31_u64)) + .saturating_add(RocksDbWeight::get().writes(14_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3309,8 +3309,8 @@ impl WeightInfo for () { // Estimated: `8556` // Minimum execution time: 461_377_000 picoseconds. Weight::from_parts(477_951_000, 8556) - .saturating_add(RocksDbWeight::get().reads(40_u64)) - .saturating_add(RocksDbWeight::get().writes(22_u64)) + .saturating_add(RocksDbWeight::get().reads(42_u64)) + .saturating_add(RocksDbWeight::get().writes(23_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3409,8 +3409,8 @@ impl WeightInfo for () { // Estimated: `8556` // Minimum execution time: 402_808_000 picoseconds. Weight::from_parts(420_035_000, 8556) - .saturating_add(RocksDbWeight::get().reads(40_u64)) - .saturating_add(RocksDbWeight::get().writes(22_u64)) + .saturating_add(RocksDbWeight::get().reads(42_u64)) + .saturating_add(RocksDbWeight::get().writes(23_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3644,8 +3644,8 @@ impl WeightInfo for () { // Estimated: `9975` // Minimum execution time: 279_983_000 picoseconds. Weight::from_parts(284_690_000, 9975) - .saturating_add(RocksDbWeight::get().reads(44_u64)) - .saturating_add(RocksDbWeight::get().writes(48_u64)) + .saturating_add(RocksDbWeight::get().reads(40_u64)) + .saturating_add(RocksDbWeight::get().writes(45_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3758,7 +3758,7 @@ impl WeightInfo for () { // Estimated: `28766` // Minimum execution time: 1_148_985_000 picoseconds. Weight::from_parts(1_154_584_000, 28766) - .saturating_add(RocksDbWeight::get().reads(159_u64)) + .saturating_add(RocksDbWeight::get().reads(161_u64)) .saturating_add(RocksDbWeight::get().writes(95_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:1) @@ -3856,8 +3856,8 @@ impl WeightInfo for () { // Estimated: `10787` // Minimum execution time: 414_015_000 picoseconds. Weight::from_parts(427_445_000, 10787) - .saturating_add(RocksDbWeight::get().reads(44_u64)) - .saturating_add(RocksDbWeight::get().writes(24_u64)) + .saturating_add(RocksDbWeight::get().reads(47_u64)) + .saturating_add(RocksDbWeight::get().writes(26_u64)) } /// Storage: `SubtensorModule::Alpha` (r:1 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3915,8 +3915,8 @@ impl WeightInfo for () { // Estimated: `10626` // Minimum execution time: 412_223_000 picoseconds. Weight::from_parts(430_190_000, 10626) - .saturating_add(RocksDbWeight::get().reads(30_u64)) - .saturating_add(RocksDbWeight::get().writes(13_u64)) + .saturating_add(RocksDbWeight::get().reads(31_u64)) + .saturating_add(RocksDbWeight::get().writes(14_u64)) } /// Storage: `Crowdloan::CurrentCrowdloanId` (r:1 w:0) /// Proof: `Crowdloan::CurrentCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -4065,9 +4065,9 @@ impl WeightInfo for () { Weight::from_parts(286_320_370, 10400) // Standard Error: 33_372 .saturating_add(Weight::from_parts(47_145_967, 0).saturating_mul(k.into())) - .saturating_add(RocksDbWeight::get().reads(54_u64)) + .saturating_add(RocksDbWeight::get().reads(50_u64)) .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(k.into()))) - .saturating_add(RocksDbWeight::get().writes(54_u64)) + .saturating_add(RocksDbWeight::get().writes(51_u64)) .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(k.into()))) .saturating_add(Weight::from_parts(0, 2579).saturating_mul(k.into())) } @@ -4295,10 +4295,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `2365` // Estimated: `8556` - // Minimum execution time: 471_702_000 picoseconds. - Weight::from_parts(484_481_000, 8556) - .saturating_add(RocksDbWeight::get().reads(30_u64)) - .saturating_add(RocksDbWeight::get().writes(16_u64)) + // Minimum execution time: 534_433_000 picoseconds. + Weight::from_parts(534_433_000, 8556) + .saturating_add(RocksDbWeight::get().reads(31_u64)) + .saturating_add(RocksDbWeight::get().writes(17_u64)) } /// Storage: `SubtensorModule::PendingChildKeyCooldown` (r:0 w:1) /// Proof: `SubtensorModule::PendingChildKeyCooldown` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) From 1321745c522a8d7654d3751d8b49e962d271860a Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 17 Apr 2026 15:03:46 -0400 Subject: [PATCH 03/13] Revert "Fix subtensor benchmarks" This reverts commit 463ebcff45456bfd2b70a65fe03e0321ddee40e5. --- pallets/subtensor/src/weights.rs | 140 +++++++++++++++---------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/pallets/subtensor/src/weights.rs b/pallets/subtensor/src/weights.rs index 18900ad336..d6c63175f0 100644 --- a/pallets/subtensor/src/weights.rs +++ b/pallets/subtensor/src/weights.rs @@ -192,8 +192,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `13600` // Minimum execution time: 348_026_000 picoseconds. Weight::from_parts(354_034_000, 13600) - .saturating_add(T::DbWeight::get().reads(47_u64)) - .saturating_add(T::DbWeight::get().writes(39_u64)) + .saturating_add(T::DbWeight::get().reads(46_u64)) + .saturating_add(T::DbWeight::get().writes(38_u64)) } /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) /// Proof: `SubtensorModule::CommitRevealWeightsEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -296,10 +296,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `2307` // Estimated: `8556` - // Minimum execution time: 399_660_000 picoseconds. - Weight::from_parts(399_660_000, 8556) - .saturating_add(T::DbWeight::get().reads(28_u64)) - .saturating_add(T::DbWeight::get().writes(16_u64)) + // Minimum execution time: 338_691_000 picoseconds. + Weight::from_parts(346_814_000, 8556) + .saturating_add(T::DbWeight::get().reads(27_u64)) + .saturating_add(T::DbWeight::get().writes(15_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -427,10 +427,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1639` // Estimated: `13600` - // Minimum execution time: 385_433_000 picoseconds. - Weight::from_parts(385_433_000, 13600) - .saturating_add(T::DbWeight::get().reads(47_u64)) - .saturating_add(T::DbWeight::get().writes(39_u64)) + // Minimum execution time: 341_145_000 picoseconds. + Weight::from_parts(345_863_000, 13600) + .saturating_add(T::DbWeight::get().reads(46_u64)) + .saturating_add(T::DbWeight::get().writes(38_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -611,8 +611,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `10091` // Minimum execution time: 289_917_000 picoseconds. Weight::from_parts(293_954_000, 10091) - .saturating_add(T::DbWeight::get().reads(41_u64)) - .saturating_add(T::DbWeight::get().writes(46_u64)) + .saturating_add(T::DbWeight::get().reads(45_u64)) + .saturating_add(T::DbWeight::get().writes(49_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1032,10 +1032,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `2307` // Estimated: `8556` - // Minimum execution time: 444_193_000 picoseconds. - Weight::from_parts(444_193_000, 8556) - .saturating_add(T::DbWeight::get().reads(28_u64)) - .saturating_add(T::DbWeight::get().writes(16_u64)) + // Minimum execution time: 376_539_000 picoseconds. + Weight::from_parts(383_750_000, 8556) + .saturating_add(T::DbWeight::get().reads(27_u64)) + .saturating_add(T::DbWeight::get().writes(15_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1130,8 +1130,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `10626` // Minimum execution time: 387_646_000 picoseconds. Weight::from_parts(403_169_000, 10626) - .saturating_add(T::DbWeight::get().reads(31_u64)) - .saturating_add(T::DbWeight::get().writes(14_u64)) + .saturating_add(T::DbWeight::get().reads(30_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1191,8 +1191,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `8556` // Minimum execution time: 461_377_000 picoseconds. Weight::from_parts(477_951_000, 8556) - .saturating_add(T::DbWeight::get().reads(42_u64)) - .saturating_add(T::DbWeight::get().writes(23_u64)) + .saturating_add(T::DbWeight::get().reads(40_u64)) + .saturating_add(T::DbWeight::get().writes(22_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1291,8 +1291,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `8556` // Minimum execution time: 402_808_000 picoseconds. Weight::from_parts(420_035_000, 8556) - .saturating_add(T::DbWeight::get().reads(42_u64)) - .saturating_add(T::DbWeight::get().writes(23_u64)) + .saturating_add(T::DbWeight::get().reads(40_u64)) + .saturating_add(T::DbWeight::get().writes(22_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1526,8 +1526,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `9975` // Minimum execution time: 279_983_000 picoseconds. Weight::from_parts(284_690_000, 9975) - .saturating_add(T::DbWeight::get().reads(40_u64)) - .saturating_add(T::DbWeight::get().writes(45_u64)) + .saturating_add(T::DbWeight::get().reads(44_u64)) + .saturating_add(T::DbWeight::get().writes(48_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1640,7 +1640,7 @@ impl WeightInfo for SubstrateWeight { // Estimated: `28766` // Minimum execution time: 1_148_985_000 picoseconds. Weight::from_parts(1_154_584_000, 28766) - .saturating_add(T::DbWeight::get().reads(161_u64)) + .saturating_add(T::DbWeight::get().reads(159_u64)) .saturating_add(T::DbWeight::get().writes(95_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:1) @@ -1738,8 +1738,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `10787` // Minimum execution time: 414_015_000 picoseconds. Weight::from_parts(427_445_000, 10787) - .saturating_add(T::DbWeight::get().reads(47_u64)) - .saturating_add(T::DbWeight::get().writes(26_u64)) + .saturating_add(T::DbWeight::get().reads(44_u64)) + .saturating_add(T::DbWeight::get().writes(24_u64)) } /// Storage: `SubtensorModule::Alpha` (r:1 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1797,8 +1797,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `10626` // Minimum execution time: 412_223_000 picoseconds. Weight::from_parts(430_190_000, 10626) - .saturating_add(T::DbWeight::get().reads(31_u64)) - .saturating_add(T::DbWeight::get().writes(14_u64)) + .saturating_add(T::DbWeight::get().reads(30_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)) } /// Storage: `Crowdloan::CurrentCrowdloanId` (r:1 w:0) /// Proof: `Crowdloan::CurrentCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -1947,9 +1947,9 @@ impl WeightInfo for SubstrateWeight { Weight::from_parts(286_320_370, 10400) // Standard Error: 33_372 .saturating_add(Weight::from_parts(47_145_967, 0).saturating_mul(k.into())) - .saturating_add(T::DbWeight::get().reads(50_u64)) + .saturating_add(T::DbWeight::get().reads(54_u64)) .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(k.into()))) - .saturating_add(T::DbWeight::get().writes(51_u64)) + .saturating_add(T::DbWeight::get().writes(54_u64)) .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(k.into()))) .saturating_add(Weight::from_parts(0, 2579).saturating_mul(k.into())) } @@ -2177,10 +2177,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `2365` // Estimated: `8556` - // Minimum execution time: 534_433_000 picoseconds. - Weight::from_parts(534_433_000, 8556) - .saturating_add(T::DbWeight::get().reads(31_u64)) - .saturating_add(T::DbWeight::get().writes(17_u64)) + // Minimum execution time: 471_702_000 picoseconds. + Weight::from_parts(484_481_000, 8556) + .saturating_add(T::DbWeight::get().reads(30_u64)) + .saturating_add(T::DbWeight::get().writes(16_u64)) } /// Storage: `SubtensorModule::PendingChildKeyCooldown` (r:0 w:1) /// Proof: `SubtensorModule::PendingChildKeyCooldown` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) @@ -2310,8 +2310,8 @@ impl WeightInfo for () { // Estimated: `13600` // Minimum execution time: 348_026_000 picoseconds. Weight::from_parts(354_034_000, 13600) - .saturating_add(RocksDbWeight::get().reads(47_u64)) - .saturating_add(RocksDbWeight::get().writes(39_u64)) + .saturating_add(RocksDbWeight::get().reads(46_u64)) + .saturating_add(RocksDbWeight::get().writes(38_u64)) } /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) /// Proof: `SubtensorModule::CommitRevealWeightsEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -2414,10 +2414,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `2307` // Estimated: `8556` - // Minimum execution time: 399_660_000 picoseconds. - Weight::from_parts(399_660_000, 8556) - .saturating_add(RocksDbWeight::get().reads(28_u64)) - .saturating_add(RocksDbWeight::get().writes(16_u64)) + // Minimum execution time: 338_691_000 picoseconds. + Weight::from_parts(346_814_000, 8556) + .saturating_add(RocksDbWeight::get().reads(27_u64)) + .saturating_add(RocksDbWeight::get().writes(15_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -2545,10 +2545,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1639` // Estimated: `13600` - // Minimum execution time: 385_433_000 picoseconds. - Weight::from_parts(385_433_000, 13600) - .saturating_add(RocksDbWeight::get().reads(47_u64)) - .saturating_add(RocksDbWeight::get().writes(39_u64)) + // Minimum execution time: 341_145_000 picoseconds. + Weight::from_parts(345_863_000, 13600) + .saturating_add(RocksDbWeight::get().reads(46_u64)) + .saturating_add(RocksDbWeight::get().writes(38_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -2729,8 +2729,8 @@ impl WeightInfo for () { // Estimated: `10091` // Minimum execution time: 289_917_000 picoseconds. Weight::from_parts(293_954_000, 10091) - .saturating_add(RocksDbWeight::get().reads(41_u64)) - .saturating_add(RocksDbWeight::get().writes(46_u64)) + .saturating_add(RocksDbWeight::get().reads(45_u64)) + .saturating_add(RocksDbWeight::get().writes(49_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3150,10 +3150,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `2307` // Estimated: `8556` - // Minimum execution time: 444_193_000 picoseconds. - Weight::from_parts(444_193_000, 8556) - .saturating_add(RocksDbWeight::get().reads(28_u64)) - .saturating_add(RocksDbWeight::get().writes(16_u64)) + // Minimum execution time: 376_539_000 picoseconds. + Weight::from_parts(383_750_000, 8556) + .saturating_add(RocksDbWeight::get().reads(27_u64)) + .saturating_add(RocksDbWeight::get().writes(15_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3248,8 +3248,8 @@ impl WeightInfo for () { // Estimated: `10626` // Minimum execution time: 387_646_000 picoseconds. Weight::from_parts(403_169_000, 10626) - .saturating_add(RocksDbWeight::get().reads(31_u64)) - .saturating_add(RocksDbWeight::get().writes(14_u64)) + .saturating_add(RocksDbWeight::get().reads(30_u64)) + .saturating_add(RocksDbWeight::get().writes(13_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3309,8 +3309,8 @@ impl WeightInfo for () { // Estimated: `8556` // Minimum execution time: 461_377_000 picoseconds. Weight::from_parts(477_951_000, 8556) - .saturating_add(RocksDbWeight::get().reads(42_u64)) - .saturating_add(RocksDbWeight::get().writes(23_u64)) + .saturating_add(RocksDbWeight::get().reads(40_u64)) + .saturating_add(RocksDbWeight::get().writes(22_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3409,8 +3409,8 @@ impl WeightInfo for () { // Estimated: `8556` // Minimum execution time: 402_808_000 picoseconds. Weight::from_parts(420_035_000, 8556) - .saturating_add(RocksDbWeight::get().reads(42_u64)) - .saturating_add(RocksDbWeight::get().writes(23_u64)) + .saturating_add(RocksDbWeight::get().reads(40_u64)) + .saturating_add(RocksDbWeight::get().writes(22_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3644,8 +3644,8 @@ impl WeightInfo for () { // Estimated: `9975` // Minimum execution time: 279_983_000 picoseconds. Weight::from_parts(284_690_000, 9975) - .saturating_add(RocksDbWeight::get().reads(40_u64)) - .saturating_add(RocksDbWeight::get().writes(45_u64)) + .saturating_add(RocksDbWeight::get().reads(44_u64)) + .saturating_add(RocksDbWeight::get().writes(48_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3758,7 +3758,7 @@ impl WeightInfo for () { // Estimated: `28766` // Minimum execution time: 1_148_985_000 picoseconds. Weight::from_parts(1_154_584_000, 28766) - .saturating_add(RocksDbWeight::get().reads(161_u64)) + .saturating_add(RocksDbWeight::get().reads(159_u64)) .saturating_add(RocksDbWeight::get().writes(95_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:1) @@ -3856,8 +3856,8 @@ impl WeightInfo for () { // Estimated: `10787` // Minimum execution time: 414_015_000 picoseconds. Weight::from_parts(427_445_000, 10787) - .saturating_add(RocksDbWeight::get().reads(47_u64)) - .saturating_add(RocksDbWeight::get().writes(26_u64)) + .saturating_add(RocksDbWeight::get().reads(44_u64)) + .saturating_add(RocksDbWeight::get().writes(24_u64)) } /// Storage: `SubtensorModule::Alpha` (r:1 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3915,8 +3915,8 @@ impl WeightInfo for () { // Estimated: `10626` // Minimum execution time: 412_223_000 picoseconds. Weight::from_parts(430_190_000, 10626) - .saturating_add(RocksDbWeight::get().reads(31_u64)) - .saturating_add(RocksDbWeight::get().writes(14_u64)) + .saturating_add(RocksDbWeight::get().reads(30_u64)) + .saturating_add(RocksDbWeight::get().writes(13_u64)) } /// Storage: `Crowdloan::CurrentCrowdloanId` (r:1 w:0) /// Proof: `Crowdloan::CurrentCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -4065,9 +4065,9 @@ impl WeightInfo for () { Weight::from_parts(286_320_370, 10400) // Standard Error: 33_372 .saturating_add(Weight::from_parts(47_145_967, 0).saturating_mul(k.into())) - .saturating_add(RocksDbWeight::get().reads(50_u64)) + .saturating_add(RocksDbWeight::get().reads(54_u64)) .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(k.into()))) - .saturating_add(RocksDbWeight::get().writes(51_u64)) + .saturating_add(RocksDbWeight::get().writes(54_u64)) .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(k.into()))) .saturating_add(Weight::from_parts(0, 2579).saturating_mul(k.into())) } @@ -4295,10 +4295,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `2365` // Estimated: `8556` - // Minimum execution time: 534_433_000 picoseconds. - Weight::from_parts(534_433_000, 8556) - .saturating_add(RocksDbWeight::get().reads(31_u64)) - .saturating_add(RocksDbWeight::get().writes(17_u64)) + // Minimum execution time: 471_702_000 picoseconds. + Weight::from_parts(484_481_000, 8556) + .saturating_add(RocksDbWeight::get().reads(30_u64)) + .saturating_add(RocksDbWeight::get().writes(16_u64)) } /// Storage: `SubtensorModule::PendingChildKeyCooldown` (r:0 w:1) /// Proof: `SubtensorModule::PendingChildKeyCooldown` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) From 9b7a28146e68be07c213ebde0a83f48c619e3198 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 17 Apr 2026 15:22:34 -0400 Subject: [PATCH 04/13] Add RPC to read hotkey conviction and most convicted hotkey on a subnet --- Cargo.lock | 1 + pallets/subtensor/runtime-api/Cargo.toml | 2 + pallets/subtensor/runtime-api/src/lib.rs | 3 + pallets/subtensor/src/lib.rs | 4 +- pallets/subtensor/src/macros/dispatches.rs | 9 +- pallets/subtensor/src/macros/errors.rs | 2 +- pallets/subtensor/src/staking/lock.rs | 46 ++++--- pallets/subtensor/src/staking/stake_utils.rs | 10 +- pallets/subtensor/src/tests/locks.rs | 137 +++++++++++++------ runtime/src/lib.rs | 10 +- 10 files changed, 145 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e7cf36d22e..08bdafbb68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18199,6 +18199,7 @@ dependencies = [ "parity-scale-codec", "sp-api", "sp-runtime", + "substrate-fixed", "subtensor-runtime-common", ] diff --git a/pallets/subtensor/runtime-api/Cargo.toml b/pallets/subtensor/runtime-api/Cargo.toml index b427fc333c..a83c3b3178 100644 --- a/pallets/subtensor/runtime-api/Cargo.toml +++ b/pallets/subtensor/runtime-api/Cargo.toml @@ -15,6 +15,7 @@ workspace = true sp-api.workspace = true sp-runtime.workspace = true codec = { workspace = true, features = ["derive"] } +substrate-fixed.workspace = true subtensor-runtime-common.workspace = true # local pallet-subtensor.workspace = true @@ -26,6 +27,7 @@ std = [ "pallet-subtensor/std", "sp-api/std", "sp-runtime/std", + "substrate-fixed/std", "subtensor-runtime-common/std", ] pow-faucet = [] diff --git a/pallets/subtensor/runtime-api/src/lib.rs b/pallets/subtensor/runtime-api/src/lib.rs index 84da95cd36..4f4a782745 100644 --- a/pallets/subtensor/runtime-api/src/lib.rs +++ b/pallets/subtensor/runtime-api/src/lib.rs @@ -12,6 +12,7 @@ use pallet_subtensor::rpc_info::{ subnet_info::{SubnetHyperparams, SubnetHyperparamsV2, SubnetInfo, SubnetInfov2}, }; use sp_runtime::AccountId32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, TaoBalance}; // Here we declare the runtime API. It is implemented it the `impl` block in @@ -55,6 +56,8 @@ sp_api::decl_runtime_apis! { fn get_stake_info_for_coldkeys( coldkey_accounts: Vec ) -> Vec<(AccountId32, Vec>)>; fn get_stake_info_for_hotkey_coldkey_netuid( hotkey_account: AccountId32, coldkey_account: AccountId32, netuid: NetUid ) -> Option>; fn get_stake_fee( origin: Option<(AccountId32, NetUid)>, origin_coldkey_account: AccountId32, destination: Option<(AccountId32, NetUid)>, destination_coldkey_account: AccountId32, amount: u64 ) -> u64; + fn get_hotkey_conviction(hotkey: AccountId32, netuid: NetUid) -> U64F64; + fn get_most_convicted_hotkey_on_subnet(netuid: NetUid) -> Option; } pub trait SubnetRegistrationRuntimeApi { diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index e0a07653a6..e28b64bfa4 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1493,9 +1493,7 @@ pub mod pallet { >; /// Exponential lock state for a coldkey on a subnet. - #[derive( - Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, - )] + #[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo)] pub struct LockState { /// The hotkey this stake is locked to. pub hotkey: AccountId, diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index b3e9daa6e3..498de7d069 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2559,12 +2559,7 @@ mod dispatches { amount: AlphaBalance, ) -> DispatchResult { let coldkey = ensure_signed(origin)?; - Self::do_lock_stake( - &coldkey, - netuid, - &hotkey, - amount, - ) - } + Self::do_lock_stake(&coldkey, netuid, &hotkey, amount) + } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 87c4152795..7086aa328d 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -296,6 +296,6 @@ mod errors { /// Lock hotkey mismatch: existing lock is for a different hotkey. LockHotkeyMismatch, /// Insufficient stake on subnet to cover the lock amount. - InsufficientStakeForLock, + InsufficientStakeForLock, } } diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index e496a41011..32b9a198c8 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -15,8 +15,9 @@ impl Pallet { return U64F64::saturating_from_num(0); } let min_ratio = I64F64::saturating_from_num(-40); - let neg_ratio = - I64F64::saturating_from_num(-(dt as i128)).checked_div(I64F64::saturating_from_num(tau)).unwrap_or(min_ratio); + let neg_ratio = I64F64::saturating_from_num(-(dt as i128)) + .checked_div(I64F64::saturating_from_num(tau)) + .unwrap_or(min_ratio); let clamped = neg_ratio.max(min_ratio); let result: I64F64 = exp(clamped).unwrap_or(I64F64::saturating_from_num(0)); if result < I64F64::saturating_from_num(0) { @@ -30,10 +31,7 @@ impl Pallet { /// /// X_new = decay * X_old /// Y_new = decay * (Y_old + dt * X_old) - pub fn roll_forward_lock( - lock: LockState, - now: u64, - ) -> LockState { + pub fn roll_forward_lock(lock: LockState, now: u64) -> LockState { if now <= lock.last_update { return lock; } @@ -43,9 +41,14 @@ impl Pallet { let dt_fixed = U64F64::saturating_from_num(dt); let mass_fixed = U64F64::saturating_from_num(lock.locked_mass); - let new_locked_mass = decay.saturating_mul(mass_fixed).saturating_to_num::().into(); - let new_conviction = - decay.saturating_mul(lock.conviction.saturating_add(dt_fixed.saturating_mul(mass_fixed))); + let new_locked_mass = decay + .saturating_mul(mass_fixed) + .saturating_to_num::() + .into(); + let new_conviction = decay.saturating_mul( + lock.conviction + .saturating_add(dt_fixed.saturating_mul(mass_fixed)), + ); LockState { hotkey: lock.hotkey, @@ -59,7 +62,9 @@ impl Pallet { pub fn total_coldkey_alpha_on_subnet(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { StakingHotkeys::::get(coldkey) .into_iter() - .map(|hotkey| Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, coldkey, netuid)) + .map(|hotkey| { + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, coldkey, netuid) + }) .fold(AlphaBalance::ZERO, |acc, stake| acc.saturating_add(stake)) } @@ -101,10 +106,7 @@ impl Pallet { hotkey: &T::AccountId, amount: AlphaBalance, ) -> dispatch::DispatchResult { - ensure!( - !amount.is_zero(), - Error::::AmountTooLow - ); + ensure!(!amount.is_zero(), Error::::AmountTooLow); let total = Self::total_coldkey_alpha_on_subnet(coldkey, netuid); let now = Self::get_current_block_as_u64(); @@ -124,10 +126,7 @@ impl Pallet { ); } Some(existing) => { - ensure!( - *hotkey == existing.hotkey, - Error::::LockHotkeyMismatch - ); + ensure!(*hotkey == existing.hotkey, Error::::LockHotkeyMismatch); let lock = Self::roll_forward_lock(existing, now); let new_locked = lock.locked_mass.saturating_add(amount); ensure!(total >= new_locked, Error::::InsufficientStakeForLock); @@ -160,8 +159,10 @@ impl Pallet { let now = Self::get_current_block_as_u64(); let lock = Self::roll_forward_lock(existing, now); let dust = DUST_THRESHOLD.into(); - - if lock.locked_mass < dust && lock.conviction < U64F64::saturating_from_num(DUST_THRESHOLD) { + + if lock.locked_mass < dust + && lock.conviction < U64F64::saturating_from_num(DUST_THRESHOLD) + { Lock::::remove(coldkey, netuid); } else { Lock::::insert(coldkey, netuid, lock); @@ -206,7 +207,10 @@ impl Pallet { scores .into_values() - .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(sp_std::cmp::Ordering::Equal)) + .max_by(|a, b| { + a.1.partial_cmp(&b.1) + .unwrap_or(sp_std::cmp::Ordering::Equal) + }) .map(|(hotkey, _)| hotkey) } } diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 363f7d6276..435dda3b99 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -1159,7 +1159,10 @@ impl Pallet { // Ensure that unstaked amount is not greater than available to unstake (due to locks) let alpha_available = Self::available_to_unstake(coldkey, netuid); - ensure!(alpha_available >= alpha_unstaked, Error::::CannotUnstakeLock); + ensure!( + alpha_available >= alpha_unstaked, + Error::::CannotUnstakeLock + ); Ok(()) } @@ -1310,7 +1313,10 @@ impl Pallet { // (cross-coldkey transfer or cross-subnet move), the remaining amount must cover the lock. if origin_coldkey != destination_coldkey || origin_netuid != destination_netuid { let alpha_available = Self::available_to_unstake(origin_coldkey, origin_netuid); - ensure!(alpha_available >= alpha_amount, Error::::CannotUnstakeLock); + ensure!( + alpha_available >= alpha_amount, + Error::::CannotUnstakeLock + ); } Ok(()) diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index 9de6abfa2a..01adc9f777 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -1,8 +1,8 @@ #![allow(clippy::unwrap_used, clippy::arithmetic_side_effects)] use approx::assert_abs_diff_eq; -use frame_support::{assert_noop, assert_ok}; use frame_support::weights::Weight; +use frame_support::{assert_noop, assert_ok}; use sp_core::U256; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, TaoBalance}; @@ -46,7 +46,11 @@ fn setup_subnet_with_stake( netuid } -fn get_alpha(hotkey: &U256, coldkey: &U256, netuid: subtensor_runtime_common::NetUid) -> AlphaBalance { +fn get_alpha( + hotkey: &U256, + coldkey: &U256, + netuid: subtensor_runtime_common::NetUid, +) -> AlphaBalance { SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid) } @@ -75,7 +79,10 @@ fn test_lock_stake_creates_new_lock() { assert_eq!(lock.hotkey, hotkey); assert_eq!(lock.locked_mass, lock_amount.into()); assert_eq!(lock.conviction, U64F64::saturating_from_num(0)); - assert_eq!(lock.last_update, SubtensorModule::get_current_block_as_u64()); + assert_eq!( + lock.last_update, + SubtensorModule::get_current_block_as_u64() + ); }); } @@ -200,10 +207,7 @@ fn test_available_to_unstake_fully_locked() { let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - total, + &coldkey, netuid, &hotkey, total, )); let available = SubtensorModule::available_to_unstake(&coldkey, netuid); @@ -223,12 +227,22 @@ fn test_lock_stake_topup() { let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); let first_lock = 1000u64; - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, first_lock.into())); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + first_lock.into() + )); step_block(100); let second_lock = 500u64; - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, second_lock.into())); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + second_lock.into() + )); let lock = Lock::::get(coldkey, netuid).unwrap(); // locked_mass should be decayed(first_lock) + second_lock @@ -237,7 +251,10 @@ fn test_lock_stake_topup() { assert!(lock.locked_mass < 1501.into()); // conviction should have grown from the time the first lock was active assert!(lock.conviction > U64F64::saturating_from_num(0)); - assert_eq!(lock.last_update, SubtensorModule::get_current_block_as_u64()); + assert_eq!( + lock.last_update, + SubtensorModule::get_current_block_as_u64() + ); }); } @@ -250,11 +267,17 @@ fn test_lock_stake_topup_multiple_times() { let chunk = 500u64.into(); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, chunk)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, chunk + )); step_block(50); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, chunk)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, chunk + )); step_block(50); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, chunk)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, chunk + )); let lock = Lock::::get(coldkey, netuid).unwrap(); // After three top-ups with small decay, should be close to 1500 @@ -274,9 +297,13 @@ fn test_lock_stake_topup_same_block() { let first = 1000u64.into(); let second = 500u64.into(); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, first)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, first + )); // No block advancement — same block top-up - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, second)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, second + )); let lock = Lock::::get(coldkey, netuid).unwrap(); // dt=0 means no decay, simple addition @@ -297,12 +324,7 @@ fn test_lock_stake_zero_amount() { let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); assert_noop!( - SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - AlphaBalance::ZERO, - ), + SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, AlphaBalance::ZERO,), Error::::AmountTooLow ); }); @@ -341,12 +363,7 @@ fn test_lock_stake_wrong_hotkey() { )); assert_noop!( - SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey_b, - 500u64.into(), - ), + SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey_b, 500u64.into(),), Error::::LockHotkeyMismatch ); }); @@ -362,7 +379,9 @@ fn test_lock_stake_topup_exceeds_total() { let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); // Lock 80% initially let initial = total * 8.into() / 10.into(); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, initial)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, initial + )); // Try to top up the remaining 30% (exceeds total by 10%) let topup = total * 3.into() / 10.into(); @@ -417,7 +436,12 @@ fn test_roll_forward_locked_mass_decays() { let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); let lock_amount = 10000u64; - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, lock_amount.into())); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount.into() + )); // Advance one full tau via direct block number jump (step_block overflows u16 for tau=216000) let tau = TauBlocks::::get(); @@ -444,7 +468,12 @@ fn test_roll_forward_conviction_grows_then_decays() { let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); let lock_amount = 10000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, lock_amount)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount + )); // Conviction at t=0 is 0 let c0 = SubtensorModule::get_conviction(&coldkey, netuid); @@ -519,7 +548,12 @@ fn test_unstake_allowed_up_to_available() { let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); let lock_amount = total / 2.into(); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, lock_amount)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount + )); // Unstake the unlocked half let alpha = get_alpha(&hotkey, &coldkey, netuid); @@ -569,7 +603,9 @@ fn test_unstake_allowed_after_decay() { let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, total + )); // Advance many taus so lock decays to near-zero (use set_block_number to avoid u16 overflow) let tau = TauBlocks::::get(); @@ -603,7 +639,9 @@ fn test_unstake_partial_after_partial_decay() { let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, total + )); // Advance one tau: lock ~ 37% of original let tau = TauBlocks::::get(); @@ -649,7 +687,9 @@ fn test_move_stake_same_coldkey_same_subnet_allowed() { let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); // Lock the full amount to hotkey_a - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey_a, total)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey_a, total + )); // Move from hotkey_a to hotkey_b on same subnet — total coldkey alpha unchanged let alpha = get_alpha(&hotkey_a, &coldkey, netuid); @@ -682,7 +722,9 @@ fn test_move_stake_cross_subnet_blocked_by_lock() { ); let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid_a); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid_a, &hotkey, total)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid_a, &hotkey, total + )); step_block(1); @@ -1200,12 +1242,7 @@ fn test_hotkey_swap_lock_becomes_stale() { // Trying to top up to new_hotkey fails with mismatch assert_noop!( - SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &new_hotkey, - 100u64.into(), - ), + SubtensorModule::do_lock_stake(&coldkey, netuid, &new_hotkey, 100u64.into(),), Error::::LockHotkeyMismatch ); }); @@ -1328,7 +1365,9 @@ fn test_burn_alpha_bypasses_lock() { let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, total + )); step_block(1); @@ -1472,7 +1511,12 @@ fn test_emissions_do_not_break_lock_invariant() { let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); let total_alpha_before = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total_alpha_before)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + total_alpha_before + )); // Simulate emission: directly increase alpha for the hotkey on subnet // This increases the pool value for all share holders (including our coldkey) @@ -1508,7 +1552,12 @@ fn test_neuron_replacement_does_not_affect_lock() { register_ok_neuron(netuid, hotkey, coldkey, 0); let lock_amount = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, lock_amount)); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount + )); let total_before = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); let locked_before = SubtensorModule::get_current_locked(&coldkey, netuid); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ed6d4d6176..27007cdda5 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -73,7 +73,7 @@ use sp_std::prelude::*; use sp_version::NativeVersion; use sp_version::RuntimeVersion; use stp_shield::ShieldedTransaction; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::{U64F64, U96F32}; use subtensor_precompiles::Precompiles; use subtensor_runtime_common::{AlphaBalance, AuthorshipInfo, TaoBalance, time::*, *}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -2528,6 +2528,14 @@ impl_runtime_apis! { fn get_stake_fee( origin: Option<(AccountId32, NetUid)>, origin_coldkey_account: AccountId32, destination: Option<(AccountId32, NetUid)>, destination_coldkey_account: AccountId32, amount: u64 ) -> u64 { SubtensorModule::get_stake_fee( origin, origin_coldkey_account, destination, destination_coldkey_account, amount ) } + + fn get_hotkey_conviction(hotkey: AccountId32, netuid: NetUid) -> U64F64 { + SubtensorModule::hotkey_conviction(&hotkey, netuid) + } + + fn get_most_convicted_hotkey_on_subnet(netuid: NetUid) -> Option { + SubtensorModule::subnet_king(netuid) + } } impl subtensor_custom_rpc_runtime_api::SubnetRegistrationRuntimeApi for Runtime { From af68f70720d2b1c2bafefedfabf837b12a92cb2b Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 17 Apr 2026 15:27:41 -0400 Subject: [PATCH 05/13] Add a test for exp_decay to test clamping --- pallets/subtensor/src/tests/locks.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index 01adc9f777..4570f75e37 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -428,6 +428,24 @@ fn test_exp_decay_one_tau() { }); } +#[test] +fn test_exp_decay_clamps_large_dt_to_min_ratio() { + new_test_ext(1).execute_with(|| { + let tau = 216000u64; + let clamped_result = SubtensorModule::exp_decay(40 * tau, tau); + let oversized_result = SubtensorModule::exp_decay(100 * tau, tau); + + let diff = if oversized_result > clamped_result { + oversized_result - clamped_result + } else { + clamped_result - oversized_result + }; + + assert!(diff < U64F64::saturating_from_num(0.000000001)); + assert!(oversized_result > U64F64::saturating_from_num(0)); + }); +} + #[test] fn test_roll_forward_locked_mass_decays() { new_test_ext(1).execute_with(|| { From 22b7ec3513851b26f4299f101a88f55753f1d42c Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 17 Apr 2026 15:36:12 -0400 Subject: [PATCH 06/13] clippy --- pallets/subtensor/src/staking/lock.rs | 3 ++- pallets/subtensor/src/tests/locks.rs | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index 32b9a198c8..e3354ad747 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -1,4 +1,5 @@ use super::*; +use sp_std::ops::Neg; use substrate_fixed::transcendental::exp; use substrate_fixed::types::{I64F64, U64F64}; use subtensor_runtime_common::NetUid; @@ -15,7 +16,7 @@ impl Pallet { return U64F64::saturating_from_num(0); } let min_ratio = I64F64::saturating_from_num(-40); - let neg_ratio = I64F64::saturating_from_num(-(dt as i128)) + let neg_ratio = I64F64::saturating_from_num((dt as i128).neg()) .checked_div(I64F64::saturating_from_num(tau)) .unwrap_or(min_ratio); let clamped = neg_ratio.max(min_ratio); diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index 4570f75e37..341c642319 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -1,4 +1,8 @@ -#![allow(clippy::unwrap_used, clippy::arithmetic_side_effects)] +#![allow( + clippy::expect_used, + clippy::unwrap_used, + clippy::arithmetic_side_effects +)] use approx::assert_abs_diff_eq; use frame_support::weights::Weight; From 831ab98d23d562996a9e78c10eb4c4c48c93b53e Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 17 Apr 2026 16:28:07 -0400 Subject: [PATCH 07/13] Add freeze struct to LockState --- pallets/subtensor/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index e28b64bfa4..79a4c6429b 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1493,6 +1493,7 @@ pub mod pallet { >; /// Exponential lock state for a coldkey on a subnet. + #[crate::freeze_struct("cfa10602e0577f6e")] #[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo)] pub struct LockState { /// The hotkey this stake is locked to. From aaccea9ccda977575c66900da2d4a1caae2ff6de Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 17 Apr 2026 16:28:34 -0400 Subject: [PATCH 08/13] Spec bump --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 27007cdda5..514ee64214 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -272,7 +272,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 397, + spec_version: 398, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 6464408538dd40ea58300008a93790859ec4a0d0 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 20 Apr 2026 14:42:47 -0400 Subject: [PATCH 09/13] Prepare for moving lock, fix recycle and burn alpha --- pallets/subtensor/src/macros/errors.rs | 2 + pallets/subtensor/src/macros/events.rs | 12 +++++ pallets/subtensor/src/staking/lock.rs | 11 +++++ .../subtensor/src/staking/recycle_alpha.rs | 6 +++ pallets/subtensor/src/staking/stake_utils.rs | 12 +---- pallets/subtensor/src/tests/locks.rs | 44 +++++++++++-------- 6 files changed, 58 insertions(+), 29 deletions(-) diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 7086aa328d..1992d14b5f 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -297,5 +297,7 @@ mod errors { LockHotkeyMismatch, /// Insufficient stake on subnet to cover the lock amount. InsufficientStakeForLock, + /// No existing lock found for the given coldkey and subnet. + NoExistingLock, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index f0da9a3d3d..2f74f0e764 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -582,5 +582,17 @@ mod events { /// The alpha amount locked. amount: AlphaBalance, }, + + /// Stake has been unlocked from a hotkey on a subnet. + LockMoved { + /// The coldkey that moved the lock. + coldkey: T::AccountId, + /// The hotkey the lock was moved from. + origin_hotkey: T::AccountId, + /// The hotkey the lock was moved to. + destination_hotkey: T::AccountId, + /// The subnet the lock is on. + netuid: NetUid, + }, } } diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index e3354ad747..d936bf1ba8 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -98,6 +98,17 @@ impl Pallet { } } + /// Ensures that the amount can be unstaked + pub fn ensure_available_to_unstake( + coldkey: &T::AccountId, + netuid: NetUid, + amount: AlphaBalance, + ) -> Result<(), Error> { + let alpha_available = Self::available_to_unstake(coldkey, netuid); + ensure!(alpha_available >= amount, Error::::CannotUnstakeLock); + Ok(()) + } + /// Locks stake for a coldkey on a subnet to a specific hotkey. /// If no lock exists, creates one. If one exists, the hotkey must match. /// Top-up adds to locked_mass after rolling forward. diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index bb93c12818..d59ea34da0 100644 --- a/pallets/subtensor/src/staking/recycle_alpha.rs +++ b/pallets/subtensor/src/staking/recycle_alpha.rs @@ -38,6 +38,9 @@ impl Pallet { Error::::HotKeyAccountNotExists ); + // Ensure that recycled amount is not greater than available to unstake (due to locks) + Self::ensure_available_to_unstake(&coldkey, netuid, amount)?; + // Ensure that the hotkey has enough stake to withdraw. // Cap the amount at available Alpha because user might be paying transaxtion fees // in Alpha and their total is already reduced by now. @@ -96,6 +99,9 @@ impl Pallet { Error::::HotKeyAccountNotExists ); + // Ensure that burned amount is not greater than available to unstake (due to locks) + Self::ensure_available_to_unstake(&coldkey, netuid, amount)?; + // Ensure that the hotkey has enough stake to withdraw. // Cap the amount at available Alpha because user might be paying transaxtion fees // in Alpha and their total is already reduced by now. diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 435dda3b99..17f6650e8a 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -1158,11 +1158,7 @@ impl Pallet { ); // Ensure that unstaked amount is not greater than available to unstake (due to locks) - let alpha_available = Self::available_to_unstake(coldkey, netuid); - ensure!( - alpha_available >= alpha_unstaked, - Error::::CannotUnstakeLock - ); + Self::ensure_available_to_unstake(&coldkey, netuid, alpha_unstaked)?; Ok(()) } @@ -1312,11 +1308,7 @@ impl Pallet { // Enforce lock invariant: if the operation reduces total coldkey alpha on origin subnet // (cross-coldkey transfer or cross-subnet move), the remaining amount must cover the lock. if origin_coldkey != destination_coldkey || origin_netuid != destination_netuid { - let alpha_available = Self::available_to_unstake(origin_coldkey, origin_netuid); - ensure!( - alpha_available >= alpha_amount, - Error::::CannotUnstakeLock - ); + Self::ensure_available_to_unstake(origin_coldkey, origin_netuid, alpha_amount)?; } Ok(()) diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index 341c642319..d285c736c2 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -1363,24 +1363,27 @@ fn test_recycle_alpha_bypasses_lock() { Error::::CannotUnstakeLock ); - // BUG: recycle_alpha bypasses lock — it succeeds despite full lock + // recycle_alpha checks lock and should fail if it would reduce alpha below locked amount let recycle_amount = alpha / 2.into(); - assert_ok!(SubtensorModule::do_recycle_alpha( - RuntimeOrigin::signed(coldkey), - hotkey, - recycle_amount, - netuid, - )); + assert_noop!( + SubtensorModule::do_recycle_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + recycle_amount, + netuid, + ), + Error::::CannotUnstakeLock + ); - // Alpha is now below locked_mass — lock invariant violated + // Alpha is not below locked_mass let total_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); let locked = SubtensorModule::get_current_locked(&coldkey, netuid); - assert!(total_after < locked); + assert!(total_after >= locked); }); } #[test] -fn test_burn_alpha_bypasses_lock() { +fn test_burn_alpha_checks_lock() { new_test_ext(1).execute_with(|| { let coldkey = U256::from(1); let hotkey = U256::from(2); @@ -1393,20 +1396,23 @@ fn test_burn_alpha_bypasses_lock() { step_block(1); - // BUG: burn_alpha bypasses lock — it succeeds despite full lock + // burn_alpha checks lock and should fail if it would reduce alpha below locked amount let alpha = get_alpha(&hotkey, &coldkey, netuid); let burn_amount = alpha / 2.into(); - assert_ok!(SubtensorModule::do_burn_alpha( - RuntimeOrigin::signed(coldkey), - hotkey, - burn_amount, - netuid, - )); + assert_noop!( + SubtensorModule::do_burn_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + burn_amount, + netuid, + ), + Error::::CannotUnstakeLock + ); - // Alpha is now below locked_mass — lock invariant violated + // Alpha is not below locked_mass let total_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); let locked = SubtensorModule::get_current_locked(&coldkey, netuid); - assert!(total_after < locked); + assert!(total_after >= locked); }); } From 1740fef08abb76922c15c46a7f503806d5ec5b62 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 20 Apr 2026 15:49:28 -0400 Subject: [PATCH 10/13] Fix clear_small_nomination_if_required to also clear locks --- pallets/subtensor/src/staking/helpers.rs | 3 ++ pallets/subtensor/src/staking/lock.rs | 19 ++---------- pallets/subtensor/src/tests/locks.rs | 38 ++++-------------------- 3 files changed, 11 insertions(+), 49 deletions(-) diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index 5c785f199b..2fd80f63c2 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -245,6 +245,9 @@ impl Pallet { if let Ok(cleared_stake) = maybe_cleared_stake { // Add the stake to the coldkey account. Self::add_balance_to_coldkey_account(coldkey, cleared_stake.into()); + + // Clear the lock if exists + Self::maybe_cleanup_lock(coldkey, netuid); } else { // Just clear small alpha let alpha = diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index d936bf1ba8..e002b43034 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -4,8 +4,6 @@ use substrate_fixed::transcendental::exp; use substrate_fixed::types::{I64F64, U64F64}; use subtensor_runtime_common::NetUid; -const DUST_THRESHOLD: u64 = 100; - impl Pallet { /// Computes exp(-dt / tau) as a U64F64 decay factor. pub fn exp_decay(dt: u64, tau: u64) -> U64F64 { @@ -165,21 +163,10 @@ impl Pallet { Ok(()) } - /// Clears the lock if both locked_mass and conviction have decayed below the dust threshold. + /// Clears the lock. This function will be called if the alpha stake drops below minimum + /// threshold. pub fn maybe_cleanup_lock(coldkey: &T::AccountId, netuid: NetUid) { - if let Some(existing) = Lock::::get(coldkey, netuid) { - let now = Self::get_current_block_as_u64(); - let lock = Self::roll_forward_lock(existing, now); - let dust = DUST_THRESHOLD.into(); - - if lock.locked_mass < dust - && lock.conviction < U64F64::saturating_from_num(DUST_THRESHOLD) - { - Lock::::remove(coldkey, netuid); - } else { - Lock::::insert(coldkey, netuid, lock); - } - } + Lock::::remove(coldkey, netuid); } /// Returns the total conviction for a hotkey on a subnet, diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index d285c736c2..4f8c67b406 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -1127,34 +1127,6 @@ fn test_maybe_cleanup_lock_removes_dust() { }); } -#[test] -fn test_maybe_cleanup_lock_preserves_active_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - 100_000u64.into(), - )); - - step_block(100); - - SubtensorModule::maybe_cleanup_lock(&coldkey, netuid); - - let lock = Lock::::get(coldkey, netuid); - assert!(lock.is_some()); - // last_update should be rolled forward to current block - assert_eq!( - lock.unwrap().last_update, - SubtensorModule::get_current_block_as_u64() - ); - }); -} - #[test] fn test_maybe_cleanup_lock_no_lock() { new_test_ext(1).execute_with(|| { @@ -1336,11 +1308,11 @@ fn test_lock_stake_extrinsic() { } // ========================================================================= -// GROUP 14: Recycle/burn alpha bypass (BUG: bypasses lock) +// GROUP 14: Recycle/burn alpha checks against lock // ========================================================================= #[test] -fn test_recycle_alpha_bypasses_lock() { +fn test_recycle_alpha_checks_lock() { new_test_ext(1).execute_with(|| { let coldkey = U256::from(1); let hotkey = U256::from(2); @@ -1475,11 +1447,11 @@ fn test_subnet_dissolution_and_netuid_reuse() { } // ========================================================================= -// GROUP 16: Clear small nomination bypass +// GROUP 16: Clear small nomination checks lock // ========================================================================= #[test] -fn test_clear_small_nomination_bypasses_lock() { +fn test_clear_small_nomination_checks_lock() { new_test_ext(1).execute_with(|| { let owner_coldkey = U256::from(100); let owner_hotkey = U256::from(101); @@ -1523,7 +1495,7 @@ fn test_clear_small_nomination_bypasses_lock() { assert_eq!(nominator_alpha_after, AlphaBalance::ZERO); // Lock entry still exists, now orphaned - assert!(Lock::::get(nominator, netuid).is_some()); + assert!(Lock::::get(nominator, netuid).is_none()); }); } From aae56612ac8df8b681d40ddf7fc3a5d94c270573 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 20 Apr 2026 15:59:49 -0400 Subject: [PATCH 11/13] Add Lock cleanup on network dissolution --- pallets/subtensor/src/staking/remove_stake.rs | 9 +++++++++ pallets/subtensor/src/tests/locks.rs | 8 ++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 0750456106..cae3e6b48f 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -600,6 +600,15 @@ impl Pallet { Self::add_balance_to_coldkey_account(&owner_coldkey, refund); } + // 9) Cleanup all subnet stake locks if any. + let lock_keys: Vec<(T::AccountId, NetUid)> = Lock::::iter_keys() + .filter(|(_, this_netuid)| *this_netuid == netuid) + .map(|(coldkey, this_netuid)| (coldkey.clone(), this_netuid)) + .collect(); + for (coldkey, netuid) in lock_keys { + Lock::::remove(coldkey, netuid); + } + Ok(()) } } diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index 4f8c67b406..2ea33c93d1 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -1416,8 +1416,9 @@ fn test_subnet_dissolution_orphans_locks() { AlphaBalance::ZERO ); - // BUG: Lock entry is orphaned — still present despite no alpha - assert!(Lock::::get(coldkey, netuid).is_some()); + // Lock entries are not orphaned + let lock = Lock::::get(coldkey, netuid); + assert!(lock.is_none()); }); } @@ -1441,8 +1442,7 @@ fn test_subnet_dissolution_and_netuid_reuse() { // The stale lock from old subnet remains let stale_lock = Lock::::get(coldkey, netuid); - assert!(stale_lock.is_some()); - assert_eq!(stale_lock.unwrap().hotkey, hotkey_old); + assert!(stale_lock.is_none()); }); } From 515ac540e9902888c02a0c64ac4cf67acbd982e9 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 20 Apr 2026 16:45:22 -0400 Subject: [PATCH 12/13] Prepare for swapping locks on coldkey swaps --- pallets/subtensor/src/staking/lock.rs | 63 ++++++++++++++++++++++ pallets/subtensor/src/swap/swap_coldkey.rs | 3 ++ 2 files changed, 66 insertions(+) diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index e002b43034..34abe94ff6 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -212,4 +212,67 @@ impl Pallet { }) .map(|(hotkey, _)| hotkey) } + + /// Transfers the lock from one coldkey to another for all subnets. This is used when a + /// user swaps their coldkey and we want to preserve their locks. + /// The hotkey and netuid remain the same, only the coldkey changes. + /// + /// If the new coldkey already has a lock for the same subnet, the locks are merged by summing + /// the locked_mass and conviction after rolling forward both locks to now. + pub fn transfer_lock_coldkey(_old_coldkey: &T::AccountId, _new_coldkey: &T::AccountId) { + // let now = Self::get_current_block_as_u64(); + // let mut locks_to_transfer: Vec<(NetUid, LockState)> = Vec::new(); + + // // Gather locks from old coldkey + // for (coldkey, netuid, lock) in Lock::::iter() { + // if coldkey == *old_coldkey { + // locks_to_transfer.push((netuid, lock)); + // } + // } + + // // Transfer each lock to new coldkey + // for (netuid, old_lock) in locks_to_transfer { + // let rolled_old_lock = Self::roll_forward_lock(old_lock, now); + // match Lock::::get(new_coldkey, netuid) { + // None => { + // // No existing lock for new coldkey, simply transfer + // Lock::::insert( + // new_coldkey, + // netuid, + // LockState { + // hotkey: rolled_old_lock.hotkey.clone(), + // locked_mass: rolled_old_lock.locked_mass, + // conviction: rolled_old_lock.conviction, + // last_update: now, + // }, + // ); + // } + // Some(existing) => { + // // Existing lock for new coldkey, merge them + // let rolled_existing = Self::roll_forward_lock(existing, now); + // ensure!( + // rolled_old_lock.hotkey == rolled_existing.hotkey, + // Error::::LockHotkeyMismatch + // ); + // let new_locked_mass = + // rolled_old_lock.locked_mass.saturating_add(rolled_existing.locked_mass); + // let new_conviction = + // rolled_old_lock.conviction.saturating_add(rolled_existing.conviction); + // Lock::::insert( + // new_coldkey, + // netuid, + // LockState { + // hotkey: rolled_old_lock.hotkey.clone(), + // locked_mass: new_locked_mass, + // conviction: new_conviction, + // last_update: now, + // }, + // ); + + // // Remove the old lock since it's now merged + // Lock::::remove(old_coldkey, netuid); + // } + // } + // } + } } diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index 68cf6d8b56..f273387df0 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -31,6 +31,9 @@ impl Pallet { Self::transfer_staking_hotkeys(old_coldkey, new_coldkey); Self::transfer_hotkeys_ownership(old_coldkey, new_coldkey); + // Transfer stake locks + Self::transfer_lock_coldkey(old_coldkey, new_coldkey); + // Transfer any remaining balance from old_coldkey to new_coldkey let remaining_balance = Self::get_coldkey_balance(old_coldkey); if remaining_balance > 0.into() { From 71a7139620bf1d0fdde12384fddbb60c70070168 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 20 Apr 2026 19:03:19 -0400 Subject: [PATCH 13/13] Remove O(n) iteration on Lock entries in subnet_king --- pallets/subtensor/src/lib.rs | 24 +++++ pallets/subtensor/src/staking/lock.rs | 132 +++++++++++++++++--------- 2 files changed, 112 insertions(+), 44 deletions(-) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 79a4c6429b..44502616a9 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1518,6 +1518,30 @@ pub mod pallet { OptionQuery, >; + /// Exponential lock state for a hotkey on a subnet. + #[crate::freeze_struct("aba5b4d024b9837a")] + #[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo)] + pub struct HotkeyLockState { + /// Exponentially decaying locked amount. + pub locked_mass: AlphaBalance, + /// Matured decaying score (integral of locked_mass over time). + pub conviction: U64F64, + /// Block number of last roll-forward. + pub last_update: u64, + } + + /// --- DMAP ( netuid, hotkey ) --> LockState | Total lock per hotkey per subnet. + #[pallet::storage] + pub type HotkeyLock = StorageDoubleMap< + _, + Identity, + NetUid, // subnet + Blake2_128Concat, + T::AccountId, // hotkey + HotkeyLockState, // Total merged lock + OptionQuery, + >; + /// Default decay timescale: ~30 days at 12s blocks. #[pallet::type_value] pub fn DefaultTauBlocks() -> u64 { diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index 34abe94ff6..d3ad0f2d26 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -1,4 +1,5 @@ use super::*; +use sp_std::collections::btree_map::BTreeMap; use sp_std::ops::Neg; use substrate_fixed::transcendental::exp; use substrate_fixed::types::{I64F64, U64F64}; @@ -26,6 +27,25 @@ impl Pallet { } } + fn calculate_decayed_mass_and_conviction( + locked_mass: AlphaBalance, + conviction: U64F64, + dt: u64, + ) -> (AlphaBalance, U64F64) { + let tau = TauBlocks::::get(); + + let decay = Self::exp_decay(dt, tau); + let dt_fixed = U64F64::saturating_from_num(dt); + let mass_fixed = U64F64::saturating_from_num(locked_mass); + let new_locked_mass = decay + .saturating_mul(mass_fixed) + .saturating_to_num::() + .into(); + let new_conviction = + decay.saturating_mul(conviction.saturating_add(dt_fixed.saturating_mul(mass_fixed))); + (new_locked_mass, new_conviction) + } + /// Rolls a LockState forward to `now` using exponential decay. /// /// X_new = decay * X_old @@ -35,19 +55,8 @@ impl Pallet { return lock; } let dt = now.saturating_sub(lock.last_update); - let tau = TauBlocks::::get(); - let decay = Self::exp_decay(dt, tau); - - let dt_fixed = U64F64::saturating_from_num(dt); - let mass_fixed = U64F64::saturating_from_num(lock.locked_mass); - let new_locked_mass = decay - .saturating_mul(mass_fixed) - .saturating_to_num::() - .into(); - let new_conviction = decay.saturating_mul( - lock.conviction - .saturating_add(dt_fixed.saturating_mul(mass_fixed)), - ); + let (new_locked_mass, new_conviction) = + Self::calculate_decayed_mass_and_conviction(lock.locked_mass, lock.conviction, dt); LockState { hotkey: lock.hotkey, @@ -57,6 +66,22 @@ impl Pallet { } } + /// Rolls a HotkeyLockState forward to `now` using exponential decay. + pub fn roll_forward_hotkey_lock(lock: HotkeyLockState, now: u64) -> HotkeyLockState { + if now <= lock.last_update { + return lock; + } + let dt = now.saturating_sub(lock.last_update); + let (new_locked_mass, new_conviction) = + Self::calculate_decayed_mass_and_conviction(lock.locked_mass, lock.conviction, dt); + + HotkeyLockState { + locked_mass: new_locked_mass, + conviction: new_conviction, + last_update: now, + } + } + /// Returns the sum of raw alpha shares for a coldkey across all hotkeys on a given subnet. pub fn total_coldkey_alpha_on_subnet(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { StakingHotkeys::::get(coldkey) @@ -153,6 +178,9 @@ impl Pallet { } } + // Update the total hotkey lock + Self::upsert_hotkey_lock(hotkey, netuid, amount); + Self::deposit_event(Event::StakeLocked { coldkey: coldkey.clone(), hotkey: hotkey.clone(), @@ -169,55 +197,71 @@ impl Pallet { Lock::::remove(coldkey, netuid); } + /// Update the total lock for a hotkey on a subnet or create one if + /// it doesn't exist. + /// + /// Roll the existing hotkey lock forward to now, then add the + /// latest conviction and locked mass. + pub fn upsert_hotkey_lock(hotkey: &T::AccountId, netuid: NetUid, amount: AlphaBalance) { + let total_lock = HotkeyLock::::get(netuid, hotkey); + + // Roll forward the total lock to now + let now = Self::get_current_block_as_u64(); + let rolled_hotkey_lock = if let Some(lock) = total_lock { + Self::roll_forward_hotkey_lock(lock, now) + } else { + HotkeyLockState { + locked_mass: 0.into(), + conviction: U64F64::saturating_from_num(0), + last_update: now, + } + }; + + // Merge the new lock into the rolled total lock (only add mass) + let new_locked_mass = rolled_hotkey_lock.locked_mass.saturating_add(amount); + let new_hotkey_lock = HotkeyLockState { + locked_mass: new_locked_mass, + conviction: rolled_hotkey_lock.conviction, + last_update: now, + }; + HotkeyLock::::insert(netuid, hotkey, new_hotkey_lock); + } + /// Returns the total conviction for a hotkey on a subnet, /// summed over all coldkeys that have locked to this hotkey. pub fn hotkey_conviction(hotkey: &T::AccountId, netuid: NetUid) -> U64F64 { - let now = Self::get_current_block_as_u64(); - let mut total = U64F64::saturating_from_num(0); - for (_coldkey, _subnet_id, lock) in Lock::::iter() { - if _subnet_id != netuid { - continue; - } - if *hotkey == lock.hotkey { - let rolled = Self::roll_forward_lock(lock, now); - total = total.saturating_add(rolled.conviction); - } + let lock = HotkeyLock::::get(netuid, hotkey); + if let Some(lock) = lock { + Self::roll_forward_hotkey_lock(lock, Self::get_current_block_as_u64()).conviction + } else { + U64F64::saturating_from_num(0) } - total } /// Finds the hotkey with the highest conviction on a given subnet. pub fn subnet_king(netuid: NetUid) -> Option { let now = Self::get_current_block_as_u64(); - let mut scores: sp_std::collections::btree_map::BTreeMap, (T::AccountId, U64F64)> = - sp_std::collections::btree_map::BTreeMap::new(); + let mut scores: BTreeMap = BTreeMap::new(); - for (_coldkey, subnet_id, lock) in Lock::::iter() { - if subnet_id != netuid { - continue; - } - let rolled = Self::roll_forward_lock(lock, now); - let key = rolled.hotkey.encode(); + HotkeyLock::::iter_prefix(netuid).for_each(|(hotkey, lock)| { + let rolled = Self::roll_forward_hotkey_lock(lock, now); let entry = scores - .entry(key) - .or_insert_with(|| (rolled.hotkey.clone(), U64F64::saturating_from_num(0))); - entry.1 = entry.1.saturating_add(rolled.conviction); - } + .entry(hotkey) + .or_insert_with(|| U64F64::saturating_from_num(0)); + *entry = entry.saturating_add(rolled.conviction); + }); scores - .into_values() - .max_by(|a, b| { - a.1.partial_cmp(&b.1) - .unwrap_or(sp_std::cmp::Ordering::Equal) - }) + .into_iter() + .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(core::cmp::Ordering::Equal)) .map(|(hotkey, _)| hotkey) } - /// Transfers the lock from one coldkey to another for all subnets. This is used when a + /// Transfers the lock from one coldkey to another for all subnets. This is used when a /// user swaps their coldkey and we want to preserve their locks. /// The hotkey and netuid remain the same, only the coldkey changes. - /// - /// If the new coldkey already has a lock for the same subnet, the locks are merged by summing + /// + /// If the new coldkey already has a lock for the same subnet, the locks are merged by summing /// the locked_mass and conviction after rolling forward both locks to now. pub fn transfer_lock_coldkey(_old_coldkey: &T::AccountId, _new_coldkey: &T::AccountId) { // let now = Self::get_current_block_as_u64();