Skip to content

Commit a3ef6e0

Browse files
Merge pull request #611 from opentensor/feat/cold_key_swap
Feat/cold key swap
2 parents d81e523 + a0fa527 commit a3ef6e0

File tree

11 files changed

+1175
-19
lines changed

11 files changed

+1175
-19
lines changed

justfile

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,11 @@ clippy:
3131

3232
clippy-fix:
3333
@echo "Running cargo clippy with automatic fixes on potentially dirty code..."
34-
cargo +{{RUSTV}} clippy --fix --allow-dirty --workspace --all-targets -- \
35-
-A clippy::todo \
36-
-A clippy::unimplemented \
37-
-A clippy::indexing_slicing
38-
@echo "Running cargo clippy with automatic fixes on potentially dirty code..."
39-
cargo +{{RUSTV}} clippy --fix --allow-dirty --workspace --all-targets -- \
34+
cargo +{{RUSTV}} clippy --fix --allow-dirty --allow-staged --workspace --all-targets -- \
4035
-A clippy::todo \
4136
-A clippy::unimplemented \
4237
-A clippy::indexing_slicing
38+
4339
fix:
4440
@echo "Running cargo fix..."
4541
cargo +{{RUSTV}} fix --workspace

pallets/subtensor/src/errors.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,19 @@ mod errors {
132132
AlphaHighTooLow,
133133
/// Alpha low is out of range: alpha_low > 0 && alpha_low < 0.8
134134
AlphaLowOutOfRange,
135+
/// The coldkey has already been swapped
136+
ColdKeyAlreadyAssociated,
137+
/// The coldkey swap transaction rate limit exceeded
138+
ColdKeySwapTxRateLimitExceeded,
139+
/// The new coldkey is the same as the old coldkey
140+
NewColdKeyIsSameWithOld,
141+
/// The coldkey does not exist
142+
NotExistColdkey,
143+
/// The coldkey balance is not enough to pay for the swap
144+
NotEnoughBalanceToPaySwapColdKey,
145+
/// No balance to transfer
146+
NoBalanceToTransfer,
147+
/// Same coldkey
148+
SameColdkey,
135149
}
136150
}

pallets/subtensor/src/events.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,27 @@ mod events {
132132
MinDelegateTakeSet(u16),
133133
/// the target stakes per interval is set by sudo/admin transaction
134134
TargetStakesPerIntervalSet(u64),
135+
/// A coldkey has been swapped
136+
ColdkeySwapped {
137+
/// the account ID of old coldkey
138+
old_coldkey: T::AccountId,
139+
/// the account ID of new coldkey
140+
new_coldkey: T::AccountId,
141+
},
142+
/// All balance of a hotkey has been unstaked and transferred to a new coldkey
143+
AllBalanceUnstakedAndTransferredToNewColdkey {
144+
/// The account ID of the current coldkey
145+
current_coldkey: T::AccountId,
146+
/// The account ID of the new coldkey
147+
new_coldkey: T::AccountId,
148+
/// The account ID of the hotkey
149+
hotkey: T::AccountId,
150+
/// The current stake of the hotkey
151+
current_stake: u64,
152+
/// The total balance of the hotkey
153+
total_balance: <<T as Config>::Currency as fungible::Inspect<
154+
<T as frame_system::Config>::AccountId,
155+
>>::Balance,
156+
},
135157
}
136158
}

pallets/subtensor/src/lib.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@ pub mod pallet {
363363
#[pallet::storage] // --- MAP ( hot ) --> cold | Returns the controlling coldkey for a hotkey.
364364
pub type Owner<T: Config> =
365365
StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId, ValueQuery, DefaultAccount<T>>;
366+
#[pallet::storage] // --- MAP ( cold ) --> Vec<hot> | Returns the vector of hotkeys controlled by this coldkey.
367+
pub type OwnedHotkeys<T: Config> =
368+
StorageMap<_, Blake2_128Concat, T::AccountId, Vec<T::AccountId>, ValueQuery>;
366369
#[pallet::storage] // --- MAP ( hot ) --> take | Returns the hotkey delegation take. And signals that this key is open for delegation.
367370
pub type Delegates<T: Config> =
368371
StorageMap<_, Blake2_128Concat, T::AccountId, u16, ValueQuery, DefaultDefaultTake<T>>;
@@ -1204,6 +1207,13 @@ pub mod pallet {
12041207
// Fill stake information.
12051208
Owner::<T>::insert(hotkey.clone(), coldkey.clone());
12061209

1210+
// Update OwnedHotkeys map
1211+
let mut hotkeys = OwnedHotkeys::<T>::get(coldkey);
1212+
if !hotkeys.contains(hotkey) {
1213+
hotkeys.push(hotkey.clone());
1214+
OwnedHotkeys::<T>::insert(coldkey, hotkeys);
1215+
}
1216+
12071217
TotalHotkeyStake::<T>::insert(hotkey.clone(), stake);
12081218
TotalColdkeyStake::<T>::insert(
12091219
coldkey.clone(),
@@ -1325,7 +1335,9 @@ pub mod pallet {
13251335
// Storage version v4 -> v5
13261336
.saturating_add(migration::migrate_delete_subnet_3::<T>())
13271337
// Doesn't check storage version. TODO: Remove after upgrade
1328-
.saturating_add(migration::migration5_total_issuance::<T>(false));
1338+
.saturating_add(migration::migration5_total_issuance::<T>(false))
1339+
// Populate OwnedHotkeys map for coldkey swap. Doesn't update storage vesion.
1340+
.saturating_add(migration::migrate_populate_owned::<T>());
13291341

13301342
weight
13311343
}
@@ -1970,6 +1982,61 @@ pub mod pallet {
19701982
Self::do_swap_hotkey(origin, &hotkey, &new_hotkey)
19711983
}
19721984

1985+
/// The extrinsic for user to change the coldkey associated with their account.
1986+
///
1987+
/// # Arguments
1988+
///
1989+
/// * `origin` - The origin of the call, must be signed by the old coldkey.
1990+
/// * `old_coldkey` - The current coldkey associated with the account.
1991+
/// * `new_coldkey` - The new coldkey to be associated with the account.
1992+
///
1993+
/// # Returns
1994+
///
1995+
/// Returns a `DispatchResultWithPostInfo` indicating success or failure of the operation.
1996+
///
1997+
/// # Weight
1998+
///
1999+
/// Weight is calculated based on the number of database reads and writes.
2000+
#[pallet::call_index(71)]
2001+
#[pallet::weight((Weight::from_parts(1_940_000_000, 0)
2002+
.saturating_add(T::DbWeight::get().reads(272))
2003+
.saturating_add(T::DbWeight::get().writes(527)), DispatchClass::Operational, Pays::No))]
2004+
pub fn swap_coldkey(
2005+
origin: OriginFor<T>,
2006+
old_coldkey: T::AccountId,
2007+
new_coldkey: T::AccountId,
2008+
) -> DispatchResultWithPostInfo {
2009+
Self::do_swap_coldkey(origin, &old_coldkey, &new_coldkey)
2010+
}
2011+
2012+
/// Unstakes all tokens associated with a hotkey and transfers them to a new coldkey.
2013+
///
2014+
/// # Arguments
2015+
///
2016+
/// * `origin` - The origin of the call, must be signed by the current coldkey.
2017+
/// * `hotkey` - The hotkey associated with the stakes to be unstaked.
2018+
/// * `new_coldkey` - The new coldkey to receive the unstaked tokens.
2019+
///
2020+
/// # Returns
2021+
///
2022+
/// Returns a `DispatchResult` indicating success or failure of the operation.
2023+
///
2024+
/// # Weight
2025+
///
2026+
/// Weight is calculated based on the number of database reads and writes.
2027+
#[pallet::call_index(72)]
2028+
#[pallet::weight((Weight::from_parts(1_940_000_000, 0)
2029+
.saturating_add(T::DbWeight::get().reads(272))
2030+
.saturating_add(T::DbWeight::get().writes(527)), DispatchClass::Operational, Pays::No))]
2031+
pub fn unstake_all_and_transfer_to_new_coldkey(
2032+
origin: OriginFor<T>,
2033+
hotkey: T::AccountId,
2034+
new_coldkey: T::AccountId,
2035+
) -> DispatchResult {
2036+
let current_coldkey = ensure_signed(origin)?;
2037+
Self::do_unstake_all_and_transfer_to_new_coldkey(current_coldkey, hotkey, new_coldkey)
2038+
}
2039+
19732040
// ---- SUDO ONLY FUNCTIONS ------------------------------------------------------------
19742041

19752042
// ==================================

pallets/subtensor/src/migration.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,3 +477,65 @@ pub fn migrate_to_v2_fixed_total_stake<T: Config>() -> Weight {
477477
Weight::zero()
478478
}
479479
}
480+
481+
/// Migrate the OwnedHotkeys map to the new storage format
482+
pub fn migrate_populate_owned<T: Config>() -> Weight {
483+
// Setup migration weight
484+
let mut weight = T::DbWeight::get().reads(1);
485+
let migration_name = "Populate OwnedHotkeys map";
486+
487+
// Check if this migration is needed (if OwnedHotkeys map is empty)
488+
let migrate = OwnedHotkeys::<T>::iter().next().is_none();
489+
490+
// Only runs if the migration is needed
491+
if migrate {
492+
info!(target: LOG_TARGET_1, ">>> Starting Migration: {}", migration_name);
493+
494+
let mut longest_hotkey_vector: usize = 0;
495+
let mut longest_coldkey: Option<T::AccountId> = None;
496+
let mut keys_touched: u64 = 0;
497+
let mut storage_reads: u64 = 0;
498+
let mut storage_writes: u64 = 0;
499+
500+
// Iterate through all Owner entries
501+
Owner::<T>::iter().for_each(|(hotkey, coldkey)| {
502+
storage_reads = storage_reads.saturating_add(1); // Read from Owner storage
503+
let mut hotkeys = OwnedHotkeys::<T>::get(&coldkey);
504+
storage_reads = storage_reads.saturating_add(1); // Read from OwnedHotkeys storage
505+
506+
// Add the hotkey if it's not already in the vector
507+
if !hotkeys.contains(&hotkey) {
508+
hotkeys.push(hotkey);
509+
keys_touched = keys_touched.saturating_add(1);
510+
511+
// Update longest hotkey vector info
512+
if longest_hotkey_vector < hotkeys.len() {
513+
longest_hotkey_vector = hotkeys.len();
514+
longest_coldkey = Some(coldkey.clone());
515+
}
516+
517+
// Update the OwnedHotkeys storage
518+
OwnedHotkeys::<T>::insert(&coldkey, hotkeys);
519+
storage_writes = storage_writes.saturating_add(1); // Write to OwnedHotkeys storage
520+
}
521+
522+
// Accrue weight for reads and writes
523+
weight = weight.saturating_add(T::DbWeight::get().reads_writes(2, 1));
524+
});
525+
526+
// Log migration results
527+
info!(
528+
target: LOG_TARGET_1,
529+
"Migration {} finished. Keys touched: {}, Longest hotkey vector: {}, Storage reads: {}, Storage writes: {}",
530+
migration_name, keys_touched, longest_hotkey_vector, storage_reads, storage_writes
531+
);
532+
if let Some(c) = longest_coldkey {
533+
info!(target: LOG_TARGET_1, "Longest hotkey vector is controlled by: {:?}", c);
534+
}
535+
536+
weight
537+
} else {
538+
info!(target: LOG_TARGET_1, "Migration {} already done!", migration_name);
539+
Weight::zero()
540+
}
541+
}

pallets/subtensor/src/staking.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::*;
2+
use dispatch::RawOrigin;
23
use frame_support::{
34
storage::IterableStorageDoubleMap,
45
traits::{
@@ -9,6 +10,7 @@ use frame_support::{
910
Imbalance,
1011
},
1112
};
13+
use num_traits::Zero;
1214

1315
impl<T: Config> Pallet<T> {
1416
/// ---- The implementation for the extrinsic become_delegate: signals that this hotkey allows delegated stake.
@@ -560,6 +562,13 @@ impl<T: Config> Pallet<T> {
560562
if !Self::hotkey_account_exists(hotkey) {
561563
Stake::<T>::insert(hotkey, coldkey, 0);
562564
Owner::<T>::insert(hotkey, coldkey);
565+
566+
// Update OwnedHotkeys map
567+
let mut hotkeys = OwnedHotkeys::<T>::get(coldkey);
568+
if !hotkeys.contains(hotkey) {
569+
hotkeys.push(hotkey.clone());
570+
OwnedHotkeys::<T>::insert(coldkey, hotkeys);
571+
}
563572
}
564573
}
565574

@@ -781,6 +790,31 @@ impl<T: Config> Pallet<T> {
781790
Ok(credit)
782791
}
783792

793+
pub fn kill_coldkey_account(
794+
coldkey: &T::AccountId,
795+
amount: <<T as Config>::Currency as fungible::Inspect<<T as system::Config>::AccountId>>::Balance,
796+
) -> Result<u64, DispatchError> {
797+
if amount == 0 {
798+
return Ok(0);
799+
}
800+
801+
let credit = T::Currency::withdraw(
802+
coldkey,
803+
amount,
804+
Precision::Exact,
805+
Preservation::Expendable,
806+
Fortitude::Force,
807+
)
808+
.map_err(|_| Error::<T>::BalanceWithdrawalError)?
809+
.peek();
810+
811+
if credit == 0 {
812+
return Err(Error::<T>::ZeroBalanceAfterWithdrawn.into());
813+
}
814+
815+
Ok(credit)
816+
}
817+
784818
pub fn unstake_all_coldkeys_from_hotkey_account(hotkey: &T::AccountId) {
785819
// Iterate through all coldkeys that have a stake on this hotkey account.
786820
for (delegate_coldkey_i, stake_i) in
@@ -795,4 +829,96 @@ impl<T: Config> Pallet<T> {
795829
Self::add_balance_to_coldkey_account(&delegate_coldkey_i, stake_i);
796830
}
797831
}
832+
833+
/// Unstakes all tokens associated with a hotkey and transfers them to a new coldkey.
834+
///
835+
/// This function performs the following operations:
836+
/// 1. Verifies that the hotkey exists and is owned by the current coldkey.
837+
/// 2. Ensures that the new coldkey is different from the current one.
838+
/// 3. Unstakes all balance if there's any stake.
839+
/// 4. Transfers the entire balance of the hotkey to the new coldkey.
840+
/// 5. Verifies the success of the transfer and handles partial transfers if necessary.
841+
///
842+
/// # Arguments
843+
///
844+
/// * `current_coldkey` - The AccountId of the current coldkey.
845+
/// * `hotkey` - The AccountId of the hotkey whose balance is being unstaked and transferred.
846+
/// * `new_coldkey` - The AccountId of the new coldkey to receive the unstaked tokens.
847+
///
848+
/// # Returns
849+
///
850+
/// Returns a `DispatchResult` indicating success or failure of the operation.
851+
///
852+
/// # Errors
853+
///
854+
/// This function will return an error if:
855+
/// * The hotkey account does not exist.
856+
/// * The current coldkey does not own the hotkey.
857+
/// * The new coldkey is the same as the current coldkey.
858+
/// * There is no balance to transfer.
859+
/// * The transfer fails or is only partially successful.
860+
///
861+
/// # Events
862+
///
863+
/// Emits an `AllBalanceUnstakedAndTransferredToNewColdkey` event upon successful execution.
864+
/// Emits a `PartialBalanceTransferredToNewColdkey` event if only a partial transfer is successful.
865+
///
866+
pub fn do_unstake_all_and_transfer_to_new_coldkey(
867+
current_coldkey: T::AccountId,
868+
hotkey: T::AccountId,
869+
new_coldkey: T::AccountId,
870+
) -> DispatchResult {
871+
// Ensure the hotkey exists and is owned by the current coldkey
872+
ensure!(
873+
Self::hotkey_account_exists(&hotkey),
874+
Error::<T>::HotKeyAccountNotExists
875+
);
876+
ensure!(
877+
Self::coldkey_owns_hotkey(&current_coldkey, &hotkey),
878+
Error::<T>::NonAssociatedColdKey
879+
);
880+
881+
// Ensure the new coldkey is different from the current one
882+
ensure!(current_coldkey != new_coldkey, Error::<T>::SameColdkey);
883+
884+
// Get the current stake
885+
let current_stake: u64 = Self::get_stake_for_coldkey_and_hotkey(&current_coldkey, &hotkey);
886+
887+
// Unstake all balance if there's any stake
888+
if current_stake > 0 {
889+
Self::do_remove_stake(
890+
RawOrigin::Signed(current_coldkey.clone()).into(),
891+
hotkey.clone(),
892+
current_stake,
893+
)?;
894+
}
895+
896+
// Get the total balance of the current coldkey account
897+
// let total_balance: <<T as Config>::Currency as fungible::Inspect<<T as system::Config>::AccountId>>::Balance = T::Currency::total_balance(&current_coldkey);
898+
899+
let total_balance = Self::get_coldkey_balance(&current_coldkey);
900+
log::info!("Total Bank Balance: {:?}", total_balance);
901+
902+
// Ensure there's a balance to transfer
903+
ensure!(!total_balance.is_zero(), Error::<T>::NoBalanceToTransfer);
904+
905+
// Attempt to transfer the entire total balance to the new coldkey
906+
T::Currency::transfer(
907+
&current_coldkey,
908+
&new_coldkey,
909+
total_balance,
910+
Preservation::Expendable,
911+
)?;
912+
913+
// Emit the event
914+
Self::deposit_event(Event::AllBalanceUnstakedAndTransferredToNewColdkey {
915+
current_coldkey: current_coldkey.clone(),
916+
new_coldkey: new_coldkey.clone(),
917+
hotkey: hotkey.clone(),
918+
current_stake,
919+
total_balance,
920+
});
921+
922+
Ok(())
923+
}
798924
}

0 commit comments

Comments
 (0)