diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index ccf047b2b3..568bd23041 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -2219,6 +2219,36 @@ pub mod pallet { Ok(()) } + /// Set whether subnet owner cut is auto-locked for a subnet. + /// It is only callable by root and subnet owner. + #[pallet::call_index(95)] + #[pallet::weight(( + Weight::from_parts(25_000_000, 0) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(1)), + DispatchClass::Operational, + Pays::Yes, + ))] + pub fn sudo_set_owner_cut_auto_lock_enabled( + origin: OriginFor, + netuid: NetUid, + enabled: bool, + ) -> DispatchResult { + pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; + + ensure!( + pallet_subtensor::Pallet::::if_subnet_exist(netuid), + Error::::SubnetDoesNotExist + ); + ensure!(!netuid.is_root(), Error::::NotPermittedOnRootSubnet); + + pallet_subtensor::Pallet::::set_owner_cut_auto_lock_enabled(netuid, enabled); + log::debug!("OwnerCutAutoLockEnabledSet( netuid: {netuid:?}, enabled: {enabled:?} ) "); + + Ok(()) + } + /// Enables or disables subnet pool-side emission for a subnet. /// /// This does not remove the subnet from emission share calculation and does not diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index 61b9662492..67be797c78 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -2457,6 +2457,54 @@ fn test_sudo_set_owner_cut_enabled() { }); } +#[test] +fn test_sudo_set_owner_cut_auto_lock_enabled() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(11); + let owner = U256::from(1234); + let non_owner = U256::from(4321); + let call = RuntimeCall::AdminUtils(crate::Call::sudo_set_owner_cut_auto_lock_enabled { + netuid, + enabled: true, + }); + + add_network(netuid, 10); + SubnetOwner::::insert(netuid, owner); + + assert_ok!(AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::root(), + 0 + )); + + let dispatch_info = call.get_dispatch_info(); + assert_eq!(dispatch_info.pays_fee, Pays::Yes); + + assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + assert_noop!( + AdminUtils::sudo_set_owner_cut_auto_lock_enabled( + <::RuntimeOrigin>::signed(non_owner), + netuid, + true + ), + DispatchError::BadOrigin + ); + + assert_ok!(AdminUtils::sudo_set_owner_cut_auto_lock_enabled( + <::RuntimeOrigin>::signed(owner), + netuid, + false + )); + assert!(!SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + + assert_ok!(AdminUtils::sudo_set_owner_cut_auto_lock_enabled( + <::RuntimeOrigin>::root(), + netuid, + true + )); + assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + }); +} + // cargo test --package pallet-admin-utils --lib -- tests::test_sudo_set_mechanism_count_and_emissions --exact --show-output #[test] fn test_sudo_set_mechanism_count_and_emissions() { diff --git a/pallets/subtensor/rpc/src/lib.rs b/pallets/subtensor/rpc/src/lib.rs index 6feff774ad..f3123d1195 100644 --- a/pallets/subtensor/rpc/src/lib.rs +++ b/pallets/subtensor/rpc/src/lib.rs @@ -14,7 +14,7 @@ use subtensor_runtime_common::{MechId, NetUid, TaoBalance}; use sp_api::ProvideRuntimeApi; pub use subtensor_custom_rpc_runtime_api::{ - DelegateInfoRuntimeApi, NeuronInfoRuntimeApi, SubnetInfoRuntimeApi, + DelegateInfoRuntimeApi, NeuronInfoRuntimeApi, StakeInfoRuntimeApi, SubnetInfoRuntimeApi, SubnetRegistrationRuntimeApi, }; @@ -111,6 +111,13 @@ pub trait SubtensorCustomApi { fn get_subnet_to_prune(&self, at: Option) -> RpcResult>; #[method(name = "subnetInfo_getSubnetAccountId")] fn get_subnet_account_id(&self, netuid: NetUid, at: Option) -> RpcResult>; + #[method(name = "stakeInfo_getColdkeyLock")] + fn get_coldkey_lock( + &self, + coldkey: AccountId32, + netuid: NetUid, + at: Option, + ) -> RpcResult>; } pub struct SubtensorCustom { @@ -158,6 +165,7 @@ where C::Api: DelegateInfoRuntimeApi, C::Api: NeuronInfoRuntimeApi, C::Api: SubnetInfoRuntimeApi, + C::Api: StakeInfoRuntimeApi, C::Api: SubnetRegistrationRuntimeApi, { fn get_delegates(&self, at: Option<::Hash>) -> RpcResult> { @@ -547,4 +555,19 @@ where Err(_) => Err(Error::RuntimeError("Subnet does not exist".to_string()).into()), } } + + fn get_coldkey_lock( + &self, + coldkey: AccountId32, + netuid: NetUid, + at: Option<::Hash>, + ) -> RpcResult> { + let api = self.client.runtime_api(); + let at = at.unwrap_or_else(|| self.client.info().best_hash); + + match api.get_coldkey_lock(at, coldkey, netuid) { + Ok(result) => Ok(result.encode()), + Err(e) => Err(Error::RuntimeError(format!("Unable to get coldkey lock: {e:?}")).into()), + } + } } diff --git a/pallets/subtensor/runtime-api/src/lib.rs b/pallets/subtensor/runtime-api/src/lib.rs index 741facfc87..6efd548120 100644 --- a/pallets/subtensor/runtime-api/src/lib.rs +++ b/pallets/subtensor/runtime-api/src/lib.rs @@ -11,6 +11,7 @@ use pallet_subtensor::rpc_info::{ stake_info::StakeInfo, subnet_info::{SubnetHyperparams, SubnetHyperparamsV2, SubnetInfo, SubnetInfov2}, }; +use pallet_subtensor::staking::lock::LockState; use sp_runtime::AccountId32; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, TaoBalance}; @@ -57,6 +58,7 @@ 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_coldkey_lock(coldkey: AccountId32, netuid: NetUid) -> Option; fn get_hotkey_conviction(hotkey: AccountId32, netuid: NetUid) -> U64F64; fn get_most_convicted_hotkey_on_subnet(netuid: NetUid) -> Option; } diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index b44d76175a..b64043a4f5 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -360,6 +360,7 @@ impl Pallet { Yuma3On::::remove(netuid); AlphaValues::::remove(netuid); SubtokenEnabled::::remove(netuid); + OwnerCutAutoLockEnabled::::remove(netuid); ImmuneOwnerUidsLimit::::remove(netuid); // --- 18. Consensus aux vectors. diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 7c664fc1c1..934d47e34d 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1556,26 +1556,42 @@ pub mod pallet { OptionQuery, >; - /// --- MAP ( netuid ) --> LockState | Aggregate owner-coldkey lock for a subnet. + /// --- MAP ( netuid ) --> LockState | Total perpetual lock to the owner hotkey for a subnet. #[pallet::storage] pub type OwnerLock = StorageMap<_, Identity, NetUid, LockState, OptionQuery>; - /// --- DMAP ( coldkey, netuid ) --> false | When present, this coldkey's lock decays. - /// Missing entries mean the lock is perpetual. + /// --- MAP ( netuid ) --> LockState | Total decaying lock to the owner hotkey for a subnet. + #[pallet::storage] + pub type DecayingOwnerLock = StorageMap<_, Identity, NetUid, LockState, OptionQuery>; + + /// --- DMAP ( coldkey, netuid ) --> false | When present and false, this coldkey's lock is perpetual. + /// Missing entries mean the lock decays by default. #[pallet::storage] pub type DecayingLock = StorageDoubleMap<_, Blake2_128Concat, T::AccountId, Identity, NetUid, bool, OptionQuery>; - /// Default unlock timescale: 90% decay over ~365.25 days at 12s blocks. + /// Default value for owner cut auto-locking. + #[pallet::type_value] + pub fn DefaultOwnerCutAutoLockEnabled() -> bool { + true + } + + /// --- MAP ( netuid ) --> bool | Whether subnet owner cut should be auto-locked. + /// Missing entries default to true, so auto-locking is enabled unless explicitly disabled. + #[pallet::storage] + pub type OwnerCutAutoLockEnabled = + StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultOwnerCutAutoLockEnabled>; + + /// Default unlock timescale: 50% lock back in 1 month. #[pallet::type_value] pub fn DefaultUnlockRate() -> u64 { - 1_142_108 + 311_622 } - /// Default maturity timescale: Conviction is ~5.2x faster than the default unlock rate. + /// Default maturity timescale: 50% conviction in 1 month #[pallet::type_value] pub fn DefaultMaturityRate() -> u64 { - 216_000 + 311_622 } /// --- ITEM( maturity_rate ) | Decay timescale in blocks for lock conviction. diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 55f6bd84a9..8a0791b881 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -178,7 +178,9 @@ mod hooks { // Fix testnet Subtensor TotalIssuance after the EVM fees issue. .saturating_add(migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::()) // Remove deprecated conviction lock storage. - .saturating_add(migrations::migrate_remove_deprecated_conviction_maps::migrate_remove_deprecated_conviction_maps::()); + .saturating_add(migrations::migrate_remove_deprecated_conviction_maps::migrate_remove_deprecated_conviction_maps::()) + // Reset testnet conviction lock storage before deploying the current design. + .saturating_add(migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::()); weight } diff --git a/pallets/subtensor/src/migrations/migrate_reset_tnet_conviction_locks.rs b/pallets/subtensor/src/migrations/migrate_reset_tnet_conviction_locks.rs new file mode 100644 index 0000000000..277af2037e --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_reset_tnet_conviction_locks.rs @@ -0,0 +1,81 @@ +use super::*; +use frame_support::weights::Weight; +use scale_info::prelude::string::String; + +/// Clears conviction v2 lock state that only exists on testnet before this +/// conviction design is deployed more broadly. +/// +/// `devnet-ready` had `Lock`, `HotkeyLock`, `DecayingHotkeyLock`, `OwnerLock`, +/// and `DecayingLock`, but did not have `DecayingOwnerLock`. `OwnerLock` also +/// used the old owner-coldkey aggregate semantics. Clear these prefixes without +/// decoding values so old or incompatible aggregate bytes are removed safely. +pub fn migrate_reset_tnet_conviction_locks() -> Weight { + let migration_name = b"migrate_reset_tnet_conviction_locks".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name) + ); + + // This only affects testnet: mainnet has not had this conviction lock state + // deployed with live values yet. + let lock_removal = Lock::::clear(u32::MAX, None); + weight = weight.saturating_add( + T::DbWeight::get().reads_writes(lock_removal.loops as u64, lock_removal.backend as u64), + ); + + let hotkey_lock_removal = HotkeyLock::::clear(u32::MAX, None); + weight = weight.saturating_add(T::DbWeight::get().reads_writes( + hotkey_lock_removal.loops as u64, + hotkey_lock_removal.backend as u64, + )); + + let decaying_hotkey_lock_removal = DecayingHotkeyLock::::clear(u32::MAX, None); + weight = weight.saturating_add(T::DbWeight::get().reads_writes( + decaying_hotkey_lock_removal.loops as u64, + decaying_hotkey_lock_removal.backend as u64, + )); + + let owner_lock_removal = OwnerLock::::clear(u32::MAX, None); + weight = weight.saturating_add(T::DbWeight::get().reads_writes( + owner_lock_removal.loops as u64, + owner_lock_removal.backend as u64, + )); + + let decaying_owner_lock_removal = DecayingOwnerLock::::clear(u32::MAX, None); + weight = weight.saturating_add(T::DbWeight::get().reads_writes( + decaying_owner_lock_removal.loops as u64, + decaying_owner_lock_removal.backend as u64, + )); + + let decaying_lock_removal = DecayingLock::::clear(u32::MAX, None); + weight = weight.saturating_add(T::DbWeight::get().reads_writes( + decaying_lock_removal.loops as u64, + decaying_lock_removal.backend as u64, + )); + + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{:?}' completed successfully. Removed Lock: {:?}, HotkeyLock: {:?}, DecayingHotkeyLock: {:?}, OwnerLock: {:?}, DecayingOwnerLock: {:?}, DecayingLock: {:?}.", + String::from_utf8_lossy(&migration_name), + lock_removal.backend, + hotkey_lock_removal.backend, + decaying_hotkey_lock_removal.backend, + owner_lock_removal.backend, + decaying_owner_lock_removal.backend, + decaying_lock_removal.backend, + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index f582a631fc..ae0188ec63 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -49,6 +49,7 @@ pub mod migrate_remove_unused_maps_and_values; pub mod migrate_remove_zero_total_hotkey_alpha; pub mod migrate_reset_bonds_moving_average; pub mod migrate_reset_max_burn; +pub mod migrate_reset_tnet_conviction_locks; pub mod migrate_reset_unactive_sn; pub mod migrate_set_first_emission_block_number; pub mod migrate_set_min_burn; diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index d6fd16a483..27c5e5d646 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -22,66 +22,317 @@ pub struct LockState { pub last_update: u64, } -impl Pallet { - pub fn insert_lock_state( - coldkey: &T::AccountId, - netuid: NetUid, - hotkey: &T::AccountId, - lock_state: LockState, - ) { - if !lock_state.locked_mass.is_zero() - || lock_state.conviction > U64F64::saturating_from_num(0) - { - Lock::::insert((coldkey, netuid, hotkey), lock_state); - } else { - // If there is no record previously, this is a no-op - Lock::::remove((coldkey, netuid, hotkey)); +/// A struct that incapsulates Lock primitives such as adding, removing, +/// rolling, and updating aggregates. +/// +/// This model has one individual lock state, which relates to the stake owner +/// (locking coldkey) lock and 4 aggregates that are maintained in operations. +pub struct ConvictionModel { + /// Whether this model's individual lock targets the subnet owner hotkey. + owner_lock: bool, + /// Whether this model's individual lock uses the non-decaying lock mode. + perpetual_lock: bool, + /// Individual stake owner coldkey lock + individual_lock: LockState, + individual_lock_dirty: bool, + /// Perpetual non-owner aggregate + agg_perpetual_general: LockState, + agg_perpetual_general_dirty: bool, + /// Decaying non-owner aggregate + agg_decaying_general: LockState, + agg_decaying_general_dirty: bool, + /// Perpetual owner aggregate + agg_perpetual_owner: LockState, + agg_perpetual_owner_dirty: bool, + /// Decaying owner aggregate + agg_decaying_owner: LockState, + agg_decaying_owner_dirty: bool, +} + +impl ConvictionModel { + pub fn new( + owner_lock: bool, + perpetual_lock: bool, + individual_lock: LockState, + agg_perpetual_general: LockState, + agg_decaying_general: LockState, + agg_perpetual_owner: LockState, + agg_decaying_owner: LockState, + ) -> Self { + Self { + owner_lock, + perpetual_lock, + individual_lock, + individual_lock_dirty: false, + agg_perpetual_general, + agg_perpetual_general_dirty: false, + agg_decaying_general, + agg_decaying_general_dirty: false, + agg_perpetual_owner, + agg_perpetual_owner_dirty: false, + agg_decaying_owner, + agg_decaying_owner_dirty: false, } } - pub fn insert_hotkey_lock_state(netuid: NetUid, hotkey: &T::AccountId, lock_state: LockState) { - if !lock_state.locked_mass.is_zero() - || lock_state.conviction > U64F64::saturating_from_num(0) - { - HotkeyLock::::insert(netuid, hotkey, lock_state); + pub fn roll_forward(&mut self, now: u64, unlock_rate: u64, maturity_rate: u64) { + self.individual_lock = Self::roll_forward_lock( + self.individual_lock.clone(), + now, + unlock_rate, + maturity_rate, + self.owner_lock, + self.perpetual_lock, + ); + self.individual_lock_dirty = true; + self.agg_perpetual_general = Self::roll_forward_lock( + self.agg_perpetual_general.clone(), + now, + unlock_rate, + maturity_rate, + false, + true, + ); + self.agg_perpetual_general_dirty = true; + self.agg_decaying_general = Self::roll_forward_lock( + self.agg_decaying_general.clone(), + now, + unlock_rate, + maturity_rate, + false, + false, + ); + self.agg_decaying_general_dirty = true; + self.agg_perpetual_owner = Self::roll_forward_lock( + self.agg_perpetual_owner.clone(), + now, + unlock_rate, + maturity_rate, + true, + true, + ); + self.agg_perpetual_owner_dirty = true; + self.agg_decaying_owner = Self::roll_forward_lock( + self.agg_decaying_owner.clone(), + now, + unlock_rate, + maturity_rate, + true, + false, + ); + self.agg_decaying_owner_dirty = true; + } + + pub fn individual_lock(&self) -> &LockState { + &self.individual_lock + } + + pub fn agg_perpetual_general(&self) -> &LockState { + &self.agg_perpetual_general + } + + pub fn agg_decaying_general(&self) -> &LockState { + &self.agg_decaying_general + } + + pub fn agg_perpetual_owner(&self) -> &LockState { + &self.agg_perpetual_owner + } + + pub fn agg_decaying_owner(&self) -> &LockState { + &self.agg_decaying_owner + } + + pub fn aggregate_lock(&self) -> &LockState { + if self.owner_lock && self.perpetual_lock { + &self.agg_perpetual_owner + } else if self.owner_lock { + &self.agg_decaying_owner + } else if self.perpetual_lock { + &self.agg_perpetual_general } else { - HotkeyLock::::remove(netuid, hotkey); + &self.agg_decaying_general } } - pub fn insert_decaying_hotkey_lock_state( - netuid: NetUid, - hotkey: &T::AccountId, - lock_state: LockState, + pub fn individual_lock_dirty(&self) -> bool { + self.individual_lock_dirty + } + + pub fn agg_perpetual_general_dirty(&self) -> bool { + self.agg_perpetual_general_dirty + } + + pub fn agg_decaying_general_dirty(&self) -> bool { + self.agg_decaying_general_dirty + } + + pub fn agg_perpetual_owner_dirty(&self) -> bool { + self.agg_perpetual_owner_dirty + } + + pub fn agg_decaying_owner_dirty(&self) -> bool { + self.agg_decaying_owner_dirty + } + + pub fn merge(&mut self, conv: &ConvictionModel) { + self.individual_lock = Self::merge_lock(&self.individual_lock, &conv.individual_lock); + self.individual_lock_dirty = true; + self.agg_perpetual_general = + Self::merge_lock(&self.agg_perpetual_general, &conv.agg_perpetual_general); + self.agg_perpetual_general_dirty = true; + self.agg_decaying_general = + Self::merge_lock(&self.agg_decaying_general, &conv.agg_decaying_general); + self.agg_decaying_general_dirty = true; + self.agg_perpetual_owner = + Self::merge_lock(&self.agg_perpetual_owner, &conv.agg_perpetual_owner); + self.agg_perpetual_owner_dirty = true; + self.agg_decaying_owner = + Self::merge_lock(&self.agg_decaying_owner, &conv.agg_decaying_owner); + self.agg_decaying_owner_dirty = true; + } + + pub fn set_individual_lock(&mut self, lock: LockState) { + self.individual_lock = lock; + self.individual_lock_dirty = true; + } + + pub fn set_rolled_individual_lock( + &mut self, + lock: LockState, + now: u64, + unlock_rate: u64, + maturity_rate: u64, ) { - if !lock_state.locked_mass.is_zero() - || lock_state.conviction > U64F64::saturating_from_num(0) - { - DecayingHotkeyLock::::insert(netuid, hotkey, lock_state); + self.individual_lock = Self::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + self.owner_lock, + self.perpetual_lock, + ); + self.individual_lock_dirty = true; + } + + pub fn roll_forward_individual(&mut self, now: u64, unlock_rate: u64, maturity_rate: u64) { + self.individual_lock = Self::roll_forward_lock( + self.individual_lock.clone(), + now, + unlock_rate, + maturity_rate, + self.owner_lock, + self.perpetual_lock, + ); + self.individual_lock_dirty = true; + } + + pub fn roll_forward_aggregate(&mut self, now: u64, unlock_rate: u64, maturity_rate: u64) { + let owner_lock = self.owner_lock; + let perpetual_lock = self.perpetual_lock; + let (aggregate, aggregate_dirty) = self.aggregate_mut(); + *aggregate = Self::roll_forward_lock( + aggregate.clone(), + now, + unlock_rate, + maturity_rate, + owner_lock, + perpetual_lock, + ); + *aggregate_dirty = true; + } + + pub fn add_to_aggregate(&mut self, added: &LockState) { + let (aggregate, aggregate_dirty) = self.aggregate_mut(); + *aggregate = Self::merge_lock(aggregate, added); + *aggregate_dirty = true; + } + + pub fn reduce_aggregate(&mut self, locked_mass: AlphaBalance, conviction: U64F64) { + let (aggregate, aggregate_dirty) = self.aggregate_mut(); + *aggregate = Self::reduce_lock(aggregate, locked_mass, conviction); + *aggregate_dirty = true; + } + + pub fn reduce(&mut self, locked_mass: AlphaBalance, conviction: U64F64) { + self.individual_lock = Self::reduce_lock(&self.individual_lock, locked_mass, conviction); + self.individual_lock_dirty = true; + + let (aggregate, aggregate_dirty) = self.aggregate_mut(); + *aggregate = Self::reduce_lock(aggregate, locked_mass, conviction); + *aggregate_dirty = true; + } + + pub fn force_reduce_individual(&mut self, amount: AlphaBalance, now: u64) { + let rolled = self.individual_lock.clone(); + let new_locked_mass = rolled.locked_mass.saturating_sub(amount); + let locked_mass_diff = rolled.locked_mass.saturating_sub(new_locked_mass); + + let conviction_diff = if new_locked_mass.is_zero() { + self.individual_lock = LockState { + locked_mass: AlphaBalance::ZERO, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }; + rolled.conviction } else { - DecayingHotkeyLock::::remove(netuid, hotkey); - } + let removed_proportion = U64F64::saturating_from_num(u64::from(amount)) + .safe_div(U64F64::saturating_from_num(u64::from(rolled.locked_mass))); + let new_conviction = rolled + .conviction + .saturating_mul(U64F64::saturating_from_num(1).saturating_sub(removed_proportion)); + self.individual_lock = LockState { + locked_mass: new_locked_mass, + conviction: new_conviction, + last_update: now, + }; + rolled.conviction.saturating_sub(new_conviction) + }; + self.individual_lock_dirty = true; + + self.reduce_aggregate(locked_mass_diff, conviction_diff); } - pub fn insert_owner_lock_state(netuid: NetUid, lock_state: LockState) { - if !lock_state.locked_mass.is_zero() - || lock_state.conviction > U64F64::saturating_from_num(0) - { - OwnerLock::::insert(netuid, lock_state); + fn aggregate_mut(&mut self) -> (&mut LockState, &mut bool) { + if self.owner_lock && self.perpetual_lock { + ( + &mut self.agg_perpetual_owner, + &mut self.agg_perpetual_owner_dirty, + ) + } else if self.owner_lock { + ( + &mut self.agg_decaying_owner, + &mut self.agg_decaying_owner_dirty, + ) + } else if self.perpetual_lock { + ( + &mut self.agg_perpetual_general, + &mut self.agg_perpetual_general_dirty, + ) } else { - OwnerLock::::remove(netuid); + ( + &mut self.agg_decaying_general, + &mut self.agg_decaying_general_dirty, + ) } } - fn is_subnet_owner_coldkey(netuid: NetUid, coldkey: &T::AccountId) -> bool { - coldkey == &SubnetOwner::::get(netuid) + fn merge_lock(lhs: &LockState, rhs: &LockState) -> LockState { + LockState { + locked_mass: lhs.locked_mass.saturating_add(rhs.locked_mass), + conviction: lhs.conviction.saturating_add(rhs.conviction), + last_update: lhs.last_update.max(rhs.last_update), + } } - fn is_perpetual_lock(coldkey: &T::AccountId, netuid: NetUid) -> bool { - !DecayingLock::::contains_key(coldkey, netuid) + fn reduce_lock(lock: &LockState, locked_mass: AlphaBalance, conviction: U64F64) -> LockState { + LockState { + locked_mass: lock.locked_mass.saturating_sub(locked_mass), + conviction: lock.conviction.saturating_sub(conviction), + last_update: lock.last_update, + } } - /// 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 { @@ -106,11 +357,10 @@ impl Pallet { locked_mass: AlphaBalance, conviction: U64F64, dt: u64, + unlock_rate: u64, + maturity_rate: u64, perpetual_lock: bool, ) -> (AlphaBalance, U64F64) { - let unlock_rate = UnlockRate::::get(); - let maturity_rate = MaturityRate::::get(); - let unlock_decay = Self::exp_decay(dt, unlock_rate); let maturity_decay = Self::exp_decay(dt, maturity_rate); let mass_fixed = U64F64::saturating_from_num(locked_mass); @@ -157,13 +407,11 @@ impl Pallet { (new_locked_mass, new_conviction) } - /// Rolls a LockState forward to `now` using exponential decay. - /// - /// X_new = decay * X_old - /// Z_new = decay_Z * Z_old + gamma * X_old pub fn roll_forward_lock( lock: LockState, now: u64, + unlock_rate: u64, + maturity_rate: u64, owner_lock: bool, perpetual_lock: bool, ) -> LockState { @@ -173,6 +421,8 @@ impl Pallet { lock.locked_mass, lock.conviction, dt, + unlock_rate, + maturity_rate, perpetual_lock, ); @@ -191,38 +441,140 @@ impl Pallet { rolled } +} - pub fn roll_forward_individual_lock( +impl Pallet { + pub fn insert_lock_state( coldkey: &T::AccountId, netuid: NetUid, - lock: LockState, - now: u64, - ) -> LockState { - let owner_lock = Self::is_subnet_owner_coldkey(netuid, coldkey); - let perpetual_lock = Self::is_perpetual_lock(coldkey, netuid); - Self::roll_forward_lock(lock, now, owner_lock, perpetual_lock) + hotkey: &T::AccountId, + lock_state: LockState, + ) { + if !lock_state.locked_mass.is_zero() + || lock_state.conviction > U64F64::saturating_from_num(0) + { + Lock::::insert((coldkey, netuid, hotkey), lock_state); + } else { + // If there is no record previously, this is a no-op + Lock::::remove((coldkey, netuid, hotkey)); + } } - pub fn roll_forward_owner_lock(netuid: NetUid, lock: LockState, now: u64) -> LockState { - let owner_coldkey = SubnetOwner::::get(netuid); - Self::roll_forward_lock( - lock, - now, - true, - Self::is_perpetual_lock(&owner_coldkey, netuid), - ) + pub fn insert_hotkey_lock_state(netuid: NetUid, hotkey: &T::AccountId, lock_state: LockState) { + if !lock_state.locked_mass.is_zero() + || lock_state.conviction > U64F64::saturating_from_num(0) + { + HotkeyLock::::insert(netuid, hotkey, lock_state); + } else { + HotkeyLock::::remove(netuid, hotkey); + } } - pub fn roll_forward_hotkey_lock(_netuid: NetUid, lock: LockState, now: u64) -> LockState { - Self::roll_forward_lock(lock, now, false, true) + pub fn insert_decaying_hotkey_lock_state( + netuid: NetUid, + hotkey: &T::AccountId, + lock_state: LockState, + ) { + if !lock_state.locked_mass.is_zero() + || lock_state.conviction > U64F64::saturating_from_num(0) + { + DecayingHotkeyLock::::insert(netuid, hotkey, lock_state); + } else { + DecayingHotkeyLock::::remove(netuid, hotkey); + } } - pub fn roll_forward_decaying_hotkey_lock( - _netuid: NetUid, - lock: LockState, + pub fn insert_owner_lock_state(netuid: NetUid, lock_state: LockState) { + if !lock_state.locked_mass.is_zero() + || lock_state.conviction > U64F64::saturating_from_num(0) + { + OwnerLock::::insert(netuid, lock_state); + } else { + OwnerLock::::remove(netuid); + } + } + + pub fn insert_decaying_owner_lock_state(netuid: NetUid, lock_state: LockState) { + if !lock_state.locked_mass.is_zero() + || lock_state.conviction > U64F64::saturating_from_num(0) + { + DecayingOwnerLock::::insert(netuid, lock_state); + } else { + DecayingOwnerLock::::remove(netuid); + } + } + + fn is_subnet_owner_hotkey(netuid: NetUid, hotkey: &T::AccountId) -> bool { + hotkey == &SubnetOwnerHotkey::::get(netuid) + } + + fn is_perpetual_lock(coldkey: &T::AccountId, netuid: NetUid) -> bool { + DecayingLock::::get(coldkey, netuid) == Some(false) + } + + fn empty_lock(now: u64) -> LockState { + LockState { + locked_mass: AlphaBalance::ZERO, + conviction: U64F64::saturating_from_num(0), + last_update: now, + } + } + + fn read_conviction_model_for_hotkey( + coldkey: &T::AccountId, + netuid: NetUid, + hotkey: &T::AccountId, now: u64, - ) -> LockState { - Self::roll_forward_lock(lock, now, false, false) + ) -> ConvictionModel { + ConvictionModel::new( + Self::is_subnet_owner_hotkey(netuid, hotkey), + Self::is_perpetual_lock(coldkey, netuid), + Lock::::get((coldkey, netuid, hotkey)).unwrap_or_else(|| Self::empty_lock(now)), + HotkeyLock::::get(netuid, hotkey).unwrap_or_else(|| Self::empty_lock(now)), + DecayingHotkeyLock::::get(netuid, hotkey).unwrap_or_else(|| Self::empty_lock(now)), + OwnerLock::::get(netuid).unwrap_or_else(|| Self::empty_lock(now)), + DecayingOwnerLock::::get(netuid).unwrap_or_else(|| Self::empty_lock(now)), + ) + } + + fn read_conviction_model( + coldkey: &T::AccountId, + netuid: NetUid, + now: u64, + ) -> Option<(T::AccountId, ConvictionModel)> { + Lock::::iter_prefix((coldkey, netuid)) + .next() + .map(|(hotkey, _lock)| { + let model = Self::read_conviction_model_for_hotkey(coldkey, netuid, &hotkey, now); + (hotkey, model) + }) + } + + fn save_conviction_model( + coldkey: &T::AccountId, + netuid: NetUid, + hotkey: &T::AccountId, + model: ConvictionModel, + ) { + if model.individual_lock_dirty() { + Self::insert_lock_state(coldkey, netuid, hotkey, model.individual_lock().clone()); + } + if model.agg_perpetual_general_dirty() { + Self::insert_hotkey_lock_state(netuid, hotkey, model.agg_perpetual_general().clone()); + } + if model.agg_decaying_general_dirty() { + Self::insert_decaying_hotkey_lock_state( + netuid, + hotkey, + model.agg_decaying_general().clone(), + ); + } + if model.agg_perpetual_owner_dirty() { + Self::insert_owner_lock_state(netuid, model.agg_perpetual_owner().clone()); + } + if model.agg_decaying_owner_dirty() { + Self::insert_decaying_owner_lock_state(netuid, model.agg_decaying_owner().clone()); + } } pub fn do_set_perpetual_lock( @@ -233,9 +585,10 @@ impl Pallet { let now = Self::get_current_block_as_u64(); let current_enabled = Self::is_perpetual_lock(coldkey, netuid); - if let Some((hotkey, lock)) = Lock::::iter_prefix((coldkey, netuid)).next() { - let rolled = Self::roll_forward_individual_lock(coldkey, netuid, lock, now); - Self::insert_lock_state(coldkey, netuid, &hotkey, rolled.clone()); + if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { + model.roll_forward_individual(now, UnlockRate::::get(), MaturityRate::::get()); + let rolled = model.individual_lock().clone(); + Self::save_conviction_model(coldkey, netuid, &hotkey, model); if current_enabled != enabled { Self::reduce_aggregate_lock( @@ -249,15 +602,15 @@ impl Pallet { } if enabled { - DecayingLock::::remove(coldkey, netuid); - } else { DecayingLock::::insert(coldkey, netuid, false); + } else { + DecayingLock::::remove(coldkey, netuid); } if current_enabled != enabled - && let Some((hotkey, lock)) = Lock::::iter_prefix((coldkey, netuid)).next() + && let Some((hotkey, model)) = Self::read_conviction_model(coldkey, netuid, now) { - Self::add_aggregate_lock(coldkey, &hotkey, netuid, lock); + Self::add_aggregate_lock(coldkey, &hotkey, netuid, model.individual_lock().clone()); } Self::deposit_event(Event::PerpetualLockUpdated { coldkey: coldkey.clone(), @@ -280,10 +633,14 @@ impl Pallet { /// Returns the current locked amount for a coldkey on a subnet. pub fn get_current_locked(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { let now = Self::get_current_block_as_u64(); - Lock::::iter_prefix((coldkey, netuid)) - .next() - .map(|(_hotkey, lock)| { - Self::roll_forward_individual_lock(coldkey, netuid, lock, now).locked_mass + Self::read_conviction_model(coldkey, netuid, now) + .map(|(_hotkey, mut model)| { + model.roll_forward_individual( + now, + UnlockRate::::get(), + MaturityRate::::get(), + ); + model.individual_lock().locked_mass }) .unwrap_or(AlphaBalance::ZERO) } @@ -291,14 +648,27 @@ impl Pallet { /// 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(); - Lock::::iter_prefix((coldkey, netuid)) - .next() - .map(|(_hotkey, lock)| { - Self::roll_forward_individual_lock(coldkey, netuid, lock, now).conviction + Self::read_conviction_model(coldkey, netuid, now) + .map(|(_hotkey, mut model)| { + model.roll_forward_individual( + now, + UnlockRate::::get(), + MaturityRate::::get(), + ); + model.individual_lock().conviction }) .unwrap_or_else(|| U64F64::saturating_from_num(0)) } + /// Returns the current lock for a coldkey on a subnet, rolled forward to now. + pub fn get_coldkey_lock(coldkey: &T::AccountId, netuid: NetUid) -> Option { + let now = Self::get_current_block_as_u64(); + Self::read_conviction_model(coldkey, netuid, now).map(|(_hotkey, mut model)| { + model.roll_forward_individual(now, UnlockRate::::get(), MaturityRate::::get()); + model.individual_lock().clone() + }) + } + /// 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); @@ -339,39 +709,53 @@ impl Pallet { let total = Self::total_coldkey_alpha_on_subnet(coldkey, netuid); let now = Self::get_current_block_as_u64(); - let existing = Lock::::iter_prefix((coldkey, netuid)).next(); - - match existing { - None => { - ensure!(total >= amount, Error::::InsufficientStakeForLock); - - let lock = Self::roll_forward_individual_lock( - coldkey, - netuid, - LockState { - locked_mass: amount, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }, - now, - ); - Self::insert_lock_state(coldkey, netuid, hotkey, lock); - } - Some((existing_hotkey, existing)) => { + let mut model = match Self::read_conviction_model(coldkey, netuid, now) { + Some((existing_hotkey, model)) => { ensure!(*hotkey == existing_hotkey, Error::::LockHotkeyMismatch); - - let mut lock = Self::roll_forward_individual_lock(coldkey, netuid, existing, now); - lock.locked_mass = lock.locked_mass.saturating_add(amount); - ensure!( - total >= lock.locked_mass, - Error::::InsufficientStakeForLock - ); - let lock = Self::roll_forward_individual_lock(coldkey, netuid, lock, now); - Self::insert_lock_state(coldkey, netuid, hotkey, lock); + model } + None => Self::read_conviction_model_for_hotkey(coldkey, netuid, hotkey, now), + }; + model.roll_forward_individual(now, UnlockRate::::get(), MaturityRate::::get()); + + if model.individual_lock().locked_mass.is_zero() + && model.individual_lock().conviction == U64F64::saturating_from_num(0) + { + ensure!(total >= amount, Error::::InsufficientStakeForLock); + + model.set_rolled_individual_lock( + LockState { + locked_mass: amount, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }, + now, + UnlockRate::::get(), + MaturityRate::::get(), + ); + } else { + let mut lock = model.individual_lock().clone(); + lock.locked_mass = lock.locked_mass.saturating_add(amount); + ensure!( + total >= lock.locked_mass, + Error::::InsufficientStakeForLock + ); + model.set_rolled_individual_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + ); } - Self::upsert_aggregate_lock(coldkey, hotkey, netuid, amount); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + model.add_to_aggregate(&LockState { + locked_mass: amount, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + Self::save_conviction_model(coldkey, netuid, hotkey, model); Self::deposit_event(Event::StakeLocked { coldkey: coldkey.clone(), @@ -386,41 +770,12 @@ impl Pallet { /// Reduces the coldkey lock by a specified alpha amount and the coldkey conviction /// proportionally. pub fn force_reduce_lock(coldkey: &T::AccountId, netuid: NetUid, amount: AlphaBalance) { - if let Some((existing_hotkey, lock)) = Lock::::iter_prefix((coldkey, netuid)).next() { - let now = Self::get_current_block_as_u64(); - let rolled = Self::roll_forward_individual_lock(coldkey, netuid, lock, now); - let new_locked_mass = rolled.locked_mass.saturating_sub(amount); - let locked_mass_diff = rolled.locked_mass.saturating_sub(new_locked_mass); - - // Remove or update lock - let conviction_diff = if new_locked_mass.is_zero() { - Lock::::remove((coldkey.clone(), netuid, existing_hotkey.clone())); - rolled.conviction - } else { - let removed_proportion = U64F64::saturating_from_num(u64::from(amount)) - .safe_div(U64F64::saturating_from_num(u64::from(rolled.locked_mass))); - let new_conviction = rolled.conviction.saturating_mul( - U64F64::saturating_from_num(1).saturating_sub(removed_proportion), - ); - Lock::::insert( - (coldkey.clone(), netuid, existing_hotkey.clone()), - LockState { - locked_mass: new_locked_mass, - conviction: new_conviction, - last_update: now, - }, - ); - rolled.conviction.saturating_sub(new_conviction) - }; - - // Reduce the total hotkey lock by the rolled locked mass and conviction - Self::reduce_aggregate_lock( - coldkey, - &existing_hotkey, - netuid, - locked_mass_diff, - conviction_diff, - ); + let now = Self::get_current_block_as_u64(); + if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { + model.roll_forward_individual(now, UnlockRate::::get(), MaturityRate::::get()); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + model.force_reduce_individual(amount, now); + Self::save_conviction_model(coldkey, netuid, &hotkey, model); } } @@ -430,31 +785,18 @@ impl Pallet { let now = Self::get_current_block_as_u64(); // Cleanup locks for the specific coldkey and hotkey - if let Some((hotkey, lock)) = Lock::::iter_prefix((coldkey.clone(), netuid)).next() { - let rolled = Self::roll_forward_individual_lock(coldkey, netuid, lock, now); + if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { + model.roll_forward_individual(now, UnlockRate::::get(), MaturityRate::::get()); + let rolled = model.individual_lock().clone(); if rolled.locked_mass.is_zero() { - Lock::::remove((coldkey.clone(), netuid, hotkey.clone())); - } - - if Self::is_subnet_owner_coldkey(netuid, coldkey) { - if let Some(lock) = OwnerLock::::get(netuid) { - let rolled = Self::roll_forward_owner_lock(netuid, lock, now); - if rolled.locked_mass.is_zero() { - OwnerLock::::remove(netuid); - } - } - } else if Self::is_perpetual_lock(coldkey, netuid) { - if let Some(lock) = HotkeyLock::::get(netuid, &hotkey) { - let rolled = Self::roll_forward_hotkey_lock(netuid, lock, now); - if rolled.locked_mass.is_zero() { - HotkeyLock::::remove(netuid, hotkey); - } - } - } else if let Some(lock) = DecayingHotkeyLock::::get(netuid, &hotkey) { - let rolled = Self::roll_forward_decaying_hotkey_lock(netuid, lock, now); - if rolled.locked_mass.is_zero() { - DecayingHotkeyLock::::remove(netuid, hotkey); - } + model.set_individual_lock(LockState { + locked_mass: AlphaBalance::ZERO, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + model.reduce_aggregate(rolled.locked_mass, rolled.conviction); + Self::save_conviction_model(coldkey, netuid, &hotkey, model); } } } @@ -471,44 +813,16 @@ impl Pallet { amount: AlphaBalance, ) { let now = Self::get_current_block_as_u64(); - let owner_lock = Self::is_subnet_owner_coldkey(netuid, coldkey); - let perpetual_lock = Self::is_perpetual_lock(coldkey, netuid); - - let rolled_lock = if owner_lock { - OwnerLock::::get(netuid).map(|lock| Self::roll_forward_owner_lock(netuid, lock, now)) - } else if perpetual_lock { - HotkeyLock::::get(netuid, hotkey) - .map(|lock| Self::roll_forward_hotkey_lock(netuid, lock, now)) - } else { - DecayingHotkeyLock::::get(netuid, hotkey) - .map(|lock| Self::roll_forward_decaying_hotkey_lock(netuid, lock, now)) - } - .unwrap_or(LockState { - locked_mass: 0.into(), - conviction: U64F64::saturating_from_num(0), - last_update: now, - }); - - let new_lock = LockState { - locked_mass: rolled_lock.locked_mass.saturating_add(amount), - conviction: rolled_lock.conviction, - last_update: now, - }; - let new_lock = if owner_lock { - Self::roll_forward_owner_lock(netuid, new_lock, now) - } else if perpetual_lock { - Self::roll_forward_hotkey_lock(netuid, new_lock, now) - } else { - Self::roll_forward_decaying_hotkey_lock(netuid, new_lock, now) - }; - - if owner_lock { - Self::insert_owner_lock_state(netuid, new_lock); - } else if perpetual_lock { - Self::insert_hotkey_lock_state(netuid, hotkey, new_lock); - } else { - Self::insert_decaying_hotkey_lock_state(netuid, hotkey, new_lock); - } + Self::add_aggregate_lock( + coldkey, + hotkey, + netuid, + LockState { + locked_mass: amount, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }, + ); } /// Merges an already-existing lock state into the aggregate lock bucket. @@ -518,8 +832,8 @@ impl Pallet { /// both locked mass and conviction from the moved lock because that conviction /// was already earned before the aggregate bucket changed. /// - /// Owner coldkey locks are merged into `OwnerLock`; all other locks are merged - /// into `HotkeyLock` for the destination hotkey. + /// Locks to the subnet owner hotkey are merged into `OwnerLock`; all other + /// locks are merged into the destination hotkey's perpetual or decaying bucket. fn add_aggregate_lock( coldkey: &T::AccountId, hotkey: &T::AccountId, @@ -527,67 +841,15 @@ impl Pallet { added: LockState, ) { let now = Self::get_current_block_as_u64(); - if Self::is_subnet_owner_coldkey(netuid, coldkey) { - let current = OwnerLock::::get(netuid) - .map(|lock| Self::roll_forward_owner_lock(netuid, lock, now)) - .unwrap_or(LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }); - let merged = LockState { - locked_mass: current.locked_mass.saturating_add(added.locked_mass), - conviction: current.conviction.saturating_add(added.conviction), - last_update: now, - }; - Self::insert_owner_lock_state( - netuid, - Self::roll_forward_owner_lock(netuid, merged, now), - ); - } else if Self::is_perpetual_lock(coldkey, netuid) { - let current = HotkeyLock::::get(netuid, hotkey) - .map(|lock| Self::roll_forward_hotkey_lock(netuid, lock, now)) - .unwrap_or(LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }); - let merged = LockState { - locked_mass: current.locked_mass.saturating_add(added.locked_mass), - conviction: current.conviction.saturating_add(added.conviction), - last_update: now, - }; - Self::insert_hotkey_lock_state( - netuid, - hotkey, - Self::roll_forward_hotkey_lock(netuid, merged, now), - ); - } else { - let current = DecayingHotkeyLock::::get(netuid, hotkey) - .map(|lock| Self::roll_forward_decaying_hotkey_lock(netuid, lock, now)) - .unwrap_or(LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }); - let merged = LockState { - locked_mass: current.locked_mass.saturating_add(added.locked_mass), - conviction: current.conviction.saturating_add(added.conviction), - last_update: now, - }; - Self::insert_decaying_hotkey_lock_state( - netuid, - hotkey, - Self::roll_forward_decaying_hotkey_lock(netuid, merged, now), - ); - } + let mut model = Self::read_conviction_model_for_hotkey(coldkey, netuid, hotkey, now); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + model.add_to_aggregate(&added); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + Self::save_conviction_model(coldkey, netuid, hotkey, model); } - /// Reduce the aggregate lock bucket for a coldkey's current lock mode. - /// - /// Owner coldkey locks reduce `OwnerLock`; perpetual non-owner locks reduce - /// `HotkeyLock`; decaying non-owner locks reduce `DecayingHotkeyLock`. - pub fn reduce_aggregate_lock( + /// Reduces locked mass and conviction from exactly one aggregate bucket. + fn reduce_aggregate_lock( coldkey: &T::AccountId, hotkey: &T::AccountId, netuid: NetUid, @@ -595,62 +857,75 @@ impl Pallet { conviction: U64F64, ) { let now = Self::get_current_block_as_u64(); - if Self::is_subnet_owner_coldkey(netuid, coldkey) { - if let Some(lock) = OwnerLock::::get(netuid) { - let rolled = Self::roll_forward_owner_lock(netuid, lock, now); - Self::insert_owner_lock_state( - netuid, - LockState { - locked_mass: rolled.locked_mass.saturating_sub(amount), - conviction: rolled.conviction.saturating_sub(conviction), - last_update: now, - }, - ); - } - } else if Self::is_perpetual_lock(coldkey, netuid) { - if let Some(lock) = HotkeyLock::::get(netuid, hotkey) { - let rolled = Self::roll_forward_hotkey_lock(netuid, lock, now); - Self::insert_hotkey_lock_state( - netuid, - hotkey, - LockState { - locked_mass: rolled.locked_mass.saturating_sub(amount), - conviction: rolled.conviction.saturating_sub(conviction), - last_update: now, - }, - ); - } - } else if let Some(lock) = DecayingHotkeyLock::::get(netuid, hotkey) { - let rolled = Self::roll_forward_decaying_hotkey_lock(netuid, lock, now); - Self::insert_decaying_hotkey_lock_state( - netuid, - hotkey, - LockState { - locked_mass: rolled.locked_mass.saturating_sub(amount), - conviction: rolled.conviction.saturating_sub(conviction), - last_update: now, - }, - ); - } + let mut model = Self::read_conviction_model_for_hotkey(coldkey, netuid, hotkey, now); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + model.reduce_aggregate(amount, conviction); + Self::save_conviction_model(coldkey, netuid, hotkey, model); } /// 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 unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); let perpetual_conviction = HotkeyLock::::get(netuid, hotkey) - .map(|lock| Self::roll_forward_hotkey_lock(netuid, lock, now).conviction) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ) + .conviction + }) .unwrap_or_else(|| U64F64::saturating_from_num(0)); let decaying_conviction = DecayingHotkeyLock::::get(netuid, hotkey) - .map(|lock| Self::roll_forward_decaying_hotkey_lock(netuid, lock, now).conviction) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ) + .conviction + }) .unwrap_or_else(|| U64F64::saturating_from_num(0)); let hotkey_conviction = perpetual_conviction.saturating_add(decaying_conviction); if hotkey == &SubnetOwnerHotkey::::get(netuid) { - hotkey_conviction.saturating_add( - OwnerLock::::get(netuid) - .map(|lock| Self::roll_forward_owner_lock(netuid, lock, now).conviction) - .unwrap_or_else(|| U64F64::saturating_from_num(0)), - ) + let owner_conviction = OwnerLock::::get(netuid) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ) + .conviction + }) + .unwrap_or_else(|| U64F64::saturating_from_num(0)); + let decaying_owner_conviction = DecayingOwnerLock::::get(netuid) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ) + .conviction + }) + .unwrap_or_else(|| U64F64::saturating_from_num(0)); + hotkey_conviction + .saturating_add(owner_conviction) + .saturating_add(decaying_owner_conviction) } else { hotkey_conviction } @@ -659,41 +934,101 @@ impl Pallet { /// Returns total rolled aggregate conviction across all hotkey and owner locks on a subnet. pub fn get_total_conviction(netuid: NetUid) -> U64F64 { let now = Self::get_current_block_as_u64(); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); let hotkey_conviction = HotkeyLock::::iter_prefix(netuid) - .map(|(_hotkey, lock)| Self::roll_forward_hotkey_lock(netuid, lock, now).conviction) + .map(|(_hotkey, lock)| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ) + .conviction + }) .fold(U64F64::saturating_from_num(0), |acc, conviction| { acc.saturating_add(conviction) }); let decaying_hotkey_conviction = DecayingHotkeyLock::::iter_prefix(netuid) .map(|(_hotkey, lock)| { - Self::roll_forward_decaying_hotkey_lock(netuid, lock, now).conviction + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ) + .conviction }) .fold(U64F64::saturating_from_num(0), |acc, conviction| { acc.saturating_add(conviction) }); let owner_conviction = OwnerLock::::get(netuid) - .map(|lock| Self::roll_forward_owner_lock(netuid, lock, now).conviction) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ) + .conviction + }) + .unwrap_or_else(|| U64F64::saturating_from_num(0)); + let decaying_owner_conviction = DecayingOwnerLock::::get(netuid) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ) + .conviction + }) .unwrap_or_else(|| U64F64::saturating_from_num(0)); hotkey_conviction .saturating_add(decaying_hotkey_conviction) .saturating_add(owner_conviction) + .saturating_add(decaying_owner_conviction) } /// 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 unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); let mut scores: BTreeMap = BTreeMap::new(); HotkeyLock::::iter_prefix(netuid).for_each(|(hotkey, lock)| { - let rolled = Self::roll_forward_hotkey_lock(netuid, lock, now); + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ); let entry = scores .entry(hotkey) .or_insert_with(|| U64F64::saturating_from_num(0)); *entry = entry.saturating_add(rolled.conviction); }); DecayingHotkeyLock::::iter_prefix(netuid).for_each(|(hotkey, lock)| { - let rolled = Self::roll_forward_decaying_hotkey_lock(netuid, lock, now); + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ); let entry = scores .entry(hotkey) .or_insert_with(|| U64F64::saturating_from_num(0)); @@ -701,7 +1036,29 @@ impl Pallet { }); if let Some(lock) = OwnerLock::::get(netuid) { let owner_hotkey = SubnetOwnerHotkey::::get(netuid); - let rolled = Self::roll_forward_owner_lock(netuid, lock, now); + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ); + let entry = scores + .entry(owner_hotkey) + .or_insert_with(|| U64F64::saturating_from_num(0)); + *entry = entry.saturating_add(rolled.conviction); + } + if let Some(lock) = DecayingOwnerLock::::get(netuid) { + let owner_hotkey = SubnetOwnerHotkey::::get(netuid); + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ); let entry = scores .entry(owner_hotkey) .or_insert_with(|| U64F64::saturating_from_num(0)); @@ -762,6 +1119,8 @@ impl Pallet { return; } let old_owner_hotkey = SubnetOwnerHotkey::::get(netuid); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); // Register new owner as a neuron if not yet registered. if Self::get_uid_for_net_and_hotkey(netuid, &king_hotkey).is_err() @@ -770,85 +1129,158 @@ impl Pallet { return; } - // Move OwnerLock to HotkeyLock for old owner hotkey and HotkeyLock to OwnerLock for - // new owner hotkey + // Move aggregate buckets using the hotkey's new role. if let Some(owner_lock) = OwnerLock::::take(netuid) { - let moved_owner_lock = Self::roll_forward_owner_lock(netuid, owner_lock, now); - if Self::is_perpetual_lock(¤t_owner_coldkey, netuid) { - let old_hotkey_lock = HotkeyLock::::get(netuid, &old_owner_hotkey) - .map(|lock| Self::roll_forward_hotkey_lock(netuid, lock, now)) - .unwrap_or(LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }); - Self::insert_hotkey_lock_state( - netuid, - &old_owner_hotkey, + let moved_owner_lock = ConvictionModel::roll_forward_lock( + owner_lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ); + let current = HotkeyLock::::get(netuid, &old_owner_hotkey) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ) + }) + .unwrap_or_else(|| Self::empty_lock(now)); + Self::insert_hotkey_lock_state( + netuid, + &old_owner_hotkey, + LockState { + locked_mass: current + .locked_mass + .saturating_add(moved_owner_lock.locked_mass), + conviction: current + .conviction + .saturating_add(moved_owner_lock.conviction), + last_update: now, + }, + ); + } + if let Some(owner_lock) = DecayingOwnerLock::::take(netuid) { + let moved_owner_lock = ConvictionModel::roll_forward_lock( + owner_lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ); + let current = DecayingHotkeyLock::::get(netuid, &old_owner_hotkey) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ) + }) + .unwrap_or_else(|| Self::empty_lock(now)); + Self::insert_decaying_hotkey_lock_state( + netuid, + &old_owner_hotkey, + LockState { + locked_mass: current + .locked_mass + .saturating_add(moved_owner_lock.locked_mass), + conviction: current + .conviction + .saturating_add(moved_owner_lock.conviction), + last_update: now, + }, + ); + } + if let Some(king_lock) = HotkeyLock::::take(netuid, &king_hotkey) { + let moved_king_lock = ConvictionModel::roll_forward_lock( + king_lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ); + let current = OwnerLock::::get(netuid) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ) + }) + .unwrap_or_else(|| Self::empty_lock(now)); + Self::insert_owner_lock_state( + netuid, + ConvictionModel::roll_forward_lock( LockState { - locked_mass: old_hotkey_lock + locked_mass: current .locked_mass - .saturating_add(moved_owner_lock.locked_mass), - conviction: old_hotkey_lock + .saturating_add(moved_king_lock.locked_mass), + conviction: current .conviction - .saturating_add(moved_owner_lock.conviction), + .saturating_add(moved_king_lock.conviction), last_update: now, }, - ); - } else { - let old_hotkey_lock = DecayingHotkeyLock::::get(netuid, &old_owner_hotkey) - .map(|lock| Self::roll_forward_decaying_hotkey_lock(netuid, lock, now)) - .unwrap_or(LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }); - Self::insert_decaying_hotkey_lock_state( - netuid, - &old_owner_hotkey, + now, + unlock_rate, + maturity_rate, + true, + true, + ), + ); + } + if let Some(king_lock) = DecayingHotkeyLock::::take(netuid, &king_hotkey) { + let moved_king_lock = ConvictionModel::roll_forward_lock( + king_lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ); + let current = DecayingOwnerLock::::get(netuid) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ) + }) + .unwrap_or_else(|| Self::empty_lock(now)); + Self::insert_decaying_owner_lock_state( + netuid, + ConvictionModel::roll_forward_lock( LockState { - locked_mass: old_hotkey_lock + locked_mass: current .locked_mass - .saturating_add(moved_owner_lock.locked_mass), - conviction: old_hotkey_lock + .saturating_add(moved_king_lock.locked_mass), + conviction: current .conviction - .saturating_add(moved_owner_lock.conviction), + .saturating_add(moved_king_lock.conviction), last_update: now, }, - ); - } - } - - let moved_hotkey_lock = HotkeyLock::::take(netuid, &king_hotkey) - .map(|king_lock| Self::roll_forward_hotkey_lock(netuid, king_lock, now)); - let moved_decaying_king_lock = DecayingHotkeyLock::::take(netuid, &king_hotkey) - .map(|king_lock| Self::roll_forward_decaying_hotkey_lock(netuid, king_lock, now)); - if moved_hotkey_lock.is_some() || moved_decaying_king_lock.is_some() { - let merged = LockState { - locked_mass: moved_hotkey_lock - .as_ref() - .map(|lock| lock.locked_mass) - .unwrap_or(AlphaBalance::ZERO) - .saturating_add( - moved_decaying_king_lock - .as_ref() - .map(|lock| lock.locked_mass) - .unwrap_or(AlphaBalance::ZERO), - ), - conviction: moved_hotkey_lock - .as_ref() - .map(|lock| lock.conviction) - .unwrap_or_else(|| U64F64::saturating_from_num(0)) - .saturating_add( - moved_decaying_king_lock - .as_ref() - .map(|lock| lock.conviction) - .unwrap_or_else(|| U64F64::saturating_from_num(0)), - ), - last_update: now, - }; - let new_owner_lock = Self::roll_forward_owner_lock(netuid, merged, now); - Self::insert_owner_lock_state(netuid, new_owner_lock); + now, + unlock_rate, + maturity_rate, + true, + false, + ), + ); } // Reassign subnet owner coldkey and owner hotkey. @@ -864,9 +1296,18 @@ impl Pallet { /// Ensure the coldkey does not have an active lock on any subnets. pub fn ensure_no_active_locks(coldkey: &T::AccountId) -> Result<(), Error> { let now = Self::get_current_block_as_u64(); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); - for ((netuid, _hotkey), lock) in Lock::::iter_prefix((coldkey,)) { - let rolled = Self::roll_forward_individual_lock(coldkey, netuid, lock, now); + for ((netuid, hotkey), lock) in Lock::::iter_prefix((coldkey,)) { + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &hotkey), + Self::is_perpetual_lock(coldkey, netuid), + ); if rolled.locked_mass > AlphaBalance::ZERO { return Err(Error::::ActiveLockExists); } @@ -899,9 +1340,24 @@ impl Pallet { // Remove locks for old coldkey and insert for new for (netuid, hotkey, lock) in locks_to_transfer { let now = Self::get_current_block_as_u64(); - let old_lock = Self::roll_forward_individual_lock(old_coldkey, netuid, lock, now); - let new_lock = - Self::roll_forward_individual_lock(new_coldkey, netuid, old_lock.clone(), now); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + let old_lock = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &hotkey), + Self::is_perpetual_lock(old_coldkey, netuid), + ); + let new_lock = ConvictionModel::roll_forward_lock( + old_lock.clone(), + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &hotkey), + Self::is_perpetual_lock(new_coldkey, netuid), + ); Lock::::remove((old_coldkey.clone(), netuid, hotkey.clone())); Self::reduce_aggregate_lock( old_coldkey, @@ -928,59 +1384,188 @@ impl Pallet { /// Conviction is not reset because the hotkey ownership does not change, it's still /// the same hotkey owner who will own the new hotkey. pub fn swap_hotkey_locks(old_hotkey: &T::AccountId, new_hotkey: &T::AccountId) -> (u64, u64) { - let mut locks_to_transfer: Vec<(T::AccountId, NetUid, T::AccountId, LockState)> = - Vec::new(); - let mut hotkey_locks_to_transfer: Vec<(NetUid, LockState)> = Vec::new(); - let mut decaying_hotkey_locks_to_transfer: Vec<(NetUid, LockState)> = Vec::new(); + let mut locks_to_transfer: Vec<(T::AccountId, NetUid, LockState)> = Vec::new(); + let mut netuids_to_transfer: Vec<(NetUid, bool, bool)> = Vec::new(); let mut reads: u64 = 0; let mut writes: u64 = 0; let netuids = Self::get_all_subnet_netuids(); - // Gather hotkey locks for old hotkey for netuid in netuids { - if let Some(lock) = HotkeyLock::::get(netuid, old_hotkey) { - hotkey_locks_to_transfer.push((netuid, lock)); - } - if let Some(lock) = DecayingHotkeyLock::::get(netuid, old_hotkey) { - decaying_hotkey_locks_to_transfer.push((netuid, lock)); + let old_is_owner_hotkey = Self::is_subnet_owner_hotkey(netuid, old_hotkey); + let new_is_owner_hotkey = Self::is_subnet_owner_hotkey(netuid, new_hotkey); + let has_hotkey_lock = HotkeyLock::::contains_key(netuid, old_hotkey); + let has_decaying_hotkey_lock = + DecayingHotkeyLock::::contains_key(netuid, old_hotkey); + let has_owner_lock = old_is_owner_hotkey && OwnerLock::::contains_key(netuid); + let has_decaying_owner_lock = + old_is_owner_hotkey && DecayingOwnerLock::::contains_key(netuid); + + if old_is_owner_hotkey + || new_is_owner_hotkey + || has_hotkey_lock + || has_decaying_hotkey_lock + || has_owner_lock + || has_decaying_owner_lock + { + netuids_to_transfer.push(( + netuid, + old_is_owner_hotkey, + old_is_owner_hotkey || new_is_owner_hotkey, + )); } - reads = reads.saturating_add(1); + reads = reads.saturating_add(5); } - // Gather locks for old hotkey (only if hotkey locks exist, otherwise skip to save reads) - if !hotkey_locks_to_transfer.is_empty() || !decaying_hotkey_locks_to_transfer.is_empty() { + if !netuids_to_transfer.is_empty() { for ((coldkey, netuid, hotkey), lock) in Lock::::iter() { if hotkey == *old_hotkey { - locks_to_transfer.push((coldkey, netuid, hotkey, lock)); + locks_to_transfer.push((coldkey, netuid, lock)); } reads = reads.saturating_add(1); } } - // Remove locks for old hotkey and insert for new - for (coldkey, netuid, _hotkey, lock) in locks_to_transfer { + for (coldkey, netuid, lock) in locks_to_transfer { let now = Self::get_current_block_as_u64(); - let rolled = Self::roll_forward_individual_lock(&coldkey, netuid, lock, now); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + let old_owner_lock = netuids_to_transfer + .iter() + .any(|(rebuild_netuid, is_owner, _)| *rebuild_netuid == netuid && *is_owner); + let new_owner_lock = netuids_to_transfer + .iter() + .any(|(rebuild_netuid, _, is_owner)| *rebuild_netuid == netuid && *is_owner); + let perpetual_lock = Self::is_perpetual_lock(&coldkey, netuid); + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + old_owner_lock, + perpetual_lock, + ); + let moved = ConvictionModel::roll_forward_lock( + rolled, + now, + unlock_rate, + maturity_rate, + new_owner_lock, + perpetual_lock, + ); Lock::::remove((coldkey.clone(), netuid, old_hotkey.clone())); - Self::insert_lock_state(&coldkey, netuid, new_hotkey, rolled); + Self::insert_lock_state(&coldkey, netuid, new_hotkey, moved); writes = writes.saturating_add(2); } - // Remove hotkey locks for old hotkey and insert for new - for (netuid, lock) in hotkey_locks_to_transfer { - let now = Self::get_current_block_as_u64(); - let rolled = Self::roll_forward_hotkey_lock(netuid, lock, now); - HotkeyLock::::remove(netuid, old_hotkey); - Self::insert_hotkey_lock_state(netuid, new_hotkey, rolled); - writes = writes.saturating_add(2); - } - for (netuid, lock) in decaying_hotkey_locks_to_transfer { + for (netuid, old_was_owner, new_is_owner) in netuids_to_transfer { let now = Self::get_current_block_as_u64(); - let rolled = Self::roll_forward_decaying_hotkey_lock(netuid, lock, now); - DecayingHotkeyLock::::remove(netuid, old_hotkey); - Self::insert_decaying_hotkey_lock_state(netuid, new_hotkey, rolled); - writes = writes.saturating_add(2); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + let moved_perpetual_lock = if old_was_owner { + OwnerLock::::take(netuid).map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ) + }) + } else { + HotkeyLock::::take(netuid, old_hotkey).map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ) + }) + }; + let moved_decaying_lock = if old_was_owner { + DecayingOwnerLock::::take(netuid).map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ) + }) + } else { + DecayingHotkeyLock::::take(netuid, old_hotkey).map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ) + }) + }; + + if let Some(lock) = moved_perpetual_lock { + if new_is_owner { + Self::insert_owner_lock_state( + netuid, + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ), + ); + } else { + Self::insert_hotkey_lock_state( + netuid, + new_hotkey, + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ), + ); + } + } + if let Some(lock) = moved_decaying_lock { + if new_is_owner { + Self::insert_decaying_owner_lock_state( + netuid, + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ), + ); + } else { + Self::insert_decaying_hotkey_lock_state( + netuid, + new_hotkey, + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ), + ); + } + } + writes = writes.saturating_add(6); } (reads, writes) } @@ -1004,9 +1589,12 @@ impl Pallet { ); let now = Self::get_current_block_as_u64(); - match Lock::::iter_prefix((coldkey, netuid)).next() { - Some((origin_hotkey, existing)) => { - let mut lock = Self::roll_forward_individual_lock(coldkey, netuid, existing, now); + match Self::read_conviction_model(coldkey, netuid, now) { + Some((origin_hotkey, mut model)) => { + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + model.roll_forward_individual(now, unlock_rate, maturity_rate); + let mut lock = model.individual_lock().clone(); let removed = lock.clone(); if Self::get_owning_coldkey_for_hotkey(&origin_hotkey) @@ -1014,7 +1602,14 @@ impl Pallet { { lock.conviction = U64F64::saturating_from_num(0); } - lock = Self::roll_forward_individual_lock(coldkey, netuid, lock, now); + lock = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, destination_hotkey), + Self::is_perpetual_lock(coldkey, netuid), + ); Lock::::remove((coldkey.clone(), netuid, origin_hotkey.clone())); Self::insert_lock_state(coldkey, netuid, destination_hotkey, lock.clone()); @@ -1040,13 +1635,19 @@ impl Pallet { } pub fn auto_lock_owner_cut(netuid: NetUid, amount: AlphaBalance) { + if !OwnerCutAutoLockEnabled::::get(netuid) { + return; + } + let subnet_owner_coldkey = Self::get_subnet_owner(netuid); // Determine the lock hotkey. If no locks exist, assign subnet owner's hotkey, otherwise // auto-lock to existing lock hotkey - let lock_hotkey = if let Some((existing_hotkey, _existing)) = - Lock::::iter_prefix((&subnet_owner_coldkey, netuid)).next() - { + let lock_hotkey = if let Some((existing_hotkey, _model)) = Self::read_conviction_model( + &subnet_owner_coldkey, + netuid, + Self::get_current_block_as_u64(), + ) { existing_hotkey } else { SubnetOwnerHotkey::::get(netuid) @@ -1085,21 +1686,20 @@ impl Pallet { let mut remaining_to_transfer = amount; // Read the locks for source and destination coldkey (if exist) and roll forward - let Some((source_hotkey, source_lock)) = - Lock::::iter_prefix((origin_coldkey, netuid)).next() + let Some((source_hotkey, mut source_model)) = + Self::read_conviction_model(origin_coldkey, netuid, now) else { return Ok(()); }; - let mut source_lock = - Self::roll_forward_individual_lock(origin_coldkey, netuid, source_lock, now); - let maybe_destination_lock = Lock::::iter_prefix((destination_coldkey, netuid)) - .next() - .map(|(hotkey, lock)| { - ( - hotkey, - Self::roll_forward_individual_lock(destination_coldkey, netuid, lock, now), - ) + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + source_model.roll_forward_individual(now, unlock_rate, maturity_rate); + let mut source_lock = source_model.individual_lock().clone(); + let maybe_destination_lock = Self::read_conviction_model(destination_coldkey, netuid, now) + .map(|(hotkey, mut model)| { + model.roll_forward_individual(now, unlock_rate, maturity_rate); + (hotkey, model.individual_lock().clone()) }); let mut destination_hotkey = maybe_destination_lock @@ -1162,9 +1762,22 @@ impl Pallet { .saturating_add(conviction_transfer); } - source_lock = Self::roll_forward_individual_lock(origin_coldkey, netuid, source_lock, now); - destination_lock = - Self::roll_forward_individual_lock(destination_coldkey, netuid, destination_lock, now); + source_lock = ConvictionModel::roll_forward_lock( + source_lock, + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &source_hotkey), + Self::is_perpetual_lock(origin_coldkey, netuid), + ); + destination_lock = ConvictionModel::roll_forward_lock( + destination_lock, + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &destination_hotkey), + Self::is_perpetual_lock(destination_coldkey, netuid), + ); // Upsert updated locks (only once per this fn) even if there were no updates because // of roll-forward @@ -1238,8 +1851,9 @@ impl Pallet { } } - // OwnerLock: (netuid) + // OwnerLock / DecayingOwnerLock: (netuid) OwnerLock::::remove(netuid); + DecayingOwnerLock::::remove(netuid); // DecayingLock: (coldkey, netuid) { diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index 46a834946b..7121874400 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -494,4 +494,14 @@ impl Pallet { pub fn set_owner_cut_enabled_flag(netuid: NetUid, value: bool) { OwnerCutEnabled::::insert(netuid, value); } + + /// Returns whether owner cut auto-locking is enabled for the given subnet. + pub fn get_owner_cut_auto_lock_enabled(netuid: NetUid) -> bool { + OwnerCutAutoLockEnabled::::get(netuid) + } + + /// Sets whether owner cut should be auto-locked for the given subnet. + pub fn set_owner_cut_auto_lock_enabled(netuid: NetUid, value: bool) { + OwnerCutAutoLockEnabled::::insert(netuid, value); + } } diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index f4eede30e4..92218ae51d 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -15,7 +15,7 @@ use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance}; use subtensor_swap_interface::SwapHandler; use super::mock::*; -use crate::staking::lock::LockState; +use crate::staking::lock::{ConvictionModel, LockState}; use crate::*; // --------------------------------------------------------------------------- @@ -52,6 +52,7 @@ fn setup_subnet_with_stake( false, ) .unwrap(); + DecayingLock::::insert(coldkey, netuid, false); netuid } @@ -64,6 +65,45 @@ fn get_alpha( SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid) } +fn roll_forward_lock( + lock: LockState, + now: u64, + owner_lock: bool, + perpetual_lock: bool, +) -> LockState { + ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ) +} + +fn roll_forward_individual_lock( + coldkey: &U256, + netuid: subtensor_runtime_common::NetUid, + hotkey: &U256, + lock: LockState, + now: u64, +) -> LockState { + roll_forward_lock( + lock, + now, + hotkey == &SubnetOwnerHotkey::::get(netuid), + DecayingLock::::get(coldkey, netuid) == Some(false), + ) +} + +fn roll_forward_hotkey_lock(lock: LockState, now: u64) -> LockState { + roll_forward_lock(lock, now, false, true) +} + +fn roll_forward_decaying_hotkey_lock(lock: LockState, now: u64) -> LockState { + roll_forward_lock(lock, now, false, false) +} + // ========================================================================= // GROUP 1: Green-path — basic lock creation // ========================================================================= @@ -100,6 +140,31 @@ fn test_lock_stake_creates_new_lock() { }); } +#[test] +fn test_lock_stake_defaults_to_decaying_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); + DecayingLock::::remove(coldkey, netuid); + + let lock_amount: AlphaBalance = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + assert!(DecayingLock::::get(coldkey, netuid).is_none()); + assert!(HotkeyLock::::get(netuid, hotkey).is_none()); + + let decaying_hotkey_lock = DecayingHotkeyLock::::get(netuid, hotkey) + .expect("default lock should use decaying aggregate"); + assert_eq!(decaying_hotkey_lock.locked_mass, lock_amount); + }); +} + #[test] fn test_lock_stake_by_subnet_owner_coldkey_gets_immediate_conviction() { new_test_ext(1).execute_with(|| { @@ -127,6 +192,125 @@ fn test_lock_stake_by_subnet_owner_coldkey_gets_immediate_conviction() { }); } +#[test] +fn test_lock_to_subnet_owner_hotkey_gets_immediate_conviction_for_non_owner_coldkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let staker_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, staker_hotkey, 300_000_000_000); + let owner_hotkey = SubnetOwnerHotkey::::get(netuid); + + let lock_amount: AlphaBalance = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &owner_hotkey, + lock_amount, + )); + + let lock = Lock::::get((coldkey, netuid, owner_hotkey)) + .expect("lock to owner hotkey should exist"); + assert_eq!(lock.locked_mass, lock_amount); + assert_eq!(lock.conviction, U64F64::saturating_from_num(5000)); + + let owner_lock = OwnerLock::::get(netuid).expect("owner lock should exist"); + assert_eq!(owner_lock.locked_mass, lock_amount); + assert_eq!(owner_lock.conviction, U64F64::saturating_from_num(5000)); + assert!( + HotkeyLock::::get(netuid, owner_hotkey).is_none(), + "lock to owner hotkey should use OwnerLock, not HotkeyLock" + ); + }); +} + +#[test] +fn test_decaying_lock_to_subnet_owner_hotkey_keeps_decaying_mass() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let staker_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, staker_hotkey, 300_000_000_000); + let owner_hotkey = SubnetOwnerHotkey::::get(netuid); + + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &coldkey, netuid, false, + )); + + let lock_amount: AlphaBalance = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &owner_hotkey, + lock_amount, + )); + + step_block(1_000); + let now = SubtensorModule::get_current_block_as_u64(); + let rolled = roll_forward_individual_lock( + &coldkey, + netuid, + &owner_hotkey, + Lock::::get((coldkey, netuid, owner_hotkey)).unwrap(), + now, + ); + + assert!(rolled.locked_mass < lock_amount); + assert_eq!( + rolled.conviction, + U64F64::saturating_from_num(u64::from(rolled.locked_mass)) + ); + assert_eq!( + SubtensorModule::hotkey_conviction(&owner_hotkey, netuid), + rolled.conviction + ); + assert!( + OwnerLock::::get(netuid).is_none(), + "decaying lock to owner hotkey should not use perpetual OwnerLock" + ); + assert!( + DecayingOwnerLock::::get(netuid).is_some(), + "decaying lock to owner hotkey should use DecayingOwnerLock" + ); + }); +} + +#[test] +fn test_lock_by_subnet_owner_coldkey_to_non_owner_hotkey_matures_normally() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1); + let non_owner_hotkey = U256::from(2); + let owner_hotkey = U256::from(3); + let netuid = setup_subnet_with_stake(owner_coldkey, non_owner_hotkey, 300_000_000_000); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &owner_coldkey, + &owner_hotkey + )); + SubnetOwner::::insert(netuid, owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, owner_hotkey); + + let lock_amount: AlphaBalance = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &owner_coldkey, + netuid, + &non_owner_hotkey, + lock_amount, + )); + + let lock = Lock::::get((owner_coldkey, netuid, non_owner_hotkey)) + .expect("lock to non-owner hotkey should exist"); + assert_eq!(lock.locked_mass, lock_amount); + assert_eq!(lock.conviction, U64F64::saturating_from_num(0)); + assert!( + OwnerLock::::get(netuid).is_none(), + "owner coldkey lock to a non-owner hotkey should not use OwnerLock" + ); + + let hotkey_lock = + HotkeyLock::::get(netuid, non_owner_hotkey).expect("hotkey lock should exist"); + assert_eq!(hotkey_lock.locked_mass, lock_amount); + assert_eq!(hotkey_lock.conviction, U64F64::saturating_from_num(0)); + }); +} + #[test] fn test_lock_stake_topup_by_subnet_owner_coldkey_gets_immediate_conviction() { new_test_ext(1).execute_with(|| { @@ -299,25 +483,23 @@ fn test_mixed_perpetual_and_decaying_non_owner_locks_same_hotkey_update_aggregat step_block(1_000); let now = SubtensorModule::get_current_block_as_u64(); - let perpetual_lock = SubtensorModule::roll_forward_individual_lock( + let perpetual_lock = roll_forward_individual_lock( &perpetual_coldkey, netuid, + &hotkey, Lock::::get((perpetual_coldkey, netuid, hotkey)).unwrap(), now, ); - let decaying_lock = SubtensorModule::roll_forward_individual_lock( + let decaying_lock = roll_forward_individual_lock( &decaying_coldkey, netuid, + &hotkey, Lock::::get((decaying_coldkey, netuid, hotkey)).unwrap(), now, ); - let perpetual_hotkey_lock = SubtensorModule::roll_forward_hotkey_lock( - netuid, - HotkeyLock::::get(netuid, hotkey).unwrap(), - now, - ); - let decaying_hotkey_lock = SubtensorModule::roll_forward_decaying_hotkey_lock( - netuid, + let perpetual_hotkey_lock = + roll_forward_hotkey_lock(HotkeyLock::::get(netuid, hotkey).unwrap(), now); + let decaying_hotkey_lock = roll_forward_decaying_hotkey_lock( DecayingHotkeyLock::::get(netuid, hotkey).unwrap(), now, ); @@ -383,7 +565,7 @@ fn plot_perpetual_decay_perpetual_lock_curve() { let lock = Lock::::get((owner_coldkey, netuid, owner_hotkey)).unwrap(); let rolled = - SubtensorModule::roll_forward_individual_lock(&owner_coldkey, netuid, lock, block); + roll_forward_individual_lock(&owner_coldkey, netuid, &owner_hotkey, lock, block); SubtensorModule::insert_lock_state( &owner_coldkey, netuid, @@ -431,8 +613,7 @@ fn plot_decaying_non_owner_lock_curve() { System::set_block_number(block); let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); - let rolled = - SubtensorModule::roll_forward_individual_lock(&coldkey, netuid, lock, block); + let rolled = roll_forward_individual_lock(&coldkey, netuid, &hotkey, lock, block); SubtensorModule::insert_lock_state(&coldkey, netuid, &hotkey, rolled.clone()); SubtensorModule::insert_hotkey_lock_state(netuid, &hotkey, rolled.clone()); println!( @@ -485,13 +666,12 @@ fn plot_perpetual_decay_perpetual_non_owner_lock_curve() { } let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); - let rolled = - SubtensorModule::roll_forward_individual_lock(&coldkey, netuid, lock, block); + let rolled = roll_forward_individual_lock(&coldkey, netuid, &hotkey, lock, block); SubtensorModule::insert_lock_state(&coldkey, netuid, &hotkey, rolled.clone()); - if DecayingLock::::contains_key(coldkey, netuid) { - SubtensorModule::insert_decaying_hotkey_lock_state(netuid, &hotkey, rolled.clone()); - } else { + if DecayingLock::::get(coldkey, netuid) == Some(false) { SubtensorModule::insert_hotkey_lock_state(netuid, &hotkey, rolled.clone()); + } else { + SubtensorModule::insert_decaying_hotkey_lock_state(netuid, &hotkey, rolled.clone()); } println!( "{},{},{}", @@ -590,6 +770,47 @@ fn test_get_conviction_no_lock() { }); } +#[test] +fn test_get_coldkey_lock_rolls_forward() { + 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(), + )); + + let initial_lock = + SubtensorModule::get_coldkey_lock(&coldkey, netuid).expect("coldkey lock should exist"); + assert_eq!(initial_lock.conviction, U64F64::from_num(0)); + + step_block(1000); + + let rolled_lock = + SubtensorModule::get_coldkey_lock(&coldkey, netuid).expect("coldkey lock should exist"); + assert_eq!(rolled_lock.locked_mass, initial_lock.locked_mass); + assert!(rolled_lock.conviction > initial_lock.conviction); + assert_eq!( + rolled_lock.last_update, + SubtensorModule::get_current_block_as_u64() + ); + }); +} + +#[test] +fn test_get_coldkey_lock_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let netuid = subtensor_runtime_common::NetUid::from(1); + + assert!(SubtensorModule::get_coldkey_lock(&coldkey, netuid).is_none()); + }); +} + #[test] fn test_available_to_unstake_no_lock() { new_test_ext(1).execute_with(|| { @@ -839,13 +1060,13 @@ fn test_lock_stake_topup_exceeds_total() { } // ========================================================================= -// GROUP 5: Exponential decay math +// GROUP 5: ConvictionModel roll-forward math // ========================================================================= #[test] fn test_exp_decay_zero_dt() { new_test_ext(1).execute_with(|| { - let result = SubtensorModule::exp_decay(0, 216000); + let result = ConvictionModel::exp_decay(0, 216000); assert_eq!(result, U64F64::from_num(1)); }); } @@ -853,7 +1074,7 @@ fn test_exp_decay_zero_dt() { #[test] fn test_exp_decay_zero_tau() { new_test_ext(1).execute_with(|| { - let result = SubtensorModule::exp_decay(1000, 0); + let result = ConvictionModel::exp_decay(1000, 0); assert_eq!(result, U64F64::from_num(0)); }); } @@ -862,7 +1083,7 @@ fn test_exp_decay_zero_tau() { fn test_exp_decay_one_tau() { new_test_ext(1).execute_with(|| { let tau = 216000u64; - let result = SubtensorModule::exp_decay(tau, tau); + let result = ConvictionModel::exp_decay(tau, tau); // exp(-1) ~= 0.36787944 let expected = U64F64::from_num(0.36787944f64); let diff = if result > expected { @@ -878,8 +1099,8 @@ fn test_exp_decay_one_tau() { 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 clamped_result = ConvictionModel::exp_decay(40 * tau, tau); + let oversized_result = ConvictionModel::exp_decay(100 * tau, tau); let diff = if oversized_result > clamped_result { oversized_result - clamped_result @@ -893,32 +1114,97 @@ fn test_exp_decay_clamps_large_dt_to_min_ratio() { } #[test] -fn test_roll_forward_locked_mass_decays() { +fn test_roll_forward_individual_lock_uses_lock_owner_and_decay_mode() { 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 owner_hotkey = SubnetOwnerHotkey::::get(netuid); + DecayingLock::::remove(coldkey, netuid); - let lock_amount = 10000u64; - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount.into() - )); - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &coldkey, netuid, false, - )); + let lock = LockState { + locked_mass: 10_000u64.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let now = 1_000u64; - // Advance one full unlock rate via direct block number jump. - let tau = UnlockRate::::get(); - let target = System::block_number() + tau; - System::set_block_number(target); + let rolled = + roll_forward_individual_lock(&coldkey, netuid, &owner_hotkey, lock.clone(), now); + let expected = ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + true, + false, + ); - let locked = SubtensorModule::get_current_locked(&coldkey, netuid); + assert_eq!(rolled, expected); + }); +} + +#[test] +fn test_roll_forward_hotkey_lock_uses_perpetual_general_mode() { + new_test_ext(1).execute_with(|| { + let lock = LockState { + locked_mass: 10_000u64.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let now = 1_000u64; + + let rolled = roll_forward_hotkey_lock(lock.clone(), now); + let expected = ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + false, + true, + ); + + assert_eq!(rolled, expected); + }); +} + +#[test] +fn test_roll_forward_decaying_hotkey_lock_uses_decaying_general_mode() { + new_test_ext(1).execute_with(|| { + let lock = LockState { + locked_mass: 10_000u64.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let now = 1_000u64; + + let rolled = roll_forward_decaying_hotkey_lock(lock.clone(), now); + let expected = ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + false, + false, + ); + + assert_eq!(rolled, expected); + }); +} + +#[test] +fn test_roll_forward_locked_mass_decays() { + new_test_ext(1).execute_with(|| { + let lock_amount = 10000u64; + let lock = LockState { + locked_mass: lock_amount.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let rolled = roll_forward_lock(lock, UnlockRate::::get(), false, false); - assert!(locked < lock_amount.into()); - assert!(locked > AlphaBalance::ZERO); + assert!(rolled.locked_mass < lock_amount.into()); + assert!(rolled.locked_mass > AlphaBalance::ZERO); }); } @@ -927,8 +1213,9 @@ fn test_roll_forward_conviction_uses_unequal_rate_closed_form() { new_test_ext(1).execute_with(|| { let locked_mass = 10_000u64; let dt = 10_000u64; - let unlock_rate = UnlockRate::::get(); - let maturity_rate = unlock_rate * 12 / 10; + let unlock_rate = 200_000u64; + let maturity_rate = 240_000u64; + UnlockRate::::set(unlock_rate); MaturityRate::::set(maturity_rate); assert_ne!(unlock_rate, maturity_rate); @@ -937,10 +1224,10 @@ fn test_roll_forward_conviction_uses_unequal_rate_closed_form() { conviction: U64F64::from_num(0), last_update: 0, }; - let rolled = SubtensorModule::roll_forward_lock(lock, dt, false, false); + let rolled = roll_forward_lock(lock, dt, false, false); - let unlock_decay = SubtensorModule::exp_decay(dt, unlock_rate); - let maturity_decay = SubtensorModule::exp_decay(dt, maturity_rate); + let unlock_decay = ConvictionModel::exp_decay(dt, unlock_rate); + let maturity_decay = ConvictionModel::exp_decay(dt, maturity_rate); let gamma = U64F64::from_num(unlock_rate) .saturating_mul(maturity_decay.saturating_sub(unlock_decay)) .safe_div(U64F64::from_num(maturity_rate.saturating_sub(unlock_rate))); @@ -969,7 +1256,7 @@ fn test_roll_forward_adjacent_large_rates_and_large_mass_match_f64_closed_form() conviction: U64F64::from_num(0), last_update: 0, }; - let rolled = SubtensorModule::roll_forward_lock(lock, dt, false, false); + let rolled = roll_forward_lock(lock, dt, false, false); let decay_x = (-(dt as f64) / unlock_rate as f64).exp(); let decay_z = (-(dt as f64) / maturity_rate as f64).exp(); @@ -1007,8 +1294,8 @@ fn test_roll_forward_scales_linearly_with_locked_mass() { last_update: 0, }; - let rolled_base = SubtensorModule::roll_forward_lock(base, dt, false, false); - let rolled_double = SubtensorModule::roll_forward_lock(double, dt, false, false); + let rolled_base = roll_forward_lock(base, dt, false, false); + let rolled_double = roll_forward_lock(double, dt, false, false); assert_abs_diff_eq!( u64::from(rolled_double.locked_mass) as f64, @@ -1034,9 +1321,9 @@ fn test_roll_forward_chunked_update_matches_single_update() { let mid = 10_000u64; let end = 20_000u64; - let rolled_once = SubtensorModule::roll_forward_lock(lock.clone(), end, false, false); - let rolled_twice = SubtensorModule::roll_forward_lock( - SubtensorModule::roll_forward_lock(lock, mid, false, false), + let rolled_once = roll_forward_lock(lock.clone(), end, false, false); + let rolled_twice = roll_forward_lock( + roll_forward_lock(lock, mid, false, false), end, false, false, @@ -1073,7 +1360,7 @@ fn test_roll_forward_conviction_stays_below_original_mass_for_one_shot_lock() { MaturityRate::::get(), MaturityRate::::get().saturating_mul(5), ] { - let rolled = SubtensorModule::roll_forward_lock(lock.clone(), dt, false, false); + let rolled = roll_forward_lock(lock.clone(), dt, false, false); assert!(rolled.conviction <= cap); } }); @@ -1082,6 +1369,9 @@ fn test_roll_forward_conviction_stays_below_original_mass_for_one_shot_lock() { #[test] fn test_roll_forward_decaying_conviction_peak_is_below_original_lock() { new_test_ext(1).execute_with(|| { + UnlockRate::::set(200_000u64); + MaturityRate::::set(240_000u64); + let locked_mass = 10_000u64; let unlock_rate = UnlockRate::::get() as f64; let maturity_rate = MaturityRate::::get() as f64; @@ -1096,7 +1386,7 @@ fn test_roll_forward_decaying_conviction_peak_is_below_original_lock() { last_update: 0, }; - let rolled = SubtensorModule::roll_forward_lock(lock, peak_block, false, false); + let rolled = roll_forward_lock(lock, peak_block, false, false); assert!(rolled.conviction < U64F64::from_num(locked_mass)); }); @@ -1112,8 +1402,7 @@ fn test_roll_forward_perpetual_mass_does_not_decay_and_conviction_matures() { last_update: 0, }; - let rolled = - SubtensorModule::roll_forward_lock(lock, MaturityRate::::get(), false, true); + let rolled = roll_forward_lock(lock, MaturityRate::::get(), false, true); assert_eq!(rolled.locked_mass, locked_mass.into()); assert!(rolled.conviction > U64F64::from_num(0)); @@ -1138,7 +1427,7 @@ fn test_roll_forward_perpetual_conviction_never_exceeds_lock() { MaturityRate::::get().saturating_mul(10), MaturityRate::::get().saturating_mul(1_000), ] { - let rolled = SubtensorModule::roll_forward_lock(lock.clone(), dt, false, true); + let rolled = roll_forward_lock(lock.clone(), dt, false, true); assert_eq!(rolled.locked_mass, locked_mass.into()); assert!(rolled.conviction <= U64F64::from_num(locked_mass)); } @@ -1148,40 +1437,26 @@ fn test_roll_forward_perpetual_conviction_never_exceeds_lock() { #[test] fn test_roll_forward_conviction_converges_to_zero() { 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 - )); - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &coldkey, netuid, false, - )); + let lock_amount = 10000u64; + let lock = LockState { + locked_mass: lock_amount.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; - // Conviction at t=0 is 0 - let c0 = SubtensorModule::get_conviction(&coldkey, netuid); + let c0 = lock.conviction; assert_eq!(c0, U64F64::from_num(0)); - // After some time, conviction should have grown - step_block(100); - let c1 = SubtensorModule::get_conviction(&coldkey, netuid); + let rolled = roll_forward_lock(lock.clone(), 100, false, false); + let c1 = rolled.conviction; assert!(c1 > U64F64::from_num(0)); - // After more time, conviction should be even higher - step_block(1000); - let c2 = SubtensorModule::get_conviction(&coldkey, netuid); + let rolled = roll_forward_lock(lock.clone(), 1_100, false, false); + let c2 = rolled.conviction; assert!(c2 > c1); - // After a very long time (many taus), conviction is close to zero let tau = MaturityRate::::get(); - let target = System::block_number() + tau * 1000; - System::set_block_number(target); - let c_late = SubtensorModule::get_conviction(&coldkey, netuid); + let c_late = roll_forward_lock(lock, tau * 1000, false, false).conviction; assert_abs_diff_eq!(c_late.to_num::(), 0., epsilon = 0.0000001); }); } @@ -1194,7 +1469,7 @@ fn test_roll_forward_no_change_when_now_equals_last_update() { conviction: U64F64::from_num(1234), last_update: 100, }; - let rolled = SubtensorModule::roll_forward_lock(lock.clone(), 100, false, false); + let rolled = roll_forward_lock(lock.clone(), 100, false, false); assert_eq!(rolled.locked_mass, lock.locked_mass); assert_eq!(rolled.conviction, lock.conviction); assert_eq!(rolled.last_update, 100); @@ -1323,6 +1598,7 @@ fn test_do_transfer_stake_same_subnet_transfers_lock_to_destination_coldkey() { let coldkey_receiver = U256::from(5); let hotkey = U256::from(2); let netuid = setup_subnet_with_stake(coldkey_sender, hotkey, 100_000_000_000); + DecayingLock::::insert(coldkey_receiver, netuid, false); let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); let lock_half = total / 2.into(); @@ -1350,7 +1626,7 @@ fn test_do_transfer_stake_same_subnet_transfers_lock_to_destination_coldkey() { transfer_amount, )); - let expected_sender_lock = SubtensorModule::roll_forward_lock( + let expected_sender_lock = roll_forward_lock( sender_lock_before, SubtensorModule::get_current_block_as_u64(), false, @@ -1367,7 +1643,7 @@ fn test_do_transfer_stake_same_subnet_transfers_lock_to_destination_coldkey() { let hotkey_lock_after = HotkeyLock::::get(netuid, hotkey).expect("hotkey lock should remain"); - let expected_hotkey_lock = SubtensorModule::roll_forward_lock( + let expected_hotkey_lock = roll_forward_lock( hotkey_lock_before, SubtensorModule::get_current_block_as_u64(), false, @@ -1456,7 +1732,7 @@ fn test_transfer_stake_cross_coldkey_allowed_partial() { Lock::::get((coldkey_sender, netuid, hotkey)).expect("sender lock should remain"); assert_eq!( sender_lock_after.locked_mass, - SubtensorModule::roll_forward_lock(sender_lock_before, 2, false, true).locked_mass + roll_forward_lock(sender_lock_before, 2, false, true).locked_mass ); assert!(Lock::::get((coldkey_receiver, netuid, hotkey)).is_none()); }); @@ -1497,6 +1773,7 @@ fn test_lock_on_multiple_subnets() { false, ) .unwrap(); + DecayingLock::::insert(coldkey, netuid_b, false); // Lock on subnet A to hotkey_a assert_ok!(SubtensorModule::do_lock_stake( @@ -1716,13 +1993,13 @@ fn test_mixed_perpetual_owner_and_decaying_non_owner_locks_roll_forward() { System::set_block_number(System::block_number() + UnlockRate::::get()); - let owner_lock = SubtensorModule::roll_forward_lock( + let owner_lock = roll_forward_lock( OwnerLock::::get(netuid).unwrap(), SubtensorModule::get_current_block_as_u64(), true, true, ); - let staker_lock = SubtensorModule::roll_forward_lock( + let staker_lock = roll_forward_lock( HotkeyLock::::get(netuid, staker_hotkey).unwrap(), SubtensorModule::get_current_block_as_u64(), false, @@ -1840,9 +2117,8 @@ fn test_total_conviction_equals_sum_of_individual_lock_convictions_for_many_lock let now = SubtensorModule::get_current_block_as_u64(); let individual_sum = Lock::::iter() .filter(|((_coldkey, lock_netuid, _hotkey), _lock)| *lock_netuid == netuid) - .map(|((coldkey, _netuid, _hotkey), lock)| { - SubtensorModule::roll_forward_individual_lock(&coldkey, netuid, lock, now) - .conviction + .map(|((coldkey, _netuid, hotkey), lock)| { + roll_forward_individual_lock(&coldkey, netuid, &hotkey, lock, now).conviction }) .fold(U64F64::from_num(0), |acc, conviction| { acc.saturating_add(conviction) @@ -1943,6 +2219,8 @@ fn test_change_subnet_owner_if_needed_reassigns_to_subnet_king() { let old_owner_coldkey = U256::from(1); let old_owner_hotkey = U256::from(2); let netuid = setup_subnet_with_stake(old_owner_coldkey, old_owner_hotkey, 100_000_000_000); + SubnetOwner::::insert(netuid, old_owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); let new_owner_coldkey = U256::from(5); let king_hotkey = U256::from(6); @@ -1964,7 +2242,7 @@ fn test_change_subnet_owner_if_needed_reassigns_to_subnet_king() { (new_owner_coldkey, netuid, king_hotkey), LockState { locked_mass, - conviction: U64F64::from_num(10), + conviction: U64F64::from_num(1_000), last_update: now, }, ); @@ -1986,13 +2264,157 @@ fn test_change_subnet_owner_if_needed_reassigns_to_subnet_king() { // The new owner's aggregate conviction is progressed to locked mass. let owner_lock = Lock::::get((new_owner_coldkey, netuid, king_hotkey)).unwrap(); - assert_eq!(owner_lock.conviction, U64F64::from_num(10)); + assert_eq!(owner_lock.conviction, U64F64::from_num(1_000)); let king_lock = OwnerLock::::get(netuid).unwrap(); assert_eq!(king_lock.conviction, U64F64::from_num(1_000)); }); } +#[test] +fn test_change_subnet_owner_rebuilds_old_owner_hotkey_by_lock_mode() { + new_test_ext(1).execute_with(|| { + let old_owner_coldkey = U256::from(1); + let old_owner_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(old_owner_coldkey, old_owner_hotkey, 100_000_000_000); + SubnetOwner::::insert(netuid, old_owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); + + let perpetual_coldkey = U256::from(3); + let decaying_coldkey = U256::from(4); + let king_coldkey = U256::from(5); + let king_hotkey = U256::from(6); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &king_coldkey, + &king_hotkey + )); + register_ok_neuron(netuid, king_hotkey, king_coldkey, 0); + + let now = crate::staking::lock::ONE_YEAR + 1; + System::set_block_number(now); + NetworkRegisteredAt::::insert(netuid, 1); + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000u64)); + DecayingLock::::insert(perpetual_coldkey, netuid, false); + + Lock::::insert( + (perpetual_coldkey, netuid, old_owner_hotkey), + LockState { + locked_mass: 400u64.into(), + conviction: U64F64::from_num(400), + last_update: now, + }, + ); + Lock::::insert( + (decaying_coldkey, netuid, old_owner_hotkey), + LockState { + locked_mass: 300u64.into(), + conviction: U64F64::from_num(300), + last_update: now, + }, + ); + OwnerLock::::insert( + netuid, + LockState { + locked_mass: 400u64.into(), + conviction: U64F64::from_num(400), + last_update: now, + }, + ); + DecayingOwnerLock::::insert( + netuid, + LockState { + locked_mass: 300u64.into(), + conviction: U64F64::from_num(300), + last_update: now, + }, + ); + Lock::::insert( + (king_coldkey, netuid, king_hotkey), + LockState { + locked_mass: 1_000u64.into(), + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + HotkeyLock::::insert( + netuid, + king_hotkey, + LockState { + locked_mass: 1_000u64.into(), + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + + SubtensorModule::change_subnet_owner_if_needed(netuid); + + assert_eq!(SubnetOwnerHotkey::::get(netuid), king_hotkey); + assert_eq!( + HotkeyLock::::get(netuid, old_owner_hotkey) + .unwrap() + .locked_mass, + 400u64.into() + ); + assert_eq!( + DecayingHotkeyLock::::get(netuid, old_owner_hotkey) + .unwrap() + .locked_mass, + 300u64.into() + ); + assert_eq!( + OwnerLock::::get(netuid).unwrap().locked_mass, + 1_000u64.into() + ); + }); +} + +#[test] +fn test_swap_hotkey_locks_moves_owner_hotkey_aggregate_to_owner_lock() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1); + let old_owner_hotkey = U256::from(2); + let new_owner_hotkey = U256::from(3); + let locking_coldkey = U256::from(4); + let netuid = setup_subnet_with_stake(owner_coldkey, old_owner_hotkey, 100_000_000_000); + SubnetOwner::::insert(netuid, owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); + + assert_ok!(SubtensorModule::create_account_if_non_existent( + &owner_coldkey, + &new_owner_hotkey + )); + + let now = SubtensorModule::get_current_block_as_u64(); + Lock::::insert( + (locking_coldkey, netuid, old_owner_hotkey), + LockState { + locked_mass: 500u64.into(), + conviction: U64F64::from_num(500), + last_update: now, + }, + ); + OwnerLock::::insert( + netuid, + LockState { + locked_mass: 500u64.into(), + conviction: U64F64::from_num(500), + last_update: now, + }, + ); + + SubtensorModule::swap_hotkey_locks(&old_owner_hotkey, &new_owner_hotkey); + + assert!(Lock::::get((locking_coldkey, netuid, old_owner_hotkey)).is_none()); + assert!(Lock::::get((locking_coldkey, netuid, new_owner_hotkey)).is_some()); + assert!(HotkeyLock::::get(netuid, new_owner_hotkey).is_none()); + assert!(DecayingHotkeyLock::::get(netuid, new_owner_hotkey).is_none()); + assert_eq!( + OwnerLock::::get(netuid).unwrap().locked_mass, + 500u64.into() + ); + }); +} + #[test] fn test_change_subnet_owner_if_needed_does_not_reassign_when_required_condition_is_missing() { let assert_owner_unchanged = @@ -2178,6 +2600,7 @@ fn test_reduce_lock_two_coldkeys() { false, ) .unwrap(); + DecayingLock::::insert(coldkey2, netuid, false); // Mock a non-zero conviction for both coldkeys let lock1 = Lock::::get((coldkey1, netuid, hotkey)).unwrap_or(LockState { @@ -2884,6 +3307,7 @@ fn test_clear_small_nomination_reduces_only_tiny_amount_from_lock_state() { false, ) .unwrap(); + DecayingLock::::insert(nominator, netuid, false); let large_alpha_before = get_alpha(&hotkey_large, &nominator, netuid); let tiny_alpha_before = get_alpha(&hotkey_tiny, &nominator, netuid); @@ -3032,6 +3456,7 @@ fn test_epoch_distribution_auto_locks_owner_cut() { SubtensorModule::set_max_allowed_validators(netuid, 1); step_block(subnet_tempo); SubnetOwnerCut::::set(u16::MAX / 10); + OwnerCutAutoLockEnabled::::insert(netuid, true); let owner_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &subnet_owner_hotkey).unwrap(); @@ -3084,6 +3509,35 @@ fn test_epoch_distribution_auto_locks_owner_cut() { }); } +#[test] +fn test_auto_lock_owner_cut_is_enabled_by_default_and_can_be_disabled() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = + setup_subnet_with_stake(subnet_owner_coldkey, subnet_owner_hotkey, 100_000_000_000); + let owner_cut: AlphaBalance = 10_000_000u64.into(); + + assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + SubtensorModule::auto_lock_owner_cut(netuid, owner_cut); + + let owner_lock = Lock::::get((subnet_owner_coldkey, netuid, subnet_owner_hotkey)) + .expect("owner cut should be auto-locked by default"); + assert_eq!(owner_lock.locked_mass, owner_cut); + + Lock::::remove((subnet_owner_coldkey, netuid, subnet_owner_hotkey)); + OwnerCutAutoLockEnabled::::insert(netuid, false); + assert!(!SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + SubtensorModule::auto_lock_owner_cut(netuid, owner_cut); + + assert!( + Lock::::iter_prefix((subnet_owner_coldkey, netuid)) + .next() + .is_none() + ); + }); +} + // ========================================================================= // GROUP 18: Neuron replacement // ========================================================================= @@ -3194,7 +3648,7 @@ fn test_moving_lock() { } #[test] -fn test_moving_lock_to_subnet_owner_hotkey_does_not_get_owner_conviction_for_non_owner_coldkey() { +fn test_moving_lock_to_subnet_owner_hotkey_gets_owner_conviction_for_non_owner_coldkey() { new_test_ext(1).execute_with(|| { let coldkey = U256::from(1); let hotkey_origin = U256::from(2); @@ -3217,11 +3671,15 @@ fn test_moving_lock_to_subnet_owner_hotkey_does_not_get_owner_conviction_for_non let lock = Lock::::get((coldkey, netuid, owner_hotkey)).unwrap(); assert_eq!(lock.locked_mass, lock_amount); - assert_eq!(lock.conviction, U64F64::from_num(0)); + assert_eq!(lock.conviction, U64F64::from_num(5000)); - let hotkey_lock = HotkeyLock::::get(netuid, owner_hotkey).unwrap(); - assert_eq!(hotkey_lock.locked_mass, lock_amount); - assert_eq!(hotkey_lock.conviction, U64F64::from_num(0)); + assert!( + HotkeyLock::::get(netuid, owner_hotkey).is_none(), + "lock moved to owner hotkey should use OwnerLock" + ); + let owner_lock = OwnerLock::::get(netuid).unwrap(); + assert_eq!(owner_lock.locked_mass, lock_amount); + assert_eq!(owner_lock.conviction, U64F64::from_num(5000)); }); } @@ -3256,6 +3714,7 @@ fn test_moving_partial_lock() { false, ) .unwrap(); + DecayingLock::::insert(coldkey2, netuid, false); let lock_amount = 5000u64.into(); assert_ok!(SubtensorModule::do_lock_stake( @@ -3340,6 +3799,7 @@ fn test_moving_partial_lock_same_owners() { false, ) .unwrap(); + DecayingLock::::insert(coldkey2, netuid, false); let lock_amount = 5000u64.into(); assert_ok!(SubtensorModule::do_lock_stake( diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index a4c68e9d1b..f13c2ae186 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -7,6 +7,7 @@ )] use super::mock::*; +use crate::staking::lock::LockState; use crate::*; use alloc::collections::BTreeMap; use approx::{assert_abs_diff_eq, assert_relative_eq}; @@ -4394,3 +4395,97 @@ fn test_migrate_fix_total_issuance_evm_fees() { ); }); } + +#[test] +fn test_migrate_reset_tnet_conviction_locks() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &[u8] = b"migrate_reset_tnet_conviction_locks"; + + let netuid = NetUid::from(1); + let other_netuid = NetUid::from(2); + let coldkey_1 = U256::from(1001); + let coldkey_2 = U256::from(1002); + let hotkey_1 = U256::from(2001); + let hotkey_2 = U256::from(2002); + + let lock_1 = LockState { + locked_mass: AlphaBalance::from(10_u64), + conviction: U64F64::from_num(1.5), + last_update: 11, + }; + let lock_2 = LockState { + locked_mass: AlphaBalance::from(20_u64), + conviction: U64F64::from_num(2.5), + last_update: 22, + }; + + Lock::::insert((coldkey_1, netuid, hotkey_1), lock_1.clone()); + Lock::::insert((coldkey_2, other_netuid, hotkey_2), lock_2.clone()); + HotkeyLock::::insert(netuid, hotkey_1, lock_1.clone()); + DecayingHotkeyLock::::insert(other_netuid, hotkey_2, lock_2.clone()); + OwnerLock::::insert(netuid, lock_1.clone()); + DecayingOwnerLock::::insert(other_netuid, lock_2.clone()); + DecayingLock::::insert(coldkey_1, netuid, false); + DecayingLock::::insert(coldkey_2, other_netuid, false); + + assert!(!HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + assert_eq!(Lock::::iter().count(), 2); + assert_eq!(HotkeyLock::::iter().count(), 1); + assert_eq!(DecayingHotkeyLock::::iter().count(), 1); + assert_eq!(OwnerLock::::iter().count(), 1); + assert_eq!(DecayingOwnerLock::::iter().count(), 1); + assert_eq!(DecayingLock::::iter().count(), 2); + + let raw_owner_lock_key = { + let mut key = Vec::new(); + key.extend_from_slice(&twox_128("SubtensorModule".as_bytes())); + key.extend_from_slice(&twox_128("OwnerLock".as_bytes())); + key.extend_from_slice(&NetUid::from(99).encode()); + key + }; + let raw_decaying_hotkey_lock_key = { + let mut key = Vec::new(); + key.extend_from_slice(&twox_128("SubtensorModule".as_bytes())); + key.extend_from_slice(&twox_128("DecayingHotkeyLock".as_bytes())); + key.extend_from_slice(&NetUid::from(100).encode()); + key.extend_from_slice(&Blake2_128Concat::hash(&U256::from(3003).encode())); + key + }; + + // Simulate deprecated aggregate entries with bytes that the current + // `LockState` type should never need to decode during this reset. + put_raw(&raw_owner_lock_key, &123_u32.encode()); + put_raw(&raw_decaying_hotkey_lock_key, &(456_u32, 789_u32).encode()); + assert!(get_raw(&raw_owner_lock_key).is_some()); + assert!(get_raw(&raw_decaying_hotkey_lock_key).is_some()); + + let weight = + crate::migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::(); + + assert!(!weight.is_zero(), "migration weight should be non-zero"); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + assert!(get_raw(&raw_owner_lock_key).is_none()); + assert!(get_raw(&raw_decaying_hotkey_lock_key).is_none()); + assert_eq!(Lock::::iter().count(), 0); + assert_eq!(HotkeyLock::::iter().count(), 0); + assert_eq!(DecayingHotkeyLock::::iter().count(), 0); + assert_eq!(OwnerLock::::iter().count(), 0); + assert_eq!(DecayingOwnerLock::::iter().count(), 0); + assert_eq!(DecayingLock::::iter().count(), 0); + + Lock::::insert((coldkey_1, netuid, hotkey_1), lock_1); + let second_weight = + crate::migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::(); + + assert_eq!( + second_weight, + ::DbWeight::get().reads(1), + "second run should only read the migration flag" + ); + assert_eq!( + Lock::::iter().count(), + 1, + "migration must not run more than once" + ); + }); +} diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index d55a8e8411..d236ea0c8a 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -435,6 +435,7 @@ fn dissolve_clears_all_per_subnet_storages() { Yuma3On::::insert(net, true); AlphaValues::::insert(net, (1u16, 2u16)); SubtokenEnabled::::insert(net, true); + OwnerCutAutoLockEnabled::::insert(net, true); ImmuneOwnerUidsLimit::::insert(net, 1u16); // Per‑subnet vectors / indexes @@ -590,6 +591,7 @@ fn dissolve_clears_all_per_subnet_storages() { assert!(!Yuma3On::::contains_key(net)); assert!(!AlphaValues::::contains_key(net)); assert!(!SubtokenEnabled::::contains_key(net)); + assert!(!OwnerCutAutoLockEnabled::::contains_key(net)); assert!(!ImmuneOwnerUidsLimit::::contains_key(net)); // Per‑subnet vectors / indexes @@ -2318,9 +2320,9 @@ fn dissolve_clears_all_lock_maps_for_removed_network() { OwnerLock::::insert(other_net, lock_b.clone()); // --- DecayingLock - DecayingLock::::insert(cold_1, net, true); - DecayingLock::::insert(cold_2, net, true); - DecayingLock::::insert(cold_1, other_net, true); + DecayingLock::::insert(cold_1, net, false); + DecayingLock::::insert(cold_2, net, false); + DecayingLock::::insert(cold_1, other_net, false); // Sanity checks before dissolve assert!(Lock::::contains_key((cold_1, net, hot_1))); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 735ebd03d2..65b81748bd 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -274,7 +274,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: 408, + spec_version: 409, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -2549,6 +2549,10 @@ impl_runtime_apis! { SubtensorModule::get_stake_fee( origin, origin_coldkey_account, destination, destination_coldkey_account, amount ) } + fn get_coldkey_lock(coldkey: AccountId32, netuid: NetUid) -> Option { + SubtensorModule::get_coldkey_lock(&coldkey, netuid) + } + fn get_hotkey_conviction(hotkey: AccountId32, netuid: NetUid) -> U64F64 { SubtensorModule::hotkey_conviction(&hotkey, netuid) }