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 94a18d7d16..44502616a9 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1492,6 +1492,66 @@ pub mod pallet { ValueQuery, >; + /// 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. + 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, + >; + + /// 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 { + 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..498de7d069 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2533,5 +2533,33 @@ 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..1992d14b5f 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -293,5 +293,11 @@ 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, + /// 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 fe10bfec7a..2f74f0e764 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -570,5 +570,29 @@ 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, + }, + + /// 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/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 new file mode 100644 index 0000000000..d3ad0f2d26 --- /dev/null +++ b/pallets/subtensor/src/staking/lock.rs @@ -0,0 +1,322 @@ +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}; +use subtensor_runtime_common::NetUid; + +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).neg()) + .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) + } + } + + 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 + /// 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 (new_locked_mass, new_conviction) = + Self::calculate_decayed_mass_and_conviction(lock.locked_mass, lock.conviction, dt); + + LockState { + hotkey: lock.hotkey, + locked_mass: new_locked_mass, + conviction: new_conviction, + last_update: now, + } + } + + /// 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) + .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 + } + } + + /// 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. + 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, + }, + ); + } + } + + // Update the total hotkey lock + Self::upsert_hotkey_lock(hotkey, netuid, amount); + + Self::deposit_event(Event::StakeLocked { + coldkey: coldkey.clone(), + hotkey: hotkey.clone(), + netuid, + amount, + }); + + Ok(()) + } + + /// 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) { + 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 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) + } + } + + /// 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: BTreeMap = BTreeMap::new(); + + HotkeyLock::::iter_prefix(netuid).for_each(|(hotkey, lock)| { + let rolled = Self::roll_forward_hotkey_lock(lock, now); + let entry = scores + .entry(hotkey) + .or_insert_with(|| U64F64::saturating_from_num(0)); + *entry = entry.saturating_add(rolled.conviction); + }); + + scores + .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 + /// 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/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/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/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/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 5e22cc09ac..17f6650e8a 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -1157,6 +1157,9 @@ impl Pallet { Error::::HotKeyAccountNotExists ); + // Ensure that unstaked amount is not greater than available to unstake (due to locks) + Self::ensure_available_to_unstake(&coldkey, netuid, alpha_unstaked)?; + Ok(()) } @@ -1302,6 +1305,12 @@ 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 { + Self::ensure_available_to_unstake(origin_coldkey, origin_netuid, alpha_amount)?; + } + Ok(()) } 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() { diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs new file mode 100644 index 0000000000..2ea33c93d1 --- /dev/null +++ b/pallets/subtensor/src/tests/locks.rs @@ -0,0 +1,1586 @@ +#![allow( + clippy::expect_used, + clippy::unwrap_used, + clippy::arithmetic_side_effects +)] + +use approx::assert_abs_diff_eq; +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}; +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_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(|| { + 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_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 checks against lock +// ========================================================================= + +#[test] +fn test_recycle_alpha_checks_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 + ); + + // recycle_alpha checks lock and should fail if it would reduce alpha below locked amount + let recycle_amount = alpha / 2.into(); + assert_noop!( + SubtensorModule::do_recycle_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + recycle_amount, + netuid, + ), + Error::::CannotUnstakeLock + ); + + // 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); + }); +} + +#[test] +fn test_burn_alpha_checks_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); + + // 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_noop!( + SubtensorModule::do_burn_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + burn_amount, + netuid, + ), + Error::::CannotUnstakeLock + ); + + // 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); + }); +} + +// ========================================================================= +// 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 + ); + + // Lock entries are not orphaned + let lock = Lock::::get(coldkey, netuid); + assert!(lock.is_none()); + }); +} + +#[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_none()); + }); +} + +// ========================================================================= +// GROUP 16: Clear small nomination checks lock +// ========================================================================= + +#[test] +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); + 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_none()); + }); +} + +// ========================================================================= +// 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; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ed6d4d6176..514ee64214 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}; @@ -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, @@ -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 {