From d19b049e42b882b8284906ce59dcfe35a49d8622 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 21 May 2026 16:27:09 -0400 Subject: [PATCH 1/8] Anyone is able to lock stake to subnet owners hotkey and receives immediate conviction --- pallets/subtensor/src/lib.rs | 6 +- pallets/subtensor/src/staking/lock.rs | 661 ++++++++++++++------------ pallets/subtensor/src/tests/locks.rs | 308 +++++++++++- 3 files changed, 666 insertions(+), 309 deletions(-) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 8e9b31d93d..6f6da2fbc1 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1556,10 +1556,14 @@ 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>; + /// --- 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, this coldkey's lock decays. /// Missing entries mean the lock is perpetual. #[pallet::storage] diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index d6fd16a483..637296c0d7 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -73,14 +73,89 @@ impl Pallet { } } - fn is_subnet_owner_coldkey(netuid: NetUid, coldkey: &T::AccountId) -> bool { - coldkey == &SubnetOwner::::get(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::::contains_key(coldkey, netuid) } + fn empty_lock(now: u64) -> LockState { + LockState { + locked_mass: AlphaBalance::ZERO, + conviction: U64F64::saturating_from_num(0), + last_update: now, + } + } + + fn aggregate_lock_mode( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + netuid: NetUid, + ) -> (bool, bool) { + ( + Self::is_subnet_owner_hotkey(netuid, hotkey), + Self::is_perpetual_lock(coldkey, netuid), + ) + } + + fn roll_forward_aggregate_lock( + lock: LockState, + now: u64, + owner_lock: bool, + perpetual_lock: bool, + ) -> LockState { + Self::roll_forward_lock(lock, now, owner_lock, perpetual_lock) + } + + fn get_aggregate_lock( + netuid: NetUid, + hotkey: &T::AccountId, + owner_lock: bool, + perpetual_lock: bool, + now: u64, + ) -> Option { + if owner_lock && perpetual_lock { + OwnerLock::::get(netuid) + } else if owner_lock { + DecayingOwnerLock::::get(netuid) + } else if perpetual_lock { + HotkeyLock::::get(netuid, hotkey) + } else { + DecayingHotkeyLock::::get(netuid, hotkey) + } + .map(|lock| Self::roll_forward_aggregate_lock(lock, now, owner_lock, perpetual_lock)) + } + + fn insert_aggregate_lock( + netuid: NetUid, + hotkey: &T::AccountId, + owner_lock: bool, + perpetual_lock: bool, + lock: LockState, + ) { + if owner_lock && perpetual_lock { + Self::insert_owner_lock_state(netuid, lock); + } else if owner_lock { + Self::insert_decaying_owner_lock_state(netuid, lock); + } else if perpetual_lock { + Self::insert_hotkey_lock_state(netuid, hotkey, lock); + } else { + Self::insert_decaying_hotkey_lock_state(netuid, hotkey, lock); + } + } + /// Computes exp(-dt / tau) as a U64F64 decay factor. pub fn exp_decay(dt: u64, tau: u64) -> U64F64 { if tau == 0 || dt == 0 { @@ -195,24 +270,15 @@ impl Pallet { pub fn roll_forward_individual_lock( coldkey: &T::AccountId, netuid: NetUid, + hotkey: &T::AccountId, lock: LockState, now: u64, ) -> LockState { - let owner_lock = Self::is_subnet_owner_coldkey(netuid, coldkey); + let owner_lock = Self::is_subnet_owner_hotkey(netuid, hotkey); let perpetual_lock = Self::is_perpetual_lock(coldkey, netuid); Self::roll_forward_lock(lock, now, owner_lock, perpetual_lock) } - 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 roll_forward_hotkey_lock(_netuid: NetUid, lock: LockState, now: u64) -> LockState { Self::roll_forward_lock(lock, now, false, true) } @@ -234,7 +300,7 @@ impl Pallet { 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); + let rolled = Self::roll_forward_individual_lock(coldkey, netuid, &hotkey, lock, now); Self::insert_lock_state(coldkey, netuid, &hotkey, rolled.clone()); if current_enabled != enabled { @@ -282,8 +348,8 @@ impl Pallet { 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 + .map(|(hotkey, lock)| { + Self::roll_forward_individual_lock(coldkey, netuid, &hotkey, lock, now).locked_mass }) .unwrap_or(AlphaBalance::ZERO) } @@ -293,8 +359,8 @@ impl Pallet { 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 + .map(|(hotkey, lock)| { + Self::roll_forward_individual_lock(coldkey, netuid, &hotkey, lock, now).conviction }) .unwrap_or_else(|| U64F64::saturating_from_num(0)) } @@ -348,6 +414,7 @@ impl Pallet { let lock = Self::roll_forward_individual_lock( coldkey, netuid, + hotkey, LockState { locked_mass: amount, conviction: U64F64::saturating_from_num(0), @@ -360,13 +427,14 @@ impl Pallet { Some((existing_hotkey, existing)) => { ensure!(*hotkey == existing_hotkey, Error::::LockHotkeyMismatch); - let mut lock = Self::roll_forward_individual_lock(coldkey, netuid, existing, now); + let mut lock = + Self::roll_forward_individual_lock(coldkey, netuid, hotkey, 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); + let lock = Self::roll_forward_individual_lock(coldkey, netuid, hotkey, lock, now); Self::insert_lock_state(coldkey, netuid, hotkey, lock); } } @@ -388,7 +456,8 @@ impl Pallet { 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 rolled = + Self::roll_forward_individual_lock(coldkey, netuid, &existing_hotkey, lock, now); let new_locked_mass = rolled.locked_mass.saturating_sub(amount); let locked_mass_diff = rolled.locked_mass.saturating_sub(new_locked_mass); @@ -431,30 +500,16 @@ impl Pallet { // 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); + let rolled = Self::roll_forward_individual_lock(coldkey, netuid, &hotkey, lock, now); 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); - } + Self::reduce_aggregate_lock( + coldkey, + &hotkey, + netuid, + rolled.locked_mass, + rolled.conviction, + ); } } } @@ -471,44 +526,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 +545,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 +554,25 @@ 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 (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, hotkey, netuid); + let current = Self::get_aggregate_lock(netuid, hotkey, owner_lock, perpetual_lock, now) + .unwrap_or_else(|| Self::empty_lock(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_aggregate_lock( + netuid, + hotkey, + owner_lock, + perpetual_lock, + Self::roll_forward_aggregate_lock(merged, now, owner_lock, perpetual_lock), + ); } - /// 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,36 +580,15 @@ 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( + let (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, hotkey, netuid); + if let Some(rolled) = + Self::get_aggregate_lock(netuid, hotkey, owner_lock, perpetual_lock, now) + { + Self::insert_aggregate_lock( netuid, hotkey, + owner_lock, + perpetual_lock, LockState { locked_mass: rolled.locked_mass.saturating_sub(amount), conviction: rolled.conviction.saturating_sub(conviction), @@ -646,11 +610,15 @@ impl Pallet { .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| Self::roll_forward_lock(lock, now, true, true).conviction) + .unwrap_or_else(|| U64F64::saturating_from_num(0)); + let decaying_owner_conviction = DecayingOwnerLock::::get(netuid) + .map(|lock| Self::roll_forward_lock(lock, now, 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 } @@ -672,12 +640,16 @@ impl Pallet { acc.saturating_add(conviction) }); let owner_conviction = OwnerLock::::get(netuid) - .map(|lock| Self::roll_forward_owner_lock(netuid, lock, now).conviction) + .map(|lock| Self::roll_forward_lock(lock, now, true, true).conviction) + .unwrap_or_else(|| U64F64::saturating_from_num(0)); + let decaying_owner_conviction = DecayingOwnerLock::::get(netuid) + .map(|lock| Self::roll_forward_lock(lock, now, 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. @@ -701,7 +673,15 @@ 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 = Self::roll_forward_lock(lock, now, 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 = Self::roll_forward_lock(lock, now, true, false); let entry = scores .entry(owner_hotkey) .or_insert_with(|| U64F64::saturating_from_num(0)); @@ -770,85 +750,90 @@ 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 = Self::roll_forward_lock(owner_lock, now, true, true); + let current = HotkeyLock::::get(netuid, &old_owner_hotkey) + .map(|lock| Self::roll_forward_hotkey_lock(netuid, lock, now)) + .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 = Self::roll_forward_lock(owner_lock, now, true, false); + let current = DecayingHotkeyLock::::get(netuid, &old_owner_hotkey) + .map(|lock| Self::roll_forward_decaying_hotkey_lock(netuid, lock, now)) + .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 = Self::roll_forward_hotkey_lock(netuid, king_lock, now); + let current = OwnerLock::::get(netuid) + .map(|lock| Self::roll_forward_lock(lock, now, true, true)) + .unwrap_or_else(|| Self::empty_lock(now)); + Self::insert_owner_lock_state( + netuid, + Self::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, + true, + true, + ), + ); + } + if let Some(king_lock) = DecayingHotkeyLock::::take(netuid, &king_hotkey) { + let moved_king_lock = Self::roll_forward_decaying_hotkey_lock(netuid, king_lock, now); + let current = DecayingOwnerLock::::get(netuid) + .map(|lock| Self::roll_forward_lock(lock, now, true, false)) + .unwrap_or_else(|| Self::empty_lock(now)); + Self::insert_decaying_owner_lock_state( + netuid, + Self::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, + true, + false, + ), + ); } // Reassign subnet owner coldkey and owner hotkey. @@ -865,8 +850,8 @@ impl Pallet { pub fn ensure_no_active_locks(coldkey: &T::AccountId) -> Result<(), Error> { let now = Self::get_current_block_as_u64(); - 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 = Self::roll_forward_individual_lock(coldkey, netuid, &hotkey, lock, now); if rolled.locked_mass > AlphaBalance::ZERO { return Err(Error::::ActiveLockExists); } @@ -899,9 +884,15 @@ 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 old_lock = + Self::roll_forward_individual_lock(old_coldkey, netuid, &hotkey, lock, now); + let new_lock = Self::roll_forward_individual_lock( + new_coldkey, + netuid, + &hotkey, + old_lock.clone(), + now, + ); Lock::::remove((old_coldkey.clone(), netuid, hotkey.clone())); Self::reduce_aggregate_lock( old_coldkey, @@ -928,59 +919,110 @@ 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 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 = Self::roll_forward_lock(lock, now, old_owner_lock, perpetual_lock); + let moved = Self::roll_forward_lock(rolled, now, 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 moved_perpetual_lock = if old_was_owner { + OwnerLock::::take(netuid) + .map(|lock| Self::roll_forward_lock(lock, now, true, true)) + } else { + HotkeyLock::::take(netuid, old_hotkey) + .map(|lock| Self::roll_forward_hotkey_lock(netuid, lock, now)) + }; + let moved_decaying_lock = if old_was_owner { + DecayingOwnerLock::::take(netuid) + .map(|lock| Self::roll_forward_lock(lock, now, true, false)) + } else { + DecayingHotkeyLock::::take(netuid, old_hotkey) + .map(|lock| Self::roll_forward_decaying_hotkey_lock(netuid, lock, now)) + }; + + if let Some(lock) = moved_perpetual_lock { + if new_is_owner { + Self::insert_owner_lock_state( + netuid, + Self::roll_forward_lock(lock, now, true, true), + ); + } else { + Self::insert_hotkey_lock_state( + netuid, + new_hotkey, + Self::roll_forward_hotkey_lock(netuid, lock, now), + ); + } + } + if let Some(lock) = moved_decaying_lock { + if new_is_owner { + Self::insert_decaying_owner_lock_state( + netuid, + Self::roll_forward_lock(lock, now, true, false), + ); + } else { + Self::insert_decaying_hotkey_lock_state( + netuid, + new_hotkey, + Self::roll_forward_decaying_hotkey_lock(netuid, lock, now), + ); + } + } + writes = writes.saturating_add(6); } (reads, writes) } @@ -1006,7 +1048,13 @@ impl Pallet { match Lock::::iter_prefix((coldkey, netuid)).next() { Some((origin_hotkey, existing)) => { - let mut lock = Self::roll_forward_individual_lock(coldkey, netuid, existing, now); + let mut lock = Self::roll_forward_individual_lock( + coldkey, + netuid, + &origin_hotkey, + existing, + now, + ); let removed = lock.clone(); if Self::get_owning_coldkey_for_hotkey(&origin_hotkey) @@ -1014,7 +1062,13 @@ impl Pallet { { lock.conviction = U64F64::saturating_from_num(0); } - lock = Self::roll_forward_individual_lock(coldkey, netuid, lock, now); + lock = Self::roll_forward_individual_lock( + coldkey, + netuid, + destination_hotkey, + lock, + now, + ); Lock::::remove((coldkey.clone(), netuid, origin_hotkey.clone())); Self::insert_lock_state(coldkey, netuid, destination_hotkey, lock.clone()); @@ -1091,15 +1145,24 @@ impl Pallet { return Ok(()); }; - let mut source_lock = - Self::roll_forward_individual_lock(origin_coldkey, netuid, source_lock, now); + let mut source_lock = Self::roll_forward_individual_lock( + origin_coldkey, + netuid, + &source_hotkey, + 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 rolled_lock = Self::roll_forward_individual_lock( + destination_coldkey, + netuid, + &hotkey, + lock, + now, + ); + (hotkey, rolled_lock) }); let mut destination_hotkey = maybe_destination_lock @@ -1162,9 +1225,20 @@ 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 = Self::roll_forward_individual_lock( + origin_coldkey, + netuid, + &source_hotkey, + source_lock, + now, + ); + destination_lock = Self::roll_forward_individual_lock( + destination_coldkey, + netuid, + &destination_hotkey, + destination_lock, + now, + ); // Upsert updated locks (only once per this fn) even if there were no updates because // of roll-forward @@ -1238,8 +1312,9 @@ impl Pallet { } } - // OwnerLock: (netuid) + // OwnerLock / DecayingOwnerLock: (netuid) OwnerLock::::remove(netuid); + DecayingOwnerLock::::remove(netuid); // DecayingLock: (coldkey, netuid) { diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index f4eede30e4..b5b26b0c3c 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -127,6 +127,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 = SubtensorModule::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(|| { @@ -302,12 +421,14 @@ fn test_mixed_perpetual_and_decaying_non_owner_locks_same_hotkey_update_aggregat let perpetual_lock = SubtensorModule::roll_forward_individual_lock( &perpetual_coldkey, netuid, + &hotkey, Lock::::get((perpetual_coldkey, netuid, hotkey)).unwrap(), now, ); let decaying_lock = SubtensorModule::roll_forward_individual_lock( &decaying_coldkey, netuid, + &hotkey, Lock::::get((decaying_coldkey, netuid, hotkey)).unwrap(), now, ); @@ -382,8 +503,13 @@ 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); + let rolled = SubtensorModule::roll_forward_individual_lock( + &owner_coldkey, + netuid, + &owner_hotkey, + lock, + block, + ); SubtensorModule::insert_lock_state( &owner_coldkey, netuid, @@ -431,8 +557,9 @@ 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 = SubtensorModule::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,8 +612,9 @@ 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 = SubtensorModule::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()); @@ -1840,8 +1968,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) + .map(|((coldkey, _netuid, hotkey), lock)| { + SubtensorModule::roll_forward_individual_lock(&coldkey, netuid, &hotkey, lock, now) .conviction }) .fold(U64F64::from_num(0), |acc, conviction| { @@ -1943,6 +2071,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 +2094,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 +2116,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(decaying_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 = @@ -3194,7 +3468,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 +3491,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)); }); } From 2b9de9c3993331b326d88f6fb8d4050a90f3c0f1 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 22 May 2026 18:31:29 -0400 Subject: [PATCH 2/8] Add migration to clean the existing testnet conviction state --- pallets/subtensor/src/macros/hooks.rs | 4 +- .../migrate_reset_testnet_conviction_locks.rs | 81 ++++++++++++++++ pallets/subtensor/src/migrations/mod.rs | 1 + pallets/subtensor/src/tests/migration.rs | 95 +++++++++++++++++++ 4 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 pallets/subtensor/src/migrations/migrate_reset_testnet_conviction_locks.rs diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 55f6bd84a9..1870c16607 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_testnet_conviction_locks::migrate_reset_testnet_conviction_locks::()); weight } diff --git a/pallets/subtensor/src/migrations/migrate_reset_testnet_conviction_locks.rs b/pallets/subtensor/src/migrations/migrate_reset_testnet_conviction_locks.rs new file mode 100644 index 0000000000..9632c3a397 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_reset_testnet_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_testnet_conviction_locks() -> Weight { + let migration_name = b"migrate_reset_testnet_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..ff80dacd67 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_testnet_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/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index a4c68e9d1b..ab412c920a 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_testnet_conviction_locks() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &[u8] = b"migrate_reset_testnet_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_testnet_conviction_locks::migrate_reset_testnet_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_testnet_conviction_locks::migrate_reset_testnet_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" + ); + }); +} From bdc4f162a24e925088502f664df0578701409873 Mon Sep 17 00:00:00 2001 From: "subtensor-ai-review[bot]" Date: Fri, 22 May 2026 22:38:46 +0000 Subject: [PATCH 3/8] chore: auditor auto-fix --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 735ebd03d2..1730b62896 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, From 155f246f9430dad63620661b453d33a6b2291b3a Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 25 May 2026 09:55:53 -0400 Subject: [PATCH 4/8] Conviction cleanup refactoring - in progress --- pallets/subtensor/src/staking/lock.rs | 1204 ++++++++++++++++++++----- 1 file changed, 960 insertions(+), 244 deletions(-) diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index 637296c0d7..ef59aa0510 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -22,141 +22,296 @@ 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 { + /// 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, +} - 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); +impl ConvictionModel { + pub fn new( + individual_lock: LockState, + agg_perpetual_general: LockState, + agg_decaying_general: LockState, + agg_perpetual_owner: LockState, + agg_decaying_owner: LockState, + ) -> Self { + Self { + 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_decaying_hotkey_lock_state( - netuid: NetUid, - hotkey: &T::AccountId, - lock_state: LockState, + pub fn roll_forward( + &mut self, + now: u64, + unlock_rate: u64, + maturity_rate: u64, + individual_owner_lock: bool, + individual_perpetual_lock: bool, ) { - 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); - } + self.individual_lock = Self::roll_forward_lock( + self.individual_lock.clone(), + now, + unlock_rate, + maturity_rate, + individual_owner_lock, + individual_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 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 individual_lock(&self) -> &LockState { + &self.individual_lock } - 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); + 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, owner_lock: bool, perpetual_lock: bool) -> &LockState { + if owner_lock && perpetual_lock { + &self.agg_perpetual_owner + } else if owner_lock { + &self.agg_decaying_owner + } else if perpetual_lock { + &self.agg_perpetual_general } else { - DecayingOwnerLock::::remove(netuid); + &self.agg_decaying_general } } - fn is_subnet_owner_hotkey(netuid: NetUid, hotkey: &T::AccountId) -> bool { - hotkey == &SubnetOwnerHotkey::::get(netuid) + pub fn individual_lock_dirty(&self) -> bool { + self.individual_lock_dirty } - fn is_perpetual_lock(coldkey: &T::AccountId, netuid: NetUid) -> bool { - !DecayingLock::::contains_key(coldkey, netuid) + pub fn agg_perpetual_general_dirty(&self) -> bool { + self.agg_perpetual_general_dirty } - fn empty_lock(now: u64) -> LockState { - LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::saturating_from_num(0), - last_update: now, - } + pub fn agg_decaying_general_dirty(&self) -> bool { + self.agg_decaying_general_dirty } - fn aggregate_lock_mode( - coldkey: &T::AccountId, - hotkey: &T::AccountId, - netuid: NetUid, - ) -> (bool, bool) { - ( - Self::is_subnet_owner_hotkey(netuid, hotkey), - Self::is_perpetual_lock(coldkey, netuid), - ) + pub fn agg_perpetual_owner_dirty(&self) -> bool { + self.agg_perpetual_owner_dirty } - fn roll_forward_aggregate_lock( - lock: LockState, + 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 roll_forward_individual( + &mut self, now: u64, + unlock_rate: u64, + maturity_rate: u64, owner_lock: bool, perpetual_lock: bool, - ) -> LockState { - Self::roll_forward_lock(lock, now, owner_lock, perpetual_lock) + ) { + self.individual_lock = Self::roll_forward_lock( + self.individual_lock.clone(), + now, + unlock_rate, + maturity_rate, + owner_lock, + perpetual_lock, + ); + self.individual_lock_dirty = true; } - fn get_aggregate_lock( - netuid: NetUid, - hotkey: &T::AccountId, + pub fn roll_forward_aggregate( + &mut self, + now: u64, + unlock_rate: u64, + maturity_rate: u64, owner_lock: bool, perpetual_lock: bool, - now: u64, - ) -> Option { - if owner_lock && perpetual_lock { - OwnerLock::::get(netuid) - } else if owner_lock { - DecayingOwnerLock::::get(netuid) - } else if perpetual_lock { - HotkeyLock::::get(netuid, hotkey) - } else { - DecayingHotkeyLock::::get(netuid, hotkey) - } - .map(|lock| Self::roll_forward_aggregate_lock(lock, now, owner_lock, perpetual_lock)) + ) { + let (aggregate, aggregate_dirty) = self.aggregate_mut(owner_lock, perpetual_lock); + *aggregate = Self::roll_forward_lock( + aggregate.clone(), + now, + unlock_rate, + maturity_rate, + owner_lock, + perpetual_lock, + ); + *aggregate_dirty = true; } - fn insert_aggregate_lock( - netuid: NetUid, - hotkey: &T::AccountId, + pub fn add_to_aggregate(&mut self, added: &LockState, owner_lock: bool, perpetual_lock: bool) { + let (aggregate, aggregate_dirty) = self.aggregate_mut(owner_lock, perpetual_lock); + *aggregate = Self::merge_lock(aggregate, added); + *aggregate_dirty = true; + } + + pub fn reduce_aggregate( + &mut self, + locked_mass: AlphaBalance, + conviction: U64F64, owner_lock: bool, perpetual_lock: bool, - lock: LockState, ) { + let (aggregate, aggregate_dirty) = self.aggregate_mut(owner_lock, perpetual_lock); + *aggregate = Self::reduce_lock(aggregate, locked_mass, conviction); + *aggregate_dirty = true; + } + + pub fn reduce( + &mut self, + locked_mass: AlphaBalance, + conviction: U64F64, + owner_lock: bool, + perpetual_lock: bool, + ) { + self.individual_lock = Self::reduce_lock(&self.individual_lock, locked_mass, conviction); + self.individual_lock_dirty = true; + + let (aggregate, aggregate_dirty) = self.aggregate_mut(owner_lock, perpetual_lock); + *aggregate = Self::reduce_lock(aggregate, locked_mass, conviction); + *aggregate_dirty = true; + } + + fn aggregate_mut( + &mut self, + owner_lock: bool, + perpetual_lock: bool, + ) -> (&mut LockState, &mut bool) { if owner_lock && perpetual_lock { - Self::insert_owner_lock_state(netuid, lock); + ( + &mut self.agg_perpetual_owner, + &mut self.agg_perpetual_owner_dirty, + ) } else if owner_lock { - Self::insert_decaying_owner_lock_state(netuid, lock); + ( + &mut self.agg_decaying_owner, + &mut self.agg_decaying_owner_dirty, + ) } else if perpetual_lock { - Self::insert_hotkey_lock_state(netuid, hotkey, lock); + ( + &mut self.agg_perpetual_general, + &mut self.agg_perpetual_general_dirty, + ) } else { - Self::insert_decaying_hotkey_lock_state(netuid, hotkey, lock); + ( + &mut self.agg_decaying_general, + &mut self.agg_decaying_general_dirty, + ) + } + } + + 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 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 { @@ -181,11 +336,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); @@ -232,13 +386,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 { @@ -248,6 +400,8 @@ impl Pallet { lock.locked_mass, lock.conviction, dt, + unlock_rate, + maturity_rate, perpetual_lock, ); @@ -266,7 +420,161 @@ impl Pallet { rolled } +} + +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)); + } + } + + 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 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 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::::contains_key(coldkey, netuid) + } + + fn empty_lock(now: u64) -> LockState { + LockState { + locked_mass: AlphaBalance::ZERO, + conviction: U64F64::saturating_from_num(0), + last_update: now, + } + } + + fn aggregate_lock_mode( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + netuid: NetUid, + ) -> (bool, bool) { + ( + Self::is_subnet_owner_hotkey(netuid, hotkey), + Self::is_perpetual_lock(coldkey, netuid), + ) + } + + fn read_conviction_model( + coldkey: &T::AccountId, + netuid: NetUid, + hotkey: &T::AccountId, + now: u64, + ) -> ConvictionModel { + ConvictionModel::new( + 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 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()); + } + } + + #[cfg(test)] + pub fn exp_decay(dt: u64, tau: u64) -> U64F64 { + ConvictionModel::exp_decay(dt, tau) + } + + #[cfg(test)] + pub 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, + ) + } + #[cfg(test)] pub fn roll_forward_individual_lock( coldkey: &T::AccountId, netuid: NetUid, @@ -274,21 +582,42 @@ impl Pallet { lock: LockState, now: u64, ) -> LockState { - let owner_lock = Self::is_subnet_owner_hotkey(netuid, hotkey); - let perpetual_lock = Self::is_perpetual_lock(coldkey, netuid); - Self::roll_forward_lock(lock, now, owner_lock, perpetual_lock) + ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + Self::is_subnet_owner_hotkey(netuid, hotkey), + Self::is_perpetual_lock(coldkey, netuid), + ) } + #[cfg(test)] pub fn roll_forward_hotkey_lock(_netuid: NetUid, lock: LockState, now: u64) -> LockState { - Self::roll_forward_lock(lock, now, false, true) + ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + false, + true, + ) } + #[cfg(test)] pub fn roll_forward_decaying_hotkey_lock( _netuid: NetUid, lock: LockState, now: u64, ) -> LockState { - Self::roll_forward_lock(lock, now, false, false) + ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + false, + false, + ) } pub fn do_set_perpetual_lock( @@ -299,9 +628,18 @@ 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, &hotkey, lock, now); - Self::insert_lock_state(coldkey, netuid, &hotkey, rolled.clone()); + if let Some((hotkey, _lock)) = Lock::::iter_prefix((coldkey, netuid)).next() { + let (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, &hotkey, netuid); + let mut model = Self::read_conviction_model(coldkey, netuid, &hotkey, now); + model.roll_forward_individual( + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ); + let rolled = model.individual_lock().clone(); + Self::save_conviction_model(coldkey, netuid, &hotkey, model); if current_enabled != enabled { Self::reduce_aggregate_lock( @@ -348,8 +686,18 @@ impl Pallet { let now = Self::get_current_block_as_u64(); Lock::::iter_prefix((coldkey, netuid)) .next() - .map(|(hotkey, lock)| { - Self::roll_forward_individual_lock(coldkey, netuid, &hotkey, lock, now).locked_mass + .map(|(hotkey, _lock)| { + let (owner_lock, perpetual_lock) = + Self::aggregate_lock_mode(coldkey, &hotkey, netuid); + let mut model = Self::read_conviction_model(coldkey, netuid, &hotkey, now); + model.roll_forward_individual( + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ); + model.individual_lock().locked_mass }) .unwrap_or(AlphaBalance::ZERO) } @@ -359,8 +707,18 @@ impl Pallet { let now = Self::get_current_block_as_u64(); Lock::::iter_prefix((coldkey, netuid)) .next() - .map(|(hotkey, lock)| { - Self::roll_forward_individual_lock(coldkey, netuid, &hotkey, lock, now).conviction + .map(|(hotkey, _lock)| { + let (owner_lock, perpetual_lock) = + Self::aggregate_lock_mode(coldkey, &hotkey, netuid); + let mut model = Self::read_conviction_model(coldkey, netuid, &hotkey, now); + model.roll_forward_individual( + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ); + model.individual_lock().conviction }) .unwrap_or_else(|| U64F64::saturating_from_num(0)) } @@ -406,40 +764,77 @@ impl Pallet { let now = Self::get_current_block_as_u64(); let existing = Lock::::iter_prefix((coldkey, netuid)).next(); + let (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, hotkey, netuid); + let mut model = Self::read_conviction_model(coldkey, netuid, hotkey, now); + model.roll_forward_individual( + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ); match existing { None => { ensure!(total >= amount, Error::::InsufficientStakeForLock); - let lock = Self::roll_forward_individual_lock( - coldkey, - netuid, - hotkey, + model.set_individual_lock(ConvictionModel::roll_forward_lock( LockState { locked_mass: amount, conviction: U64F64::saturating_from_num(0), last_update: now, }, now, - ); - Self::insert_lock_state(coldkey, netuid, hotkey, lock); + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + )); } - Some((existing_hotkey, existing)) => { + Some((existing_hotkey, _existing)) => { ensure!(*hotkey == existing_hotkey, Error::::LockHotkeyMismatch); - let mut lock = - Self::roll_forward_individual_lock(coldkey, netuid, hotkey, existing, now); + let mut lock = model.individual_lock().clone(); 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, hotkey, lock, now); - Self::insert_lock_state(coldkey, netuid, hotkey, lock); + model.set_individual_lock(ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + )); } } - Self::upsert_aggregate_lock(coldkey, hotkey, netuid, amount); + model.roll_forward_aggregate( + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ); + model.add_to_aggregate( + &LockState { + locked_mass: amount, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }, + owner_lock, + perpetual_lock, + ); + model.roll_forward_aggregate( + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ); + Self::save_conviction_model(coldkey, netuid, hotkey, model); Self::deposit_event(Event::StakeLocked { coldkey: coldkey.clone(), @@ -454,16 +849,28 @@ 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() { + 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, &existing_hotkey, lock, now); + let (owner_lock, perpetual_lock) = + Self::aggregate_lock_mode(coldkey, &existing_hotkey, netuid); + let mut model = Self::read_conviction_model(coldkey, netuid, &existing_hotkey, now); + model.roll_forward_individual( + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ); + let rolled = model.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); - // Remove or update lock let conviction_diff = if new_locked_mass.is_zero() { - Lock::::remove((coldkey.clone(), netuid, existing_hotkey.clone())); + model.set_individual_lock(LockState { + locked_mass: AlphaBalance::ZERO, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }); rolled.conviction } else { let removed_proportion = U64F64::saturating_from_num(u64::from(amount)) @@ -471,25 +878,28 @@ impl Pallet { 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, - }, - ); + model.set_individual_lock(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, + model.roll_forward_aggregate( + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ); + model.reduce_aggregate( locked_mass_diff, conviction_diff, + owner_lock, + perpetual_lock, ); + Self::save_conviction_model(coldkey, netuid, &existing_hotkey, model); } } @@ -499,17 +909,37 @@ 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, &hotkey, lock, now); + if let Some((hotkey, _lock)) = Lock::::iter_prefix((coldkey.clone(), netuid)).next() { + let (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, &hotkey, netuid); + let mut model = Self::read_conviction_model(coldkey, netuid, &hotkey, now); + model.roll_forward_individual( + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ); + let rolled = model.individual_lock().clone(); if rolled.locked_mass.is_zero() { - Lock::::remove((coldkey.clone(), netuid, hotkey.clone())); - Self::reduce_aggregate_lock( - coldkey, - &hotkey, - netuid, + 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(), + owner_lock, + perpetual_lock, + ); + model.reduce_aggregate( rolled.locked_mass, rolled.conviction, + owner_lock, + perpetual_lock, ); + Self::save_conviction_model(coldkey, netuid, &hotkey, model); } } } @@ -555,20 +985,23 @@ impl Pallet { ) { let now = Self::get_current_block_as_u64(); let (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, hotkey, netuid); - let current = Self::get_aggregate_lock(netuid, hotkey, owner_lock, perpetual_lock, now) - .unwrap_or_else(|| Self::empty_lock(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_aggregate_lock( - netuid, - hotkey, + let mut model = Self::read_conviction_model(coldkey, netuid, hotkey, now); + model.roll_forward_aggregate( + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ); + model.add_to_aggregate(&added, owner_lock, perpetual_lock); + model.roll_forward_aggregate( + now, + UnlockRate::::get(), + MaturityRate::::get(), owner_lock, perpetual_lock, - Self::roll_forward_aggregate_lock(merged, now, owner_lock, perpetual_lock), ); + Self::save_conviction_model(coldkey, netuid, hotkey, model); } /// Reduces locked mass and conviction from exactly one aggregate bucket. @@ -581,40 +1014,77 @@ impl Pallet { ) { let now = Self::get_current_block_as_u64(); let (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, hotkey, netuid); - if let Some(rolled) = - Self::get_aggregate_lock(netuid, hotkey, owner_lock, perpetual_lock, now) - { - Self::insert_aggregate_lock( - netuid, - hotkey, - owner_lock, - perpetual_lock, - 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(coldkey, netuid, hotkey, now); + model.roll_forward_aggregate( + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ); + model.reduce_aggregate(amount, conviction, owner_lock, perpetual_lock); + 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) { let owner_conviction = OwnerLock::::get(netuid) - .map(|lock| Self::roll_forward_lock(lock, now, true, true).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| Self::roll_forward_lock(lock, now, true, false).conviction) + .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) @@ -627,23 +1097,63 @@ 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_lock(lock, now, true, true).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| Self::roll_forward_lock(lock, now, true, false).conviction) + .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 @@ -655,17 +1165,33 @@ impl Pallet { /// 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)); @@ -673,7 +1199,14 @@ impl Pallet { }); if let Some(lock) = OwnerLock::::get(netuid) { let owner_hotkey = SubnetOwnerHotkey::::get(netuid); - let rolled = Self::roll_forward_lock(lock, now, true, true); + 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)); @@ -681,7 +1214,14 @@ impl Pallet { } if let Some(lock) = DecayingOwnerLock::::get(netuid) { let owner_hotkey = SubnetOwnerHotkey::::get(netuid); - let rolled = Self::roll_forward_lock(lock, now, true, false); + 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)); @@ -742,6 +1282,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() @@ -752,9 +1294,25 @@ impl Pallet { // Move aggregate buckets using the hotkey's new role. if let Some(owner_lock) = OwnerLock::::take(netuid) { - let moved_owner_lock = Self::roll_forward_lock(owner_lock, now, true, true); + 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| Self::roll_forward_hotkey_lock(netuid, lock, now)) + .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, @@ -771,9 +1329,25 @@ impl Pallet { ); } if let Some(owner_lock) = DecayingOwnerLock::::take(netuid) { - let moved_owner_lock = Self::roll_forward_lock(owner_lock, now, true, false); + 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| Self::roll_forward_decaying_hotkey_lock(netuid, lock, now)) + .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, @@ -790,13 +1364,29 @@ impl Pallet { ); } if let Some(king_lock) = HotkeyLock::::take(netuid, &king_hotkey) { - let moved_king_lock = Self::roll_forward_hotkey_lock(netuid, king_lock, now); + let moved_king_lock = ConvictionModel::roll_forward_lock( + king_lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ); let current = OwnerLock::::get(netuid) - .map(|lock| Self::roll_forward_lock(lock, now, true, true)) + .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, - Self::roll_forward_lock( + ConvictionModel::roll_forward_lock( LockState { locked_mass: current .locked_mass @@ -807,19 +1397,37 @@ impl Pallet { last_update: now, }, now, + unlock_rate, + maturity_rate, true, true, ), ); } if let Some(king_lock) = DecayingHotkeyLock::::take(netuid, &king_hotkey) { - let moved_king_lock = Self::roll_forward_decaying_hotkey_lock(netuid, king_lock, now); + let moved_king_lock = ConvictionModel::roll_forward_lock( + king_lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ); let current = DecayingOwnerLock::::get(netuid) - .map(|lock| Self::roll_forward_lock(lock, now, true, false)) + .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, - Self::roll_forward_lock( + ConvictionModel::roll_forward_lock( LockState { locked_mass: current .locked_mass @@ -830,6 +1438,8 @@ impl Pallet { last_update: now, }, now, + unlock_rate, + maturity_rate, true, false, ), @@ -849,9 +1459,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, &hotkey, lock, now); + 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); } @@ -884,14 +1503,23 @@ 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, &hotkey, lock, now); - let new_lock = Self::roll_forward_individual_lock( - new_coldkey, - netuid, - &hotkey, + 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( @@ -963,6 +1591,8 @@ impl Pallet { for (coldkey, netuid, lock) in locks_to_transfer { let now = Self::get_current_block_as_u64(); + 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); @@ -970,8 +1600,22 @@ impl Pallet { .iter() .any(|(rebuild_netuid, _, is_owner)| *rebuild_netuid == netuid && *is_owner); let perpetual_lock = Self::is_perpetual_lock(&coldkey, netuid); - let rolled = Self::roll_forward_lock(lock, now, old_owner_lock, perpetual_lock); - let moved = Self::roll_forward_lock(rolled, now, new_owner_lock, perpetual_lock); + 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, moved); writes = writes.saturating_add(2); @@ -979,32 +1623,80 @@ impl Pallet { for (netuid, old_was_owner, new_is_owner) in netuids_to_transfer { let now = Self::get_current_block_as_u64(); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); let moved_perpetual_lock = if old_was_owner { - OwnerLock::::take(netuid) - .map(|lock| Self::roll_forward_lock(lock, now, true, true)) + 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| Self::roll_forward_hotkey_lock(netuid, lock, now)) + 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| Self::roll_forward_lock(lock, now, true, false)) + 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| Self::roll_forward_decaying_hotkey_lock(netuid, lock, now)) + 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, - Self::roll_forward_lock(lock, now, true, true), + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ), ); } else { Self::insert_hotkey_lock_state( netuid, new_hotkey, - Self::roll_forward_hotkey_lock(netuid, lock, now), + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ), ); } } @@ -1012,13 +1704,27 @@ impl Pallet { if new_is_owner { Self::insert_decaying_owner_lock_state( netuid, - Self::roll_forward_lock(lock, now, true, false), + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ), ); } else { Self::insert_decaying_hotkey_lock_state( netuid, new_hotkey, - Self::roll_forward_decaying_hotkey_lock(netuid, lock, now), + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ), ); } } @@ -1048,12 +1754,15 @@ impl Pallet { match Lock::::iter_prefix((coldkey, netuid)).next() { Some((origin_hotkey, existing)) => { - let mut lock = Self::roll_forward_individual_lock( - coldkey, - netuid, - &origin_hotkey, + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + let mut lock = ConvictionModel::roll_forward_lock( existing, now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &origin_hotkey), + Self::is_perpetual_lock(coldkey, netuid), ); let removed = lock.clone(); @@ -1062,12 +1771,13 @@ impl Pallet { { lock.conviction = U64F64::saturating_from_num(0); } - lock = Self::roll_forward_individual_lock( - coldkey, - netuid, - destination_hotkey, + 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())); @@ -1145,22 +1855,26 @@ impl Pallet { return Ok(()); }; - let mut source_lock = Self::roll_forward_individual_lock( - origin_coldkey, - netuid, - &source_hotkey, + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + let mut 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), ); let maybe_destination_lock = Lock::::iter_prefix((destination_coldkey, netuid)) .next() .map(|(hotkey, lock)| { - let rolled_lock = Self::roll_forward_individual_lock( - destination_coldkey, - netuid, - &hotkey, + let rolled_lock = ConvictionModel::roll_forward_lock( lock, now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &hotkey), + Self::is_perpetual_lock(destination_coldkey, netuid), ); (hotkey, rolled_lock) }); @@ -1225,19 +1939,21 @@ impl Pallet { .saturating_add(conviction_transfer); } - source_lock = Self::roll_forward_individual_lock( - origin_coldkey, - netuid, - &source_hotkey, + 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 = Self::roll_forward_individual_lock( - destination_coldkey, - netuid, - &destination_hotkey, + 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 From 6b36fd9571c1d30f5a7b80cc3f766c835083f32d Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 25 May 2026 13:01:46 -0400 Subject: [PATCH 5/8] Conviction cleanup refactoring, default decaying, togglable owner cut auto-lock, 1/2 year unlock rate, fix for owner immediate conviction --- pallets/admin-utils/src/lib.rs | 30 ++ pallets/admin-utils/src/tests/mod.rs | 48 +++ pallets/subtensor/src/coinbase/root.rs | 1 + pallets/subtensor/src/lib.rs | 13 +- pallets/subtensor/src/staking/lock.rs | 550 ++++++++---------------- pallets/subtensor/src/subnets/subnet.rs | 10 + pallets/subtensor/src/tests/locks.rs | 334 +++++++++----- pallets/subtensor/src/tests/networks.rs | 8 +- 8 files changed, 519 insertions(+), 475 deletions(-) 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..2c8ac4ed31 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, + true + )); + assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + + assert_ok!(AdminUtils::sudo_set_owner_cut_auto_lock_enabled( + <::RuntimeOrigin>::root(), + netuid, + false + )); + 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/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 b523fe80c2..dafd328d9a 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1564,16 +1564,21 @@ pub mod pallet { #[pallet::storage] pub type DecayingOwnerLock = StorageMap<_, Identity, NetUid, LockState, OptionQuery>; - /// --- DMAP ( coldkey, netuid ) --> false | When present, this coldkey's lock decays. - /// Missing entries mean the lock is perpetual. + /// --- 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. + /// --- MAP ( netuid ) --> bool | Whether subnet owner cut should be auto-locked. + /// Missing entries default to false, so auto-locking is opt-in per subnet. + #[pallet::storage] + pub type OwnerCutAutoLockEnabled = StorageMap<_, Identity, NetUid, bool, ValueQuery>; + + /// Default unlock timescale: 90% decay over half of 365.25 days at 12s blocks. #[pallet::type_value] pub fn DefaultUnlockRate() -> u64 { - 1_142_108 + 571_054 } /// Default maturity timescale: Conviction is ~5.2x faster than the default unlock rate. diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index ef59aa0510..d4750440cb 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -28,6 +28,10 @@ pub struct LockState { /// 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, @@ -47,6 +51,8 @@ pub struct ConvictionModel { impl ConvictionModel { pub fn new( + owner_lock: bool, + perpetual_lock: bool, individual_lock: LockState, agg_perpetual_general: LockState, agg_decaying_general: LockState, @@ -54,6 +60,8 @@ impl ConvictionModel { agg_decaying_owner: LockState, ) -> Self { Self { + owner_lock, + perpetual_lock, individual_lock, individual_lock_dirty: false, agg_perpetual_general, @@ -67,21 +75,14 @@ impl ConvictionModel { } } - pub fn roll_forward( - &mut self, - now: u64, - unlock_rate: u64, - maturity_rate: u64, - individual_owner_lock: bool, - individual_perpetual_lock: bool, - ) { + 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, - individual_owner_lock, - individual_perpetual_lock, + self.owner_lock, + self.perpetual_lock, ); self.individual_lock_dirty = true; self.agg_perpetual_general = Self::roll_forward_lock( @@ -142,12 +143,12 @@ impl ConvictionModel { &self.agg_decaying_owner } - pub fn aggregate_lock(&self, owner_lock: bool, perpetual_lock: bool) -> &LockState { - if owner_lock && perpetual_lock { + pub fn aggregate_lock(&self) -> &LockState { + if self.owner_lock && self.perpetual_lock { &self.agg_perpetual_owner - } else if owner_lock { + } else if self.owner_lock { &self.agg_decaying_owner - } else if perpetual_lock { + } else if self.perpetual_lock { &self.agg_perpetual_general } else { &self.agg_decaying_general @@ -196,34 +197,40 @@ impl ConvictionModel { self.individual_lock_dirty = true; } - pub fn roll_forward_individual( + pub fn set_rolled_individual_lock( &mut self, + lock: LockState, now: u64, unlock_rate: u64, maturity_rate: u64, - owner_lock: bool, - perpetual_lock: bool, ) { + 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, - owner_lock, - perpetual_lock, + 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, - owner_lock: bool, - perpetual_lock: bool, - ) { - let (aggregate, aggregate_dirty) = self.aggregate_mut(owner_lock, perpetual_lock); + 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, @@ -235,55 +242,69 @@ impl ConvictionModel { *aggregate_dirty = true; } - pub fn add_to_aggregate(&mut self, added: &LockState, owner_lock: bool, perpetual_lock: bool) { - let (aggregate, aggregate_dirty) = self.aggregate_mut(owner_lock, perpetual_lock); + 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, - owner_lock: bool, - perpetual_lock: bool, - ) { - let (aggregate, aggregate_dirty) = self.aggregate_mut(owner_lock, perpetual_lock); + 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, - owner_lock: bool, - perpetual_lock: bool, - ) { + 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(owner_lock, perpetual_lock); + let (aggregate, aggregate_dirty) = self.aggregate_mut(); *aggregate = Self::reduce_lock(aggregate, locked_mass, conviction); *aggregate_dirty = true; } - fn aggregate_mut( - &mut self, - owner_lock: bool, - perpetual_lock: bool, - ) -> (&mut LockState, &mut bool) { - if owner_lock && perpetual_lock { + 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 { + 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); + } + + 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 owner_lock { + } else if self.owner_lock { ( &mut self.agg_decaying_owner, &mut self.agg_decaying_owner_dirty, ) - } else if perpetual_lock { + } else if self.perpetual_lock { ( &mut self.agg_perpetual_general, &mut self.agg_perpetual_general_dirty, @@ -488,7 +509,7 @@ impl Pallet { } fn is_perpetual_lock(coldkey: &T::AccountId, netuid: NetUid) -> bool { - !DecayingLock::::contains_key(coldkey, netuid) + DecayingLock::::get(coldkey, netuid) == Some(false) } fn empty_lock(now: u64) -> LockState { @@ -499,24 +520,15 @@ impl Pallet { } } - fn aggregate_lock_mode( - coldkey: &T::AccountId, - hotkey: &T::AccountId, - netuid: NetUid, - ) -> (bool, bool) { - ( - Self::is_subnet_owner_hotkey(netuid, hotkey), - Self::is_perpetual_lock(coldkey, netuid), - ) - } - - fn read_conviction_model( + fn read_conviction_model_for_hotkey( coldkey: &T::AccountId, netuid: NetUid, hotkey: &T::AccountId, now: u64, ) -> 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)), @@ -525,6 +537,19 @@ impl Pallet { ) } + 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, @@ -552,74 +577,6 @@ impl Pallet { } } - #[cfg(test)] - pub fn exp_decay(dt: u64, tau: u64) -> U64F64 { - ConvictionModel::exp_decay(dt, tau) - } - - #[cfg(test)] - pub 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, - ) - } - - #[cfg(test)] - pub fn roll_forward_individual_lock( - coldkey: &T::AccountId, - netuid: NetUid, - hotkey: &T::AccountId, - lock: LockState, - now: u64, - ) -> LockState { - ConvictionModel::roll_forward_lock( - lock, - now, - UnlockRate::::get(), - MaturityRate::::get(), - Self::is_subnet_owner_hotkey(netuid, hotkey), - Self::is_perpetual_lock(coldkey, netuid), - ) - } - - #[cfg(test)] - pub fn roll_forward_hotkey_lock(_netuid: NetUid, lock: LockState, now: u64) -> LockState { - ConvictionModel::roll_forward_lock( - lock, - now, - UnlockRate::::get(), - MaturityRate::::get(), - false, - true, - ) - } - - #[cfg(test)] - pub fn roll_forward_decaying_hotkey_lock( - _netuid: NetUid, - lock: LockState, - now: u64, - ) -> LockState { - ConvictionModel::roll_forward_lock( - lock, - now, - UnlockRate::::get(), - MaturityRate::::get(), - false, - false, - ) - } - pub fn do_set_perpetual_lock( coldkey: &T::AccountId, netuid: NetUid, @@ -628,16 +585,8 @@ 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 (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, &hotkey, netuid); - let mut model = Self::read_conviction_model(coldkey, netuid, &hotkey, now); - model.roll_forward_individual( - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ); + 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); @@ -653,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(), @@ -684,18 +633,12 @@ 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)| { - let (owner_lock, perpetual_lock) = - Self::aggregate_lock_mode(coldkey, &hotkey, netuid); - let mut model = Self::read_conviction_model(coldkey, netuid, &hotkey, now); + Self::read_conviction_model(coldkey, netuid, now) + .map(|(_hotkey, mut model)| { model.roll_forward_individual( now, UnlockRate::::get(), MaturityRate::::get(), - owner_lock, - perpetual_lock, ); model.individual_lock().locked_mass }) @@ -705,18 +648,12 @@ 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)| { - let (owner_lock, perpetual_lock) = - Self::aggregate_lock_mode(coldkey, &hotkey, netuid); - let mut model = Self::read_conviction_model(coldkey, netuid, &hotkey, now); + Self::read_conviction_model(coldkey, netuid, now) + .map(|(_hotkey, mut model)| { model.roll_forward_individual( now, UnlockRate::::get(), MaturityRate::::get(), - owner_lock, - perpetual_lock, ); model.individual_lock().conviction }) @@ -763,77 +700,52 @@ 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(); - let (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, hotkey, netuid); - let mut model = Self::read_conviction_model(coldkey, netuid, hotkey, now); - model.roll_forward_individual( - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ); - - match existing { - None => { - ensure!(total >= amount, Error::::InsufficientStakeForLock); - - model.set_individual_lock(ConvictionModel::roll_forward_lock( - LockState { - locked_mass: amount, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }, - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_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 = model.individual_lock().clone(); - lock.locked_mass = lock.locked_mass.saturating_add(amount); - ensure!( - total >= lock.locked_mass, - Error::::InsufficientStakeForLock - ); - model.set_individual_lock(ConvictionModel::roll_forward_lock( - lock, - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_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(), + ); } - model.roll_forward_aggregate( - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ); - model.add_to_aggregate( - &LockState { - locked_mass: amount, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }, - owner_lock, - perpetual_lock, - ); - model.roll_forward_aggregate( - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ); + 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 { @@ -849,57 +761,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 (owner_lock, perpetual_lock) = - Self::aggregate_lock_mode(coldkey, &existing_hotkey, netuid); - let mut model = Self::read_conviction_model(coldkey, netuid, &existing_hotkey, now); - model.roll_forward_individual( - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ); - let rolled = model.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() { - model.set_individual_lock(LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }); - 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), - ); - model.set_individual_lock(LockState { - locked_mass: new_locked_mass, - conviction: new_conviction, - last_update: now, - }); - rolled.conviction.saturating_sub(new_conviction) - }; - - model.roll_forward_aggregate( - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ); - model.reduce_aggregate( - locked_mass_diff, - conviction_diff, - owner_lock, - perpetual_lock, - ); - Self::save_conviction_model(coldkey, netuid, &existing_hotkey, model); + 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); } } @@ -909,16 +776,8 @@ 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 (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, &hotkey, netuid); - let mut model = Self::read_conviction_model(coldkey, netuid, &hotkey, now); - model.roll_forward_individual( - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ); + 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() { model.set_individual_lock(LockState { @@ -926,19 +785,8 @@ impl Pallet { conviction: U64F64::saturating_from_num(0), last_update: now, }); - model.roll_forward_aggregate( - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ); - model.reduce_aggregate( - rolled.locked_mass, - rolled.conviction, - owner_lock, - perpetual_lock, - ); + 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); } } @@ -984,23 +832,10 @@ impl Pallet { added: LockState, ) { let now = Self::get_current_block_as_u64(); - let (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, hotkey, netuid); - let mut model = Self::read_conviction_model(coldkey, netuid, hotkey, now); - model.roll_forward_aggregate( - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ); - model.add_to_aggregate(&added, owner_lock, perpetual_lock); - model.roll_forward_aggregate( - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ); + 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); } @@ -1013,16 +848,9 @@ impl Pallet { conviction: U64F64, ) { let now = Self::get_current_block_as_u64(); - let (owner_lock, perpetual_lock) = Self::aggregate_lock_mode(coldkey, hotkey, netuid); - let mut model = Self::read_conviction_model(coldkey, netuid, hotkey, now); - model.roll_forward_aggregate( - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ); - model.reduce_aggregate(amount, conviction, owner_lock, perpetual_lock); + 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); } @@ -1752,18 +1580,12 @@ impl Pallet { ); let now = Self::get_current_block_as_u64(); - match Lock::::iter_prefix((coldkey, netuid)).next() { - Some((origin_hotkey, existing)) => { + match Self::read_conviction_model(coldkey, netuid, now) { + Some((origin_hotkey, mut model)) => { let unlock_rate = UnlockRate::::get(); let maturity_rate = MaturityRate::::get(); - let mut lock = ConvictionModel::roll_forward_lock( - existing, - now, - unlock_rate, - maturity_rate, - Self::is_subnet_owner_hotkey(netuid, &origin_hotkey), - Self::is_perpetual_lock(coldkey, netuid), - ); + 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) @@ -1804,13 +1626,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) @@ -1849,34 +1677,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 unlock_rate = UnlockRate::::get(); let maturity_rate = MaturityRate::::get(); - let mut 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), - ); - let maybe_destination_lock = Lock::::iter_prefix((destination_coldkey, netuid)) - .next() - .map(|(hotkey, lock)| { - let rolled_lock = ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - Self::is_subnet_owner_hotkey(netuid, &hotkey), - Self::is_perpetual_lock(destination_coldkey, netuid), - ); - (hotkey, rolled_lock) + 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 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 b5b26b0c3c..cf8ca7198b 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(|| { @@ -180,7 +245,7 @@ fn test_decaying_lock_to_subnet_owner_hotkey_keeps_decaying_mass() { step_block(1_000); let now = SubtensorModule::get_current_block_as_u64(); - let rolled = SubtensorModule::roll_forward_individual_lock( + let rolled = roll_forward_individual_lock( &coldkey, netuid, &owner_hotkey, @@ -418,27 +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, ); @@ -503,13 +564,8 @@ 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, - &owner_hotkey, - lock, - block, - ); + let rolled = + roll_forward_individual_lock(&owner_coldkey, netuid, &owner_hotkey, lock, block); SubtensorModule::insert_lock_state( &owner_coldkey, netuid, @@ -557,9 +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, &hotkey, 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!( @@ -612,14 +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, &hotkey, 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!( "{},{},{}", @@ -967,13 +1019,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)); }); } @@ -981,7 +1033,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)); }); } @@ -990,7 +1042,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 { @@ -1006,8 +1058,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 @@ -1021,32 +1073,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); + }); +} - assert!(locked < lock_amount.into()); - assert!(locked > AlphaBalance::ZERO); +#[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!(rolled.locked_mass < lock_amount.into()); + assert!(rolled.locked_mass > AlphaBalance::ZERO); }); } @@ -1065,10 +1182,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))); @@ -1097,7 +1214,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(); @@ -1135,8 +1252,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, @@ -1162,9 +1279,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, @@ -1201,7 +1318,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); } }); @@ -1224,7 +1341,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)); }); @@ -1240,8 +1357,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)); @@ -1266,7 +1382,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)); } @@ -1276,40 +1392,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); }); } @@ -1322,7 +1424,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); @@ -1451,6 +1553,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(); @@ -1478,7 +1581,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, @@ -1495,7 +1598,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, @@ -1584,7 +1687,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()); }); @@ -1625,6 +1728,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( @@ -1844,13 +1948,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, @@ -1969,8 +2073,7 @@ fn test_total_conviction_equals_sum_of_individual_lock_convictions_for_many_lock 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, &hotkey, lock, now) - .conviction + roll_forward_individual_lock(&coldkey, netuid, &hotkey, lock, now).conviction }) .fold(U64F64::from_num(0), |acc, conviction| { acc.saturating_add(conviction) @@ -2146,7 +2249,7 @@ fn test_change_subnet_owner_rebuilds_old_owner_hotkey_by_lock_mode() { System::set_block_number(now); NetworkRegisteredAt::::insert(netuid, 1); SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000u64)); - DecayingLock::::insert(decaying_coldkey, netuid, false); + DecayingLock::::insert(perpetual_coldkey, netuid, false); Lock::::insert( (perpetual_coldkey, netuid, old_owner_hotkey), @@ -2452,6 +2555,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 { @@ -3158,6 +3262,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); @@ -3306,6 +3411,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(); @@ -3358,6 +3464,32 @@ fn test_epoch_distribution_auto_locks_owner_cut() { }); } +#[test] +fn test_auto_lock_owner_cut_is_disabled_by_default() { + 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); + assert!( + Lock::::iter_prefix((subnet_owner_coldkey, netuid)) + .next() + .is_none() + ); + + OwnerCutAutoLockEnabled::::insert(netuid, true); + 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 once enabled"); + assert_eq!(owner_lock.locked_mass, owner_cut); + }); +} + // ========================================================================= // GROUP 18: Neuron replacement // ========================================================================= @@ -3534,6 +3666,7 @@ fn test_moving_partial_lock() { false, ) .unwrap(); + DecayingLock::::insert(coldkey2, netuid, false); let lock_amount = 5000u64.into(); assert_ok!(SubtensorModule::do_lock_stake( @@ -3618,6 +3751,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/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))); From 091058cd1fdfaedbb9d2bce7291a36952e6fded1 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 25 May 2026 13:19:55 -0400 Subject: [PATCH 6/8] Default to auto-lock owner cut, remove test word from migration --- pallets/admin-utils/src/tests/mod.rs | 10 +++---- pallets/subtensor/src/lib.rs | 19 ++++++++----- pallets/subtensor/src/macros/hooks.rs | 2 +- ...=> migrate_reset_tnet_conviction_locks.rs} | 4 +-- pallets/subtensor/src/migrations/mod.rs | 2 +- pallets/subtensor/src/tests/locks.rs | 27 ++++++++++++------- pallets/subtensor/src/tests/migration.rs | 8 +++--- 7 files changed, 43 insertions(+), 29 deletions(-) rename pallets/subtensor/src/migrations/{migrate_reset_testnet_conviction_locks.rs => migrate_reset_tnet_conviction_locks.rs} (95%) diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index 2c8ac4ed31..67be797c78 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -2479,7 +2479,7 @@ fn test_sudo_set_owner_cut_auto_lock_enabled() { 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!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); assert_noop!( AdminUtils::sudo_set_owner_cut_auto_lock_enabled( <::RuntimeOrigin>::signed(non_owner), @@ -2492,16 +2492,16 @@ fn test_sudo_set_owner_cut_auto_lock_enabled() { assert_ok!(AdminUtils::sudo_set_owner_cut_auto_lock_enabled( <::RuntimeOrigin>::signed(owner), netuid, - true + false )); - assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + assert!(!SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); assert_ok!(AdminUtils::sudo_set_owner_cut_auto_lock_enabled( <::RuntimeOrigin>::root(), netuid, - false + true )); - assert!(!SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); }); } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index dafd328d9a..934d47e34d 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1570,21 +1570,28 @@ pub mod pallet { pub type DecayingLock = StorageDoubleMap<_, Blake2_128Concat, T::AccountId, Identity, NetUid, bool, OptionQuery>; + /// 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 false, so auto-locking is opt-in per subnet. + /// Missing entries default to true, so auto-locking is enabled unless explicitly disabled. #[pallet::storage] - pub type OwnerCutAutoLockEnabled = StorageMap<_, Identity, NetUid, bool, ValueQuery>; + pub type OwnerCutAutoLockEnabled = + StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultOwnerCutAutoLockEnabled>; - /// Default unlock timescale: 90% decay over half of 365.25 days at 12s blocks. + /// Default unlock timescale: 50% lock back in 1 month. #[pallet::type_value] pub fn DefaultUnlockRate() -> u64 { - 571_054 + 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 1870c16607..8a0791b881 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -180,7 +180,7 @@ mod hooks { // Remove deprecated conviction lock storage. .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_testnet_conviction_locks::migrate_reset_testnet_conviction_locks::()); + .saturating_add(migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::()); weight } diff --git a/pallets/subtensor/src/migrations/migrate_reset_testnet_conviction_locks.rs b/pallets/subtensor/src/migrations/migrate_reset_tnet_conviction_locks.rs similarity index 95% rename from pallets/subtensor/src/migrations/migrate_reset_testnet_conviction_locks.rs rename to pallets/subtensor/src/migrations/migrate_reset_tnet_conviction_locks.rs index 9632c3a397..277af2037e 100644 --- a/pallets/subtensor/src/migrations/migrate_reset_testnet_conviction_locks.rs +++ b/pallets/subtensor/src/migrations/migrate_reset_tnet_conviction_locks.rs @@ -9,8 +9,8 @@ use scale_info::prelude::string::String; /// 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_testnet_conviction_locks() -> Weight { - let migration_name = b"migrate_reset_testnet_conviction_locks".to_vec(); +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) { diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index ff80dacd67..ae0188ec63 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -49,7 +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_testnet_conviction_locks; +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/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index cf8ca7198b..e60150327d 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -1172,8 +1172,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); @@ -1327,6 +1328,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; @@ -3465,7 +3469,7 @@ fn test_epoch_distribution_auto_locks_owner_cut() { } #[test] -fn test_auto_lock_owner_cut_is_disabled_by_default() { +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); @@ -3473,20 +3477,23 @@ fn test_auto_lock_owner_cut_is_disabled_by_default() { 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() ); - - OwnerCutAutoLockEnabled::::insert(netuid, true); - 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 once enabled"); - assert_eq!(owner_lock.locked_mass, owner_cut); }); } diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index ab412c920a..f13c2ae186 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -4397,9 +4397,9 @@ fn test_migrate_fix_total_issuance_evm_fees() { } #[test] -fn test_migrate_reset_testnet_conviction_locks() { +fn test_migrate_reset_tnet_conviction_locks() { new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &[u8] = b"migrate_reset_testnet_conviction_locks"; + const MIGRATION_NAME: &[u8] = b"migrate_reset_tnet_conviction_locks"; let netuid = NetUid::from(1); let other_netuid = NetUid::from(2); @@ -4460,7 +4460,7 @@ fn test_migrate_reset_testnet_conviction_locks() { assert!(get_raw(&raw_decaying_hotkey_lock_key).is_some()); let weight = - crate::migrations::migrate_reset_testnet_conviction_locks::migrate_reset_testnet_conviction_locks::(); + 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())); @@ -4475,7 +4475,7 @@ fn test_migrate_reset_testnet_conviction_locks() { Lock::::insert((coldkey_1, netuid, hotkey_1), lock_1); let second_weight = - crate::migrations::migrate_reset_testnet_conviction_locks::migrate_reset_testnet_conviction_locks::(); + crate::migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::(); assert_eq!( second_weight, From af92d767ad49699164142247b62fae75bb89f9d6 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 25 May 2026 14:01:14 -0400 Subject: [PATCH 7/8] Add get_coldkey_lock RPC --- pallets/subtensor/rpc/src/lib.rs | 25 ++++++++++++++- pallets/subtensor/runtime-api/src/lib.rs | 2 ++ pallets/subtensor/src/staking/lock.rs | 9 ++++++ pallets/subtensor/src/tests/locks.rs | 41 ++++++++++++++++++++++++ runtime/src/lib.rs | 4 +++ 5 files changed, 80 insertions(+), 1 deletion(-) 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/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index d4750440cb..27c5e5d646 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -660,6 +660,15 @@ impl Pallet { .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); diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index e60150327d..92218ae51d 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -770,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(|| { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 1730b62896..65b81748bd 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -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) } From dec8c87d4e52b2c2b9b540faa21c7ec19968ac08 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 25 May 2026 15:38:20 -0400 Subject: [PATCH 8/8] ping ci