diff --git a/.github/workflows/devnet-ready-labels.yml b/.github/workflows/devnet-ready-labels.yml new file mode 100644 index 0000000000..ab53327e71 --- /dev/null +++ b/.github/workflows/devnet-ready-labels.yml @@ -0,0 +1,17 @@ +name: devnet-companion Label Check +on: + pull_request: + types: [opened, labeled, unlabeled, synchronize] + branches: [devnet-ready] +jobs: + check-labels: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: mheap/github-action-required-labels@v5 + with: + mode: minimum + count: 1 + labels: devnet-companion diff --git a/.github/workflows/testnet-ready-labels.yml b/.github/workflows/testnet-ready-labels.yml new file mode 100644 index 0000000000..8570d20112 --- /dev/null +++ b/.github/workflows/testnet-ready-labels.yml @@ -0,0 +1,17 @@ +name: testnet-companion Label Check +on: + pull_request: + types: [opened, labeled, unlabeled, synchronize] + branches: [testnet-ready] +jobs: + check-labels: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: mheap/github-action-required-labels@v5 + with: + mode: minimum + count: 1 + labels: testnet-companion diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3616041b2..132d360b86 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,9 +15,9 @@ add appropriate labels to your PR as shown below. Three positive reviews are required. 4. Once the required passing reviews have been obtained, you are ready to request that your PR be included in the next `devnet` deploy. To do this, you should open a companion PR merging - your branch into the `devnet-ready` branch. You must include a link to the parent PR in the - description and preface your PR title with "(Devnet Ready)" or the PR will be - closed/ignored. + a copy of your branch into the `devnet-ready` branch. You must include a link to the parent + PR in the description and preface your PR title with "(Devnet Ready)" or the PR will be + closed/ignored. Your companion PR should have the `devnet-companion` label. 5. A core team administrator will review your "(Devnet Ready)" PR, verifying that it logically matches the changes introduced in the parent PR (there will sometimes be minor differences due to merge conflicts) and will either request changes or approve the PR and merge it. Once @@ -86,11 +86,13 @@ | `runtime` | PR contains substantive changes to runtime / pallet code | none | | `breaking-change` | PR requires synchronized changes with bittensor | Triggers an automatic bot message so the relevant teams are made aware of the change well in advance | | `migration` | PR contains one or more migrations | none | +| `devnet-companion` | Designates a devnet companion PR | Presence of `devnet-companion` label is checked | | `devnet-ready` | PR's branch has been merged into the `devnet-ready` branch and will be included in the next `devnet` deploy | none | | `on-devnet` | PR has been deployed to `devnet` | Removes `devnet-ready` | | `devnet-pass` | PR has passed manual testing on `devnet` | `devnet-pass` or `devnet-skip` required | | `devnet-skip` | Allows a critical hotfix PR to skip required testing on `devnet` | `devnet-pass` or `devnet-skip` required | | `devnet-fail` | PR has failed manual testing on `devnet` and requires modification | none | +| `testnet-companion` | Designates a testnet companion PR | Presence of `testnet-companion` label is checked | | `on-testnet` | PR has been deployed to `testnet` | none | | `testnet-pass` | PR has passed manual testing on `testnet` | `testnet-pass` or `testnet-skip` required | | `testnet-skip` | Allows a critical hotfix PR to skip required manual testing and SOP on `testnet` | `testnet-pass` or `testnet-skip` required | diff --git a/pallets/admin-utils/tests/mock.rs b/pallets/admin-utils/tests/mock.rs index 16a1f79f4f..a78eb6d3da 100644 --- a/pallets/admin-utils/tests/mock.rs +++ b/pallets/admin-utils/tests/mock.rs @@ -110,6 +110,7 @@ parameter_types! { pub const InitialSubnetLimit: u16 = 10; // Max 10 subnets. pub const InitialNetworkRateLimit: u64 = 0; pub const InitialTargetStakesPerInterval: u16 = 1; + pub const InitialHotkeySwapCost: u64 = 1_000_000_000; pub const InitialAlphaHigh: u16 = 58982; // Represents 0.9 as per the production default pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default pub const InitialLiquidAlphaOn: bool = false; // Default value for LiquidAlphaOn @@ -164,6 +165,7 @@ impl pallet_subtensor::Config for Test { type InitialSubnetLimit = InitialSubnetLimit; type InitialNetworkRateLimit = InitialNetworkRateLimit; type InitialTargetStakesPerInterval = InitialTargetStakesPerInterval; + type HotkeySwapCost = InitialHotkeySwapCost; type AlphaHigh = InitialAlphaHigh; type AlphaLow = InitialAlphaLow; type LiquidAlphaOn = InitialLiquidAlphaOn; diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index efb528cdeb..af05db7b26 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -43,6 +43,7 @@ mod registration; mod root; mod serving; mod staking; +mod swap; mod uids; mod utils; mod weights; @@ -240,6 +241,9 @@ pub mod pallet { /// Initial target stakes per interval issuance. #[pallet::constant] type InitialTargetStakesPerInterval: Get; + /// Cost of swapping a hotkey. + #[pallet::constant] + type HotkeySwapCost: Get; /// The upper bound for the alpha parameter. Used for Liquid Alpha. #[pallet::constant] type AlphaHigh: Get; @@ -744,7 +748,7 @@ pub mod pallet { pub(super) type TxDelegateTakeRateLimit = StorageValue<_, u64, ValueQuery, DefaultTxDelegateTakeRateLimit>; #[pallet::storage] // --- MAP ( key ) --> last_block - pub(super) type LastTxBlock = + pub type LastTxBlock = StorageMap<_, Identity, T::AccountId, u64, ValueQuery, DefaultLastTxBlock>; #[pallet::storage] // --- MAP ( key ) --> last_block pub(super) type LastTxBlockDelegateTake = @@ -760,10 +764,10 @@ pub mod pallet { pub type ServingRateLimit = StorageMap<_, Identity, u16, u64, ValueQuery, DefaultServingRateLimit>; #[pallet::storage] // --- MAP ( netuid, hotkey ) --> axon_info - pub(super) type Axons = + pub type Axons = StorageDoubleMap<_, Identity, u16, Blake2_128Concat, T::AccountId, AxonInfoOf, OptionQuery>; #[pallet::storage] // --- MAP ( netuid, hotkey ) --> prometheus_info - pub(super) type Prometheus = StorageDoubleMap< + pub type Prometheus = StorageDoubleMap< _, Identity, u16, @@ -1017,13 +1021,13 @@ pub mod pallet { } #[pallet::storage] // --- DMAP ( netuid, hotkey ) --> uid - pub(super) type Uids = + pub type Uids = StorageDoubleMap<_, Identity, u16, Blake2_128Concat, T::AccountId, u16, OptionQuery>; #[pallet::storage] // --- DMAP ( netuid, uid ) --> hotkey - pub(super) type Keys = + pub type Keys = StorageDoubleMap<_, Identity, u16, Identity, u16, T::AccountId, ValueQuery, DefaultKey>; #[pallet::storage] // --- DMAP ( netuid ) --> (hotkey, se, ve) - pub(super) type LoadedEmission = + pub type LoadedEmission = StorageMap<_, Identity, u16, Vec<(T::AccountId, u64, u64)>, OptionQuery>; #[pallet::storage] // --- DMAP ( netuid ) --> active diff --git a/pallets/subtensor/src/registration.rs b/pallets/subtensor/src/registration.rs index dda00db541..4688bcbb59 100644 --- a/pallets/subtensor/src/registration.rs +++ b/pallets/subtensor/src/registration.rs @@ -1,6 +1,5 @@ use super::*; -use frame_support::storage::IterableStorageDoubleMap; -use sp_core::{Get, H256, U256}; +use sp_core::{H256, U256}; use sp_io::hashing::{keccak_256, sha2_256}; use sp_runtime::Saturating; use system::pallet_prelude::BlockNumberFor; @@ -395,7 +394,7 @@ impl Pallet { UsedWork::::insert(work.clone(), current_block_number); // --- 5. Add Balance via faucet. - let balance_to_add: u64 = 100_000_000_000; + let balance_to_add: u64 = 1_000_000_000_000; Self::coinbase(100_000_000_000); // We are creating tokens here from the coinbase. Self::add_balance_to_coldkey_account(&coldkey, balance_to_add); @@ -591,140 +590,4 @@ impl Pallet { let vec_work: Vec = Self::hash_to_vec(work); (nonce, vec_work) } - - pub fn do_swap_hotkey( - origin: T::RuntimeOrigin, - old_hotkey: &T::AccountId, - new_hotkey: &T::AccountId, - ) -> DispatchResultWithPostInfo { - let coldkey = ensure_signed(origin)?; - - let mut weight = T::DbWeight::get().reads_writes(2, 0); - ensure!( - Self::coldkey_owns_hotkey(&coldkey, old_hotkey), - Error::::NonAssociatedColdKey - ); - - let block: u64 = Self::get_current_block_as_u64(); - ensure!( - !Self::exceeds_tx_rate_limit(Self::get_last_tx_block(&coldkey), block), - Error::::HotKeySetTxRateLimitExceeded - ); - - weight.saturating_accrue(T::DbWeight::get().reads(2)); - - ensure!(old_hotkey != new_hotkey, Error::::NewHotKeyIsSameWithOld); - ensure!( - !Self::is_hotkey_registered_on_any_network(new_hotkey), - Error::::HotKeyAlreadyRegisteredInSubNet - ); - - weight.saturating_accrue( - T::DbWeight::get().reads((TotalNetworks::::get().saturating_add(1)) as u64), - ); - - let swap_cost = 1_000_000_000u64; - ensure!( - Self::can_remove_balance_from_coldkey_account(&coldkey, swap_cost), - Error::::NotEnoughBalanceToPaySwapHotKey - ); - let actual_burn_amount = Self::remove_balance_from_coldkey_account(&coldkey, swap_cost)?; - Self::burn_tokens(actual_burn_amount); - - Owner::::remove(old_hotkey); - Owner::::insert(new_hotkey, coldkey.clone()); - weight.saturating_accrue(T::DbWeight::get().writes(2)); - - if let Ok(total_hotkey_stake) = TotalHotkeyStake::::try_get(old_hotkey) { - TotalHotkeyStake::::remove(old_hotkey); - TotalHotkeyStake::::insert(new_hotkey, total_hotkey_stake); - - weight.saturating_accrue(T::DbWeight::get().writes(2)); - } - - if let Ok(delegate_take) = Delegates::::try_get(old_hotkey) { - Delegates::::remove(old_hotkey); - Delegates::::insert(new_hotkey, delegate_take); - - weight.saturating_accrue(T::DbWeight::get().writes(2)); - } - - if let Ok(last_tx) = LastTxBlock::::try_get(old_hotkey) { - LastTxBlock::::remove(old_hotkey); - LastTxBlock::::insert(new_hotkey, last_tx); - - weight.saturating_accrue(T::DbWeight::get().writes(2)); - } - - let mut coldkey_stake: Vec<(T::AccountId, u64)> = vec![]; - for (coldkey, stake_amount) in Stake::::iter_prefix(old_hotkey) { - coldkey_stake.push((coldkey.clone(), stake_amount)); - } - - let _ = Stake::::clear_prefix(old_hotkey, coldkey_stake.len() as u32, None); - weight.saturating_accrue(T::DbWeight::get().writes(coldkey_stake.len() as u64)); - - for (coldkey, stake_amount) in coldkey_stake { - Stake::::insert(new_hotkey, coldkey, stake_amount); - weight.saturating_accrue(T::DbWeight::get().writes(1)); - } - - let mut netuid_is_member: Vec = vec![]; - for netuid in as IterableStorageDoubleMap>::iter_key_prefix(old_hotkey) { - netuid_is_member.push(netuid); - } - - let _ = IsNetworkMember::::clear_prefix(old_hotkey, netuid_is_member.len() as u32, None); - weight.saturating_accrue(T::DbWeight::get().writes(netuid_is_member.len() as u64)); - - for netuid in netuid_is_member.iter() { - IsNetworkMember::::insert(new_hotkey, netuid, true); - weight.saturating_accrue(T::DbWeight::get().writes(1)); - } - - for netuid in netuid_is_member.iter() { - if let Ok(axon_info) = Axons::::try_get(netuid, old_hotkey) { - Axons::::remove(netuid, old_hotkey); - Axons::::insert(netuid, new_hotkey, axon_info); - - weight.saturating_accrue(T::DbWeight::get().writes(2)); - } - } - - for netuid in netuid_is_member.iter() { - if let Ok(uid) = Uids::::try_get(netuid, old_hotkey) { - Uids::::remove(netuid, old_hotkey); - Uids::::insert(netuid, new_hotkey, uid); - - weight.saturating_accrue(T::DbWeight::get().writes(2)); - - Keys::::insert(netuid, uid, new_hotkey); - - weight.saturating_accrue(T::DbWeight::get().writes(1)); - - LoadedEmission::::mutate(netuid, |emission_exists| match emission_exists { - Some(emissions) => { - if let Some(emission) = emissions.get_mut(uid as usize) { - let (_, se, ve) = emission; - *emission = (new_hotkey.clone(), *se, *ve); - } - } - None => {} - }); - - weight.saturating_accrue(T::DbWeight::get().writes(1)); - } - } - - Self::set_last_tx_block(&coldkey, block); - weight.saturating_accrue(T::DbWeight::get().writes(1)); - - Self::deposit_event(Event::HotkeySwapped { - coldkey, - old_hotkey: old_hotkey.clone(), - new_hotkey: new_hotkey.clone(), - }); - - Ok(Some(weight).into()) - } } diff --git a/pallets/subtensor/src/swap.rs b/pallets/subtensor/src/swap.rs new file mode 100644 index 0000000000..ce090d7361 --- /dev/null +++ b/pallets/subtensor/src/swap.rs @@ -0,0 +1,399 @@ +use super::*; +use frame_support::{storage::IterableStorageDoubleMap, weights::Weight}; +use sp_core::Get; + +impl Pallet { + /// Swaps the hotkey of a coldkey account. + /// + /// # Arguments + /// + /// * `origin` - The origin of the transaction, and also the coldkey account. + /// * `old_hotkey` - The old hotkey to be swapped. + /// * `new_hotkey` - The new hotkey to replace the old one. + /// + /// # Returns + /// + /// * `DispatchResultWithPostInfo` - The result of the dispatch. + /// + /// # Errors + /// + /// * `NonAssociatedColdKey` - If the coldkey does not own the old hotkey. + /// * `HotKeySetTxRateLimitExceeded` - If the transaction rate limit is exceeded. + /// * `NewHotKeyIsSameWithOld` - If the new hotkey is the same as the old hotkey. + /// * `HotKeyAlreadyRegisteredInSubNet` - If the new hotkey is already registered in the subnet. + /// * `NotEnoughBalanceToPaySwapHotKey` - If there is not enough balance to pay for the swap. + pub fn do_swap_hotkey( + origin: T::RuntimeOrigin, + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + ) -> DispatchResultWithPostInfo { + let coldkey = ensure_signed(origin)?; + + let mut weight = T::DbWeight::get().reads(2); + + ensure!(old_hotkey != new_hotkey, Error::::NewHotKeyIsSameWithOld); + ensure!( + !Self::is_hotkey_registered_on_any_network(new_hotkey), + Error::::HotKeyAlreadyRegisteredInSubNet + ); + + weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 0)); + ensure!( + Self::coldkey_owns_hotkey(&coldkey, old_hotkey), + Error::::NonAssociatedColdKey + ); + + let block: u64 = Self::get_current_block_as_u64(); + ensure!( + !Self::exceeds_tx_rate_limit(Self::get_last_tx_block(&coldkey), block), + Error::::HotKeySetTxRateLimitExceeded + ); + + weight.saturating_accrue( + T::DbWeight::get().reads((TotalNetworks::::get().saturating_add(1u16)) as u64), + ); + + let swap_cost = Self::get_hotkey_swap_cost(); + log::debug!("Swap cost: {:?}", swap_cost); + + ensure!( + Self::can_remove_balance_from_coldkey_account(&coldkey, swap_cost), + Error::::NotEnoughBalanceToPaySwapHotKey + ); + let actual_burn_amount = Self::remove_balance_from_coldkey_account(&coldkey, swap_cost)?; + Self::burn_tokens(actual_burn_amount); + + Self::swap_owner(old_hotkey, new_hotkey, &coldkey, &mut weight); + Self::swap_total_hotkey_stake(old_hotkey, new_hotkey, &mut weight); + Self::swap_delegates(old_hotkey, new_hotkey, &mut weight); + Self::swap_stake(old_hotkey, new_hotkey, &mut weight); + + // Store the value of is_network_member for the old key + let netuid_is_member: Vec = Self::get_netuid_is_member(old_hotkey, &mut weight); + + Self::swap_is_network_member(old_hotkey, new_hotkey, &netuid_is_member, &mut weight); + Self::swap_axons(old_hotkey, new_hotkey, &netuid_is_member, &mut weight); + Self::swap_keys(old_hotkey, new_hotkey, &netuid_is_member, &mut weight); + Self::swap_loaded_emission(old_hotkey, new_hotkey, &netuid_is_member, &mut weight); + Self::swap_uids(old_hotkey, new_hotkey, &netuid_is_member, &mut weight); + Self::swap_prometheus(old_hotkey, new_hotkey, &netuid_is_member, &mut weight); + + Self::swap_total_hotkey_coldkey_stakes_this_interval(old_hotkey, new_hotkey, &mut weight); + + Self::set_last_tx_block(&coldkey, block); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + Self::deposit_event(Event::HotkeySwapped { + coldkey, + old_hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey.clone(), + }); + + Ok(Some(weight).into()) + } + + /// Retrieves the network membership status for a given hotkey. + /// + /// # Arguments + /// + /// * `old_hotkey` - The hotkey to check for network membership. + /// + /// # Returns + /// + /// * `Vec` - A vector of network IDs where the hotkey is a member. + pub fn get_netuid_is_member(old_hotkey: &T::AccountId, weight: &mut Weight) -> Vec { + let netuid_is_member: Vec = + as IterableStorageDoubleMap<_, _, _>>::iter_prefix(old_hotkey) + .map(|(netuid, _)| netuid) + .collect(); + weight.saturating_accrue(T::DbWeight::get().reads(netuid_is_member.len() as u64)); + netuid_is_member + } + + /// Swaps the owner of the hotkey. + /// + /// # Arguments + /// + /// * `old_hotkey` - The old hotkey. + /// * `new_hotkey` - The new hotkey. + /// * `coldkey` - The coldkey owning the hotkey. + /// * `weight` - The weight of the transaction. + /// + pub fn swap_owner( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + coldkey: &T::AccountId, + weight: &mut Weight, + ) { + Owner::::remove(old_hotkey); + Owner::::insert(new_hotkey, coldkey.clone()); + weight.saturating_accrue(T::DbWeight::get().writes(2)); + } + + /// Swaps the total stake of the hotkey. + /// + /// # Arguments + /// + /// * `old_hotkey` - The old hotkey. + /// * `new_hotkey` - The new hotkey. + /// * `weight` - The weight of the transaction. + /// + /// # Weight Calculation + /// + /// * Reads: 1 if the old hotkey exists, otherwise 1 for the failed read. + /// * Writes: 2 if the old hotkey exists (one for removal and one for insertion). + pub fn swap_total_hotkey_stake( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + weight: &mut Weight, + ) { + if let Ok(total_hotkey_stake) = TotalHotkeyStake::::try_get(old_hotkey) { + TotalHotkeyStake::::remove(old_hotkey); + TotalHotkeyStake::::insert(new_hotkey, total_hotkey_stake); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); + } else { + weight.saturating_accrue(T::DbWeight::get().reads(1)); + } + } + + /// Swaps the delegates of the hotkey. + /// + /// # Arguments + /// + /// * `old_hotkey` - The old hotkey. + /// * `new_hotkey` - The new hotkey. + /// * `weight` - The weight of the transaction. + /// + /// # Weight Calculation + /// + /// * Reads: 1 if the old hotkey exists, otherwise 1 for the failed read. + /// * Writes: 2 if the old hotkey exists (one for removal and one for insertion). + pub fn swap_delegates( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + weight: &mut Weight, + ) { + if let Ok(delegate_take) = Delegates::::try_get(old_hotkey) { + Delegates::::remove(old_hotkey); + Delegates::::insert(new_hotkey, delegate_take); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); + } else { + weight.saturating_accrue(T::DbWeight::get().reads(1)); + } + } + + /// Swaps the stake of the hotkey. + /// + /// # Arguments + /// + /// * `old_hotkey` - The old hotkey. + /// * `new_hotkey` - The new hotkey. + /// * `weight` - The weight of the transaction. + pub fn swap_stake(old_hotkey: &T::AccountId, new_hotkey: &T::AccountId, weight: &mut Weight) { + let mut writes: u64 = 0; + let stakes: Vec<(T::AccountId, u64)> = Stake::::iter_prefix(old_hotkey).collect(); + let stake_count = stakes.len() as u32; + + for (coldkey, stake_amount) in stakes { + Stake::::insert(new_hotkey, &coldkey, stake_amount); + writes = writes.saturating_add(1u64); // One write for insert + } + + // Clear the prefix for the old hotkey after transferring all stakes + let _ = Stake::::clear_prefix(old_hotkey, stake_count, None); + writes = writes.saturating_add(1); // One write for insert; // One write for clear_prefix + + weight.saturating_accrue(T::DbWeight::get().writes(writes)); + } + + /// Swaps the network membership status of the hotkey. + /// + /// # Arguments + /// + /// * `old_hotkey` - The old hotkey. + /// * `new_hotkey` - The new hotkey. + /// * `netuid_is_member` - A vector of network IDs where the hotkey is a member. + /// * `weight` - The weight of the transaction. + pub fn swap_is_network_member( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuid_is_member: &[u16], + weight: &mut Weight, + ) { + let _ = IsNetworkMember::::clear_prefix(old_hotkey, netuid_is_member.len() as u32, None); + weight.saturating_accrue(T::DbWeight::get().writes(netuid_is_member.len() as u64)); + for netuid in netuid_is_member.iter() { + IsNetworkMember::::insert(new_hotkey, netuid, true); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + } + } + + /// Swaps the axons of the hotkey. + /// + /// # Arguments + /// + /// * `old_hotkey` - The old hotkey. + /// * `new_hotkey` - The new hotkey. + /// * `netuid_is_member` - A vector of network IDs where the hotkey is a member. + /// * `weight` - The weight of the transaction. + /// + /// # Weight Calculation + /// + /// * Reads: 1 for each network ID if the old hotkey exists in that network. + /// * Writes: 2 for each network ID if the old hotkey exists in that network (one for removal and one for insertion). + pub fn swap_axons( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuid_is_member: &[u16], + weight: &mut Weight, + ) { + for netuid in netuid_is_member.iter() { + if let Ok(axon_info) = Axons::::try_get(netuid, old_hotkey) { + Axons::::remove(netuid, old_hotkey); + Axons::::insert(netuid, new_hotkey, axon_info); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); + } else { + weight.saturating_accrue(T::DbWeight::get().reads(1)); + } + } + } + + /// Swaps the references in the keys storage map of the hotkey. + /// + /// # Arguments + /// + /// * `old_hotkey` - The old hotkey. + /// * `new_hotkey` - The new hotkey. + /// * `netuid_is_member` - A vector of network IDs where the hotkey is a member. + /// * `weight` - The weight of the transaction. + pub fn swap_keys( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuid_is_member: &[u16], + weight: &mut Weight, + ) { + let mut writes: u64 = 0; + for netuid in netuid_is_member { + let keys: Vec<(u16, T::AccountId)> = Keys::::iter_prefix(netuid).collect(); + for (uid, key) in keys { + if key == *old_hotkey { + log::info!("old hotkey found: {:?}", old_hotkey); + Keys::::insert(netuid, uid, new_hotkey.clone()); + } + writes = writes.saturating_add(2u64); + } + } + log::info!("writes: {:?}", writes); + weight.saturating_accrue(T::DbWeight::get().writes(writes)); + } + + /// Swaps the loaded emission of the hotkey. + /// + /// # Arguments + /// + /// * `old_hotkey` - The old hotkey. + /// * `new_hotkey` - The new hotkey. + /// * `netuid_is_member` - A vector of network IDs where the hotkey is a member. + /// * `weight` - The weight of the transaction. + /// + pub fn swap_loaded_emission( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuid_is_member: &[u16], + weight: &mut Weight, + ) { + for netuid in netuid_is_member { + if let Some(mut emissions) = LoadedEmission::::get(netuid) { + for emission in emissions.iter_mut() { + if emission.0 == *old_hotkey { + emission.0 = new_hotkey.clone(); + } + } + LoadedEmission::::insert(netuid, emissions); + } + } + weight.saturating_accrue(T::DbWeight::get().writes(netuid_is_member.len() as u64)); + } + + /// Swaps the UIDs of the hotkey. + /// + /// # Arguments + /// + /// * `old_hotkey` - The old hotkey. + /// * `new_hotkey` - The new hotkey. + /// * `netuid_is_member` - A vector of network IDs where the hotkey is a member. + /// * `weight` - The weight of the transaction. + /// + pub fn swap_uids( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuid_is_member: &[u16], + weight: &mut Weight, + ) { + for netuid in netuid_is_member.iter() { + if let Ok(uid) = Uids::::try_get(netuid, old_hotkey) { + Uids::::remove(netuid, old_hotkey); + Uids::::insert(netuid, new_hotkey, uid); + weight.saturating_accrue(T::DbWeight::get().writes(2)); + } + } + } + + /// Swaps the Prometheus data of the hotkey. + /// + /// # Arguments + /// + /// * `old_hotkey` - The old hotkey. + /// * `new_hotkey` - The new hotkey. + /// * `netuid_is_member` - A vector of network IDs where the hotkey is a member. + /// * `weight` - The weight of the transaction. + /// + /// # Weight Calculation + /// + /// * Reads: 1 for each network ID if the old hotkey exists in that network. + /// * Writes: 2 for each network ID if the old hotkey exists in that network (one for removal and one for insertion). + pub fn swap_prometheus( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuid_is_member: &[u16], + weight: &mut Weight, + ) { + for netuid in netuid_is_member.iter() { + if let Ok(prometheus_info) = Prometheus::::try_get(netuid, old_hotkey) { + Prometheus::::remove(netuid, old_hotkey); + Prometheus::::insert(netuid, new_hotkey, prometheus_info); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); + } else { + weight.saturating_accrue(T::DbWeight::get().reads(1)); + } + } + } + + /// Swaps the total hotkey-coldkey stakes for the current interval. + /// + /// # Arguments + /// + /// * `old_hotkey` - The old hotkey. + /// * `new_hotkey` - The new hotkey. + /// * `weight` - The weight of the transaction. + /// + pub fn swap_total_hotkey_coldkey_stakes_this_interval( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + weight: &mut Weight, + ) { + let stakes: Vec<(T::AccountId, (u64, u64))> = + TotalHotkeyColdkeyStakesThisInterval::::iter_prefix(old_hotkey).collect(); + log::info!("Stakes to swap: {:?}", stakes); + for (coldkey, stake) in stakes { + log::info!( + "Swapping stake for coldkey: {:?}, stake: {:?}", + coldkey, + stake + ); + TotalHotkeyColdkeyStakesThisInterval::::insert(new_hotkey, &coldkey, stake); + TotalHotkeyColdkeyStakesThisInterval::::remove(old_hotkey, &coldkey); + weight.saturating_accrue(T::DbWeight::get().writes(2)); // One write for insert and one for remove + } + } +} diff --git a/pallets/subtensor/src/utils.rs b/pallets/subtensor/src/utils.rs index 193d625fc2..15d659a51d 100644 --- a/pallets/subtensor/src/utils.rs +++ b/pallets/subtensor/src/utils.rs @@ -3,6 +3,7 @@ use crate::{ system::{ensure_root, ensure_signed_or_root}, Error, }; +use sp_core::Get; use sp_core::U256; use substrate_fixed::types::I32F32; @@ -663,6 +664,10 @@ impl Pallet { NominatorMinRequiredStake::::put(min_stake); } + pub fn get_hotkey_swap_cost() -> u64 { + T::HotkeySwapCost::get() + } + pub fn get_alpha_values(netuid: u16) -> (u16, u16) { AlphaValues::::get(netuid) } diff --git a/pallets/subtensor/tests/mock.rs b/pallets/subtensor/tests/mock.rs index 78aac3d95d..240f424489 100644 --- a/pallets/subtensor/tests/mock.rs +++ b/pallets/subtensor/tests/mock.rs @@ -1,5 +1,6 @@ #![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] +use frame_support::weights::constants::RocksDbWeight; use frame_support::{ assert_ok, derive_impl, dispatch::DispatchResultWithPostInfo, @@ -88,7 +89,7 @@ impl system::Config for Test { type BaseCallFilter = Everything; type BlockWeights = (); type BlockLength = (); - type DbWeight = (); + type DbWeight = RocksDbWeight; type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; type Hash = H256; @@ -160,6 +161,7 @@ parameter_types! { pub const InitialSubnetLimit: u16 = 10; // Max 10 subnets. pub const InitialNetworkRateLimit: u64 = 0; pub const InitialTargetStakesPerInterval: u16 = 2; + pub const InitialHotkeySwapCost: u64 = 1_000_000_000; pub const InitialAlphaHigh: u16 = 58982; // Represents 0.9 as per the production default pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default pub const InitialLiquidAlphaOn: bool = false; // Default value for LiquidAlphaOn @@ -369,6 +371,7 @@ impl pallet_subtensor::Config for Test { type InitialSubnetLimit = InitialSubnetLimit; type InitialNetworkRateLimit = InitialNetworkRateLimit; type InitialTargetStakesPerInterval = InitialTargetStakesPerInterval; + type HotkeySwapCost = InitialHotkeySwapCost; type AlphaHigh = InitialAlphaHigh; type AlphaLow = InitialAlphaLow; type LiquidAlphaOn = InitialLiquidAlphaOn; diff --git a/pallets/subtensor/tests/registration.rs b/pallets/subtensor/tests/registration.rs index 0da10bc48e..676d49a449 100644 --- a/pallets/subtensor/tests/registration.rs +++ b/pallets/subtensor/tests/registration.rs @@ -38,7 +38,7 @@ fn test_registration_subscribe_ok_dispatch_info_ok() { assert_eq!( call.get_dispatch_info(), DispatchInfo { - weight: frame_support::weights::Weight::from_parts(192_000_000, 0), + weight: frame_support::weights::Weight::from_parts(2_992_000_000, 0), class: DispatchClass::Normal, pays_fee: Pays::No } diff --git a/pallets/subtensor/tests/serving.rs b/pallets/subtensor/tests/serving.rs index 851edeee27..41e9888ccb 100644 --- a/pallets/subtensor/tests/serving.rs +++ b/pallets/subtensor/tests/serving.rs @@ -50,7 +50,7 @@ fn test_serving_subscribe_ok_dispatch_info_ok() { assert_eq!( call.get_dispatch_info(), DispatchInfo { - weight: frame_support::weights::Weight::from_parts(46_000_000, 0), + weight: frame_support::weights::Weight::from_parts(246_000_000, 0), class: DispatchClass::Normal, pays_fee: Pays::No } @@ -295,7 +295,7 @@ fn test_prometheus_serving_subscribe_ok_dispatch_info_ok() { assert_eq!( call.get_dispatch_info(), DispatchInfo { - weight: frame_support::weights::Weight::from_parts(45_000_000, 0), + weight: frame_support::weights::Weight::from_parts(245_000_000, 0), class: DispatchClass::Normal, pays_fee: Pays::No } diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index 766b3a495b..529332f04b 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -26,7 +26,7 @@ fn test_add_stake_dispatch_info_ok() { assert_eq!( call.get_dispatch_info(), DispatchInfo { - weight: frame_support::weights::Weight::from_parts(124_000_000, 0), + weight: frame_support::weights::Weight::from_parts(1_074_000_000, 0), class: DispatchClass::Normal, pays_fee: Pays::No } @@ -532,7 +532,7 @@ fn test_remove_stake_dispatch_info_ok() { assert_eq!( call.get_dispatch_info(), DispatchInfo { - weight: frame_support::weights::Weight::from_parts(111_000_000, 0) + weight: frame_support::weights::Weight::from_parts(1_061_000_000, 0) .add_proof_size(43991), class: DispatchClass::Normal, pays_fee: Pays::No diff --git a/pallets/subtensor/tests/swap.rs b/pallets/subtensor/tests/swap.rs new file mode 100644 index 0000000000..af7d19d2d1 --- /dev/null +++ b/pallets/subtensor/tests/swap.rs @@ -0,0 +1,1050 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] + +use codec::Encode; +use frame_support::weights::Weight; +use frame_support::{assert_err, assert_ok}; +use frame_system::Config; +mod mock; +use mock::*; +use pallet_subtensor::*; +use sp_core::U256; + +#[test] +fn test_do_swap_hotkey_ok() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + let tempo: u16 = 13; + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let swap_cost = 1_000_000_000u64; + + // Setup initial state + add_network(netuid, tempo, 0); + register_ok_neuron(netuid, old_hotkey, coldkey, 0); + SubtensorModule::add_balance_to_coldkey_account(&coldkey, swap_cost); + + // Perform the swap + assert_ok!(SubtensorModule::do_swap_hotkey( + <::RuntimeOrigin>::signed(coldkey), + &old_hotkey, + &new_hotkey + )); + + // Verify the swap + assert_eq!( + SubtensorModule::get_owning_coldkey_for_hotkey(&new_hotkey), + coldkey + ); + assert_ne!( + SubtensorModule::get_owning_coldkey_for_hotkey(&old_hotkey), + coldkey + ); + + // Verify other storage changes + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&new_hotkey), + SubtensorModule::get_total_stake_for_hotkey(&old_hotkey) + ); + assert_eq!( + SubtensorModule::get_delegate(new_hotkey.encode()), + SubtensorModule::get_delegate(old_hotkey.encode()) + ); + assert_eq!( + SubtensorModule::get_last_tx_block(&new_hotkey), + SubtensorModule::get_last_tx_block(&old_hotkey) + ); + + // Verify raw storage maps + // Stake + for (coldkey, stake_amount) in Stake::::iter_prefix(old_hotkey) { + assert_eq!(Stake::::get(new_hotkey, coldkey), stake_amount); + } + + let mut weight = Weight::zero(); + // UIDs + for netuid in SubtensorModule::get_netuid_is_member(&old_hotkey, &mut weight) { + assert_eq!( + Uids::::get(netuid, new_hotkey), + Uids::::get(netuid, old_hotkey) + ); + } + + // Prometheus + for netuid in SubtensorModule::get_netuid_is_member(&old_hotkey, &mut weight) { + assert_eq!( + Prometheus::::get(netuid, new_hotkey), + Prometheus::::get(netuid, old_hotkey) + ); + } + + // LoadedEmission + for netuid in SubtensorModule::get_netuid_is_member(&old_hotkey, &mut weight) { + assert_eq!( + LoadedEmission::::get(netuid).unwrap(), + LoadedEmission::::get(netuid).unwrap() + ); + } + + // IsNetworkMember + for netuid in SubtensorModule::get_netuid_is_member(&old_hotkey, &mut weight) { + assert!(IsNetworkMember::::contains_key(new_hotkey, netuid)); + assert!(!IsNetworkMember::::contains_key(old_hotkey, netuid)); + } + + // Owner + assert_eq!(Owner::::get(new_hotkey), coldkey); + + // TotalHotkeyStake + assert_eq!( + TotalHotkeyStake::::get(new_hotkey), + TotalHotkeyStake::::get(old_hotkey) + ); + + // Delegates + assert_eq!( + Delegates::::get(new_hotkey), + Delegates::::get(old_hotkey) + ); + + // LastTxBlock + assert_eq!( + LastTxBlock::::get(new_hotkey), + LastTxBlock::::get(old_hotkey) + ); + + // Axons + for netuid in SubtensorModule::get_netuid_is_member(&old_hotkey, &mut weight) { + assert_eq!( + Axons::::get(netuid, new_hotkey), + Axons::::get(netuid, old_hotkey) + ); + } + + // TotalHotkeyColdkeyStakesThisInterval + assert_eq!( + TotalHotkeyColdkeyStakesThisInterval::::get(new_hotkey, coldkey), + TotalHotkeyColdkeyStakesThisInterval::::get(old_hotkey, coldkey) + ); + }); +} + +#[test] +fn test_do_swap_hotkey_ok_robust() { + new_test_ext(1).execute_with(|| { + let num_subnets: u16 = 10; + let tempo: u16 = 13; + let swap_cost = 1_000_000_000u64; + + // Create 10 sets of keys + let mut old_hotkeys = vec![]; + let mut new_hotkeys = vec![]; + let mut coldkeys = vec![]; + + for i in 0..10 { + old_hotkeys.push(U256::from(i * 2 + 1)); + new_hotkeys.push(U256::from(i * 2 + 2)); + coldkeys.push(U256::from(i * 2 + 11)); + } + + // Setup initial state + for netuid in 1..=num_subnets { + add_network(netuid, tempo, 0); + SubtensorModule::set_max_registrations_per_block(netuid, 20); + SubtensorModule::set_target_registrations_per_interval(netuid, 1000); + log::info!( + "Registrations this interval for netuid {:?} is {:?}", + netuid, + SubtensorModule::get_target_registrations_per_interval(netuid) + ); + for i in 0..10 { + register_ok_neuron(netuid, old_hotkeys[i], coldkeys[i], 0); + } + } + + // Add balance to coldkeys for swap cost + for coldkey in coldkeys.iter().take(10) { + SubtensorModule::add_balance_to_coldkey_account(coldkey, swap_cost); + } + + // Perform the swaps for only two hotkeys + assert_ok!(SubtensorModule::do_swap_hotkey( + <::RuntimeOrigin>::signed(coldkeys[0]), + &old_hotkeys[0], + &new_hotkeys[0] + )); + assert_ok!(SubtensorModule::do_swap_hotkey( + <::RuntimeOrigin>::signed(coldkeys[1]), + &old_hotkeys[1], + &new_hotkeys[1] + )); + + // Verify the swaps + for netuid in 1..=num_subnets { + for i in 0..10 { + if i == 0 || i == 1 { + assert_eq!( + SubtensorModule::get_owning_coldkey_for_hotkey(&new_hotkeys[i]), + coldkeys[i] + ); + assert_ne!( + SubtensorModule::get_owning_coldkey_for_hotkey(&old_hotkeys[i]), + coldkeys[i] + ); + + // Verify other storage changes + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&new_hotkeys[i]), + SubtensorModule::get_total_stake_for_hotkey(&old_hotkeys[i]) + ); + + assert_eq!( + SubtensorModule::get_delegate(new_hotkeys[i].encode()), + SubtensorModule::get_delegate(old_hotkeys[i].encode()) + ); + + assert_eq!( + SubtensorModule::get_last_tx_block(&new_hotkeys[i]), + SubtensorModule::get_last_tx_block(&old_hotkeys[i]) + ); + + // Verify raw storage maps + // Stake + for (coldkey, stake_amount) in Stake::::iter_prefix(old_hotkeys[i]) { + assert_eq!(Stake::::get(new_hotkeys[i], coldkey), stake_amount); + } + + let mut weight = Weight::zero(); + // UIDs + for netuid in + SubtensorModule::get_netuid_is_member(&old_hotkeys[i], &mut weight) + { + assert_eq!( + Uids::::get(netuid, new_hotkeys[i]), + Uids::::get(netuid, old_hotkeys[i]) + ); + } + + // Prometheus + for netuid in + SubtensorModule::get_netuid_is_member(&old_hotkeys[i], &mut weight) + { + assert_eq!( + Prometheus::::get(netuid, new_hotkeys[i]), + Prometheus::::get(netuid, old_hotkeys[i]) + ); + } + + // LoadedEmission + for netuid in + SubtensorModule::get_netuid_is_member(&old_hotkeys[i], &mut weight) + { + assert_eq!( + LoadedEmission::::get(netuid).unwrap(), + LoadedEmission::::get(netuid).unwrap() + ); + } + + // IsNetworkMember + for netuid in + SubtensorModule::get_netuid_is_member(&old_hotkeys[i], &mut weight) + { + assert!(IsNetworkMember::::contains_key( + new_hotkeys[i], + netuid + )); + assert!(!IsNetworkMember::::contains_key( + old_hotkeys[i], + netuid + )); + } + + // Owner + assert_eq!(Owner::::get(new_hotkeys[i]), coldkeys[i]); + + // Keys + for (uid, hotkey) in Keys::::iter_prefix(netuid) { + if hotkey == old_hotkeys[i] { + assert_eq!(Keys::::get(netuid, uid), new_hotkeys[i]); + } + } + } else { + // Ensure other hotkeys remain unchanged + assert_eq!( + SubtensorModule::get_owning_coldkey_for_hotkey(&old_hotkeys[i]), + coldkeys[i] + ); + assert_ne!( + SubtensorModule::get_owning_coldkey_for_hotkey(&new_hotkeys[i]), + coldkeys[i] + ); + } + } + } + }); +} + +#[test] +fn test_swap_hotkey_tx_rate_limit_exceeded() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + let tempo: u16 = 13; + let old_hotkey = U256::from(1); + let new_hotkey_1 = U256::from(2); + let new_hotkey_2 = U256::from(4); + let coldkey = U256::from(3); + let swap_cost = 1_000_000_000u64 * 2; + + let tx_rate_limit = 1; + + // Get the current transaction rate limit + let current_tx_rate_limit = SubtensorModule::get_tx_rate_limit(); + log::info!("current_tx_rate_limit: {:?}", current_tx_rate_limit); + + // Set the transaction rate limit + SubtensorModule::set_tx_rate_limit(tx_rate_limit); + // assert the rate limit is set to 1000 blocks + assert_eq!(SubtensorModule::get_tx_rate_limit(), tx_rate_limit); + + // Setup initial state + add_network(netuid, tempo, 0); + register_ok_neuron(netuid, old_hotkey, coldkey, 0); + SubtensorModule::add_balance_to_coldkey_account(&coldkey, swap_cost); + + // Perform the first swap + assert_ok!(SubtensorModule::do_swap_hotkey( + <::RuntimeOrigin>::signed(coldkey), + &old_hotkey, + &new_hotkey_1 + )); + + // Attempt to perform another swap immediately, which should fail due to rate limit + assert_err!( + SubtensorModule::do_swap_hotkey( + <::RuntimeOrigin>::signed(coldkey), + &new_hotkey_1, + &new_hotkey_2 + ), + Error::::HotKeySetTxRateLimitExceeded + ); + + // move in time past the rate limit + step_block(1001); + assert_ok!(SubtensorModule::do_swap_hotkey( + <::RuntimeOrigin>::signed(coldkey), + &new_hotkey_1, + &new_hotkey_2 + )); + }); +} + +#[test] +fn test_do_swap_hotkey_err_not_owner() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + let tempo: u16 = 13; + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let not_owner_coldkey = U256::from(4); + let swap_cost = 1_000_000_000u64; + + // Setup initial state + add_network(netuid, tempo, 0); + register_ok_neuron(netuid, old_hotkey, coldkey, 0); + SubtensorModule::add_balance_to_coldkey_account(¬_owner_coldkey, swap_cost); + + // Attempt the swap with a non-owner coldkey + assert_err!( + SubtensorModule::do_swap_hotkey( + <::RuntimeOrigin>::signed(not_owner_coldkey), + &old_hotkey, + &new_hotkey + ), + Error::::NonAssociatedColdKey + ); + }); +} + +#[test] +fn test_swap_owner_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let mut weight = Weight::zero(); + + // Initialize Owner for old_hotkey + Owner::::insert(old_hotkey, coldkey); + + // Perform the swap + SubtensorModule::swap_owner(&old_hotkey, &new_hotkey, &coldkey, &mut weight); + + // Verify the swap + assert_eq!(Owner::::get(new_hotkey), coldkey); + assert!(!Owner::::contains_key(old_hotkey)); + }); +} + +#[test] +fn test_swap_owner_old_hotkey_not_exist() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let mut weight = Weight::zero(); + + // Ensure old_hotkey does not exist + assert!(!Owner::::contains_key(old_hotkey)); + + // Perform the swap + SubtensorModule::swap_owner(&old_hotkey, &new_hotkey, &coldkey, &mut weight); + + // Verify the swap + assert_eq!(Owner::::get(new_hotkey), coldkey); + assert!(!Owner::::contains_key(old_hotkey)); + }); +} + +#[test] +fn test_swap_owner_new_hotkey_already_exists() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let another_coldkey = U256::from(4); + let mut weight = Weight::zero(); + + // Initialize Owner for old_hotkey and new_hotkey + Owner::::insert(old_hotkey, coldkey); + Owner::::insert(new_hotkey, another_coldkey); + + // Perform the swap + SubtensorModule::swap_owner(&old_hotkey, &new_hotkey, &coldkey, &mut weight); + + // Verify the swap + assert_eq!(Owner::::get(new_hotkey), coldkey); + assert!(!Owner::::contains_key(old_hotkey)); + }); +} + +#[test] +fn test_swap_owner_weight_update() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let mut weight = Weight::zero(); + + // Initialize Owner for old_hotkey + Owner::::insert(old_hotkey, coldkey); + + // Perform the swap + SubtensorModule::swap_owner(&old_hotkey, &new_hotkey, &coldkey, &mut weight); + + // Verify the weight update + let expected_weight = ::DbWeight::get().writes(2); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_swap_total_hotkey_stake_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let total_stake = 1000u64; + let mut weight = Weight::zero(); + + // Initialize TotalHotkeyStake for old_hotkey + TotalHotkeyStake::::insert(old_hotkey, total_stake); + + // Perform the swap + SubtensorModule::swap_total_hotkey_stake(&old_hotkey, &new_hotkey, &mut weight); + + // Verify the swap + assert_eq!(TotalHotkeyStake::::get(new_hotkey), total_stake); + assert!(!TotalHotkeyStake::::contains_key(old_hotkey)); + }); +} + +#[test] +fn test_swap_total_hotkey_stake_old_hotkey_not_exist() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let mut weight = Weight::zero(); + + // Ensure old_hotkey does not exist + assert!(!TotalHotkeyStake::::contains_key(old_hotkey)); + + // Perform the swap + SubtensorModule::swap_total_hotkey_stake(&old_hotkey, &new_hotkey, &mut weight); + + // Verify that new_hotkey does not have a stake + assert!(!TotalHotkeyStake::::contains_key(new_hotkey)); + }); +} + +#[test] +fn test_swap_total_hotkey_stake_weight_update() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let total_stake = 1000u64; + let mut weight = Weight::zero(); + + // Initialize TotalHotkeyStake for old_hotkey + TotalHotkeyStake::::insert(old_hotkey, total_stake); + + // Perform the swap + SubtensorModule::swap_total_hotkey_stake(&old_hotkey, &new_hotkey, &mut weight); + + // Verify the weight update + let expected_weight = ::DbWeight::get().reads_writes(1, 2); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_swap_delegates_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let delegate_take = 10u16; + let mut weight = Weight::zero(); + + // Initialize Delegates for old_hotkey + Delegates::::insert(old_hotkey, delegate_take); + + // Perform the swap + SubtensorModule::swap_delegates(&old_hotkey, &new_hotkey, &mut weight); + + // Verify the swap + assert_eq!(Delegates::::get(new_hotkey), delegate_take); + assert!(!Delegates::::contains_key(old_hotkey)); + }); +} + +#[test] +fn test_swap_delegates_old_hotkey_not_exist() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let mut weight = Weight::zero(); + + // Ensure old_hotkey does not exist + assert!(!Delegates::::contains_key(old_hotkey)); + + // Perform the swap + SubtensorModule::swap_delegates(&old_hotkey, &new_hotkey, &mut weight); + + // Verify that new_hotkey does not have a delegate + assert!(!Delegates::::contains_key(new_hotkey)); + }); +} + +#[test] +fn test_swap_delegates_weight_update() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let delegate_take = 10u16; + let mut weight = Weight::zero(); + + // Initialize Delegates for old_hotkey + Delegates::::insert(old_hotkey, delegate_take); + + // Perform the swap + SubtensorModule::swap_delegates(&old_hotkey, &new_hotkey, &mut weight); + + // Verify the weight update + let expected_weight = ::DbWeight::get().reads_writes(1, 2); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_swap_stake_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let stake_amount = 1000u64; + let mut weight = Weight::zero(); + + // Initialize Stake for old_hotkey + Stake::::insert(old_hotkey, coldkey, stake_amount); + + // Perform the swap + SubtensorModule::swap_stake(&old_hotkey, &new_hotkey, &mut weight); + + // Verify the swap + assert_eq!(Stake::::get(new_hotkey, coldkey), stake_amount); + assert!(!Stake::::contains_key(old_hotkey, coldkey)); + }); +} + +#[test] +fn test_swap_stake_old_hotkey_not_exist() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let stake_amount = 1000u64; + let mut weight = Weight::zero(); + + // Initialize Stake for old_hotkey + Stake::::insert(old_hotkey, coldkey, stake_amount); + + // Ensure old_hotkey has a stake + assert!(Stake::::contains_key(old_hotkey, coldkey)); + + // Perform the swap + SubtensorModule::swap_stake(&old_hotkey, &new_hotkey, &mut weight); + + // Verify that new_hotkey has the stake and old_hotkey does not + assert!(Stake::::contains_key(new_hotkey, coldkey)); + assert!(!Stake::::contains_key(old_hotkey, coldkey)); + }); +} + +#[test] +fn test_swap_stake_weight_update() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let stake_amount = 1000u64; + let mut weight = Weight::zero(); + + // Initialize Stake for old_hotkey + Stake::::insert(old_hotkey, coldkey, stake_amount); + + // Perform the swap + SubtensorModule::swap_stake(&old_hotkey, &new_hotkey, &mut weight); + + // Verify the weight update + let expected_weight = ::DbWeight::get().writes(2); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_swap_is_network_member_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + let mut weight = Weight::zero(); + + // Initialize IsNetworkMember for old_hotkey + for netuid in &netuid_is_member { + IsNetworkMember::::insert(old_hotkey, netuid, true); + } + + // Perform the swap + SubtensorModule::swap_is_network_member( + &old_hotkey, + &new_hotkey, + &netuid_is_member, + &mut weight, + ); + + // Verify the swap + for netuid in &netuid_is_member { + assert!(IsNetworkMember::::contains_key(new_hotkey, netuid)); + assert!(!IsNetworkMember::::contains_key(old_hotkey, netuid)); + } + }); +} + +#[test] +fn test_swap_is_network_member_weight_update() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + let mut weight = Weight::zero(); + + // Initialize IsNetworkMember for old_hotkey + for netuid in &netuid_is_member { + IsNetworkMember::::insert(old_hotkey, netuid, true); + } + + // Perform the swap + SubtensorModule::swap_is_network_member( + &old_hotkey, + &new_hotkey, + &netuid_is_member, + &mut weight, + ); + + // Verify the weight update + let expected_weight = ::DbWeight::get().writes(4); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_swap_axons_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + let axon_info = AxonInfo { + block: 100, + version: 1, + ip: 0x1234567890abcdef, + port: 8080, + ip_type: 4, + protocol: 1, + placeholder1: 0, + placeholder2: 0, + }; + let mut weight = Weight::zero(); + + // Initialize Axons for old_hotkey + for netuid in &netuid_is_member { + Axons::::insert(netuid, old_hotkey, axon_info.clone()); + } + + // Perform the swap + SubtensorModule::swap_axons(&old_hotkey, &new_hotkey, &netuid_is_member, &mut weight); + + // Verify the swap + for netuid in &netuid_is_member { + assert_eq!(Axons::::get(netuid, new_hotkey).unwrap(), axon_info); + assert!(!Axons::::contains_key(netuid, old_hotkey)); + } + }); +} + +#[test] +fn test_swap_axons_weight_update() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + let axon_info = AxonInfo { + block: 100, + version: 1, + ip: 0x1234567890abcdef, + port: 8080, + ip_type: 4, + protocol: 1, + placeholder1: 0, + placeholder2: 0, + }; + let mut weight = Weight::zero(); + + // Initialize Axons for old_hotkey + for netuid in &netuid_is_member { + Axons::::insert(netuid, old_hotkey, axon_info.clone()); + } + + // Perform the swap + SubtensorModule::swap_axons(&old_hotkey, &new_hotkey, &netuid_is_member, &mut weight); + + // Verify the weight update + let expected_weight = netuid_is_member.len() as u64 + * ::DbWeight::get().reads_writes(1, 2); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_swap_keys_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + let uid = 42u16; + let mut weight = Weight::zero(); + + // Initialize Keys for old_hotkey + for netuid in &netuid_is_member { + log::info!("Inserting old_hotkey:{:?} netuid:{:?}", old_hotkey, netuid); + Keys::::insert(*netuid, uid, old_hotkey); + } + + // Perform the swap + SubtensorModule::swap_keys(&old_hotkey, &new_hotkey, &netuid_is_member, &mut weight); + + // Verify the swap + for netuid in &netuid_is_member { + log::info!( + "neutuid, uid, hotkey: {:?}, {:?}, {:?}", + netuid, + uid, + new_hotkey + ); + assert_eq!(Keys::::get(netuid, uid), new_hotkey); + } + }); +} + +#[test] +fn test_swap_keys_weight_update() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + let uid = 42u16; + let mut weight = Weight::zero(); + + // Initialize Keys for old_hotkey + for netuid in &netuid_is_member { + Keys::::insert(*netuid, uid, old_hotkey); + } + + // Perform the swap + SubtensorModule::swap_keys(&old_hotkey, &new_hotkey, &netuid_is_member, &mut weight); + + // Verify the weight update + let expected_weight = ::DbWeight::get().writes(4); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_swap_loaded_emission_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + let se = 100u64; + let ve = 200u64; + let mut weight = Weight::zero(); + + // Initialize LoadedEmission for old_hotkey + for netuid in &netuid_is_member { + LoadedEmission::::mutate(netuid, |emission_exists| { + if let Some(emissions) = emission_exists { + emissions.push((old_hotkey, se, ve)); + } else { + *emission_exists = Some(vec![(old_hotkey, se, ve)]); + } + }); + } + + // Perform the swap + SubtensorModule::swap_loaded_emission( + &old_hotkey, + &new_hotkey, + &netuid_is_member, + &mut weight, + ); + + // Verify the swap + for netuid in &netuid_is_member { + let emissions = LoadedEmission::::get(netuid).unwrap(); + assert!(emissions.iter().any(|(hk, _, _)| hk == &new_hotkey)); + assert!(!emissions.iter().any(|(hk, _, _)| hk == &old_hotkey)); + } + }); +} + +#[test] +fn test_swap_loaded_emission_weight_update() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + // let uid = 42u64; + let se = 100u64; + let ve = 200u64; + let mut weight = Weight::zero(); + + // Initialize LoadedEmission for old_hotkey + for netuid in &netuid_is_member { + LoadedEmission::::mutate(netuid, |emission_exists| { + if let Some(emissions) = emission_exists { + emissions.push((old_hotkey, se, ve)); + } else { + *emission_exists = Some(vec![(old_hotkey, se, ve)]); + } + }); + } + + // Perform the swap + SubtensorModule::swap_loaded_emission( + &old_hotkey, + &new_hotkey, + &netuid_is_member, + &mut weight, + ); + + // Verify the weight update + let expected_weight = ::DbWeight::get().writes(2); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_swap_uids_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + let uid = 42u16; + let mut weight = Weight::zero(); + + // Initialize Uids for old_hotkey + for netuid in &netuid_is_member { + Uids::::insert(netuid, old_hotkey, uid); + } + + // Perform the swap + SubtensorModule::swap_uids(&old_hotkey, &new_hotkey, &netuid_is_member, &mut weight); + + // Verify the swap + for netuid in &netuid_is_member { + assert_eq!(Uids::::get(netuid, new_hotkey).unwrap(), uid); + assert!(!Uids::::contains_key(netuid, old_hotkey)); + } + }); +} + +#[test] +fn test_swap_uids_weight_update() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + let uid = 42u16; + let mut weight = Weight::zero(); + + // Initialize Uids for old_hotkey + for netuid in &netuid_is_member { + Uids::::insert(netuid, old_hotkey, uid); + } + + // Perform the swap + SubtensorModule::swap_uids(&old_hotkey, &new_hotkey, &netuid_is_member, &mut weight); + + // Verify the weight update + let expected_weight = ::DbWeight::get().writes(4); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_swap_prometheus_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + let prometheus_info = PrometheusInfo { + block: 100, + version: 1, + ip: 0x1234567890abcdef, + port: 8080, + ip_type: 4, + }; + let mut weight = Weight::zero(); + + // Initialize Prometheus for old_hotkey + for netuid in &netuid_is_member { + Prometheus::::insert(netuid, old_hotkey, prometheus_info.clone()); + } + + // Perform the swap + SubtensorModule::swap_prometheus(&old_hotkey, &new_hotkey, &netuid_is_member, &mut weight); + + // Verify the swap + for netuid in &netuid_is_member { + assert_eq!( + Prometheus::::get(netuid, new_hotkey).unwrap(), + prometheus_info + ); + assert!(!Prometheus::::contains_key(netuid, old_hotkey)); + } + }); +} + +#[test] +fn test_swap_prometheus_weight_update() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let netuid_is_member = vec![1u16, 2u16]; + let prometheus_info = PrometheusInfo { + block: 100, + version: 1, + ip: 0x1234567890abcdef, + port: 8080, + ip_type: 4, + }; + let mut weight = Weight::zero(); + + // Initialize Prometheus for old_hotkey + for netuid in &netuid_is_member { + Prometheus::::insert(netuid, old_hotkey, prometheus_info.clone()); + } + + // Perform the swap + SubtensorModule::swap_prometheus(&old_hotkey, &new_hotkey, &netuid_is_member, &mut weight); + + // Verify the weight update + let expected_weight = netuid_is_member.len() as u64 + * ::DbWeight::get().reads_writes(1, 2); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_swap_total_hotkey_coldkey_stakes_this_interval_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let stake = (1000u64, 42u64); // Example tuple value + let mut weight = Weight::zero(); + + // Initialize TotalHotkeyColdkeyStakesThisInterval for old_hotkey + TotalHotkeyColdkeyStakesThisInterval::::insert(old_hotkey, coldkey, stake); + + // Perform the swap + SubtensorModule::swap_total_hotkey_coldkey_stakes_this_interval( + &old_hotkey, + &new_hotkey, + &mut weight, + ); + + // Verify the swap + assert_eq!( + TotalHotkeyColdkeyStakesThisInterval::::get(new_hotkey, coldkey), + stake + ); + assert!(!TotalHotkeyColdkeyStakesThisInterval::::contains_key( + old_hotkey, coldkey + )); + }); +} + +#[test] +fn test_swap_total_hotkey_coldkey_stakes_this_interval_weight_update() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let stake = (1000u64, 42u64); + let mut weight = Weight::zero(); + + // Initialize TotalHotkeyColdkeyStakesThisInterval for old_hotkey + TotalHotkeyColdkeyStakesThisInterval::::insert(old_hotkey, coldkey, stake); + + // Perform the swap + + SubtensorModule::swap_total_hotkey_coldkey_stakes_this_interval( + &old_hotkey, + &new_hotkey, + &mut weight, + ); + + // Verify the weight update + let expected_weight = ::DbWeight::get().writes(2); + assert_eq!(weight, expected_weight); + }); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 26b4aca4c9..32a4f8b593 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -868,6 +868,7 @@ parameter_types! { pub const SubtensorInitialNetworkLockReductionInterval: u64 = 14 * 7200; pub const SubtensorInitialNetworkRateLimit: u64 = 7200; pub const SubtensorInitialTargetStakesPerInterval: u16 = 1; + pub const SubtensorInitialHotkeySwapCost: u64 = 1_000_000_000; pub const InitialAlphaHigh: u16 = 58982; // Represents 0.9 as per the production default pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default pub const InitialLiquidAlphaOn: bool = false; // Default value for LiquidAlphaOn @@ -922,6 +923,7 @@ impl pallet_subtensor::Config for Runtime { type InitialSubnetLimit = SubtensorInitialSubnetLimit; type InitialNetworkRateLimit = SubtensorInitialNetworkRateLimit; type InitialTargetStakesPerInterval = SubtensorInitialTargetStakesPerInterval; + type HotkeySwapCost = SubtensorInitialHotkeySwapCost; type AlphaHigh = InitialAlphaHigh; type AlphaLow = InitialAlphaLow; type LiquidAlphaOn = InitialLiquidAlphaOn;