diff --git a/.github/workflows/check-finney.yml b/.github/workflows/check-finney.yml index 642e02c0a..23ef93cdf 100644 --- a/.github/workflows/check-finney.yml +++ b/.github/workflows/check-finney.yml @@ -30,7 +30,7 @@ jobs: - name: Check that spec_version has been bumped run: | - spec_version=$(PATH=$PATH:$HOME/.cargo/.bin substrate-spec-version ${{ secrets.NUCLEUS_ARCHIVE_NODE }} | tr -d '\n') + spec_version=$(PATH=$PATH:$HOME/.cargo/.bin substrate-spec-version ${{ vars.NUCLEUS_ARCHIVE_NODE }} | tr -d '\n') echo "network spec_version: $spec_version" : ${spec_version:?bad spec version} local_spec_version=$(cargo run -p node-subtensor-runtime --bin spec_version | tr -d '\n') @@ -49,6 +49,6 @@ jobs: uses: "paritytech/try-runtime-gha@v0.1.0" with: runtime-package: "node-subtensor-runtime" - node-uri: ${{ secrets.NUCLEUS_ARCHIVE_NODE }} + node-uri: ${{ vars.NUCLEUS_ARCHIVE_NODE }} checks: "pre-and-post" extra-args: "--disable-spec-version-check --no-weight-warnings" diff --git a/Cargo.toml b/Cargo.toml index f9a7968b9..4f162ca6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ indexing-slicing = "deny" arithmetic-side-effects = "deny" type_complexity = "allow" unwrap-used = "deny" +manual_inspect = "allow" [workspace.dependencies] cargo-husky = { version = "1", default-features = false } diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 09f1b327b..4486356a4 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -249,6 +249,20 @@ impl Pallet { }); } + /// Calculates the nonviable stake for a nominator. + /// The nonviable stake is the stake that was added by the nominator since the last emission drain. + /// This stake will not receive emission until the next emission drain. + /// Note: if the stake delta is below zero, we return zero. We don't allow more stake than the nominator has. + pub fn get_nonviable_stake(hotkey: &T::AccountId, nominator: &T::AccountId) -> u64 { + let stake_delta = StakeDeltaSinceLastEmissionDrain::::get(hotkey, nominator); + if stake_delta.is_negative() { + 0 + } else { + // Should never fail the into, but we handle it anyway. + stake_delta.try_into().unwrap_or(u64::MAX) + } + } + //. --- 4. Drains the accumulated hotkey emission through to the nominators. The hotkey takes a proportion of the emission. /// The remainder is drained through to the nominators keeping track of the last stake increase event to ensure that the hotkey does not /// gain more emission than it's stake since the last drain. @@ -268,71 +282,67 @@ impl Pallet { // --- 1.0 Drain the hotkey emission. PendingdHotkeyEmission::::insert(hotkey, 0); - // --- 2 Retrieve the last time this hotkey's emissions were drained. - let last_emission_drain: u64 = LastHotkeyEmissionDrain::::get(hotkey); - - // --- 3 Update the block value to the current block number. + // --- 2 Update the block value to the current block number. LastHotkeyEmissionDrain::::insert(hotkey, block_number); - // --- 4 Retrieve the total stake for the hotkey from all nominations. + // --- 3 Retrieve the total stake for the hotkey from all nominations. let total_hotkey_stake: u64 = Self::get_total_stake_for_hotkey(hotkey); - // --- 5 Calculate the emission take for the hotkey. + // --- 4 Calculate the emission take for the hotkey. let take_proportion: I64F64 = I64F64::from_num(Delegates::::get(hotkey)) .saturating_div(I64F64::from_num(u16::MAX)); let hotkey_take: u64 = (take_proportion.saturating_mul(I64F64::from_num(emission))).to_num::(); - // --- 6 Compute the remaining emission after deducting the hotkey's take. + // --- 5 Compute the remaining emission after deducting the hotkey's take. let emission_minus_take: u64 = emission.saturating_sub(hotkey_take); - // --- 7 Calculate the remaining emission after the hotkey's take. + // --- 6 Calculate the remaining emission after the hotkey's take. let mut remainder: u64 = emission_minus_take; - // --- 8 Iterate over each nominator and get all viable stake. + // --- 7 Iterate over each nominator and get all viable stake. let mut total_viable_nominator_stake: u64 = total_hotkey_stake; - for (nominator, nominator_stake) in Stake::::iter_prefix(hotkey) { - if false && LastAddStakeIncrease::::get(hotkey, nominator) > last_emission_drain { - total_viable_nominator_stake = - total_viable_nominator_stake.saturating_sub(nominator_stake); - } + for (nominator, _) in Stake::::iter_prefix(hotkey) { + let nonviable_nomintaor_stake = Self::get_nonviable_stake(hotkey, &nominator); + + total_viable_nominator_stake = + total_viable_nominator_stake.saturating_sub(nonviable_nomintaor_stake); } - // --- 9 Iterate over each nominator. + // --- 8 Iterate over each nominator. if total_viable_nominator_stake != 0 { for (nominator, nominator_stake) in Stake::::iter_prefix(hotkey) { - // --- 10 Check if the stake was manually increased by the user since the last emission drain for this hotkey. - // If it was, skip this nominator as they will not receive their proportion of the emission. - if false - && LastAddStakeIncrease::::get(hotkey, nominator.clone()) - > last_emission_drain - { - continue; - } + // --- 9 Skip emission for any stake the was added by the nominator since the last emission drain. + // This means the nominator will get emission on existing stake, but not on new stake, until the next emission drain. + let viable_nominator_stake = + nominator_stake.saturating_sub(Self::get_nonviable_stake(hotkey, &nominator)); - // --- 11 Calculate this nominator's share of the emission. - let nominator_emission: I64F64 = I64F64::from_num(nominator_stake) + // --- 10 Calculate this nominator's share of the emission. + let nominator_emission: I64F64 = I64F64::from_num(viable_nominator_stake) .checked_div(I64F64::from_num(total_viable_nominator_stake)) .unwrap_or(I64F64::from_num(0)) .saturating_mul(I64F64::from_num(emission_minus_take)); - // --- 12 Increase the stake for the nominator. + // --- 11 Increase the stake for the nominator. Self::increase_stake_on_coldkey_hotkey_account( &nominator, hotkey, nominator_emission.to_num::(), ); - // --- 13* Record event and Subtract the nominator's emission from the remainder. + // --- 12* Record event and Subtract the nominator's emission from the remainder. total_new_tao = total_new_tao.saturating_add(nominator_emission.to_num::()); remainder = remainder.saturating_sub(nominator_emission.to_num::()); } } - // --- 14 Finally, add the stake to the hotkey itself, including its take and the remaining emission. + // --- 13 Finally, add the stake to the hotkey itself, including its take and the remaining emission. let hotkey_new_tao: u64 = hotkey_take.saturating_add(remainder); Self::increase_stake_on_hotkey_account(hotkey, hotkey_new_tao); + // --- 14 Reset the stake delta for the hotkey. + let _ = StakeDeltaSinceLastEmissionDrain::::clear_prefix(hotkey, u32::MAX, None); + // --- 15 Record new tao creation event and return the amount created. total_new_tao = total_new_tao.saturating_add(hotkey_new_tao); total_new_tao diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 2985736c8..6c8da7a69 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -226,6 +226,11 @@ pub mod pallet { 0 } #[pallet::type_value] + /// Default stake delta. + pub fn DefaultStakeDelta() -> i128 { + 0 + } + #[pallet::type_value] /// Default stakes per interval. pub fn DefaultStakesPerInterval() -> (u64, u64) { (0, 0) @@ -770,6 +775,18 @@ pub mod pallet { DefaultAccountTake, >; #[pallet::storage] + /// Map ( hot, cold ) --> stake: i128 | Stake added/removed since last emission drain. + pub type StakeDeltaSinceLastEmissionDrain = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Identity, + T::AccountId, + i128, + ValueQuery, + DefaultStakeDelta, + >; + #[pallet::storage] /// DMAP ( parent, netuid ) --> Vec<(proportion,child)> pub type ChildKeys = StorageDoubleMap< _, @@ -1250,7 +1267,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) { - let _stake = Self::get_total_stake_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)); @@ -1260,9 +1277,9 @@ pub mod pallet { } /// Is the caller allowed to set weights - pub fn check_weights_min_stake(hotkey: &T::AccountId) -> bool { + pub fn check_weights_min_stake(hotkey: &T::AccountId, netuid: u16) -> bool { // Blacklist weights transactions for low stake peers. - Self::get_total_stake_for_hotkey(hotkey) >= Self::get_weights_min_stake() + Self::get_stake_for_hotkey_on_subnet(hotkey, netuid) >= Self::get_weights_min_stake() } /// Helper function to check if register is allowed @@ -1355,8 +1372,8 @@ where Pallet::::get_priority_set_weights(who, netuid) } - pub fn check_weights_min_stake(who: &T::AccountId) -> bool { - Pallet::::check_weights_min_stake(who) + pub fn check_weights_min_stake(who: &T::AccountId, netuid: u16) -> bool { + Pallet::::check_weights_min_stake(who, netuid) } } @@ -1394,7 +1411,7 @@ where ) -> TransactionValidity { match call.is_sub_type() { Some(Call::commit_weights { netuid, .. }) => { - if Self::check_weights_min_stake(who) { + if Self::check_weights_min_stake(who, *netuid) { let priority: u64 = Self::get_priority_set_weights(who, *netuid); Ok(ValidTransaction { priority, @@ -1406,7 +1423,7 @@ where } } Some(Call::reveal_weights { netuid, .. }) => { - if Self::check_weights_min_stake(who) { + if Self::check_weights_min_stake(who, *netuid) { let priority: u64 = Self::get_priority_set_weights(who, *netuid); Ok(ValidTransaction { priority, @@ -1418,7 +1435,7 @@ where } } Some(Call::set_weights { netuid, .. }) => { - if Self::check_weights_min_stake(who) { + if Self::check_weights_min_stake(who, *netuid) { let priority: u64 = Self::get_priority_set_weights(who, *netuid); Ok(ValidTransaction { priority, @@ -1430,7 +1447,7 @@ where } } Some(Call::set_root_weights { netuid, hotkey, .. }) => { - if Self::check_weights_min_stake(hotkey) { + if Self::check_weights_min_stake(hotkey, *netuid) { let priority: u64 = Self::get_priority_set_weights(hotkey, *netuid); Ok(ValidTransaction { priority, diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 76f140002..865cabdc5 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -70,7 +70,9 @@ mod hooks { // Storage version v8 -> v9 .saturating_add(migrations::migrate_fix_total_coldkey_stake::migrate_fix_total_coldkey_stake::()) // Migrate Delegate Ids on chain - .saturating_add(migrations::migrate_chain_identity::migrate_set_hotkey_identities::()); + .saturating_add(migrations::migrate_chain_identity::migrate_set_hotkey_identities::()) + // Fix pending emissions + .saturating_add(migrations::migrate_fix_pending_emission::migrate_fix_pending_emission::()); weight } 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..81da5538b --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_fix_pending_emission.rs @@ -0,0 +1,151 @@ +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, +) -> 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); + 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(old_hotkey).saturating_sub(null_stake); + if new_total_coldkey_stake == 0 { + TotalColdkeyStake::::remove(old_hotkey); + } else { + TotalColdkeyStake::::insert(old_hotkey, new_total_coldkey_stake); + } + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + + let new_total_hotkey_stake = TotalHotkeyStake::::get(old_hotkey).saturating_sub(null_stake); + if new_total_hotkey_stake == 0 { + TotalHotkeyStake::::remove(old_hotkey); + } else { + TotalHotkeyStake::::insert(old_hotkey, new_total_hotkey_stake); + } + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + + TotalStake::::put(TotalStake::::get().saturating_sub(null_stake)); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + + // 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 stake to the pending emissions for the new hotkey and the old hotkey + PendingdHotkeyEmission::::insert( + new_hotkey, + pending_emissions_new + .saturating_add(pending_emissions_old) + .saturating_add(null_stake), + ); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + weight +} + +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 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); + + match (taostats_old_hk_account, taostats_new_hk_account) { + (Ok(taostats_old_hk_acct), Ok(taostats_new_hk_acct)) => { + weight.saturating_accrue(migrate_pending_emissions_including_null_stake::( + &taostats_old_hk_acct, + &taostats_new_hk_acct, + )); + } + _ => { + log::warn!("Failed to get account id from ss58 for taostats hotkeys"); + } + } + + 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) { + (Ok(datura_old_hk_acct), Ok(datura_new_hk_acct)) => { + weight.saturating_accrue(migrate_pending_emissions_including_null_stake::( + &datura_old_hk_acct, + &datura_new_hk_acct, + )); + } + _ => { + log::warn!("Failed to get account id from ss58 for datura hotkeys"); + } + } + + weight +} +// Public migrate function to be called by Lib.rs on upgrade. +pub fn migrate_fix_pending_emission() -> 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(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 +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index 6036b23e0..d62983f96 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -3,6 +3,7 @@ pub mod migrate_chain_identity; 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/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index c9cbd7e04..72d8374bc 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -70,8 +70,10 @@ impl Pallet { Error::::StakeRateLimitExceeded ); - // Set the last time the stake increased for nominator drain protection. - LastAddStakeIncrease::::insert(&hotkey, &coldkey, Self::get_current_block_as_u64()); + // Track this addition in the stake delta. + StakeDeltaSinceLastEmissionDrain::::mutate(&hotkey, &coldkey, |stake_delta| { + *stake_delta = stake_delta.saturating_add_unsigned(stake_to_be_added as u128); + }); // If coldkey is not owner of the hotkey, it's a nomination stake. if !Self::coldkey_owns_hotkey(&coldkey, &hotkey) { diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index 0328d94e6..9fd60ea51 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -297,6 +297,9 @@ impl Pallet { staking_hotkeys.retain(|h| h != hotkey); StakingHotkeys::::insert(coldkey, staking_hotkeys); + // Update stake delta + StakeDeltaSinceLastEmissionDrain::::remove(hotkey, coldkey); + current_stake } @@ -431,6 +434,9 @@ impl Pallet { // Add the balance to the coldkey account. Self::add_balance_to_coldkey_account(&delegate_coldkey_i, stake_i); + + // Remove stake delta + StakeDeltaSinceLastEmissionDrain::::remove(hotkey, &delegate_coldkey_i); } } } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 4118e8d07..587583f5e 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -76,6 +76,11 @@ impl Pallet { // We remove the balance from the hotkey. Self::decrease_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake_to_be_removed); + // 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, stake_to_be_removed); diff --git a/pallets/subtensor/src/subnets/uids.rs b/pallets/subtensor/src/subnets/uids.rs index fff358f1c..0dd3b3ddc 100644 --- a/pallets/subtensor/src/subnets/uids.rs +++ b/pallets/subtensor/src/subnets/uids.rs @@ -117,7 +117,7 @@ impl Pallet { /// pub fn get_stake_for_uid_and_subnetwork(netuid: u16, neuron_uid: u16) -> u64 { if let Ok(hotkey) = Self::get_hotkey_for_net_and_uid(netuid, neuron_uid) { - Self::get_total_stake_for_hotkey(&hotkey) + Self::get_stake_for_hotkey_on_subnet(&hotkey, netuid) } else { 0 } diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index bcbd2a330..4742c3fca 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -169,7 +169,20 @@ impl Pallet { weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); } - // 4. Swap total coldkey stake. + // 4. Swap StakeDeltaSinceLastEmissionDrain + for hotkey in StakingHotkeys::::get(old_coldkey) { + let old_stake_delta = StakeDeltaSinceLastEmissionDrain::::get(&hotkey, old_coldkey); + let new_stake_delta = StakeDeltaSinceLastEmissionDrain::::get(&hotkey, new_coldkey); + StakeDeltaSinceLastEmissionDrain::::insert( + &hotkey, + new_coldkey, + new_stake_delta.saturating_add(old_stake_delta), + ); + StakeDeltaSinceLastEmissionDrain::::remove(&hotkey, old_coldkey); + weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); + } + + // 5. Swap total coldkey stake. // TotalColdkeyStake: MAP ( coldkey ) --> u64 | Total stake of the coldkey. let old_coldkey_stake: u64 = TotalColdkeyStake::::get(old_coldkey); // Get the stake of the new coldkey. @@ -183,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); @@ -197,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); @@ -216,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/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 793e34bff..54095d5fb 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -206,21 +206,30 @@ impl Pallet { Delegates::::insert(new_hotkey, old_delegate_take); weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); } - // 9. Swap all subnet specific info. + + // 9. swap PendingdHotkeyEmission + if PendingdHotkeyEmission::::contains_key(old_hotkey) { + let old_pending_hotkey_emission = PendingdHotkeyEmission::::get(old_hotkey); + PendingdHotkeyEmission::::remove(old_hotkey); + PendingdHotkeyEmission::::insert(new_hotkey, old_pending_hotkey_emission); + weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); + } + + // 10. Swap all subnet specific info. let all_netuids: Vec = Self::get_all_subnet_netuids(); for netuid in all_netuids { - // 9.1 Remove the previous hotkey and insert the new hotkey from membership. + // 10.1 Remove the previous hotkey and insert the new hotkey from membership. // IsNetworkMember( hotkey, netuid ) -> bool -- is the hotkey a subnet member. let is_network_member: bool = IsNetworkMember::::get(old_hotkey, netuid); IsNetworkMember::::remove(old_hotkey, netuid); IsNetworkMember::::insert(new_hotkey, netuid, is_network_member); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); - // 9.2 Swap Uids + Keys. + // 10.2 Swap Uids + Keys. // Keys( netuid, hotkey ) -> uid -- the uid the hotkey has in the network if it is a member. // Uids( netuid, hotkey ) -> uid -- the uids that the hotkey has. if is_network_member { - // 9.2.1 Swap the UIDS + // 10.2.1 Swap the UIDS if let Ok(old_uid) = Uids::::try_get(netuid, old_hotkey) { Uids::::remove(netuid, old_hotkey); Uids::::insert(netuid, new_hotkey, old_uid); @@ -232,7 +241,7 @@ impl Pallet { } } - // 9.3 Swap Prometheus. + // 10.3 Swap Prometheus. // Prometheus( netuid, hotkey ) -> prometheus -- the prometheus data that a hotkey has in the network. if is_network_member { if let Ok(old_prometheus_info) = Prometheus::::try_get(netuid, old_hotkey) { @@ -242,7 +251,7 @@ impl Pallet { } } - // 9.4. Swap axons. + // 10.4. Swap axons. // Axons( netuid, hotkey ) -> axon -- the axon that the hotkey has. if is_network_member { if let Ok(old_axon_info) = Axons::::try_get(netuid, old_hotkey) { @@ -252,7 +261,7 @@ impl Pallet { } } - // 9.5 Swap WeightCommits + // 10.5 Swap WeightCommits // WeightCommits( hotkey ) --> Vec -- the weight commits for the hotkey. if is_network_member { if let Ok(old_weight_commits) = WeightCommits::::try_get(netuid, old_hotkey) { @@ -262,7 +271,7 @@ impl Pallet { } } - // 9.6. Swap the subnet loaded emission. + // 10.6. Swap the subnet loaded emission. // LoadedEmission( netuid ) --> Vec<(hotkey, u64)> -- the loaded emission for the subnet. if is_network_member { if let Some(mut old_loaded_emission) = LoadedEmission::::get(netuid) { @@ -278,7 +287,7 @@ impl Pallet { } } - // 10. Swap Stake. + // 11. Swap Stake. // Stake( hotkey, coldkey ) -> stake -- the stake that the hotkey controls on behalf of the coldkey. let stakes: Vec<(T::AccountId, u64)> = Stake::::iter_prefix(old_hotkey).collect(); // Clear the entire old prefix here. @@ -308,7 +317,7 @@ impl Pallet { weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); } - // 11. Swap ChildKeys. + // 12. Swap ChildKeys. // ChildKeys( parent, netuid ) --> Vec<(proportion,child)> -- the child keys of the parent. for netuid in Self::get_all_subnet_netuids() { // Get the children of the old hotkey for this subnet @@ -319,7 +328,7 @@ impl Pallet { ChildKeys::::insert(new_hotkey, netuid, my_children); } - // 12. Swap ParentKeys. + // 13. Swap ParentKeys. // ParentKeys( child, netuid ) --> Vec<(proportion,parent)> -- the parent keys of the child. for netuid in Self::get_all_subnet_netuids() { // Get the parents of the old hotkey for this subnet @@ -343,6 +352,19 @@ impl Pallet { } } + // 13. Swap Stake Delta for all coldkeys. + for (coldkey, stake_delta) in StakeDeltaSinceLastEmissionDrain::::iter_prefix(old_hotkey) + { + let new_stake_delta = StakeDeltaSinceLastEmissionDrain::::get(new_hotkey, &coldkey); + StakeDeltaSinceLastEmissionDrain::::insert( + new_hotkey, + &coldkey, + new_stake_delta.saturating_add(stake_delta), + ); + StakeDeltaSinceLastEmissionDrain::::remove(old_hotkey, &coldkey); + weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); + } + // Return successful after swapping all the relevant terms. Ok(()) } diff --git a/pallets/subtensor/tests/coinbase.rs b/pallets/subtensor/tests/coinbase.rs index 0f2a32cc3..f82ebd0ed 100644 --- a/pallets/subtensor/tests/coinbase.rs +++ b/pallets/subtensor/tests/coinbase.rs @@ -2,7 +2,8 @@ use crate::mock::*; mod mock; use frame_support::assert_ok; -use pallet_subtensor::LastAddStakeIncrease; + +use pallet_subtensor::TargetStakesPerInterval; use sp_core::U256; use substrate_fixed::types::I64F64; @@ -157,6 +158,109 @@ fn test_set_and_get_hotkey_emission_tempo() { }); } +// Test getting nonviable stake +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test coinbase test_get_nonviable_stake -- --nocapture +#[test] +fn test_get_nonviable_stake() { + new_test_ext(1).execute_with(|| { + let netuid = 1u16; + let delegate_coldkey = U256::from(1); + let delegate_hotkey = U256::from(2); + let delegator = U256::from(3); + + let owner_added_stake = 123; + let owner_removed_stake = 456; + let owner_stake = 1_000 + owner_removed_stake; + // Add more than removed to test that the delta is updated correctly + let owner_adds_more_stake = owner_removed_stake + 1; + + let delegator_added_stake = 999; + + // Set stake rate limit very high + TargetStakesPerInterval::::put(1e9 as u64); + + add_network(netuid, 0, 0); + register_ok_neuron(netuid, delegate_hotkey, delegate_coldkey, 0); + // Give extra stake to the owner + SubtensorModule::increase_stake_on_coldkey_hotkey_account( + &delegate_coldkey, + &delegate_hotkey, + owner_stake, + ); + + // Register as a delegate + assert_ok!(SubtensorModule::become_delegate( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey + )); + + // Verify that the key starts with 0 nonviable stake + assert_eq!( + SubtensorModule::get_nonviable_stake(&delegate_hotkey, &delegate_coldkey), + 0 + ); + + // Give the coldkey some balance; extra just in-case + SubtensorModule::add_balance_to_coldkey_account( + &delegate_coldkey, + owner_added_stake + owner_adds_more_stake, + ); + + // Add some stake + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_added_stake + )); + + // Verify the nonviable stake is the same as the added stake + assert_eq!( + SubtensorModule::get_nonviable_stake(&delegate_hotkey, &delegate_coldkey), + owner_added_stake + ); + + // Add some stake from a delegator + SubtensorModule::add_balance_to_coldkey_account(&delegator, delegator_added_stake); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegator), + delegate_hotkey, + delegator_added_stake + )); + + // Verify that the nonviable stake doesn't change when a different account adds stake + assert_eq!( + SubtensorModule::get_nonviable_stake(&delegate_hotkey, &delegate_coldkey), + owner_added_stake + ); + + // Remove some stake + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_removed_stake + )); + + // The stake delta is negative, so the nonviable stake should be 0 + assert_eq!( + SubtensorModule::get_nonviable_stake(&delegate_hotkey, &delegate_coldkey), + 0 + ); + + // Add more stake than was removed + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_adds_more_stake + )); + + // Verify that the nonviable stake is the net of the operations + assert_eq!( + SubtensorModule::get_nonviable_stake(&delegate_hotkey, &delegate_coldkey), + owner_adds_more_stake - owner_removed_stake + owner_added_stake + ); + }); +} + // 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() { @@ -311,9 +415,9 @@ fn test_coinbase_nominator_drainage_overflow() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_no_affected_by_last_add_stake -- --nocapture +// 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_affected_by_last_add_stake() { +fn test_coinbase_nominator_drainage_no_deltas() { new_test_ext(1).execute_with(|| { // 1. Set up the network and accounts let netuid: u16 = 1; @@ -341,11 +445,11 @@ fn test_coinbase_nominator_drainage_no_affected_by_last_add_stake() { log::debug!("Balances added to accounts"); // 4. Make the hotkey a delegate - let vali_take = (u16::MAX as u64 / 10); + let val_take = (u16::MAX as u64 / 10); assert_ok!(SubtensorModule::do_become_delegate( RuntimeOrigin::signed(coldkey), hotkey, - vali_take as u16 + val_take as u16 )); log::debug!("Hotkey became a delegate with minimum take"); @@ -353,15 +457,151 @@ fn test_coinbase_nominator_drainage_no_affected_by_last_add_stake() { // 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; + 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); - // Make add_stake call for nominator1 - // This should not affect the emission distribution + 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); - // Will be greater than the bock emission - LastAddStakeIncrease::::insert(hotkey, nominator1, 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!( @@ -372,14 +612,17 @@ fn test_coinbase_nominator_drainage_no_affected_by_last_add_stake() { ); 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 - 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); + 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), - initial_stake * 2 + 200 + 123 ); assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); @@ -391,7 +634,7 @@ fn test_coinbase_nominator_drainage_no_affected_by_last_add_stake() { // 7. Simulate blocks and check emissions next_block(); - assert_eq!(SubtensorModule::get_pending_emission(netuid), to_emit); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 10); log::debug!( "After first block, pending emission: {}", SubtensorModule::get_pending_emission(netuid) @@ -417,12 +660,12 @@ fn test_coinbase_nominator_drainage_no_affected_by_last_add_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 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: {}", @@ -440,30 +683,765 @@ fn test_coinbase_nominator_drainage_no_affected_by_last_add_stake() { 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!(hotkey_stake, 2, "Hotkey stake mismatch"); assert_eq!( nominator1_stake, - initial_stake + nominator_emission, + 100 + 123 + nominator_emission, "Nominator1 stake mismatch" ); assert_eq!( nominator2_stake, - initial_stake + nominator_emission, + 100 + nominator_emission, "Nominator2 stake mismatch" ); // 10. Check total stake + // Includes the added stake from nominator1 assert_eq!( total_stake, - initial_stake + initial_stake + total_emission, + 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" ); diff --git a/pallets/subtensor/tests/migration.rs b/pallets/subtensor/tests/migration.rs index 6c40d7d78..0df00c449 100644 --- a/pallets/subtensor/tests/migration.rs +++ b/pallets/subtensor/tests/migration.rs @@ -1,10 +1,12 @@ #![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] mod mock; +use codec::{Decode, Encode}; use frame_support::{assert_ok, weights::Weight}; use frame_system::Config; use mock::*; use pallet_subtensor::*; -use sp_core::U256; +use sp_core::{crypto::Ss58Codec, U256}; +use substrate_fixed::types::extra::U2; #[test] fn test_initialise_ti() { @@ -430,3 +432,86 @@ fn run_migration_and_check(migration_name: &'static str) -> frame_support::weigh // Return the weight of the executed migration weight } + +fn run_pending_emissions_migration_and_check( + migration_name: &'static str, +) -> frame_support::weights::Weight { + // Execute the migration and store its weight + let weight: frame_support::weights::Weight = + pallet_subtensor::migrations::migrate_fix_pending_emission::migrate_fix_pending_emission::< + Test, + >(); + + // 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 rand_coldkeys = [U256::from(1), U256::from(2), U256::from(3), U256::from(4)]; + + 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); + + // 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); + 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, 123_456_789); + let expected_taostats_new_hk_pending_emission: u64 = 987_654 + 100_000 + 123_456_789; + + // 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 added to new the Datura hotkey + assert_eq!( + PendingdHotkeyEmission::::get(datura_new_hk_account), + expected_datura_new_hk_pending_emission + ); + }) +} diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index f053c7ca6..a55db996b 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2306,3 +2306,106 @@ fn test_get_total_delegated_stake_exclude_owner_stake() { ); }); } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test staking -- test_stake_delta_tracks_adds_and_removes --exact --nocapture +#[test] +fn test_stake_delta_tracks_adds_and_removes() { + new_test_ext(1).execute_with(|| { + let netuid = 1u16; + let delegate_coldkey = U256::from(1); + let delegate_hotkey = U256::from(2); + let delegator = U256::from(3); + + let owner_stake = 1000; + let owner_added_stake = 123; + let owner_removed_stake = 456; + // Add more than removed to test that the delta is updated correctly + let owner_adds_more_stake = owner_removed_stake + 1; + + let delegator_added_stake = 999; + + // Set stake rate limit very high + TargetStakesPerInterval::::put(1e9 as u64); + + add_network(netuid, 0, 0); + register_ok_neuron(netuid, delegate_hotkey, delegate_coldkey, 0); + // Give extra stake to the owner + SubtensorModule::increase_stake_on_coldkey_hotkey_account( + &delegate_coldkey, + &delegate_hotkey, + owner_stake, + ); + + // Register as a delegate + assert_ok!(SubtensorModule::become_delegate( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey + )); + + // Verify that the stake delta is empty + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + 0 + ); + + // Give the coldkey some balance; extra just in case + SubtensorModule::add_balance_to_coldkey_account( + &delegate_coldkey, + owner_added_stake + owner_adds_more_stake, + ); + + // Add some stake + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_added_stake + )); + + // Verify that the stake delta is correct + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake) + ); + + // Add some stake from a delegator + SubtensorModule::add_balance_to_coldkey_account(&delegator, delegator_added_stake); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegator), + delegate_hotkey, + delegator_added_stake + )); + + // Verify that the stake delta is unchanged for the owner + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake) + ); + + // Remove some stake + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_removed_stake + )); + + // Verify that the stake delta is correct + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake).saturating_sub_unsigned(owner_removed_stake.into()) + ); + + // Add more stake than was removed + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_adds_more_stake + )); + + // Verify that the stake delta is correct + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake) + .saturating_add_unsigned((owner_adds_more_stake - owner_removed_stake).into()) + ); + }); +} diff --git a/pallets/subtensor/tests/swap_coldkey.rs b/pallets/subtensor/tests/swap_coldkey.rs index 0fe601cab..37467646f 100644 --- a/pallets/subtensor/tests/swap_coldkey.rs +++ b/pallets/subtensor/tests/swap_coldkey.rs @@ -1628,3 +1628,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 bff738b86..57f206452 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] @@ -1115,3 +1116,91 @@ fn test_swap_complex_parent_child_structure() { ); }); } + +// 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/pallets/subtensor/tests/weights.rs b/pallets/subtensor/tests/weights.rs index 214e3add0..373c301be 100644 --- a/pallets/subtensor/tests/weights.rs +++ b/pallets/subtensor/tests/weights.rs @@ -477,11 +477,11 @@ fn test_set_weights_min_stake_failed() { // Check the signed extension function. assert_eq!(SubtensorModule::get_weights_min_stake(), 20_000_000_000_000); - assert!(!SubtensorModule::check_weights_min_stake(&hotkey)); + assert!(!SubtensorModule::check_weights_min_stake(&hotkey, netuid)); SubtensorModule::increase_stake_on_hotkey_account(&hotkey, 19_000_000_000_000); - assert!(!SubtensorModule::check_weights_min_stake(&hotkey)); + assert!(!SubtensorModule::check_weights_min_stake(&hotkey, netuid)); SubtensorModule::increase_stake_on_hotkey_account(&hotkey, 20_000_000_000_000); - assert!(SubtensorModule::check_weights_min_stake(&hotkey)); + assert!(SubtensorModule::check_weights_min_stake(&hotkey, netuid)); // Check that it fails at the pallet level. SubtensorModule::set_weights_min_stake(100_000_000_000_000); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index c5f3e6898..1aa30ac3c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -142,7 +142,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: 201, + spec_version: 202, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,