diff --git a/pallets/subtensor/src/extensions/subtensor.rs b/pallets/subtensor/src/extensions/subtensor.rs index 7455dbb78a..64571843e7 100644 --- a/pallets/subtensor/src/extensions/subtensor.rs +++ b/pallets/subtensor/src/extensions/subtensor.rs @@ -7,7 +7,6 @@ use sp_runtime::traits::{ AsSystemOriginSigner, DispatchInfoOf, Dispatchable, Implication, TransactionExtension, ValidateResult, }; -use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError}; use sp_runtime::{ impl_tx_ext_default, transaction_validity::{TransactionSource, TransactionValidity, ValidTransaction}, @@ -17,8 +16,6 @@ use sp_std::vec::Vec; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{CustomTransactionError, NetUid, NetUidStorageIndex}; -const ADD_STAKE_BURN_PRIORITY_BOOST: u64 = 100; - type CallOf = ::RuntimeCall; type OriginOf = ::RuntimeOrigin; @@ -262,13 +259,6 @@ where .map_err(|_| CustomTransactionError::EvmKeyAssociateRateLimitExceeded)?; Ok((Default::default(), (), origin)) } - Some(Call::add_stake_burn { netuid, .. }) => { - Pallet::::ensure_subnet_owner(origin.clone(), *netuid).map_err(|_| { - TransactionValidityError::Invalid(InvalidTransaction::BadSigner) - })?; - - Ok((Self::validity_ok(ADD_STAKE_BURN_PRIORITY_BOOST), (), origin)) - } _ => Ok((Default::default(), (), origin)), } } diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 6aa949ae45..ecd8d4212a 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -131,6 +131,8 @@ mod hooks { .saturating_add(migrations::migrate_rate_limiting_last_blocks::migrate_obsolete_rate_limiting_last_blocks_storage::()) // Re-encode rate limit keys after introducing OwnerHyperparamUpdate variant .saturating_add(migrations::migrate_rate_limit_keys::migrate_rate_limit_keys::()) + // Remove AddStakeBurn entries from LastRateLimitedBlock + .saturating_add(migrations::migrate_remove_add_stake_burn_rate_limit::migrate_remove_add_stake_burn_rate_limit::()) // Migrate remove network modality .saturating_add(migrations::migrate_remove_network_modality::migrate_remove_network_modality::()) // Migrate Immunity Period diff --git a/pallets/subtensor/src/migrations/migrate_remove_add_stake_burn_rate_limit.rs b/pallets/subtensor/src/migrations/migrate_remove_add_stake_burn_rate_limit.rs new file mode 100644 index 0000000000..dcbf307855 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_remove_add_stake_burn_rate_limit.rs @@ -0,0 +1,53 @@ +use alloc::string::String; +use alloc::vec::Vec; +use frame_support::{traits::Get, weights::Weight}; + +use crate::{Config, HasMigrationRun, LastRateLimitedBlock, RateLimitKey}; + +const MIGRATION_NAME: &[u8] = b"migrate_remove_add_stake_burn_rate_limit"; + +pub fn migrate_remove_add_stake_burn_rate_limit() -> Weight { + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(MIGRATION_NAME) { + log::info!( + "Migration '{}' already executed - skipping", + String::from_utf8_lossy(MIGRATION_NAME) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(MIGRATION_NAME) + ); + + let mut scanned_count = 0u64; + let keys_to_remove = LastRateLimitedBlock::::iter_keys() + .filter_map(|key| { + scanned_count = scanned_count.saturating_add(1); + matches!(key, RateLimitKey::AddStakeBurn(_)).then_some(key) + }) + .collect::>(); + let removed_count = keys_to_remove.len() as u64; + + weight = weight.saturating_add(T::DbWeight::get().reads(scanned_count)); + + for key in &keys_to_remove { + LastRateLimitedBlock::::remove(key); + } + + weight = weight.saturating_add(T::DbWeight::get().writes(removed_count)); + + HasMigrationRun::::insert(MIGRATION_NAME, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{}' completed. scanned_entries={}, removed_add_stake_burn_entries={}", + String::from_utf8_lossy(MIGRATION_NAME), + scanned_count, + removed_count + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index 9974fd0175..d8177a8ccf 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -35,6 +35,7 @@ pub mod migrate_populate_owned_hotkeys; pub mod migrate_rao; pub mod migrate_rate_limit_keys; pub mod migrate_rate_limiting_last_blocks; +pub mod migrate_remove_add_stake_burn_rate_limit; pub mod migrate_remove_commitments_rate_limit; pub mod migrate_remove_network_modality; pub mod migrate_remove_old_identity_maps; diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index b1ded8659f..cacde73d74 100644 --- a/pallets/subtensor/src/staking/recycle_alpha.rs +++ b/pallets/subtensor/src/staking/recycle_alpha.rs @@ -1,6 +1,5 @@ use super::*; use crate::{Error, system::ensure_signed}; -use frame_support::storage::{TransactionOutcome, transactional}; use subtensor_runtime_common::{AlphaBalance, NetUid}; impl Pallet { @@ -125,6 +124,7 @@ impl Pallet { Ok(amount) } + pub(crate) fn do_add_stake_burn( origin: OriginFor, hotkey: T::AccountId, @@ -132,17 +132,6 @@ impl Pallet { amount: TaoBalance, limit: Option, ) -> DispatchResult { - Self::ensure_subnet_owner(origin.clone(), netuid)?; - - let current_block = Self::get_current_block_as_u64(); - let last_block = Self::get_rate_limited_last_block(&RateLimitKey::AddStakeBurn(netuid)); - let rate_limit = TransactionType::AddStakeBurn.rate_limit_on_subnet::(netuid); - - ensure!( - last_block.is_zero() || current_block.saturating_sub(last_block) >= rate_limit, - Error::::AddStakeBurnRateLimitExceeded - ); - let alpha = if let Some(limit) = limit { Self::do_add_stake_limit(origin.clone(), hotkey.clone(), netuid, amount, limit, false)? } else { @@ -151,8 +140,6 @@ impl Pallet { Self::do_burn_alpha(origin, hotkey.clone(), alpha, netuid)?; - Self::set_rate_limited_last_block(&RateLimitKey::AddStakeBurn(netuid), current_block); - Self::deposit_event(Event::AddStakeBurn { netuid, hotkey, @@ -173,20 +160,12 @@ impl Pallet { netuid: NetUid, amount: TaoBalance, ) -> Result { - transactional::with_transaction(|| { - let alpha = match Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount) { - Ok(a) => a, - Err(e) => return TransactionOutcome::Rollback(Err(e)), - }; - match Self::do_recycle_alpha(origin, hotkey, alpha, netuid) { - Ok(real_alpha) => TransactionOutcome::Commit(Ok(real_alpha)), - Err(e) => TransactionOutcome::Rollback(Err(e)), - } - }) + let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?; + Self::do_recycle_alpha(origin, hotkey, alpha, netuid) } /// Atomically stakes TAO and burns the resulting alpha. Permissionless - /// counterpart to `do_add_stake_burn`: no subnet-owner guard and no rate + /// counterpart to `do_add_stake_burn`: return the amount of alpha burned. /// limit. Used by the chain extension. pub fn do_add_stake_burn_permissionless( origin: OriginFor, @@ -194,15 +173,7 @@ impl Pallet { netuid: NetUid, amount: TaoBalance, ) -> Result { - transactional::with_transaction(|| { - let alpha = match Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount) { - Ok(a) => a, - Err(e) => return TransactionOutcome::Rollback(Err(e)), - }; - match Self::do_burn_alpha(origin, hotkey, alpha, netuid) { - Ok(real_alpha) => TransactionOutcome::Commit(Ok(real_alpha)), - Err(e) => TransactionOutcome::Rollback(Err(e)), - } - }) + let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?; + Self::do_burn_alpha(origin, hotkey, alpha, netuid) } } diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 4bbfce09c7..bf280556e0 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -1123,6 +1123,45 @@ fn test_migrate_rate_limit_keys() { }); } +#[test] +fn test_migrate_remove_add_stake_burn_rate_limit() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &[u8] = b"migrate_remove_add_stake_burn_rate_limit"; + let netuid = NetUid::from(1); + let other_netuid = NetUid::from(2); + let preserved_netuid = NetUid::from(3); + let add_stake_burn_key = RateLimitKey::AddStakeBurn(netuid); + let other_add_stake_burn_key = RateLimitKey::AddStakeBurn(other_netuid); + let preserved_key = RateLimitKey::SetSNOwnerHotkey(preserved_netuid); + + SubtensorModule::set_rate_limited_last_block(&add_stake_burn_key, 100); + SubtensorModule::set_rate_limited_last_block(&other_add_stake_burn_key, 200); + SubtensorModule::set_rate_limited_last_block(&preserved_key, 300); + + let weight = + crate::migrations::migrate_remove_add_stake_burn_rate_limit::migrate_remove_add_stake_burn_rate_limit::(); + + assert!( + HasMigrationRun::::get(MIGRATION_NAME.to_vec()), + "Migration should be marked as executed" + ); + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + + assert_eq!( + SubtensorModule::get_rate_limited_last_block(&add_stake_burn_key), + 0 + ); + assert_eq!( + SubtensorModule::get_rate_limited_last_block(&other_add_stake_burn_key), + 0 + ); + assert_eq!( + SubtensorModule::get_rate_limited_last_block(&preserved_key), + 300 + ); + }); +} + #[test] fn test_migrate_fix_staking_hot_keys() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/tests/recycle_alpha.rs b/pallets/subtensor/src/tests/recycle_alpha.rs index 3bd14306bf..3da1112972 100644 --- a/pallets/subtensor/src/tests/recycle_alpha.rs +++ b/pallets/subtensor/src/tests/recycle_alpha.rs @@ -774,7 +774,7 @@ fn test_add_stake_burn_with_limit_success() { } #[test] -fn test_add_stake_burn_non_owner_fails() { +fn test_add_stake_burn_non_owner_succeeds() { new_test_ext(1).execute_with(|| { let hotkey_account_id = U256::from(1); let coldkey_account_id = U256::from(2); @@ -793,17 +793,30 @@ fn test_add_stake_burn_non_owner_fails() { // Give non-owner some balance add_balance_to_coldkey_account(&non_owner_coldkey, amount.into()); - // Non-owner trying to call add_stake_burn should fail with BadOrigin - assert_noop!( - SubtensorModule::add_stake_burn( - RuntimeOrigin::signed(non_owner_coldkey), - hotkey_account_id, - netuid, - amount.into(), - None, + // Any signed origin can atomically stake and burn. + assert_ok!(SubtensorModule::add_stake_burn( + RuntimeOrigin::signed(non_owner_coldkey), + hotkey_account_id, + netuid, + amount.into(), + None, + )); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &non_owner_coldkey, + netuid ), - DispatchError::BadOrigin + AlphaBalance::ZERO ); + + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AddStakeBurn { .. }) + ) + })); }); } @@ -827,7 +840,7 @@ fn test_add_stake_burn_nonexistent_subnet_fails() { amount.into(), None, ), - DispatchError::BadOrigin + Error::::SubnetNotExists ); }); } @@ -861,66 +874,3 @@ fn test_add_stake_burn_insufficient_balance_fails() { ); }); } - -#[test] -fn test_add_stake_burn_rate_limit_exceeded() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(533453); - let coldkey_account_id = U256::from(55453); - let amount: u64 = 10_000_000_000; // 10 TAO - - // Add network - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - - // Setup reserves with large liquidity - let tao_reserve = TaoBalance::from(1_000_000_000_000_u64); - let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - - // Give coldkey sufficient balance for multiple "add stake and burn" operations. - add_balance_to_coldkey_account(&coldkey_account_id, (amount * 10).into()); - - assert_eq!( - SubtensorModule::get_rate_limited_last_block(&RateLimitKey::AddStakeBurn(netuid)), - 0 - ); - - // First "add stake and burn" should succeed - assert_ok!(SubtensorModule::add_stake_burn( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into(), - None, - )); - - assert_eq!( - SubtensorModule::get_rate_limited_last_block(&RateLimitKey::AddStakeBurn(netuid)), - SubtensorModule::get_current_block_as_u64() - ); - - // Second "add stake and burn" immediately after should fail due to rate limit - assert_noop!( - SubtensorModule::add_stake_burn( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into(), - None, - ), - Error::::AddStakeBurnRateLimitExceeded - ); - - // After stepping past the rate limit, "add stake and burn" should succeed again - let rate_limit = TransactionType::AddStakeBurn.rate_limit_on_subnet::(netuid); - step_block(rate_limit as u16); - - assert_ok!(SubtensorModule::add_stake_burn( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into(), - None, - )); - }); -} diff --git a/pallets/utility/src/weights.rs b/pallets/utility/src/weights.rs index f5234ee528..462804199f 100644 --- a/pallets/utility/src/weights.rs +++ b/pallets/utility/src/weights.rs @@ -2,7 +2,7 @@ //! Autogenerated weights for `pallet_subtensor_utility` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 -//! DATE: 2026-04-23, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `runnervmeorf1`, CPU: `AMD EPYC 7763 64-Core Processor` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` @@ -22,7 +22,7 @@ // --no-storage-info // --no-min-squares // --no-median-slopes -// --output=/tmp/tmp.KdzJkvj7lA +// --output=/tmp/tmp.4nwfKx4NPm // --template=/home/runner/work/subtensor/subtensor/.maintain/frame-weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -57,10 +57,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 5_069_000 picoseconds. - Weight::from_parts(17_591_644, 3983) - // Standard Error: 2_161 - .saturating_add(Weight::from_parts(5_615_733, 0).saturating_mul(c.into())) + // Minimum execution time: 4_859_000 picoseconds. + Weight::from_parts(28_407_150, 3983) + // Standard Error: 6_395 + .saturating_add(Weight::from_parts(5_254_263, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) @@ -71,8 +71,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 15_228_000 picoseconds. - Weight::from_parts(15_679_000, 3983) + // Minimum execution time: 14_828_000 picoseconds. + Weight::from_parts(15_318_000, 3983) .saturating_add(T::DbWeight::get().reads(2_u64)) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) @@ -84,18 +84,18 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 5_110_000 picoseconds. - Weight::from_parts(17_036_214, 3983) - // Standard Error: 2_084 - .saturating_add(Weight::from_parts(5_820_595, 0).saturating_mul(c.into())) + // Minimum execution time: 4_528_000 picoseconds. + Weight::from_parts(18_871_700, 3983) + // Standard Error: 1_818 + .saturating_add(Weight::from_parts(5_525_521, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) } fn dispatch_as() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 6_962_000 picoseconds. - Weight::from_parts(7_233_000, 0) + // Minimum execution time: 6_562_000 picoseconds. + Weight::from_parts(6_823_000, 0) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -106,18 +106,18 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 5_019_000 picoseconds. - Weight::from_parts(16_701_377, 3983) - // Standard Error: 2_713 - .saturating_add(Weight::from_parts(5_639_027, 0).saturating_mul(c.into())) + // Minimum execution time: 4_939_000 picoseconds. + Weight::from_parts(12_544_904, 3983) + // Standard Error: 3_006 + .saturating_add(Weight::from_parts(5_296_996, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) } fn dispatch_as_fallible() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 6_943_000 picoseconds. - Weight::from_parts(7_243_000, 0) + // Minimum execution time: 6_472_000 picoseconds. + Weight::from_parts(6_862_000, 0) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -127,8 +127,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 21_730_000 picoseconds. - Weight::from_parts(22_171_000, 3983) + // Minimum execution time: 21_039_000 picoseconds. + Weight::from_parts(21_600_000, 3983) .saturating_add(T::DbWeight::get().reads(2_u64)) } } @@ -144,10 +144,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 5_069_000 picoseconds. - Weight::from_parts(17_591_644, 3983) - // Standard Error: 2_161 - .saturating_add(Weight::from_parts(5_615_733, 0).saturating_mul(c.into())) + // Minimum execution time: 4_859_000 picoseconds. + Weight::from_parts(28_407_150, 3983) + // Standard Error: 6_395 + .saturating_add(Weight::from_parts(5_254_263, 0).saturating_mul(c.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) @@ -158,8 +158,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 15_228_000 picoseconds. - Weight::from_parts(15_679_000, 3983) + // Minimum execution time: 14_828_000 picoseconds. + Weight::from_parts(15_318_000, 3983) .saturating_add(RocksDbWeight::get().reads(2_u64)) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) @@ -171,18 +171,18 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 5_110_000 picoseconds. - Weight::from_parts(17_036_214, 3983) - // Standard Error: 2_084 - .saturating_add(Weight::from_parts(5_820_595, 0).saturating_mul(c.into())) + // Minimum execution time: 4_528_000 picoseconds. + Weight::from_parts(18_871_700, 3983) + // Standard Error: 1_818 + .saturating_add(Weight::from_parts(5_525_521, 0).saturating_mul(c.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) } fn dispatch_as() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 6_962_000 picoseconds. - Weight::from_parts(7_233_000, 0) + // Minimum execution time: 6_562_000 picoseconds. + Weight::from_parts(6_823_000, 0) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -193,18 +193,18 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 5_019_000 picoseconds. - Weight::from_parts(16_701_377, 3983) - // Standard Error: 2_713 - .saturating_add(Weight::from_parts(5_639_027, 0).saturating_mul(c.into())) + // Minimum execution time: 4_939_000 picoseconds. + Weight::from_parts(12_544_904, 3983) + // Standard Error: 3_006 + .saturating_add(Weight::from_parts(5_296_996, 0).saturating_mul(c.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) } fn dispatch_as_fallible() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 6_943_000 picoseconds. - Weight::from_parts(7_243_000, 0) + // Minimum execution time: 6_472_000 picoseconds. + Weight::from_parts(6_862_000, 0) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -214,8 +214,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 21_730_000 picoseconds. - Weight::from_parts(22_171_000, 3983) + // Minimum execution time: 21_039_000 picoseconds. + Weight::from_parts(21_600_000, 3983) .saturating_add(RocksDbWeight::get().reads(2_u64)) } }