diff --git a/pallets/admin-utils/tests/mock.rs b/pallets/admin-utils/tests/mock.rs index f0e613fe89..3a67264b11 100644 --- a/pallets/admin-utils/tests/mock.rs +++ b/pallets/admin-utils/tests/mock.rs @@ -110,6 +110,8 @@ parameter_types! { pub const InitialNetworkLockReductionInterval: u64 = 2; // 2 blocks. pub const InitialSubnetLimit: u16 = 10; // Max 10 subnets. pub const InitialNetworkRateLimit: u64 = 0; + pub const InitialTargetStakesPerInterval: u16 = 1; + } impl pallet_subtensor::Config for Test { @@ -158,6 +160,7 @@ impl pallet_subtensor::Config for Test { type InitialNetworkLockReductionInterval = InitialNetworkLockReductionInterval; type InitialSubnetLimit = InitialSubnetLimit; type InitialNetworkRateLimit = InitialNetworkRateLimit; + type InitialTargetStakesPerInterval = InitialTargetStakesPerInterval; } impl system::Config for Test { diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index ced6cb0b76..1ded32dd2c 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -182,6 +182,8 @@ pub mod pallet { type InitialSubnetLimit: Get; #[pallet::constant] // Initial network creation rate limit type InitialNetworkRateLimit: Get; + #[pallet::constant] // Initial target stakes per interval issuance. + type InitialTargetStakesPerInterval: Get; } pub type AccountIdOf = ::AccountId; @@ -212,6 +214,10 @@ pub mod pallet { 0 } #[pallet::type_value] + pub fn DefaultStakesPerInterval() -> (u64, u64) { + (0, 0) + } + #[pallet::type_value] pub fn DefaultBlockEmission() -> u64 { 1_000_000_000 } @@ -227,6 +233,14 @@ pub mod pallet { pub fn DefaultAccount() -> T::AccountId { T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap() } + #[pallet::type_value] + pub fn DefaultTargetStakesPerInterval() -> u64 { + T::InitialTargetStakesPerInterval::get() + } + #[pallet::type_value] + pub fn DefaultStakeInterval() -> u64 { + 360 + } #[pallet::storage] // --- ITEM ( total_stake ) pub type TotalStake = StorageValue<_, u64, ValueQuery>; @@ -236,12 +250,22 @@ pub mod pallet { pub type BlockEmission = StorageValue<_, u64, ValueQuery, DefaultBlockEmission>; #[pallet::storage] // --- ITEM ( total_issuance ) pub type TotalIssuance = StorageValue<_, u64, ValueQuery, DefaultTotalIssuance>; + #[pallet::storage] // --- ITEM (target_stakes_per_interval) + pub type TargetStakesPerInterval = + StorageValue<_, u64, ValueQuery, DefaultTargetStakesPerInterval>; + #[pallet::storage] // --- ITEM (default_stake_interval) + pub type StakeInterval = StorageValue<_, u64, ValueQuery, DefaultStakeInterval>; #[pallet::storage] // --- MAP ( hot ) --> stake | Returns the total amount of stake under a hotkey. pub type TotalHotkeyStake = StorageMap<_, Identity, T::AccountId, u64, ValueQuery, DefaultAccountTake>; #[pallet::storage] // --- MAP ( cold ) --> stake | Returns the total amount of stake under a coldkey. pub type TotalColdkeyStake = StorageMap<_, Identity, T::AccountId, u64, ValueQuery, DefaultAccountTake>; + #[pallet::storage] + // --- MAP (hot) --> stake | Returns a tuple (u64: stakes, u64: block_number) + pub type TotalHotkeyStakesThisInterval = + StorageMap<_, Identity, T::AccountId, (u64, u64), ValueQuery, DefaultStakesPerInterval>; + #[pallet::storage] // --- MAP ( hot ) --> cold | Returns the controlling coldkey for a hotkey. pub type Owner = StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId, ValueQuery, DefaultAccount>; @@ -924,7 +948,9 @@ pub mod pallet { MaxAllowedUidsExceeded, // --- Thrown when number of accounts going to be registered exceeds MaxAllowedUids for the network. TooManyUids, // ---- Thrown when the caller attempts to set weights with more uids than allowed. TxRateLimitExceeded, // --- Thrown when a transactor exceeds the rate limit for transactions. - RegistrationDisabled, // --- Thrown when registration is disabled + StakeRateLimitExceeded, // --- Thrown when a transactor exceeds the rate limit for stakes. + UnstakeRateLimitExceeded, // --- Thrown when a transactor exceeds the rate limit for unstakes. + RegistrationDisabled, // --- Thrown when registration is disabled TooManyRegistrationsThisInterval, // --- Thrown when registration attempt exceeds allowed in interval BenchmarkingOnly, // --- Thrown when a function is only available for benchmarking HotkeyOriginMismatch, // --- Thrown when the hotkey passed is not the origin, but it should be @@ -1822,14 +1848,32 @@ where return Err(InvalidTransaction::Call.into()); } } - Some(Call::add_stake { .. }) => Ok(ValidTransaction { - priority: Self::get_priority_vanilla(), - ..Default::default() - }), - Some(Call::remove_stake { .. }) => Ok(ValidTransaction { - priority: Self::get_priority_vanilla(), - ..Default::default() - }), + Some(Call::add_stake { hotkey, .. }) => { + let stakes_this_interval = Pallet::::get_stakes_this_interval_for_hotkey(hotkey); + let max_stakes_per_interval = Pallet::::get_target_stakes_per_interval(); + + if stakes_this_interval >= max_stakes_per_interval { + return InvalidTransaction::ExhaustsResources.into(); + } + + Ok(ValidTransaction { + priority: Self::get_priority_vanilla(), + ..Default::default() + }) + } + Some(Call::remove_stake { hotkey, .. }) => { + let stakes_this_interval = Pallet::::get_stakes_this_interval_for_hotkey(hotkey); + let max_stakes_per_interval = Pallet::::get_target_stakes_per_interval(); + + if stakes_this_interval >= max_stakes_per_interval { + return InvalidTransaction::ExhaustsResources.into(); + } + + Ok(ValidTransaction { + priority: Self::get_priority_vanilla(), + ..Default::default() + }) + } Some(Call::register { netuid, .. } | Call::burned_register { netuid, .. }) => { let registrations_this_interval = Pallet::::get_registrations_this_interval(*netuid); diff --git a/pallets/subtensor/src/staking.rs b/pallets/subtensor/src/staking.rs index e77dfc0fb8..f7bd4cb067 100644 --- a/pallets/subtensor/src/staking.rs +++ b/pallets/subtensor/src/staking.rs @@ -164,19 +164,31 @@ impl Pallet { Error::::TxRateLimitExceeded ); - // --- 7. Ensure the remove operation from the coldkey is a success. + // --- 7. Ensure we don't exceed stake rate limit + let stakes_this_interval = Self::get_stakes_this_interval_for_hotkey(&hotkey); + ensure!( + stakes_this_interval < Self::get_target_stakes_per_interval(), + Error::::StakeRateLimitExceeded + ); + + // --- 8. Ensure the remove operation from the coldkey is a success. ensure!( Self::remove_balance_from_coldkey_account(&coldkey, stake_as_balance.unwrap()) == true, Error::::BalanceWithdrawalError ); - // --- 8. If we reach here, add the balance to the hotkey. + // --- 9. If we reach here, add the balance to the hotkey. Self::increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake_to_be_added); // Set last block for rate limiting Self::set_last_tx_block(&coldkey, block); - // --- 9. Emit the staking event. + // --- 10. Emit the staking event. + Self::set_stakes_this_interval_for_hotkey( + &hotkey, + stakes_this_interval + 1, + block, + ); log::info!( "StakeAdded( hotkey:{:?}, stake_to_be_added:{:?} )", hotkey, @@ -184,7 +196,7 @@ impl Pallet { ); Self::deposit_event(Event::StakeAdded(hotkey, stake_to_be_added)); - // --- 10. Ok and return. + // --- 11. Ok and return. Ok(()) } @@ -273,16 +285,28 @@ impl Pallet { Error::::TxRateLimitExceeded ); - // --- 7. We remove the balance from the hotkey. + // --- 7. Ensure we don't exceed stake rate limit + let unstakes_this_interval = Self::get_stakes_this_interval_for_hotkey(&hotkey); + ensure!( + unstakes_this_interval < Self::get_target_stakes_per_interval(), + Error::::UnstakeRateLimitExceeded + ); + + // --- 8. We remove the balance from the hotkey. Self::decrease_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake_to_be_removed); - // --- 8. We add the balancer to the coldkey. If the above fails we will not credit this coldkey. + // --- 9. We add the balancer to the coldkey. If the above fails we will not credit this coldkey. Self::add_balance_to_coldkey_account(&coldkey, stake_to_be_added_as_currency.unwrap()); // Set last block for rate limiting Self::set_last_tx_block(&coldkey, block); - // --- 9. Emit the unstaking event. + // --- 10. Emit the unstaking event. + Self::set_stakes_this_interval_for_hotkey( + &hotkey, + unstakes_this_interval + 1, + block, + ); log::info!( "StakeRemoved( hotkey:{:?}, stake_to_be_removed:{:?} )", hotkey, @@ -290,7 +314,7 @@ impl Pallet { ); Self::deposit_event(Event::StakeRemoved(hotkey, stake_to_be_removed)); - // --- 10. Done and ok. + // --- 11. Done and ok. Ok(()) } @@ -342,6 +366,37 @@ impl Pallet { return 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_hotkey(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) = TotalHotkeyStakesThisInterval::::get(hotkey); + + // Calculate the block number after which the stakes for the hotkey should be reset. + let block_to_reset_after = block_last_staked_at + 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_hotkey(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 { + return TargetStakesPerInterval::::get(); + } + // Creates a cold - hot pairing account if the hotkey is not already an active account. // pub fn create_account_if_non_existent(coldkey: &T::AccountId, hotkey: &T::AccountId) { diff --git a/pallets/subtensor/src/utils.rs b/pallets/subtensor/src/utils.rs index c0fcb9cd4d..666a8df339 100644 --- a/pallets/subtensor/src/utils.rs +++ b/pallets/subtensor/src/utils.rs @@ -137,7 +137,15 @@ impl Pallet { WeightsMinStake::::put(min_stake); Self::deposit_event(Event::WeightsMinStake(min_stake)); } - + pub fn set_target_stakes_per_interval(target_stakes_per_interval: u64) { + TargetStakesPerInterval::::set(target_stakes_per_interval) + } + pub fn set_stakes_this_interval_for_hotkey(hotkey: &T::AccountId, stakes_this_interval: u64, last_staked_block_number: u64) { + TotalHotkeyStakesThisInterval::::insert(hotkey, (stakes_this_interval, last_staked_block_number)); + } + pub fn set_stake_interval(block: u64) { + StakeInterval::::set(block); + } pub fn get_rank_for_uid(netuid: u16, uid: u16) -> u16 { let vec = Rank::::get(netuid); if (uid as usize) < vec.len() { diff --git a/pallets/subtensor/tests/mock.rs b/pallets/subtensor/tests/mock.rs index db50f03d09..f2fd10d3e9 100644 --- a/pallets/subtensor/tests/mock.rs +++ b/pallets/subtensor/tests/mock.rs @@ -160,6 +160,7 @@ parameter_types! { pub const InitialNetworkLockReductionInterval: u64 = 2; // 2 blocks. pub const InitialSubnetLimit: u16 = 10; // Max 10 subnets. pub const InitialNetworkRateLimit: u64 = 0; + pub const InitialTargetStakesPerInterval: u16 = 1; } // Configure collective pallet for council @@ -357,6 +358,7 @@ impl pallet_subtensor::Config for Test { type InitialNetworkLockReductionInterval = InitialNetworkLockReductionInterval; type InitialSubnetLimit = InitialSubnetLimit; type InitialNetworkRateLimit = InitialNetworkRateLimit; + type InitialTargetStakesPerInterval = InitialTargetStakesPerInterval; } impl pallet_utility::Config for Test { diff --git a/pallets/subtensor/tests/senate.rs b/pallets/subtensor/tests/senate.rs index 4177476fd7..ca252c39c7 100644 --- a/pallets/subtensor/tests/senate.rs +++ b/pallets/subtensor/tests/senate.rs @@ -505,6 +505,8 @@ fn test_senate_not_leave_when_stake_removed() { let burn_cost = 1000; let coldkey_account_id = U256::from(667); // Neighbour of the beast, har har + SubtensorModule::set_target_stakes_per_interval(2); + //add network SubtensorModule::set_burn(netuid, burn_cost); add_network(netuid, tempo, 0); diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index 5d907c20bf..7f905bebad 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -1,11 +1,12 @@ -use frame_support::{assert_noop, assert_ok, traits::Currency}; +use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; use frame_system::Config; mod mock; use frame_support::dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, Pays}; -use frame_support::sp_runtime::DispatchError; +use frame_support::sp_runtime::{transaction_validity::InvalidTransaction, DispatchError}; use mock::*; -use pallet_subtensor::Error; +use pallet_subtensor::{Error, SubtensorSignedExtension}; use sp_core::{H256, U256}; +use sp_runtime::traits::{DispatchInfoOf, SignedExtension}; /*********************************************************** staking::add_stake() tests @@ -333,9 +334,226 @@ fn test_add_stake_total_issuance_no_change() { }); } +#[test] +fn test_reset_stakes_per_interval() { + new_test_ext().execute_with(|| { + let hotkey = U256::from(561337); + + SubtensorModule::set_stake_interval(7); + SubtensorModule::set_stakes_this_interval_for_hotkey(&hotkey, 5, 1); + step_block(1); + + assert_eq!( + SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey), + 5 + ); + + // block: 7 interval not yet passed + step_block(6); + assert_eq!( + SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey), + 5 + ); + + // block 8: interval passed + step_block(1); + assert_eq!( + SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey), + 0 + ); + }); +} + +#[test] +fn test_add_stake_under_limit() { + new_test_ext().execute_with(|| { + let hotkey_account_id = U256::from(561337); + let coldkey_account_id = U256::from(61337); + let who: ::AccountId = hotkey_account_id.into(); + let netuid: u16 = 1; + let start_nonce: u64 = 0; + let tempo: u16 = 13; + let max_stakes = 2; + + SubtensorModule::set_target_stakes_per_interval(max_stakes); + + let call: pallet_subtensor::Call = pallet_subtensor::Call::add_stake { + hotkey: hotkey_account_id, + amount_staked: 1, + }; + let info: DispatchInfo = + DispatchInfoOf::<::RuntimeCall>::default(); + let extension = SubtensorSignedExtension::::new(); + let result = extension.validate(&who, &call.into(), &info, 10); + + assert_ok!(result); + + 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_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey_account_id), + hotkey_account_id, + 1, + )); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey_account_id), + hotkey_account_id, + 1, + )); + + let current_stakes = + SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey_account_id); + assert!(current_stakes <= max_stakes); + }); +} + +#[test] +fn test_add_stake_rate_limit_exceeded() { + new_test_ext().execute_with(|| { + let hotkey_account_id = U256::from(561337); + let coldkey_account_id = U256::from(61337); + let who: ::AccountId = hotkey_account_id.into(); + 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_hotkey( + &hotkey_account_id, + max_stakes, + block_number, + ); + + let call: pallet_subtensor::Call = pallet_subtensor::Call::add_stake { + hotkey: hotkey_account_id, + amount_staked: 1, + }; + let info: DispatchInfo = + DispatchInfoOf::<::RuntimeCall>::default(); + let extension = SubtensorSignedExtension::::new(); + let result = extension.validate(&who, &call.into(), &info, 10); + + assert_err!(result, InvalidTransaction::ExhaustsResources); + + 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!( + SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey_account_id), + hotkey_account_id, + 1, + ), + Error::::StakeRateLimitExceeded + ); + + let current_stakes = + SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey_account_id); + assert_eq!(current_stakes, max_stakes); + }); +} + // /*********************************************************** // staking::remove_stake() tests // ************************************************************/ +#[test] +fn test_remove_stake_under_limit() { + new_test_ext().execute_with(|| { + let hotkey_account_id = U256::from(561337); + let coldkey_account_id = U256::from(61337); + let who: ::AccountId = hotkey_account_id.into(); + let netuid: u16 = 1; + let start_nonce: u64 = 0; + let tempo: u16 = 13; + let max_unstakes = 2; + + SubtensorModule::set_target_stakes_per_interval(max_unstakes); + + let call = pallet_subtensor::Call::remove_stake { + hotkey: hotkey_account_id, + amount_unstaked: 1, + }; + let info: DispatchInfo = + DispatchInfoOf::<::RuntimeCall>::default(); + let extension = SubtensorSignedExtension::::new(); + let result = extension.validate(&who, &call.into(), &info, 10); + + assert_ok!(result); + + 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_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, + 1, + )); + + let current_unstakes = + SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey_account_id); + assert!(current_unstakes <= max_unstakes); + }); +} + +#[test] +fn test_remove_stake_rate_limit_exceeded() { + new_test_ext().execute_with(|| { + let hotkey_account_id = U256::from(561337); + let coldkey_account_id = U256::from(61337); + let who: ::AccountId = hotkey_account_id.into(); + 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_hotkey( + &hotkey_account_id, + max_unstakes, + block_number, + ); + + let call = pallet_subtensor::Call::remove_stake { + hotkey: hotkey_account_id, + amount_unstaked: 1, + }; + let info: DispatchInfo = + DispatchInfoOf::<::RuntimeCall>::default(); + let extension = SubtensorSignedExtension::::new(); + let result = extension.validate(&who, &call.into(), &info, 10); + + assert_err!(result, InvalidTransaction::ExhaustsResources); + + 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 + ); + + let current_unstakes = + SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey_account_id); + assert_eq!(current_unstakes, max_unstakes); + }); +} + #[test] #[cfg(not(tarpaulin))] fn test_remove_stake_dispatch_info_ok() { @@ -1018,6 +1236,7 @@ fn test_full_with_delegating() { SubtensorModule::set_max_registrations_per_block(netuid, 4); SubtensorModule::set_target_registrations_per_interval(netuid, 4); SubtensorModule::set_max_allowed_uids(netuid, 4); // Allow all 4 to be registered at once + SubtensorModule::set_target_stakes_per_interval(10); // Increase max stakes per interval // Neither key can add stake because they dont have fundss. assert_eq!( @@ -1597,6 +1816,7 @@ fn test_full_with_delegating_some_servers() { let coldkey1 = U256::from(4); SubtensorModule::set_max_registrations_per_block(netuid, 4); SubtensorModule::set_max_allowed_uids(netuid, 10); // Allow at least 10 to be registered at once, so no unstaking occurs + SubtensorModule::set_target_stakes_per_interval(10); // Increase max stakes per interval // Neither key can add stake because they dont have fundss. assert_eq!( @@ -1924,6 +2144,7 @@ fn test_full_block_emission_occurs() { let coldkey1 = U256::from(4); SubtensorModule::set_max_registrations_per_block(netuid, 4); SubtensorModule::set_max_allowed_uids(netuid, 10); // Allow at least 10 to be registered at once, so no unstaking occurs + SubtensorModule::set_target_stakes_per_interval(10); // Increase max stakes per interval // Neither key can add stake because they dont have fundss. assert_eq!( diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 26d71cd0c1..9b8fe1591c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -665,6 +665,7 @@ parameter_types! { pub const SubtensorInitialSubnetLimit: u16 = 12; pub const SubtensorInitialNetworkLockReductionInterval: u64 = 14 * 7200; pub const SubtensorInitialNetworkRateLimit: u64 = 1 * 7200; + pub const SubtensorInitialTargetStakesPerInterval: u16 = 1; } impl pallet_subtensor::Config for Runtime { @@ -713,6 +714,7 @@ impl pallet_subtensor::Config for Runtime { type InitialSubnetOwnerCut = SubtensorInitialSubnetOwnerCut; type InitialSubnetLimit = SubtensorInitialSubnetLimit; type InitialNetworkRateLimit = SubtensorInitialNetworkRateLimit; + type InitialTargetStakesPerInterval = SubtensorInitialTargetStakesPerInterval; } use sp_runtime::BoundedVec;