diff --git a/pallets/subtensor/src/coinbase/block_step.rs b/pallets/subtensor/src/coinbase/block_step.rs index a7e658e89a..caac19b685 100644 --- a/pallets/subtensor/src/coinbase/block_step.rs +++ b/pallets/subtensor/src/coinbase/block_step.rs @@ -18,7 +18,10 @@ impl Pallet { Self::run_coinbase(block_emission); // --- 4. Set pending children on the epoch; but only after the coinbase has been run. Self::try_set_pending_children(block_number); - // Return ok. + + // Clear stake delta for netuids with running epoch this block + Self::clear_stake_delta(block_number); + Ok(()) } diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 00b0c2fa55..ea65428426 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -508,10 +508,20 @@ impl Pallet { for hotkey in hotkeys { // Get hotkey ALPHA on subnet. let alpha_stake: u64 = Self::get_stake_for_hotkey_on_subnet(hotkey, netuid); + // Get hotkey TAO on root. let root_stake: u64 = Self::get_stake_for_hotkey_on_subnet(hotkey, Self::get_root_netuid()); - stake_map.insert(hotkey.clone(), (alpha_stake, root_stake)); + + let alpha_stake_delta = Self::get_stake_delta_for_hotkey_on_subnet(hotkey, netuid); + let root_stake_delta = + Self::get_stake_delta_for_hotkey_on_subnet(hotkey, Self::get_root_netuid()); + + let final_alpha_stake = alpha_stake.saturating_sub(alpha_stake_delta); + + let final_root_stake = root_stake.saturating_sub(root_stake_delta); + + stake_map.insert(hotkey.clone(), (final_alpha_stake, final_root_stake)); } stake_map } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 197cd5f8f7..38e1100887 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -909,6 +909,18 @@ pub mod pallet { 50400 } + #[pallet::type_value] + /// Default stake delta. + pub fn DefaultStakeDeltaMap() -> i128 { + 0 + } + + #[pallet::type_value] + /// Default value for enable stake delta calculation. + pub fn DefaultEnableStakeDeltaCalculation() -> bool { + true + } + #[pallet::storage] pub type MinActivityCutoff = StorageValue<_, u16, ValueQuery, DefaultMinActivityCutoff>; @@ -1176,6 +1188,24 @@ pub mod pallet { pub type TokenSymbol = StorageMap<_, Identity, u16, Vec, ValueQuery, DefaultUnicodeVecU8>; + #[pallet::storage] + /// Map ( netuid, hot ) --> StakeDeltaMap| Stake added/removed since last emission drain for coldkey. + pub type StakeDeltaSinceLastEmissionDrain = StorageDoubleMap< + _, + Blake2_128Concat, + u16, + Identity, + T::AccountId, + i128, + ValueQuery, + DefaultStakeDeltaMap, + >; + + #[pallet::storage] + /// --- ITEM( enables effect of the saved stake delta ) + pub type EnableStakeDeltaCalculation = + StorageValue<_, bool, ValueQuery, DefaultEnableStakeDeltaCalculation>; + /// ============================ /// ==== Global Parameters ===== /// ============================ diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index b7010185f7..2bc42e432f 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -2,7 +2,7 @@ use super::*; //use frame_system::pallet_prelude::BlockNumberFor; use safe_math::*; use share_pool::{SharePool, SharePoolDataOperations}; -//use sp_runtime::Saturating; +use sp_runtime::SaturatedConversion; use sp_std::ops::Neg; use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32, U110F18}; @@ -159,7 +159,7 @@ impl Pallet { // Step 2: Get the global tao stake for the hotkey let tao_stake = I64F64::saturating_from_num(Self::get_tao_inherited_for_hotkey_on_subnet( - hotkey, netuid, + hotkey, netuid, false, )); log::debug!("tao_stake: {:?}", tao_stake); @@ -185,9 +185,13 @@ impl Pallet { .map(|uid| { if Keys::::contains_key(netuid, uid) { let hotkey: T::AccountId = Keys::::get(netuid, uid); - I64F64::saturating_from_num(Self::get_inherited_for_hotkey_on_subnet( - &hotkey, netuid, - )) + let initial_stake = Self::get_inherited_for_hotkey_on_subnet(&hotkey, netuid); + + let stake_delta = Self::get_stake_delta_for_hotkey_on_subnet(&hotkey, netuid); + + let final_stake = initial_stake.saturating_sub(stake_delta); + + I64F64::saturating_from_num(final_stake) } else { I64F64::saturating_from_num(0) } @@ -202,7 +206,7 @@ impl Pallet { if Keys::::contains_key(netuid, uid) { let hotkey: T::AccountId = Keys::::get(netuid, uid); I64F64::saturating_from_num(Self::get_tao_inherited_for_hotkey_on_subnet( - &hotkey, netuid, + &hotkey, netuid, true, )) } else { I64F64::saturating_from_num(0) @@ -243,6 +247,7 @@ impl Pallet { /// # Arguments /// * `hotkey` - AccountId of the hotkey whose total inherited stake is to be calculated. /// * `netuid` - Network unique identifier specifying the subnet context. + /// * `use_stake_delta` - Whether to use stake delta for root stake for hotkey. /// /// # Returns /// * `u64`: The total inherited alpha for the hotkey on the subnet after considering the @@ -250,10 +255,24 @@ impl Pallet { /// /// # Note /// This function uses saturating arithmetic to prevent overflows. - pub fn get_tao_inherited_for_hotkey_on_subnet(hotkey: &T::AccountId, netuid: u16) -> u64 { - let initial_tao: U96F32 = U96F32::saturating_from_num( - Self::get_stake_for_hotkey_on_subnet(hotkey, Self::get_root_netuid()), - ); + pub fn get_tao_inherited_for_hotkey_on_subnet( + hotkey: &T::AccountId, + netuid: u16, + use_stake_delta: bool, + ) -> u64 { + let initial_tao: U96F32 = U96F32::saturating_from_num({ + let initial_tao = Self::get_stake_for_hotkey_on_subnet(hotkey, Self::get_root_netuid()); + + if use_stake_delta { + let stake_delta = + Self::get_stake_delta_for_hotkey_on_subnet(hotkey, Self::get_root_netuid()); + + // modified tao + initial_tao.saturating_sub(stake_delta) + } else { + initial_tao + } + }); // Initialize variables to track alpha allocated to children and inherited from parents. let mut tao_to_children: U96F32 = U96F32::saturating_from_num(0); @@ -298,9 +317,23 @@ impl Pallet { // Step 4: Calculate the total tao inherited from parents. for (proportion, parent) in parents { // Retrieve the parent's total stake on this subnet. - let parent_tao: U96F32 = U96F32::saturating_from_num( - Self::get_stake_for_hotkey_on_subnet(&parent, Self::get_root_netuid()), - ); + let initial_tao = + Self::get_stake_for_hotkey_on_subnet(&parent, Self::get_root_netuid()); + + let parent_tao = U96F32::saturating_from_num({ + if use_stake_delta { + let stake_delta = Self::get_stake_delta_for_hotkey_on_subnet( + &parent, + Self::get_root_netuid(), + ); + + // modified tao + initial_tao.saturating_sub(stake_delta) + } else { + initial_tao + } + }); + log::trace!( "Parent tao for parent {:?} on subnet {}: {:?}", parent, @@ -344,6 +377,31 @@ impl Pallet { finalized_tao.saturating_to_num::() } + pub fn get_stake_delta_for_hotkey_on_subnet(hotkey: &T::AccountId, netuid: u16) -> u64 { + let enable_stake_delta = EnableStakeDeltaCalculation::::get(); + + if !enable_stake_delta { + return 0; + } + + let total_stake = StakeDeltaSinceLastEmissionDrain::::get(netuid, hotkey); + + // We only care about positive stake delta + total_stake.max(0).saturated_into() + } + + // Remove all stake delta after the epoch calculation + pub fn clear_stake_delta(block_number: u64) { + let subnets: Vec = Self::get_all_subnet_netuids(); + + for netuid in subnets.into_iter() { + if Self::should_run_epoch(netuid, block_number) { + // We don't use the operation result + let _ = StakeDeltaSinceLastEmissionDrain::::clear_prefix(netuid, u32::MAX, None); + } + } + } + pub fn get_inherited_for_hotkey_on_subnet(hotkey: &T::AccountId, netuid: u16) -> u64 { // Step 1: Retrieve the initial total stake (alpha) for the hotkey on the specified subnet. let initial_alpha: U96F32 = @@ -787,6 +845,11 @@ impl Pallet { let actual_alpha_decrease = Self::decrease_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, alpha); + // Track this substraction in the stake delta. + StakeDeltaSinceLastEmissionDrain::::mutate(netuid, hotkey, |stake_delta| { + *stake_delta = stake_delta.saturating_sub(actual_alpha_decrease.into()); + }); + // Step 2: Swap the alpha for TAO. let tao: u64 = Self::swap_alpha_for_tao(netuid, actual_alpha_decrease); @@ -856,6 +919,11 @@ impl Pallet { actual_alpha = Self::increase_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, netuid, alpha, ); + + // Track this addition in the stake delta. + StakeDeltaSinceLastEmissionDrain::::mutate(netuid, hotkey, |stake_delta| { + *stake_delta = stake_delta.saturating_add(actual_alpha.into()); + }); } // Step 4. Increase Tao reserves by the fee amount. diff --git a/pallets/subtensor/src/tests/children.rs b/pallets/subtensor/src/tests/children.rs index 1a9016f13f..ff3dec2e8c 100644 --- a/pallets/subtensor/src/tests/children.rs +++ b/pallets/subtensor/src/tests/children.rs @@ -2812,6 +2812,7 @@ fn test_childkey_take_drain() { let proportion: u64 = u64::MAX / 2; // Add network, register hotkeys, and setup network parameters + EnableStakeDeltaCalculation::::put(false); add_network(netuid, subnet_tempo, 0); register_ok_neuron(netuid, child_hotkey, child_coldkey, 0); register_ok_neuron(netuid, parent_hotkey, parent_coldkey, 1); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 1345f36b7d..9089332b0f 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -947,11 +947,11 @@ fn test_get_root_children() { // Assert Alice and Bob TAO inherited stakes assert_eq!( - SubtensorModule::get_tao_inherited_for_hotkey_on_subnet(&alice, alpha), + SubtensorModule::get_tao_inherited_for_hotkey_on_subnet(&alice, alpha, false), 0 ); assert_eq!( - SubtensorModule::get_tao_inherited_for_hotkey_on_subnet(&bob, alpha), + SubtensorModule::get_tao_inherited_for_hotkey_on_subnet(&bob, alpha, false), bob_root_stake + alice_root_stake ); diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 9a672676ee..0112cf0bde 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -1925,6 +1925,13 @@ fn test_staking_sets_div_variables() { // Wait for 1 epoch step_block(tempo + 1); + // Stake delta will mitigate stake movement + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, hotkey_account_id), + 0 + ); + // Wait for 2 epoch + step_block(tempo + 1); // Verify that divident variables have been set let stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( diff --git a/pallets/subtensor/src/tests/staking2.rs b/pallets/subtensor/src/tests/staking2.rs index 6fbabf83b2..71134f96fe 100644 --- a/pallets/subtensor/src/tests/staking2.rs +++ b/pallets/subtensor/src/tests/staking2.rs @@ -6,7 +6,7 @@ use frame_support::{ weights::Weight, }; use sp_core::U256; -use substrate_fixed::types::{I96F32, U96F32}; +use substrate_fixed::types::{I64F64, I96F32, U96F32}; // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --workspace --test staking2 -- test_swap_tao_for_alpha_dynamic_mechanism --exact --nocapture #[test] @@ -946,3 +946,262 @@ fn test_stake_fee_calculation() { assert_ne!(stake_fee_8, default_fee); }); } + +#[test] +fn test_stake_delta() { + fn emulate_emissions(enable_stake_delta: bool) -> u64 { + let mut out_emission = 0u64; + + new_test_ext(1).execute_with(|| { + let parent_hotkey_take = 0u16; + let parent_coldkey = U256::from(1); + let parent_hotkey = U256::from(3); + let child_coldkey = U256::from(2); + let child_hotkey = U256::from(4); + let miner_coldkey = U256::from(5); + let miner_hotkey = U256::from(6); + let nominator = U256::from(7); + let netuid: u16 = 1; + let subnet_tempo = 10; + let stake = 100_000_000_000; + let proportion: u64 = u64::MAX / 2; + + EnableStakeDeltaCalculation::::put(enable_stake_delta); + add_network(netuid, subnet_tempo, 0); + register_ok_neuron(netuid, child_hotkey, child_coldkey, 0); + register_ok_neuron(netuid, parent_hotkey, parent_coldkey, 1); + register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 1); + SubtensorModule::add_balance_to_coldkey_account( + &parent_coldkey, + stake + ExistentialDeposit::get(), + ); + SubtensorModule::add_balance_to_coldkey_account( + &nominator, + stake + ExistentialDeposit::get(), + ); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_max_allowed_validators(netuid, 2); + step_block(subnet_tempo); + SubnetOwnerCut::::set(0); + + // Set children + mock_set_children_no_epochs(netuid, &parent_hotkey, &[(proportion, child_hotkey)]); + + // Set 20% childkey take + let max_take: u16 = 0xFFFF / 5; + SubtensorModule::set_max_childkey_take(max_take); + assert_ok!(SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(child_coldkey), + child_hotkey, + netuid, + max_take + )); + + // Set hotkey take for parent + SubtensorModule::set_max_delegate_take(parent_hotkey_take); + Delegates::::insert(parent_hotkey, parent_hotkey_take); + + // Set 0% for childkey-as-a-delegate take + Delegates::::insert(child_hotkey, 0); + + // Setup stakes: + // Stake from parent + // Stake from nominator to childkey + // Parent gives 50% of stake to childkey + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(parent_coldkey), + parent_hotkey, + netuid, + stake + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator), + child_hotkey, + netuid, + stake + )); + + // Setup YUMA so that it creates emissions + Weights::::insert(netuid, 0, vec![(2, 0xFFFF)]); + Weights::::insert(netuid, 1, vec![(2, 0xFFFF)]); + BlockAtRegistration::::set(netuid, 0, 1); + BlockAtRegistration::::set(netuid, 1, 1); + BlockAtRegistration::::set(netuid, 2, 1); + LastUpdate::::set(netuid, vec![2, 2, 2]); + Kappa::::set(netuid, u16::MAX / 5); + ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active + ValidatorPermit::::insert(netuid, vec![true, true, false]); + + // Run run_coinbase to hit subnet epoch + let parent_stake_before = SubtensorModule::get_total_stake_for_coldkey(&parent_coldkey); + + step_block(subnet_tempo); + + let parent_emission = + SubtensorModule::get_total_stake_for_coldkey(&parent_coldkey) - parent_stake_before; + + out_emission = parent_emission; + }); + + out_emission + } + + let no_stake_delta_parent_emission = emulate_emissions(false); + let with_stake_delta_parent_emission = emulate_emissions(true); + + assert!(no_stake_delta_parent_emission > with_stake_delta_parent_emission); +} + +#[test] +fn test_stake_map_with_stake_delta() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid: u16 = 1; + let stake = 100_000_000_000; + let root_stake = 50_000_000_000; + let tempo = 10; + + add_network(netuid, 10, 0); + add_network(SubtensorModule::get_root_netuid(), tempo, 0); + register_ok_neuron(netuid, hotkey, coldkey, 1); + + SubtensorModule::add_balance_to_coldkey_account( + &coldkey, + 2 * stake + ExistentialDeposit::get(), + ); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + stake + )); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + SubtensorModule::get_root_netuid(), + root_stake + )); + + // Stake delta disabled + EnableStakeDeltaCalculation::::put(false); + let alpha_stake: u64 = SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey, netuid); + let root_stake: u64 = SubtensorModule::get_stake_for_hotkey_on_subnet( + &hotkey, + SubtensorModule::get_root_netuid(), + ); + let stake_map = SubtensorModule::get_stake_map(netuid, vec![&hotkey]) + .into_iter() + .collect::>(); + + assert_eq!(stake_map, vec![(hotkey, (alpha_stake, root_stake))]); + + // Stake delta enabled + EnableStakeDeltaCalculation::::put(true); + let stake_map = SubtensorModule::get_stake_map(netuid, vec![&hotkey]) + .into_iter() + .collect::>(); + + assert_eq!(stake_map, vec![(hotkey, (0, 0))]); + + // Stake delta enabled and epoch passed + EnableStakeDeltaCalculation::::put(true); + step_block(tempo); + + let alpha_stake: u64 = SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey, netuid); + let root_stake: u64 = SubtensorModule::get_stake_for_hotkey_on_subnet( + &hotkey, + SubtensorModule::get_root_netuid(), + ); + let stake_map = SubtensorModule::get_stake_map(netuid, vec![&hotkey]) + .into_iter() + .collect::>(); + + assert_eq!(stake_map, vec![(hotkey, (alpha_stake, root_stake))]); + }); +} + +#[test] +fn test_stake_weights_with_stake_delta() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid: u16 = 1; + let stake = 100_000_000_000; + let root_stake = 50_000_000_000; + let tempo = 10; + + add_network(netuid, 10, 0); + add_network(SubtensorModule::get_root_netuid(), tempo, 0); + register_ok_neuron(netuid, hotkey, coldkey, 1); + + SubtensorModule::add_balance_to_coldkey_account( + &coldkey, + 2 * stake + ExistentialDeposit::get(), + ); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + stake + )); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + SubtensorModule::get_root_netuid(), + root_stake + )); + + // Stake delta disabled + EnableStakeDeltaCalculation::::put(false); + let alpha_stake: u64 = SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey, netuid); + let root_stake: u64 = SubtensorModule::get_stake_for_hotkey_on_subnet( + &hotkey, + SubtensorModule::get_root_netuid(), + ); + let stake_weights = SubtensorModule::get_stake_weights_for_network(netuid); + + assert_eq!( + (stake_weights.1, stake_weights.2), + ( + vec![I64F64::saturating_from_num(alpha_stake)], + vec![I64F64::saturating_from_num(root_stake)] + ) + ); + + // Stake delta enabled + EnableStakeDeltaCalculation::::put(true); + let stake_weights = SubtensorModule::get_stake_weights_for_network(netuid); + + assert_eq!( + (stake_weights.1, stake_weights.2), + ( + vec![I64F64::saturating_from_num(0)], + vec![I64F64::saturating_from_num(0)] + ) + ); + + // Stake delta enabled and epoch passed + EnableStakeDeltaCalculation::::put(true); + step_block(tempo); + + let alpha_stake: u64 = SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey, netuid); + let root_stake: u64 = SubtensorModule::get_stake_for_hotkey_on_subnet( + &hotkey, + SubtensorModule::get_root_netuid(), + ); + let stake_weights = SubtensorModule::get_stake_weights_for_network(netuid); + + assert_eq!( + (stake_weights.1, stake_weights.2), + ( + vec![I64F64::saturating_from_num(alpha_stake)], + vec![I64F64::saturating_from_num(root_stake)] + ) + ); + }); +}