diff --git a/node/src/service.rs b/node/src/service.rs index fef86dee6..91ffc3d45 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -35,15 +35,15 @@ use node_subtensor_runtime::{ /// imported and generated. const GRANDPA_JUSTIFICATION_PERIOD: u32 = 512; -/// Only enable the benchmarking host functions when we actually want to benchmark. -#[cfg(feature = "runtime-benchmarks")] +/// Always enable runtime benchmark host functions, the genesis state +/// was built with them so we're stuck with them forever. +/// +/// They're just a noop, never actually get used if the runtime was not compiled with +/// `runtime-benchmarks`. pub type HostFunctions = ( sp_io::SubstrateHostFunctions, frame_benchmarking::benchmarking::HostFunctions, ); -/// Otherwise we use empty host functions for ext host functions. -#[cfg(not(feature = "runtime-benchmarks"))] -pub type HostFunctions = sp_io::SubstrateHostFunctions; pub type Backend = FullBackend; pub type Client = FullClient; diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 744b66e0b..4dc24a7a4 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1258,8 +1258,7 @@ pub mod pallet { /// Returns the transaction priority for setting weights. pub fn get_priority_set_weights(hotkey: &T::AccountId, netuid: u16) -> u64 { if let Ok(uid) = Self::get_uid_for_net_and_hotkey(netuid, hotkey) { - // TODO rethink this. - let _stake = Self::get_global_for_hotkey(hotkey); + let _stake = Self::get_stake_for_hotkey_on_subnet(hotkey, netuid); let current_block_number: u64 = Self::get_current_block_as_u64(); let default_priority: u64 = current_block_number.saturating_sub(Self::get_last_update_for_uid(netuid, uid)); @@ -1271,18 +1270,7 @@ pub mod pallet { /// Is the caller allowed to set weights pub fn check_weights_min_stake(hotkey: &T::AccountId, netuid: u16) -> bool { // Blacklist weights transactions for low stake peers. - let min_stake = Self::get_weights_min_stake(); - let hotkey_stake = Self::get_stake_for_hotkey_on_subnet(hotkey, netuid); - let result = hotkey_stake >= min_stake; - log::info!( - "Checking weights min stake for hotkey: {:?}, netuid: {}, min_stake: {}, hotkey_stake: {}, result: {}", - hotkey, - netuid, - min_stake, - hotkey_stake, - result - ); - result + Self::get_stake_for_hotkey_on_subnet(hotkey, netuid) >= Self::get_weights_min_stake() } /// Helper function to check if register is allowed @@ -1437,6 +1425,18 @@ where Err(InvalidTransaction::Custom(2).into()) } } + Some(Call::batch_reveal_weights { netuid, .. }) => { + if Self::check_weights_min_stake(who, *netuid) { + let priority: u64 = Self::get_priority_set_weights(who, *netuid); + Ok(ValidTransaction { + priority, + longevity: 1, + ..Default::default() + }) + } else { + Err(InvalidTransaction::Custom(6).into()) + } + } Some(Call::set_weights { netuid, .. }) => { if Self::check_weights_min_stake(who, *netuid) { let priority: u64 = Self::get_priority_set_weights(who, *netuid); @@ -1451,7 +1451,6 @@ where } Some(Call::set_root_weights { netuid, hotkey, .. }) => { if Self::check_weights_min_stake(hotkey, *netuid) { - let priority: u64 = Self::get_priority_set_weights(hotkey, *netuid); Ok(ValidTransaction { priority, longevity: 1, diff --git a/pallets/subtensor/src/migrations/migrate_fix_pending_emission.rs b/pallets/subtensor/src/migrations/migrate_fix_pending_emission.rs new file mode 100644 index 000000000..b5e833aeb --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_fix_pending_emission.rs @@ -0,0 +1,501 @@ +use super::*; +use alloc::string::String; +use frame_support::{traits::Get, weights::Weight}; +use sp_core::crypto::Ss58Codec; +use sp_runtime::AccountId32; + +fn get_account_id_from_ss58(ss58_str: &str) -> Result { + let account = + AccountId32::from_ss58check(ss58_str).map_err(|_| codec::Error::from("Invalid SS58"))?; + let onchain_account = T::AccountId::decode(&mut account.as_ref())?; + + Ok(onchain_account) +} + +/** + * Migrates the pending emissions from the old hotkey to the new hotkey. + * Also migrates the stake entry of (old_hotkey, 0x000) to the pending emissions of the new hotkey. + */ +fn migrate_pending_emissions_including_null_stake( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + migration_account: &T::AccountId, +) -> Weight { + let mut weight = T::DbWeight::get().reads(0); + let null_account = &DefaultAccount::::get(); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + + // Get the pending emissions for the OLD hotkey + let pending_emissions_old: u64 = PendingdHotkeyEmission::::get(old_hotkey); + PendingdHotkeyEmission::::remove(old_hotkey); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + + // Get the stake for the 0x000 key + let null_stake = Stake::::get(old_hotkey, null_account); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + // Remove + Stake::::remove(old_hotkey, null_account); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + let new_total_coldkey_stake = + TotalColdkeyStake::::get(null_account).saturating_sub(null_stake); + if new_total_coldkey_stake == 0 { + TotalColdkeyStake::::remove(null_account); + } else { + TotalColdkeyStake::::insert(null_account, new_total_coldkey_stake); + } + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + + let new_staking_hotkeys = StakingHotkeys::::get(null_account); + let new_staking_hotkeys = new_staking_hotkeys + .into_iter() + .filter(|hk| hk != old_hotkey) + .collect::>(); + StakingHotkeys::::insert(null_account, new_staking_hotkeys); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + + // Insert the stake from the null account to the MIGRATION account under the OLD hotkey + Stake::::insert(old_hotkey, migration_account, null_stake); + TotalColdkeyStake::::insert( + migration_account, + TotalColdkeyStake::::get(migration_account).saturating_add(null_stake), + ); + let mut new_staking_hotkeys = StakingHotkeys::::get(migration_account); + if !new_staking_hotkeys.contains(old_hotkey) { + new_staking_hotkeys.push(old_hotkey.clone()); + } + StakingHotkeys::::insert(migration_account, new_staking_hotkeys); + weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 3)); + + // Get the pending emissions for the NEW hotkey + let pending_emissions_new: u64 = PendingdHotkeyEmission::::get(new_hotkey); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + + // Add the pending emissions for the new hotkey and the old hotkey + PendingdHotkeyEmission::::insert( + new_hotkey, + pending_emissions_new.saturating_add(pending_emissions_old), + ); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + weight +} + +// This executes the migration to fix the pending emissions +// This also migrates the stake entry of (old_hotkey, 0x000) to the Migration Account for +// both the old hotkeys. +pub fn do_migrate_fix_pending_emission() -> Weight { + // Initialize the weight with one read operation. + let mut weight = T::DbWeight::get().reads(1); + + let taostats_old_hotkey = "5Hddm3iBFD2GLT5ik7LZnT3XJUnRnN8PoeCFgGQgawUVKNm8"; + let taostats_new_hotkey = "5GKH9FPPnWSUoeeTJp19wVtd84XqFW4pyK2ijV2GsFbhTrP1"; + let migration_coldkey = "5GeRjQYsobRWFnrbBmGe5ugme3rfnDVF69N45YtdBpUFsJG8"; + + let taostats_old_hk_account = get_account_id_from_ss58::(taostats_old_hotkey); + let taostats_new_hk_account = get_account_id_from_ss58::(taostats_new_hotkey); + let migration_ck_account = get_account_id_from_ss58::(migration_coldkey); + + match ( + taostats_old_hk_account, + taostats_new_hk_account, + migration_ck_account.clone(), + ) { + (Ok(taostats_old_hk_acct), Ok(taostats_new_hk_acct), Ok(migration_ck_account)) => { + weight.saturating_accrue(migrate_pending_emissions_including_null_stake::( + &taostats_old_hk_acct, + &taostats_new_hk_acct, + &migration_ck_account, + )); + log::info!("Migrated pending emissions from taostats old hotkey to new hotkey"); + } + _ => { + log::warn!("Failed to get account id from ss58 for taostats hotkeys"); + return weight; + } + } + + let datura_old_hotkey = "5FKstHjZkh4v3qAMSBa1oJcHCLjxYZ8SNTSz1opTv4hR7gVB"; + let datura_new_hotkey = "5GP7c3fFazW9GXK8Up3qgu2DJBk8inu4aK9TZy3RuoSWVCMi"; + + let datura_old_hk_account = get_account_id_from_ss58::(datura_old_hotkey); + let datura_new_hk_account = get_account_id_from_ss58::(datura_new_hotkey); + + match ( + datura_old_hk_account, + datura_new_hk_account, + migration_ck_account, + ) { + (Ok(datura_old_hk_acct), Ok(datura_new_hk_acct), Ok(migration_ck_account)) => { + weight.saturating_accrue(migrate_pending_emissions_including_null_stake::( + &datura_old_hk_acct, + &datura_new_hk_acct, + &migration_ck_account, + )); + log::info!("Migrated pending emissions from datura old hotkey to new hotkey"); + } + _ => { + log::warn!("Failed to get account id from ss58 for datura hotkeys"); + return weight; + } + } + + weight +} + +/// Collection of storage item formats from the previous storage version. +/// +/// Required so we can read values in the v0 storage format during the migration. +#[cfg(feature = "try-runtime")] +mod v0 { + use subtensor_macros::freeze_struct; + + #[freeze_struct("2228babfc0580c62")] + #[derive(codec::Encode, codec::Decode, Clone, PartialEq, Debug)] + pub struct OldStorage { + pub total_issuance_before: u64, + pub total_stake_before: u64, + pub expected_taostats_new_hk_pending_emission: u64, + pub expected_datura_new_hk_pending_emission: u64, + pub old_migration_stake_taostats: u64, + pub old_null_stake_taostats: u64, + pub old_migration_stake_datura: u64, + pub old_null_stake_datura: u64, + } +} + +impl Pallet { + #[cfg(feature = "try-runtime")] + fn check_null_stake_invariants( + old_storage: v0::OldStorage, + ) -> Result<(), sp_runtime::TryRuntimeError> { + let null_account = &DefaultAccount::::get(); + + let taostats_old_hotkey = "5Hddm3iBFD2GLT5ik7LZnT3XJUnRnN8PoeCFgGQgawUVKNm8"; + let taostats_new_hotkey = "5GKH9FPPnWSUoeeTJp19wVtd84XqFW4pyK2ijV2GsFbhTrP1"; + let migration_coldkey = "5GeRjQYsobRWFnrbBmGe5ugme3rfnDVF69N45YtdBpUFsJG8"; + + let taostats_old_hk_account = &get_account_id_from_ss58::(taostats_old_hotkey); + let taostats_new_hk_account = &get_account_id_from_ss58::(taostats_new_hotkey); + let migration_ck_account = &get_account_id_from_ss58::(migration_coldkey); + + let old = old_storage; + let null_stake_total = old + .old_null_stake_taostats + .saturating_add(old.old_null_stake_datura) + .saturating_add(old.old_migration_stake_taostats) + .saturating_add(old.old_migration_stake_datura); + + match ( + taostats_old_hk_account, + taostats_new_hk_account, + migration_ck_account, + ) { + (Ok(taostats_old_hk_acct), Ok(taostats_new_hk_acct), Ok(migration_ck_acct)) => { + // Check the pending emission is added to new the TaoStats hotkey + assert_eq!( + PendingdHotkeyEmission::::get(taostats_new_hk_acct), + old.expected_taostats_new_hk_pending_emission + ); + + assert_eq!(PendingdHotkeyEmission::::get(taostats_old_hk_acct), 0); + + assert_eq!(Stake::::get(taostats_old_hk_acct, null_account), 0); + + assert!(StakingHotkeys::::get(migration_ck_acct).contains(taostats_old_hk_acct)); + + assert_eq!( + Self::get_stake_for_coldkey_and_hotkey(null_account, taostats_old_hk_acct), + 0 + ); + + // Check the total hotkey stake is the same + assert_eq!( + TotalHotkeyStake::::get(taostats_old_hk_acct), + old.old_null_stake_taostats + .saturating_add(old.old_migration_stake_taostats) + ); + + let new_null_stake_taostats = + Self::get_stake_for_coldkey_and_hotkey(migration_ck_acct, taostats_old_hk_acct); + + assert_eq!( + new_null_stake_taostats, + old.old_null_stake_taostats + .saturating_add(old.old_migration_stake_taostats) + ); + } + _ => { + log::warn!("Failed to get account id from ss58 for taostats hotkeys"); + return Err("Failed to get account id from ss58 for taostats hotkeys".into()); + } + } + + let datura_old_hotkey = "5FKstHjZkh4v3qAMSBa1oJcHCLjxYZ8SNTSz1opTv4hR7gVB"; + let datura_new_hotkey = "5GP7c3fFazW9GXK8Up3qgu2DJBk8inu4aK9TZy3RuoSWVCMi"; + + let datura_old_hk_account = &get_account_id_from_ss58::(datura_old_hotkey); + let datura_new_hk_account = &get_account_id_from_ss58::(datura_new_hotkey); + + match ( + datura_old_hk_account, + datura_new_hk_account, + migration_ck_account, + ) { + (Ok(datura_old_hk_acct), Ok(datura_new_hk_acct), Ok(migration_ck_acct)) => { + // Check the pending emission is added to new Datura hotkey + assert_eq!( + crate::PendingdHotkeyEmission::::get(datura_new_hk_acct), + old.expected_datura_new_hk_pending_emission + ); + + // Check the pending emission is removed from old ones + assert_eq!(PendingdHotkeyEmission::::get(datura_old_hk_acct), 0); + + // Check the stake entry is removed + assert_eq!(Stake::::get(datura_old_hk_acct, null_account), 0); + + assert!(StakingHotkeys::::get(migration_ck_acct).contains(datura_old_hk_acct)); + + assert_eq!( + Self::get_stake_for_coldkey_and_hotkey(null_account, datura_old_hk_acct), + 0 + ); + + // Check the total hotkey stake is the same + assert_eq!( + TotalHotkeyStake::::get(datura_old_hk_acct), + old.old_null_stake_datura + .saturating_add(old.old_migration_stake_datura) + ); + + let new_null_stake_datura = + Self::get_stake_for_coldkey_and_hotkey(migration_ck_acct, datura_old_hk_acct); + + assert_eq!( + new_null_stake_datura, + old.old_null_stake_datura + .saturating_add(old.old_migration_stake_datura) + ); + } + _ => { + log::warn!("Failed to get account id from ss58 for datura hotkeys"); + return Err("Failed to get account id from ss58 for datura hotkeys".into()); + } + } + + match migration_ck_account { + Ok(migration_ck_acct) => { + // Check the migration key has stake with both *old* hotkeys + assert_eq!( + TotalColdkeyStake::::get(migration_ck_acct), + null_stake_total + ); + } + _ => { + log::warn!("Failed to get account id from ss58 for migration coldkey"); + return Err("Failed to get account id from ss58 for migration coldkey".into()); + } + } + + // Check the total issuance is the SAME following migration (no TAO issued) + let expected_total_issuance = old.total_issuance_before; + let expected_total_stake = old.total_stake_before; + assert_eq!(Self::get_total_issuance(), expected_total_issuance); + + // Check total stake is the SAME following the migration (no new TAO staked) + assert_eq!(TotalStake::::get(), expected_total_stake); + // Check the total stake maps are updated following the migration (removal of old null_account stake entries) + assert_eq!(TotalColdkeyStake::::get(null_account), 0); + + // Check staking hotkeys is updated + assert_eq!(StakingHotkeys::::get(null_account), vec![]); + + Ok(()) + } +} + +pub mod migration { + use frame_support::pallet_prelude::Weight; + use frame_support::traits::OnRuntimeUpgrade; + use sp_core::Get; + + use super::*; + + pub struct Migration(PhantomData); + + #[cfg(feature = "try-runtime")] + fn get_old_storage_values() -> Result { + log::info!("Getting old storage values for migration"); + + let null_account = &DefaultAccount::::get(); + let migration_coldkey = "5GeRjQYsobRWFnrbBmGe5ugme3rfnDVF69N45YtdBpUFsJG8"; + let migration_account = &get_account_id_from_ss58::(migration_coldkey); + + let taostats_old_hotkey = "5Hddm3iBFD2GLT5ik7LZnT3XJUnRnN8PoeCFgGQgawUVKNm8"; + let taostats_new_hotkey = "5GKH9FPPnWSUoeeTJp19wVtd84XqFW4pyK2ijV2GsFbhTrP1"; + + let taostats_old_hk_account = &get_account_id_from_ss58::(taostats_old_hotkey); + let taostats_new_hk_account = &get_account_id_from_ss58::(taostats_new_hotkey); + + let total_issuance_before = crate::Pallet::::get_total_issuance(); + let mut expected_taostats_new_hk_pending_emission: u64 = 0; + let mut expected_datura_new_hk_pending_emission: u64 = 0; + let (old_null_stake_taostats, old_migration_stake_taostats) = match ( + taostats_old_hk_account, + taostats_new_hk_account, + migration_account, + ) { + (Ok(taostats_old_hk_acct), Ok(taostats_new_hk_acct), Ok(migration_acct)) => { + expected_taostats_new_hk_pending_emission = + expected_taostats_new_hk_pending_emission + .saturating_add(PendingdHotkeyEmission::::get(taostats_old_hk_acct)) + .saturating_add(PendingdHotkeyEmission::::get(taostats_new_hk_acct)); + + Ok::<(u64, u64), sp_runtime::TryRuntimeError>(( + crate::Pallet::::get_stake_for_coldkey_and_hotkey( + null_account, + taostats_old_hk_acct, + ), + crate::Pallet::::get_stake_for_coldkey_and_hotkey( + migration_acct, + taostats_old_hk_acct, + ), + )) + } + _ => { + log::warn!("Failed to get account id from ss58 for taostats hotkeys"); + Err("Failed to get account id from ss58 for taostats hotkeys".into()) + } + }?; + + let datura_old_hotkey = "5FKstHjZkh4v3qAMSBa1oJcHCLjxYZ8SNTSz1opTv4hR7gVB"; + let datura_new_hotkey = "5GP7c3fFazW9GXK8Up3qgu2DJBk8inu4aK9TZy3RuoSWVCMi"; + + let datura_old_hk_account = &get_account_id_from_ss58::(datura_old_hotkey); + let datura_new_hk_account = &get_account_id_from_ss58::(datura_new_hotkey); + + let (old_null_stake_datura, old_migration_stake_datura) = match ( + datura_old_hk_account, + datura_new_hk_account, + migration_account, + ) { + (Ok(datura_old_hk_acct), Ok(datura_new_hk_acct), Ok(migration_acct)) => { + expected_datura_new_hk_pending_emission = expected_datura_new_hk_pending_emission + .saturating_add(PendingdHotkeyEmission::::get(datura_old_hk_acct)) + .saturating_add(PendingdHotkeyEmission::::get(datura_new_hk_acct)); + + Ok::<(u64, u64), sp_runtime::TryRuntimeError>(( + crate::Pallet::::get_stake_for_coldkey_and_hotkey( + null_account, + datura_old_hk_acct, + ), + crate::Pallet::::get_stake_for_coldkey_and_hotkey( + migration_acct, + datura_old_hk_acct, + ), + )) + } + _ => { + log::warn!("Failed to get account id from ss58 for datura hotkeys"); + Err("Failed to get account id from ss58 for datura hotkeys".into()) + } + }?; + + let total_stake_before: u64 = crate::Pallet::::get_total_stake(); + + let result = v0::OldStorage { + total_issuance_before, + total_stake_before, + expected_taostats_new_hk_pending_emission, + expected_datura_new_hk_pending_emission, + old_migration_stake_taostats, + old_null_stake_taostats, + old_migration_stake_datura, + old_null_stake_datura, + }; + + log::info!("Got old storage values for migration"); + + Ok(result) + } + + impl OnRuntimeUpgrade for Migration { + /// Runs the migration to fix the pending emissions. + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + use codec::Encode; + + // Get the old storage values + match get_old_storage_values::() { + Ok(old_storage) => { + log::info!("Successfully got old storage values for migration"); + let encoded = old_storage.encode(); + + Ok(encoded) + } + Err(e) => { + log::error!("Failed to get old storage values for migration: {:?}", e); + Err("Failed to get old storage values for migration".into()) + } + } + } + + // Runs the migrate function for the fix_pending_emission migration + fn on_runtime_upgrade() -> Weight { + let migration_name = b"fix_pending_emission".to_vec(); + + // Initialize the weight with one read operation. + let mut weight = T::DbWeight::get().reads(1); + + // Check if the migration has already run + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + migration_name + ); + return Weight::zero(); + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name) + ); + + // Run the migration + weight.saturating_accrue( + migrations::migrate_fix_pending_emission::do_migrate_fix_pending_emission::(), + ); + + // Mark the migration as completed + HasMigrationRun::::insert(&migration_name, true); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{:?}' completed. Marked in storage.", + String::from_utf8_lossy(&migration_name) + ); + + // Return the migration weight. + weight + } + + /// Performs post-upgrade checks to ensure the migration was successful. + /// + /// This function is only compiled when the "try-runtime" feature is enabled. + #[cfg(feature = "try-runtime")] + fn post_upgrade(state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { + use codec::Decode; + + let old_storage: v0::OldStorage = + v0::OldStorage::decode(&mut &state[..]).map_err(|_| { + sp_runtime::TryRuntimeError::Other("Failed to decode old value from storage") + })?; + + // Verify that all null stake invariants are satisfied after the migration + crate::Pallet::::check_null_stake_invariants(old_storage)?; + + Ok(()) + } + } +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index b731b1b7c..53c7f0b00 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -4,6 +4,7 @@ pub mod migrate_commit_reveal_v2; pub mod migrate_create_root_network; pub mod migrate_delete_subnet_21; pub mod migrate_delete_subnet_3; +pub mod migrate_fix_pending_emission; pub mod migrate_fix_total_coldkey_stake; pub mod migrate_init_total_issuance; pub mod migrate_populate_owned_hotkeys; diff --git a/pallets/subtensor/src/rpc_info/neuron_info.rs b/pallets/subtensor/src/rpc_info/neuron_info.rs index 26f222d9c..4870e9a94 100644 --- a/pallets/subtensor/src/rpc_info/neuron_info.rs +++ b/pallets/subtensor/src/rpc_info/neuron_info.rs @@ -1,6 +1,5 @@ use super::*; use frame_support::pallet_prelude::{Decode, Encode}; -use frame_support::storage::IterableStorageDoubleMap; extern crate alloc; use codec::Compact; @@ -177,12 +176,10 @@ impl Pallet { let last_update = Self::get_last_update_for_uid(netuid, uid); let validator_permit = Self::get_validator_permit_for_uid(netuid, uid); - let stake: Vec<(T::AccountId, Compact)> = - as IterableStorageDoubleMap>::iter_prefix( - hotkey.clone(), - ) - .map(|(coldkey, stake)| (coldkey, stake.into())) - .collect(); + let stake: Vec<(T::AccountId, Compact)> = vec![( + coldkey.clone(), + Self::get_stake_for_hotkey_on_subnet(&hotkey, netuid).into(), + )]; let neuron = NeuronInfoLite { hotkey: hotkey.clone(), diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index eaee696f0..172c79016 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -150,6 +150,135 @@ impl Pallet { } } + /// Returns true if the cold-hot staking account has enough balance to fulfill the decrement. + /// + /// # Arguments + /// * `coldkey` - The coldkey account ID. + /// * `hotkey` - The hotkey account ID. + /// * `decrement` - The amount to be decremented. + /// + /// # Returns + /// True if the account has enough balance, false otherwise. + pub fn has_enough_stake(coldkey: &T::AccountId, hotkey: &T::AccountId, decrement: u64) -> bool { + Self::get_stake_for_coldkey_and_hotkey(coldkey, hotkey) >= decrement + } + + /// Increases the stake on the hotkey account under its owning coldkey. + /// + /// # Arguments + /// * `hotkey` - The hotkey account ID. + /// * `increment` - The amount to be incremented. + pub fn increase_stake_on_hotkey_account(hotkey: &T::AccountId, increment: u64) { + Self::increase_stake_on_coldkey_hotkey_account( + &Self::get_owning_coldkey_for_hotkey(hotkey), + hotkey, + increment, + ); + } + + /// Decreases the stake on the hotkey account under its owning coldkey. + /// + /// # Arguments + /// * `hotkey` - The hotkey account ID. + /// * `decrement` - The amount to be decremented. + pub fn decrease_stake_on_hotkey_account(hotkey: &T::AccountId, decrement: u64) { + Self::decrease_stake_on_coldkey_hotkey_account( + &Self::get_owning_coldkey_for_hotkey(hotkey), + hotkey, + decrement, + ); + } + + // Increases the stake on the cold - hot pairing by increment while also incrementing other counters. + // This function should be called rather than set_stake under account. + // + pub fn increase_stake_on_coldkey_hotkey_account( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + increment: u64, + ) { + log::debug!( + "Increasing stake: coldkey: {:?}, hotkey: {:?}, amount: {}", + coldkey, + hotkey, + increment + ); + + TotalColdkeyStake::::insert( + coldkey, + TotalColdkeyStake::::get(coldkey).saturating_add(increment), + ); + TotalHotkeyStake::::insert( + hotkey, + TotalHotkeyStake::::get(hotkey).saturating_add(increment), + ); + Stake::::insert( + hotkey, + coldkey, + Stake::::get(hotkey, coldkey).saturating_add(increment), + ); + TotalStake::::put(TotalStake::::get().saturating_add(increment)); + + // Update StakingHotkeys map + let mut staking_hotkeys = StakingHotkeys::::get(coldkey); + if !staking_hotkeys.contains(hotkey) { + staking_hotkeys.push(hotkey.clone()); + StakingHotkeys::::insert(coldkey, staking_hotkeys); + } + } + + // Decreases the stake on the cold - hot pairing by the decrement while decreasing other counters. + // + pub fn decrease_stake_on_coldkey_hotkey_account( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + decrement: u64, + ) { + TotalColdkeyStake::::mutate(coldkey, |old| *old = old.saturating_sub(decrement)); + TotalHotkeyStake::::insert( + hotkey, + TotalHotkeyStake::::get(hotkey).saturating_sub(decrement), + ); + Stake::::insert( + hotkey, + coldkey, + Stake::::get(hotkey, coldkey).saturating_sub(decrement), + ); + TotalStake::::put(TotalStake::::get().saturating_sub(decrement)); + + // TODO: Tech debt: Remove StakingHotkeys entry if stake goes to 0 + } + + /// Empties the stake associated with a given coldkey-hotkey account pairing. + /// This function retrieves the current stake for the specified coldkey-hotkey pairing, + /// then subtracts this stake amount from both the TotalColdkeyStake and TotalHotkeyStake. + /// It also removes the stake entry for the hotkey-coldkey pairing and adjusts the TotalStake + /// and TotalIssuance by subtracting the removed stake amount. + /// + /// Returns the amount of stake that was removed. + /// + /// # Arguments + /// + /// * `coldkey` - A reference to the AccountId of the coldkey involved in the staking. + /// * `hotkey` - A reference to the AccountId of the hotkey associated with the coldkey. + pub fn empty_stake_on_coldkey_hotkey_account( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + ) -> u64 { + let current_stake: u64 = Stake::::get(hotkey, coldkey); + TotalColdkeyStake::::mutate(coldkey, |old| *old = old.saturating_sub(current_stake)); + TotalHotkeyStake::::mutate(hotkey, |stake| *stake = stake.saturating_sub(current_stake)); + Stake::::remove(hotkey, coldkey); + TotalStake::::mutate(|stake| *stake = stake.saturating_sub(current_stake)); + + // Update StakingHotkeys map + let mut staking_hotkeys = StakingHotkeys::::get(coldkey); + staking_hotkeys.retain(|h| h != hotkey); + StakingHotkeys::::insert(coldkey, staking_hotkeys); + + current_stake + } + /// Clears the nomination for an account, if it is a nominator account and the stake is below the minimum required threshold. pub fn clear_small_nomination_if_required( hotkey: &T::AccountId, @@ -265,20 +394,18 @@ impl Pallet { Ok(credit) } - // TODO deprecate this function. - // DEPRECATED. - // pub fn unstake_all_coldkeys_from_hotkey_account_on_network(hotkey: &T::AccountId, netuid: u16) { - // // Iterate through all coldkeys that have a stake on this hotkey account. - // for (nominator, _) in Stake::::iter_prefix(hotkey) { - // for netuid_i in Self::get_all_subnet_netuids() { - // if netuid_i != netuid { - // continue; - // } - // let alpha: u64 = - // Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, &nominator, netuid_i); - // let tao: u64 = Self::unstake_from_subnet(hotkey, &nominator, netuid_i, alpha); - // Self::add_balance_to_coldkey_account(&nominator, tao); - // } - // } - // } + pub fn unstake_all_coldkeys_from_hotkey_account(hotkey: &T::AccountId) { + // Iterate through all coldkeys that have a stake on this hotkey account. + for (delegate_coldkey_i, stake_i) in + as IterableStorageDoubleMap>::iter_prefix( + hotkey, + ) + { + // Remove the stake from the coldkey - hotkey pairing. + Self::decrease_stake_on_coldkey_hotkey_account(&delegate_coldkey_i, hotkey, stake_i); + + // Add the balance to the coldkey account. + Self::add_balance_to_coldkey_account(&delegate_coldkey_i, stake_i); + } + } } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index dd0cc4807..3096a7d4d 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -82,6 +82,11 @@ impl Pallet { let tao_unstaked: u64 = Self::unstake_from_subnet(&hotkey, &coldkey, netuid, alpha_unstaked); + // Track this removal in the stake delta. + StakeDeltaSinceLastEmissionDrain::::mutate(&hotkey, &coldkey, |stake_delta| { + *stake_delta = stake_delta.saturating_sub_unsigned(stake_to_be_removed as u128); + }); + // We add the balance to the coldkey. If the above fails we will not credit this coldkey. Self::add_balance_to_coldkey_account(&coldkey, tao_unstaked); diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index 51d32bfd3..330c4a33f 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -196,7 +196,7 @@ impl Pallet { } weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); - // 5. Swap StakingHotkeys. + // 6. Swap StakingHotkeys. // StakingHotkeys: MAP ( coldkey ) --> Vec | Hotkeys staking for the coldkey. let old_staking_hotkeys: Vec = StakingHotkeys::::get(old_coldkey); let mut new_staking_hotkeys: Vec = StakingHotkeys::::get(new_coldkey); @@ -210,7 +210,7 @@ impl Pallet { StakingHotkeys::::insert(new_coldkey, new_staking_hotkeys); weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); - // 6. Swap hotkey owners. + // 7. Swap hotkey owners. // Owner: MAP ( hotkey ) --> coldkey | Owner of the hotkey. // OwnedHotkeys: MAP ( coldkey ) --> Vec | Hotkeys owned by the coldkey. let old_owned_hotkeys: Vec = OwnedHotkeys::::get(old_coldkey); @@ -229,7 +229,7 @@ impl Pallet { OwnedHotkeys::::insert(new_coldkey, new_owned_hotkeys); weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); - // 7. Transfer remaining balance. + // 8. Transfer remaining balance. // Balance: MAP ( coldkey ) --> u64 | Balance of the coldkey. // Transfer any remaining balance from old_coldkey to new_coldkey let remaining_balance = Self::get_coldkey_balance(old_coldkey); diff --git a/pallets/subtensor/src/utils/try_state.rs b/pallets/subtensor/src/utils/try_state.rs index 3c01a9b64..385a21bdd 100644 --- a/pallets/subtensor/src/utils/try_state.rs +++ b/pallets/subtensor/src/utils/try_state.rs @@ -17,7 +17,7 @@ impl Pallet { // Calculate the total staked amount let mut total_staked: u64 = 0; - for (_account, _netuid, stake) in Stake::::iter() { + for (_hotkey, _coldkey, stake) in Stake::::iter() { total_staked = total_staked.saturating_add(stake); } diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index 6f1101b4e..69532dbec 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3595,3 +3595,194 @@ fn test_rank_trust_incentive_calculation_with_parent_child() { // }); } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test children -- test_childkey_set_weights_single_parent --exact --nocapture +#[test] +fn test_childkey_set_weights_single_parent() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + add_network(netuid, 1, 0); + + // Define hotkeys + let parent: U256 = U256::from(1); + let child: U256 = U256::from(2); + let weight_setter: U256 = U256::from(3); + + // Define coldkeys with more readable names + let coldkey_parent: U256 = U256::from(100); + let coldkey_child: U256 = U256::from(101); + let coldkey_weight_setter: U256 = U256::from(102); + + let stake_to_give_child = 109_999; + + // Register parent with minimal stake and child with high stake + SubtensorModule::add_balance_to_coldkey_account(&coldkey_parent, 1); + SubtensorModule::add_balance_to_coldkey_account(&coldkey_child, stake_to_give_child + 10); + SubtensorModule::add_balance_to_coldkey_account(&coldkey_weight_setter, 1_000_000); + + // Add neurons for parent, child and weight_setter + register_ok_neuron(netuid, parent, coldkey_parent, 1); + register_ok_neuron(netuid, child, coldkey_child, 1); + register_ok_neuron(netuid, weight_setter, coldkey_weight_setter, 1); + + SubtensorModule::increase_stake_on_coldkey_hotkey_account( + &coldkey_parent, + &parent, + stake_to_give_child, + ); + SubtensorModule::increase_stake_on_coldkey_hotkey_account( + &coldkey_weight_setter, + &weight_setter, + 1_000_000, + ); + + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // Set parent-child relationship + assert_ok!(SubtensorModule::do_set_children( + RuntimeOrigin::signed(coldkey_parent), + parent, + netuid, + vec![(u64::MAX, child)] + )); + step_block(7200 + 1); + // Set weights on the child using the weight_setter account + let origin = RuntimeOrigin::signed(weight_setter); + let uids: Vec = vec![1]; // Only set weight for the child (UID 1) + let values: Vec = vec![u16::MAX]; // Use maximum value for u16 + let version_key = SubtensorModule::get_weights_version_key(netuid); + assert_ok!(SubtensorModule::set_weights( + origin, + netuid, + uids.clone(), + values.clone(), + version_key + )); + + // Set the min stake very high + SubtensorModule::set_weights_min_stake(stake_to_give_child * 5); + + // Check the child has less stake than required + assert!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid) + < SubtensorModule::get_weights_min_stake() + ); + + // Check the child cannot set weights + assert_noop!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(child), + netuid, + uids.clone(), + values.clone(), + version_key + ), + Error::::NotEnoughStakeToSetWeights + ); + + assert!(!SubtensorModule::check_weights_min_stake(&child, netuid)); + + // Set a minimum stake to set weights + SubtensorModule::set_weights_min_stake(stake_to_give_child - 5); + + // Check if the stake for the child is above + assert!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid) + >= SubtensorModule::get_weights_min_stake() + ); + + // Check the child can set weights + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(child), + netuid, + uids, + values, + version_key + )); + + assert!(SubtensorModule::check_weights_min_stake(&child, netuid)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test children -- test_set_weights_no_parent --exact --nocapture +#[test] +fn test_set_weights_no_parent() { + // Verify that a regular key without a parent delegation is effected by the minimum stake requirements + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + add_network(netuid, 1, 0); + + let hotkey: U256 = U256::from(2); + let spare_hk: U256 = U256::from(3); + + let coldkey: U256 = U256::from(101); + let spare_ck = U256::from(102); + + let stake_to_give_child = 109_999; + + SubtensorModule::add_balance_to_coldkey_account(&coldkey, stake_to_give_child + 10); + + // Is registered + register_ok_neuron(netuid, hotkey, coldkey, 1); + // Register a spare key + register_ok_neuron(netuid, spare_hk, spare_ck, 1); + + SubtensorModule::increase_stake_on_coldkey_hotkey_account( + &coldkey, + &hotkey, + stake_to_give_child, + ); + + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // Has stake and no parent + step_block(7200 + 1); + + let uids: Vec = vec![1]; // Set weights on the other hotkey + let values: Vec = vec![u16::MAX]; // Use maximum value for u16 + let version_key = SubtensorModule::get_weights_version_key(netuid); + + // Set the min stake very high + SubtensorModule::set_weights_min_stake(stake_to_give_child * 5); + + // Check the key has less stake than required + assert!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey, netuid) + < SubtensorModule::get_weights_min_stake() + ); + + // Check the hotkey cannot set weights + assert_noop!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + values.clone(), + version_key + ), + Error::::NotEnoughStakeToSetWeights + ); + + assert!(!SubtensorModule::check_weights_min_stake(&hotkey, netuid)); + + // Set a minimum stake to set weights + SubtensorModule::set_weights_min_stake(stake_to_give_child - 5); + + // Check if the stake for the hotkey is above + assert!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey, netuid) + >= SubtensorModule::get_weights_min_stake() + ); + + // Check the hotkey can set weights + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + values, + version_key + )); + + assert!(SubtensorModule::check_weights_min_stake(&hotkey, netuid)); + }); +} diff --git a/pallets/subtensor/tests/coinbase.rs b/pallets/subtensor/tests/coinbase.rs index 11ae3c3e8..7a575147a 100644 --- a/pallets/subtensor/tests/coinbase.rs +++ b/pallets/subtensor/tests/coinbase.rs @@ -6,6 +6,9 @@ use frame_support::{assert_err, assert_ok}; use pallet_subtensor::*; use sp_core::Get; use sp_core::U256; +use substrate_fixed::types::I64F64; + +use pallet_subtensor::TargetStakesPerInterval; // Test the ability to hash all sorts of hotkeys. #[test] @@ -422,5 +425,1190 @@ fn test_distribute_owner_cut_is_actually_used() { // Verify distribution assert!(Alpha::::get((subnet_owner_hotkey, subnet_owner_coldkey, netuid)) > 0); +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_overflow -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_overflow() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let vali_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + vali_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 5e9 as u64); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 5e9 as u64); + let initial_stake = 5e9 as u64; + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + // 5. Set emission and verify initial states + let to_emit = 20_000e9 as u64; + SubtensorModule::set_emission_values(&[netuid], vec![to_emit]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), to_emit); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + initial_stake * 2 + ); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), to_emit); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let hotkey_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + hotkey_stake, + nominator1_stake, + nominator2_stake + ); + + // 9. Verify distribution + let total_emission = to_emit * 2; // to_emit per block for 2 blocks + let hotkey_emission = (I64F64::from_num(total_emission) / I64F64::from_num(u16::MAX) + * I64F64::from_num(vali_take)) + .to_num::(); + let remaining_emission = total_emission - hotkey_emission; + let nominator_emission = remaining_emission / 2; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: {}", + hotkey_emission, + nominator_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", hotkey_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + let expected_hotkey_stake = 4_000e9 as u64; + let eps = 0.5e9 as u64; + assert!( + hotkey_stake >= expected_hotkey_stake - eps + && hotkey_stake <= expected_hotkey_stake + eps, + "Hotkey stake mismatch - expected: {}, actual: {}", + expected_hotkey_stake, + hotkey_stake + ); + assert_eq!( + nominator1_stake, + initial_stake + nominator_emission, + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + initial_stake + nominator_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + assert_eq!( + total_stake, + initial_stake + initial_stake + total_emission, + "Total stake mismatch" + ); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_no_deltas -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_no_deltas() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 100); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 100); + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + // 5. Set emission and verify initial states + SubtensorModule::set_emission_values(&[netuid], vec![10]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), 10); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!(SubtensorModule::get_total_stake_for_hotkey(&hotkey), 200); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 10); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let hotkey_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + hotkey_stake, + nominator1_stake, + nominator2_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = 20; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + let nominator_emission = remaining_emission / 2; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: {}", + hotkey_emission, + nominator_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", hotkey_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + assert_eq!(hotkey_stake, 2, "Hotkey stake mismatch"); + assert_eq!( + nominator1_stake, + 100 + nominator_emission, + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + 100 + nominator_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + assert_eq!(total_stake, 200 + total_emission, "Total stake mismatch"); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_with_positive_delta -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_with_positive_delta() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 100); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 100); + + // Do an add_stake for nominator 1 + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + 123 + )); // We should not expect this to impact the emissions + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + let nominator1_stake_before = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + assert_eq!(nominator1_stake_before, 100 + 123); // The stake should include the added stake + + // 5. Set emission and verify initial states + SubtensorModule::set_emission_values(&[netuid], vec![10]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), 10); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + 200 + 123 + ); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 10); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let hotkey_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + hotkey_stake, + nominator1_stake, + nominator2_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = 20; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + let nominator_emission = remaining_emission / 2; + // Notice that nominator emission is equal for both nominators, even though nominator1 added stake + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: {}", + hotkey_emission, + nominator_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", hotkey_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + assert_eq!(hotkey_stake, 2, "Hotkey stake mismatch"); + assert_eq!( + nominator1_stake, + 100 + 123 + nominator_emission, + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + 100 + nominator_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + // Includes the added stake from nominator1 + assert_eq!( + total_stake, + 200 + 123 + total_emission, + "Total stake mismatch" + ); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_with_negative_delta -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_with_negative_delta() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 100); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 100); + + // Do an remove_stake for nominator 1 + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + 12 + )); // We should expect the emissions to be impacted; + // The viable stake should be the *new* stake for nominator 1 + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + let nominator_1_stake_before = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + // Notice that nominator1 stake is the new stake, including the removed stake + assert_eq!(nominator_1_stake_before, 100 - 12); + + // 5. Set emission and verify initial states + SubtensorModule::set_emission_values(&[netuid], vec![10]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), 10); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + 200 - 12 + ); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 10); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let delegate_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}, Total Hotkey Stake: {}", + delegate_stake, + nominator1_stake, + nominator2_stake, + total_hotkey_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = 20; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + + let nominator_1_emission = remaining_emission * nominator1_stake / total_hotkey_stake; + let nominator_2_emission = remaining_emission * nominator2_stake / total_hotkey_stake; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: 1;{}, 2;{}", + hotkey_emission, + nominator_1_emission, + nominator_2_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", delegate_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + assert_eq!(delegate_stake, 2, "Hotkey stake mismatch"); + assert_eq!( + nominator1_stake, + 100 - 12 + nominator_1_emission, + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + 100 + nominator_2_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + assert_eq!( + total_stake, + 200 - 12 + total_emission, + "Total stake mismatch" + ); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_with_neutral_delta -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_with_neutral_delta() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 100); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 100); + + // Do an remove_stake for nominator 1 + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + 12 + )); + // Do an add_stake for nominator 1 of the same amount + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + 12 + )); // The viable stake should match the initial stake, because the delta is 0 + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + let nominator1_stake_before = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + // Notice that nominator1 stake is the unchanged from the initial stake + assert_eq!(nominator1_stake_before, 100); + + // 5. Set emission and verify initial states + SubtensorModule::set_emission_values(&[netuid], vec![10]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), 10); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!(SubtensorModule::get_total_stake_for_hotkey(&hotkey), 200); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 10); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let delegate_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}, Total Hotkey Stake: {}", + delegate_stake, + nominator1_stake, + nominator2_stake, + total_hotkey_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = 20; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + + let nominator_1_emission = remaining_emission * nominator1_stake / total_hotkey_stake; + let nominator_2_emission = remaining_emission * nominator2_stake / total_hotkey_stake; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: 1;{}, 2;{}", + hotkey_emission, + nominator_1_emission, + nominator_2_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", delegate_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + assert_eq!(delegate_stake, 2, "Hotkey stake mismatch"); + assert_eq!( + nominator1_stake, + 100 + nominator_1_emission, // We expect the emission to be calculated based on the initial stake + // Because the delta is 0. + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + 100 + nominator_2_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + assert_eq!(total_stake, 200 + total_emission, "Total stake mismatch"); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_with_net_positive_delta -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_with_net_positive_delta() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 100); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 100); + + let initial_nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let intial_total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let initial_nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + assert_eq!(initial_nominator1_stake, initial_nominator2_stake); // Initial stakes should be equal + + let removed_stake = 12; + // Do an add_stake for nominator 1 of MORE than was removed + let added_stake = removed_stake + 1; + let net_change: i128 = i128::from(added_stake) - i128::from(removed_stake); // Positive net change + + // Do an remove_stake for nominator 1 + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + removed_stake + )); + + // Do an add_stake for nominator 1 of MORE than was removed + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + added_stake + )); // We should expect the emissions to be impacted; + // The viable stake should be the same initial stake for nominator 1 + // NOT the new stake amount, because the delta is net positive + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + let nominator_1_stake_before = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + // Notice that nominator1 stake is the new stake, including the removed stake + assert_eq!( + nominator_1_stake_before, + u64::try_from(100 + net_change).unwrap() + ); + + // 5. Set emission and verify initial states + SubtensorModule::set_emission_values(&[netuid], vec![10]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), 10); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + u64::try_from(200 + net_change).unwrap() + ); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 10); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let delegate_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}, Total Hotkey Stake: {}", + delegate_stake, + nominator1_stake, + nominator2_stake, + total_hotkey_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = 20; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + + // We expect to distribute using the initial stake for nominator 1; because the delta is net positive + // We also use the INITIAL total hotkey stake + let nominator_1_emission = + remaining_emission * initial_nominator1_stake / intial_total_hotkey_stake; + let nominator_2_emission = + remaining_emission * initial_nominator2_stake / intial_total_hotkey_stake; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: 1;{}, 2;{}", + hotkey_emission, + nominator_1_emission, + nominator_2_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", delegate_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + assert_eq!(delegate_stake, 2, "Hotkey stake mismatch"); + assert_eq!( + nominator1_stake, + u64::try_from( + net_change + .checked_add_unsigned(100 + nominator_1_emission as u128) + .unwrap() + ) + .unwrap(), + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + initial_nominator2_stake + nominator_2_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + assert_eq!( + total_stake, + u64::try_from( + net_change + .checked_add_unsigned(200 + total_emission as u128) + .unwrap() + ) + .unwrap(), + "Total stake mismatch" + ); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_with_net_negative_delta -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_with_net_negative_delta() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 300); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 300); + + let initial_nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let intial_total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let initial_nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + assert_eq!(initial_nominator1_stake, initial_nominator2_stake); // Initial stakes should be equal + + let removed_stake = 220; + // Do an add_stake for nominator 1 of LESS than was removed + let added_stake = removed_stake - 188; + let net_change: i128 = i128::from(added_stake) - i128::from(removed_stake); // Negative net change + assert!(net_change < 0); + + // Do an remove_stake for nominator 1 + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + removed_stake + )); + + // Do an add_stake for nominator 1 of MORE than was removed + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + added_stake + )); // We should expect the emissions to be impacted; + // The viable stake should be the LESS than the initial stake for nominator 1 + // Which IS the new stake amount, because the delta is net negative + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + let total_stake_before = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let nominator_1_stake_before = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + // Notice that nominator1 stake is the new stake, including the removed stake + assert_eq!( + nominator_1_stake_before, + u64::try_from(300 + net_change).unwrap() + ); + + // 5. Set emission and verify initial states + let to_emit = 10_000e9 as u64; + SubtensorModule::set_emission_values(&[netuid], vec![to_emit]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), to_emit); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + u64::try_from(600 + net_change).unwrap() + ); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), to_emit); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let delegate_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}, Total Hotkey Stake: {}", + delegate_stake, + nominator1_stake, + nominator2_stake, + total_hotkey_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = to_emit * 2; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + + // We expect to distribute using the NEW stake for nominator 1; because the delta is net negative + // We also use the INITIAL total hotkey stake + // Note: nominator_1_stake_before is the new stake for nominator 1, before the epochs run + let nominator_1_emission = + remaining_emission * nominator_1_stake_before / total_stake_before; + let nominator_2_emission = + remaining_emission * initial_nominator2_stake / total_stake_before; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: 1;{}, 2;{}", + hotkey_emission, + nominator_1_emission, + nominator_2_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", delegate_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Do a fuzzy check on the final stakes + let eps = 0.2e9 as u64; + + let expected_delegate_stake: u64 = 2_000e9 as u64; + assert!( + expected_delegate_stake - eps <= delegate_stake + && expected_delegate_stake + eps >= delegate_stake, + "Hotkey stake mismatch - Expected: {}, Actual: {}", + expected_delegate_stake, + delegate_stake + ); + + let expected_1_stake = u64::try_from( + net_change + .checked_add_unsigned((initial_nominator1_stake + nominator_1_emission) as u128) + .unwrap(), + ) + .unwrap(); + assert!( + expected_1_stake - eps <= nominator1_stake + && expected_1_stake + eps >= nominator1_stake, + "Nominator1 stake mismatch - Expected: {}, Actual: {}", + expected_1_stake, + nominator1_stake + ); + let expected_2_stake = initial_nominator2_stake + nominator_2_emission; + assert!( + expected_2_stake - eps <= nominator2_stake + && expected_2_stake + eps >= nominator2_stake, + "Nominator2 stake mismatch - Expected: {}, Actual: {}", + expected_2_stake, + nominator2_stake + ); + + // 10. Check total stake + assert_eq!( + total_stake, + u64::try_from( + net_change + .checked_add_unsigned( + (initial_nominator2_stake + initial_nominator1_stake + total_emission) + as u128 + ) + .unwrap() + ) + .unwrap(), + "Total stake mismatch" + ); + + log::debug!("Test completed"); }); } diff --git a/pallets/subtensor/tests/migration.rs b/pallets/subtensor/tests/migration.rs index fc2483516..2a18b9fe2 100644 --- a/pallets/subtensor/tests/migration.rs +++ b/pallets/subtensor/tests/migration.rs @@ -11,9 +11,10 @@ use frame_support::{ use frame_system::Config; use mock::*; use pallet_subtensor::*; -use sp_core::{H256, U256}; +use sp_core::{crypto::Ss58Codec, H256, U256}; use sp_io::hashing::twox_128; use sp_runtime::traits::Zero; +use substrate_fixed::types::extra::U2; #[test] fn test_initialise_ti() { @@ -528,3 +529,158 @@ fn test_migrate_commit_reveal_2() { assert!(!weight.is_zero(), "Migration weight should be non-zero"); }); } + +fn run_pending_emissions_migration_and_check( + migration_name: &'static str, +) -> frame_support::weights::Weight { + use frame_support::traits::OnRuntimeUpgrade; + + // Execute the migration and store its weight + let weight: frame_support::weights::Weight = + pallet_subtensor::migrations::migrate_fix_pending_emission::migration::Migration::::on_runtime_upgrade(); + + // Check if the migration has been marked as completed + assert!(HasMigrationRun::::get( + migration_name.as_bytes().to_vec() + )); + + // Return the weight of the executed migration + weight +} + +fn get_account_id_from_ss58(ss58_str: &str) -> U256 { + let account_id = sp_core::crypto::AccountId32::from_ss58check(ss58_str).unwrap(); + let account_id = AccountId::decode(&mut account_id.as_ref()).unwrap(); + account_id +} + +// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --test migration -- test_migrate_fix_pending_emissions --exact --nocapture +#[test] +fn test_migrate_fix_pending_emissions() { + new_test_ext(1).execute_with(|| { + let migration_name = "fix_pending_emission"; + + let null_account = &U256::from(0); // The null account + + let taostats_old_hotkey = "5Hddm3iBFD2GLT5ik7LZnT3XJUnRnN8PoeCFgGQgawUVKNm8"; + let taostats_new_hotkey = "5GKH9FPPnWSUoeeTJp19wVtd84XqFW4pyK2ijV2GsFbhTrP1"; + + let taostats_old_hk_account: &AccountId = &get_account_id_from_ss58(taostats_old_hotkey); + let taostats_new_hk_account: &AccountId = &get_account_id_from_ss58(taostats_new_hotkey); + + let datura_old_hotkey = "5FKstHjZkh4v3qAMSBa1oJcHCLjxYZ8SNTSz1opTv4hR7gVB"; + let datura_new_hotkey = "5GP7c3fFazW9GXK8Up3qgu2DJBk8inu4aK9TZy3RuoSWVCMi"; + + let datura_old_hk_account: &AccountId = &get_account_id_from_ss58(datura_old_hotkey); + let datura_new_hk_account: &AccountId = &get_account_id_from_ss58(datura_new_hotkey); + + let migration_coldkey = "5GeRjQYsobRWFnrbBmGe5ugme3rfnDVF69N45YtdBpUFsJG8"; + let migration_account: &AccountId = &get_account_id_from_ss58(migration_coldkey); + + // "Issue" the TAO we're going to insert to stake + let null_stake_datura = 123_456_789; + let null_stake_tao_stats = 123_456_789; + let null_stake_total = null_stake_datura + null_stake_tao_stats; + SubtensorModule::set_total_issuance(null_stake_total); + TotalStake::::put(null_stake_total); + TotalColdkeyStake::::insert(null_account, null_stake_total); + TotalHotkeyStake::::insert(datura_old_hk_account, null_stake_datura); + TotalHotkeyStake::::insert(taostats_old_hk_account, null_stake_tao_stats); + + // Setup the old Datura hotkey with a pending emission + PendingdHotkeyEmission::::insert(datura_old_hk_account, 10_000); + // Setup the NEW Datura hotkey with a pending emission + PendingdHotkeyEmission::::insert(datura_new_hk_account, 123_456_789); + Stake::::insert(datura_old_hk_account, null_account, null_stake_datura); + let expected_datura_new_hk_pending_emission: u64 = 123_456_789 + 10_000; + + // Setup the old TaoStats hotkey with a pending emission + PendingdHotkeyEmission::::insert(taostats_old_hk_account, 987_654); + // Setup the new TaoStats hotkey with a pending emission + PendingdHotkeyEmission::::insert(taostats_new_hk_account, 100_000); + // Setup the old TaoStats hotkey with a null-key stake entry + Stake::::insert(taostats_old_hk_account, null_account, null_stake_tao_stats); + let expected_taostats_new_hk_pending_emission: u64 = 987_654 + 100_000; + + let total_issuance_before = SubtensorModule::get_total_issuance(); + + // Run migration + let first_weight = run_pending_emissions_migration_and_check(migration_name); + assert!(first_weight != Weight::zero()); + + // Check the pending emission is added to new Datura hotkey + assert_eq!( + PendingdHotkeyEmission::::get(datura_new_hk_account), + expected_datura_new_hk_pending_emission + ); + + // Check the pending emission is added to new the TaoStats hotkey + assert_eq!( + PendingdHotkeyEmission::::get(taostats_new_hk_account), + expected_taostats_new_hk_pending_emission + ); + + // Check the pending emission is removed from old ones + assert_eq!( + PendingdHotkeyEmission::::get(datura_old_hk_account), + 0 + ); + + assert_eq!( + PendingdHotkeyEmission::::get(taostats_old_hk_account), + 0 + ); + + // Check the stake entry is removed + assert_eq!(Stake::::get(datura_old_hk_account, null_account), 0); + assert_eq!(Stake::::get(taostats_old_hk_account, null_account), 0); + + // Check the total issuance is the SAME following migration (no TAO issued) + let expected_total_issuance = total_issuance_before; + assert_eq!( + SubtensorModule::get_total_issuance(), + expected_total_issuance + ); + + // Check total stake is the SAME following the migration (no new TAO staked) + assert_eq!(TotalStake::::get(), expected_total_issuance); + // Check the total stake maps are updated following the migration (removal of old null_account stake entries) + assert_eq!(TotalColdkeyStake::::get(null_account), 0); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(null_account, datura_old_hk_account), + 0 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey( + null_account, + taostats_old_hk_account + ), + 0 + ); + + // Check staking hotkeys is updated + assert_eq!(StakingHotkeys::::get(null_account), vec![]); + + // Check the migration key has stake with both *old* hotkeys + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey( + migration_account, + datura_old_hk_account + ), + null_stake_datura + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey( + migration_account, + taostats_old_hk_account + ), + null_stake_tao_stats + ); + assert_eq!( + TotalColdkeyStake::::get(migration_account), + null_stake_total + ); + assert!(StakingHotkeys::::get(migration_account).contains(datura_old_hk_account)); + assert!(StakingHotkeys::::get(migration_account).contains(taostats_old_hk_account)); + }) +} diff --git a/pallets/subtensor/tests/swap_coldkey.rs b/pallets/subtensor/tests/swap_coldkey.rs index f1b3bf043..e05cc5357 100644 --- a/pallets/subtensor/tests/swap_coldkey.rs +++ b/pallets/subtensor/tests/swap_coldkey.rs @@ -1178,3 +1178,41 @@ fn test_coldkey_swap_no_identity_no_changes_newcoldkey_exists() { assert!(Identities::::get(new_coldkey).is_some()); }); } + +// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --test swap_coldkey -- test_coldkey_swap_stake_delta --exact --nocapture +#[test] +fn test_coldkey_swap_stake_delta() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(3); + let new_coldkey = U256::from(4); + let hotkey = U256::from(5); + + let netuid = 1; + let burn_cost = 10; + let tempo = 1; + + // Give the old coldkey a stake delta on hotkey + StakeDeltaSinceLastEmissionDrain::::insert(hotkey, old_coldkey, 123); + // Give the new coldkey a stake delta on hotkey + StakeDeltaSinceLastEmissionDrain::::insert(hotkey, new_coldkey, 456); + let expected_stake_delta = 123 + 456; + // Add StakingHotkeys entry + StakingHotkeys::::insert(old_coldkey, vec![hotkey]); + + // Give balance for the swap fees + SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, 100e9 as u64); + + // Perform the coldkey swap + assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey)); + + // Ensure the stake delta is correctly transferred + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(hotkey, new_coldkey), + expected_stake_delta + ); + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(hotkey, old_coldkey), + 0 + ); + }); +} diff --git a/pallets/subtensor/tests/swap_hotkey.rs b/pallets/subtensor/tests/swap_hotkey.rs index a332b7ccf..813939422 100644 --- a/pallets/subtensor/tests/swap_hotkey.rs +++ b/pallets/subtensor/tests/swap_hotkey.rs @@ -9,6 +9,7 @@ use mock::*; use pallet_subtensor::*; use sp_core::H256; use sp_core::U256; +use sp_runtime::SaturatedConversion; // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey -- test_swap_owner --exact --nocapture #[test] @@ -1195,3 +1196,91 @@ fn test_swap_child_hotkey_childkey_maps() { ); }) } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey -- test_hotkey_swap_stake_delta --exact --nocapture +#[test] +fn test_hotkey_swap_stake_delta() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(3); + let new_hotkey = U256::from(4); + let coldkey = U256::from(7); + + let coldkeys = [U256::from(1), U256::from(2), U256::from(5)]; + + let mut weight = Weight::zero(); + + // Set up initial state + // Add stake delta for each coldkey and the old_hotkey + for &coldkey in coldkeys.iter() { + StakeDeltaSinceLastEmissionDrain::::insert( + old_hotkey, + coldkey, + (123 + coldkey.saturated_into::()), + ); + + StakingHotkeys::::insert(coldkey, vec![old_hotkey]); + } + + // Add stake delta for one coldkey and the new_hotkey + StakeDeltaSinceLastEmissionDrain::::insert(new_hotkey, coldkeys[0], 456); + // Add corresponding StakingHotkeys + StakingHotkeys::::insert(coldkeys[0], vec![old_hotkey, new_hotkey]); + + // Perform the swap + SubtensorModule::perform_hotkey_swap(&old_hotkey, &new_hotkey, &coldkey, &mut weight); + + // Ensure the stake delta is correctly transferred for each coldkey + // -- coldkey[0] maintains its stake delta from the new_hotkey and the old_hotkey + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(new_hotkey, coldkeys[0]), + 123 + coldkeys[0].saturated_into::() + 456 + ); + // -- coldkey[1..] maintains its stake delta from the old_hotkey + for &coldkey in coldkeys[1..].iter() { + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(new_hotkey, coldkey), + 123 + coldkey.saturated_into::() + ); + assert!(!StakeDeltaSinceLastEmissionDrain::::contains_key( + old_hotkey, coldkey + )); + } + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey -- test_swap_hotkey_with_pending_emissions --exact --nocapture +#[test] +fn test_swap_hotkey_with_pending_emissions() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let netuid = 0u16; + let mut weight = Weight::zero(); + + let pending_emission = 123_456_789u64; + + // Set up initial state + add_network(netuid, 0, 1); + + // Set up pending emissions + PendingdHotkeyEmission::::insert(old_hotkey, pending_emission); + // Verify the pending emissions are set + assert_eq!( + PendingdHotkeyEmission::::get(old_hotkey), + pending_emission + ); + // Verify the new hotkey does not have any pending emissions + assert!(!PendingdHotkeyEmission::::contains_key(new_hotkey)); + + // Perform the swap + SubtensorModule::perform_hotkey_swap(&old_hotkey, &new_hotkey, &coldkey, &mut weight); + + // Verify the pending emissions are transferred + assert_eq!( + PendingdHotkeyEmission::::get(new_hotkey), + pending_emission + ); + assert!(!PendingdHotkeyEmission::::contains_key(old_hotkey)); + }); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 781cada26..504dd73d1 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -160,7 +160,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: 205, + spec_version: 207, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -652,6 +652,7 @@ pub enum ProxyType { Transfer, SmallTransfer, RootWeights, + SudoUncheckedSetCode, } // Transfers below SMALL_TRANSFER_LIMIT are considered small transfers pub const SMALL_TRANSFER_LIMIT: Balance = 500_000_000; // 0.5 TAO @@ -696,6 +697,7 @@ impl InstanceFilter for ProxyType { | RuntimeCall::SubtensorModule(pallet_subtensor::Call::burned_register { .. }) | RuntimeCall::Triumvirate(..) | RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_root_weights { .. }) + | RuntimeCall::Sudo(..) ), ProxyType::Triumvirate => matches!( c, @@ -722,6 +724,17 @@ impl InstanceFilter for ProxyType { c, RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_root_weights { .. }) ), + ProxyType::SudoUncheckedSetCode => match c { + RuntimeCall::Sudo(pallet_sudo::Call::sudo_unchecked_weight { call, weight: _ }) => { + let inner_call: RuntimeCall = *call.clone(); + + matches!( + inner_call, + RuntimeCall::System(frame_system::Call::set_code { .. }) + ) + } + _ => false, + }, } } fn is_superset(&self, o: &Self) -> bool { diff --git a/runtime/src/precompiles/solidity/staking.abi b/runtime/src/precompiles/solidity/staking.abi index 5baf231c9..44b1829c4 100644 --- a/runtime/src/precompiles/solidity/staking.abi +++ b/runtime/src/precompiles/solidity/staking.abi @@ -5,6 +5,11 @@ "internalType": "bytes32", "name": "hotkey", "type": "bytes32" + }, + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" } ], "name": "addStake", @@ -23,6 +28,11 @@ "internalType": "uint256", "name": "amount", "type": "uint256" + }, + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" } ], "name": "removeStake", @@ -30,4 +40,4 @@ "stateMutability": "payable", "type": "function" } -] \ No newline at end of file +] diff --git a/runtime/src/precompiles/solidity/staking.sol b/runtime/src/precompiles/solidity/staking.sol index 8d0b1f6aa..ec7fb7297 100644 --- a/runtime/src/precompiles/solidity/staking.sol +++ b/runtime/src/precompiles/solidity/staking.sol @@ -3,40 +3,43 @@ pragma solidity ^0.8.0; address constant ISTAKING_ADDRESS = 0x0000000000000000000000000000000000000801; interface IStaking { - /** - * @dev Adds a subtensor stake corresponding to the value sent with the transaction, associated - * with the `hotkey`. - * - * This function allows external accounts and contracts to stake TAO into the subtensor pallet, - * which effectively calls `add_stake` on the subtensor pallet with specified hotkey as a parameter - * and coldkey being the hashed address mapping of H160 sender address to Substrate ss58 address as - * implemented in Frontier HashedAddressMapping: - * https://github.com/polkadot-evm/frontier/blob/2e219e17a526125da003e64ef22ec037917083fa/frame/evm/src/lib.rs#L739 - * - * @param hotkey The hotkey public key (32 bytes). - * - * Requirements: - * - `hotkey` must be a valid hotkey registered on the network, ensuring that the stake is - * correctly attributed. - */ - function addStake(bytes32 hotkey) external payable; + /** + * @dev Adds a subtensor stake corresponding to the value sent with the transaction, associated + * with the `hotkey`. + * + * This function allows external accounts and contracts to stake TAO into the subtensor pallet, + * which effectively calls `add_stake` on the subtensor pallet with specified hotkey as a parameter + * and coldkey being the hashed address mapping of H160 sender address to Substrate ss58 address as + * implemented in Frontier HashedAddressMapping: + * https://github.com/polkadot-evm/frontier/blob/2e219e17a526125da003e64ef22ec037917083fa/frame/evm/src/lib.rs#L739 + * + * @param hotkey The hotkey public key (32 bytes). + * @param netuid The subnet to stake to (uint16). Currently a noop, functionality will be enabled with RAO. + * + * Requirements: + * - `hotkey` must be a valid hotkey registered on the network, ensuring that the stake is + * correctly attributed. + */ + function addStake(bytes32 hotkey, uint16 netuid) external payable; - /** - * @dev Removes a subtensor stake `amount` from the specified `hotkey`. - * - * This function allows external accounts and contracts to unstake TAO from the subtensor pallet, - * which effectively calls `remove_stake` on the subtensor pallet with specified hotkey as a parameter - * and coldkey being the hashed address mapping of H160 sender address to Substrate ss58 address as - * implemented in Frontier HashedAddressMapping: - * https://github.com/polkadot-evm/frontier/blob/2e219e17a526125da003e64ef22ec037917083fa/frame/evm/src/lib.rs#L739 - * - * @param hotkey The hotkey public key (32 bytes). - * @param amount The amount to unstake in rao. - * - * Requirements: - * - `hotkey` must be a valid hotkey registered on the network, ensuring that the stake is - * correctly attributed. - * - The existing stake amount must be not lower than specified amount - */ - function removeStake(bytes32 hotkey, uint256 amount) external; -} \ No newline at end of file + /** + * @dev Removes a subtensor stake `amount` from the specified `hotkey`. + * + * This function allows external accounts and contracts to unstake TAO from the subtensor pallet, + * which effectively calls `remove_stake` on the subtensor pallet with specified hotkey as a parameter + * and coldkey being the hashed address mapping of H160 sender address to Substrate ss58 address as + * implemented in Frontier HashedAddressMapping: + * https://github.com/polkadot-evm/frontier/blob/2e219e17a526125da003e64ef22ec037917083fa/frame/evm/src/lib.rs#L739 + * + * @param hotkey The hotkey public key (32 bytes). + * @param amount The amount to unstake in rao. + * @param netuid The subnet to stake to (uint16). Currently a noop, functionality will be enabled with RAO. + + * + * Requirements: + * - `hotkey` must be a valid hotkey registered on the network, ensuring that the stake is + * correctly attributed. + * - The existing stake amount must be not lower than specified amount + */ + function removeStake(bytes32 hotkey, uint256 amount, uint16 netuid) external; +} diff --git a/runtime/src/precompiles/staking.rs b/runtime/src/precompiles/staking.rs index c70e66d88..daa95ba93 100644 --- a/runtime/src/precompiles/staking.rs +++ b/runtime/src/precompiles/staking.rs @@ -53,10 +53,10 @@ impl StakingPrecompile { .map_or_else(vec::Vec::new, |slice| slice.to_vec()); // Avoiding borrowing conflicts match method_id { - id if id == get_method_id("addStake(bytes32)") => { + id if id == get_method_id("addStake(bytes32,uint16)") => { Self::add_stake(handle, &method_input) } - id if id == get_method_id("removeStake(bytes32,uint256)") => { + id if id == get_method_id("removeStake(bytes32,uint256,uint16)") => { Self::remove_stake(handle, &method_input) } _ => Err(PrecompileFailure::Error {