diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index ff17aa4281..1f19397589 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -91,9 +91,7 @@ mod errors { /// A transactor exceeded the rate limit for setting or swapping hotkey. HotKeySetTxRateLimitExceeded, /// A transactor exceeded the rate limit for staking. - StakeRateLimitExceeded, - /// A transactor exceeded the rate limit for unstaking. - UnstakeRateLimitExceeded, + StakingRateLimitExceeded, /// Registration is disabled. SubNetRegistrationDisabled, /// The number of registration attempts exceeded the allowed number in the interval. diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index 72d8374bcd..6438212e71 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -62,19 +62,6 @@ impl Pallet { Error::::HotKeyNotDelegateAndSignerNotOwnHotKey ); - // Ensure we don't exceed stake rate limit - let stakes_this_interval = - Self::get_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey); - ensure!( - stakes_this_interval < Self::get_target_stakes_per_interval(), - Error::::StakeRateLimitExceeded - ); - - // 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) { let total_stake_after_add = @@ -86,6 +73,8 @@ impl Pallet { ); } + Self::try_increase_staking_counter(&coldkey, &hotkey)?; + // Ensure the remove operation from the coldkey is a success. let actual_amount_to_stake = Self::remove_balance_from_coldkey_account(&coldkey, stake_to_be_added)?; @@ -93,17 +82,15 @@ impl Pallet { // If we reach here, add the balance to the hotkey. Self::increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, actual_amount_to_stake); + // 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); + }); + // Set last block for rate limiting - let block: u64 = Self::get_current_block_as_u64(); + let block = Self::get_current_block_as_u64(); Self::set_last_tx_block(&coldkey, block); - // Emit the staking event. - Self::set_stakes_this_interval_for_coldkey_hotkey( - &coldkey, - &hotkey, - stakes_this_interval.saturating_add(1), - block, - ); log::debug!( "StakeAdded( hotkey:{:?}, stake_to_be_added:{:?} )", hotkey, diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index 7248088954..4a5369ecfd 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -59,42 +59,6 @@ impl Pallet { Stake::::get(hotkey, coldkey) } - // Retrieves the total stakes for a given hotkey (account ID) for the current staking interval. - pub fn get_stakes_this_interval_for_coldkey_hotkey( - coldkey: &T::AccountId, - hotkey: &T::AccountId, - ) -> u64 { - // Retrieve the configured stake interval duration from storage. - let stake_interval = StakeInterval::::get(); - - // Obtain the current block number as an unsigned 64-bit integer. - let current_block = Self::get_current_block_as_u64(); - - // Fetch the total stakes and the last block number when stakes were made for the hotkey. - let (stakes, block_last_staked_at) = - TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey); - - // Calculate the block number after which the stakes for the hotkey should be reset. - let block_to_reset_after = block_last_staked_at.saturating_add(stake_interval); - - // If the current block number is beyond the reset point, - // it indicates the end of the staking interval for the hotkey. - if block_to_reset_after <= current_block { - // Reset the stakes for this hotkey for the current interval. - Self::set_stakes_this_interval_for_coldkey_hotkey( - coldkey, - hotkey, - 0, - block_last_staked_at, - ); - // Return 0 as the stake amount since we've just reset the stakes. - return 0; - } - - // If the staking interval has not yet ended, return the current stake amount. - stakes - } - pub fn get_target_stakes_per_interval() -> u64 { TargetStakesPerInterval::::get() } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 71d3c31087..f5cf87bc70 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -65,13 +65,7 @@ impl Pallet { Error::::NotEnoughStakeToWithdraw ); - // Ensure we don't exceed stake rate limit - let unstakes_this_interval = - Self::get_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey); - ensure!( - unstakes_this_interval < Self::get_target_stakes_per_interval(), - Error::::UnstakeRateLimitExceeded - ); + Self::try_increase_staking_counter(&coldkey, &hotkey)?; // We remove the balance from the hotkey. Self::decrease_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake_to_be_removed); @@ -98,16 +92,10 @@ impl Pallet { } // Set last block for rate limiting - let block: u64 = Self::get_current_block_as_u64(); + let block = Self::get_current_block_as_u64(); Self::set_last_tx_block(&coldkey, block); // Emit the unstaking event. - Self::set_stakes_this_interval_for_coldkey_hotkey( - &coldkey, - &hotkey, - unstakes_this_interval.saturating_add(1), - block, - ); log::debug!( "StakeRemoved( hotkey:{:?}, stake_to_be_removed:{:?} )", hotkey, diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 72079292f0..e6675e0cda 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -340,182 +340,175 @@ fn test_add_stake_total_issuance_no_change() { #[test] fn test_reset_stakes_per_interval() { - new_test_ext(0).execute_with(|| { + new_test_ext(1).execute_with(|| { let coldkey = U256::from(561330); let hotkey = U256::from(561337); - SubtensorModule::set_stake_interval(7); - SubtensorModule::set_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey, 5, 1); + SubtensorModule::set_stake_interval(3); + SubtensorModule::set_target_stakes_per_interval(3); + + assert_ok!(SubtensorModule::try_increase_staking_counter( + &coldkey, &hotkey + )); + + assert_eq!( + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (1, 1) + ); + + // block 2 step_block(1); assert_eq!( - SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey), - 5 + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (1, 1) ); - // block: 7 interval not yet passed - step_block(6); + assert_ok!(SubtensorModule::try_increase_staking_counter( + &coldkey, &hotkey + )); + assert_eq!( - SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey), - 5 + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (2, 1) ); - // block 8: interval passed + // block 3 step_block(1); + assert_eq!( - SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey), - 0 + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (2, 1) + ); + + // block 4 - interval passed + step_block(1); + + assert_eq!( + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (2, 1) + ); + + assert_ok!(SubtensorModule::try_increase_staking_counter( + &coldkey, &hotkey + )); + + assert_eq!( + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (1, 4) ); }); } #[test] -fn test_add_stake_under_limit() { +fn test_staking_rate_limit() { new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(561337); - let coldkey_account_id = U256::from(61337); - let netuid: u16 = 1; - let start_nonce: u64 = 0; - let tempo: u16 = 13; - let max_stakes = 2; + let hotkey = U256::from(561337); + let coldkey = U256::from(61337); + let netuid = 1; + let max_stakes = 3; + SubtensorModule::set_stake_interval(3); SubtensorModule::set_target_stakes_per_interval(max_stakes); - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, start_nonce); - SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 60000); + + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 60000); + + // block 1, stake 1 + assert_eq!( + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (0, 0) + ); + assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(coldkey_account_id), - hotkey_account_id, + <::RuntimeOrigin>::signed(coldkey), + hotkey, 1, )); + + assert_eq!( + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (1, 1) + ); + + // block 2 + step_block(1); + + // stake 2 and 3 assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(coldkey_account_id), - hotkey_account_id, + <::RuntimeOrigin>::signed(coldkey), + hotkey, + 1, + )); + // remove should increase the counter + assert_ok!(SubtensorModule::remove_stake( + <::RuntimeOrigin>::signed(coldkey), + hotkey, 1, )); - let current_stakes = SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey( - &coldkey_account_id, - &hotkey_account_id, + // counter should be increased, while the block should not be changed + assert_eq!( + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (3, 1) ); - assert!(current_stakes <= max_stakes); - }); -} -#[test] -fn test_add_stake_rate_limit_exceeded() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(561337); - let coldkey_account_id = U256::from(61337); - let netuid: u16 = 1; - let start_nonce: u64 = 0; - let tempo: u16 = 13; - let max_stakes = 2; - let block_number = 1; - - SubtensorModule::set_target_stakes_per_interval(max_stakes); - SubtensorModule::set_stakes_this_interval_for_coldkey_hotkey( - &coldkey_account_id, - &hotkey_account_id, - max_stakes, - block_number, - ); + // block 3 + step_block(1); - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, start_nonce); - SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 60000); - assert_err!( + // stake 4 + assert_noop!( SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(coldkey_account_id), - hotkey_account_id, + <::RuntimeOrigin>::signed(coldkey), + hotkey, 1, ), - Error::::StakeRateLimitExceeded + Error::::StakingRateLimitExceeded ); - - let current_stakes = SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey( - &coldkey_account_id, - &hotkey_account_id, + assert_noop!( + SubtensorModule::remove_stake( + <::RuntimeOrigin>::signed(coldkey), + hotkey, + 1, + ), + Error::::StakingRateLimitExceeded ); - assert_eq!(current_stakes, max_stakes); - }); -} -// /*********************************************************** -// staking::remove_stake() tests -// ************************************************************/ -#[test] -fn test_remove_stake_under_limit() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(561337); - let coldkey_account_id = U256::from(61337); - let netuid: u16 = 1; - let start_nonce: u64 = 0; - let tempo: u16 = 13; - let max_unstakes = 2; + // should not be changed + assert_eq!( + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (3, 1) + ); - SubtensorModule::set_target_stakes_per_interval(max_unstakes); - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, start_nonce); - SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 60000); - SubtensorModule::increase_stake_on_hotkey_account(&hotkey_account_id, 2); + // block 4 + step_block(1); - assert_ok!(SubtensorModule::remove_stake( - <::RuntimeOrigin>::signed(coldkey_account_id), - hotkey_account_id, - 1, - )); - assert_ok!(SubtensorModule::remove_stake( - <::RuntimeOrigin>::signed(coldkey_account_id), - hotkey_account_id, + // should pass now, because of the interval end + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey), + hotkey, 1, )); - let current_unstakes = SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey( - &coldkey_account_id, - &hotkey_account_id, + assert_eq!( + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (1, 4) ); - assert!(current_unstakes <= max_unstakes); - }); -} -#[test] -fn test_remove_stake_rate_limit_exceeded() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(561337); - let coldkey_account_id = U256::from(61337); - let netuid: u16 = 1; - let start_nonce: u64 = 0; - let tempo: u16 = 13; - let max_unstakes = 1; - let block_number = 1; - - SubtensorModule::set_target_stakes_per_interval(max_unstakes); - SubtensorModule::set_stakes_this_interval_for_coldkey_hotkey( - &coldkey_account_id, - &hotkey_account_id, - max_unstakes, - block_number, - ); + // block 5 + step_block(1); - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, start_nonce); - SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 60000); - SubtensorModule::increase_stake_on_hotkey_account(&hotkey_account_id, 2); - assert_err!( - SubtensorModule::remove_stake( - <::RuntimeOrigin>::signed(coldkey_account_id), - hotkey_account_id, - 2, - ), - Error::::UnstakeRateLimitExceeded - ); + assert_ok!(SubtensorModule::remove_stake( + <::RuntimeOrigin>::signed(coldkey), + hotkey, + 1, + )); - let current_unstakes = SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey( - &coldkey_account_id, - &hotkey_account_id, + assert_eq!( + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey), + (2, 4) ); - assert_eq!(current_unstakes, max_unstakes); }); } diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 472fccd58b..4c3fd2ba1e 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -154,18 +154,37 @@ impl Pallet { target_stakes_per_interval, )); } - pub fn set_stakes_this_interval_for_coldkey_hotkey( + + // Counts staking events within the [`StakeInterval`]. It increases the counter by 1 in case no + // limit exceeded, otherwise returns an error. + pub(crate) fn try_increase_staking_counter( coldkey: &T::AccountId, hotkey: &T::AccountId, - stakes_this_interval: u64, - last_staked_block_number: u64, - ) { - TotalHotkeyColdkeyStakesThisInterval::::insert( - coldkey, - hotkey, - (stakes_this_interval, last_staked_block_number), + ) -> DispatchResult { + let current_block = Self::get_current_block_as_u64(); + let stake_interval = StakeInterval::::get(); + let stakes_limit = TargetStakesPerInterval::::get(); + let (stakes_count, last_staked_at) = + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey); + + // Reset staking counter if it's been stake_interval blocks since the first staking action of the series. + if stakes_count == 0 || last_staked_at.saturating_add(stake_interval) <= current_block { + TotalHotkeyColdkeyStakesThisInterval::::insert(coldkey, hotkey, (1, current_block)); + return Ok(()); + } + + ensure!( + stakes_count < stakes_limit, + Error::::StakingRateLimitExceeded ); + + TotalHotkeyColdkeyStakesThisInterval::::mutate(coldkey, hotkey, |(count, _)| { + *count = count.saturating_add(1); + }); + + Ok(()) } + pub fn set_stake_interval(block: u64) { StakeInterval::::set(block); } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 6a3c8a69a8..574b76df65 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -220,7 +220,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: 216, + spec_version: 217, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,