From a37cc79f2bc2f20e7f2a4c5074e4e7cdfbe327e5 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 17 Oct 2025 15:09:46 +0300 Subject: [PATCH 01/95] Initialize pallet-rate-limiting --- Cargo.lock | 11 ++ Cargo.toml | 1 + pallets/rate-limiting/Cargo.toml | 31 +++++ pallets/rate-limiting/src/lib.rs | 225 +++++++++++++++++++++++++++++++ 4 files changed, 268 insertions(+) create mode 100644 pallets/rate-limiting/Cargo.toml create mode 100644 pallets/rate-limiting/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6cc892ad7c..79ed81c6a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10205,6 +10205,17 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-rate-limiting" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "sp-std", +] + [[package]] name = "pallet-recovery" version = "41.0.0" diff --git a/Cargo.toml b/Cargo.toml index ce2f3cf2ed..5a8d1742d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ pallet-subtensor = { path = "pallets/subtensor", default-features = false } pallet-subtensor-swap = { path = "pallets/swap", default-features = false } pallet-subtensor-swap-runtime-api = { path = "pallets/swap/runtime-api", default-features = false } pallet-subtensor-swap-rpc = { path = "pallets/swap/rpc", default-features = false } +pallet-rate-limiting = { path = "pallets/rate-limiting", default-features = false } procedural-fork = { path = "support/procedural-fork", default-features = false } safe-math = { path = "primitives/safe-math", default-features = false } share-pool = { path = "primitives/share-pool", default-features = false } diff --git a/pallets/rate-limiting/Cargo.toml b/pallets/rate-limiting/Cargo.toml new file mode 100644 index 0000000000..559dbaf816 --- /dev/null +++ b/pallets/rate-limiting/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "pallet-rate-limiting" +version = "0.1.0" +edition.workspace = true + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true, features = ["derive"] } +frame-support.workspace = true +frame-system.workspace = true +scale-info = { workspace = true, features = ["derive"] } +sp-std.workspace = true + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-std/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs new file mode 100644 index 0000000000..1b52782826 --- /dev/null +++ b/pallets/rate-limiting/src/lib.rs @@ -0,0 +1,225 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +//! Basic rate limiting pallet. + +pub use pallet::*; + +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata}; +use scale_info::TypeInfo; +use sp_std::fmt; + +#[frame_support::pallet] +pub mod pallet { + use crate::TransactionIdentifier; + use codec::Codec; + use frame_support::{ + pallet_prelude::*, sp_runtime::traits::Saturating, traits::GetCallMetadata, + }; + use frame_system::pallet_prelude::*; + use sp_std::vec::Vec; + + /// Configuration trait for the rate limiting pallet. + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching runtime call type. + type RuntimeCall: Parameter + + Codec + + GetCallMetadata + + IsType<::RuntimeCall>; + } + + /// Storage mapping from transaction identifier to its block-based rate limit. + #[pallet::storage] + #[pallet::getter(fn limits)] + pub type Limits = + StorageMap<_, Blake2_128Concat, TransactionIdentifier, BlockNumberFor, OptionQuery>; + + /// Tracks when a transaction was last observed. + #[pallet::storage] + pub type LastSeen = + StorageMap<_, Blake2_128Concat, TransactionIdentifier, BlockNumberFor, OptionQuery>; + + /// Events emitted by the rate limiting pallet. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A rate limit was set or updated. + RateLimitSet { + /// Identifier of the affected transaction. + transaction: TransactionIdentifier, + /// The new limit expressed in blocks. + limit: BlockNumberFor, + /// Pallet name associated with the transaction. + pallet: Vec, + /// Extrinsic name associated with the transaction. + extrinsic: Vec, + }, + /// A rate limit was cleared. + RateLimitCleared { + /// Identifier of the affected transaction. + transaction: TransactionIdentifier, + /// Pallet name associated with the transaction. + pallet: Vec, + /// Extrinsic name associated with the transaction. + extrinsic: Vec, + }, + } + + /// Errors that can occur while configuring rate limits. + #[pallet::error] + pub enum Error { + /// Failed to extract the pallet and extrinsic indices from the call. + InvalidRuntimeCall, + /// Attempted to remove a limit that is not present. + MissingRateLimit, + } + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + impl Pallet { + /// Returns `true` when the given transaction identifier passes its configured rate limit. + pub fn is_within_limit(identifier: &TransactionIdentifier) -> Result { + let Some(limit) = Limits::::get(identifier) else { + return Ok(true); + }; + + let current = frame_system::Pallet::::block_number(); + if let Some(last) = LastSeen::::get(identifier) { + let delta = current.saturating_sub(last); + if delta < limit { + return Ok(false); + } + } + + Ok(true) + } + } + + #[pallet::call] + impl Pallet { + /// Sets the rate limit, in blocks, for the given call. + /// + /// The supplied `call` is only used to derive the pallet and extrinsic indices; **any + /// arguments embedded in the call are ignored**. + #[pallet::call_index(0)] + #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] + pub fn set_rate_limit( + origin: OriginFor, + call: Box<::RuntimeCall>, + limit: BlockNumberFor, + ) -> DispatchResult { + ensure_root(origin)?; + + let identifier = TransactionIdentifier::from_call::(call.as_ref())?; + Limits::::insert(&identifier, limit); + + let (pallet_name, extrinsic_name) = identifier.names::()?; + let pallet = Vec::from(pallet_name.as_bytes()); + let extrinsic = Vec::from(extrinsic_name.as_bytes()); + + Self::deposit_event(Event::RateLimitSet { + transaction: identifier, + limit, + pallet, + extrinsic, + }); + + Ok(()) + } + + /// Clears the rate limit for the given call, if present. + /// + /// The supplied `call` is only used to derive the pallet and extrinsic indices; **any + /// arguments embedded in the call are ignored**. + #[pallet::call_index(1)] + #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] + pub fn clear_rate_limit( + origin: OriginFor, + call: Box<::RuntimeCall>, + ) -> DispatchResult { + ensure_root(origin)?; + + let identifier = TransactionIdentifier::from_call::(call.as_ref())?; + + let (pallet_name, extrinsic_name) = identifier.names::()?; + let pallet = Vec::from(pallet_name.as_bytes()); + let extrinsic = Vec::from(extrinsic_name.as_bytes()); + + ensure!( + Limits::::take(&identifier).is_some(), + Error::::MissingRateLimit + ); + + Self::deposit_event(Event::RateLimitCleared { + transaction: identifier, + pallet, + extrinsic, + }); + + Ok(()) + } + } +} + +/// Identifies a runtime call by pallet and extrinsic indices. +#[derive( + Clone, Copy, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, +)] +pub struct TransactionIdentifier { + /// Index of the pallet containing the extrinsic. + pub pallet_index: u8, + /// Variant index of the extrinsic within the pallet. + pub extrinsic_index: u8, +} + +impl TransactionIdentifier { + /// Builds a new identifier from pallet/extrinsic indices. + const fn new(pallet_index: u8, extrinsic_index: u8) -> Self { + Self { + pallet_index, + extrinsic_index, + } + } + + /// Returns the pallet and extrinsic name associated with this identifier. + fn names(&self) -> Result<(&'static str, &'static str), DispatchError> + where + T: Config, + { + let modules = ::RuntimeCall::get_module_names(); + let pallet_name = modules + .get(self.pallet_index as usize) + .copied() + .ok_or(Error::::InvalidRuntimeCall)?; + let call_names = ::RuntimeCall::get_call_names(pallet_name); + let extrinsic_name = call_names + .get(self.extrinsic_index as usize) + .copied() + .ok_or(Error::::InvalidRuntimeCall)?; + Ok((pallet_name, extrinsic_name)) + } + + /// Builds an identifier from a runtime call by extracting its pallet/extrinsic indices. + fn from_call(call: &::RuntimeCall) -> Result + where + T: Config, + { + call.using_encoded(|encoded| { + let pallet_index = *encoded.get(0).ok_or(Error::::InvalidRuntimeCall)?; + let extrinsic_index = *encoded.get(1).ok_or(Error::::InvalidRuntimeCall)?; + Ok(TransactionIdentifier::new(pallet_index, extrinsic_index)) + }) + } +} + +impl fmt::Debug for TransactionIdentifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TransactionIdentifier") + .field("pallet_index", &self.pallet_index) + .field("extrinsic_index", &self.extrinsic_index) + .finish() + } +} From e903915b076c93a2b71a4ab695984bb7b2385700 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 17 Oct 2025 16:04:53 +0300 Subject: [PATCH 02/95] Add transaction extension for rate limiting --- Cargo.lock | 1 + pallets/rate-limiting/Cargo.toml | 2 + pallets/rate-limiting/src/lib.rs | 93 +++------------ .../src/transaction_extension.rs | 108 ++++++++++++++++++ pallets/rate-limiting/src/types.rs | 81 +++++++++++++ 5 files changed, 211 insertions(+), 74 deletions(-) create mode 100644 pallets/rate-limiting/src/transaction_extension.rs create mode 100644 pallets/rate-limiting/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 79ed81c6a7..4295ea61a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10214,6 +10214,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-std", + "subtensor-runtime-common", ] [[package]] diff --git a/pallets/rate-limiting/Cargo.toml b/pallets/rate-limiting/Cargo.toml index 559dbaf816..76c6e18142 100644 --- a/pallets/rate-limiting/Cargo.toml +++ b/pallets/rate-limiting/Cargo.toml @@ -15,6 +15,7 @@ frame-support.workspace = true frame-system.workspace = true scale-info = { workspace = true, features = ["derive"] } sp-std.workspace = true +subtensor-runtime-common.workspace = true [features] default = ["std"] @@ -24,6 +25,7 @@ std = [ "frame-system/std", "scale-info/std", "sp-std/std", + "subtensor-runtime-common/std", ] try-runtime = [ "frame-support/try-runtime", diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 1b52782826..16a1130769 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -3,22 +3,24 @@ //! Basic rate limiting pallet. pub use pallet::*; +pub use transaction_extension::RateLimitTransactionExtension; +pub use types::{Scope, TransactionIdentifier}; -use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata}; -use scale_info::TypeInfo; -use sp_std::fmt; +mod transaction_extension; +mod types; #[frame_support::pallet] pub mod pallet { - use crate::TransactionIdentifier; use codec::Codec; + use core::fmt::Debug; use frame_support::{ pallet_prelude::*, sp_runtime::traits::Saturating, traits::GetCallMetadata, }; - use frame_system::pallet_prelude::*; + use frame_system::{ensure_root, pallet_prelude::*}; use sp_std::vec::Vec; + use crate::types::TransactionIdentifier; + /// Configuration trait for the rate limiting pallet. #[pallet::config] pub trait Config: frame_system::Config { @@ -27,9 +29,12 @@ pub mod pallet { + Codec + GetCallMetadata + IsType<::RuntimeCall>; + + /// Context type used for contextual (per-group) rate limits. + type ScopeContext: Parameter + Clone + PartialEq + Eq + Debug; } - /// Storage mapping from transaction identifier to its block-based rate limit. + /// Storage mapping from transaction identifier to its configured rate limit. #[pallet::storage] #[pallet::getter(fn limits)] pub type Limits = @@ -49,7 +54,7 @@ pub mod pallet { /// Identifier of the affected transaction. transaction: TransactionIdentifier, /// The new limit expressed in blocks. - limit: BlockNumberFor, + block_span: BlockNumberFor, /// Pallet name associated with the transaction. pallet: Vec, /// Extrinsic name associated with the transaction. @@ -82,14 +87,14 @@ pub mod pallet { impl Pallet { /// Returns `true` when the given transaction identifier passes its configured rate limit. pub fn is_within_limit(identifier: &TransactionIdentifier) -> Result { - let Some(limit) = Limits::::get(identifier) else { + let Some(block_span) = Limits::::get(identifier) else { return Ok(true); }; let current = frame_system::Pallet::::block_number(); if let Some(last) = LastSeen::::get(identifier) { let delta = current.saturating_sub(last); - if delta < limit { + if delta < block_span { return Ok(false); } } @@ -100,7 +105,7 @@ pub mod pallet { #[pallet::call] impl Pallet { - /// Sets the rate limit, in blocks, for the given call. + /// Sets the rate limit configuration for the given call. /// /// The supplied `call` is only used to derive the pallet and extrinsic indices; **any /// arguments embedded in the call are ignored**. @@ -109,12 +114,12 @@ pub mod pallet { pub fn set_rate_limit( origin: OriginFor, call: Box<::RuntimeCall>, - limit: BlockNumberFor, + block_span: BlockNumberFor, ) -> DispatchResult { ensure_root(origin)?; let identifier = TransactionIdentifier::from_call::(call.as_ref())?; - Limits::::insert(&identifier, limit); + Limits::::insert(&identifier, block_span); let (pallet_name, extrinsic_name) = identifier.names::()?; let pallet = Vec::from(pallet_name.as_bytes()); @@ -122,7 +127,7 @@ pub mod pallet { Self::deposit_event(Event::RateLimitSet { transaction: identifier, - limit, + block_span, pallet, extrinsic, }); @@ -163,63 +168,3 @@ pub mod pallet { } } } - -/// Identifies a runtime call by pallet and extrinsic indices. -#[derive( - Clone, Copy, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, -)] -pub struct TransactionIdentifier { - /// Index of the pallet containing the extrinsic. - pub pallet_index: u8, - /// Variant index of the extrinsic within the pallet. - pub extrinsic_index: u8, -} - -impl TransactionIdentifier { - /// Builds a new identifier from pallet/extrinsic indices. - const fn new(pallet_index: u8, extrinsic_index: u8) -> Self { - Self { - pallet_index, - extrinsic_index, - } - } - - /// Returns the pallet and extrinsic name associated with this identifier. - fn names(&self) -> Result<(&'static str, &'static str), DispatchError> - where - T: Config, - { - let modules = ::RuntimeCall::get_module_names(); - let pallet_name = modules - .get(self.pallet_index as usize) - .copied() - .ok_or(Error::::InvalidRuntimeCall)?; - let call_names = ::RuntimeCall::get_call_names(pallet_name); - let extrinsic_name = call_names - .get(self.extrinsic_index as usize) - .copied() - .ok_or(Error::::InvalidRuntimeCall)?; - Ok((pallet_name, extrinsic_name)) - } - - /// Builds an identifier from a runtime call by extracting its pallet/extrinsic indices. - fn from_call(call: &::RuntimeCall) -> Result - where - T: Config, - { - call.using_encoded(|encoded| { - let pallet_index = *encoded.get(0).ok_or(Error::::InvalidRuntimeCall)?; - let extrinsic_index = *encoded.get(1).ok_or(Error::::InvalidRuntimeCall)?; - Ok(TransactionIdentifier::new(pallet_index, extrinsic_index)) - }) - } -} - -impl fmt::Debug for TransactionIdentifier { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("TransactionIdentifier") - .field("pallet_index", &self.pallet_index) - .field("extrinsic_index", &self.extrinsic_index) - .finish() - } -} diff --git a/pallets/rate-limiting/src/transaction_extension.rs b/pallets/rate-limiting/src/transaction_extension.rs new file mode 100644 index 0000000000..deacab0874 --- /dev/null +++ b/pallets/rate-limiting/src/transaction_extension.rs @@ -0,0 +1,108 @@ +use codec::{Decode, DecodeWithMemTracking, Encode}; +use frame_support::{ + dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo}, + pallet_prelude::Weight, + sp_runtime::{ + traits::{ + DispatchInfoOf, DispatchOriginOf, Dispatchable, Implication, TransactionExtension, + ValidateResult, + }, + transaction_validity::{ + InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction, + }, + }, +}; +use scale_info::TypeInfo; +use sp_std::{marker::PhantomData, result::Result}; + +use crate::{Config, LastSeen, Limits, Pallet, types::TransactionIdentifier}; + +/// Identifier returned in the transaction metadata for the rate limiting extension. +const IDENTIFIER: &str = "RateLimitTransactionExtension"; + +/// Custom error code used to signal a rate limit violation. +const RATE_LIMIT_DENIED: u8 = 1; + +/// Transaction extension that enforces pallet rate limiting rules. +#[derive(Default, Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)] +pub struct RateLimitTransactionExtension(PhantomData); + +impl core::fmt::Debug for RateLimitTransactionExtension { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(IDENTIFIER) + } +} + +impl TransactionExtension<::RuntimeCall> for RateLimitTransactionExtension +where + T: Config + Send + Sync + TypeInfo, + ::RuntimeCall: Dispatchable, +{ + const IDENTIFIER: &'static str = IDENTIFIER; + + type Implicit = (); + type Val = Option; + type Pre = Option; + + fn weight(&self, _call: &::RuntimeCall) -> Weight { + Weight::zero() + } + + fn validate( + &self, + origin: DispatchOriginOf<::RuntimeCall>, + call: &::RuntimeCall, + _info: &DispatchInfoOf<::RuntimeCall>, + _len: usize, + _self_implicit: Self::Implicit, + _inherited_implication: &impl Implication, + _source: TransactionSource, + ) -> ValidateResult::RuntimeCall> { + let identifier = match TransactionIdentifier::from_call::(call) { + Ok(identifier) => identifier, + Err(_) => return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + }; + + if Limits::::get(&identifier).is_none() { + return Ok((ValidTransaction::default(), None, origin)); + } + + let within_limit = Pallet::::is_within_limit(&identifier) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + + if !within_limit { + return Err(TransactionValidityError::Invalid( + InvalidTransaction::Custom(RATE_LIMIT_DENIED), + )); + } + + Ok((ValidTransaction::default(), Some(identifier), origin)) + } + + fn prepare( + self, + val: Self::Val, + _origin: &DispatchOriginOf<::RuntimeCall>, + _call: &::RuntimeCall, + _info: &DispatchInfoOf<::RuntimeCall>, + _len: usize, + ) -> Result { + Ok(val) + } + + fn post_dispatch( + pre: Self::Pre, + _info: &DispatchInfoOf<::RuntimeCall>, + _post_info: &mut PostDispatchInfo, + _len: usize, + result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + if result.is_ok() { + if let Some(identifier) = pre { + let block_number = frame_system::Pallet::::block_number(); + LastSeen::::insert(&identifier, block_number); + } + } + Ok(()) + } +} diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs new file mode 100644 index 0000000000..88143c84fb --- /dev/null +++ b/pallets/rate-limiting/src/types.rs @@ -0,0 +1,81 @@ +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata}; +use scale_info::TypeInfo; + +/// Defines the scope within which a rate limit applies. +#[derive( + Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Debug, +)] +pub enum Scope { + /// Rate limit applies chain-wide. + Global, + /// Rate limit applies to a specific context (e.g., subnet). + Contextual(Context), +} + +/// Identifies a runtime call by pallet and extrinsic indices. +#[derive( + Clone, + Copy, + PartialEq, + Eq, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + Debug, +)] +pub struct TransactionIdentifier { + /// Pallet variant index. + pub pallet_index: u8, + /// Call variant index within the pallet. + pub extrinsic_index: u8, +} + +impl TransactionIdentifier { + /// Builds a new identifier from pallet/extrinsic indices. + pub const fn new(pallet_index: u8, extrinsic_index: u8) -> Self { + Self { + pallet_index, + extrinsic_index, + } + } + + /// Returns the pallet and extrinsic names associated with this identifier. + pub fn names(&self) -> Result<(&'static str, &'static str), DispatchError> + where + T: crate::pallet::Config, + ::RuntimeCall: GetCallMetadata, + { + let modules = ::RuntimeCall::get_module_names(); + let pallet_name = modules + .get(self.pallet_index as usize) + .copied() + .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; + let call_names = ::RuntimeCall::get_call_names(pallet_name); + let extrinsic_name = call_names + .get(self.extrinsic_index as usize) + .copied() + .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; + Ok((pallet_name, extrinsic_name)) + } + + /// Builds an identifier from a runtime call by extracting pallet/extrinsic indices. + pub fn from_call( + call: &::RuntimeCall, + ) -> Result + where + T: crate::pallet::Config, + { + call.using_encoded(|encoded| { + let pallet_index = *encoded + .get(0) + .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; + let extrinsic_index = *encoded + .get(1) + .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; + Ok(TransactionIdentifier::new(pallet_index, extrinsic_index)) + }) + } +} From ea289efdb4ad960be3c18597bed6c147bf32e5fb Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 17 Oct 2025 19:13:46 +0300 Subject: [PATCH 03/95] Implement contextual scope --- pallets/rate-limiting/src/lib.rs | 32 ++++++++++++++----- .../src/transaction_extension.rs | 12 ++++--- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 16a1130769..5affba481c 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -12,14 +12,13 @@ mod types; #[frame_support::pallet] pub mod pallet { use codec::Codec; - use core::fmt::Debug; use frame_support::{ pallet_prelude::*, sp_runtime::traits::Saturating, traits::GetCallMetadata, }; use frame_system::{ensure_root, pallet_prelude::*}; use sp_std::vec::Vec; - use crate::types::TransactionIdentifier; + use crate::types::{Scope, TransactionIdentifier}; /// Configuration trait for the rate limiting pallet. #[pallet::config] @@ -31,7 +30,7 @@ pub mod pallet { + IsType<::RuntimeCall>; /// Context type used for contextual (per-group) rate limits. - type ScopeContext: Parameter + Clone + PartialEq + Eq + Debug; + type ScopeContext: Parameter + Clone + PartialEq + Eq; } /// Storage mapping from transaction identifier to its configured rate limit. @@ -41,9 +40,18 @@ pub mod pallet { StorageMap<_, Blake2_128Concat, TransactionIdentifier, BlockNumberFor, OptionQuery>; /// Tracks when a transaction was last observed. + /// + /// The second key is `None` for `Scope::Global` and `Some(context)` for `Scope::Contextual`. #[pallet::storage] - pub type LastSeen = - StorageMap<_, Blake2_128Concat, TransactionIdentifier, BlockNumberFor, OptionQuery>; + pub type LastSeen = StorageDoubleMap< + _, + Blake2_128Concat, + TransactionIdentifier, + Blake2_128Concat, + Option<::ScopeContext>, + BlockNumberFor, + OptionQuery, + >; /// Events emitted by the rate limiting pallet. #[pallet::event] @@ -85,14 +93,22 @@ pub mod pallet { pub struct Pallet(_); impl Pallet { - /// Returns `true` when the given transaction identifier passes its configured rate limit. - pub fn is_within_limit(identifier: &TransactionIdentifier) -> Result { + /// Returns `true` when the given transaction identifier passes its configured rate limit within the provided scope. + pub fn is_within_limit( + identifier: &TransactionIdentifier, + scope: Scope<::ScopeContext>, + ) -> Result { let Some(block_span) = Limits::::get(identifier) else { return Ok(true); }; let current = frame_system::Pallet::::block_number(); - if let Some(last) = LastSeen::::get(identifier) { + let context_key = match scope { + Scope::Global => None, + Scope::Contextual(ctx) => Some(ctx), + }; + + if let Some(last) = LastSeen::::get(identifier, context_key.clone()) { let delta = current.saturating_sub(last); if delta < block_span { return Ok(false); diff --git a/pallets/rate-limiting/src/transaction_extension.rs b/pallets/rate-limiting/src/transaction_extension.rs index deacab0874..a109d9c8c1 100644 --- a/pallets/rate-limiting/src/transaction_extension.rs +++ b/pallets/rate-limiting/src/transaction_extension.rs @@ -15,7 +15,10 @@ use frame_support::{ use scale_info::TypeInfo; use sp_std::{marker::PhantomData, result::Result}; -use crate::{Config, LastSeen, Limits, Pallet, types::TransactionIdentifier}; +use crate::{ + Config, LastSeen, Limits, Pallet, + types::{Scope, TransactionIdentifier}, +}; /// Identifier returned in the transaction metadata for the rate limiting extension. const IDENTIFIER: &str = "RateLimitTransactionExtension"; @@ -67,8 +70,9 @@ where return Ok((ValidTransaction::default(), None, origin)); } - let within_limit = Pallet::::is_within_limit(&identifier) - .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + let within_limit = + Pallet::::is_within_limit(&identifier, Scope::::Global) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; if !within_limit { return Err(TransactionValidityError::Invalid( @@ -100,7 +104,7 @@ where if result.is_ok() { if let Some(identifier) = pre { let block_number = frame_system::Pallet::::block_number(); - LastSeen::::insert(&identifier, block_number); + LastSeen::::insert(&identifier, Option::::None, block_number); } } Ok(()) From ebcec75740e978111443fd47f64a97e67343266e Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 17 Oct 2025 19:38:16 +0300 Subject: [PATCH 04/95] Add context resolver --- pallets/rate-limiting/src/lib.rs | 28 +++++++++---------- ...ansaction_extension.rs => tx_extension.rs} | 23 +++++++++------ pallets/rate-limiting/src/types.rs | 14 ++++------ 3 files changed, 33 insertions(+), 32 deletions(-) rename pallets/rate-limiting/src/{transaction_extension.rs => tx_extension.rs} (82%) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 5affba481c..528fab3943 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -3,10 +3,10 @@ //! Basic rate limiting pallet. pub use pallet::*; -pub use transaction_extension::RateLimitTransactionExtension; -pub use types::{Scope, TransactionIdentifier}; +pub use tx_extension::RateLimitTransactionExtension; +pub use types::{RateLimitContextResolver, TransactionIdentifier}; -mod transaction_extension; +mod tx_extension; mod types; #[frame_support::pallet] @@ -18,7 +18,7 @@ pub mod pallet { use frame_system::{ensure_root, pallet_prelude::*}; use sp_std::vec::Vec; - use crate::types::{Scope, TransactionIdentifier}; + use crate::types::{RateLimitContextResolver, TransactionIdentifier}; /// Configuration trait for the rate limiting pallet. #[pallet::config] @@ -30,7 +30,10 @@ pub mod pallet { + IsType<::RuntimeCall>; /// Context type used for contextual (per-group) rate limits. - type ScopeContext: Parameter + Clone + PartialEq + Eq; + type LimitContext: Parameter + Clone + PartialEq + Eq; + + /// Resolves the context for a given runtime call. + type ContextResolver: RateLimitContextResolver<::RuntimeCall, Self::LimitContext>; } /// Storage mapping from transaction identifier to its configured rate limit. @@ -41,14 +44,14 @@ pub mod pallet { /// Tracks when a transaction was last observed. /// - /// The second key is `None` for `Scope::Global` and `Some(context)` for `Scope::Contextual`. + /// The second key is `None` for global limits and `Some(context)` for contextual limits. #[pallet::storage] pub type LastSeen = StorageDoubleMap< _, Blake2_128Concat, TransactionIdentifier, Blake2_128Concat, - Option<::ScopeContext>, + Option<::LimitContext>, BlockNumberFor, OptionQuery, >; @@ -93,22 +96,19 @@ pub mod pallet { pub struct Pallet(_); impl Pallet { - /// Returns `true` when the given transaction identifier passes its configured rate limit within the provided scope. + /// Returns `true` when the given transaction identifier passes its configured rate limit + /// within the provided context. pub fn is_within_limit( identifier: &TransactionIdentifier, - scope: Scope<::ScopeContext>, + context: Option<::LimitContext>, ) -> Result { let Some(block_span) = Limits::::get(identifier) else { return Ok(true); }; let current = frame_system::Pallet::::block_number(); - let context_key = match scope { - Scope::Global => None, - Scope::Contextual(ctx) => Some(ctx), - }; - if let Some(last) = LastSeen::::get(identifier, context_key.clone()) { + if let Some(last) = LastSeen::::get(identifier, &context) { let delta = current.saturating_sub(last); if delta < block_span { return Ok(false); diff --git a/pallets/rate-limiting/src/transaction_extension.rs b/pallets/rate-limiting/src/tx_extension.rs similarity index 82% rename from pallets/rate-limiting/src/transaction_extension.rs rename to pallets/rate-limiting/src/tx_extension.rs index a109d9c8c1..b7e55e1222 100644 --- a/pallets/rate-limiting/src/transaction_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -17,7 +17,7 @@ use sp_std::{marker::PhantomData, result::Result}; use crate::{ Config, LastSeen, Limits, Pallet, - types::{Scope, TransactionIdentifier}, + types::{RateLimitContextResolver, TransactionIdentifier}, }; /// Identifier returned in the transaction metadata for the rate limiting extension. @@ -44,8 +44,8 @@ where const IDENTIFIER: &'static str = IDENTIFIER; type Implicit = (); - type Val = Option; - type Pre = Option; + type Val = Option<(TransactionIdentifier, Option)>; + type Pre = Option<(TransactionIdentifier, Option)>; fn weight(&self, _call: &::RuntimeCall) -> Weight { Weight::zero() @@ -70,9 +70,10 @@ where return Ok((ValidTransaction::default(), None, origin)); } - let within_limit = - Pallet::::is_within_limit(&identifier, Scope::::Global) - .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + let context = ::ContextResolver::context(call); + + let within_limit = Pallet::::is_within_limit(&identifier, context.clone()) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; if !within_limit { return Err(TransactionValidityError::Invalid( @@ -80,7 +81,11 @@ where )); } - Ok((ValidTransaction::default(), Some(identifier), origin)) + Ok(( + ValidTransaction::default(), + Some((identifier, context)), + origin, + )) } fn prepare( @@ -102,9 +107,9 @@ where result: &DispatchResult, ) -> Result<(), TransactionValidityError> { if result.is_ok() { - if let Some(identifier) = pre { + if let Some((identifier, context)) = pre { let block_number = frame_system::Pallet::::block_number(); - LastSeen::::insert(&identifier, Option::::None, block_number); + LastSeen::::insert(&identifier, context, block_number); } } Ok(()) diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 88143c84fb..ee62ed894e 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -2,15 +2,11 @@ use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata}; use scale_info::TypeInfo; -/// Defines the scope within which a rate limit applies. -#[derive( - Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Debug, -)] -pub enum Scope { - /// Rate limit applies chain-wide. - Global, - /// Rate limit applies to a specific context (e.g., subnet). - Contextual(Context), +/// Resolves the optional context within which a rate limit applies. +pub trait RateLimitContextResolver { + /// Returns `Some(context)` when the limit should be applied per-context, or `None` for global + /// limits. + fn context(call: &Call) -> Option; } /// Identifies a runtime call by pallet and extrinsic indices. From 56333c9fd4fbc98b9b618eab74cd5a2938068f86 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 21 Oct 2025 15:41:52 +0300 Subject: [PATCH 05/95] Add default rate limit --- pallets/rate-limiting/src/lib.rs | 59 +++++++++++++++++++---- pallets/rate-limiting/src/tx_extension.rs | 15 ++++-- pallets/rate-limiting/src/types.rs | 20 ++++++++ 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 528fab3943..761cbed270 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -4,7 +4,7 @@ pub use pallet::*; pub use tx_extension::RateLimitTransactionExtension; -pub use types::{RateLimitContextResolver, TransactionIdentifier}; +pub use types::{RateLimit, RateLimitContextResolver, TransactionIdentifier}; mod tx_extension; mod types; @@ -18,7 +18,7 @@ pub mod pallet { use frame_system::{ensure_root, pallet_prelude::*}; use sp_std::vec::Vec; - use crate::types::{RateLimitContextResolver, TransactionIdentifier}; + use crate::types::{RateLimit, RateLimitContextResolver, TransactionIdentifier}; /// Configuration trait for the rate limiting pallet. #[pallet::config] @@ -39,8 +39,13 @@ pub mod pallet { /// Storage mapping from transaction identifier to its configured rate limit. #[pallet::storage] #[pallet::getter(fn limits)] - pub type Limits = - StorageMap<_, Blake2_128Concat, TransactionIdentifier, BlockNumberFor, OptionQuery>; + pub type Limits = StorageMap< + _, + Blake2_128Concat, + TransactionIdentifier, + RateLimit>, + OptionQuery, + >; /// Tracks when a transaction was last observed. /// @@ -56,6 +61,11 @@ pub mod pallet { OptionQuery, >; + /// Default block span applied when an extrinsic uses the default rate limit. + #[pallet::storage] + #[pallet::getter(fn default_limit)] + pub type DefaultLimit = StorageValue<_, BlockNumberFor, ValueQuery>; + /// Events emitted by the rate limiting pallet. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -64,8 +74,8 @@ pub mod pallet { RateLimitSet { /// Identifier of the affected transaction. transaction: TransactionIdentifier, - /// The new limit expressed in blocks. - block_span: BlockNumberFor, + /// The new limit configuration applied to the transaction. + limit: RateLimit>, /// Pallet name associated with the transaction. pallet: Vec, /// Extrinsic name associated with the transaction. @@ -80,6 +90,11 @@ pub mod pallet { /// Extrinsic name associated with the transaction. extrinsic: Vec, }, + /// The default rate limit was set or updated. + DefaultRateLimitSet { + /// The new default limit expressed in blocks. + block_span: BlockNumberFor, + }, } /// Errors that can occur while configuring rate limits. @@ -102,7 +117,7 @@ pub mod pallet { identifier: &TransactionIdentifier, context: Option<::LimitContext>, ) -> Result { - let Some(block_span) = Limits::::get(identifier) else { + let Some(block_span) = Self::resolved_limit(identifier) else { return Ok(true); }; @@ -117,6 +132,14 @@ pub mod pallet { Ok(true) } + + fn resolved_limit(identifier: &TransactionIdentifier) -> Option> { + let limit = Limits::::get(identifier)?; + Some(match limit { + RateLimit::Default => DefaultLimit::::get(), + RateLimit::Exact(block_span) => block_span, + }) + } } #[pallet::call] @@ -130,12 +153,12 @@ pub mod pallet { pub fn set_rate_limit( origin: OriginFor, call: Box<::RuntimeCall>, - block_span: BlockNumberFor, + limit: RateLimit>, ) -> DispatchResult { ensure_root(origin)?; let identifier = TransactionIdentifier::from_call::(call.as_ref())?; - Limits::::insert(&identifier, block_span); + Limits::::insert(&identifier, limit); let (pallet_name, extrinsic_name) = identifier.names::()?; let pallet = Vec::from(pallet_name.as_bytes()); @@ -143,7 +166,7 @@ pub mod pallet { Self::deposit_event(Event::RateLimitSet { transaction: identifier, - block_span, + limit, pallet, extrinsic, }); @@ -182,5 +205,21 @@ pub mod pallet { Ok(()) } + + /// Sets the default rate limit in blocks applied to calls configured to use it. + #[pallet::call_index(2)] + #[pallet::weight(T::DbWeight::get().writes(1))] + pub fn set_default_rate_limit( + origin: OriginFor, + block_span: BlockNumberFor, + ) -> DispatchResult { + ensure_root(origin)?; + + DefaultLimit::::put(block_span); + + Self::deposit_event(Event::DefaultRateLimitSet { block_span }); + + Ok(()) + } } } diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index b7e55e1222..097b23b961 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -5,7 +5,7 @@ use frame_support::{ sp_runtime::{ traits::{ DispatchInfoOf, DispatchOriginOf, Dispatchable, Implication, TransactionExtension, - ValidateResult, + ValidateResult, Zero, }, transaction_validity::{ InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction, @@ -17,7 +17,7 @@ use sp_std::{marker::PhantomData, result::Result}; use crate::{ Config, LastSeen, Limits, Pallet, - types::{RateLimitContextResolver, TransactionIdentifier}, + types::{RateLimit, RateLimitContextResolver, TransactionIdentifier}, }; /// Identifier returned in the transaction metadata for the rate limiting extension. @@ -66,7 +66,16 @@ where Err(_) => return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), }; - if Limits::::get(&identifier).is_none() { + let Some(limit) = Limits::::get(&identifier) else { + return Ok((ValidTransaction::default(), None, origin)); + }; + + let block_span = match limit { + RateLimit::Default => Pallet::::default_limit(), + RateLimit::Exact(block_span) => block_span, + }; + + if block_span.is_zero() { return Ok((ValidTransaction::default(), None, origin)); } diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index ee62ed894e..124dbbe3ab 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -75,3 +75,23 @@ impl TransactionIdentifier { }) } } + +/// Configuration value for a rate limit. +#[derive( + Clone, + Copy, + PartialEq, + Eq, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + Debug, +)] +pub enum RateLimit { + /// Use the pallet-level default rate limit. + Default, + /// Apply an exact rate limit measured in blocks. + Exact(BlockNumber), +} From 1b6b03cbe77a551a7500f29c9f38fc69c28a8fd1 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 21 Oct 2025 16:26:09 +0300 Subject: [PATCH 06/95] Add genesis config for pallet-rate-limiting --- Cargo.lock | 1 + pallets/rate-limiting/Cargo.toml | 2 ++ pallets/rate-limiting/src/lib.rs | 31 +++++++++++++++++++++++++++++- pallets/rate-limiting/src/types.rs | 2 ++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4295ea61a8..e315f31c63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10213,6 +10213,7 @@ dependencies = [ "frame-system", "parity-scale-codec", "scale-info", + "serde", "sp-std", "subtensor-runtime-common", ] diff --git a/pallets/rate-limiting/Cargo.toml b/pallets/rate-limiting/Cargo.toml index 76c6e18142..24620e2d54 100644 --- a/pallets/rate-limiting/Cargo.toml +++ b/pallets/rate-limiting/Cargo.toml @@ -16,6 +16,7 @@ frame-system.workspace = true scale-info = { workspace = true, features = ["derive"] } sp-std.workspace = true subtensor-runtime-common.workspace = true +serde = { workspace = true, features = ["derive"], optional = true } [features] default = ["std"] @@ -26,6 +27,7 @@ std = [ "scale-info/std", "sp-std/std", "subtensor-runtime-common/std", + "serde", ] try-runtime = [ "frame-support/try-runtime", diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 761cbed270..0b132c3dda 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -13,7 +13,9 @@ mod types; pub mod pallet { use codec::Codec; use frame_support::{ - pallet_prelude::*, sp_runtime::traits::Saturating, traits::GetCallMetadata, + pallet_prelude::*, + sp_runtime::traits::{Saturating, Zero}, + traits::{BuildGenesisConfig, GetCallMetadata}, }; use frame_system::{ensure_root, pallet_prelude::*}; use sp_std::vec::Vec; @@ -106,6 +108,33 @@ pub mod pallet { MissingRateLimit, } + #[pallet::genesis_config] + pub struct GenesisConfig { + pub default_limit: BlockNumberFor, + pub limits: Vec<(TransactionIdentifier, RateLimit>)>, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self { + default_limit: Zero::zero(), + limits: Vec::new(), + } + } + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + DefaultLimit::::put(self.default_limit); + + for (identifier, limit) in &self.limits { + Limits::::insert(identifier, limit.clone()); + } + } + } + #[pallet::pallet] #[pallet::without_storage_info] pub struct Pallet(_); diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 124dbbe3ab..4e00053ec7 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -10,6 +10,7 @@ pub trait RateLimitContextResolver { } /// Identifies a runtime call by pallet and extrinsic indices. +#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[derive( Clone, Copy, @@ -77,6 +78,7 @@ impl TransactionIdentifier { } /// Configuration value for a rate limit. +#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[derive( Clone, Copy, From 220c9052ad73a8bf7906ec6aa2c9469bdeca33f8 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 21 Oct 2025 19:26:54 +0300 Subject: [PATCH 07/95] Add rpc method to fetch rate limit --- Cargo.lock | 25 ++++++ Cargo.toml | 4 + pallets/rate-limiting/rpc/Cargo.toml | 22 ++++++ pallets/rate-limiting/rpc/src/lib.rs | 82 ++++++++++++++++++++ pallets/rate-limiting/runtime-api/Cargo.toml | 26 +++++++ pallets/rate-limiting/runtime-api/src/lib.rs | 24 ++++++ pallets/rate-limiting/src/lib.rs | 37 ++++++++- pallets/rate-limiting/src/tx_extension.rs | 2 +- 8 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 pallets/rate-limiting/rpc/Cargo.toml create mode 100644 pallets/rate-limiting/rpc/src/lib.rs create mode 100644 pallets/rate-limiting/runtime-api/Cargo.toml create mode 100644 pallets/rate-limiting/runtime-api/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e315f31c63..6e9eba1a5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10218,6 +10218,31 @@ dependencies = [ "subtensor-runtime-common", ] +[[package]] +name = "pallet-rate-limiting-rpc" +version = "0.1.0" +dependencies = [ + "jsonrpsee", + "pallet-rate-limiting-runtime-api", + "sp-api", + "sp-blockchain", + "sp-runtime", + "subtensor-runtime-common", +] + +[[package]] +name = "pallet-rate-limiting-runtime-api" +version = "0.1.0" +dependencies = [ + "pallet-rate-limiting", + "parity-scale-codec", + "scale-info", + "serde", + "sp-api", + "sp-std", + "subtensor-runtime-common", +] + [[package]] name = "pallet-recovery" version = "41.0.0" diff --git a/Cargo.toml b/Cargo.toml index 5a8d1742d9..ed71cc537e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,8 @@ members = [ "common", "node", "pallets/*", + "pallets/rate-limiting/runtime-api", + "pallets/rate-limiting/rpc", "precompiles", "primitives/*", "runtime", @@ -60,6 +62,8 @@ pallet-subtensor-swap = { path = "pallets/swap", default-features = false } pallet-subtensor-swap-runtime-api = { path = "pallets/swap/runtime-api", default-features = false } pallet-subtensor-swap-rpc = { path = "pallets/swap/rpc", default-features = false } pallet-rate-limiting = { path = "pallets/rate-limiting", default-features = false } +pallet-rate-limiting-runtime-api = { path = "pallets/rate-limiting/runtime-api", default-features = false } +pallet-rate-limiting-rpc = { path = "pallets/rate-limiting/rpc", default-features = false } procedural-fork = { path = "support/procedural-fork", default-features = false } safe-math = { path = "primitives/safe-math", default-features = false } share-pool = { path = "primitives/share-pool", default-features = false } diff --git a/pallets/rate-limiting/rpc/Cargo.toml b/pallets/rate-limiting/rpc/Cargo.toml new file mode 100644 index 0000000000..d5bf689e8b --- /dev/null +++ b/pallets/rate-limiting/rpc/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "pallet-rate-limiting-rpc" +version = "0.1.0" +description = "RPC interface for the rate limiting pallet" +edition.workspace = true + +[dependencies] +jsonrpsee = { workspace = true, features = ["client-core", "server", "macros"] } +sp-api.workspace = true +sp-blockchain.workspace = true +sp-runtime.workspace = true +pallet-rate-limiting-runtime-api.workspace = true +subtensor-runtime-common = { workspace = true, default-features = false } + +[features] +default = ["std"] +std = [ + "sp-api/std", + "sp-runtime/std", + "pallet-rate-limiting-runtime-api/std", + "subtensor-runtime-common/std", +] diff --git a/pallets/rate-limiting/rpc/src/lib.rs b/pallets/rate-limiting/rpc/src/lib.rs new file mode 100644 index 0000000000..ca7452a7a0 --- /dev/null +++ b/pallets/rate-limiting/rpc/src/lib.rs @@ -0,0 +1,82 @@ +//! RPC interface for the rate limiting pallet. + +use jsonrpsee::{ + core::RpcResult, + proc_macros::rpc, + types::{ErrorObjectOwned, error::ErrorObject}, +}; +use sp_api::ProvideRuntimeApi; +use sp_blockchain::HeaderBackend; +use sp_runtime::traits::Block as BlockT; +use std::sync::Arc; + +pub use pallet_rate_limiting_runtime_api::{RateLimitRpcResponse, RateLimitingRuntimeApi}; + +#[rpc(client, server)] +pub trait RateLimitingRpcApi { + #[method(name = "rateLimiting_getRateLimit")] + fn get_rate_limit( + &self, + pallet: Vec, + extrinsic: Vec, + at: Option, + ) -> RpcResult>; +} + +/// Error type of this RPC api. +pub enum Error { + /// The call to runtime failed. + RuntimeError(String), +} + +impl From for ErrorObjectOwned { + fn from(e: Error) -> Self { + match e { + Error::RuntimeError(e) => ErrorObject::owned(1, e, None::<()>), + } + } +} + +impl From for i32 { + fn from(e: Error) -> i32 { + match e { + Error::RuntimeError(_) => 1, + } + } +} + +/// RPC implementation for the rate limiting pallet. +pub struct RateLimiting { + client: Arc, + _marker: std::marker::PhantomData, +} + +impl RateLimiting { + /// Creates a new instance of the rate limiting RPC helper. + pub fn new(client: Arc) -> Self { + Self { + client, + _marker: Default::default(), + } + } +} + +impl RateLimitingRpcApiServer<::Hash> for RateLimiting +where + Block: BlockT, + C: ProvideRuntimeApi + HeaderBackend + Send + Sync + 'static, + C::Api: RateLimitingRuntimeApi, +{ + fn get_rate_limit( + &self, + pallet: Vec, + extrinsic: Vec, + at: Option<::Hash>, + ) -> RpcResult> { + let api = self.client.runtime_api(); + let at = at.unwrap_or_else(|| self.client.info().best_hash); + + api.get_rate_limit(at, pallet, extrinsic) + .map_err(|e| Error::RuntimeError(format!("Unable to fetch rate limit: {e:?}")).into()) + } +} diff --git a/pallets/rate-limiting/runtime-api/Cargo.toml b/pallets/rate-limiting/runtime-api/Cargo.toml new file mode 100644 index 0000000000..2847d865dd --- /dev/null +++ b/pallets/rate-limiting/runtime-api/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "pallet-rate-limiting-runtime-api" +version = "0.1.0" +description = "Runtime API for the rate limiting pallet" +edition.workspace = true + +[dependencies] +codec = { workspace = true, features = ["derive"] } +scale-info = { workspace = true, features = ["derive"] } +sp-api.workspace = true +sp-std.workspace = true +pallet-rate-limiting.workspace = true +subtensor-runtime-common = { workspace = true, default-features = false } +serde = { workspace = true, features = ["derive"], optional = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "sp-api/std", + "sp-std/std", + "pallet-rate-limiting/std", + "subtensor-runtime-common/std", + "serde", +] diff --git a/pallets/rate-limiting/runtime-api/src/lib.rs b/pallets/rate-limiting/runtime-api/src/lib.rs new file mode 100644 index 0000000000..1a32c094ea --- /dev/null +++ b/pallets/rate-limiting/runtime-api/src/lib.rs @@ -0,0 +1,24 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; +use pallet_rate_limiting::RateLimit; +use scale_info::TypeInfo; +use sp_std::vec::Vec; +use subtensor_runtime_common::BlockNumber; + +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] +pub struct RateLimitRpcResponse { + pub limit: Option>, + pub default_limit: BlockNumber, + pub resolved: Option, +} + +sp_api::decl_runtime_apis! { + pub trait RateLimitingRuntimeApi { + fn get_rate_limit(pallet: Vec, extrinsic: Vec) -> Option; + } +} diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 0b132c3dda..a07d75d618 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -18,7 +18,7 @@ pub mod pallet { traits::{BuildGenesisConfig, GetCallMetadata}, }; use frame_system::{ensure_root, pallet_prelude::*}; - use sp_std::vec::Vec; + use sp_std::{convert::TryFrom, vec::Vec}; use crate::types::{RateLimit, RateLimitContextResolver, TransactionIdentifier}; @@ -144,7 +144,7 @@ pub mod pallet { /// within the provided context. pub fn is_within_limit( identifier: &TransactionIdentifier, - context: Option<::LimitContext>, + context: &Option<::LimitContext>, ) -> Result { let Some(block_span) = Self::resolved_limit(identifier) else { return Ok(true); @@ -152,7 +152,7 @@ pub mod pallet { let current = frame_system::Pallet::::block_number(); - if let Some(last) = LastSeen::::get(identifier, &context) { + if let Some(last) = LastSeen::::get(identifier, context) { let delta = current.saturating_sub(last); if delta < block_span { return Ok(false); @@ -169,6 +169,37 @@ pub mod pallet { RateLimit::Exact(block_span) => block_span, }) } + + /// Returns the configured limit for the specified pallet/extrinsic names, if any. + pub fn limit_for_call_names( + pallet_name: &str, + extrinsic_name: &str, + ) -> Option>> { + let identifier = Self::identifier_for_call_names(pallet_name, extrinsic_name)?; + Limits::::get(&identifier) + } + + /// Returns the resolved block span for the specified pallet/extrinsic names, if any. + pub fn resolved_limit_for_call_names( + pallet_name: &str, + extrinsic_name: &str, + ) -> Option> { + let identifier = Self::identifier_for_call_names(pallet_name, extrinsic_name)?; + Self::resolved_limit(&identifier) + } + + fn identifier_for_call_names( + pallet_name: &str, + extrinsic_name: &str, + ) -> Option { + let modules = ::RuntimeCall::get_module_names(); + let pallet_pos = modules.iter().position(|name| *name == pallet_name)?; + let call_names = ::RuntimeCall::get_call_names(pallet_name); + let extrinsic_pos = call_names.iter().position(|name| *name == extrinsic_name)?; + let pallet_index = u8::try_from(pallet_pos).ok()?; + let extrinsic_index = u8::try_from(extrinsic_pos).ok()?; + Some(TransactionIdentifier::new(pallet_index, extrinsic_index)) + } } #[pallet::call] diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index 097b23b961..6ccfe8160b 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -81,7 +81,7 @@ where let context = ::ContextResolver::context(call); - let within_limit = Pallet::::is_within_limit(&identifier, context.clone()) + let within_limit = Pallet::::is_within_limit(&identifier, &context) .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; if !within_limit { From 69151b1b9ed8cba06e03f785274cf276dd74bad8 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 27 Oct 2025 17:11:18 +0300 Subject: [PATCH 08/95] Add tests to pallet-rate-limiting --- Cargo.lock | 3 + pallets/rate-limiting/Cargo.toml | 5 + pallets/rate-limiting/src/lib.rs | 6 + pallets/rate-limiting/src/mock.rs | 90 +++++++ pallets/rate-limiting/src/tests.rs | 281 ++++++++++++++++++++++ pallets/rate-limiting/src/tx_extension.rs | 181 ++++++++++++++ 6 files changed, 566 insertions(+) create mode 100644 pallets/rate-limiting/src/mock.rs create mode 100644 pallets/rate-limiting/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 6e9eba1a5a..d116651d33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10214,6 +10214,9 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", + "sp-core", + "sp-io", + "sp-runtime", "sp-std", "subtensor-runtime-common", ] diff --git a/pallets/rate-limiting/Cargo.toml b/pallets/rate-limiting/Cargo.toml index 24620e2d54..b24ff40ea5 100644 --- a/pallets/rate-limiting/Cargo.toml +++ b/pallets/rate-limiting/Cargo.toml @@ -18,6 +18,11 @@ sp-std.workspace = true subtensor-runtime-common.workspace = true serde = { workspace = true, features = ["derive"], optional = true } +[dev-dependencies] +sp-core.workspace = true +sp-io.workspace = true +sp-runtime.workspace = true + [features] default = ["std"] std = [ diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index a07d75d618..2ac9c76114 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -9,6 +9,12 @@ pub use types::{RateLimit, RateLimitContextResolver, TransactionIdentifier}; mod tx_extension; mod types; +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + #[frame_support::pallet] pub mod pallet { use codec::Codec; diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs new file mode 100644 index 0000000000..d7b9fde20b --- /dev/null +++ b/pallets/rate-limiting/src/mock.rs @@ -0,0 +1,90 @@ +#![allow(dead_code)] + +use frame_support::{ + derive_impl, + sp_runtime::{ + BuildStorage, + traits::{BlakeTwo256, IdentityLookup}, + }, + traits::{ConstU16, ConstU32, ConstU64, Everything}, +}; +use sp_core::H256; +use sp_io::TestExternalities; + +use crate as pallet_rate_limiting; +use crate::TransactionIdentifier; + +pub type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +pub type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + RateLimiting: pallet_rate_limiting, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = ConstU16<42>; + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; + type Block = Block; +} + +pub type LimitContext = u16; + +pub struct TestContextResolver; + +impl pallet_rate_limiting::RateLimitContextResolver + for TestContextResolver +{ + fn context(_call: &RuntimeCall) -> Option { + None + } +} + +impl pallet_rate_limiting::Config for Test { + type RuntimeCall = RuntimeCall; + type LimitContext = LimitContext; + type ContextResolver = TestContextResolver; +} + +pub type RateLimitingCall = crate::Call; + +pub fn new_test_ext() -> TestExternalities { + let storage = frame_system::GenesisConfig::::default() + .build_storage() + .expect("genesis build succeeds"); + + let mut ext = TestExternalities::new(storage); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub(crate) fn identifier_for(call: &RuntimeCall) -> TransactionIdentifier { + TransactionIdentifier::from_call::(call).expect("identifier for call") +} + +pub(crate) fn pop_last_event() -> RuntimeEvent { + System::events().pop().expect("event expected").event +} diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs new file mode 100644 index 0000000000..4eb07ad926 --- /dev/null +++ b/pallets/rate-limiting/src/tests.rs @@ -0,0 +1,281 @@ +use frame_support::{assert_noop, assert_ok, error::BadOrigin}; +use sp_runtime::DispatchError; + +use crate::{ + DefaultLimit, LastSeen, Limits, RateLimit, mock::*, pallet::Error, types::TransactionIdentifier, +}; + +#[test] +fn limit_for_call_names_returns_none_if_not_set() { + new_test_ext().execute_with(|| { + assert!( + RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit").is_none() + ); + }); +} + +#[test] +fn limit_for_call_names_returns_stored_limit() { + new_test_ext().execute_with(|| { + let call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&call); + Limits::::insert(identifier, RateLimit::Exact(7)); + + let fetched = RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit") + .expect("limit should exist"); + assert_eq!(fetched, RateLimit::Exact(7)); + }); +} + +#[test] +fn resolved_limit_for_call_names_resolves_default_value() { + new_test_ext().execute_with(|| { + DefaultLimit::::put(3); + let call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&call); + Limits::::insert(identifier, RateLimit::Default); + + let resolved = + RateLimiting::resolved_limit_for_call_names("RateLimiting", "set_default_rate_limit") + .expect("resolved limit"); + assert_eq!(resolved, 3); + }); +} + +#[test] +fn resolved_limit_for_call_names_returns_none_when_unset() { + new_test_ext().execute_with(|| { + assert!( + RateLimiting::resolved_limit_for_call_names("RateLimiting", "set_default_rate_limit") + .is_none() + ); + }); +} + +#[test] +fn is_within_limit_is_true_when_no_limit() { + new_test_ext().execute_with(|| { + let call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&call); + + let result = RateLimiting::is_within_limit(&identifier, &None); + assert_eq!(result.expect("no error expected"), true); + }); +} + +#[test] +fn is_within_limit_false_when_rate_limited() { + new_test_ext().execute_with(|| { + let call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&call); + Limits::::insert(identifier, RateLimit::Exact(5)); + LastSeen::::insert(identifier, Some(1 as LimitContext), 9); + + System::set_block_number(13); + + let within = RateLimiting::is_within_limit(&identifier, &Some(1 as LimitContext)) + .expect("call succeeds"); + assert!(!within); + }); +} + +#[test] +fn is_within_limit_true_after_required_span() { + new_test_ext().execute_with(|| { + let call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&call); + Limits::::insert(identifier, RateLimit::Exact(5)); + LastSeen::::insert(identifier, Some(2 as LimitContext), 10); + + System::set_block_number(20); + + let within = RateLimiting::is_within_limit(&identifier, &Some(2 as LimitContext)) + .expect("call succeeds"); + assert!(within); + }); +} + +#[test] +fn transaction_identifier_from_call_matches_expected_indices() { + let call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + + let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); + + // System is the first pallet in the mock runtime, RateLimiting is second. + assert_eq!(identifier.pallet_index, 1); + // set_default_rate_limit has call_index 2. + assert_eq!(identifier.extrinsic_index, 2); +} + +#[test] +fn transaction_identifier_names_matches_call_metadata() { + let call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); + + let (pallet, extrinsic) = identifier.names::().expect("call metadata"); + assert_eq!(pallet, "RateLimiting"); + assert_eq!(extrinsic, "set_default_rate_limit"); +} + +#[test] +fn transaction_identifier_names_error_for_unknown_indices() { + let identifier = TransactionIdentifier::new(99, 0); + + let err = identifier.names::().expect_err("should fail"); + let expected: DispatchError = Error::::InvalidRuntimeCall.into(); + assert_eq!(err, expected); +} + +#[test] +fn set_rate_limit_updates_storage_and_emits_event() { + new_test_ext().execute_with(|| { + System::reset_events(); + + let target_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let limit = RateLimit::Exact(9); + + assert_ok!(RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + Box::new(target_call.clone()), + limit + )); + + let identifier = identifier_for(&target_call); + assert_eq!(Limits::::get(identifier), Some(limit)); + + match pop_last_event() { + RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitSet { + transaction, + limit: emitted_limit, + pallet, + extrinsic, + }) => { + assert_eq!(transaction, identifier); + assert_eq!(emitted_limit, limit); + assert_eq!(pallet, b"RateLimiting".to_vec()); + assert_eq!(extrinsic, b"set_default_rate_limit".to_vec()); + } + other => panic!("unexpected event: {:?}", other), + } + }); +} + +#[test] +fn set_rate_limit_requires_root() { + new_test_ext().execute_with(|| { + let target_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + + assert_noop!( + RateLimiting::set_rate_limit( + RuntimeOrigin::signed(1), + Box::new(target_call), + RateLimit::Exact(1) + ), + BadOrigin + ); + }); +} + +#[test] +fn set_rate_limit_accepts_default_variant() { + new_test_ext().execute_with(|| { + let target_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + + assert_ok!(RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + Box::new(target_call.clone()), + RateLimit::Default + )); + + let identifier = identifier_for(&target_call); + assert_eq!(Limits::::get(identifier), Some(RateLimit::Default)); + }); +} + +#[test] +fn clear_rate_limit_removes_entry_and_emits_event() { + new_test_ext().execute_with(|| { + System::reset_events(); + + let target_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&target_call); + Limits::::insert(identifier, RateLimit::Exact(4)); + + assert_ok!(RateLimiting::clear_rate_limit( + RuntimeOrigin::root(), + Box::new(target_call.clone()) + )); + + assert_eq!(Limits::::get(identifier), None); + + match pop_last_event() { + RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitCleared { + transaction, + pallet, + extrinsic, + }) => { + assert_eq!(transaction, identifier); + assert_eq!(pallet, b"RateLimiting".to_vec()); + assert_eq!(extrinsic, b"set_default_rate_limit".to_vec()); + } + other => panic!("unexpected event: {:?}", other), + } + }); +} + +#[test] +fn clear_rate_limit_fails_when_missing() { + new_test_ext().execute_with(|| { + let target_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + + assert_noop!( + RateLimiting::clear_rate_limit(RuntimeOrigin::root(), Box::new(target_call)), + Error::::MissingRateLimit + ); + }); +} + +#[test] +fn set_default_rate_limit_updates_storage_and_emits_event() { + new_test_ext().execute_with(|| { + System::reset_events(); + + assert_ok!(RateLimiting::set_default_rate_limit( + RuntimeOrigin::root(), + 42 + )); + + assert_eq!(DefaultLimit::::get(), 42); + + match pop_last_event() { + RuntimeEvent::RateLimiting(crate::pallet::Event::DefaultRateLimitSet { + block_span, + }) => { + assert_eq!(block_span, 42); + } + other => panic!("unexpected event: {:?}", other), + } + }); +} + +#[test] +fn set_default_rate_limit_requires_root() { + new_test_ext().execute_with(|| { + assert_noop!( + RateLimiting::set_default_rate_limit(RuntimeOrigin::signed(1), 5), + BadOrigin + ); + }); +} diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index 6ccfe8160b..f06e72975e 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -124,3 +124,184 @@ where Ok(()) } } + +#[cfg(test)] +mod tests { + use codec::Encode; + use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo}; + use sp_runtime::{ + traits::{TransactionExtension, TxBaseImplication}, + transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError}, + }; + + use crate::{LastSeen, Limits, RateLimit, types::TransactionIdentifier}; + + use super::*; + use crate::mock::*; + + fn remark_call() -> RuntimeCall { + RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }) + } + + fn new_tx_extension() -> RateLimitTransactionExtension { + RateLimitTransactionExtension(Default::default()) + } + + fn validate_with_tx_extension( + extension: &RateLimitTransactionExtension, + call: &RuntimeCall, + ) -> Result< + ( + sp_runtime::transaction_validity::ValidTransaction, + Option<(TransactionIdentifier, Option)>, + RuntimeOrigin, + ), + TransactionValidityError, + > { + let info = call.get_dispatch_info(); + let len = call.encode().len(); + extension.validate( + RuntimeOrigin::signed(42), + call, + &info, + len, + (), + &TxBaseImplication(()), + TransactionSource::External, + ) + } + + #[test] + fn tx_extension_allows_calls_without_limit() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + let call = remark_call(); + + let (_valid, val, _origin) = + validate_with_tx_extension(&extension, &call).expect("valid"); + assert!(val.is_none()); + + let info = call.get_dispatch_info(); + let len = call.encode().len(); + let origin_for_prepare = RuntimeOrigin::signed(42); + let pre = extension + .clone() + .prepare(val.clone(), &origin_for_prepare, &call, &info, len) + .expect("prepare succeeds"); + + let mut post = PostDispatchInfo::default(); + RateLimitTransactionExtension::::post_dispatch( + pre, + &info, + &mut post, + len, + &Ok(()), + ) + .expect("post_dispatch succeeds"); + + let identifier = identifier_for(&call); + assert_eq!( + LastSeen::::get(identifier, None::), + None + ); + }); + } + + #[test] + fn tx_extension_records_last_seen_for_successful_call() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + let call = remark_call(); + let identifier = identifier_for(&call); + Limits::::insert(identifier, RateLimit::Exact(5)); + + System::set_block_number(10); + + let (_valid, val, _) = validate_with_tx_extension(&extension, &call).expect("valid"); + assert!(val.is_some()); + + let info = call.get_dispatch_info(); + let len = call.encode().len(); + let origin_for_prepare = RuntimeOrigin::signed(42); + let pre = extension + .clone() + .prepare(val.clone(), &origin_for_prepare, &call, &info, len) + .expect("prepare succeeds"); + + let mut post = PostDispatchInfo::default(); + RateLimitTransactionExtension::::post_dispatch( + pre, + &info, + &mut post, + len, + &Ok(()), + ) + .expect("post_dispatch succeeds"); + + assert_eq!( + LastSeen::::get(identifier, None::), + Some(10) + ); + }); + } + + #[test] + fn tx_extension_rejects_when_call_occurs_too_soon() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + let call = remark_call(); + let identifier = identifier_for(&call); + Limits::::insert(identifier, RateLimit::Exact(5)); + LastSeen::::insert(identifier, None::, 20); + + System::set_block_number(22); + + let err = + validate_with_tx_extension(&extension, &call).expect_err("should be rate limited"); + match err { + TransactionValidityError::Invalid(InvalidTransaction::Custom(code)) => { + assert_eq!(code, 1); + } + other => panic!("unexpected error: {:?}", other), + } + }); + } + + #[test] + fn tx_extension_skips_last_seen_when_span_zero() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + let call = remark_call(); + let identifier = identifier_for(&call); + Limits::::insert(identifier, RateLimit::Exact(0)); + + System::set_block_number(30); + + let (_valid, val, _) = validate_with_tx_extension(&extension, &call).expect("valid"); + assert!(val.is_none()); + + let info = call.get_dispatch_info(); + let len = call.encode().len(); + let origin_for_prepare = RuntimeOrigin::signed(42); + let pre = extension + .clone() + .prepare(val.clone(), &origin_for_prepare, &call, &info, len) + .expect("prepare succeeds"); + + let mut post = PostDispatchInfo::default(); + RateLimitTransactionExtension::::post_dispatch( + pre, + &info, + &mut post, + len, + &Ok(()), + ) + .expect("post_dispatch succeeds"); + + assert_eq!( + LastSeen::::get(identifier, None::), + None + ); + }); + } +} From 13b8774bdfb1bfdb6dcebbb30045fb2282da02d7 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 27 Oct 2025 18:18:27 +0300 Subject: [PATCH 09/95] Add crate-level documentation for pallet-rate-limiting --- pallets/rate-limiting/src/lib.rs | 70 +++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 2ac9c76114..e9659c7abd 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -1,6 +1,74 @@ #![cfg_attr(not(feature = "std"), no_std)] -//! Basic rate limiting pallet. +//! Rate limiting for runtime calls with optional contextual restrictions. +//! +//! # Overview +//! +//! `pallet-rate-limiting` lets a runtime restrict how frequently particular calls can execute. +//! Limits are stored on-chain, keyed by the call's pallet/variant pair. Each entry can specify an +//! exact block span or defer to a configured default. The pallet exposes three roots-only +//! extrinsics to manage this data: +//! +//! - [`set_rate_limit`](pallet::Pallet::set_rate_limit): assign a limit to an extrinsic, either as +//! `RateLimit::Exact(blocks)` or `RateLimit::Default`. +//! - [`clear_rate_limit`](pallet::Pallet::clear_rate_limit): remove a stored limit. +//! - [`set_default_rate_limit`](pallet::Pallet::set_default_rate_limit): set the global default +//! block span used by `RateLimit::Default` entries. +//! +//! The pallet also tracks the last block in which a rate-limited call was executed, per optional +//! *context*. Context allows one limit definition (for example, “set weights”) to be enforced per +//! subnet, account, or other grouping chosen by the runtime. The storage layout is: +//! +//! - [`Limits`](pallet::Limits): `TransactionIdentifier → RateLimit` +//! - [`DefaultLimit`](pallet::DefaultLimit): `BlockNumber` +//! - [`LastSeen`](pallet::LastSeen): `(TransactionIdentifier, Option) → BlockNumber` +//! +//! # Transaction extension +//! +//! Enforcement happens via [`RateLimitTransactionExtension`], which implements +//! `sp_runtime::traits::TransactionExtension`. The extension consults `Limits`, fetches the current +//! block, and decides whether the call is eligible. If successful, it returns metadata that causes +//! [`LastSeen`](pallet::LastSeen) to update after dispatch. A rejected call yields +//! `InvalidTransaction::Custom(1)`. +//! +//! To enable the extension, add it to your runtime's transaction extension tuple. For example: +//! +//! ```rust +//! pub type TransactionExtensions = ( +//! // ... other extensions ... +//! pallet_rate_limiting::RateLimitTransactionExtension, +//! ); +//! ``` +//! +//! # Context resolver +//! +//! The extension needs to know when two invocations should share a rate limit. This is controlled +//! by implementing [`RateLimitContextResolver`] for the runtime call type (or for a helper that the +//! runtime wires into [`Config::ContextResolver`]). The resolver receives the call and returns +//! `Some(context)` if the rate should be scoped (e.g. by `netuid`), or `None` for a global limit. +//! +//! ```rust +//! pub struct WeightsContextResolver; +//! +//! impl pallet_rate_limiting::RateLimitContextResolver +//! for WeightsContextResolver +//! { +//! fn context(call: &RuntimeCall) -> Option { +//! match call { +//! RuntimeCall::Subtensor(pallet_subtensor::Call::set_weights { netuid, .. }) => { +//! Some(*netuid) +//! } +//! _ => None, +//! } +//! } +//! } +//! +//! impl pallet_rate_limiting::Config for Runtime { +//! type RuntimeCall = RuntimeCall; +//! type LimitContext = NetUid; +//! type ContextResolver = WeightsContextResolver; +//! } +//! ``` pub use pallet::*; pub use tx_extension::RateLimitTransactionExtension; From a4a1c88b1764bbe777246846f25c542a95d0460f Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 27 Oct 2025 18:24:18 +0300 Subject: [PATCH 10/95] Add benchmarks for pallet-rate-limiting --- Cargo.lock | 1 + pallets/rate-limiting/Cargo.toml | 9 ++- pallets/rate-limiting/src/benchmarking.rs | 72 +++++++++++++++++++++++ pallets/rate-limiting/src/lib.rs | 14 ++++- pallets/rate-limiting/src/mock.rs | 13 ++++ pallets/rate-limiting/src/tests.rs | 38 +----------- pallets/rate-limiting/src/types.rs | 41 +++++++++++++ 7 files changed, 147 insertions(+), 41 deletions(-) create mode 100644 pallets/rate-limiting/src/benchmarking.rs diff --git a/Cargo.lock b/Cargo.lock index d116651d33..ee22c024f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10209,6 +10209,7 @@ dependencies = [ name = "pallet-rate-limiting" version = "0.1.0" dependencies = [ + "frame-benchmarking", "frame-support", "frame-system", "parity-scale-codec", diff --git a/pallets/rate-limiting/Cargo.toml b/pallets/rate-limiting/Cargo.toml index b24ff40ea5..3447145622 100644 --- a/pallets/rate-limiting/Cargo.toml +++ b/pallets/rate-limiting/Cargo.toml @@ -11,12 +11,13 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] codec = { workspace = true, features = ["derive"] } +frame-benchmarking = { workspace = true, optional = true } frame-support.workspace = true frame-system.workspace = true scale-info = { workspace = true, features = ["derive"] } +serde = { workspace = true, features = ["derive"], optional = true } sp-std.workspace = true subtensor-runtime-common.workspace = true -serde = { workspace = true, features = ["derive"], optional = true } [dev-dependencies] sp-core.workspace = true @@ -27,12 +28,16 @@ sp-runtime.workspace = true default = ["std"] std = [ "codec/std", + "frame-benchmarking?/std", "frame-support/std", "frame-system/std", "scale-info/std", + "serde", "sp-std/std", "subtensor-runtime-common/std", - "serde", +] +runtime-benchmarks = [ + "frame-benchmarking", ] try-runtime = [ "frame-support/try-runtime", diff --git a/pallets/rate-limiting/src/benchmarking.rs b/pallets/rate-limiting/src/benchmarking.rs new file mode 100644 index 0000000000..3266674ac7 --- /dev/null +++ b/pallets/rate-limiting/src/benchmarking.rs @@ -0,0 +1,72 @@ +//! Benchmarking setup for pallet-rate-limiting +#![cfg(feature = "runtime-benchmarks")] +#![allow(clippy::arithmetic_side_effects)] + +use codec::Decode; +use frame_benchmarking::v2::*; +use frame_system::{RawOrigin, pallet_prelude::BlockNumberFor}; + +use super::*; + +pub trait BenchmarkHelper { + fn sample_call() -> Call; +} + +impl BenchmarkHelper for () +where + Call: Decode, +{ + fn sample_call() -> Call { + Decode::decode(&mut &[][..]).expect("Provide a call via BenchmarkHelper::sample_call") + } +} + +fn sample_call() -> Box<::RuntimeCall> +where + T::BenchmarkHelper: BenchmarkHelper<::RuntimeCall>, +{ + Box::new(T::BenchmarkHelper::sample_call()) +} + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn set_rate_limit() { + let call = sample_call::(); + let limit = RateLimit::>::Exact(BlockNumberFor::::from(10u32)); + + #[extrinsic_call] + _(RawOrigin::Root, call, limit.clone()); + + assert!(Limits::::iter().any(|(_, stored)| stored == limit)); + } + + #[benchmark] + fn clear_rate_limit() { + let call = sample_call::(); + let limit = RateLimit::>::Exact(BlockNumberFor::::from(10u32)); + + // Pre-populate limit for benchmark call + let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); + Limits::::insert(identifier, limit); + + #[extrinsic_call] + _(RawOrigin::Root, call); + + assert!(Limits::::get(identifier).is_none()); + } + + #[benchmark] + fn set_default_rate_limit() { + let block_span = BlockNumberFor::::from(10u32); + + #[extrinsic_call] + _(RawOrigin::Root, block_span); + + assert_eq!(DefaultLimit::::get(), block_span); + } + + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index e9659c7abd..637216e71b 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -33,7 +33,7 @@ //! //! To enable the extension, add it to your runtime's transaction extension tuple. For example: //! -//! ```rust +//! ```ignore //! pub type TransactionExtensions = ( //! // ... other extensions ... //! pallet_rate_limiting::RateLimitTransactionExtension, @@ -47,7 +47,7 @@ //! runtime wires into [`Config::ContextResolver`]). The resolver receives the call and returns //! `Some(context)` if the rate should be scoped (e.g. by `netuid`), or `None` for a global limit. //! -//! ```rust +//! ```ignore //! pub struct WeightsContextResolver; //! //! impl pallet_rate_limiting::RateLimitContextResolver @@ -70,10 +70,14 @@ //! } //! ``` +#[cfg(feature = "runtime-benchmarks")] +pub use benchmarking::BenchmarkHelper; pub use pallet::*; pub use tx_extension::RateLimitTransactionExtension; pub use types::{RateLimit, RateLimitContextResolver, TransactionIdentifier}; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; mod tx_extension; mod types; @@ -94,6 +98,8 @@ pub mod pallet { use frame_system::{ensure_root, pallet_prelude::*}; use sp_std::{convert::TryFrom, vec::Vec}; + #[cfg(feature = "runtime-benchmarks")] + use crate::benchmarking::BenchmarkHelper as BenchmarkHelperTrait; use crate::types::{RateLimit, RateLimitContextResolver, TransactionIdentifier}; /// Configuration trait for the rate limiting pallet. @@ -110,6 +116,10 @@ pub mod pallet { /// Resolves the context for a given runtime call. type ContextResolver: RateLimitContextResolver<::RuntimeCall, Self::LimitContext>; + + /// Helper used to construct runtime calls for benchmarking. + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: BenchmarkHelperTrait<::RuntimeCall>; } /// Storage mapping from transaction identifier to its configured rate limit. diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index d7b9fde20b..4218e757a1 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -10,6 +10,7 @@ use frame_support::{ }; use sp_core::H256; use sp_io::TestExternalities; +use sp_std::vec::Vec; use crate as pallet_rate_limiting; use crate::TransactionIdentifier; @@ -67,6 +68,18 @@ impl pallet_rate_limiting::Config for Test { type RuntimeCall = RuntimeCall; type LimitContext = LimitContext; type ContextResolver = TestContextResolver; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = BenchHelper; +} + +#[cfg(feature = "runtime-benchmarks")] +pub struct BenchHelper; + +#[cfg(feature = "runtime-benchmarks")] +impl crate::BenchmarkHelper for BenchHelper { + fn sample_call() -> RuntimeCall { + RuntimeCall::System(frame_system::Call::remark { remark: Vec::new() }) + } } pub type RateLimitingCall = crate::Call; diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index 4eb07ad926..5a8d2dd933 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -1,9 +1,6 @@ use frame_support::{assert_noop, assert_ok, error::BadOrigin}; -use sp_runtime::DispatchError; -use crate::{ - DefaultLimit, LastSeen, Limits, RateLimit, mock::*, pallet::Error, types::TransactionIdentifier, -}; +use crate::{DefaultLimit, LastSeen, Limits, RateLimit, mock::*, pallet::Error}; #[test] fn limit_for_call_names_returns_none_if_not_set() { @@ -100,39 +97,6 @@ fn is_within_limit_true_after_required_span() { }); } -#[test] -fn transaction_identifier_from_call_matches_expected_indices() { - let call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - - let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); - - // System is the first pallet in the mock runtime, RateLimiting is second. - assert_eq!(identifier.pallet_index, 1); - // set_default_rate_limit has call_index 2. - assert_eq!(identifier.extrinsic_index, 2); -} - -#[test] -fn transaction_identifier_names_matches_call_metadata() { - let call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); - - let (pallet, extrinsic) = identifier.names::().expect("call metadata"); - assert_eq!(pallet, "RateLimiting"); - assert_eq!(extrinsic, "set_default_rate_limit"); -} - -#[test] -fn transaction_identifier_names_error_for_unknown_indices() { - let identifier = TransactionIdentifier::new(99, 0); - - let err = identifier.names::().expect_err("should fail"); - let expected: DispatchError = Error::::InvalidRuntimeCall.into(); - assert_eq!(err, expected); -} - #[test] fn set_rate_limit_updates_storage_and_emits_event() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 4e00053ec7..72e2a43777 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -97,3 +97,44 @@ pub enum RateLimit { /// Apply an exact rate limit measured in blocks. Exact(BlockNumber), } + +#[cfg(test)] +mod tests { + use sp_runtime::DispatchError; + + use super::*; + use crate::{mock::*, pallet::Error}; + + #[test] + fn transaction_identifier_from_call_matches_expected_indices() { + let call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + + let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); + + // System is the first pallet in the mock runtime, RateLimiting is second. + assert_eq!(identifier.pallet_index, 1); + // set_default_rate_limit has call_index 2. + assert_eq!(identifier.extrinsic_index, 2); + } + + #[test] + fn transaction_identifier_names_matches_call_metadata() { + let call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); + + let (pallet, extrinsic) = identifier.names::().expect("call metadata"); + assert_eq!(pallet, "RateLimiting"); + assert_eq!(extrinsic, "set_default_rate_limit"); + } + + #[test] + fn transaction_identifier_names_error_for_unknown_indices() { + let identifier = TransactionIdentifier::new(99, 0); + + let err = identifier.names::().expect_err("should fail"); + let expected: DispatchError = Error::::InvalidRuntimeCall.into(); + assert_eq!(err, expected); + } +} From 6ccc421e94f866ed99f85499de05673107ea142d Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 29 Oct 2025 15:18:52 +0300 Subject: [PATCH 11/95] Extend rate limit setting with context --- pallets/rate-limiting/src/benchmarking.rs | 13 +- pallets/rate-limiting/src/lib.rs | 67 ++++++--- pallets/rate-limiting/src/tests.rs | 175 +++++++++++++++++++--- pallets/rate-limiting/src/tx_extension.rs | 21 +-- 4 files changed, 217 insertions(+), 59 deletions(-) diff --git a/pallets/rate-limiting/src/benchmarking.rs b/pallets/rate-limiting/src/benchmarking.rs index 3266674ac7..bf4a3b37b5 100644 --- a/pallets/rate-limiting/src/benchmarking.rs +++ b/pallets/rate-limiting/src/benchmarking.rs @@ -36,11 +36,13 @@ mod benchmarks { fn set_rate_limit() { let call = sample_call::(); let limit = RateLimit::>::Exact(BlockNumberFor::::from(10u32)); + let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); + let context = ::ContextResolver::context(call.as_ref()); #[extrinsic_call] - _(RawOrigin::Root, call, limit.clone()); + _(RawOrigin::Root, call, limit.clone(), None); - assert!(Limits::::iter().any(|(_, stored)| stored == limit)); + assert_eq!(Limits::::get(&identifier, context), Some(limit)); } #[benchmark] @@ -50,12 +52,13 @@ mod benchmarks { // Pre-populate limit for benchmark call let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); - Limits::::insert(identifier, limit); + let context = ::ContextResolver::context(call.as_ref()); + Limits::::insert(identifier, context.clone(), limit); #[extrinsic_call] - _(RawOrigin::Root, call); + _(RawOrigin::Root, call, None); - assert!(Limits::::get(identifier).is_none()); + assert!(Limits::::get(identifier, context).is_none()); } #[benchmark] diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 637216e71b..23eff3cf9a 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -10,8 +10,10 @@ //! extrinsics to manage this data: //! //! - [`set_rate_limit`](pallet::Pallet::set_rate_limit): assign a limit to an extrinsic, either as -//! `RateLimit::Exact(blocks)` or `RateLimit::Default`. -//! - [`clear_rate_limit`](pallet::Pallet::clear_rate_limit): remove a stored limit. +//! `RateLimit::Exact(blocks)` or `RateLimit::Default`. The optional context parameter lets you +//! scope the configuration to a particular subnet/key/account while keeping a global fallback. +//! - [`clear_rate_limit`](pallet::Pallet::clear_rate_limit): remove a stored limit for the provided +//! context (or for the global entry when `None` is supplied). //! - [`set_default_rate_limit`](pallet::Pallet::set_default_rate_limit): set the global default //! block span used by `RateLimit::Default` entries. //! @@ -45,7 +47,9 @@ //! The extension needs to know when two invocations should share a rate limit. This is controlled //! by implementing [`RateLimitContextResolver`] for the runtime call type (or for a helper that the //! runtime wires into [`Config::ContextResolver`]). The resolver receives the call and returns -//! `Some(context)` if the rate should be scoped (e.g. by `netuid`), or `None` for a global limit. +//! `Some(context)` if the rate should be scoped (e.g. by `netuid`), or `None` to use the global +//! entry. The resolver is only used when *tracking* executions; you still configure limits via the +//! explicit `context` argument on `set_rate_limit`/`clear_rate_limit`. //! //! ```ignore //! pub struct WeightsContextResolver; @@ -112,7 +116,7 @@ pub mod pallet { + IsType<::RuntimeCall>; /// Context type used for contextual (per-group) rate limits. - type LimitContext: Parameter + Clone + PartialEq + Eq; + type LimitContext: Parameter + Clone + PartialEq + Eq + MaybeSerializeDeserialize; /// Resolves the context for a given runtime call. type ContextResolver: RateLimitContextResolver<::RuntimeCall, Self::LimitContext>; @@ -122,13 +126,15 @@ pub mod pallet { type BenchmarkHelper: BenchmarkHelperTrait<::RuntimeCall>; } - /// Storage mapping from transaction identifier to its configured rate limit. + /// Storage mapping from transaction identifier and optional context to its configured rate limit. #[pallet::storage] #[pallet::getter(fn limits)] - pub type Limits = StorageMap< + pub type Limits = StorageDoubleMap< _, Blake2_128Concat, TransactionIdentifier, + Blake2_128Concat, + Option<::LimitContext>, RateLimit>, OptionQuery, >; @@ -160,6 +166,8 @@ pub mod pallet { RateLimitSet { /// Identifier of the affected transaction. transaction: TransactionIdentifier, + /// Context to which the limit applies, if any. + context: Option<::LimitContext>, /// The new limit configuration applied to the transaction. limit: RateLimit>, /// Pallet name associated with the transaction. @@ -171,6 +179,8 @@ pub mod pallet { RateLimitCleared { /// Identifier of the affected transaction. transaction: TransactionIdentifier, + /// Context from which the limit was cleared, if any. + context: Option<::LimitContext>, /// Pallet name associated with the transaction. pallet: Vec, /// Extrinsic name associated with the transaction. @@ -195,7 +205,11 @@ pub mod pallet { #[pallet::genesis_config] pub struct GenesisConfig { pub default_limit: BlockNumberFor, - pub limits: Vec<(TransactionIdentifier, RateLimit>)>, + pub limits: Vec<( + TransactionIdentifier, + Option<::LimitContext>, + RateLimit>, + )>, } #[cfg(feature = "std")] @@ -213,8 +227,8 @@ pub mod pallet { fn build(&self) { DefaultLimit::::put(self.default_limit); - for (identifier, limit) in &self.limits { - Limits::::insert(identifier, limit.clone()); + for (identifier, context, limit) in &self.limits { + Limits::::insert(identifier, context.clone(), limit.clone()); } } } @@ -230,7 +244,7 @@ pub mod pallet { identifier: &TransactionIdentifier, context: &Option<::LimitContext>, ) -> Result { - let Some(block_span) = Self::resolved_limit(identifier) else { + let Some(block_span) = Self::resolved_limit(identifier, context) else { return Ok(true); }; @@ -246,8 +260,13 @@ pub mod pallet { Ok(true) } - fn resolved_limit(identifier: &TransactionIdentifier) -> Option> { - let limit = Limits::::get(identifier)?; + pub(crate) fn resolved_limit( + identifier: &TransactionIdentifier, + context: &Option<::LimitContext>, + ) -> Option> { + let lookup = Limits::::get(identifier, context.clone()) + .or_else(|| Limits::::get(identifier, None::<::LimitContext>)); + let limit = lookup?; Some(match limit { RateLimit::Default => DefaultLimit::::get(), RateLimit::Exact(block_span) => block_span, @@ -258,18 +277,21 @@ pub mod pallet { pub fn limit_for_call_names( pallet_name: &str, extrinsic_name: &str, + context: Option<::LimitContext>, ) -> Option>> { let identifier = Self::identifier_for_call_names(pallet_name, extrinsic_name)?; - Limits::::get(&identifier) + Limits::::get(&identifier, context.clone()) + .or_else(|| Limits::::get(&identifier, None::<::LimitContext>)) } /// Returns the resolved block span for the specified pallet/extrinsic names, if any. pub fn resolved_limit_for_call_names( pallet_name: &str, extrinsic_name: &str, + context: Option<::LimitContext>, ) -> Option> { let identifier = Self::identifier_for_call_names(pallet_name, extrinsic_name)?; - Self::resolved_limit(&identifier) + Self::resolved_limit(&identifier, &context) } fn identifier_for_call_names( @@ -288,21 +310,24 @@ pub mod pallet { #[pallet::call] impl Pallet { - /// Sets the rate limit configuration for the given call. + /// Sets the rate limit configuration for the given call and optional context. /// /// The supplied `call` is only used to derive the pallet and extrinsic indices; **any - /// arguments embedded in the call are ignored**. + /// arguments embedded in the call are ignored**. The `context` parameter determines which + /// scoped entry is updated (for example a subnet identifier). Passing `None` updates the + /// global entry, which acts as a fallback when no context-specific limit exists. #[pallet::call_index(0)] #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] pub fn set_rate_limit( origin: OriginFor, call: Box<::RuntimeCall>, limit: RateLimit>, + context: Option<::LimitContext>, ) -> DispatchResult { ensure_root(origin)?; let identifier = TransactionIdentifier::from_call::(call.as_ref())?; - Limits::::insert(&identifier, limit); + Limits::::insert(&identifier, context.clone(), limit.clone()); let (pallet_name, extrinsic_name) = identifier.names::()?; let pallet = Vec::from(pallet_name.as_bytes()); @@ -310,6 +335,7 @@ pub mod pallet { Self::deposit_event(Event::RateLimitSet { transaction: identifier, + context, limit, pallet, extrinsic, @@ -321,12 +347,14 @@ pub mod pallet { /// Clears the rate limit for the given call, if present. /// /// The supplied `call` is only used to derive the pallet and extrinsic indices; **any - /// arguments embedded in the call are ignored**. + /// arguments embedded in the call are ignored**. The `context` parameter must match the + /// entry that should be removed (use `None` to remove the global configuration). #[pallet::call_index(1)] #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] pub fn clear_rate_limit( origin: OriginFor, call: Box<::RuntimeCall>, + context: Option<::LimitContext>, ) -> DispatchResult { ensure_root(origin)?; @@ -337,12 +365,13 @@ pub mod pallet { let extrinsic = Vec::from(extrinsic_name.as_bytes()); ensure!( - Limits::::take(&identifier).is_some(), + Limits::::take(&identifier, context.clone()).is_some(), Error::::MissingRateLimit ); Self::deposit_event(Event::RateLimitCleared { transaction: identifier, + context, pallet, extrinsic, }); diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index 5a8d2dd933..ce5b6d25ab 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -6,7 +6,8 @@ use crate::{DefaultLimit, LastSeen, Limits, RateLimit, mock::*, pallet::Error}; fn limit_for_call_names_returns_none_if_not_set() { new_test_ext().execute_with(|| { assert!( - RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit").is_none() + RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", None) + .is_none() ); }); } @@ -17,14 +18,36 @@ fn limit_for_call_names_returns_stored_limit() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::Exact(7)); + Limits::::insert(identifier, None::, RateLimit::Exact(7)); - let fetched = RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit") - .expect("limit should exist"); + let fetched = + RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", None) + .expect("limit should exist"); assert_eq!(fetched, RateLimit::Exact(7)); }); } +#[test] +fn limit_for_call_names_prefers_context_specific_limit() { + new_test_ext().execute_with(|| { + let call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&call); + Limits::::insert(identifier, None::, RateLimit::Exact(3)); + Limits::::insert(identifier, Some(5), RateLimit::Exact(8)); + + let fetched = + RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", Some(5)) + .expect("limit should exist"); + assert_eq!(fetched, RateLimit::Exact(8)); + + let fallback = + RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", Some(1)) + .expect("limit should exist"); + assert_eq!(fallback, RateLimit::Exact(3)); + }); +} + #[test] fn resolved_limit_for_call_names_resolves_default_value() { new_test_ext().execute_with(|| { @@ -32,21 +55,55 @@ fn resolved_limit_for_call_names_resolves_default_value() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::Default); - - let resolved = - RateLimiting::resolved_limit_for_call_names("RateLimiting", "set_default_rate_limit") - .expect("resolved limit"); + Limits::::insert(identifier, None::, RateLimit::Default); + + let resolved = RateLimiting::resolved_limit_for_call_names( + "RateLimiting", + "set_default_rate_limit", + None, + ) + .expect("resolved limit"); assert_eq!(resolved, 3); }); } +#[test] +fn resolved_limit_for_call_names_prefers_context_specific_value() { + new_test_ext().execute_with(|| { + let call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&call); + Limits::::insert(identifier, None::, RateLimit::Exact(4)); + Limits::::insert(identifier, Some(6), RateLimit::Exact(9)); + + let resolved = RateLimiting::resolved_limit_for_call_names( + "RateLimiting", + "set_default_rate_limit", + Some(6), + ) + .expect("resolved limit"); + assert_eq!(resolved, 9); + + let fallback = RateLimiting::resolved_limit_for_call_names( + "RateLimiting", + "set_default_rate_limit", + Some(1), + ) + .expect("resolved limit"); + assert_eq!(fallback, 4); + }); +} + #[test] fn resolved_limit_for_call_names_returns_none_when_unset() { new_test_ext().execute_with(|| { assert!( - RateLimiting::resolved_limit_for_call_names("RateLimiting", "set_default_rate_limit") - .is_none() + RateLimiting::resolved_limit_for_call_names( + "RateLimiting", + "set_default_rate_limit", + None, + ) + .is_none() ); }); } @@ -69,7 +126,7 @@ fn is_within_limit_false_when_rate_limited() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::Exact(5)); + Limits::::insert(identifier, Some(1 as LimitContext), RateLimit::Exact(5)); LastSeen::::insert(identifier, Some(1 as LimitContext), 9); System::set_block_number(13); @@ -86,7 +143,7 @@ fn is_within_limit_true_after_required_span() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::Exact(5)); + Limits::::insert(identifier, Some(2 as LimitContext), RateLimit::Exact(5)); LastSeen::::insert(identifier, Some(2 as LimitContext), 10); System::set_block_number(20); @@ -109,20 +166,26 @@ fn set_rate_limit_updates_storage_and_emits_event() { assert_ok!(RateLimiting::set_rate_limit( RuntimeOrigin::root(), Box::new(target_call.clone()), - limit + limit, + None, )); let identifier = identifier_for(&target_call); - assert_eq!(Limits::::get(identifier), Some(limit)); + assert_eq!( + Limits::::get(identifier, None::), + Some(limit) + ); match pop_last_event() { RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitSet { transaction, + context, limit: emitted_limit, pallet, extrinsic, }) => { assert_eq!(transaction, identifier); + assert_eq!(context, None); assert_eq!(emitted_limit, limit); assert_eq!(pallet, b"RateLimiting".to_vec()); assert_eq!(extrinsic, b"set_default_rate_limit".to_vec()); @@ -132,6 +195,29 @@ fn set_rate_limit_updates_storage_and_emits_event() { }); } +#[test] +fn set_rate_limit_supports_context_specific_limit() { + new_test_ext().execute_with(|| { + let target_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let context = Some(7u16); + assert_ok!(RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + Box::new(target_call.clone()), + RateLimit::Exact(11), + context, + )); + + let identifier = identifier_for(&target_call); + assert_eq!( + Limits::::get(identifier, Some(7)), + Some(RateLimit::Exact(11)) + ); + // global remains untouched + assert_eq!(Limits::::get(identifier, None::), None); + }); +} + #[test] fn set_rate_limit_requires_root() { new_test_ext().execute_with(|| { @@ -142,7 +228,8 @@ fn set_rate_limit_requires_root() { RateLimiting::set_rate_limit( RuntimeOrigin::signed(1), Box::new(target_call), - RateLimit::Exact(1) + RateLimit::Exact(1), + None, ), BadOrigin ); @@ -158,11 +245,15 @@ fn set_rate_limit_accepts_default_variant() { assert_ok!(RateLimiting::set_rate_limit( RuntimeOrigin::root(), Box::new(target_call.clone()), - RateLimit::Default + RateLimit::Default, + None, )); let identifier = identifier_for(&target_call); - assert_eq!(Limits::::get(identifier), Some(RateLimit::Default)); + assert_eq!( + Limits::::get(identifier, None::), + Some(RateLimit::Default) + ); }); } @@ -174,22 +265,25 @@ fn clear_rate_limit_removes_entry_and_emits_event() { let target_call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&target_call); - Limits::::insert(identifier, RateLimit::Exact(4)); + Limits::::insert(identifier, None::, RateLimit::Exact(4)); assert_ok!(RateLimiting::clear_rate_limit( RuntimeOrigin::root(), - Box::new(target_call.clone()) + Box::new(target_call.clone()), + None, )); - assert_eq!(Limits::::get(identifier), None); + assert_eq!(Limits::::get(identifier, None::), None); match pop_last_event() { RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitCleared { transaction, + context, pallet, extrinsic, }) => { assert_eq!(transaction, identifier); + assert_eq!(context, None); assert_eq!(pallet, b"RateLimiting".to_vec()); assert_eq!(extrinsic, b"set_default_rate_limit".to_vec()); } @@ -205,12 +299,49 @@ fn clear_rate_limit_fails_when_missing() { RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); assert_noop!( - RateLimiting::clear_rate_limit(RuntimeOrigin::root(), Box::new(target_call)), + RateLimiting::clear_rate_limit(RuntimeOrigin::root(), Box::new(target_call), None), Error::::MissingRateLimit ); }); } +#[test] +fn clear_rate_limit_removes_only_selected_context() { + new_test_ext().execute_with(|| { + System::reset_events(); + + let target_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&target_call); + Limits::::insert(identifier, None::, RateLimit::Exact(5)); + Limits::::insert(identifier, Some(9), RateLimit::Exact(7)); + + assert_ok!(RateLimiting::clear_rate_limit( + RuntimeOrigin::root(), + Box::new(target_call.clone()), + Some(9), + )); + + assert_eq!(Limits::::get(identifier, Some(9u16)), None); + assert_eq!( + Limits::::get(identifier, None::), + Some(RateLimit::Exact(5)) + ); + + match pop_last_event() { + RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitCleared { + transaction, + context, + .. + }) => { + assert_eq!(transaction, identifier); + assert_eq!(context, Some(9)); + } + other => panic!("unexpected event: {:?}", other), + } + }); +} + #[test] fn set_default_rate_limit_updates_storage_and_emits_event() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index f06e72975e..af02bfa453 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -16,8 +16,8 @@ use scale_info::TypeInfo; use sp_std::{marker::PhantomData, result::Result}; use crate::{ - Config, LastSeen, Limits, Pallet, - types::{RateLimit, RateLimitContextResolver, TransactionIdentifier}, + Config, LastSeen, Pallet, + types::{RateLimitContextResolver, TransactionIdentifier}, }; /// Identifier returned in the transaction metadata for the rate limiting extension. @@ -66,21 +66,16 @@ where Err(_) => return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), }; - let Some(limit) = Limits::::get(&identifier) else { - return Ok((ValidTransaction::default(), None, origin)); - }; + let context = ::ContextResolver::context(call); - let block_span = match limit { - RateLimit::Default => Pallet::::default_limit(), - RateLimit::Exact(block_span) => block_span, + let Some(block_span) = Pallet::::resolved_limit(&identifier, &context) else { + return Ok((ValidTransaction::default(), None, origin)); }; if block_span.is_zero() { return Ok((ValidTransaction::default(), None, origin)); } - let context = ::ContextResolver::context(call); - let within_limit = Pallet::::is_within_limit(&identifier, &context) .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; @@ -213,7 +208,7 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::Exact(5)); + Limits::::insert(identifier, None::, RateLimit::Exact(5)); System::set_block_number(10); @@ -251,7 +246,7 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::Exact(5)); + Limits::::insert(identifier, None::, RateLimit::Exact(5)); LastSeen::::insert(identifier, None::, 20); System::set_block_number(22); @@ -273,7 +268,7 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::Exact(0)); + Limits::::insert(identifier, None::, RateLimit::Exact(0)); System::set_block_number(30); From be9d4f7ffc7a9be1c71cf652285cb4694367d636 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 29 Oct 2025 17:46:43 +0300 Subject: [PATCH 12/95] Make rate limiting pallet instantiable --- pallets/rate-limiting/src/benchmarking.rs | 19 ++-- pallets/rate-limiting/src/lib.rs | 98 +++++++++++---------- pallets/rate-limiting/src/mock.rs | 2 +- pallets/rate-limiting/src/tests.rs | 52 ++++++----- pallets/rate-limiting/src/tx_extension.rs | 102 +++++++++++++++------- pallets/rate-limiting/src/types.rs | 36 ++++---- 6 files changed, 184 insertions(+), 125 deletions(-) diff --git a/pallets/rate-limiting/src/benchmarking.rs b/pallets/rate-limiting/src/benchmarking.rs index bf4a3b37b5..8392109772 100644 --- a/pallets/rate-limiting/src/benchmarking.rs +++ b/pallets/rate-limiting/src/benchmarking.rs @@ -36,13 +36,16 @@ mod benchmarks { fn set_rate_limit() { let call = sample_call::(); let limit = RateLimit::>::Exact(BlockNumberFor::::from(10u32)); - let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); - let context = ::ContextResolver::context(call.as_ref()); + let identifier = + TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); #[extrinsic_call] _(RawOrigin::Root, call, limit.clone(), None); - assert_eq!(Limits::::get(&identifier, context), Some(limit)); + assert_eq!( + Limits::::get(&identifier, None::<::LimitContext>), + Some(limit) + ); } #[benchmark] @@ -51,14 +54,14 @@ mod benchmarks { let limit = RateLimit::>::Exact(BlockNumberFor::::from(10u32)); // Pre-populate limit for benchmark call - let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); - let context = ::ContextResolver::context(call.as_ref()); - Limits::::insert(identifier, context.clone(), limit); + let identifier = + TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); + Limits::::insert(identifier, None::<::LimitContext>, limit); #[extrinsic_call] _(RawOrigin::Root, call, None); - assert!(Limits::::get(identifier, context).is_none()); + assert!(Limits::::get(identifier, None::<::LimitContext>).is_none()); } #[benchmark] @@ -68,7 +71,7 @@ mod benchmarks { #[extrinsic_call] _(RawOrigin::Root, block_span); - assert_eq!(DefaultLimit::::get(), block_span); + assert_eq!(DefaultLimit::::get(), block_span); } impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 23eff3cf9a..2ca3ab87ba 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -25,6 +25,9 @@ //! - [`DefaultLimit`](pallet::DefaultLimit): `BlockNumber` //! - [`LastSeen`](pallet::LastSeen): `(TransactionIdentifier, Option) → BlockNumber` //! +//! Each storage map is namespaced by pallet instance; runtimes can deploy multiple independent +//! instances to manage distinct rate-limiting scopes. +//! //! # Transaction extension //! //! Enforcement happens via [`RateLimitTransactionExtension`], which implements @@ -100,7 +103,7 @@ pub mod pallet { traits::{BuildGenesisConfig, GetCallMetadata}, }; use frame_system::{ensure_root, pallet_prelude::*}; - use sp_std::{convert::TryFrom, vec::Vec}; + use sp_std::{convert::TryFrom, marker::PhantomData, vec::Vec}; #[cfg(feature = "runtime-benchmarks")] use crate::benchmarking::BenchmarkHelper as BenchmarkHelperTrait; @@ -108,7 +111,7 @@ pub mod pallet { /// Configuration trait for the rate limiting pallet. #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config { /// The overarching runtime call type. type RuntimeCall: Parameter + Codec @@ -119,22 +122,22 @@ pub mod pallet { type LimitContext: Parameter + Clone + PartialEq + Eq + MaybeSerializeDeserialize; /// Resolves the context for a given runtime call. - type ContextResolver: RateLimitContextResolver<::RuntimeCall, Self::LimitContext>; + type ContextResolver: RateLimitContextResolver<>::RuntimeCall, Self::LimitContext>; /// Helper used to construct runtime calls for benchmarking. #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper: BenchmarkHelperTrait<::RuntimeCall>; + type BenchmarkHelper: BenchmarkHelperTrait<>::RuntimeCall>; } /// Storage mapping from transaction identifier and optional context to its configured rate limit. #[pallet::storage] #[pallet::getter(fn limits)] - pub type Limits = StorageDoubleMap< + pub type Limits, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, TransactionIdentifier, Blake2_128Concat, - Option<::LimitContext>, + Option<>::LimitContext>, RateLimit>, OptionQuery, >; @@ -143,12 +146,12 @@ pub mod pallet { /// /// The second key is `None` for global limits and `Some(context)` for contextual limits. #[pallet::storage] - pub type LastSeen = StorageDoubleMap< + pub type LastSeen, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, TransactionIdentifier, Blake2_128Concat, - Option<::LimitContext>, + Option<>::LimitContext>, BlockNumberFor, OptionQuery, >; @@ -156,18 +159,19 @@ pub mod pallet { /// Default block span applied when an extrinsic uses the default rate limit. #[pallet::storage] #[pallet::getter(fn default_limit)] - pub type DefaultLimit = StorageValue<_, BlockNumberFor, ValueQuery>; + pub type DefaultLimit, I: 'static = ()> = + StorageValue<_, BlockNumberFor, ValueQuery>; /// Events emitted by the rate limiting pallet. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { + pub enum Event, I: 'static = ()> { /// A rate limit was set or updated. RateLimitSet { /// Identifier of the affected transaction. transaction: TransactionIdentifier, /// Context to which the limit applies, if any. - context: Option<::LimitContext>, + context: Option<>::LimitContext>, /// The new limit configuration applied to the transaction. limit: RateLimit>, /// Pallet name associated with the transaction. @@ -180,7 +184,7 @@ pub mod pallet { /// Identifier of the affected transaction. transaction: TransactionIdentifier, /// Context from which the limit was cleared, if any. - context: Option<::LimitContext>, + context: Option<>::LimitContext>, /// Pallet name associated with the transaction. pallet: Vec, /// Extrinsic name associated with the transaction. @@ -195,7 +199,7 @@ pub mod pallet { /// Errors that can occur while configuring rate limits. #[pallet::error] - pub enum Error { + pub enum Error { /// Failed to extract the pallet and extrinsic indices from the call. InvalidRuntimeCall, /// Attempted to remove a limit that is not present. @@ -203,17 +207,17 @@ pub mod pallet { } #[pallet::genesis_config] - pub struct GenesisConfig { + pub struct GenesisConfig, I: 'static = ()> { pub default_limit: BlockNumberFor, pub limits: Vec<( TransactionIdentifier, - Option<::LimitContext>, + Option<>::LimitContext>, RateLimit>, )>, } #[cfg(feature = "std")] - impl Default for GenesisConfig { + impl, I: 'static> Default for GenesisConfig { fn default() -> Self { Self { default_limit: Zero::zero(), @@ -223,26 +227,26 @@ pub mod pallet { } #[pallet::genesis_build] - impl BuildGenesisConfig for GenesisConfig { + impl, I: 'static> BuildGenesisConfig for GenesisConfig { fn build(&self) { - DefaultLimit::::put(self.default_limit); + DefaultLimit::::put(self.default_limit); for (identifier, context, limit) in &self.limits { - Limits::::insert(identifier, context.clone(), limit.clone()); + Limits::::insert(identifier, context.clone(), limit.clone()); } } } #[pallet::pallet] #[pallet::without_storage_info] - pub struct Pallet(_); + pub struct Pallet(PhantomData<(T, I)>); - impl Pallet { + impl, I: 'static> Pallet { /// Returns `true` when the given transaction identifier passes its configured rate limit /// within the provided context. pub fn is_within_limit( identifier: &TransactionIdentifier, - context: &Option<::LimitContext>, + context: &Option<>::LimitContext>, ) -> Result { let Some(block_span) = Self::resolved_limit(identifier, context) else { return Ok(true); @@ -250,7 +254,7 @@ pub mod pallet { let current = frame_system::Pallet::::block_number(); - if let Some(last) = LastSeen::::get(identifier, context) { + if let Some(last) = LastSeen::::get(identifier, context) { let delta = current.saturating_sub(last); if delta < block_span { return Ok(false); @@ -262,13 +266,14 @@ pub mod pallet { pub(crate) fn resolved_limit( identifier: &TransactionIdentifier, - context: &Option<::LimitContext>, + context: &Option<>::LimitContext>, ) -> Option> { - let lookup = Limits::::get(identifier, context.clone()) - .or_else(|| Limits::::get(identifier, None::<::LimitContext>)); + let lookup = Limits::::get(identifier, context).or_else(|| { + Limits::::get(identifier, None::<>::LimitContext>) + }); let limit = lookup?; Some(match limit { - RateLimit::Default => DefaultLimit::::get(), + RateLimit::Default => DefaultLimit::::get(), RateLimit::Exact(block_span) => block_span, }) } @@ -277,18 +282,19 @@ pub mod pallet { pub fn limit_for_call_names( pallet_name: &str, extrinsic_name: &str, - context: Option<::LimitContext>, + context: Option<>::LimitContext>, ) -> Option>> { let identifier = Self::identifier_for_call_names(pallet_name, extrinsic_name)?; - Limits::::get(&identifier, context.clone()) - .or_else(|| Limits::::get(&identifier, None::<::LimitContext>)) + Limits::::get(&identifier, context.clone()).or_else(|| { + Limits::::get(&identifier, None::<>::LimitContext>) + }) } /// Returns the resolved block span for the specified pallet/extrinsic names, if any. pub fn resolved_limit_for_call_names( pallet_name: &str, extrinsic_name: &str, - context: Option<::LimitContext>, + context: Option<>::LimitContext>, ) -> Option> { let identifier = Self::identifier_for_call_names(pallet_name, extrinsic_name)?; Self::resolved_limit(&identifier, &context) @@ -298,9 +304,9 @@ pub mod pallet { pallet_name: &str, extrinsic_name: &str, ) -> Option { - let modules = ::RuntimeCall::get_module_names(); + let modules = >::RuntimeCall::get_module_names(); let pallet_pos = modules.iter().position(|name| *name == pallet_name)?; - let call_names = ::RuntimeCall::get_call_names(pallet_name); + let call_names = >::RuntimeCall::get_call_names(pallet_name); let extrinsic_pos = call_names.iter().position(|name| *name == extrinsic_name)?; let pallet_index = u8::try_from(pallet_pos).ok()?; let extrinsic_index = u8::try_from(extrinsic_pos).ok()?; @@ -309,7 +315,7 @@ pub mod pallet { } #[pallet::call] - impl Pallet { + impl, I: 'static> Pallet { /// Sets the rate limit configuration for the given call and optional context. /// /// The supplied `call` is only used to derive the pallet and extrinsic indices; **any @@ -320,16 +326,16 @@ pub mod pallet { #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] pub fn set_rate_limit( origin: OriginFor, - call: Box<::RuntimeCall>, + call: Box<>::RuntimeCall>, limit: RateLimit>, - context: Option<::LimitContext>, + context: Option<>::LimitContext>, ) -> DispatchResult { ensure_root(origin)?; - let identifier = TransactionIdentifier::from_call::(call.as_ref())?; - Limits::::insert(&identifier, context.clone(), limit.clone()); + let identifier = TransactionIdentifier::from_call::(call.as_ref())?; + Limits::::insert(&identifier, context.clone(), limit.clone()); - let (pallet_name, extrinsic_name) = identifier.names::()?; + let (pallet_name, extrinsic_name) = identifier.names::()?; let pallet = Vec::from(pallet_name.as_bytes()); let extrinsic = Vec::from(extrinsic_name.as_bytes()); @@ -353,20 +359,20 @@ pub mod pallet { #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] pub fn clear_rate_limit( origin: OriginFor, - call: Box<::RuntimeCall>, - context: Option<::LimitContext>, + call: Box<>::RuntimeCall>, + context: Option<>::LimitContext>, ) -> DispatchResult { ensure_root(origin)?; - let identifier = TransactionIdentifier::from_call::(call.as_ref())?; + let identifier = TransactionIdentifier::from_call::(call.as_ref())?; - let (pallet_name, extrinsic_name) = identifier.names::()?; + let (pallet_name, extrinsic_name) = identifier.names::()?; let pallet = Vec::from(pallet_name.as_bytes()); let extrinsic = Vec::from(extrinsic_name.as_bytes()); ensure!( - Limits::::take(&identifier, context.clone()).is_some(), - Error::::MissingRateLimit + Limits::::take(&identifier, context.clone()).is_some(), + Error::::MissingRateLimit ); Self::deposit_event(Event::RateLimitCleared { @@ -388,7 +394,7 @@ pub mod pallet { ) -> DispatchResult { ensure_root(origin)?; - DefaultLimit::::put(block_span); + DefaultLimit::::put(block_span); Self::deposit_event(Event::DefaultRateLimitSet { block_span }); diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index 4218e757a1..b80862edd5 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -95,7 +95,7 @@ pub fn new_test_ext() -> TestExternalities { } pub(crate) fn identifier_for(call: &RuntimeCall) -> TransactionIdentifier { - TransactionIdentifier::from_call::(call).expect("identifier for call") + TransactionIdentifier::from_call::(call).expect("identifier for call") } pub(crate) fn pop_last_event() -> RuntimeEvent { diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index ce5b6d25ab..62aae069db 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -18,7 +18,7 @@ fn limit_for_call_names_returns_stored_limit() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(7)); + Limits::::insert(identifier, None::, RateLimit::Exact(7)); let fetched = RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", None) @@ -33,8 +33,8 @@ fn limit_for_call_names_prefers_context_specific_limit() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(3)); - Limits::::insert(identifier, Some(5), RateLimit::Exact(8)); + Limits::::insert(identifier, None::, RateLimit::Exact(3)); + Limits::::insert(identifier, Some(5), RateLimit::Exact(8)); let fetched = RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", Some(5)) @@ -51,11 +51,11 @@ fn limit_for_call_names_prefers_context_specific_limit() { #[test] fn resolved_limit_for_call_names_resolves_default_value() { new_test_ext().execute_with(|| { - DefaultLimit::::put(3); + DefaultLimit::::put(3); let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Default); + Limits::::insert(identifier, None::, RateLimit::Default); let resolved = RateLimiting::resolved_limit_for_call_names( "RateLimiting", @@ -73,8 +73,8 @@ fn resolved_limit_for_call_names_prefers_context_specific_value() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(4)); - Limits::::insert(identifier, Some(6), RateLimit::Exact(9)); + Limits::::insert(identifier, None::, RateLimit::Exact(4)); + Limits::::insert(identifier, Some(6), RateLimit::Exact(9)); let resolved = RateLimiting::resolved_limit_for_call_names( "RateLimiting", @@ -126,8 +126,8 @@ fn is_within_limit_false_when_rate_limited() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, Some(1 as LimitContext), RateLimit::Exact(5)); - LastSeen::::insert(identifier, Some(1 as LimitContext), 9); + Limits::::insert(identifier, Some(1 as LimitContext), RateLimit::Exact(5)); + LastSeen::::insert(identifier, Some(1 as LimitContext), 9); System::set_block_number(13); @@ -143,8 +143,8 @@ fn is_within_limit_true_after_required_span() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, Some(2 as LimitContext), RateLimit::Exact(5)); - LastSeen::::insert(identifier, Some(2 as LimitContext), 10); + Limits::::insert(identifier, Some(2 as LimitContext), RateLimit::Exact(5)); + LastSeen::::insert(identifier, Some(2 as LimitContext), 10); System::set_block_number(20); @@ -172,7 +172,7 @@ fn set_rate_limit_updates_storage_and_emits_event() { let identifier = identifier_for(&target_call); assert_eq!( - Limits::::get(identifier, None::), + Limits::::get(identifier, None::), Some(limit) ); @@ -210,11 +210,14 @@ fn set_rate_limit_supports_context_specific_limit() { let identifier = identifier_for(&target_call); assert_eq!( - Limits::::get(identifier, Some(7)), + Limits::::get(identifier, Some(7)), Some(RateLimit::Exact(11)) ); // global remains untouched - assert_eq!(Limits::::get(identifier, None::), None); + assert_eq!( + Limits::::get(identifier, None::), + None + ); }); } @@ -251,7 +254,7 @@ fn set_rate_limit_accepts_default_variant() { let identifier = identifier_for(&target_call); assert_eq!( - Limits::::get(identifier, None::), + Limits::::get(identifier, None::), Some(RateLimit::Default) ); }); @@ -265,7 +268,7 @@ fn clear_rate_limit_removes_entry_and_emits_event() { let target_call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&target_call); - Limits::::insert(identifier, None::, RateLimit::Exact(4)); + Limits::::insert(identifier, None::, RateLimit::Exact(4)); assert_ok!(RateLimiting::clear_rate_limit( RuntimeOrigin::root(), @@ -273,7 +276,10 @@ fn clear_rate_limit_removes_entry_and_emits_event() { None, )); - assert_eq!(Limits::::get(identifier, None::), None); + assert_eq!( + Limits::::get(identifier, None::), + None + ); match pop_last_event() { RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitCleared { @@ -300,7 +306,7 @@ fn clear_rate_limit_fails_when_missing() { assert_noop!( RateLimiting::clear_rate_limit(RuntimeOrigin::root(), Box::new(target_call), None), - Error::::MissingRateLimit + Error::::MissingRateLimit ); }); } @@ -313,8 +319,8 @@ fn clear_rate_limit_removes_only_selected_context() { let target_call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&target_call); - Limits::::insert(identifier, None::, RateLimit::Exact(5)); - Limits::::insert(identifier, Some(9), RateLimit::Exact(7)); + Limits::::insert(identifier, None::, RateLimit::Exact(5)); + Limits::::insert(identifier, Some(9), RateLimit::Exact(7)); assert_ok!(RateLimiting::clear_rate_limit( RuntimeOrigin::root(), @@ -322,9 +328,9 @@ fn clear_rate_limit_removes_only_selected_context() { Some(9), )); - assert_eq!(Limits::::get(identifier, Some(9u16)), None); + assert_eq!(Limits::::get(identifier, Some(9u16)), None); assert_eq!( - Limits::::get(identifier, None::), + Limits::::get(identifier, None::), Some(RateLimit::Exact(5)) ); @@ -352,7 +358,7 @@ fn set_default_rate_limit_updates_storage_and_emits_event() { 42 )); - assert_eq!(DefaultLimit::::get(), 42); + assert_eq!(DefaultLimit::::get(), 42); match pop_last_event() { RuntimeEvent::RateLimiting(crate::pallet::Event::DefaultRateLimitSet { diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index af02bfa453..119ad9c707 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -27,48 +27,90 @@ const IDENTIFIER: &str = "RateLimitTransactionExtension"; const RATE_LIMIT_DENIED: u8 = 1; /// Transaction extension that enforces pallet rate limiting rules. -#[derive(Default, Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)] -pub struct RateLimitTransactionExtension(PhantomData); +#[derive(Default, Encode, Decode, DecodeWithMemTracking, TypeInfo)] +pub struct RateLimitTransactionExtension(PhantomData<(T, I)>) +where + T: Config + Send + Sync + TypeInfo, + I: 'static + TypeInfo; + +impl Clone for RateLimitTransactionExtension +where + T: Config + Send + Sync + TypeInfo, + I: 'static + TypeInfo, +{ + fn clone(&self) -> Self { + Self(PhantomData) + } +} + +impl PartialEq for RateLimitTransactionExtension +where + T: Config + Send + Sync + TypeInfo, + I: 'static + TypeInfo, +{ + fn eq(&self, _other: &Self) -> bool { + true + } +} -impl core::fmt::Debug for RateLimitTransactionExtension { +impl Eq for RateLimitTransactionExtension +where + T: Config + Send + Sync + TypeInfo, + I: 'static + TypeInfo, +{ +} + +impl core::fmt::Debug for RateLimitTransactionExtension +where + T: Config + Send + Sync + TypeInfo, + I: 'static + TypeInfo, +{ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(IDENTIFIER) } } -impl TransactionExtension<::RuntimeCall> for RateLimitTransactionExtension +impl TransactionExtension<>::RuntimeCall> + for RateLimitTransactionExtension where - T: Config + Send + Sync + TypeInfo, - ::RuntimeCall: Dispatchable, + T: Config + Send + Sync + TypeInfo, + I: 'static + TypeInfo + Send + Sync, + >::RuntimeCall: Dispatchable, { const IDENTIFIER: &'static str = IDENTIFIER; type Implicit = (); - type Val = Option<(TransactionIdentifier, Option)>; - type Pre = Option<(TransactionIdentifier, Option)>; - - fn weight(&self, _call: &::RuntimeCall) -> Weight { + type Val = Option<( + TransactionIdentifier, + Option<>::LimitContext>, + )>; + type Pre = Option<( + TransactionIdentifier, + Option<>::LimitContext>, + )>; + + fn weight(&self, _call: &>::RuntimeCall) -> Weight { Weight::zero() } fn validate( &self, - origin: DispatchOriginOf<::RuntimeCall>, - call: &::RuntimeCall, - _info: &DispatchInfoOf<::RuntimeCall>, + origin: DispatchOriginOf<>::RuntimeCall>, + call: &>::RuntimeCall, + _info: &DispatchInfoOf<>::RuntimeCall>, _len: usize, _self_implicit: Self::Implicit, _inherited_implication: &impl Implication, _source: TransactionSource, - ) -> ValidateResult::RuntimeCall> { - let identifier = match TransactionIdentifier::from_call::(call) { + ) -> ValidateResult>::RuntimeCall> { + let identifier = match TransactionIdentifier::from_call::(call) { Ok(identifier) => identifier, Err(_) => return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), }; - let context = ::ContextResolver::context(call); + let context = >::ContextResolver::context(call); - let Some(block_span) = Pallet::::resolved_limit(&identifier, &context) else { + let Some(block_span) = Pallet::::resolved_limit(&identifier, &context) else { return Ok((ValidTransaction::default(), None, origin)); }; @@ -76,7 +118,7 @@ where return Ok((ValidTransaction::default(), None, origin)); } - let within_limit = Pallet::::is_within_limit(&identifier, &context) + let within_limit = Pallet::::is_within_limit(&identifier, &context) .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; if !within_limit { @@ -95,9 +137,9 @@ where fn prepare( self, val: Self::Val, - _origin: &DispatchOriginOf<::RuntimeCall>, - _call: &::RuntimeCall, - _info: &DispatchInfoOf<::RuntimeCall>, + _origin: &DispatchOriginOf<>::RuntimeCall>, + _call: &>::RuntimeCall, + _info: &DispatchInfoOf<>::RuntimeCall>, _len: usize, ) -> Result { Ok(val) @@ -105,7 +147,7 @@ where fn post_dispatch( pre: Self::Pre, - _info: &DispatchInfoOf<::RuntimeCall>, + _info: &DispatchInfoOf<>::RuntimeCall>, _post_info: &mut PostDispatchInfo, _len: usize, result: &DispatchResult, @@ -113,7 +155,7 @@ where if result.is_ok() { if let Some((identifier, context)) = pre { let block_number = frame_system::Pallet::::block_number(); - LastSeen::::insert(&identifier, context, block_number); + LastSeen::::insert(&identifier, context, block_number); } } Ok(()) @@ -196,7 +238,7 @@ mod tests { let identifier = identifier_for(&call); assert_eq!( - LastSeen::::get(identifier, None::), + LastSeen::::get(identifier, None::), None ); }); @@ -208,7 +250,7 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(5)); + Limits::::insert(identifier, None::, RateLimit::Exact(5)); System::set_block_number(10); @@ -234,7 +276,7 @@ mod tests { .expect("post_dispatch succeeds"); assert_eq!( - LastSeen::::get(identifier, None::), + LastSeen::::get(identifier, None::), Some(10) ); }); @@ -246,8 +288,8 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(5)); - LastSeen::::insert(identifier, None::, 20); + Limits::::insert(identifier, None::, RateLimit::Exact(5)); + LastSeen::::insert(identifier, None::, 20); System::set_block_number(22); @@ -268,7 +310,7 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(0)); + Limits::::insert(identifier, None::, RateLimit::Exact(0)); System::set_block_number(30); @@ -294,7 +336,7 @@ mod tests { .expect("post_dispatch succeeds"); assert_eq!( - LastSeen::::get(identifier, None::), + LastSeen::::get(identifier, None::), None ); }); diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 72e2a43777..7d53a34ac6 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -40,38 +40,40 @@ impl TransactionIdentifier { } /// Returns the pallet and extrinsic names associated with this identifier. - pub fn names(&self) -> Result<(&'static str, &'static str), DispatchError> + pub fn names(&self) -> Result<(&'static str, &'static str), DispatchError> where - T: crate::pallet::Config, - ::RuntimeCall: GetCallMetadata, + T: crate::pallet::Config, + I: 'static, + >::RuntimeCall: GetCallMetadata, { - let modules = ::RuntimeCall::get_module_names(); + let modules = >::RuntimeCall::get_module_names(); let pallet_name = modules .get(self.pallet_index as usize) .copied() - .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; - let call_names = ::RuntimeCall::get_call_names(pallet_name); + .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; + let call_names = >::RuntimeCall::get_call_names(pallet_name); let extrinsic_name = call_names .get(self.extrinsic_index as usize) .copied() - .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; + .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; Ok((pallet_name, extrinsic_name)) } /// Builds an identifier from a runtime call by extracting pallet/extrinsic indices. - pub fn from_call( - call: &::RuntimeCall, + pub fn from_call( + call: &>::RuntimeCall, ) -> Result where - T: crate::pallet::Config, + T: crate::pallet::Config, + I: 'static, { call.using_encoded(|encoded| { let pallet_index = *encoded .get(0) - .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; + .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; let extrinsic_index = *encoded .get(1) - .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; + .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; Ok(TransactionIdentifier::new(pallet_index, extrinsic_index)) }) } @@ -110,7 +112,7 @@ mod tests { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); + let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); // System is the first pallet in the mock runtime, RateLimiting is second. assert_eq!(identifier.pallet_index, 1); @@ -122,9 +124,9 @@ mod tests { fn transaction_identifier_names_matches_call_metadata() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); + let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); - let (pallet, extrinsic) = identifier.names::().expect("call metadata"); + let (pallet, extrinsic) = identifier.names::().expect("call metadata"); assert_eq!(pallet, "RateLimiting"); assert_eq!(extrinsic, "set_default_rate_limit"); } @@ -133,8 +135,8 @@ mod tests { fn transaction_identifier_names_error_for_unknown_indices() { let identifier = TransactionIdentifier::new(99, 0); - let err = identifier.names::().expect_err("should fail"); - let expected: DispatchError = Error::::InvalidRuntimeCall.into(); + let err = identifier.names::().expect_err("should fail"); + let expected: DispatchError = Error::::InvalidRuntimeCall.into(); assert_eq!(err, expected); } } From 61eb4d286d6621d21e428841f6947cc00d3d77d7 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 30 Oct 2025 15:56:19 +0300 Subject: [PATCH 13/95] Refactor RateLimit type --- pallets/rate-limiting/runtime-api/src/lib.rs | 5 +- pallets/rate-limiting/src/benchmarking.rs | 12 +- pallets/rate-limiting/src/lib.rs | 143 ++++++++++++------- pallets/rate-limiting/src/mock.rs | 9 +- pallets/rate-limiting/src/tests.rs | 120 ++++++++++------ pallets/rate-limiting/src/tx_extension.rs | 11 +- pallets/rate-limiting/src/types.rs | 76 +++++++++- 7 files changed, 263 insertions(+), 113 deletions(-) diff --git a/pallets/rate-limiting/runtime-api/src/lib.rs b/pallets/rate-limiting/runtime-api/src/lib.rs index 1a32c094ea..98b55e9a26 100644 --- a/pallets/rate-limiting/runtime-api/src/lib.rs +++ b/pallets/rate-limiting/runtime-api/src/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std)] use codec::{Decode, Encode}; -use pallet_rate_limiting::RateLimit; +use pallet_rate_limiting::RateLimitKind; use scale_info::TypeInfo; use sp_std::vec::Vec; use subtensor_runtime_common::BlockNumber; @@ -12,7 +12,8 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] pub struct RateLimitRpcResponse { - pub limit: Option>, + pub global: Option>, + pub contextual: Vec<(Vec, RateLimitKind)>, pub default_limit: BlockNumber, pub resolved: Option, } diff --git a/pallets/rate-limiting/src/benchmarking.rs b/pallets/rate-limiting/src/benchmarking.rs index 8392109772..4c9ce17708 100644 --- a/pallets/rate-limiting/src/benchmarking.rs +++ b/pallets/rate-limiting/src/benchmarking.rs @@ -35,7 +35,7 @@ mod benchmarks { #[benchmark] fn set_rate_limit() { let call = sample_call::(); - let limit = RateLimit::>::Exact(BlockNumberFor::::from(10u32)); + let limit = RateLimitKind::>::Exact(BlockNumberFor::::from(10u32)); let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); @@ -43,25 +43,25 @@ mod benchmarks { _(RawOrigin::Root, call, limit.clone(), None); assert_eq!( - Limits::::get(&identifier, None::<::LimitContext>), - Some(limit) + Limits::::get(&identifier), + Some(RateLimit::global(limit)) ); } #[benchmark] fn clear_rate_limit() { let call = sample_call::(); - let limit = RateLimit::>::Exact(BlockNumberFor::::from(10u32)); + let limit = RateLimitKind::>::Exact(BlockNumberFor::::from(10u32)); // Pre-populate limit for benchmark call let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); - Limits::::insert(identifier, None::<::LimitContext>, limit); + Limits::::insert(identifier, RateLimit::global(limit)); #[extrinsic_call] _(RawOrigin::Root, call, None); - assert!(Limits::::get(identifier, None::<::LimitContext>).is_none()); + assert!(Limits::::get(identifier).is_none()); } #[benchmark] diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 2ca3ab87ba..cf0ebeefb6 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -6,24 +6,20 @@ //! //! `pallet-rate-limiting` lets a runtime restrict how frequently particular calls can execute. //! Limits are stored on-chain, keyed by the call's pallet/variant pair. Each entry can specify an -//! exact block span or defer to a configured default. The pallet exposes three roots-only -//! extrinsics to manage this data: +//! exact block span or defer to a configured default. The pallet exposes three extrinsics, +//! restricted by [`Config::AdminOrigin`], to manage this data: //! -//! - [`set_rate_limit`](pallet::Pallet::set_rate_limit): assign a limit to an extrinsic, either as -//! `RateLimit::Exact(blocks)` or `RateLimit::Default`. The optional context parameter lets you -//! scope the configuration to a particular subnet/key/account while keeping a global fallback. +//! - [`set_rate_limit`](pallet::Pallet::set_rate_limit): assign a limit to an extrinsic by +//! supplying a [`RateLimitKind`] span and optionally a contextual identifier. When a contextual +//! span is stored, any previously configured global span is replaced. //! - [`clear_rate_limit`](pallet::Pallet::clear_rate_limit): remove a stored limit for the provided -//! context (or for the global entry when `None` is supplied). +//! scope (either the global entry when `None` is supplied, or a specific context). //! - [`set_default_rate_limit`](pallet::Pallet::set_default_rate_limit): set the global default -//! block span used by `RateLimit::Default` entries. +//! block span used by `RateLimitKind::Default` entries. //! //! The pallet also tracks the last block in which a rate-limited call was executed, per optional //! *context*. Context allows one limit definition (for example, “set weights”) to be enforced per -//! subnet, account, or other grouping chosen by the runtime. The storage layout is: -//! -//! - [`Limits`](pallet::Limits): `TransactionIdentifier → RateLimit` -//! - [`DefaultLimit`](pallet::DefaultLimit): `BlockNumber` -//! - [`LastSeen`](pallet::LastSeen): `(TransactionIdentifier, Option) → BlockNumber` +//! subnet, account, or other grouping chosen by the runtime. //! //! Each storage map is namespaced by pallet instance; runtimes can deploy multiple independent //! instances to manage distinct rate-limiting scopes. @@ -74,6 +70,7 @@ //! type RuntimeCall = RuntimeCall; //! type LimitContext = NetUid; //! type ContextResolver = WeightsContextResolver; +//! type AdminOrigin = frame_system::EnsureRoot; //! } //! ``` @@ -81,7 +78,7 @@ pub use benchmarking::BenchmarkHelper; pub use pallet::*; pub use tx_extension::RateLimitTransactionExtension; -pub use types::{RateLimit, RateLimitContextResolver, TransactionIdentifier}; +pub use types::{RateLimit, RateLimitContextResolver, RateLimitKind, TransactionIdentifier}; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; @@ -100,26 +97,32 @@ pub mod pallet { use frame_support::{ pallet_prelude::*, sp_runtime::traits::{Saturating, Zero}, - traits::{BuildGenesisConfig, GetCallMetadata}, + traits::{BuildGenesisConfig, EnsureOrigin, GetCallMetadata}, }; - use frame_system::{ensure_root, pallet_prelude::*}; + use frame_system::pallet_prelude::*; use sp_std::{convert::TryFrom, marker::PhantomData, vec::Vec}; #[cfg(feature = "runtime-benchmarks")] use crate::benchmarking::BenchmarkHelper as BenchmarkHelperTrait; - use crate::types::{RateLimit, RateLimitContextResolver, TransactionIdentifier}; + use crate::types::{RateLimit, RateLimitContextResolver, RateLimitKind, TransactionIdentifier}; /// Configuration trait for the rate limiting pallet. #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config + where + BlockNumberFor: MaybeSerializeDeserialize, + { /// The overarching runtime call type. type RuntimeCall: Parameter + Codec + GetCallMetadata + IsType<::RuntimeCall>; + /// Origin permitted to configure rate limits. + type AdminOrigin: EnsureOrigin>; + /// Context type used for contextual (per-group) rate limits. - type LimitContext: Parameter + Clone + PartialEq + Eq + MaybeSerializeDeserialize; + type LimitContext: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize; /// Resolves the context for a given runtime call. type ContextResolver: RateLimitContextResolver<>::RuntimeCall, Self::LimitContext>; @@ -129,16 +132,14 @@ pub mod pallet { type BenchmarkHelper: BenchmarkHelperTrait<>::RuntimeCall>; } - /// Storage mapping from transaction identifier and optional context to its configured rate limit. + /// Storage mapping from transaction identifier to its configured rate limit. #[pallet::storage] #[pallet::getter(fn limits)] - pub type Limits, I: 'static = ()> = StorageDoubleMap< + pub type Limits, I: 'static = ()> = StorageMap< _, Blake2_128Concat, TransactionIdentifier, - Blake2_128Concat, - Option<>::LimitContext>, - RateLimit>, + RateLimit<>::LimitContext, BlockNumberFor>, OptionQuery, >; @@ -172,8 +173,8 @@ pub mod pallet { transaction: TransactionIdentifier, /// Context to which the limit applies, if any. context: Option<>::LimitContext>, - /// The new limit configuration applied to the transaction. - limit: RateLimit>, + /// The rate limit policy applied to the transaction. + limit: RateLimitKind>, /// Pallet name associated with the transaction. pallet: Vec, /// Extrinsic name associated with the transaction. @@ -204,6 +205,8 @@ pub mod pallet { InvalidRuntimeCall, /// Attempted to remove a limit that is not present. MissingRateLimit, + /// Contextual configuration was requested but no context can be resolved for the call. + ContextUnavailable, } #[pallet::genesis_config] @@ -212,7 +215,7 @@ pub mod pallet { pub limits: Vec<( TransactionIdentifier, Option<>::LimitContext>, - RateLimit>, + RateLimitKind>, )>, } @@ -231,8 +234,19 @@ pub mod pallet { fn build(&self) { DefaultLimit::::put(self.default_limit); - for (identifier, context, limit) in &self.limits { - Limits::::insert(identifier, context.clone(), limit.clone()); + for (identifier, context, kind) in &self.limits { + Limits::::mutate(identifier, |entry| match context { + None => { + *entry = Some(RateLimit::global(*kind)); + } + Some(ctx) => { + if let Some(config) = entry { + config.upsert_context(ctx.clone(), *kind); + } else { + *entry = Some(RateLimit::contextual_single(ctx.clone(), *kind)); + } + } + }); } } } @@ -268,13 +282,11 @@ pub mod pallet { identifier: &TransactionIdentifier, context: &Option<>::LimitContext>, ) -> Option> { - let lookup = Limits::::get(identifier, context).or_else(|| { - Limits::::get(identifier, None::<>::LimitContext>) - }); - let limit = lookup?; - Some(match limit { - RateLimit::Default => DefaultLimit::::get(), - RateLimit::Exact(block_span) => block_span, + let config = Limits::::get(identifier)?; + let kind = config.kind_for(context.as_ref())?; + Some(match *kind { + RateLimitKind::Default => DefaultLimit::::get(), + RateLimitKind::Exact(block_span) => block_span, }) } @@ -283,11 +295,10 @@ pub mod pallet { pallet_name: &str, extrinsic_name: &str, context: Option<>::LimitContext>, - ) -> Option>> { + ) -> Option>> { let identifier = Self::identifier_for_call_names(pallet_name, extrinsic_name)?; - Limits::::get(&identifier, context.clone()).or_else(|| { - Limits::::get(&identifier, None::<>::LimitContext>) - }) + Limits::::get(&identifier) + .and_then(|config| config.kind_for(context.as_ref()).copied()) } /// Returns the resolved block span for the specified pallet/extrinsic names, if any. @@ -327,13 +338,28 @@ pub mod pallet { pub fn set_rate_limit( origin: OriginFor, call: Box<>::RuntimeCall>, - limit: RateLimit>, + limit: RateLimitKind>, context: Option<>::LimitContext>, ) -> DispatchResult { - ensure_root(origin)?; + T::AdminOrigin::ensure_origin(origin)?; + + if context.is_some() + && >::ContextResolver::context(call.as_ref()).is_none() + { + return Err(Error::::ContextUnavailable.into()); + } let identifier = TransactionIdentifier::from_call::(call.as_ref())?; - Limits::::insert(&identifier, context.clone(), limit.clone()); + let context_for_event = context.clone(); + + if let Some(ref ctx) = context { + Limits::::mutate(&identifier, |slot| match slot { + Some(config) => config.upsert_context(ctx.clone(), limit), + None => *slot = Some(RateLimit::contextual_single(ctx.clone(), limit)), + }); + } else { + Limits::::insert(&identifier, RateLimit::global(limit)); + } let (pallet_name, extrinsic_name) = identifier.names::()?; let pallet = Vec::from(pallet_name.as_bytes()); @@ -341,12 +367,11 @@ pub mod pallet { Self::deposit_event(Event::RateLimitSet { transaction: identifier, - context, + context: context_for_event, limit, pallet, extrinsic, }); - Ok(()) } @@ -362,7 +387,7 @@ pub mod pallet { call: Box<>::RuntimeCall>, context: Option<>::LimitContext>, ) -> DispatchResult { - ensure_root(origin)?; + T::AdminOrigin::ensure_origin(origin)?; let identifier = TransactionIdentifier::from_call::(call.as_ref())?; @@ -370,10 +395,28 @@ pub mod pallet { let pallet = Vec::from(pallet_name.as_bytes()); let extrinsic = Vec::from(extrinsic_name.as_bytes()); - ensure!( - Limits::::take(&identifier, context.clone()).is_some(), - Error::::MissingRateLimit - ); + let mut removed = false; + Limits::::mutate_exists(&identifier, |maybe_config| { + if let Some(config) = maybe_config { + match (&context, config) { + (None, _) => { + removed = true; + *maybe_config = None; + } + (Some(ctx), RateLimit::Contextual(map)) => { + if map.remove(ctx).is_some() { + removed = true; + if map.is_empty() { + *maybe_config = None; + } + } + } + (Some(_), RateLimit::Global(_)) => {} + } + } + }); + + ensure!(removed, Error::::MissingRateLimit); Self::deposit_event(Event::RateLimitCleared { transaction: identifier, @@ -392,7 +435,7 @@ pub mod pallet { origin: OriginFor, block_span: BlockNumberFor, ) -> DispatchResult { - ensure_root(origin)?; + T::AdminOrigin::ensure_origin(origin)?; DefaultLimit::::put(block_span); diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index b80862edd5..1c792dde16 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -8,6 +8,7 @@ use frame_support::{ }, traits::{ConstU16, ConstU32, ConstU64, Everything}, }; +use frame_system::EnsureRoot; use sp_core::H256; use sp_io::TestExternalities; use sp_std::vec::Vec; @@ -59,8 +60,11 @@ pub struct TestContextResolver; impl pallet_rate_limiting::RateLimitContextResolver for TestContextResolver { - fn context(_call: &RuntimeCall) -> Option { - None + fn context(call: &RuntimeCall) -> Option { + match call { + RuntimeCall::RateLimiting(_) => Some(1), + _ => None, + } } } @@ -68,6 +72,7 @@ impl pallet_rate_limiting::Config for Test { type RuntimeCall = RuntimeCall; type LimitContext = LimitContext; type ContextResolver = TestContextResolver; + type AdminOrigin = EnsureRoot; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = BenchHelper; } diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index 62aae069db..27bf6e5472 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -1,6 +1,7 @@ use frame_support::{assert_noop, assert_ok, error::BadOrigin}; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; -use crate::{DefaultLimit, LastSeen, Limits, RateLimit, mock::*, pallet::Error}; +use crate::{DefaultLimit, LastSeen, Limits, RateLimit, RateLimitKind, mock::*, pallet::Error}; #[test] fn limit_for_call_names_returns_none_if_not_set() { @@ -18,12 +19,12 @@ fn limit_for_call_names_returns_stored_limit() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(7)); + Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(7))); let fetched = RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", None) .expect("limit should exist"); - assert_eq!(fetched, RateLimit::Exact(7)); + assert_eq!(fetched, RateLimitKind::Exact(7)); }); } @@ -33,18 +34,20 @@ fn limit_for_call_names_prefers_context_specific_limit() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(3)); - Limits::::insert(identifier, Some(5), RateLimit::Exact(8)); + Limits::::insert( + identifier, + RateLimit::contextual_single(5u16, RateLimitKind::Exact(8)), + ); let fetched = RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", Some(5)) .expect("limit should exist"); - assert_eq!(fetched, RateLimit::Exact(8)); + assert_eq!(fetched, RateLimitKind::Exact(8)); - let fallback = + assert!( RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", Some(1)) - .expect("limit should exist"); - assert_eq!(fallback, RateLimit::Exact(3)); + .is_none() + ); }); } @@ -55,7 +58,7 @@ fn resolved_limit_for_call_names_resolves_default_value() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Default); + Limits::::insert(identifier, RateLimit::global(RateLimitKind::Default)); let resolved = RateLimiting::resolved_limit_for_call_names( "RateLimiting", @@ -73,8 +76,10 @@ fn resolved_limit_for_call_names_prefers_context_specific_value() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(4)); - Limits::::insert(identifier, Some(6), RateLimit::Exact(9)); + let mut map = BTreeMap::new(); + map.insert(6u16, RateLimitKind::Exact(9)); + map.insert(2u16, RateLimitKind::Exact(4)); + Limits::::insert(identifier, RateLimit::Contextual(map)); let resolved = RateLimiting::resolved_limit_for_call_names( "RateLimiting", @@ -84,13 +89,14 @@ fn resolved_limit_for_call_names_prefers_context_specific_value() { .expect("resolved limit"); assert_eq!(resolved, 9); - let fallback = RateLimiting::resolved_limit_for_call_names( - "RateLimiting", - "set_default_rate_limit", - Some(1), - ) - .expect("resolved limit"); - assert_eq!(fallback, 4); + assert!( + RateLimiting::resolved_limit_for_call_names( + "RateLimiting", + "set_default_rate_limit", + Some(1), + ) + .is_none() + ); }); } @@ -126,7 +132,10 @@ fn is_within_limit_false_when_rate_limited() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, Some(1 as LimitContext), RateLimit::Exact(5)); + Limits::::insert( + identifier, + RateLimit::contextual_single(1 as LimitContext, RateLimitKind::Exact(5)), + ); LastSeen::::insert(identifier, Some(1 as LimitContext), 9); System::set_block_number(13); @@ -143,7 +152,10 @@ fn is_within_limit_true_after_required_span() { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - Limits::::insert(identifier, Some(2 as LimitContext), RateLimit::Exact(5)); + Limits::::insert( + identifier, + RateLimit::contextual_single(2 as LimitContext, RateLimitKind::Exact(5)), + ); LastSeen::::insert(identifier, Some(2 as LimitContext), 10); System::set_block_number(20); @@ -161,7 +173,7 @@ fn set_rate_limit_updates_storage_and_emits_event() { let target_call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let limit = RateLimit::Exact(9); + let limit = RateLimitKind::Exact(9); assert_ok!(RateLimiting::set_rate_limit( RuntimeOrigin::root(), @@ -172,8 +184,8 @@ fn set_rate_limit_updates_storage_and_emits_event() { let identifier = identifier_for(&target_call); assert_eq!( - Limits::::get(identifier, None::), - Some(limit) + Limits::::get(identifier), + Some(RateLimit::global(limit)) ); match pop_last_event() { @@ -204,19 +216,15 @@ fn set_rate_limit_supports_context_specific_limit() { assert_ok!(RateLimiting::set_rate_limit( RuntimeOrigin::root(), Box::new(target_call.clone()), - RateLimit::Exact(11), - context, + RateLimitKind::Exact(11), + context.clone(), )); let identifier = identifier_for(&target_call); + let config = Limits::::get(identifier).expect("config stored"); assert_eq!( - Limits::::get(identifier, Some(7)), - Some(RateLimit::Exact(11)) - ); - // global remains untouched - assert_eq!( - Limits::::get(identifier, None::), - None + config.kind_for(context.as_ref()).copied(), + Some(RateLimitKind::Exact(11)) ); }); } @@ -231,7 +239,7 @@ fn set_rate_limit_requires_root() { RateLimiting::set_rate_limit( RuntimeOrigin::signed(1), Box::new(target_call), - RateLimit::Exact(1), + RateLimitKind::Exact(1), None, ), BadOrigin @@ -248,14 +256,14 @@ fn set_rate_limit_accepts_default_variant() { assert_ok!(RateLimiting::set_rate_limit( RuntimeOrigin::root(), Box::new(target_call.clone()), - RateLimit::Default, + RateLimitKind::Default, None, )); let identifier = identifier_for(&target_call); assert_eq!( - Limits::::get(identifier, None::), - Some(RateLimit::Default) + Limits::::get(identifier), + Some(RateLimit::global(RateLimitKind::Default)) ); }); } @@ -268,7 +276,7 @@ fn clear_rate_limit_removes_entry_and_emits_event() { let target_call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&target_call); - Limits::::insert(identifier, None::, RateLimit::Exact(4)); + Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(4))); assert_ok!(RateLimiting::clear_rate_limit( RuntimeOrigin::root(), @@ -276,10 +284,7 @@ fn clear_rate_limit_removes_entry_and_emits_event() { None, )); - assert_eq!( - Limits::::get(identifier, None::), - None - ); + assert!(Limits::::get(identifier).is_none()); match pop_last_event() { RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitCleared { @@ -319,8 +324,10 @@ fn clear_rate_limit_removes_only_selected_context() { let target_call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&target_call); - Limits::::insert(identifier, None::, RateLimit::Exact(5)); - Limits::::insert(identifier, Some(9), RateLimit::Exact(7)); + let mut map = BTreeMap::new(); + map.insert(9u16, RateLimitKind::Exact(7)); + map.insert(10u16, RateLimitKind::Exact(5)); + Limits::::insert(identifier, RateLimit::Contextual(map)); assert_ok!(RateLimiting::clear_rate_limit( RuntimeOrigin::root(), @@ -328,10 +335,11 @@ fn clear_rate_limit_removes_only_selected_context() { Some(9), )); - assert_eq!(Limits::::get(identifier, Some(9u16)), None); + let config = Limits::::get(identifier).expect("config remains"); + assert!(config.kind_for(Some(&9u16)).is_none()); assert_eq!( - Limits::::get(identifier, None::), - Some(RateLimit::Exact(5)) + config.kind_for(Some(&10u16)).copied(), + Some(RateLimitKind::Exact(5)) ); match pop_last_event() { @@ -348,6 +356,24 @@ fn clear_rate_limit_removes_only_selected_context() { }); } +#[test] +fn set_rate_limit_rejects_unresolvable_context() { + new_test_ext().execute_with(|| { + let target_call = + RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }); + + assert_noop!( + RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + Box::new(target_call), + RateLimitKind::Exact(5), + Some(42), + ), + Error::::ContextUnavailable + ); + }); +} + #[test] fn set_default_rate_limit_updates_storage_and_emits_event() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index 119ad9c707..e72b473fd4 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -171,7 +171,10 @@ mod tests { transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError}, }; - use crate::{LastSeen, Limits, RateLimit, types::TransactionIdentifier}; + use crate::{ + LastSeen, Limits, + types::{RateLimit, RateLimitKind, TransactionIdentifier}, + }; use super::*; use crate::mock::*; @@ -250,7 +253,7 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(5)); + Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(5))); System::set_block_number(10); @@ -288,7 +291,7 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(5)); + Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(5))); LastSeen::::insert(identifier, None::, 20); System::set_block_number(22); @@ -310,7 +313,7 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, None::, RateLimit::Exact(0)); + Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(0))); System::set_block_number(30); diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 7d53a34ac6..a18fff37c6 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -1,6 +1,7 @@ use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata}; use scale_info::TypeInfo; +use sp_std::collections::btree_map::BTreeMap; /// Resolves the optional context within which a rate limit applies. pub trait RateLimitContextResolver { @@ -79,7 +80,7 @@ impl TransactionIdentifier { } } -/// Configuration value for a rate limit. +/// Policy describing the block span enforced by a rate limit. #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[derive( Clone, @@ -93,13 +94,84 @@ impl TransactionIdentifier { MaxEncodedLen, Debug, )] -pub enum RateLimit { +pub enum RateLimitKind { /// Use the pallet-level default rate limit. Default, /// Apply an exact rate limit measured in blocks. Exact(BlockNumber), } +/// Stored rate limit configuration for a transaction identifier. +/// +/// The configuration is mutually exclusive: either the call is globally limited or it stores a set +/// of per-context spans. +#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr( + feature = "std", + serde( + bound = "Context: Ord + serde::Serialize + serde::de::DeserializeOwned, BlockNumber: serde::Serialize + serde::de::DeserializeOwned" + ) +)] +#[derive(Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, Debug)] +pub enum RateLimit { + /// Global span applied to every invocation. + Global(RateLimitKind), + /// Per-context spans keyed by `Context`. + Contextual(BTreeMap>), +} + +impl RateLimit +where + Context: Ord, +{ + /// Convenience helper to build a global configuration. + pub fn global(kind: RateLimitKind) -> Self { + Self::Global(kind) + } + + /// Convenience helper to build a contextual configuration containing a single entry. + pub fn contextual_single(context: Context, kind: RateLimitKind) -> Self { + let mut map = BTreeMap::new(); + map.insert(context, kind); + Self::Contextual(map) + } + + /// Returns the span configured for the provided context, if any. + pub fn kind_for(&self, context: Option<&Context>) -> Option<&RateLimitKind> { + match self { + RateLimit::Global(kind) => Some(kind), + RateLimit::Contextual(map) => context.and_then(|ctx| map.get(ctx)), + } + } + + /// Inserts or updates a contextual entry, converting from a global configuration if needed. + pub fn upsert_context(&mut self, context: Context, kind: RateLimitKind) { + match self { + RateLimit::Global(_) => { + let mut map = BTreeMap::new(); + map.insert(context, kind); + *self = RateLimit::Contextual(map); + } + RateLimit::Contextual(map) => { + map.insert(context, kind); + } + } + } + + /// Removes a contextual entry, returning whether one existed. + pub fn remove_context(&mut self, context: &Context) -> bool { + match self { + RateLimit::Global(_) => false, + RateLimit::Contextual(map) => map.remove(context).is_some(), + } + } + + /// Returns true when the contextual configuration contains no entries. + pub fn is_contextual_empty(&self) -> bool { + matches!(self, RateLimit::Contextual(map) if map.is_empty()) + } +} + #[cfg(test)] mod tests { use sp_runtime::DispatchError; From c84cb92ec13ccdc8f05e2140add73087a0dbb678 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 30 Oct 2025 17:17:19 +0300 Subject: [PATCH 14/95] Separate context between usage key and limit scope --- pallets/rate-limiting/src/benchmarking.rs | 29 ++-- pallets/rate-limiting/src/lib.rs | 166 +++++++++++++--------- pallets/rate-limiting/src/mock.rs | 35 +++-- pallets/rate-limiting/src/tests.rs | 128 +++++++++-------- pallets/rate-limiting/src/tx_extension.rs | 33 ++--- pallets/rate-limiting/src/types.rs | 54 +++---- 6 files changed, 252 insertions(+), 193 deletions(-) diff --git a/pallets/rate-limiting/src/benchmarking.rs b/pallets/rate-limiting/src/benchmarking.rs index 4c9ce17708..65d547ab0b 100644 --- a/pallets/rate-limiting/src/benchmarking.rs +++ b/pallets/rate-limiting/src/benchmarking.rs @@ -36,30 +36,43 @@ mod benchmarks { fn set_rate_limit() { let call = sample_call::(); let limit = RateLimitKind::>::Exact(BlockNumberFor::::from(10u32)); + let scope = ::LimitScopeResolver::context(call.as_ref()); let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); #[extrinsic_call] - _(RawOrigin::Root, call, limit.clone(), None); - - assert_eq!( - Limits::::get(&identifier), - Some(RateLimit::global(limit)) - ); + _(RawOrigin::Root, call, limit.clone()); + + let stored = Limits::::get(&identifier).expect("limit stored"); + match (scope, &stored) { + (Some(ref sc), RateLimit::Scoped(map)) => { + assert_eq!(map.get(sc), Some(&limit)); + } + (None, RateLimit::Global(kind)) | (Some(_), RateLimit::Global(kind)) => { + assert_eq!(kind, &limit); + } + (None, RateLimit::Scoped(map)) => { + assert!(map.values().any(|k| k == &limit)); + } + } } #[benchmark] fn clear_rate_limit() { let call = sample_call::(); let limit = RateLimitKind::>::Exact(BlockNumberFor::::from(10u32)); + let scope = ::LimitScopeResolver::context(call.as_ref()); // Pre-populate limit for benchmark call let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); - Limits::::insert(identifier, RateLimit::global(limit)); + match scope.clone() { + Some(sc) => Limits::::insert(identifier, RateLimit::scoped_single(sc, limit)), + None => Limits::::insert(identifier, RateLimit::global(limit)), + } #[extrinsic_call] - _(RawOrigin::Root, call, None); + _(RawOrigin::Root, call); assert!(Limits::::get(identifier).is_none()); } diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index cf0ebeefb6..0579249b36 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -10,16 +10,18 @@ //! restricted by [`Config::AdminOrigin`], to manage this data: //! //! - [`set_rate_limit`](pallet::Pallet::set_rate_limit): assign a limit to an extrinsic by -//! supplying a [`RateLimitKind`] span and optionally a contextual identifier. When a contextual -//! span is stored, any previously configured global span is replaced. -//! - [`clear_rate_limit`](pallet::Pallet::clear_rate_limit): remove a stored limit for the provided -//! scope (either the global entry when `None` is supplied, or a specific context). +//! supplying a [`RateLimitKind`] span. The pallet infers the *limit scope* (for example a +//! `netuid`) using [`Config::LimitScopeResolver`] and stores the configuration for that scope, or +//! globally when no scope is resolved. +//! - [`clear_rate_limit`](pallet::Pallet::clear_rate_limit): remove a stored limit for the scope +//! derived from the provided call (or the global entry when no scope resolves). //! - [`set_default_rate_limit`](pallet::Pallet::set_default_rate_limit): set the global default //! block span used by `RateLimitKind::Default` entries. //! //! The pallet also tracks the last block in which a rate-limited call was executed, per optional -//! *context*. Context allows one limit definition (for example, “set weights”) to be enforced per -//! subnet, account, or other grouping chosen by the runtime. +//! *usage key*. A usage key may refine tracking beyond the limit scope (for example combining a +//! `netuid` with a hyperparameter name), so the two concepts are explicitly separated in the +//! configuration. //! //! Each storage map is namespaced by pallet instance; runtimes can deploy multiple independent //! instances to manage distinct rate-limiting scopes. @@ -41,21 +43,27 @@ //! ); //! ``` //! -//! # Context resolver +//! # Context resolvers //! -//! The extension needs to know when two invocations should share a rate limit. This is controlled -//! by implementing [`RateLimitContextResolver`] for the runtime call type (or for a helper that the -//! runtime wires into [`Config::ContextResolver`]). The resolver receives the call and returns -//! `Some(context)` if the rate should be scoped (e.g. by `netuid`), or `None` to use the global -//! entry. The resolver is only used when *tracking* executions; you still configure limits via the -//! explicit `context` argument on `set_rate_limit`/`clear_rate_limit`. +//! The pallet relies on two resolvers, both implementing [`RateLimitContextResolver`]: +//! +//! - [`Config::LimitScopeResolver`], which determines how limits are stored (for example by +//! returning a `netuid`). When this resolver returns `None`, the configuration is stored as a +//! global fallback. +//! - [`Config::UsageResolver`], which decides how executions are tracked in +//! [`LastSeen`](pallet::LastSeen). This can refine the limit scope (for example by returning a +//! tuple of `(netuid, hyperparameter)`). +//! +//! Each resolver receives the call and may return `Some(identifier)` when scoping is required, or +//! `None` to use the global entry. Extrinsics such as +//! [`set_rate_limit`](pallet::Pallet::set_rate_limit) automatically consult these resolvers. //! //! ```ignore //! pub struct WeightsContextResolver; //! -//! impl pallet_rate_limiting::RateLimitContextResolver -//! for WeightsContextResolver -//! { +//! // Limits are scoped per netuid. +//! pub struct ScopeResolver; +//! impl pallet_rate_limiting::RateLimitContextResolver for ScopeResolver { //! fn context(call: &RuntimeCall) -> Option { //! match call { //! RuntimeCall::Subtensor(pallet_subtensor::Call::set_weights { netuid, .. }) => { @@ -66,10 +74,29 @@ //! } //! } //! +//! // Usage tracking distinguishes hyperparameter + netuid. +//! pub struct UsageResolver; +//! impl pallet_rate_limiting::RateLimitContextResolver +//! for UsageResolver +//! { +//! fn context(call: &RuntimeCall) -> Option<(NetUid, HyperParam)> { +//! match call { +//! RuntimeCall::Subtensor(pallet_subtensor::Call::set_hyperparam { +//! netuid, +//! hyper, +//! .. +//! }) => Some((*netuid, *hyper)), +//! _ => None, +//! } +//! } +//! } +//! //! impl pallet_rate_limiting::Config for Runtime { //! type RuntimeCall = RuntimeCall; -//! type LimitContext = NetUid; -//! type ContextResolver = WeightsContextResolver; +//! type LimitScope = NetUid; +//! type LimitScopeResolver = ScopeResolver; +//! type UsageKey = (NetUid, HyperParam); +//! type UsageResolver = UsageResolver; //! type AdminOrigin = frame_system::EnsureRoot; //! } //! ``` @@ -121,11 +148,17 @@ pub mod pallet { /// Origin permitted to configure rate limits. type AdminOrigin: EnsureOrigin>; - /// Context type used for contextual (per-group) rate limits. - type LimitContext: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize; + /// Scope identifier used to namespace stored rate limits. + type LimitScope: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize; + + /// Resolves the scope for the given runtime call when configuring limits. + type LimitScopeResolver: RateLimitContextResolver<>::RuntimeCall, Self::LimitScope>; - /// Resolves the context for a given runtime call. - type ContextResolver: RateLimitContextResolver<>::RuntimeCall, Self::LimitContext>; + /// Usage key tracked in [`LastSeen`] for rate-limited calls. + type UsageKey: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize; + + /// Resolves the usage key for the given runtime call when enforcing limits. + type UsageResolver: RateLimitContextResolver<>::RuntimeCall, Self::UsageKey>; /// Helper used to construct runtime calls for benchmarking. #[cfg(feature = "runtime-benchmarks")] @@ -139,20 +172,20 @@ pub mod pallet { _, Blake2_128Concat, TransactionIdentifier, - RateLimit<>::LimitContext, BlockNumberFor>, + RateLimit<>::LimitScope, BlockNumberFor>, OptionQuery, >; /// Tracks when a transaction was last observed. /// - /// The second key is `None` for global limits and `Some(context)` for contextual limits. + /// The second key is `None` for global tracking and `Some(key)` for scoped usage tracking. #[pallet::storage] pub type LastSeen, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, TransactionIdentifier, Blake2_128Concat, - Option<>::LimitContext>, + Option<>::UsageKey>, BlockNumberFor, OptionQuery, >; @@ -171,8 +204,8 @@ pub mod pallet { RateLimitSet { /// Identifier of the affected transaction. transaction: TransactionIdentifier, - /// Context to which the limit applies, if any. - context: Option<>::LimitContext>, + /// Limit scope to which the configuration applies, if any. + scope: Option<>::LimitScope>, /// The rate limit policy applied to the transaction. limit: RateLimitKind>, /// Pallet name associated with the transaction. @@ -184,8 +217,8 @@ pub mod pallet { RateLimitCleared { /// Identifier of the affected transaction. transaction: TransactionIdentifier, - /// Context from which the limit was cleared, if any. - context: Option<>::LimitContext>, + /// Limit scope from which the configuration was cleared, if any. + scope: Option<>::LimitScope>, /// Pallet name associated with the transaction. pallet: Vec, /// Extrinsic name associated with the transaction. @@ -205,8 +238,6 @@ pub mod pallet { InvalidRuntimeCall, /// Attempted to remove a limit that is not present. MissingRateLimit, - /// Contextual configuration was requested but no context can be resolved for the call. - ContextUnavailable, } #[pallet::genesis_config] @@ -214,7 +245,7 @@ pub mod pallet { pub default_limit: BlockNumberFor, pub limits: Vec<( TransactionIdentifier, - Option<>::LimitContext>, + Option<>::LimitScope>, RateLimitKind>, )>, } @@ -234,16 +265,16 @@ pub mod pallet { fn build(&self) { DefaultLimit::::put(self.default_limit); - for (identifier, context, kind) in &self.limits { - Limits::::mutate(identifier, |entry| match context { + for (identifier, scope, kind) in &self.limits { + Limits::::mutate(identifier, |entry| match scope { None => { *entry = Some(RateLimit::global(*kind)); } - Some(ctx) => { + Some(sc) => { if let Some(config) = entry { - config.upsert_context(ctx.clone(), *kind); + config.upsert_scope(sc.clone(), *kind); } else { - *entry = Some(RateLimit::contextual_single(ctx.clone(), *kind)); + *entry = Some(RateLimit::scoped_single(sc.clone(), *kind)); } } }); @@ -257,18 +288,19 @@ pub mod pallet { impl, I: 'static> Pallet { /// Returns `true` when the given transaction identifier passes its configured rate limit - /// within the provided context. + /// within the provided usage scope. pub fn is_within_limit( identifier: &TransactionIdentifier, - context: &Option<>::LimitContext>, + scope: &Option<>::LimitScope>, + usage_key: &Option<>::UsageKey>, ) -> Result { - let Some(block_span) = Self::resolved_limit(identifier, context) else { + let Some(block_span) = Self::resolved_limit(identifier, scope) else { return Ok(true); }; let current = frame_system::Pallet::::block_number(); - if let Some(last) = LastSeen::::get(identifier, context) { + if let Some(last) = LastSeen::::get(identifier, usage_key) { let delta = current.saturating_sub(last); if delta < block_span { return Ok(false); @@ -280,10 +312,10 @@ pub mod pallet { pub(crate) fn resolved_limit( identifier: &TransactionIdentifier, - context: &Option<>::LimitContext>, + scope: &Option<>::LimitScope>, ) -> Option> { let config = Limits::::get(identifier)?; - let kind = config.kind_for(context.as_ref())?; + let kind = config.kind_for(scope.as_ref())?; Some(match *kind { RateLimitKind::Default => DefaultLimit::::get(), RateLimitKind::Exact(block_span) => block_span, @@ -294,21 +326,21 @@ pub mod pallet { pub fn limit_for_call_names( pallet_name: &str, extrinsic_name: &str, - context: Option<>::LimitContext>, + scope: Option<>::LimitScope>, ) -> Option>> { let identifier = Self::identifier_for_call_names(pallet_name, extrinsic_name)?; Limits::::get(&identifier) - .and_then(|config| config.kind_for(context.as_ref()).copied()) + .and_then(|config| config.kind_for(scope.as_ref()).copied()) } /// Returns the resolved block span for the specified pallet/extrinsic names, if any. pub fn resolved_limit_for_call_names( pallet_name: &str, extrinsic_name: &str, - context: Option<>::LimitContext>, + scope: Option<>::LimitScope>, ) -> Option> { let identifier = Self::identifier_for_call_names(pallet_name, extrinsic_name)?; - Self::resolved_limit(&identifier, &context) + Self::resolved_limit(&identifier, &scope) } fn identifier_for_call_names( @@ -327,35 +359,29 @@ pub mod pallet { #[pallet::call] impl, I: 'static> Pallet { - /// Sets the rate limit configuration for the given call and optional context. + /// Sets the rate limit configuration for the given call. /// /// The supplied `call` is only used to derive the pallet and extrinsic indices; **any - /// arguments embedded in the call are ignored**. The `context` parameter determines which - /// scoped entry is updated (for example a subnet identifier). Passing `None` updates the - /// global entry, which acts as a fallback when no context-specific limit exists. + /// arguments embedded in the call are ignored**. The applicable scope is discovered via + /// [`Config::LimitScopeResolver`]. When a scope resolves, the configuration is stored + /// against that scope; otherwise the global entry is updated. #[pallet::call_index(0)] #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] pub fn set_rate_limit( origin: OriginFor, call: Box<>::RuntimeCall>, limit: RateLimitKind>, - context: Option<>::LimitContext>, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; - if context.is_some() - && >::ContextResolver::context(call.as_ref()).is_none() - { - return Err(Error::::ContextUnavailable.into()); - } - let identifier = TransactionIdentifier::from_call::(call.as_ref())?; - let context_for_event = context.clone(); + let scope = >::LimitScopeResolver::context(call.as_ref()); + let scope_for_event = scope.clone(); - if let Some(ref ctx) = context { + if let Some(ref sc) = scope { Limits::::mutate(&identifier, |slot| match slot { - Some(config) => config.upsert_context(ctx.clone(), limit), - None => *slot = Some(RateLimit::contextual_single(ctx.clone(), limit)), + Some(config) => config.upsert_scope(sc.clone(), limit), + None => *slot = Some(RateLimit::scoped_single(sc.clone(), limit)), }); } else { Limits::::insert(&identifier, RateLimit::global(limit)); @@ -367,7 +393,7 @@ pub mod pallet { Self::deposit_event(Event::RateLimitSet { transaction: identifier, - context: context_for_event, + scope: scope_for_event, limit, pallet, extrinsic, @@ -378,18 +404,18 @@ pub mod pallet { /// Clears the rate limit for the given call, if present. /// /// The supplied `call` is only used to derive the pallet and extrinsic indices; **any - /// arguments embedded in the call are ignored**. The `context` parameter must match the - /// entry that should be removed (use `None` to remove the global configuration). + /// arguments embedded in the call are ignored**. The configuration scope is determined via + /// [`Config::LimitScopeResolver`]. When no scope resolves, the global entry is cleared. #[pallet::call_index(1)] #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] pub fn clear_rate_limit( origin: OriginFor, call: Box<>::RuntimeCall>, - context: Option<>::LimitContext>, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; let identifier = TransactionIdentifier::from_call::(call.as_ref())?; + let scope = >::LimitScopeResolver::context(call.as_ref()); let (pallet_name, extrinsic_name) = identifier.names::()?; let pallet = Vec::from(pallet_name.as_bytes()); @@ -398,13 +424,13 @@ pub mod pallet { let mut removed = false; Limits::::mutate_exists(&identifier, |maybe_config| { if let Some(config) = maybe_config { - match (&context, config) { + match (&scope, config) { (None, _) => { removed = true; *maybe_config = None; } - (Some(ctx), RateLimit::Contextual(map)) => { - if map.remove(ctx).is_some() { + (Some(sc), RateLimit::Scoped(map)) => { + if map.remove(sc).is_some() { removed = true; if map.is_empty() { *maybe_config = None; @@ -420,7 +446,7 @@ pub mod pallet { Self::deposit_event(Event::RateLimitCleared { transaction: identifier, - context, + scope, pallet, extrinsic, }); diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index 1c792dde16..aec00b45bb 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -1,5 +1,7 @@ #![allow(dead_code)] +use core::convert::TryInto; + use frame_support::{ derive_impl, sp_runtime::{ @@ -53,15 +55,30 @@ impl frame_system::Config for Test { type Block = Block; } -pub type LimitContext = u16; +pub type LimitScope = u16; +pub type UsageKey = u16; + +pub struct TestScopeResolver; +pub struct TestUsageResolver; -pub struct TestContextResolver; +impl pallet_rate_limiting::RateLimitContextResolver for TestScopeResolver { + fn context(call: &RuntimeCall) -> Option { + match call { + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { + (*block_span).try_into().ok() + } + RuntimeCall::RateLimiting(_) => Some(1), + _ => None, + } + } +} -impl pallet_rate_limiting::RateLimitContextResolver - for TestContextResolver -{ - fn context(call: &RuntimeCall) -> Option { +impl pallet_rate_limiting::RateLimitContextResolver for TestUsageResolver { + fn context(call: &RuntimeCall) -> Option { match call { + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { + (*block_span).try_into().ok() + } RuntimeCall::RateLimiting(_) => Some(1), _ => None, } @@ -70,8 +87,10 @@ impl pallet_rate_limiting::RateLimitContextResolver impl pallet_rate_limiting::Config for Test { type RuntimeCall = RuntimeCall; - type LimitContext = LimitContext; - type ContextResolver = TestContextResolver; + type LimitScope = LimitScope; + type LimitScopeResolver = TestScopeResolver; + type UsageKey = UsageKey; + type UsageResolver = TestUsageResolver; type AdminOrigin = EnsureRoot; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = BenchHelper; diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index 27bf6e5472..f02c2c52b0 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -29,14 +29,14 @@ fn limit_for_call_names_returns_stored_limit() { } #[test] -fn limit_for_call_names_prefers_context_specific_limit() { +fn limit_for_call_names_prefers_scope_specific_limit() { new_test_ext().execute_with(|| { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); Limits::::insert( identifier, - RateLimit::contextual_single(5u16, RateLimitKind::Exact(8)), + RateLimit::scoped_single(5u16, RateLimitKind::Exact(8)), ); let fetched = @@ -71,7 +71,7 @@ fn resolved_limit_for_call_names_resolves_default_value() { } #[test] -fn resolved_limit_for_call_names_prefers_context_specific_value() { +fn resolved_limit_for_call_names_prefers_scope_specific_value() { new_test_ext().execute_with(|| { let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); @@ -79,7 +79,7 @@ fn resolved_limit_for_call_names_prefers_context_specific_value() { let mut map = BTreeMap::new(); map.insert(6u16, RateLimitKind::Exact(9)); map.insert(2u16, RateLimitKind::Exact(4)); - Limits::::insert(identifier, RateLimit::Contextual(map)); + Limits::::insert(identifier, RateLimit::Scoped(map)); let resolved = RateLimiting::resolved_limit_for_call_names( "RateLimiting", @@ -121,7 +121,7 @@ fn is_within_limit_is_true_when_no_limit() { RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - let result = RateLimiting::is_within_limit(&identifier, &None); + let result = RateLimiting::is_within_limit(&identifier, &None, &None); assert_eq!(result.expect("no error expected"), true); }); } @@ -134,14 +134,18 @@ fn is_within_limit_false_when_rate_limited() { let identifier = identifier_for(&call); Limits::::insert( identifier, - RateLimit::contextual_single(1 as LimitContext, RateLimitKind::Exact(5)), + RateLimit::scoped_single(1 as LimitScope, RateLimitKind::Exact(5)), ); - LastSeen::::insert(identifier, Some(1 as LimitContext), 9); + LastSeen::::insert(identifier, Some(1 as UsageKey), 9); System::set_block_number(13); - let within = RateLimiting::is_within_limit(&identifier, &Some(1 as LimitContext)) - .expect("call succeeds"); + let within = RateLimiting::is_within_limit( + &identifier, + &Some(1 as LimitScope), + &Some(1 as UsageKey), + ) + .expect("call succeeds"); assert!(!within); }); } @@ -154,14 +158,18 @@ fn is_within_limit_true_after_required_span() { let identifier = identifier_for(&call); Limits::::insert( identifier, - RateLimit::contextual_single(2 as LimitContext, RateLimitKind::Exact(5)), + RateLimit::scoped_single(2 as LimitScope, RateLimitKind::Exact(5)), ); - LastSeen::::insert(identifier, Some(2 as LimitContext), 10); + LastSeen::::insert(identifier, Some(2 as UsageKey), 10); System::set_block_number(20); - let within = RateLimiting::is_within_limit(&identifier, &Some(2 as LimitContext)) - .expect("call succeeds"); + let within = RateLimiting::is_within_limit( + &identifier, + &Some(2 as LimitScope), + &Some(2 as UsageKey), + ) + .expect("call succeeds"); assert!(within); }); } @@ -179,25 +187,24 @@ fn set_rate_limit_updates_storage_and_emits_event() { RuntimeOrigin::root(), Box::new(target_call.clone()), limit, - None, )); let identifier = identifier_for(&target_call); assert_eq!( Limits::::get(identifier), - Some(RateLimit::global(limit)) + Some(RateLimit::scoped_single(0, limit)) ); match pop_last_event() { RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitSet { transaction, - context, + scope, limit: emitted_limit, pallet, extrinsic, }) => { assert_eq!(transaction, identifier); - assert_eq!(context, None); + assert_eq!(scope, Some(0)); assert_eq!(emitted_limit, limit); assert_eq!(pallet, b"RateLimiting".to_vec()); assert_eq!(extrinsic, b"set_default_rate_limit".to_vec()); @@ -208,24 +215,42 @@ fn set_rate_limit_updates_storage_and_emits_event() { } #[test] -fn set_rate_limit_supports_context_specific_limit() { +fn set_rate_limit_stores_global_when_scope_absent() { new_test_ext().execute_with(|| { + System::reset_events(); + let target_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let context = Some(7u16); + RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }); + let limit = RateLimitKind::Exact(11); + assert_ok!(RateLimiting::set_rate_limit( RuntimeOrigin::root(), Box::new(target_call.clone()), - RateLimitKind::Exact(11), - context.clone(), + limit, )); let identifier = identifier_for(&target_call); - let config = Limits::::get(identifier).expect("config stored"); assert_eq!( - config.kind_for(context.as_ref()).copied(), - Some(RateLimitKind::Exact(11)) + Limits::::get(identifier), + Some(RateLimit::global(limit)) ); + + match pop_last_event() { + RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitSet { + transaction, + scope, + limit: emitted_limit, + pallet, + extrinsic, + }) => { + assert_eq!(transaction, identifier); + assert_eq!(scope, None); + assert_eq!(emitted_limit, limit); + assert_eq!(pallet, b"System".to_vec()); + assert_eq!(extrinsic, b"remark".to_vec()); + } + other => panic!("unexpected event: {:?}", other), + } }); } @@ -240,7 +265,6 @@ fn set_rate_limit_requires_root() { RuntimeOrigin::signed(1), Box::new(target_call), RateLimitKind::Exact(1), - None, ), BadOrigin ); @@ -257,13 +281,12 @@ fn set_rate_limit_accepts_default_variant() { RuntimeOrigin::root(), Box::new(target_call.clone()), RateLimitKind::Default, - None, )); let identifier = identifier_for(&target_call); assert_eq!( Limits::::get(identifier), - Some(RateLimit::global(RateLimitKind::Default)) + Some(RateLimit::scoped_single(0, RateLimitKind::Default)) ); }); } @@ -274,14 +297,13 @@ fn clear_rate_limit_removes_entry_and_emits_event() { System::reset_events(); let target_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }); let identifier = identifier_for(&target_call); Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(4))); assert_ok!(RateLimiting::clear_rate_limit( RuntimeOrigin::root(), Box::new(target_call.clone()), - None, )); assert!(Limits::::get(identifier).is_none()); @@ -289,14 +311,14 @@ fn clear_rate_limit_removes_entry_and_emits_event() { match pop_last_event() { RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitCleared { transaction, - context, + scope, pallet, extrinsic, }) => { assert_eq!(transaction, identifier); - assert_eq!(context, None); - assert_eq!(pallet, b"RateLimiting".to_vec()); - assert_eq!(extrinsic, b"set_default_rate_limit".to_vec()); + assert_eq!(scope, None); + assert_eq!(pallet, b"System".to_vec()); + assert_eq!(extrinsic, b"remark".to_vec()); } other => panic!("unexpected event: {:?}", other), } @@ -310,29 +332,31 @@ fn clear_rate_limit_fails_when_missing() { RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); assert_noop!( - RateLimiting::clear_rate_limit(RuntimeOrigin::root(), Box::new(target_call), None), + RateLimiting::clear_rate_limit(RuntimeOrigin::root(), Box::new(target_call)), Error::::MissingRateLimit ); }); } #[test] -fn clear_rate_limit_removes_only_selected_context() { +fn clear_rate_limit_removes_only_selected_scope() { new_test_ext().execute_with(|| { System::reset_events(); - let target_call = + let base_call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&target_call); + let identifier = identifier_for(&base_call); let mut map = BTreeMap::new(); map.insert(9u16, RateLimitKind::Exact(7)); map.insert(10u16, RateLimitKind::Exact(5)); - Limits::::insert(identifier, RateLimit::Contextual(map)); + Limits::::insert(identifier, RateLimit::Scoped(map)); + + let scoped_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 9 }); assert_ok!(RateLimiting::clear_rate_limit( RuntimeOrigin::root(), - Box::new(target_call.clone()), - Some(9), + Box::new(scoped_call.clone()), )); let config = Limits::::get(identifier).expect("config remains"); @@ -345,35 +369,17 @@ fn clear_rate_limit_removes_only_selected_context() { match pop_last_event() { RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitCleared { transaction, - context, + scope, .. }) => { assert_eq!(transaction, identifier); - assert_eq!(context, Some(9)); + assert_eq!(scope, Some(9)); } other => panic!("unexpected event: {:?}", other), } }); } -#[test] -fn set_rate_limit_rejects_unresolvable_context() { - new_test_ext().execute_with(|| { - let target_call = - RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }); - - assert_noop!( - RateLimiting::set_rate_limit( - RuntimeOrigin::root(), - Box::new(target_call), - RateLimitKind::Exact(5), - Some(42), - ), - Error::::ContextUnavailable - ); - }); -} - #[test] fn set_default_rate_limit_updates_storage_and_emits_event() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index e72b473fd4..5276f1f396 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -80,14 +80,8 @@ where const IDENTIFIER: &'static str = IDENTIFIER; type Implicit = (); - type Val = Option<( - TransactionIdentifier, - Option<>::LimitContext>, - )>; - type Pre = Option<( - TransactionIdentifier, - Option<>::LimitContext>, - )>; + type Val = Option<(TransactionIdentifier, Option<>::UsageKey>)>; + type Pre = Option<(TransactionIdentifier, Option<>::UsageKey>)>; fn weight(&self, _call: &>::RuntimeCall) -> Weight { Weight::zero() @@ -108,9 +102,10 @@ where Err(_) => return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), }; - let context = >::ContextResolver::context(call); + let scope = >::LimitScopeResolver::context(call); + let usage = >::UsageResolver::context(call); - let Some(block_span) = Pallet::::resolved_limit(&identifier, &context) else { + let Some(block_span) = Pallet::::resolved_limit(&identifier, &scope) else { return Ok((ValidTransaction::default(), None, origin)); }; @@ -118,7 +113,7 @@ where return Ok((ValidTransaction::default(), None, origin)); } - let within_limit = Pallet::::is_within_limit(&identifier, &context) + let within_limit = Pallet::::is_within_limit(&identifier, &scope, &usage) .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; if !within_limit { @@ -129,7 +124,7 @@ where Ok(( ValidTransaction::default(), - Some((identifier, context)), + Some((identifier, usage)), origin, )) } @@ -153,9 +148,9 @@ where result: &DispatchResult, ) -> Result<(), TransactionValidityError> { if result.is_ok() { - if let Some((identifier, context)) = pre { + if let Some((identifier, usage)) = pre { let block_number = frame_system::Pallet::::block_number(); - LastSeen::::insert(&identifier, context, block_number); + LastSeen::::insert(&identifier, usage, block_number); } } Ok(()) @@ -193,7 +188,7 @@ mod tests { ) -> Result< ( sp_runtime::transaction_validity::ValidTransaction, - Option<(TransactionIdentifier, Option)>, + Option<(TransactionIdentifier, Option)>, RuntimeOrigin, ), TransactionValidityError, @@ -241,7 +236,7 @@ mod tests { let identifier = identifier_for(&call); assert_eq!( - LastSeen::::get(identifier, None::), + LastSeen::::get(identifier, None::), None ); }); @@ -279,7 +274,7 @@ mod tests { .expect("post_dispatch succeeds"); assert_eq!( - LastSeen::::get(identifier, None::), + LastSeen::::get(identifier, None::), Some(10) ); }); @@ -292,7 +287,7 @@ mod tests { let call = remark_call(); let identifier = identifier_for(&call); Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(5))); - LastSeen::::insert(identifier, None::, 20); + LastSeen::::insert(identifier, None::, 20); System::set_block_number(22); @@ -339,7 +334,7 @@ mod tests { .expect("post_dispatch succeeds"); assert_eq!( - LastSeen::::get(identifier, None::), + LastSeen::::get(identifier, None::), None ); }); diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index a18fff37c6..1daf2915b3 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -3,7 +3,7 @@ use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata}; use scale_info::TypeInfo; use sp_std::collections::btree_map::BTreeMap; -/// Resolves the optional context within which a rate limit applies. +/// Resolves the optional identifier within which a rate limit applies. pub trait RateLimitContextResolver { /// Returns `Some(context)` when the limit should be applied per-context, or `None` for global /// limits. @@ -104,71 +104,71 @@ pub enum RateLimitKind { /// Stored rate limit configuration for a transaction identifier. /// /// The configuration is mutually exclusive: either the call is globally limited or it stores a set -/// of per-context spans. +/// of per-scope spans. #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr( feature = "std", serde( - bound = "Context: Ord + serde::Serialize + serde::de::DeserializeOwned, BlockNumber: serde::Serialize + serde::de::DeserializeOwned" + bound = "Scope: Ord + serde::Serialize + serde::de::DeserializeOwned, BlockNumber: serde::Serialize + serde::de::DeserializeOwned" ) )] #[derive(Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, Debug)] -pub enum RateLimit { +pub enum RateLimit { /// Global span applied to every invocation. Global(RateLimitKind), - /// Per-context spans keyed by `Context`. - Contextual(BTreeMap>), + /// Per-scope spans keyed by `Scope`. + Scoped(BTreeMap>), } -impl RateLimit +impl RateLimit where - Context: Ord, + Scope: Ord, { /// Convenience helper to build a global configuration. pub fn global(kind: RateLimitKind) -> Self { Self::Global(kind) } - /// Convenience helper to build a contextual configuration containing a single entry. - pub fn contextual_single(context: Context, kind: RateLimitKind) -> Self { + /// Convenience helper to build a scoped configuration containing a single entry. + pub fn scoped_single(scope: Scope, kind: RateLimitKind) -> Self { let mut map = BTreeMap::new(); - map.insert(context, kind); - Self::Contextual(map) + map.insert(scope, kind); + Self::Scoped(map) } - /// Returns the span configured for the provided context, if any. - pub fn kind_for(&self, context: Option<&Context>) -> Option<&RateLimitKind> { + /// Returns the span configured for the provided scope, if any. + pub fn kind_for(&self, scope: Option<&Scope>) -> Option<&RateLimitKind> { match self { RateLimit::Global(kind) => Some(kind), - RateLimit::Contextual(map) => context.and_then(|ctx| map.get(ctx)), + RateLimit::Scoped(map) => scope.and_then(|key| map.get(key)), } } - /// Inserts or updates a contextual entry, converting from a global configuration if needed. - pub fn upsert_context(&mut self, context: Context, kind: RateLimitKind) { + /// Inserts or updates a scoped entry, converting from a global configuration if needed. + pub fn upsert_scope(&mut self, scope: Scope, kind: RateLimitKind) { match self { RateLimit::Global(_) => { let mut map = BTreeMap::new(); - map.insert(context, kind); - *self = RateLimit::Contextual(map); + map.insert(scope, kind); + *self = RateLimit::Scoped(map); } - RateLimit::Contextual(map) => { - map.insert(context, kind); + RateLimit::Scoped(map) => { + map.insert(scope, kind); } } } - /// Removes a contextual entry, returning whether one existed. - pub fn remove_context(&mut self, context: &Context) -> bool { + /// Removes a scoped entry, returning whether one existed. + pub fn remove_scope(&mut self, scope: &Scope) -> bool { match self { RateLimit::Global(_) => false, - RateLimit::Contextual(map) => map.remove(context).is_some(), + RateLimit::Scoped(map) => map.remove(scope).is_some(), } } - /// Returns true when the contextual configuration contains no entries. - pub fn is_contextual_empty(&self) -> bool { - matches!(self, RateLimit::Contextual(map) if map.is_empty()) + /// Returns true when the scoped configuration contains no entries. + pub fn is_scoped_empty(&self) -> bool { + matches!(self, RateLimit::Scoped(map) if map.is_empty()) } } From 2df6d4b8c95e15f024dcf4e961f9d30d14391542 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 31 Oct 2025 14:48:38 +0300 Subject: [PATCH 15/95] Update docs --- pallets/rate-limiting/src/lib.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 0579249b36..e1c7a1665a 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -361,10 +361,11 @@ pub mod pallet { impl, I: 'static> Pallet { /// Sets the rate limit configuration for the given call. /// - /// The supplied `call` is only used to derive the pallet and extrinsic indices; **any - /// arguments embedded in the call are ignored**. The applicable scope is discovered via - /// [`Config::LimitScopeResolver`]. When a scope resolves, the configuration is stored - /// against that scope; otherwise the global entry is updated. + /// The supplied `call` is inspected to derive the pallet/extrinsic indices and passed to + /// [`Config::LimitScopeResolver`] to determine the applicable scope. The pallet never + /// persists the call arguments directly, but a resolver may read them in order to resolve + /// its context. When a scope resolves, the configuration is stored against that scope; + /// otherwise the global entry is updated. #[pallet::call_index(0)] #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] pub fn set_rate_limit( @@ -403,9 +404,10 @@ pub mod pallet { /// Clears the rate limit for the given call, if present. /// - /// The supplied `call` is only used to derive the pallet and extrinsic indices; **any - /// arguments embedded in the call are ignored**. The configuration scope is determined via - /// [`Config::LimitScopeResolver`]. When no scope resolves, the global entry is cleared. + /// The supplied `call` is inspected to derive the pallet/extrinsic indices and passed to + /// [`Config::LimitScopeResolver`] when determining which scoped configuration to clear. + /// The pallet does not persist the call arguments, but resolvers may read them while + /// computing the scope. When no scope resolves, the global entry is cleared. #[pallet::call_index(1)] #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] pub fn clear_rate_limit( From 15c9aa9b7072fd98618b193cccf7fe3c7744371a Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 31 Oct 2025 14:58:02 +0300 Subject: [PATCH 16/95] Add an extrinsic to clear all scoped rate limits in pallet-rate-limiting --- pallets/rate-limiting/src/lib.rs | 44 +++++++++++++++++++++++- pallets/rate-limiting/src/tests.rs | 54 ++++++++++++++++++++++++++++++ pallets/rate-limiting/src/types.rs | 2 +- 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index e1c7a1665a..7b960e566e 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -224,6 +224,15 @@ pub mod pallet { /// Extrinsic name associated with the transaction. extrinsic: Vec, }, + /// All scoped and global rate limits for a call were cleared. + AllRateLimitsCleared { + /// Identifier of the affected transaction. + transaction: TransactionIdentifier, + /// Pallet name associated with the transaction. + pallet: Vec, + /// Extrinsic name associated with the transaction. + extrinsic: Vec, + }, /// The default rate limit was set or updated. DefaultRateLimitSet { /// The new default limit expressed in blocks. @@ -456,8 +465,41 @@ pub mod pallet { Ok(()) } - /// Sets the default rate limit in blocks applied to calls configured to use it. + /// Clears every stored rate limit configuration for the given call, including scoped + /// entries. + /// + /// The supplied `call` is inspected to derive the pallet and extrinsic indices. All stored + /// scopes for that call, along with any associated usage tracking entries, are removed when + /// this extrinsic succeeds. #[pallet::call_index(2)] + #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] + pub fn clear_all_rate_limits( + origin: OriginFor, + call: Box<>::RuntimeCall>, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + let identifier = TransactionIdentifier::from_call::(call.as_ref())?; + let (pallet_name, extrinsic_name) = identifier.names::()?; + let pallet = Vec::from(pallet_name.as_bytes()); + let extrinsic = Vec::from(extrinsic_name.as_bytes()); + + let removed = Limits::::take(&identifier).is_some(); + ensure!(removed, Error::::MissingRateLimit); + + let _ = LastSeen::::clear_prefix(&identifier, u32::MAX, None); + + Self::deposit_event(Event::AllRateLimitsCleared { + transaction: identifier, + pallet, + extrinsic, + }); + + Ok(()) + } + + /// Sets the default rate limit in blocks applied to calls configured to use it. + #[pallet::call_index(3)] #[pallet::weight(T::DbWeight::get().writes(1))] pub fn set_default_rate_limit( origin: OriginFor, diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index f02c2c52b0..1a89951193 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -380,6 +380,60 @@ fn clear_rate_limit_removes_only_selected_scope() { }); } +#[test] +fn clear_all_rate_limits_removes_entire_configuration() { + new_test_ext().execute_with(|| { + System::reset_events(); + + let target_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&target_call); + + let mut map = BTreeMap::new(); + map.insert(3u16, RateLimitKind::Exact(6)); + map.insert(4u16, RateLimitKind::Exact(7)); + Limits::::insert(identifier, RateLimit::Scoped(map)); + + LastSeen::::insert(identifier, Some(3u16), 11); + LastSeen::::insert(identifier, None::, 12); + + assert_ok!(RateLimiting::clear_all_rate_limits( + RuntimeOrigin::root(), + Box::new(target_call.clone()), + )); + + assert!(Limits::::get(identifier).is_none()); + assert!(LastSeen::::get(identifier, Some(3u16)).is_none()); + assert!(LastSeen::::get(identifier, None::).is_none()); + + match pop_last_event() { + RuntimeEvent::RateLimiting(crate::pallet::Event::AllRateLimitsCleared { + transaction, + pallet, + extrinsic, + }) => { + assert_eq!(transaction, identifier); + assert_eq!(pallet, b"RateLimiting".to_vec()); + assert_eq!(extrinsic, b"set_default_rate_limit".to_vec()); + } + other => panic!("unexpected event: {:?}", other), + } + }); +} + +#[test] +fn clear_all_rate_limits_fails_when_missing() { + new_test_ext().execute_with(|| { + let target_call = + RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }); + + assert_noop!( + RateLimiting::clear_all_rate_limits(RuntimeOrigin::root(), Box::new(target_call)), + Error::::MissingRateLimit + ); + }); +} + #[test] fn set_default_rate_limit_updates_storage_and_emits_event() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 1daf2915b3..4e68d0205a 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -189,7 +189,7 @@ mod tests { // System is the first pallet in the mock runtime, RateLimiting is second. assert_eq!(identifier.pallet_index, 1); // set_default_rate_limit has call_index 2. - assert_eq!(identifier.extrinsic_index, 2); + assert_eq!(identifier.extrinsic_index, 3); } #[test] From e74d65015d33e2ef116c7ec1b970e25f37da9410 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 31 Oct 2025 15:15:17 +0300 Subject: [PATCH 17/95] Clear LastSeen on clear_rate_limit call --- pallets/rate-limiting/src/lib.rs | 18 ++++++++++++++++++ pallets/rate-limiting/src/tests.rs | 13 +++++++++++++ 2 files changed, 31 insertions(+) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 7b960e566e..38cfce1a5e 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -427,6 +427,7 @@ pub mod pallet { let identifier = TransactionIdentifier::from_call::(call.as_ref())?; let scope = >::LimitScopeResolver::context(call.as_ref()); + let usage = >::UsageResolver::context(call.as_ref()); let (pallet_name, extrinsic_name) = identifier.names::()?; let pallet = Vec::from(pallet_name.as_bytes()); @@ -455,6 +456,23 @@ pub mod pallet { ensure!(removed, Error::::MissingRateLimit); + if removed { + match (scope.as_ref(), usage) { + (None, _) => { + let _ = LastSeen::::clear_prefix(&identifier, u32::MAX, None); + } + (_, Some(key)) => { + LastSeen::::remove(&identifier, Some(key)); + } + (_, None) => { + LastSeen::::remove( + &identifier, + Option::<>::UsageKey>::None, + ); + } + } + } + Self::deposit_event(Event::RateLimitCleared { transaction: identifier, scope, diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index 1a89951193..16639e2d5b 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -300,6 +300,8 @@ fn clear_rate_limit_removes_entry_and_emits_event() { RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }); let identifier = identifier_for(&target_call); Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(4))); + LastSeen::::insert(identifier, None::, 7); + LastSeen::::insert(identifier, Some(88u16), 9); assert_ok!(RateLimiting::clear_rate_limit( RuntimeOrigin::root(), @@ -307,6 +309,8 @@ fn clear_rate_limit_removes_entry_and_emits_event() { )); assert!(Limits::::get(identifier).is_none()); + assert!(LastSeen::::get(identifier, None::).is_none()); + assert!(LastSeen::::get(identifier, Some(88u16)).is_none()); match pop_last_event() { RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitCleared { @@ -350,6 +354,9 @@ fn clear_rate_limit_removes_only_selected_scope() { map.insert(9u16, RateLimitKind::Exact(7)); map.insert(10u16, RateLimitKind::Exact(5)); Limits::::insert(identifier, RateLimit::Scoped(map)); + LastSeen::::insert(identifier, Some(9u16), 11); + LastSeen::::insert(identifier, Some(10u16), 12); + LastSeen::::insert(identifier, None::, 13); let scoped_call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 9 }); @@ -365,6 +372,12 @@ fn clear_rate_limit_removes_only_selected_scope() { config.kind_for(Some(&10u16)).copied(), Some(RateLimitKind::Exact(5)) ); + assert!(LastSeen::::get(identifier, Some(9u16)).is_none()); + assert_eq!(LastSeen::::get(identifier, Some(10u16)), Some(12)); + assert_eq!( + LastSeen::::get(identifier, None::), + Some(13) + ); match pop_last_event() { RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitCleared { From fc60465b506b26f62aa71685ef2e0fae3dc6d99b Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 31 Oct 2025 16:02:15 +0300 Subject: [PATCH 18/95] Add rate limit bypassing --- pallets/rate-limiting/src/lib.rs | 27 ++++++++++--------- pallets/rate-limiting/src/mock.rs | 11 ++++++-- pallets/rate-limiting/src/tx_extension.rs | 33 ++++++++++++++++++++++- pallets/rate-limiting/src/types.rs | 24 +++++++++++++---- 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 38cfce1a5e..a5c5982307 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -45,7 +45,7 @@ //! //! # Context resolvers //! -//! The pallet relies on two resolvers, both implementing [`RateLimitContextResolver`]: +//! The pallet relies on two resolvers: //! //! - [`Config::LimitScopeResolver`], which determines how limits are stored (for example by //! returning a `netuid`). When this resolver returns `None`, the configuration is stored as a @@ -63,7 +63,7 @@ //! //! // Limits are scoped per netuid. //! pub struct ScopeResolver; -//! impl pallet_rate_limiting::RateLimitContextResolver for ScopeResolver { +//! impl pallet_rate_limiting::RateLimitScopeResolver for ScopeResolver { //! fn context(call: &RuntimeCall) -> Option { //! match call { //! RuntimeCall::Subtensor(pallet_subtensor::Call::set_weights { netuid, .. }) => { @@ -76,9 +76,8 @@ //! //! // Usage tracking distinguishes hyperparameter + netuid. //! pub struct UsageResolver; -//! impl pallet_rate_limiting::RateLimitContextResolver -//! for UsageResolver -//! { +//! impl pallet_rate_limiting::RateLimitUsageResolver +//! for UsageResolver { //! fn context(call: &RuntimeCall) -> Option<(NetUid, HyperParam)> { //! match call { //! RuntimeCall::Subtensor(pallet_subtensor::Call::set_hyperparam { @@ -105,7 +104,9 @@ pub use benchmarking::BenchmarkHelper; pub use pallet::*; pub use tx_extension::RateLimitTransactionExtension; -pub use types::{RateLimit, RateLimitContextResolver, RateLimitKind, TransactionIdentifier}; +pub use types::{ + RateLimit, RateLimitKind, RateLimitScopeResolver, RateLimitUsageResolver, TransactionIdentifier, +}; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; @@ -131,7 +132,10 @@ pub mod pallet { #[cfg(feature = "runtime-benchmarks")] use crate::benchmarking::BenchmarkHelper as BenchmarkHelperTrait; - use crate::types::{RateLimit, RateLimitContextResolver, RateLimitKind, TransactionIdentifier}; + use crate::types::{ + RateLimit, RateLimitKind, RateLimitScopeResolver, RateLimitUsageResolver, + TransactionIdentifier, + }; /// Configuration trait for the rate limiting pallet. #[pallet::config] @@ -152,13 +156,13 @@ pub mod pallet { type LimitScope: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize; /// Resolves the scope for the given runtime call when configuring limits. - type LimitScopeResolver: RateLimitContextResolver<>::RuntimeCall, Self::LimitScope>; + type LimitScopeResolver: RateLimitScopeResolver<>::RuntimeCall, Self::LimitScope>; /// Usage key tracked in [`LastSeen`] for rate-limited calls. type UsageKey: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize; /// Resolves the usage key for the given runtime call when enforcing limits. - type UsageResolver: RateLimitContextResolver<>::RuntimeCall, Self::UsageKey>; + type UsageResolver: RateLimitUsageResolver<>::RuntimeCall, Self::UsageKey>; /// Helper used to construct runtime calls for benchmarking. #[cfg(feature = "runtime-benchmarks")] @@ -465,10 +469,7 @@ pub mod pallet { LastSeen::::remove(&identifier, Some(key)); } (_, None) => { - LastSeen::::remove( - &identifier, - Option::<>::UsageKey>::None, - ); + LastSeen::::remove(&identifier, None::<>::UsageKey>); } } } diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index aec00b45bb..5fab86e4ab 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -61,7 +61,7 @@ pub type UsageKey = u16; pub struct TestScopeResolver; pub struct TestUsageResolver; -impl pallet_rate_limiting::RateLimitContextResolver for TestScopeResolver { +impl pallet_rate_limiting::RateLimitScopeResolver for TestScopeResolver { fn context(call: &RuntimeCall) -> Option { match call { RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { @@ -71,9 +71,16 @@ impl pallet_rate_limiting::RateLimitContextResolver for _ => None, } } + + fn should_bypass(call: &RuntimeCall) -> bool { + matches!( + call, + RuntimeCall::RateLimiting(RateLimitingCall::clear_rate_limit { .. }) + ) + } } -impl pallet_rate_limiting::RateLimitContextResolver for TestUsageResolver { +impl pallet_rate_limiting::RateLimitUsageResolver for TestUsageResolver { fn context(call: &RuntimeCall) -> Option { match call { RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index 5276f1f396..95696409ee 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -17,7 +17,7 @@ use sp_std::{marker::PhantomData, result::Result}; use crate::{ Config, LastSeen, Pallet, - types::{RateLimitContextResolver, TransactionIdentifier}, + types::{RateLimitScopeResolver, RateLimitUsageResolver, TransactionIdentifier}, }; /// Identifier returned in the transaction metadata for the rate limiting extension. @@ -97,6 +97,10 @@ where _inherited_implication: &impl Implication, _source: TransactionSource, ) -> ValidateResult>::RuntimeCall> { + if >::LimitScopeResolver::should_bypass(call) { + return Ok((ValidTransaction::default(), None, origin)); + } + let identifier = match TransactionIdentifier::from_call::(call) { Ok(identifier) => identifier, Err(_) => return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), @@ -178,6 +182,12 @@ mod tests { RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }) } + fn bypass_call() -> RuntimeCall { + RuntimeCall::RateLimiting(RateLimitingCall::clear_rate_limit { + call: Box::new(remark_call()), + }) + } + fn new_tx_extension() -> RateLimitTransactionExtension { RateLimitTransactionExtension(Default::default()) } @@ -242,6 +252,27 @@ mod tests { }); } + #[test] + fn tx_extension_honors_bypass_signal() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + let call = bypass_call(); + + let (valid, val, _) = + validate_with_tx_extension(&extension, &call).expect("bypass should succeed"); + assert_eq!(valid.priority, 0); + assert!(val.is_none()); + + let identifier = identifier_for(&call); + Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(3))); + LastSeen::::insert(identifier, None::, 1); + + let (_valid, post_val, _) = + validate_with_tx_extension(&extension, &call).expect("still bypassed"); + assert!(post_val.is_none()); + }); + } + #[test] fn tx_extension_records_last_seen_for_successful_call() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 4e68d0205a..0f4d4948f1 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -3,11 +3,25 @@ use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata}; use scale_info::TypeInfo; use sp_std::collections::btree_map::BTreeMap; -/// Resolves the optional identifier within which a rate limit applies. -pub trait RateLimitContextResolver { - /// Returns `Some(context)` when the limit should be applied per-context, or `None` for global - /// limits. - fn context(call: &Call) -> Option; +/// Resolves the optional identifier within which a rate limit applies and can optionally bypass +/// enforcement. +pub trait RateLimitScopeResolver { + /// Returns `Some(scope)` when the limit should be applied per-scope, or `None` for global + /// limits. + fn context(call: &Call) -> Option; + + /// Returns `true` when the rate limit should be bypassed for the provided call. Defaults to + /// `false`. + fn should_bypass(_call: &Call) -> bool { + false + } +} + +/// Resolves the optional usage tracking key applied when enforcing limits. +pub trait RateLimitUsageResolver { + /// Returns `Some(usage)` when usage should be tracked per-key, or `None` for global usage + /// tracking. + fn context(call: &Call) -> Option; } /// Identifies a runtime call by pallet and extrinsic indices. From 5c8a8abf746681bf18ef47c62cfa9c9aba212a04 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 31 Oct 2025 16:31:48 +0300 Subject: [PATCH 19/95] Add rate limit adjuster --- pallets/rate-limiting/src/lib.rs | 69 +++++++++++++++++------ pallets/rate-limiting/src/mock.rs | 15 ++++- pallets/rate-limiting/src/tests.rs | 4 +- pallets/rate-limiting/src/tx_extension.rs | 34 ++++++++++- pallets/rate-limiting/src/types.rs | 24 +++++--- 5 files changed, 115 insertions(+), 31 deletions(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index a5c5982307..e40b857ed6 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -48,8 +48,9 @@ //! The pallet relies on two resolvers: //! //! - [`Config::LimitScopeResolver`], which determines how limits are stored (for example by -//! returning a `netuid`). When this resolver returns `None`, the configuration is stored as a -//! global fallback. +//! returning a `netuid`). The resolver can also signal that a call should bypass rate limiting or +//! adjust the effective span at validation time. When it returns `None`, the configuration is +//! stored as a global fallback. //! - [`Config::UsageResolver`], which decides how executions are tracked in //! [`LastSeen`](pallet::LastSeen). This can refine the limit scope (for example by returning a //! tuple of `(netuid, hyperparameter)`). @@ -63,7 +64,7 @@ //! //! // Limits are scoped per netuid. //! pub struct ScopeResolver; -//! impl pallet_rate_limiting::RateLimitScopeResolver for ScopeResolver { +//! impl pallet_rate_limiting::RateLimitScopeResolver for ScopeResolver { //! fn context(call: &RuntimeCall) -> Option { //! match call { //! RuntimeCall::Subtensor(pallet_subtensor::Call::set_weights { netuid, .. }) => { @@ -72,12 +73,15 @@ //! _ => None, //! } //! } +//! +//! fn adjust_span(_call: &RuntimeCall, span: BlockNumber) -> BlockNumber { +//! span +//! } //! } //! //! // Usage tracking distinguishes hyperparameter + netuid. //! pub struct UsageResolver; -//! impl pallet_rate_limiting::RateLimitUsageResolver -//! for UsageResolver { +//! impl pallet_rate_limiting::RateLimitUsageResolver for UsageResolver { //! fn context(call: &RuntimeCall) -> Option<(NetUid, HyperParam)> { //! match call { //! RuntimeCall::Subtensor(pallet_subtensor::Call::set_hyperparam { @@ -156,7 +160,11 @@ pub mod pallet { type LimitScope: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize; /// Resolves the scope for the given runtime call when configuring limits. - type LimitScopeResolver: RateLimitScopeResolver<>::RuntimeCall, Self::LimitScope>; + type LimitScopeResolver: RateLimitScopeResolver< + >::RuntimeCall, + Self::LimitScope, + BlockNumberFor, + >; /// Usage key tracked in [`LastSeen`] for rate-limited calls. type UsageKey: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize; @@ -306,21 +314,17 @@ pub mod pallet { identifier: &TransactionIdentifier, scope: &Option<>::LimitScope>, usage_key: &Option<>::UsageKey>, + call: &>::RuntimeCall, ) -> Result { - let Some(block_span) = Self::resolved_limit(identifier, scope) else { + if >::LimitScopeResolver::should_bypass(call) { return Ok(true); - }; - - let current = frame_system::Pallet::::block_number(); - - if let Some(last) = LastSeen::::get(identifier, usage_key) { - let delta = current.saturating_sub(last); - if delta < block_span { - return Ok(false); - } } - Ok(true) + let Some(block_span) = Self::effective_span(call, identifier, scope) else { + return Ok(true); + }; + + Ok(Self::within_span(identifier, usage_key, block_span)) } pub(crate) fn resolved_limit( @@ -335,6 +339,37 @@ pub mod pallet { }) } + pub(crate) fn effective_span( + call: &>::RuntimeCall, + identifier: &TransactionIdentifier, + scope: &Option<>::LimitScope>, + ) -> Option> { + let span = Self::resolved_limit(identifier, scope)?; + Some(>::LimitScopeResolver::adjust_span( + call, span, + )) + } + + pub(crate) fn within_span( + identifier: &TransactionIdentifier, + usage_key: &Option<>::UsageKey>, + block_span: BlockNumberFor, + ) -> bool { + if block_span.is_zero() { + return true; + } + + if let Some(last) = LastSeen::::get(identifier, usage_key) { + let current = frame_system::Pallet::::block_number(); + let delta = current.saturating_sub(last); + if delta < block_span { + return false; + } + } + + true + } + /// Returns the configured limit for the specified pallet/extrinsic names, if any. pub fn limit_for_call_names( pallet_name: &str, diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index 5fab86e4ab..67321731a1 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -61,7 +61,9 @@ pub type UsageKey = u16; pub struct TestScopeResolver; pub struct TestUsageResolver; -impl pallet_rate_limiting::RateLimitScopeResolver for TestScopeResolver { +impl pallet_rate_limiting::RateLimitScopeResolver + for TestScopeResolver +{ fn context(call: &RuntimeCall) -> Option { match call { RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { @@ -78,6 +80,17 @@ impl pallet_rate_limiting::RateLimitScopeResolver for T RuntimeCall::RateLimiting(RateLimitingCall::clear_rate_limit { .. }) ) } + + fn adjust_span(call: &RuntimeCall, span: u64) -> u64 { + if matches!( + call, + RuntimeCall::RateLimiting(RateLimitingCall::clear_all_rate_limits { .. }) + ) { + span.saturating_mul(2) + } else { + span + } + } } impl pallet_rate_limiting::RateLimitUsageResolver for TestUsageResolver { diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index 16639e2d5b..c89543ae4b 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -121,7 +121,7 @@ fn is_within_limit_is_true_when_no_limit() { RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - let result = RateLimiting::is_within_limit(&identifier, &None, &None); + let result = RateLimiting::is_within_limit(&identifier, &None, &None, &call); assert_eq!(result.expect("no error expected"), true); }); } @@ -144,6 +144,7 @@ fn is_within_limit_false_when_rate_limited() { &identifier, &Some(1 as LimitScope), &Some(1 as UsageKey), + &call, ) .expect("call succeeds"); assert!(!within); @@ -168,6 +169,7 @@ fn is_within_limit_true_after_required_span() { &identifier, &Some(2 as LimitScope), &Some(2 as UsageKey), + &call, ) .expect("call succeeds"); assert!(within); diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index 95696409ee..623a6af3ac 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -109,7 +109,7 @@ where let scope = >::LimitScopeResolver::context(call); let usage = >::UsageResolver::context(call); - let Some(block_span) = Pallet::::resolved_limit(&identifier, &scope) else { + let Some(block_span) = Pallet::::effective_span(call, &identifier, &scope) else { return Ok((ValidTransaction::default(), None, origin)); }; @@ -117,8 +117,7 @@ where return Ok((ValidTransaction::default(), None, origin)); } - let within_limit = Pallet::::is_within_limit(&identifier, &scope, &usage) - .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + let within_limit = Pallet::::within_span(&identifier, &usage, block_span); if !within_limit { return Err(TransactionValidityError::Invalid( @@ -188,6 +187,12 @@ mod tests { }) } + fn adjustable_call() -> RuntimeCall { + RuntimeCall::RateLimiting(RateLimitingCall::clear_all_rate_limits { + call: Box::new(remark_call()), + }) + } + fn new_tx_extension() -> RateLimitTransactionExtension { RateLimitTransactionExtension(Default::default()) } @@ -273,6 +278,29 @@ mod tests { }); } + #[test] + fn tx_extension_applies_adjusted_span() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + let call = adjustable_call(); + let identifier = identifier_for(&call); + Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(4))); + LastSeen::::insert(identifier, Some(1u16), 10); + + System::set_block_number(14); + + // Stored span (4) would allow the call, but adjusted span (8) should block it. + let err = validate_with_tx_extension(&extension, &call) + .expect_err("adjusted span should apply"); + match err { + TransactionValidityError::Invalid(InvalidTransaction::Custom(code)) => { + assert_eq!(code, RATE_LIMIT_DENIED); + } + other => panic!("unexpected error: {:?}", other), + } + }); + } + #[test] fn tx_extension_records_last_seen_for_successful_call() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 0f4d4948f1..5d537bf64f 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -3,24 +3,30 @@ use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata}; use scale_info::TypeInfo; use sp_std::collections::btree_map::BTreeMap; -/// Resolves the optional identifier within which a rate limit applies and can optionally bypass -/// enforcement. -pub trait RateLimitScopeResolver { - /// Returns `Some(scope)` when the limit should be applied per-scope, or `None` for global - /// limits. +/// Resolves the optional identifier within which a rate limit applies and can optionally adjust +/// enforcement behaviour. +pub trait RateLimitScopeResolver { + /// Returns `Some(scope)` when the limit should be applied per-scope, or `None` for global + /// limits. fn context(call: &Call) -> Option; - /// Returns `true` when the rate limit should be bypassed for the provided call. Defaults to - /// `false`. + /// Returns `true` when the rate limit should be bypassed for the provided call. Defaults to + /// `false`. fn should_bypass(_call: &Call) -> bool { false } + + /// Optionally adjusts the effective span used during enforcement. Defaults to the original + /// `span`. + fn adjust_span(_call: &Call, span: Span) -> Span { + span + } } /// Resolves the optional usage tracking key applied when enforcing limits. pub trait RateLimitUsageResolver { - /// Returns `Some(usage)` when usage should be tracked per-key, or `None` for global usage - /// tracking. + /// Returns `Some(usage)` when usage should be tracked per-key, or `None` for global usage + /// tracking. fn context(call: &Call) -> Option; } From 1e8db7db79ef51089efa1b74923e1f95bc1206db Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 3 Nov 2025 15:28:09 +0300 Subject: [PATCH 20/95] Add api to migrate scope and usage keys --- pallets/rate-limiting/src/lib.rs | 70 +++++++++++++++++++ pallets/rate-limiting/src/tests.rs | 104 +++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index e40b857ed6..f57b9f0074 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -370,6 +370,76 @@ pub mod pallet { true } + /// Migrates a stored rate limit configuration from one scope to another. + /// + /// Returns `true` when an entry was moved. Passing identical `from`/`to` scopes simply + /// checks that a configuration exists. + pub fn migrate_limit_scope( + identifier: &TransactionIdentifier, + from: Option<>::LimitScope>, + to: Option<>::LimitScope>, + ) -> bool { + if from == to { + return Limits::::contains_key(identifier); + } + + let mut migrated = false; + Limits::::mutate(identifier, |maybe_config| { + if let Some(config) = maybe_config { + match (from.as_ref(), to.as_ref()) { + (None, Some(target)) => { + if let RateLimit::Global(kind) = config { + *config = RateLimit::scoped_single(target.clone(), *kind); + migrated = true; + } + } + (Some(source), Some(target)) => { + if let RateLimit::Scoped(map) = config { + if let Some(kind) = map.remove(source) { + map.insert(target.clone(), kind); + migrated = true; + } + } + } + (Some(source), None) => { + if let RateLimit::Scoped(map) = config { + if map.len() == 1 && map.contains_key(source) { + if let Some(kind) = map.remove(source) { + *config = RateLimit::global(kind); + migrated = true; + } + } + } + } + _ => {} + } + } + }); + + migrated + } + + /// Migrates the cached usage information for a rate-limited call to a new key. + /// + /// Returns `true` when an entry was moved. Passing identical keys simply checks that an + /// entry exists. + pub fn migrate_usage_key( + identifier: &TransactionIdentifier, + from: Option<>::UsageKey>, + to: Option<>::UsageKey>, + ) -> bool { + if from == to { + return LastSeen::::contains_key(identifier, &to); + } + + let Some(block) = LastSeen::::take(identifier, from) else { + return false; + }; + + LastSeen::::insert(identifier, to, block); + true + } + /// Returns the configured limit for the specified pallet/extrinsic names, if any. pub fn limit_for_call_names( pallet_name: &str, diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index c89543ae4b..0e051e9eb0 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -176,6 +176,110 @@ fn is_within_limit_true_after_required_span() { }); } +#[test] +fn migrate_limit_scope_global_to_scoped() { + new_test_ext().execute_with(|| { + let target_call = + RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }); + let identifier = identifier_for(&target_call); + + Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(3))); + + assert!(RateLimiting::migrate_limit_scope( + &identifier, + None, + Some(9) + )); + + match RateLimiting::limits(identifier).expect("config") { + RateLimit::Scoped(map) => { + assert_eq!(map.len(), 1); + assert_eq!(map.get(&9), Some(&RateLimitKind::Exact(3))); + } + other => panic!("unexpected config: {:?}", other), + } + }); +} + +#[test] +fn migrate_limit_scope_scoped_to_scoped() { + new_test_ext().execute_with(|| { + let target_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&target_call); + + let mut map = sp_std::collections::btree_map::BTreeMap::new(); + map.insert(1u16, RateLimitKind::Exact(4)); + map.insert(2u16, RateLimitKind::Exact(6)); + Limits::::insert(identifier, RateLimit::Scoped(map)); + + assert!(RateLimiting::migrate_limit_scope( + &identifier, + Some(1), + Some(3) + )); + + match RateLimiting::limits(identifier).expect("config") { + RateLimit::Scoped(map) => { + assert!(map.get(&1).is_none()); + assert_eq!(map.get(&3), Some(&RateLimitKind::Exact(4))); + assert_eq!(map.get(&2), Some(&RateLimitKind::Exact(6))); + } + other => panic!("unexpected config: {:?}", other), + } + }); +} + +#[test] +fn migrate_limit_scope_scoped_to_global() { + new_test_ext().execute_with(|| { + let target_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&target_call); + + let mut map = sp_std::collections::btree_map::BTreeMap::new(); + map.insert(7u16, RateLimitKind::Exact(8)); + Limits::::insert(identifier, RateLimit::Scoped(map)); + + assert!(RateLimiting::migrate_limit_scope( + &identifier, + Some(7), + None + )); + + match RateLimiting::limits(identifier).expect("config") { + RateLimit::Global(kind) => assert_eq!(kind, RateLimitKind::Exact(8)), + other => panic!("unexpected config: {:?}", other), + } + }); +} + +#[test] +fn migrate_usage_key_moves_entry() { + new_test_ext().execute_with(|| { + let target_call = + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let identifier = identifier_for(&target_call); + + LastSeen::::insert(identifier, Some(5u16), 11); + + assert!(RateLimiting::migrate_usage_key( + &identifier, + Some(5), + Some(6) + )); + assert!(LastSeen::::get(identifier, Some(5u16)).is_none()); + assert_eq!(LastSeen::::get(identifier, Some(6u16)), Some(11)); + + assert!(RateLimiting::migrate_usage_key(&identifier, Some(6), None)); + assert!(LastSeen::::get(identifier, Some(6u16)).is_none()); + assert_eq!( + LastSeen::::get(identifier, None::), + Some(11) + ); + }); +} + #[test] fn set_rate_limit_updates_storage_and_emits_event() { new_test_ext().execute_with(|| { From 232de64664e7cc9affe9394e45fcbf68023cdf55 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 3 Nov 2025 18:47:54 +0300 Subject: [PATCH 21/95] Pass origin to rate limit context resolvers --- pallets/rate-limiting/Cargo.toml | 2 + pallets/rate-limiting/src/benchmarking.rs | 11 +++- pallets/rate-limiting/src/lib.rs | 65 +++++++++++++++++------ pallets/rate-limiting/src/mock.rs | 14 ++--- pallets/rate-limiting/src/tests.rs | 11 ++-- pallets/rate-limiting/src/tx_extension.rs | 9 ++-- pallets/rate-limiting/src/types.rs | 16 +++--- 7 files changed, 88 insertions(+), 40 deletions(-) diff --git a/pallets/rate-limiting/Cargo.toml b/pallets/rate-limiting/Cargo.toml index 3447145622..36343ec2cf 100644 --- a/pallets/rate-limiting/Cargo.toml +++ b/pallets/rate-limiting/Cargo.toml @@ -17,6 +17,7 @@ frame-system.workspace = true scale-info = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"], optional = true } sp-std.workspace = true +sp-runtime.workspace = true subtensor-runtime-common.workspace = true [dev-dependencies] @@ -34,6 +35,7 @@ std = [ "scale-info/std", "serde", "sp-std/std", + "sp-runtime/std", "subtensor-runtime-common/std", ] runtime-benchmarks = [ diff --git a/pallets/rate-limiting/src/benchmarking.rs b/pallets/rate-limiting/src/benchmarking.rs index 65d547ab0b..2d700a4ef6 100644 --- a/pallets/rate-limiting/src/benchmarking.rs +++ b/pallets/rate-limiting/src/benchmarking.rs @@ -5,6 +5,7 @@ use codec::Decode; use frame_benchmarking::v2::*; use frame_system::{RawOrigin, pallet_prelude::BlockNumberFor}; +use sp_runtime::traits::DispatchOriginOf; use super::*; @@ -36,7 +37,10 @@ mod benchmarks { fn set_rate_limit() { let call = sample_call::(); let limit = RateLimitKind::>::Exact(BlockNumberFor::::from(10u32)); - let scope = ::LimitScopeResolver::context(call.as_ref()); + let origin = T::RuntimeOrigin::from(RawOrigin::Root); + let resolver_origin: DispatchOriginOf<::RuntimeCall> = + Into::::RuntimeCall>>::into(origin.clone()); + let scope = ::LimitScopeResolver::context(&resolver_origin, call.as_ref()); let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); @@ -61,7 +65,10 @@ mod benchmarks { fn clear_rate_limit() { let call = sample_call::(); let limit = RateLimitKind::>::Exact(BlockNumberFor::::from(10u32)); - let scope = ::LimitScopeResolver::context(call.as_ref()); + let origin = T::RuntimeOrigin::from(RawOrigin::Root); + let resolver_origin: DispatchOriginOf<::RuntimeCall> = + Into::::RuntimeCall>>::into(origin.clone()); + let scope = ::LimitScopeResolver::context(&resolver_origin, call.as_ref()); // Pre-populate limit for benchmark call let identifier = diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index f57b9f0074..4389b833a8 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -55,8 +55,8 @@ //! [`LastSeen`](pallet::LastSeen). This can refine the limit scope (for example by returning a //! tuple of `(netuid, hyperparameter)`). //! -//! Each resolver receives the call and may return `Some(identifier)` when scoping is required, or -//! `None` to use the global entry. Extrinsics such as +//! Each resolver receives the origin and call and may return `Some(identifier)` when scoping is +//! required, or `None` to use the global entry. Extrinsics such as //! [`set_rate_limit`](pallet::Pallet::set_rate_limit) automatically consult these resolvers. //! //! ```ignore @@ -64,8 +64,13 @@ //! //! // Limits are scoped per netuid. //! pub struct ScopeResolver; -//! impl pallet_rate_limiting::RateLimitScopeResolver for ScopeResolver { -//! fn context(call: &RuntimeCall) -> Option { +//! impl pallet_rate_limiting::RateLimitScopeResolver< +//! RuntimeOrigin, +//! RuntimeCall, +//! NetUid, +//! BlockNumber, +//! > for ScopeResolver { +//! fn context(origin: &RuntimeOrigin, call: &RuntimeCall) -> Option { //! match call { //! RuntimeCall::Subtensor(pallet_subtensor::Call::set_weights { netuid, .. }) => { //! Some(*netuid) @@ -74,15 +79,23 @@ //! } //! } //! -//! fn adjust_span(_call: &RuntimeCall, span: BlockNumber) -> BlockNumber { +//! fn should_bypass(origin: &RuntimeOrigin, _call: &RuntimeCall) -> bool { +//! matches!(origin, RuntimeOrigin::Root) +//! } +//! +//! fn adjust_span(_origin: &RuntimeOrigin, _call: &RuntimeCall, span: BlockNumber) -> BlockNumber { //! span //! } //! } //! //! // Usage tracking distinguishes hyperparameter + netuid. //! pub struct UsageResolver; -//! impl pallet_rate_limiting::RateLimitUsageResolver for UsageResolver { -//! fn context(call: &RuntimeCall) -> Option<(NetUid, HyperParam)> { +//! impl pallet_rate_limiting::RateLimitUsageResolver< +//! RuntimeOrigin, +//! RuntimeCall, +//! (NetUid, HyperParam), +//! > for UsageResolver { +//! fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option<(NetUid, HyperParam)> { //! match call { //! RuntimeCall::Subtensor(pallet_subtensor::Call::set_hyperparam { //! netuid, @@ -128,10 +141,10 @@ pub mod pallet { use codec::Codec; use frame_support::{ pallet_prelude::*, - sp_runtime::traits::{Saturating, Zero}, traits::{BuildGenesisConfig, EnsureOrigin, GetCallMetadata}, }; use frame_system::pallet_prelude::*; + use sp_runtime::traits::{DispatchOriginOf, Dispatchable, Saturating, Zero}; use sp_std::{convert::TryFrom, marker::PhantomData, vec::Vec}; #[cfg(feature = "runtime-benchmarks")] @@ -146,11 +159,14 @@ pub mod pallet { pub trait Config: frame_system::Config where BlockNumberFor: MaybeSerializeDeserialize, + <>::RuntimeCall as Dispatchable>::RuntimeOrigin: + From<::RuntimeOrigin>, { /// The overarching runtime call type. type RuntimeCall: Parameter + Codec + GetCallMetadata + + Dispatchable + IsType<::RuntimeCall>; /// Origin permitted to configure rate limits. @@ -161,6 +177,7 @@ pub mod pallet { /// Resolves the scope for the given runtime call when configuring limits. type LimitScopeResolver: RateLimitScopeResolver< + DispatchOriginOf<>::RuntimeCall>, >::RuntimeCall, Self::LimitScope, BlockNumberFor, @@ -170,7 +187,11 @@ pub mod pallet { type UsageKey: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize; /// Resolves the usage key for the given runtime call when enforcing limits. - type UsageResolver: RateLimitUsageResolver<>::RuntimeCall, Self::UsageKey>; + type UsageResolver: RateLimitUsageResolver< + DispatchOriginOf<>::RuntimeCall>, + >::RuntimeCall, + Self::UsageKey, + >; /// Helper used to construct runtime calls for benchmarking. #[cfg(feature = "runtime-benchmarks")] @@ -311,16 +332,17 @@ pub mod pallet { /// Returns `true` when the given transaction identifier passes its configured rate limit /// within the provided usage scope. pub fn is_within_limit( + origin: &DispatchOriginOf<>::RuntimeCall>, + call: &>::RuntimeCall, identifier: &TransactionIdentifier, scope: &Option<>::LimitScope>, usage_key: &Option<>::UsageKey>, - call: &>::RuntimeCall, ) -> Result { - if >::LimitScopeResolver::should_bypass(call) { + if >::LimitScopeResolver::should_bypass(origin, call) { return Ok(true); } - let Some(block_span) = Self::effective_span(call, identifier, scope) else { + let Some(block_span) = Self::effective_span(origin, call, identifier, scope) else { return Ok(true); }; @@ -340,13 +362,14 @@ pub mod pallet { } pub(crate) fn effective_span( + origin: &DispatchOriginOf<>::RuntimeCall>, call: &>::RuntimeCall, identifier: &TransactionIdentifier, scope: &Option<>::LimitScope>, ) -> Option> { let span = Self::resolved_limit(identifier, scope)?; Some(>::LimitScopeResolver::adjust_span( - call, span, + origin, call, span, )) } @@ -491,11 +514,15 @@ pub mod pallet { call: Box<>::RuntimeCall>, limit: RateLimitKind>, ) -> DispatchResult { + let resolver_origin: DispatchOriginOf<>::RuntimeCall> = + Into::>::RuntimeCall>>::into(origin.clone()); + let scope = + >::LimitScopeResolver::context(&resolver_origin, call.as_ref()); + let scope_for_event = scope.clone(); + T::AdminOrigin::ensure_origin(origin)?; let identifier = TransactionIdentifier::from_call::(call.as_ref())?; - let scope = >::LimitScopeResolver::context(call.as_ref()); - let scope_for_event = scope.clone(); if let Some(ref sc) = scope { Limits::::mutate(&identifier, |slot| match slot { @@ -532,11 +559,15 @@ pub mod pallet { origin: OriginFor, call: Box<>::RuntimeCall>, ) -> DispatchResult { + let resolver_origin: DispatchOriginOf<>::RuntimeCall> = + Into::>::RuntimeCall>>::into(origin.clone()); + let scope = + >::LimitScopeResolver::context(&resolver_origin, call.as_ref()); + let usage = >::UsageResolver::context(&resolver_origin, call.as_ref()); + T::AdminOrigin::ensure_origin(origin)?; let identifier = TransactionIdentifier::from_call::(call.as_ref())?; - let scope = >::LimitScopeResolver::context(call.as_ref()); - let usage = >::UsageResolver::context(call.as_ref()); let (pallet_name, extrinsic_name) = identifier.names::()?; let pallet = Vec::from(pallet_name.as_bytes()); diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index 67321731a1..fb7de0a400 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -61,10 +61,10 @@ pub type UsageKey = u16; pub struct TestScopeResolver; pub struct TestUsageResolver; -impl pallet_rate_limiting::RateLimitScopeResolver +impl pallet_rate_limiting::RateLimitScopeResolver for TestScopeResolver { - fn context(call: &RuntimeCall) -> Option { + fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option { match call { RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { (*block_span).try_into().ok() @@ -74,14 +74,14 @@ impl pallet_rate_limiting::RateLimitScopeResolver } } - fn should_bypass(call: &RuntimeCall) -> bool { + fn should_bypass(_origin: &RuntimeOrigin, call: &RuntimeCall) -> bool { matches!( call, RuntimeCall::RateLimiting(RateLimitingCall::clear_rate_limit { .. }) ) } - fn adjust_span(call: &RuntimeCall, span: u64) -> u64 { + fn adjust_span(_origin: &RuntimeOrigin, call: &RuntimeCall, span: u64) -> u64 { if matches!( call, RuntimeCall::RateLimiting(RateLimitingCall::clear_all_rate_limits { .. }) @@ -93,8 +93,10 @@ impl pallet_rate_limiting::RateLimitScopeResolver } } -impl pallet_rate_limiting::RateLimitUsageResolver for TestUsageResolver { - fn context(call: &RuntimeCall) -> Option { +impl pallet_rate_limiting::RateLimitUsageResolver + for TestUsageResolver +{ + fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option { match call { RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { (*block_span).try_into().ok() diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index 0e051e9eb0..a377d71656 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -121,7 +121,8 @@ fn is_within_limit_is_true_when_no_limit() { RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); let identifier = identifier_for(&call); - let result = RateLimiting::is_within_limit(&identifier, &None, &None, &call); + let origin = RuntimeOrigin::signed(1); + let result = RateLimiting::is_within_limit(&origin, &call, &identifier, &None, &None); assert_eq!(result.expect("no error expected"), true); }); } @@ -140,11 +141,13 @@ fn is_within_limit_false_when_rate_limited() { System::set_block_number(13); + let origin = RuntimeOrigin::signed(1); let within = RateLimiting::is_within_limit( + &origin, + &call, &identifier, &Some(1 as LimitScope), &Some(1 as UsageKey), - &call, ) .expect("call succeeds"); assert!(!within); @@ -165,11 +168,13 @@ fn is_within_limit_true_after_required_span() { System::set_block_number(20); + let origin = RuntimeOrigin::signed(1); let within = RateLimiting::is_within_limit( + &origin, + &call, &identifier, &Some(2 as LimitScope), &Some(2 as UsageKey), - &call, ) .expect("call succeeds"); assert!(within); diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index 623a6af3ac..c6c3eb745c 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -97,7 +97,7 @@ where _inherited_implication: &impl Implication, _source: TransactionSource, ) -> ValidateResult>::RuntimeCall> { - if >::LimitScopeResolver::should_bypass(call) { + if >::LimitScopeResolver::should_bypass(&origin, call) { return Ok((ValidTransaction::default(), None, origin)); } @@ -106,10 +106,11 @@ where Err(_) => return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), }; - let scope = >::LimitScopeResolver::context(call); - let usage = >::UsageResolver::context(call); + let scope = >::LimitScopeResolver::context(&origin, call); + let usage = >::UsageResolver::context(&origin, call); - let Some(block_span) = Pallet::::effective_span(call, &identifier, &scope) else { + let Some(block_span) = Pallet::::effective_span(&origin, call, &identifier, &scope) + else { return Ok((ValidTransaction::default(), None, origin)); }; diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 5d537bf64f..f2f46a6beb 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -5,29 +5,29 @@ use sp_std::collections::btree_map::BTreeMap; /// Resolves the optional identifier within which a rate limit applies and can optionally adjust /// enforcement behaviour. -pub trait RateLimitScopeResolver { +pub trait RateLimitScopeResolver { /// Returns `Some(scope)` when the limit should be applied per-scope, or `None` for global /// limits. - fn context(call: &Call) -> Option; + fn context(origin: &Origin, call: &Call) -> Option; - /// Returns `true` when the rate limit should be bypassed for the provided call. Defaults to - /// `false`. - fn should_bypass(_call: &Call) -> bool { + /// Returns `true` when the rate limit should be bypassed for the provided origin/call pair. + /// Defaults to `false`. + fn should_bypass(_origin: &Origin, _call: &Call) -> bool { false } /// Optionally adjusts the effective span used during enforcement. Defaults to the original /// `span`. - fn adjust_span(_call: &Call, span: Span) -> Span { + fn adjust_span(_origin: &Origin, _call: &Call, span: Span) -> Span { span } } /// Resolves the optional usage tracking key applied when enforcing limits. -pub trait RateLimitUsageResolver { +pub trait RateLimitUsageResolver { /// Returns `Some(usage)` when usage should be tracked per-key, or `None` for global usage /// tracking. - fn context(call: &Call) -> Option; + fn context(origin: &Origin, call: &Call) -> Option; } /// Identifies a runtime call by pallet and extrinsic indices. From e00342fc612ce0ae6b1b44684d4f34f03979bc0e Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 10 Nov 2025 18:03:52 +0300 Subject: [PATCH 22/95] Implement rate-limiting resolvers for pallet-subtensor --- Cargo.lock | 1 + common/src/lib.rs | 2 + common/src/rate_limiting.rs | 66 +++++ pallets/rate-limiting/Cargo.toml | 4 +- pallets/rate-limiting/src/lib.rs | 2 +- pallets/rate-limiting/src/types.rs | 28 +- pallets/subtensor/src/utils/rate_limiting.rs | 2 + runtime/Cargo.toml | 3 + runtime/src/lib.rs | 5 + runtime/src/rate_limiting.rs | 256 +++++++++++++++++++ 10 files changed, 357 insertions(+), 12 deletions(-) create mode 100644 common/src/rate_limiting.rs create mode 100644 runtime/src/rate_limiting.rs diff --git a/Cargo.lock b/Cargo.lock index a919bc7486..df6e2f9266 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8297,6 +8297,7 @@ dependencies = [ "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-preimage", + "pallet-rate-limiting", "pallet-registry", "pallet-safe-mode", "pallet-scheduler", diff --git a/common/src/lib.rs b/common/src/lib.rs index a98a957ad8..db4c314bbe 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -15,8 +15,10 @@ use sp_runtime::{ use subtensor_macros::freeze_struct; pub use currency::*; +pub use rate_limiting::{RateLimitScope, RateLimitUsageKey}; mod currency; +mod rate_limiting; /// Balance of an account. pub type Balance = u64; diff --git a/common/src/rate_limiting.rs b/common/src/rate_limiting.rs new file mode 100644 index 0000000000..3c88758943 --- /dev/null +++ b/common/src/rate_limiting.rs @@ -0,0 +1,66 @@ +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::pallet_prelude::Parameter; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; + +use crate::{MechId, NetUid}; + +#[derive( + Serialize, + Deserialize, + Encode, + Decode, + DecodeWithMemTracking, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + TypeInfo, + MaxEncodedLen, +)] +pub enum RateLimitScope { + Subnet(NetUid), + SubnetMechanism { netuid: NetUid, mecid: MechId }, +} + +#[derive( + Serialize, + Deserialize, + Encode, + Decode, + DecodeWithMemTracking, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + TypeInfo, + MaxEncodedLen, +)] +#[scale_info(skip_type_params(AccountId))] +pub enum RateLimitUsageKey { + Account(AccountId), + Subnet(NetUid), + AccountSubnet { + account: AccountId, + netuid: NetUid, + }, + ColdkeyHotkeySubnet { + coldkey: AccountId, + hotkey: AccountId, + netuid: NetUid, + }, + SubnetNeuron { + netuid: NetUid, + uid: u16, + }, + SubnetMechanismNeuron { + netuid: NetUid, + mecid: MechId, + uid: u16, + }, +} diff --git a/pallets/rate-limiting/Cargo.toml b/pallets/rate-limiting/Cargo.toml index 36343ec2cf..67e2710f4b 100644 --- a/pallets/rate-limiting/Cargo.toml +++ b/pallets/rate-limiting/Cargo.toml @@ -15,7 +15,7 @@ frame-benchmarking = { workspace = true, optional = true } frame-support.workspace = true frame-system.workspace = true scale-info = { workspace = true, features = ["derive"] } -serde = { workspace = true, features = ["derive"], optional = true } +serde = { workspace = true, features = ["derive"] } sp-std.workspace = true sp-runtime.workspace = true subtensor-runtime-common.workspace = true @@ -33,7 +33,7 @@ std = [ "frame-support/std", "frame-system/std", "scale-info/std", - "serde", + "serde/std", "sp-std/std", "sp-runtime/std", "subtensor-runtime-common/std", diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 4389b833a8..54dd54f5f2 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -145,7 +145,7 @@ pub mod pallet { }; use frame_system::pallet_prelude::*; use sp_runtime::traits::{DispatchOriginOf, Dispatchable, Saturating, Zero}; - use sp_std::{convert::TryFrom, marker::PhantomData, vec::Vec}; + use sp_std::{boxed::Box, convert::TryFrom, marker::PhantomData, vec::Vec}; #[cfg(feature = "runtime-benchmarks")] use crate::benchmarking::BenchmarkHelper as BenchmarkHelperTrait; diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index f2f46a6beb..4748f1576e 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -1,6 +1,7 @@ use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata}; use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; use sp_std::collections::btree_map::BTreeMap; /// Resolves the optional identifier within which a rate limit applies and can optionally adjust @@ -31,8 +32,9 @@ pub trait RateLimitUsageResolver { } /// Identifies a runtime call by pallet and extrinsic indices. -#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[derive( + Serialize, + Deserialize, Clone, Copy, PartialEq, @@ -101,8 +103,9 @@ impl TransactionIdentifier { } /// Policy describing the block span enforced by a rate limit. -#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[derive( + Serialize, + Deserialize, Clone, Copy, PartialEq, @@ -125,14 +128,21 @@ pub enum RateLimitKind { /// /// The configuration is mutually exclusive: either the call is globally limited or it stores a set /// of per-scope spans. -#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr( - feature = "std", - serde( - bound = "Scope: Ord + serde::Serialize + serde::de::DeserializeOwned, BlockNumber: serde::Serialize + serde::de::DeserializeOwned" - ) +#[derive( + Serialize, + Deserialize, + Clone, + PartialEq, + Eq, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + Debug, +)] +#[serde( + bound = "Scope: Ord + serde::Serialize + serde::de::DeserializeOwned, BlockNumber: serde::Serialize + serde::de::DeserializeOwned" )] -#[derive(Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, Debug)] pub enum RateLimit { /// Global span applied to every invocation. Global(RateLimitKind), diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index 85f58cfc64..468aecd1c1 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -1,3 +1,5 @@ +use codec::{Decode, Encode}; +use scale_info::TypeInfo; use subtensor_runtime_common::NetUid; use super::*; diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 9760ac1b53..b363eb4f5f 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -38,6 +38,7 @@ frame-system = { workspace = true } frame-try-runtime = { workspace = true, optional = true } pallet-timestamp.workspace = true pallet-transaction-payment.workspace = true +pallet-rate-limiting.workspace = true pallet-subtensor-utility.workspace = true frame-executive.workspace = true frame-metadata-hash-extension.workspace = true @@ -187,6 +188,7 @@ std = [ "pallet-timestamp/std", "pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment/std", + "pallet-rate-limiting/std", "pallet-subtensor-utility/std", "pallet-sudo/std", "pallet-multisig/std", @@ -328,6 +330,7 @@ try-runtime = [ "pallet-insecure-randomness-collective-flip/try-runtime", "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", + "pallet-rate-limiting/try-runtime", "pallet-subtensor-utility/try-runtime", "pallet-safe-mode/try-runtime", "pallet-subtensor/try-runtime", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 266a755708..9d80693ddb 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -12,6 +12,7 @@ use core::num::NonZeroU64; pub mod check_nonce; mod migrations; +mod rate_limiting; pub mod transaction_payment_wrapper; extern crate alloc; @@ -70,6 +71,10 @@ use subtensor_precompiles::Precompiles; use subtensor_runtime_common::{AlphaCurrency, TaoCurrency, time::*, *}; use subtensor_swap_interface::{Order, SwapHandler}; +pub use rate_limiting::{ + ScopeResolver as RuntimeScopeResolver, UsageResolver as RuntimeUsageResolver, +}; + // A few exports that help ease life for downstream crates. pub use frame_support::{ StorageValue, construct_runtime, parameter_types, diff --git a/runtime/src/rate_limiting.rs b/runtime/src/rate_limiting.rs new file mode 100644 index 0000000000..4f569c2daf --- /dev/null +++ b/runtime/src/rate_limiting.rs @@ -0,0 +1,256 @@ +use frame_system::RawOrigin; +use pallet_admin_utils::Call as AdminUtilsCall; +use pallet_rate_limiting::{RateLimitScopeResolver, RateLimitUsageResolver}; +use pallet_subtensor::{Call as SubtensorCall, Tempo}; +use subtensor_runtime_common::{BlockNumber, MechId, NetUid, RateLimitScope, RateLimitUsageKey}; + +use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; + +fn signed_origin(origin: &RuntimeOrigin) -> Option { + match origin.clone().into() { + Ok(RawOrigin::Signed(who)) => Some(who), + _ => None, + } +} + +fn tempo_scaled(netuid: NetUid, span: BlockNumber) -> BlockNumber { + if span == 0 { + return span; + } + let tempo = BlockNumber::from(Tempo::::get(netuid) as u32); + span.saturating_mul(tempo) +} + +fn neuron_identity(origin: &RuntimeOrigin, netuid: NetUid) -> Option<(AccountId, u16)> { + let hotkey = signed_origin(origin)?; + let uid = + pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey(netuid, &hotkey).ok()?; + Some((hotkey, uid)) +} + +fn owner_hparam_netuid(call: &AdminUtilsCall) -> Option { + match call { + AdminUtilsCall::sudo_set_activity_cutoff { netuid, .. } + | AdminUtilsCall::sudo_set_adjustment_alpha { netuid, .. } + | AdminUtilsCall::sudo_set_alpha_sigmoid_steepness { netuid, .. } + | AdminUtilsCall::sudo_set_alpha_values { netuid, .. } + | AdminUtilsCall::sudo_set_bonds_moving_average { netuid, .. } + | AdminUtilsCall::sudo_set_bonds_penalty { netuid, .. } + | AdminUtilsCall::sudo_set_bonds_reset_enabled { netuid, .. } + | AdminUtilsCall::sudo_set_commit_reveal_weights_enabled { netuid, .. } + | AdminUtilsCall::sudo_set_commit_reveal_weights_interval { netuid, .. } + | AdminUtilsCall::sudo_set_immunity_period { netuid, .. } + | AdminUtilsCall::sudo_set_liquid_alpha_enabled { netuid, .. } + | AdminUtilsCall::sudo_set_max_allowed_uids { netuid, .. } + | AdminUtilsCall::sudo_set_max_burn { netuid, .. } + | AdminUtilsCall::sudo_set_max_difficulty { netuid, .. } + | AdminUtilsCall::sudo_set_min_allowed_weights { netuid, .. } + | AdminUtilsCall::sudo_set_min_burn { netuid, .. } + | AdminUtilsCall::sudo_set_network_pow_registration_allowed { netuid, .. } + | AdminUtilsCall::sudo_set_owner_immune_neuron_limit { netuid, .. } + | AdminUtilsCall::sudo_set_recycle_or_burn { netuid, .. } + | AdminUtilsCall::sudo_set_rho { netuid, .. } + | AdminUtilsCall::sudo_set_serving_rate_limit { netuid, .. } + | AdminUtilsCall::sudo_set_sn_owner_hotkey { netuid, .. } + | AdminUtilsCall::sudo_set_toggle_transfer { netuid, .. } + | AdminUtilsCall::sudo_set_weights_version_key { netuid, .. } + | AdminUtilsCall::sudo_set_yuma3_enabled { netuid, .. } => Some(*netuid), + _ => None, + } +} + +fn admin_scope_netuid(call: &AdminUtilsCall) -> Option { + owner_hparam_netuid(call).or_else(|| match call { + AdminUtilsCall::sudo_set_mechanism_count { netuid, .. } + | AdminUtilsCall::sudo_set_mechanism_emission_split { netuid, .. } + | AdminUtilsCall::sudo_trim_to_max_allowed_uids { netuid, .. } => Some(*netuid), + _ => None, + }) +} + +#[derive(Default)] +pub struct UsageResolver; + +impl RateLimitUsageResolver> + for UsageResolver +{ + fn context(origin: &RuntimeOrigin, call: &RuntimeCall) -> Option> { + match call { + RuntimeCall::SubtensorModule(inner) => match inner { + SubtensorCall::swap_hotkey { .. } => { + signed_origin(origin).map(RateLimitUsageKey::::Account) + } + SubtensorCall::register_network { .. } + | SubtensorCall::register_network_with_identity { .. } => { + signed_origin(origin).map(RateLimitUsageKey::::Account) + } + SubtensorCall::increase_take { hotkey, .. } => { + Some(RateLimitUsageKey::::Account(hotkey.clone())) + } + SubtensorCall::set_childkey_take { hotkey, netuid, .. } + | SubtensorCall::set_children { hotkey, netuid, .. } => { + Some(RateLimitUsageKey::::AccountSubnet { + account: hotkey.clone(), + netuid: *netuid, + }) + } + SubtensorCall::set_weights { netuid, .. } + | SubtensorCall::commit_weights { netuid, .. } + | SubtensorCall::reveal_weights { netuid, .. } + | SubtensorCall::batch_reveal_weights { netuid, .. } + | SubtensorCall::commit_timelocked_weights { netuid, .. } => { + let (_, uid) = neuron_identity(origin, netuid)?; + Some(RateLimitUsageKey::::SubnetNeuron { netuid, uid }) + } + SubtensorCall::set_mechanism_weights { netuid, mecid, .. } + | SubtensorCall::commit_mechanism_weights { netuid, mecid, .. } + | SubtensorCall::reveal_mechanism_weights { netuid, mecid, .. } + | SubtensorCall::commit_crv3_mechanism_weights { netuid, mecid, .. } + | SubtensorCall::commit_timelocked_mechanism_weights { netuid, mecid, .. } => { + let (_, uid) = neuron_identity(origin, netuid)?; + Some(RateLimitUsageKey::::SubnetMechanismNeuron { + netuid, + mecid, + uid, + }) + } + SubtensorCall::serve_axon { netuid, .. } + | SubtensorCall::serve_axon_tls { netuid, .. } + | SubtensorCall::serve_prometheus { netuid, .. } => { + let hotkey = signed_origin(origin)?; + Some(RateLimitUsageKey::::AccountSubnet { + account: hotkey, + netuid: *netuid, + }) + } + SubtensorCall::associate_evm_key { netuid, .. } => { + let hotkey = signed_origin(origin)?; + let uid = pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey( + *netuid, &hotkey, + ) + .ok()?; + Some(RateLimitUsageKey::::SubnetNeuron { + netuid: *netuid, + uid, + }) + } + SubtensorCall::add_stake { hotkey, netuid, .. } + | SubtensorCall::add_stake_limit { hotkey, netuid, .. } + | SubtensorCall::remove_stake { hotkey, netuid, .. } + | SubtensorCall::remove_stake_limit { hotkey, netuid, .. } + | SubtensorCall::remove_stake_full_limit { hotkey, netuid, .. } + | SubtensorCall::transfer_stake { + hotkey, + origin_netuid: netuid, + .. + } + | SubtensorCall::swap_stake { + hotkey, + origin_netuid: netuid, + .. + } + | SubtensorCall::swap_stake_limit { + hotkey, + origin_netuid: netuid, + .. + } + | SubtensorCall::move_stake { + origin_hotkey: hotkey, + origin_netuid: netuid, + .. + } + | SubtensorCall::recycle_alpha { hotkey, netuid, .. } + | SubtensorCall::burn_alpha { hotkey, netuid, .. } => { + let coldkey = signed_origin(origin)?; + Some(RateLimitUsageKey::::ColdkeyHotkeySubnet { + coldkey, + hotkey: hotkey.clone(), + netuid: *netuid, + }) + } + _ => None, + }, + RuntimeCall::AdminUtils(inner) => { + if let Some(netuid) = owner_hparam_netuid(inner) { + // Hyperparameter setters share a global span but are tracked per subnet. + Some(RateLimitUsageKey::::Subnet(netuid)) + } else { + match inner { + AdminUtilsCall::sudo_set_mechanism_count { netuid, .. } + | AdminUtilsCall::sudo_set_mechanism_emission_split { netuid, .. } + | AdminUtilsCall::sudo_trim_to_max_allowed_uids { netuid, .. } => { + let who = signed_origin(origin)?; + Some(RateLimitUsageKey::::AccountSubnet { + account: who, + netuid: *netuid, + }) + } + _ => None, + } + } + } + _ => None, + } + } +} + +#[derive(Default)] +pub struct ScopeResolver; + +impl RateLimitScopeResolver + for ScopeResolver +{ + fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option { + match call { + RuntimeCall::SubtensorModule(inner) => match inner { + SubtensorCall::serve_axon { netuid, .. } + | SubtensorCall::serve_axon_tls { netuid, .. } + | SubtensorCall::serve_prometheus { netuid, .. } + | SubtensorCall::set_weights { netuid, .. } + | SubtensorCall::commit_weights { netuid, .. } + | SubtensorCall::reveal_weights { netuid, .. } + | SubtensorCall::batch_reveal_weights { netuid, .. } + | SubtensorCall::commit_timelocked_weights { netuid, .. } => { + Some(RateLimitScope::Subnet(*netuid)) + } + SubtensorCall::set_mechanism_weights { netuid, mecid, .. } + | SubtensorCall::commit_mechanism_weights { netuid, mecid, .. } + | SubtensorCall::reveal_mechanism_weights { netuid, mecid, .. } + | SubtensorCall::commit_crv3_mechanism_weights { netuid, mecid, .. } + | SubtensorCall::commit_timelocked_mechanism_weights { netuid, mecid, .. } => { + Some(RateLimitScope::SubnetMechanism { + netuid: *netuid, + mecid: *mecid, + }) + } + _ => None, + }, + RuntimeCall::AdminUtils(inner) => { + if owner_hparam_netuid(inner).is_some() { + // Hyperparameter setters share a global limit span; usage is tracked per subnet. + None + } else { + admin_scope_netuid(inner).map(RateLimitScope::Subnet) + } + } + _ => None, + } + } + + fn should_bypass(origin: &RuntimeOrigin, _call: &RuntimeCall) -> bool { + matches!(origin.clone().into(), Ok(RawOrigin::Root)) + } + + fn adjust_span(_origin: &RuntimeOrigin, call: &RuntimeCall, span: BlockNumber) -> BlockNumber { + match call { + RuntimeCall::AdminUtils(inner) => { + if let Some(netuid) = owner_hparam_netuid(inner) { + tempo_scaled(netuid, span) + } else { + span + } + } + _ => span, + } + } +} From 69715be7c0d3b185c47812302b6563af7aedd58e Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 12 Nov 2025 20:00:37 +0300 Subject: [PATCH 23/95] Migrate pallet-subtensor's rate limiting to pallet-rate-limiting --- Cargo.lock | 1 + pallets/rate-limiting/src/lib.rs | 12 + pallets/subtensor/Cargo.toml | 2 + runtime/Cargo.toml | 2 +- runtime/src/rate_limiting/migration.rs | 733 ++++++++++++++++++ .../mod.rs} | 2 + 6 files changed, 751 insertions(+), 1 deletion(-) create mode 100644 runtime/src/rate_limiting/migration.rs rename runtime/src/{rate_limiting.rs => rate_limiting/mod.rs} (99%) diff --git a/Cargo.lock b/Cargo.lock index df6e2f9266..04686202fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10757,6 +10757,7 @@ dependencies = [ "pallet-crowdloan", "pallet-drand", "pallet-preimage", + "pallet-rate-limiting", "pallet-scheduler", "pallet-subtensor-proxy", "pallet-subtensor-swap", diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 54dd54f5f2..d823a94cd5 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -393,6 +393,18 @@ pub mod pallet { true } + /// Inserts or updates the cached usage timestamp for a rate-limited call. + /// + /// This is primarily intended for migrations that need to hydrate the new tracking storage + /// from legacy pallets. + pub fn record_last_seen( + identifier: &TransactionIdentifier, + usage_key: Option<>::UsageKey>, + block_number: BlockNumberFor, + ) { + LastSeen::::insert(identifier, usage_key, block_number); + } + /// Migrates a stored rate limit configuration from one scope to another. /// /// Returns `true` when an entry was moved. Passing identical `from`/`to` scopes simply diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index fdd5e5f9ab..3cde2ee24b 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -55,6 +55,7 @@ sha2.workspace = true rand_chacha.workspace = true pallet-crowdloan.workspace = true pallet-subtensor-proxy.workspace = true +pallet-rate-limiting.workspace = true [dev-dependencies] pallet-balances = { workspace = true, features = ["std"] } @@ -112,6 +113,7 @@ std = [ "pallet-crowdloan/std", "pallet-drand/std", "pallet-subtensor-proxy/std", + "pallet-rate-limiting/std", "pallet-subtensor-swap/std", "subtensor-swap-interface/std", "pallet-subtensor-utility/std", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b363eb4f5f..5d40215c49 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -53,6 +53,7 @@ sp-inherents.workspace = true sp-offchain.workspace = true sp-runtime.workspace = true sp-session.workspace = true +sp-io.workspace = true sp-std.workspace = true sp-transaction-pool.workspace = true sp-version.workspace = true @@ -154,7 +155,6 @@ ethereum.workspace = true [dev-dependencies] frame-metadata.workspace = true -sp-io.workspace = true sp-tracing.workspace = true [build-dependencies] diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs new file mode 100644 index 0000000000..243c43b7ea --- /dev/null +++ b/runtime/src/rate_limiting/migration.rs @@ -0,0 +1,733 @@ +use core::convert::TryFrom; + +use codec::Encode; +use frame_support::{pallet_prelude::Parameter, traits::Get, weights::Weight}; +use frame_system::pallet_prelude::BlockNumberFor; +use log::info; +use pallet_rate_limiting::{RateLimit, RateLimitKind, TransactionIdentifier}; +use sp_io::{ + hashing::{blake2_128, twox_128}, + storage, +}; +use sp_runtime::traits::SaturatedConversion; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; +use subtensor_runtime_common::{MechId, NetUid, RateLimitScope, RateLimitUsageKey}; + +use pallet_subtensor::{ + self, + utils::rate_limiting::{Hyperparameter, TransactionType}, + AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, LastRateLimitedBlock, + LastUpdate, MaxUidsTrimmingRateLimit, MechanismCountCurrent, MechanismCountSetRateLimit, + MechanismEmissionRateLimit, NetworkRateLimit, OwnerHyperparamRateLimit, Pallet, Prometheus, + RateLimitKey, TransactionKeyLastBlock, TxChildkeyTakeRateLimit, TxDelegateTakeRateLimit, + TxRateLimit, WeightsVersionKeyRateLimit, +}; + +/// Pallet index assigned to `pallet_subtensor` in `construct_runtime!`. +const SUBTENSOR_PALLET_INDEX: u8 = 7; +/// Pallet index assigned to `pallet_admin_utils` in `construct_runtime!`. +const ADMIN_UTILS_PALLET_INDEX: u8 = 19; + +/// Marker stored in `HasMigrationRun` once the migration finishes. +const MIGRATION_NAME: &[u8] = b"migrate_rate_limiting"; + +/// `set_children` is rate-limited to once every 150 blocks. +const SET_CHILDREN_RATE_LIMIT: u64 = 150; +/// `set_sn_owner_hotkey` default interval (blocks). +const DEFAULT_SET_SN_OWNER_HOTKEY_LIMIT: u64 = 50_400; + +/// Subtensor call indices that reuse the serving rate-limit configuration. +/// TODO(grouped-rate-limits): `serve_axon` (4), `serve_axon_tls` (40), and +/// `serve_prometheus` (5) share one cooldown today. The new pallet still misses +/// grouped identifiers, so we simply port the timers as-is. +const SERVE_CALLS: [u8; 3] = [4, 40, 5]; +/// Subtensor call indices that reuse the per-subnet weight limit. +/// TODO(grouped-rate-limits): Weight commits via call 100 still touch the same +/// `LastUpdate` entries but cannot be expressed here until grouping exists. +const WEIGHT_CALLS_SUBNET: [u8; 3] = [0, 96, 113]; +/// Subtensor call indices that reuse the per-mechanism weight limit. +const WEIGHT_CALLS_MECHANISM: [u8; 4] = [119, 115, 117, 118]; +/// Subtensor call indices for register-network extrinsics. +/// TODO(grouped-rate-limits): `register_network` (59) and +/// `register_network_with_identity` (79) still share the same helper and should +/// remain grouped once pallet-rate-limiting supports aliases. +const REGISTER_NETWORK_CALLS: [u8; 2] = [59, 79]; + +/// Hyperparameter extrinsics routed through owner-or-root rate limiting. +const HYPERPARAMETERS: &[Hyperparameter] = &[ + Hyperparameter::ServingRateLimit, + Hyperparameter::MaxDifficulty, + Hyperparameter::AdjustmentAlpha, + Hyperparameter::ImmunityPeriod, + Hyperparameter::MinAllowedWeights, + Hyperparameter::MaxAllowedUids, + Hyperparameter::Kappa, + Hyperparameter::Rho, + Hyperparameter::ActivityCutoff, + Hyperparameter::PowRegistrationAllowed, + Hyperparameter::MinBurn, + Hyperparameter::MaxBurn, + Hyperparameter::BondsMovingAverage, + Hyperparameter::BondsPenalty, + Hyperparameter::CommitRevealEnabled, + Hyperparameter::LiquidAlphaEnabled, + Hyperparameter::AlphaValues, + Hyperparameter::WeightCommitInterval, + Hyperparameter::TransferEnabled, + Hyperparameter::AlphaSigmoidSteepness, + Hyperparameter::Yuma3Enabled, + Hyperparameter::BondsResetEnabled, + Hyperparameter::ImmuneNeuronLimit, + Hyperparameter::RecycleOrBurn, +]; + +type RateLimitConfigOf = RateLimit>; +type LimitEntries = Vec<(TransactionIdentifier, RateLimitConfigOf)>; +type LastSeenKey = ( + TransactionIdentifier, + Option::AccountId>>, +); +type LastSeenEntries = Vec<(LastSeenKey, BlockNumberFor)>; + +pub fn migrate_rate_limiting() -> Weight { + let mut weight = T::DbWeight::get().reads(1); + if HasMigrationRun::::get(MIGRATION_NAME) { + info!("Rate-limiting migration already executed. Skipping."); + return weight; + } + + let (limits, limit_reads) = build_limits::(); + let (last_seen, seen_reads) = build_last_seen::(); + + let limit_writes = write_limits::(&limits); + let seen_writes = write_last_seen::(&last_seen); + + HasMigrationRun::::insert(MIGRATION_NAME, true); + + weight = weight + .saturating_add(T::DbWeight::get().reads(limit_reads.saturating_add(seen_reads))) + .saturating_add( + T::DbWeight::get().writes(limit_writes.saturating_add(seen_writes).saturating_add(1)), + ); + + info!( + "Migrated {} rate-limit configs and {} last-seen entries into pallet-rate-limiting", + limits.len(), + last_seen.len() + ); + + weight +} + +fn build_limits() -> (LimitEntries, u64) { + let mut limits = LimitEntries::::new(); + let mut reads: u64 = 0; + + reads += gather_simple_limits::(&mut limits); + reads += gather_owner_hparam_limits::(&mut limits); + reads += gather_serving_limits::(&mut limits); + reads += gather_weight_limits::(&mut limits); + + (limits, reads) +} + +fn gather_simple_limits(limits: &mut LimitEntries) -> u64 { + let mut reads: u64 = 0; + + reads += 1; + if let Some(span) = block_number::(TxRateLimit::::get()) { + set_global_limit::(limits, subtensor_identifier(70), span); + } + + reads += 1; + if let Some(span) = block_number::(TxDelegateTakeRateLimit::::get()) { + // TODO(grouped-rate-limits): `decrease_take` shares the same timestamp but + // does not have its own ID here yet. + set_global_limit::(limits, subtensor_identifier(66), span); + } + + reads += 1; + if let Some(span) = block_number::(TxChildkeyTakeRateLimit::::get()) { + set_global_limit::(limits, subtensor_identifier(75), span); + } + + reads += 1; + if let Some(span) = block_number::(NetworkRateLimit::::get()) { + for call in REGISTER_NETWORK_CALLS { + set_global_limit::(limits, subtensor_identifier(call), span); + } + } + + reads += 1; + if let Some(span) = block_number::(WeightsVersionKeyRateLimit::::get()) { + set_global_limit::(limits, admin_utils_identifier(6), span); + } + + if let Some(span) = block_number::(DEFAULT_SET_SN_OWNER_HOTKEY_LIMIT) { + set_global_limit::(limits, admin_utils_identifier(67), span); + } + + if let Some(span) = block_number::(::EvmKeyAssociateRateLimit::get()) { + set_global_limit::(limits, subtensor_identifier(93), span); + } + + if let Some(span) = block_number::(MechanismCountSetRateLimit::::get()) { + set_global_limit::(limits, admin_utils_identifier(76), span); + } + + if let Some(span) = block_number::(MechanismEmissionRateLimit::::get()) { + set_global_limit::(limits, admin_utils_identifier(77), span); + } + + if let Some(span) = block_number::(MaxUidsTrimmingRateLimit::::get()) { + set_global_limit::(limits, admin_utils_identifier(78), span); + } + + if let Some(span) = block_number::(SET_CHILDREN_RATE_LIMIT) { + set_global_limit::(limits, subtensor_identifier(67), span); + } + + reads +} + +fn gather_owner_hparam_limits(limits: &mut LimitEntries) -> u64 { + let mut reads: u64 = 0; + + reads += 1; + if let Some(span) = block_number::(u64::from(OwnerHyperparamRateLimit::::get())) { + for hparam in HYPERPARAMETERS { + if let Some(identifier) = identifier_for_hyperparameter(*hparam) { + set_global_limit::(limits, identifier, span); + } + } + } + + reads +} + +fn gather_serving_limits(limits: &mut LimitEntries) -> u64 { + let mut reads: u64 = 0; + let netuids = Pallet::::get_all_subnet_netuids(); + + for netuid in netuids { + reads += 1; + if let Some(span) = block_number::(Pallet::::get_serving_rate_limit(netuid)) { + for call in SERVE_CALLS { + set_scoped_limit::( + limits, + subtensor_identifier(call), + RateLimitScope::Subnet(netuid), + span, + ); + } + } + } + + reads +} + +fn gather_weight_limits(limits: &mut LimitEntries) -> u64 { + let mut reads: u64 = 0; + let netuids = Pallet::::get_all_subnet_netuids(); + + let mut subnet_limits = BTreeMap::>::new(); + for netuid in &netuids { + reads += 1; + if let Some(span) = block_number::(Pallet::::get_weights_set_rate_limit(*netuid)) { + subnet_limits.insert(*netuid, span); + for call in WEIGHT_CALLS_SUBNET { + set_scoped_limit::( + limits, + subtensor_identifier(call), + RateLimitScope::Subnet(*netuid), + span, + ); + } + } + } + + for netuid in &netuids { + reads += 1; + let mech_count: u8 = MechanismCountCurrent::::get(*netuid).into(); + if mech_count <= 1 { + continue; + } + let Some(span) = subnet_limits.get(netuid).copied() else { + continue; + }; + for mecid in 1..mech_count { + let scope = RateLimitScope::SubnetMechanism { + netuid: *netuid, + mecid: MechId::from(mecid), + }; + for call in WEIGHT_CALLS_MECHANISM { + set_scoped_limit::(limits, subtensor_identifier(call), scope.clone(), span); + } + } + } + + reads +} + +fn build_last_seen() -> (LastSeenEntries, u64) { + let mut last_seen = LastSeenEntries::::new(); + let mut reads: u64 = 0; + + reads += import_last_rate_limited_blocks::(&mut last_seen); + reads += import_transaction_key_last_blocks::(&mut last_seen); + reads += import_last_update_entries::(&mut last_seen); + reads += import_serving_entries::(&mut last_seen); + reads += import_evm_entries::(&mut last_seen); + + (last_seen, reads) +} + +fn import_last_rate_limited_blocks(entries: &mut LastSeenEntries) -> u64 { + let mut reads: u64 = 0; + for (key, block) in LastRateLimitedBlock::::iter() { + reads += 1; + if block == 0 { + continue; + } + match key { + RateLimitKey::SetSNOwnerHotkey(netuid) => { + if let Some(identifier) = + identifier_for_transaction_type(TransactionType::SetSNOwnerHotkey) + { + record_last_seen_entry::( + entries, + identifier, + Some(RateLimitUsageKey::Subnet(netuid)), + block, + ); + } + } + RateLimitKey::OwnerHyperparamUpdate(netuid, hyper) => { + if let Some(identifier) = identifier_for_hyperparameter(hyper) { + record_last_seen_entry::( + entries, + identifier, + Some(RateLimitUsageKey::Subnet(netuid)), + block, + ); + } + } + RateLimitKey::LastTxBlock(account) => { + record_last_seen_entry::( + entries, + subtensor_identifier(70), + Some(RateLimitUsageKey::Account(account.clone())), + block, + ); + } + RateLimitKey::LastTxBlockDelegateTake(account) => { + record_last_seen_entry::( + entries, + subtensor_identifier(66), + Some(RateLimitUsageKey::Account(account.clone())), + block, + ); + } + RateLimitKey::NetworkLastRegistered | RateLimitKey::LastTxBlockChildKeyTake(_) => { + // TODO(grouped-rate-limits): Global network registration lock is still outside + // pallet-rate-limiting. We will migrate it once grouped identifiers land. + } + } + } + reads +} + +fn import_transaction_key_last_blocks(entries: &mut LastSeenEntries) -> u64 { + let mut reads: u64 = 0; + for ((account, netuid, tx_kind), block) in TransactionKeyLastBlock::::iter() { + reads += 1; + if block == 0 { + continue; + } + let tx_type = TransactionType::from(tx_kind); + let Some(identifier) = identifier_for_transaction_type(tx_type) else { + continue; + }; + let Some(usage) = usage_key_from_transaction_type(tx_type, &account, netuid) else { + continue; + }; + record_last_seen_entry::(entries, identifier, Some(usage), block); + } + reads +} + +fn import_last_update_entries(entries: &mut LastSeenEntries) -> u64 { + let mut reads: u64 = 0; + for (index, blocks) in LastUpdate::::iter() { + reads += 1; + let netuid = Pallet::::get_netuid(index); + let sub_id = u16::from(index) + .checked_div(pallet_subtensor::subnets::mechanism::GLOBAL_MAX_SUBNET_COUNT) + .unwrap_or_default(); + let is_mechanism = sub_id != 0; + let Ok(sub_id) = u8::try_from(sub_id) else { + continue; + }; + let mecid = MechId::from(sub_id); + + for (uid, last_block) in blocks.into_iter().enumerate() { + if last_block == 0 { + continue; + } + let Ok(uid_u16) = u16::try_from(uid) else { + continue; + }; + let usage = if is_mechanism { + RateLimitUsageKey::SubnetMechanismNeuron { + netuid, + mecid, + uid: uid_u16, + } + } else { + RateLimitUsageKey::SubnetNeuron { + netuid, + uid: uid_u16, + } + }; + + let call_set: &[u8] = if is_mechanism { + &WEIGHT_CALLS_MECHANISM + } else { + &WEIGHT_CALLS_SUBNET + }; + + for call in call_set { + record_last_seen_entry::( + entries, + subtensor_identifier(*call), + Some(usage.clone()), + last_block, + ); + } + } + } + reads +} + +fn import_serving_entries(entries: &mut LastSeenEntries) -> u64 { + let mut reads: u64 = 0; + for (netuid, hotkey, axon) in Axons::::iter() { + reads += 1; + if axon.block == 0 { + continue; + } + let usage = RateLimitUsageKey::AccountSubnet { + account: hotkey.clone(), + netuid, + }; + for call in [4u8, 40u8] { + record_last_seen_entry::( + entries, + subtensor_identifier(call), + Some(usage.clone()), + axon.block, + ); + } + } + + for (netuid, hotkey, prom) in Prometheus::::iter() { + reads += 1; + if prom.block == 0 { + continue; + } + let usage = RateLimitUsageKey::AccountSubnet { + account: hotkey, + netuid, + }; + record_last_seen_entry::(entries, subtensor_identifier(5), Some(usage), prom.block); + } + + reads +} + +fn import_evm_entries(entries: &mut LastSeenEntries) -> u64 { + let mut reads: u64 = 0; + for (netuid, uid, (_, block)) in AssociatedEvmAddress::::iter() { + reads += 1; + if block == 0 { + continue; + } + record_last_seen_entry::( + entries, + subtensor_identifier(93), + Some(RateLimitUsageKey::SubnetNeuron { netuid, uid }), + block, + ); + } + reads +} + +/// TODO(rate-limiting-storage): Swap these manual writes for +/// `pallet_rate_limiting::Pallet` APIs once the runtime wires the pallet in. +fn write_limits(limits: &LimitEntries) -> u64 { + if limits.is_empty() { + return 0; + } + let prefix = storage_prefix("RateLimiting", "Limits"); + let mut writes = 0; + for (identifier, limit) in limits.iter() { + let key = map_storage_key(&prefix, identifier); + storage::set(&key, &limit.encode()); + writes += 1; + } + writes +} + +fn write_last_seen(entries: &LastSeenEntries) -> u64 { + if entries.is_empty() { + return 0; + } + let prefix = storage_prefix("RateLimiting", "LastSeen"); + let mut writes = 0; + for ((identifier, usage), block) in entries.iter() { + let key = double_map_storage_key(&prefix, identifier, usage); + storage::set(&key, &block.encode()); + writes += 1; + } + writes +} + +fn block_number(value: u64) -> Option> { + if value == 0 { + return None; + } + Some(value.saturated_into::>()) +} + +fn set_global_limit( + limits: &mut LimitEntries, + identifier: TransactionIdentifier, + span: BlockNumberFor, +) { + if let Some((_, config)) = limits.iter_mut().find(|(id, _)| *id == identifier) { + *config = RateLimit::global(RateLimitKind::Exact(span)); + } else { + limits.push((identifier, RateLimit::global(RateLimitKind::Exact(span)))); + } +} + +fn set_scoped_limit( + limits: &mut LimitEntries, + identifier: TransactionIdentifier, + scope: RateLimitScope, + span: BlockNumberFor, +) { + if let Some((_, config)) = limits.iter_mut().find(|(id, _)| *id == identifier) { + match config { + RateLimit::Global(_) => { + *config = RateLimit::scoped_single(scope, RateLimitKind::Exact(span)); + } + RateLimit::Scoped(map) => { + map.insert(scope, RateLimitKind::Exact(span)); + } + } + } else { + limits.push(( + identifier, + RateLimit::scoped_single(scope, RateLimitKind::Exact(span)), + )); + } +} + +fn record_last_seen_entry( + entries: &mut LastSeenEntries, + identifier: TransactionIdentifier, + usage: Option>, + block: u64, +) { + let Some(block_number) = block_number::(block) else { + return; + }; + + let key = (identifier, usage); + if let Some((_, existing)) = entries.iter_mut().find(|(entry_key, _)| *entry_key == key) { + if block_number > *existing { + *existing = block_number; + } + } else { + entries.push((key, block_number)); + } +} + +fn storage_prefix(pallet: &str, storage: &str) -> Vec { + let mut out = Vec::with_capacity(32); + out.extend_from_slice(&twox_128(pallet.as_bytes())); + out.extend_from_slice(&twox_128(storage.as_bytes())); + out +} + +fn map_storage_key(prefix: &[u8], key: impl Encode) -> Vec { + let mut final_key = Vec::with_capacity(prefix.len() + 32); + final_key.extend_from_slice(prefix); + let encoded = key.encode(); + let hash = blake2_128(&encoded); + final_key.extend_from_slice(&hash); + final_key.extend_from_slice(&encoded); + final_key +} + +fn double_map_storage_key(prefix: &[u8], key1: impl Encode, key2: impl Encode) -> Vec { + let mut final_key = Vec::with_capacity(prefix.len() + 64); + final_key.extend_from_slice(prefix); + let first = map_storage_key(&[], key1); + final_key.extend_from_slice(&first); + let second = map_storage_key(&[], key2); + final_key.extend_from_slice(&second); + final_key +} + +const fn admin_utils_identifier(call_index: u8) -> TransactionIdentifier { + TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, call_index) +} + +const fn subtensor_identifier(call_index: u8) -> TransactionIdentifier { + TransactionIdentifier::new(SUBTENSOR_PALLET_INDEX, call_index) +} + +/// Returns the `TransactionIdentifier` for the admin-utils extrinsic that controls `hparam`. +/// +/// Only hyperparameters that are currently rate-limited (i.e. routed through +/// `ensure_sn_owner_or_root_with_limits`) are mapped; others return `None`. +pub fn identifier_for_hyperparameter(hparam: Hyperparameter) -> Option { + use Hyperparameter::*; + + let identifier = match hparam { + Unknown | MaxWeightLimit => return None, + ServingRateLimit => admin_utils_identifier(3), + MaxDifficulty => admin_utils_identifier(5), + AdjustmentAlpha => admin_utils_identifier(9), + ImmunityPeriod => admin_utils_identifier(13), + MinAllowedWeights => admin_utils_identifier(14), + MaxAllowedUids => admin_utils_identifier(15), + Kappa => admin_utils_identifier(16), + Rho => admin_utils_identifier(17), + ActivityCutoff => admin_utils_identifier(18), + PowRegistrationAllowed => admin_utils_identifier(20), + MinBurn => admin_utils_identifier(22), + MaxBurn => admin_utils_identifier(23), + BondsMovingAverage => admin_utils_identifier(26), + BondsPenalty => admin_utils_identifier(60), + CommitRevealEnabled => admin_utils_identifier(49), + LiquidAlphaEnabled => admin_utils_identifier(50), + AlphaValues => admin_utils_identifier(51), + WeightCommitInterval => admin_utils_identifier(57), + TransferEnabled => admin_utils_identifier(61), + AlphaSigmoidSteepness => admin_utils_identifier(68), + Yuma3Enabled => admin_utils_identifier(69), + BondsResetEnabled => admin_utils_identifier(70), + ImmuneNeuronLimit => admin_utils_identifier(72), + RecycleOrBurn => admin_utils_identifier(80), + _ => return None, + }; + + Some(identifier) +} + +/// Returns the `TransactionIdentifier` for the extrinsic associated with the given transaction +/// type, mirroring current rate-limit enforcement. +pub fn identifier_for_transaction_type(tx: TransactionType) -> Option { + use TransactionType::*; + + let identifier = match tx { + SetChildren => subtensor_identifier(67), + SetChildkeyTake => subtensor_identifier(75), + RegisterNetwork => subtensor_identifier(59), + SetWeightsVersionKey => admin_utils_identifier(6), + SetSNOwnerHotkey => admin_utils_identifier(67), + OwnerHyperparamUpdate(hparam) => return identifier_for_hyperparameter(hparam), + MechanismCountUpdate => admin_utils_identifier(76), + MechanismEmission => admin_utils_identifier(77), + MaxUidsTrimming => admin_utils_identifier(78), + Unknown => return None, + _ => return None, + }; + + Some(identifier) +} + +/// Maps legacy `RateLimitKey` entries to the new usage-key representation. +pub fn usage_key_from_legacy_key( + key: &RateLimitKey, +) -> Option> +where + AccountId: Parameter + Clone, +{ + match key { + RateLimitKey::SetSNOwnerHotkey(netuid) => Some(RateLimitUsageKey::Subnet(*netuid)), + RateLimitKey::OwnerHyperparamUpdate(netuid, _) => Some(RateLimitUsageKey::Subnet(*netuid)), + RateLimitKey::NetworkLastRegistered => None, + RateLimitKey::LastTxBlock(account) + | RateLimitKey::LastTxBlockChildKeyTake(account) + | RateLimitKey::LastTxBlockDelegateTake(account) => { + Some(RateLimitUsageKey::Account(account.clone())) + } + } +} + +/// Produces the usage key for a `TransactionType` that was stored in `TransactionKeyLastBlock`. +pub fn usage_key_from_transaction_type( + tx: TransactionType, + account: &AccountId, + netuid: NetUid, +) -> Option> +where + AccountId: Parameter + Clone, +{ + match tx { + TransactionType::SetChildren | TransactionType::SetChildkeyTake => { + Some(RateLimitUsageKey::AccountSubnet { + account: account.clone(), + netuid, + }) + } + TransactionType::SetWeightsVersionKey => Some(RateLimitUsageKey::Subnet(netuid)), + TransactionType::MechanismCountUpdate + | TransactionType::MechanismEmission + | TransactionType::MaxUidsTrimming => Some(RateLimitUsageKey::AccountSubnet { + account: account.clone(), + netuid, + }), + TransactionType::OwnerHyperparamUpdate(_) => Some(RateLimitUsageKey::Subnet(netuid)), + TransactionType::RegisterNetwork => Some(RateLimitUsageKey::Account(account.clone())), + TransactionType::SetSNOwnerHotkey => Some(RateLimitUsageKey::Subnet(netuid)), + TransactionType::Unknown => None, + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_hyperparameters() { + assert_eq!( + identifier_for_hyperparameter(Hyperparameter::ServingRateLimit), + Some(admin_utils_identifier(3)) + ); + assert!(identifier_for_hyperparameter(Hyperparameter::MaxWeightLimit).is_none()); + } + + #[test] + fn maps_transaction_types() { + assert_eq!( + identifier_for_transaction_type(TransactionType::SetChildren), + Some(subtensor_identifier(67)) + ); + assert!(identifier_for_transaction_type(TransactionType::Unknown).is_none()); + } + + #[test] + fn maps_usage_keys() { + let acct = 42u64; + assert!(matches!( + usage_key_from_legacy_key(&RateLimitKey::LastTxBlock(acct)), + Some(RateLimitUsageKey::Account(42)) + )); + } +} diff --git a/runtime/src/rate_limiting.rs b/runtime/src/rate_limiting/mod.rs similarity index 99% rename from runtime/src/rate_limiting.rs rename to runtime/src/rate_limiting/mod.rs index 019cdcd458..713c8bacf6 100644 --- a/runtime/src/rate_limiting.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -6,6 +6,8 @@ use subtensor_runtime_common::{BlockNumber, NetUid, RateLimitScope, RateLimitUsa use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; +pub(crate) mod migration; + fn signed_origin(origin: &RuntimeOrigin) -> Option { match origin.clone().into() { Ok(RawOrigin::Signed(who)) => Some(who), From 821f1ff5ebce853d040c4c9acfdb5735ba29c1bc Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 19 Nov 2025 16:07:37 +0300 Subject: [PATCH 24/95] Add grouped targets for pallet-rate-limiting --- pallets/rate-limiting/src/benchmarking.rs | 161 +++- pallets/rate-limiting/src/lib.rs | 822 +++++++++++++++---- pallets/rate-limiting/src/mock.rs | 8 +- pallets/rate-limiting/src/tests.rs | 928 ++++++++++++---------- pallets/rate-limiting/src/tx_extension.rs | 163 +++- pallets/rate-limiting/src/types.rs | 108 ++- runtime/src/rate_limiting/migration.rs | 29 +- 7 files changed, 1525 insertions(+), 694 deletions(-) diff --git a/pallets/rate-limiting/src/benchmarking.rs b/pallets/rate-limiting/src/benchmarking.rs index 2d700a4ef6..23dfecec85 100644 --- a/pallets/rate-limiting/src/benchmarking.rs +++ b/pallets/rate-limiting/src/benchmarking.rs @@ -5,7 +5,7 @@ use codec::Decode; use frame_benchmarking::v2::*; use frame_system::{RawOrigin, pallet_prelude::BlockNumberFor}; -use sp_runtime::traits::DispatchOriginOf; +use sp_runtime::traits::{One, Saturating}; use super::*; @@ -29,59 +29,144 @@ where Box::new(T::BenchmarkHelper::sample_call()) } +fn seed_group(name: &[u8], sharing: GroupSharing) -> ::GroupId { + Pallet::::create_group(RawOrigin::Root.into(), name.to_vec(), sharing) + .expect("group created"); + Pallet::::next_group_id().saturating_sub(::GroupId::one()) +} + +fn register_call_with_group( + group: Option<::GroupId>, +) -> TransactionIdentifier { + let call = sample_call::(); + let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("id"); + Pallet::::register_call(RawOrigin::Root.into(), call, group).expect("registered"); + identifier +} + #[benchmarks] mod benchmarks { use super::*; + use sp_std::vec::Vec; #[benchmark] - fn set_rate_limit() { + fn register_call() { let call = sample_call::(); - let limit = RateLimitKind::>::Exact(BlockNumberFor::::from(10u32)); - let origin = T::RuntimeOrigin::from(RawOrigin::Root); - let resolver_origin: DispatchOriginOf<::RuntimeCall> = - Into::::RuntimeCall>>::into(origin.clone()); - let scope = ::LimitScopeResolver::context(&resolver_origin, call.as_ref()); - let identifier = - TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); + let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("id"); + let target = RateLimitTarget::Transaction(identifier); #[extrinsic_call] - _(RawOrigin::Root, call, limit.clone()); - - let stored = Limits::::get(&identifier).expect("limit stored"); - match (scope, &stored) { - (Some(ref sc), RateLimit::Scoped(map)) => { - assert_eq!(map.get(sc), Some(&limit)); - } - (None, RateLimit::Global(kind)) | (Some(_), RateLimit::Global(kind)) => { - assert_eq!(kind, &limit); - } - (None, RateLimit::Scoped(map)) => { - assert!(map.values().any(|k| k == &limit)); - } - } + _(RawOrigin::Root, call, None); + + assert!(Limits::::contains_key(target)); } #[benchmark] - fn clear_rate_limit() { + fn set_rate_limit() { let call = sample_call::(); + let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("id"); + let target = RateLimitTarget::Transaction(identifier); + Limits::::insert(target, RateLimit::global(RateLimitKind::Default)); + let limit = RateLimitKind::>::Exact(BlockNumberFor::::from(10u32)); - let origin = T::RuntimeOrigin::from(RawOrigin::Root); - let resolver_origin: DispatchOriginOf<::RuntimeCall> = - Into::::RuntimeCall>>::into(origin.clone()); - let scope = ::LimitScopeResolver::context(&resolver_origin, call.as_ref()); - - // Pre-populate limit for benchmark call - let identifier = - TransactionIdentifier::from_call::(call.as_ref()).expect("identifier"); - match scope.clone() { - Some(sc) => Limits::::insert(identifier, RateLimit::scoped_single(sc, limit)), - None => Limits::::insert(identifier, RateLimit::global(limit)), - } #[extrinsic_call] - _(RawOrigin::Root, call); + _(RawOrigin::Root, target, None, limit); + + let stored = Limits::::get(target).expect("limit stored"); + assert!( + matches!(stored, RateLimit::Global(RateLimitKind::Exact(span)) if span == BlockNumberFor::::from(10u32)) + ); + } + + #[benchmark] + fn assign_call_to_group() { + let group = seed_group::(b"grp", GroupSharing::UsageOnly); + let identifier = register_call_with_group::(None); + + #[extrinsic_call] + _(RawOrigin::Root, identifier, group); + + assert_eq!(CallGroups::::get(identifier), Some(group)); + assert!(GroupMembers::::get(group).contains(&identifier)); + } + + #[benchmark] + fn remove_call_from_group() { + let group = seed_group::(b"team", GroupSharing::ConfigOnly); + let identifier = register_call_with_group::(Some(group)); + + #[extrinsic_call] + _(RawOrigin::Root, identifier); + + assert!(CallGroups::::get(identifier).is_none()); + assert!(!GroupMembers::::get(group).contains(&identifier)); + } + + #[benchmark] + fn create_group() { + let name = b"bench".to_vec(); + let sharing = GroupSharing::ConfigAndUsage; + + #[extrinsic_call] + _(RawOrigin::Root, name.clone(), sharing); + + let group = Pallet::::next_group_id().saturating_sub(::GroupId::one()); + let details = Groups::::get(group).expect("group stored"); + let stored: Vec = details.name.into(); + assert_eq!(stored, name); + assert_eq!(details.sharing, sharing); + } + + #[benchmark] + fn update_group() { + let group = seed_group::(b"old", GroupSharing::UsageOnly); + let new_name = b"new".to_vec(); + let new_sharing = GroupSharing::ConfigAndUsage; + + #[extrinsic_call] + _( + RawOrigin::Root, + group, + Some(new_name.clone()), + Some(new_sharing), + ); + + let details = Groups::::get(group).expect("group exists"); + let stored: Vec = details.name.into(); + assert_eq!(stored, new_name); + assert_eq!(details.sharing, new_sharing); + } + + #[benchmark] + fn delete_group() { + let group = seed_group::(b"delete", GroupSharing::UsageOnly); + + #[extrinsic_call] + _(RawOrigin::Root, group); + + assert!(Groups::::get(group).is_none()); + } + + #[benchmark] + fn deregister_call() { + let group = seed_group::(b"dreg", GroupSharing::ConfigAndUsage); + let identifier = register_call_with_group::(Some(group)); + let target = RateLimitTarget::Transaction(identifier); + let usage_target = Pallet::::usage_target(&identifier).expect("usage target"); + LastSeen::::insert( + usage_target, + None::, + BlockNumberFor::::from(1u32), + ); + + #[extrinsic_call] + _(RawOrigin::Root, identifier, None, true); - assert!(Limits::::get(identifier).is_none()); + assert!(Limits::::get(target).is_none()); + assert!(LastSeen::::get(usage_target, None::).is_none()); + assert!(CallGroups::::get(identifier).is_none()); + assert!(!GroupMembers::::get(group).contains(&identifier)); } #[benchmark] diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index d823a94cd5..2782e32cd0 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -5,26 +5,38 @@ //! # Overview //! //! `pallet-rate-limiting` lets a runtime restrict how frequently particular calls can execute. -//! Limits are stored on-chain, keyed by the call's pallet/variant pair. Each entry can specify an -//! exact block span or defer to a configured default. The pallet exposes three extrinsics, -//! restricted by [`Config::AdminOrigin`], to manage this data: +//! Limits are stored on-chain, keyed by explicit [`RateLimitTarget`] values. A target is either a +//! single [`TransactionIdentifier`] (the pallet/extrinsic indices) or a named *group* managed by the +//! admin APIs. Groups provide a way to give multiple calls the same configuration and/or usage +//! tracking without duplicating storage. Each target entry stores either a global span or a set of +//! scoped spans resolved at runtime. The pallet exposes a handful of extrinsics, restricted by +//! [`Config::AdminOrigin`], to manage this data: //! -//! - [`set_rate_limit`](pallet::Pallet::set_rate_limit): assign a limit to an extrinsic by -//! supplying a [`RateLimitKind`] span. The pallet infers the *limit scope* (for example a -//! `netuid`) using [`Config::LimitScopeResolver`] and stores the configuration for that scope, or -//! globally when no scope is resolved. -//! - [`clear_rate_limit`](pallet::Pallet::clear_rate_limit): remove a stored limit for the scope -//! derived from the provided call (or the global entry when no scope resolves). +//! - [`register_call`](pallet::Pallet::register_call): register a call for rate limiting, seed its +//! initial configuration using [`Config::LimitScopeResolver`], and optionally place it into a +//! group. +//! - [`set_rate_limit`](pallet::Pallet::set_rate_limit): assign or override the limit at a specific +//! target/scope by supplying a [`RateLimitKind`] span. +//! - [`assign_call_to_group`](pallet::Pallet::assign_call_to_group) and +//! [`remove_call_from_group`](pallet::Pallet::remove_call_from_group): manage group membership for +//! registered calls. +//! - [`deregister_call`](pallet::Pallet::deregister_call): remove scoped configuration or wipe the +//! registration entirely. //! - [`set_default_rate_limit`](pallet::Pallet::set_default_rate_limit): set the global default //! block span used by `RateLimitKind::Default` entries. //! -//! The pallet also tracks the last block in which a rate-limited call was executed, per optional -//! *usage key*. A usage key may refine tracking beyond the limit scope (for example combining a -//! `netuid` with a hyperparameter name), so the two concepts are explicitly separated in the -//! configuration. +//! The pallet also tracks the last block in which a target was observed, per optional *usage key*. +//! A usage key may refine tracking beyond the limit scope (for example combining a `netuid` with a +//! hyperparameter), so the two concepts are explicitly separated in the configuration. When the +//! admin puts several calls into a group and marks usage as shared, each dispatch still runs the +//! resolver: the group only chooses the storage target, while the resolver output (or `None`) picks +//! the row under that target. Calls that resolve to the same usage key update the same timestamp; +//! calls that resolve to different keys keep isolated timers even when they share a group. The same +//! rule applies to limit scopes—grouping funnels configuration into the same target, but the scope +//! resolver decides whether that entry is global or per-context. //! //! Each storage map is namespaced by pallet instance; runtimes can deploy multiple independent -//! instances to manage distinct rate-limiting scopes. +//! instances to manage distinct rate-limiting scopes (in the global sense). //! //! # Transaction extension //! @@ -57,7 +69,11 @@ //! //! Each resolver receives the origin and call and may return `Some(identifier)` when scoping is //! required, or `None` to use the global entry. Extrinsics such as -//! [`set_rate_limit`](pallet::Pallet::set_rate_limit) automatically consult these resolvers. +//! [`set_rate_limit`](pallet::Pallet::set_rate_limit) automatically consult these resolvers. When a +//! call belongs to a group the pallet still runs the resolver—instead of indexing storage at the +//! transaction-level target, it indexes at the group target. Resolving to different contexts keeps +//! independent limit/usage rows even though the calls share a group; resolving to the same context +//! causes them to share enforcement state. //! //! ```ignore //! pub struct WeightsContextResolver; @@ -122,7 +138,8 @@ pub use benchmarking::BenchmarkHelper; pub use pallet::*; pub use tx_extension::RateLimitTransactionExtension; pub use types::{ - RateLimit, RateLimitKind, RateLimitScopeResolver, RateLimitUsageResolver, TransactionIdentifier, + GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitScopeResolver, + RateLimitTarget, RateLimitUsageResolver, TransactionIdentifier, }; #[cfg(feature = "runtime-benchmarks")] @@ -140,20 +157,28 @@ mod tests; pub mod pallet { use codec::Codec; use frame_support::{ + BoundedBTreeSet, BoundedVec, pallet_prelude::*, traits::{BuildGenesisConfig, EnsureOrigin, GetCallMetadata}, }; use frame_system::pallet_prelude::*; - use sp_runtime::traits::{DispatchOriginOf, Dispatchable, Saturating, Zero}; + use sp_runtime::traits::{ + AtLeast32BitUnsigned, DispatchOriginOf, Dispatchable, Member, One, Saturating, Zero, + }; use sp_std::{boxed::Box, convert::TryFrom, marker::PhantomData, vec::Vec}; #[cfg(feature = "runtime-benchmarks")] use crate::benchmarking::BenchmarkHelper as BenchmarkHelperTrait; use crate::types::{ - RateLimit, RateLimitKind, RateLimitScopeResolver, RateLimitUsageResolver, - TransactionIdentifier, + GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitScopeResolver, + RateLimitTarget, RateLimitUsageResolver, TransactionIdentifier, }; + type GroupNameOf = BoundedVec>::MaxGroupNameLength>; + type GroupMembersOf = + BoundedBTreeSet>::MaxGroupMembers>; + type GroupDetailsOf = RateLimitGroup<>::GroupId, GroupNameOf>; + /// Configuration trait for the rate limiting pallet. #[pallet::config] pub trait Config: frame_system::Config @@ -193,30 +218,45 @@ pub mod pallet { Self::UsageKey, >; + /// Identifier assigned to managed groups. + type GroupId: Parameter + + Member + + Copy + + MaybeSerializeDeserialize + + MaxEncodedLen + + AtLeast32BitUnsigned + + Default; + + /// Maximum number of extrinsics that may belong to a single group. + #[pallet::constant] + type MaxGroupMembers: Get; + + /// Maximum length (in bytes) of a group name. + #[pallet::constant] + type MaxGroupNameLength: Get; + /// Helper used to construct runtime calls for benchmarking. #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper: BenchmarkHelperTrait<>::RuntimeCall>; } - /// Storage mapping from transaction identifier to its configured rate limit. + /// Storage mapping from rate limit target to its configured rate limit. #[pallet::storage] #[pallet::getter(fn limits)] pub type Limits, I: 'static = ()> = StorageMap< _, Blake2_128Concat, - TransactionIdentifier, + RateLimitTarget<>::GroupId>, RateLimit<>::LimitScope, BlockNumberFor>, OptionQuery, >; - /// Tracks when a transaction was last observed. - /// - /// The second key is `None` for global tracking and `Some(key)` for scoped usage tracking. + /// Tracks when a rate-limited target was last observed per usage key. #[pallet::storage] pub type LastSeen, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, - TransactionIdentifier, + RateLimitTarget<>::GroupId>, Blake2_128Concat, Option<>::UsageKey>, BlockNumberFor, @@ -229,48 +269,131 @@ pub mod pallet { pub type DefaultLimit, I: 'static = ()> = StorageValue<_, BlockNumberFor, ValueQuery>; + /// Maps a transaction identifier to its assigned group. + #[pallet::storage] + #[pallet::getter(fn call_group)] + pub type CallGroups, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + TransactionIdentifier, + >::GroupId, + OptionQuery, + >; + + /// Metadata for each configured group. + #[pallet::storage] + #[pallet::getter(fn groups)] + pub type Groups, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + >::GroupId, + GroupDetailsOf, + OptionQuery, + >; + + /// Tracks membership for each group. + #[pallet::storage] + #[pallet::getter(fn group_members)] + pub type GroupMembers, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + >::GroupId, + GroupMembersOf, + ValueQuery, + >; + + /// Enforces unique group names. + #[pallet::storage] + #[pallet::getter(fn group_id_by_name)] + pub type GroupNameIndex, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, GroupNameOf, >::GroupId, OptionQuery>; + + /// Identifier used for the next group creation. + #[pallet::storage] + #[pallet::getter(fn next_group_id)] + pub type NextGroupId, I: 'static = ()> = + StorageValue<_, >::GroupId, ValueQuery>; + /// Events emitted by the rate limiting pallet. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { - /// A rate limit was set or updated. - RateLimitSet { - /// Identifier of the affected transaction. + /// A call was registered for rate limiting. + CallRegistered { + /// Identifier of the registered transaction. transaction: TransactionIdentifier, - /// Limit scope to which the configuration applies, if any. + /// Scope seeded during registration (if any). scope: Option<>::LimitScope>, - /// The rate limit policy applied to the transaction. - limit: RateLimitKind>, + /// Optional group assignment applied at registration time. + group: Option<>::GroupId>, /// Pallet name associated with the transaction. pallet: Vec, /// Extrinsic name associated with the transaction. extrinsic: Vec, }, - /// A rate limit was cleared. - RateLimitCleared { - /// Identifier of the affected transaction. - transaction: TransactionIdentifier, - /// Limit scope from which the configuration was cleared, if any. + /// A rate limit was set or updated for the specified target. + RateLimitSet { + /// Target whose configuration changed. + target: RateLimitTarget<>::GroupId>, + /// Identifier of the transaction when the target represents a call. + transaction: Option, + /// Limit scope to which the configuration applies, if any. scope: Option<>::LimitScope>, - /// Pallet name associated with the transaction. - pallet: Vec, - /// Extrinsic name associated with the transaction. - extrinsic: Vec, + /// The rate limit policy applied to the target. + limit: RateLimitKind>, + /// Pallet name associated with the transaction, when available. + pallet: Option>, + /// Extrinsic name associated with the transaction, when available. + extrinsic: Option>, }, - /// All scoped and global rate limits for a call were cleared. - AllRateLimitsCleared { - /// Identifier of the affected transaction. - transaction: TransactionIdentifier, - /// Pallet name associated with the transaction. - pallet: Vec, - /// Extrinsic name associated with the transaction. - extrinsic: Vec, + /// A rate-limited call was deregistered or had a scoped entry cleared. + CallDeregistered { + /// Target whose configuration changed. + target: RateLimitTarget<>::GroupId>, + /// Identifier of the transaction when the target represents a call. + transaction: Option, + /// Limit scope from which the configuration was cleared, if any. + scope: Option<>::LimitScope>, + /// Pallet name associated with the transaction, when available. + pallet: Option>, + /// Extrinsic name associated with the transaction, when available. + extrinsic: Option>, }, /// The default rate limit was set or updated. DefaultRateLimitSet { /// The new default limit expressed in blocks. block_span: BlockNumberFor, }, + /// A group was created. + GroupCreated { + /// Identifier of the new group. + group: >::GroupId, + /// Human readable group name. + name: Vec, + /// Sharing policy configured for the group. + sharing: GroupSharing, + }, + /// A group's metadata or policy changed. + GroupUpdated { + /// Identifier of the group. + group: >::GroupId, + /// Human readable name. + name: Vec, + /// Updated sharing configuration. + sharing: GroupSharing, + }, + /// A group was deleted. + GroupDeleted { + /// Identifier of the removed group. + group: >::GroupId, + }, + /// A transaction was assigned to or removed from a group. + CallGroupUpdated { + /// Identifier of the transaction. + transaction: TransactionIdentifier, + /// Updated group assignment (None when cleared). + group: Option<>::GroupId>, + }, } /// Errors that can occur while configuring rate limits. @@ -280,6 +403,30 @@ pub mod pallet { InvalidRuntimeCall, /// Attempted to remove a limit that is not present. MissingRateLimit, + /// Group metadata was not found. + UnknownGroup, + /// Attempted to create or rename a group to an existing name. + DuplicateGroupName, + /// Group name exceeds the configured maximum length. + GroupNameTooLong, + /// Operation requires the group to have no members. + GroupHasMembers, + /// Adding a member would exceed the configured limit. + GroupMemberLimitExceeded, + /// Call already belongs to the requested group. + CallAlreadyInGroup, + /// Call is not assigned to a group. + CallNotInGroup, + /// Operation requires the call to be registered first. + CallNotRegistered, + /// Attempted to register a call that already exists. + CallAlreadyRegistered, + /// Rate limit for this call must be configured via its group target. + MustTargetGroup, + /// Resolver failed to supply a required context value. + MissingScope, + /// Group cannot be removed because configuration or usage entries remain. + GroupInUse, } #[pallet::genesis_config] @@ -306,9 +453,12 @@ pub mod pallet { impl, I: 'static> BuildGenesisConfig for GenesisConfig { fn build(&self) { DefaultLimit::::put(self.default_limit); + let initial: >::GroupId = Zero::zero(); + NextGroupId::::put(initial); for (identifier, scope, kind) in &self.limits { - Limits::::mutate(identifier, |entry| match scope { + let target = RateLimitTarget::Transaction(*identifier); + Limits::::mutate(target, |entry| match scope { None => { *entry = Some(RateLimit::global(*kind)); } @@ -342,18 +492,22 @@ pub mod pallet { return Ok(true); } - let Some(block_span) = Self::effective_span(origin, call, identifier, scope) else { + let target = Self::config_target(identifier)?; + Self::ensure_scope_available(&target, scope)?; + + let Some(block_span) = Self::effective_span(origin, call, &target, scope) else { return Ok(true); }; - Ok(Self::within_span(identifier, usage_key, block_span)) + let usage_target = Self::usage_target(identifier)?; + Ok(Self::within_span(&usage_target, usage_key, block_span)) } pub(crate) fn resolved_limit( - identifier: &TransactionIdentifier, + target: &RateLimitTarget<>::GroupId>, scope: &Option<>::LimitScope>, ) -> Option> { - let config = Limits::::get(identifier)?; + let config = Limits::::get(target)?; let kind = config.kind_for(scope.as_ref())?; Some(match *kind { RateLimitKind::Default => DefaultLimit::::get(), @@ -364,17 +518,17 @@ pub mod pallet { pub(crate) fn effective_span( origin: &DispatchOriginOf<>::RuntimeCall>, call: &>::RuntimeCall, - identifier: &TransactionIdentifier, + target: &RateLimitTarget<>::GroupId>, scope: &Option<>::LimitScope>, ) -> Option> { - let span = Self::resolved_limit(identifier, scope)?; + let span = Self::resolved_limit(target, scope)?; Some(>::LimitScopeResolver::adjust_span( origin, call, span, )) } pub(crate) fn within_span( - identifier: &TransactionIdentifier, + target: &RateLimitTarget<>::GroupId>, usage_key: &Option<>::UsageKey>, block_span: BlockNumberFor, ) -> bool { @@ -382,7 +536,7 @@ pub mod pallet { return true; } - if let Some(last) = LastSeen::::get(identifier, usage_key) { + if let Some(last) = LastSeen::::get(target, usage_key.clone()) { let current = frame_system::Pallet::::block_number(); let delta = current.saturating_sub(last); if delta < block_span { @@ -398,11 +552,11 @@ pub mod pallet { /// This is primarily intended for migrations that need to hydrate the new tracking storage /// from legacy pallets. pub fn record_last_seen( - identifier: &TransactionIdentifier, + target: RateLimitTarget<>::GroupId>, usage_key: Option<>::UsageKey>, block_number: BlockNumberFor, ) { - LastSeen::::insert(identifier, usage_key, block_number); + LastSeen::::insert(target, usage_key, block_number); } /// Migrates a stored rate limit configuration from one scope to another. @@ -410,16 +564,16 @@ pub mod pallet { /// Returns `true` when an entry was moved. Passing identical `from`/`to` scopes simply /// checks that a configuration exists. pub fn migrate_limit_scope( - identifier: &TransactionIdentifier, + target: RateLimitTarget<>::GroupId>, from: Option<>::LimitScope>, to: Option<>::LimitScope>, ) -> bool { if from == to { - return Limits::::contains_key(identifier); + return Limits::::contains_key(target); } let mut migrated = false; - Limits::::mutate(identifier, |maybe_config| { + Limits::::mutate(target, |maybe_config| { if let Some(config) = maybe_config { match (from.as_ref(), to.as_ref()) { (None, Some(target)) => { @@ -459,19 +613,19 @@ pub mod pallet { /// Returns `true` when an entry was moved. Passing identical keys simply checks that an /// entry exists. pub fn migrate_usage_key( - identifier: &TransactionIdentifier, + target: RateLimitTarget<>::GroupId>, from: Option<>::UsageKey>, to: Option<>::UsageKey>, ) -> bool { if from == to { - return LastSeen::::contains_key(identifier, &to); + return LastSeen::::contains_key(target, to); } - let Some(block) = LastSeen::::take(identifier, from) else { + let Some(block) = LastSeen::::take(target, from) else { return false; }; - LastSeen::::insert(identifier, to, block); + LastSeen::::insert(target, to, block); true } @@ -482,8 +636,8 @@ pub mod pallet { scope: Option<>::LimitScope>, ) -> Option>> { let identifier = Self::identifier_for_call_names(pallet_name, extrinsic_name)?; - Limits::::get(&identifier) - .and_then(|config| config.kind_for(scope.as_ref()).copied()) + let target = Self::config_target(&identifier).ok()?; + Limits::::get(target).and_then(|config| config.kind_for(scope.as_ref()).copied()) } /// Returns the resolved block span for the specified pallet/extrinsic names, if any. @@ -493,7 +647,8 @@ pub mod pallet { scope: Option<>::LimitScope>, ) -> Option> { let identifier = Self::identifier_for_call_names(pallet_name, extrinsic_name)?; - Self::resolved_limit(&identifier, &scope) + let target = Self::config_target(&identifier).ok()?; + Self::resolved_limit(&target, &scope) } fn identifier_for_call_names( @@ -508,165 +663,323 @@ pub mod pallet { let extrinsic_index = u8::try_from(extrinsic_pos).ok()?; Some(TransactionIdentifier::new(pallet_index, extrinsic_index)) } + + fn ensure_call_registered(identifier: &TransactionIdentifier) -> DispatchResult { + let target = RateLimitTarget::Transaction(*identifier); + ensure!( + Limits::::contains_key(target), + Error::::CallNotRegistered + ); + Ok(()) + } + + fn ensure_call_unregistered(identifier: &TransactionIdentifier) -> DispatchResult { + let target = RateLimitTarget::Transaction(*identifier); + ensure!( + !Limits::::contains_key(target), + Error::::CallAlreadyRegistered + ); + Ok(()) + } + + fn call_metadata( + identifier: &TransactionIdentifier, + ) -> Result<(Vec, Vec), DispatchError> { + let (pallet_name, extrinsic_name) = identifier.names::()?; + Ok(( + Vec::from(pallet_name.as_bytes()), + Vec::from(extrinsic_name.as_bytes()), + )) + } + + pub(crate) fn config_target( + identifier: &TransactionIdentifier, + ) -> Result>::GroupId>, DispatchError> { + Self::target_for(identifier, GroupSharing::config_uses_group) + } + + pub(crate) fn usage_target( + identifier: &TransactionIdentifier, + ) -> Result>::GroupId>, DispatchError> { + Self::target_for(identifier, GroupSharing::usage_uses_group) + } + + fn target_for( + identifier: &TransactionIdentifier, + predicate: impl Fn(GroupSharing) -> bool, + ) -> Result>::GroupId>, DispatchError> { + let group = Self::group_assignment(identifier)?; + Ok(Self::target_from_details( + identifier, + group.as_ref(), + predicate, + )) + } + + fn group_assignment( + identifier: &TransactionIdentifier, + ) -> Result>, DispatchError> { + let Some(group) = CallGroups::::get(identifier) else { + return Ok(None); + }; + let details = Self::ensure_group_details(group)?; + Ok(Some(details)) + } + + fn target_from_details( + identifier: &TransactionIdentifier, + details: Option<&GroupDetailsOf>, + predicate: impl Fn(GroupSharing) -> bool, + ) -> RateLimitTarget<>::GroupId> { + if let Some(details) = details { + if predicate(details.sharing) { + return RateLimitTarget::Group(details.id); + } + } + RateLimitTarget::Transaction(*identifier) + } + + fn ensure_group_details( + group: >::GroupId, + ) -> Result, DispatchError> { + Groups::::get(group).ok_or(Error::::UnknownGroup.into()) + } + + fn ensure_scope_available( + target: &RateLimitTarget<>::GroupId>, + scope: &Option<>::LimitScope>, + ) -> Result<(), DispatchError> { + if scope.is_some() { + return Ok(()); + } + + if let Some(RateLimit::Scoped(map)) = Limits::::get(target) { + if !map.is_empty() { + return Err(Error::::MissingScope.into()); + } + } + + Ok(()) + } + + fn bounded_group_name(name: Vec) -> Result, DispatchError> { + GroupNameOf::::try_from(name).map_err(|_| Error::::GroupNameTooLong.into()) + } + + fn ensure_group_name_available( + name: &GroupNameOf, + current: Option<>::GroupId>, + ) -> DispatchResult { + if let Some(existing) = GroupNameIndex::::get(name) { + ensure!(Some(existing) == current, Error::::DuplicateGroupName); + } + Ok(()) + } + + fn ensure_group_deletable(group: >::GroupId) -> DispatchResult { + ensure!( + GroupMembers::::get(group).is_empty(), + Error::::GroupHasMembers + ); + let target = RateLimitTarget::Group(group); + ensure!( + !Limits::::contains_key(target), + Error::::GroupInUse + ); + ensure!( + LastSeen::::iter_prefix(target).next().is_none(), + Error::::GroupInUse + ); + Ok(()) + } + + fn insert_call_into_group( + identifier: &TransactionIdentifier, + group: >::GroupId, + ) -> DispatchResult { + GroupMembers::::try_mutate(group, |members| -> DispatchResult { + match members.try_insert(*identifier) { + Ok(true) => Ok(()), + Ok(false) => Err(Error::::CallAlreadyInGroup.into()), + Err(_) => Err(Error::::GroupMemberLimitExceeded.into()), + } + })?; + Ok(()) + } + + fn detach_call_from_group( + identifier: &TransactionIdentifier, + group: >::GroupId, + ) -> bool { + GroupMembers::::mutate(group, |members| members.remove(identifier)) + } } #[pallet::call] impl, I: 'static> Pallet { - /// Sets the rate limit configuration for the given call. - /// - /// The supplied `call` is inspected to derive the pallet/extrinsic indices and passed to - /// [`Config::LimitScopeResolver`] to determine the applicable scope. The pallet never - /// persists the call arguments directly, but a resolver may read them in order to resolve - /// its context. When a scope resolves, the configuration is stored against that scope; - /// otherwise the global entry is updated. + /// Registers a call for rate limiting and seeds its initial configuration. #[pallet::call_index(0)] - #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] - pub fn set_rate_limit( + #[pallet::weight(T::DbWeight::get().reads_writes(3, 3))] + pub fn register_call( origin: OriginFor, call: Box<>::RuntimeCall>, - limit: RateLimitKind>, + group: Option<>::GroupId>, ) -> DispatchResult { let resolver_origin: DispatchOriginOf<>::RuntimeCall> = Into::>::RuntimeCall>>::into(origin.clone()); let scope = >::LimitScopeResolver::context(&resolver_origin, call.as_ref()); - let scope_for_event = scope.clone(); T::AdminOrigin::ensure_origin(origin)?; let identifier = TransactionIdentifier::from_call::(call.as_ref())?; + Self::ensure_call_unregistered(&identifier)?; + + let target = RateLimitTarget::Transaction(identifier); if let Some(ref sc) = scope { - Limits::::mutate(&identifier, |slot| match slot { - Some(config) => config.upsert_scope(sc.clone(), limit), - None => *slot = Some(RateLimit::scoped_single(sc.clone(), limit)), - }); + Limits::::insert( + target, + RateLimit::scoped_single(sc.clone(), RateLimitKind::Default), + ); } else { - Limits::::insert(&identifier, RateLimit::global(limit)); + Limits::::insert(target, RateLimit::global(RateLimitKind::Default)); } - let (pallet_name, extrinsic_name) = identifier.names::()?; - let pallet = Vec::from(pallet_name.as_bytes()); - let extrinsic = Vec::from(extrinsic_name.as_bytes()); + let mut assigned_group = None; + if let Some(group_id) = group { + Self::ensure_group_details(group_id)?; + Self::insert_call_into_group(&identifier, group_id)?; + CallGroups::::insert(&identifier, group_id); + assigned_group = Some(group_id); + } - Self::deposit_event(Event::RateLimitSet { + let (pallet, extrinsic) = Self::call_metadata(&identifier)?; + Self::deposit_event(Event::CallRegistered { transaction: identifier, - scope: scope_for_event, - limit, - pallet, - extrinsic, + scope: scope.clone(), + group: assigned_group, + pallet: pallet.clone(), + extrinsic: extrinsic.clone(), }); + + if let Some(group_id) = assigned_group { + Self::deposit_event(Event::CallGroupUpdated { + transaction: identifier, + group: Some(group_id), + }); + } + Ok(()) } - /// Clears the rate limit for the given call, if present. - /// - /// The supplied `call` is inspected to derive the pallet/extrinsic indices and passed to - /// [`Config::LimitScopeResolver`] when determining which scoped configuration to clear. - /// The pallet does not persist the call arguments, but resolvers may read them while - /// computing the scope. When no scope resolves, the global entry is cleared. + /// Configures a rate limit for either a transaction or group target. #[pallet::call_index(1)] - #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] - pub fn clear_rate_limit( + #[pallet::weight(T::DbWeight::get().reads_writes(2, 2))] + pub fn set_rate_limit( origin: OriginFor, - call: Box<>::RuntimeCall>, + target: RateLimitTarget<>::GroupId>, + scope: Option<>::LimitScope>, + limit: RateLimitKind>, ) -> DispatchResult { - let resolver_origin: DispatchOriginOf<>::RuntimeCall> = - Into::>::RuntimeCall>>::into(origin.clone()); - let scope = - >::LimitScopeResolver::context(&resolver_origin, call.as_ref()); - let usage = >::UsageResolver::context(&resolver_origin, call.as_ref()); - T::AdminOrigin::ensure_origin(origin)?; - let identifier = TransactionIdentifier::from_call::(call.as_ref())?; - - let (pallet_name, extrinsic_name) = identifier.names::()?; - let pallet = Vec::from(pallet_name.as_bytes()); - let extrinsic = Vec::from(extrinsic_name.as_bytes()); - - let mut removed = false; - Limits::::mutate_exists(&identifier, |maybe_config| { - if let Some(config) = maybe_config { - match (&scope, config) { - (None, _) => { - removed = true; - *maybe_config = None; - } - (Some(sc), RateLimit::Scoped(map)) => { - if map.remove(sc).is_some() { - removed = true; - if map.is_empty() { - *maybe_config = None; - } - } - } - (Some(_), RateLimit::Global(_)) => {} + let (transaction, pallet, extrinsic) = match target { + RateLimitTarget::Transaction(identifier) => { + Self::ensure_call_registered(&identifier)?; + if let Some(group) = CallGroups::::get(&identifier) { + let details = Self::ensure_group_details(group)?; + ensure!( + !details.sharing.config_uses_group(), + Error::::MustTargetGroup + ); } + let (pallet, extrinsic) = Self::call_metadata(&identifier)?; + (Some(identifier), Some(pallet), Some(extrinsic)) } - }); - - ensure!(removed, Error::::MissingRateLimit); - - if removed { - match (scope.as_ref(), usage) { - (None, _) => { - let _ = LastSeen::::clear_prefix(&identifier, u32::MAX, None); - } - (_, Some(key)) => { - LastSeen::::remove(&identifier, Some(key)); - } - (_, None) => { - LastSeen::::remove(&identifier, None::<>::UsageKey>); - } + RateLimitTarget::Group(group) => { + Self::ensure_group_details(group)?; + (None, None, None) } + }; + + if let Some(ref scoped) = scope { + Limits::::mutate(target, |slot| match slot { + Some(config) => config.upsert_scope(scoped.clone(), limit), + None => *slot = Some(RateLimit::scoped_single(scoped.clone(), limit)), + }); + } else { + Limits::::insert(target, RateLimit::global(limit)); } - Self::deposit_event(Event::RateLimitCleared { - transaction: identifier, + Self::deposit_event(Event::RateLimitSet { + target, + transaction, scope, + limit, pallet, extrinsic, }); - Ok(()) } - /// Clears every stored rate limit configuration for the given call, including scoped - /// entries. - /// - /// The supplied `call` is inspected to derive the pallet and extrinsic indices. All stored - /// scopes for that call, along with any associated usage tracking entries, are removed when - /// this extrinsic succeeds. + /// Assigns a registered call to the specified group. #[pallet::call_index(2)] - #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] - pub fn clear_all_rate_limits( + #[pallet::weight(T::DbWeight::get().reads_writes(3, 3))] + pub fn assign_call_to_group( origin: OriginFor, - call: Box<>::RuntimeCall>, + transaction: TransactionIdentifier, + group: >::GroupId, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; - let identifier = TransactionIdentifier::from_call::(call.as_ref())?; - let (pallet_name, extrinsic_name) = identifier.names::()?; - let pallet = Vec::from(pallet_name.as_bytes()); - let extrinsic = Vec::from(extrinsic_name.as_bytes()); + Self::ensure_call_registered(&transaction)?; + Self::ensure_group_details(group)?; - let removed = Limits::::take(&identifier).is_some(); - ensure!(removed, Error::::MissingRateLimit); + let current = CallGroups::::get(&transaction); + if current == Some(group) { + return Err(Error::::CallAlreadyInGroup.into()); + } - let _ = LastSeen::::clear_prefix(&identifier, u32::MAX, None); + Self::insert_call_into_group(&transaction, group)?; + if let Some(existing) = current { + Self::detach_call_from_group(&transaction, existing); + } + CallGroups::::insert(&transaction, group); - Self::deposit_event(Event::AllRateLimitsCleared { - transaction: identifier, - pallet, - extrinsic, + Self::deposit_event(Event::CallGroupUpdated { + transaction, + group: Some(group), }); Ok(()) } - /// Sets the default rate limit in blocks applied to calls configured to use it. + /// Removes a registered call from its current group assignment. #[pallet::call_index(3)] + #[pallet::weight(T::DbWeight::get().reads_writes(2, 2))] + pub fn remove_call_from_group( + origin: OriginFor, + transaction: TransactionIdentifier, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + Self::ensure_call_registered(&transaction)?; + let Some(group) = CallGroups::::take(&transaction) else { + return Err(Error::::CallNotInGroup.into()); + }; + Self::detach_call_from_group(&transaction, group); + + Self::deposit_event(Event::CallGroupUpdated { + transaction, + group: None, + }); + + Ok(()) + } + + /// Sets the default rate limit that applies when an extrinsic uses [`RateLimitKind::Default`]. + #[pallet::call_index(4)] #[pallet::weight(T::DbWeight::get().writes(1))] pub fn set_default_rate_limit( origin: OriginFor, @@ -675,8 +988,175 @@ pub mod pallet { T::AdminOrigin::ensure_origin(origin)?; DefaultLimit::::put(block_span); - Self::deposit_event(Event::DefaultRateLimitSet { block_span }); + Ok(()) + } + + /// Creates a new rate-limiting group with the provided name and sharing configuration. + #[pallet::call_index(5)] + #[pallet::weight(T::DbWeight::get().reads_writes(1, 3))] + pub fn create_group( + origin: OriginFor, + name: Vec, + sharing: GroupSharing, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + let bounded = Self::bounded_group_name(name)?; + Self::ensure_group_name_available(&bounded, None)?; + + let group = NextGroupId::::mutate(|current| { + let next = current.saturating_add(One::one()); + sp_std::mem::replace(current, next) + }); + + Groups::::insert( + group, + RateLimitGroup { + id: group, + name: bounded.clone(), + sharing, + }, + ); + GroupNameIndex::::insert(&bounded, group); + GroupMembers::::insert(group, GroupMembersOf::::new()); + + let name_bytes: Vec = bounded.into(); + Self::deposit_event(Event::GroupCreated { + group, + name: name_bytes, + sharing, + }); + Ok(()) + } + + /// Updates the metadata or sharing configuration of an existing group. + #[pallet::call_index(6)] + #[pallet::weight(T::DbWeight::get().reads_writes(3, 3))] + pub fn update_group( + origin: OriginFor, + group: >::GroupId, + name: Option>, + sharing: Option, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + Groups::::try_mutate(group, |maybe_details| -> DispatchResult { + let details = maybe_details.as_mut().ok_or(Error::::UnknownGroup)?; + + if let Some(new_name) = name { + let bounded = Self::bounded_group_name(new_name)?; + Self::ensure_group_name_available(&bounded, Some(group))?; + GroupNameIndex::::remove(&details.name); + GroupNameIndex::::insert(&bounded, group); + details.name = bounded; + } + + if let Some(new_sharing) = sharing { + details.sharing = new_sharing; + } + + Ok(()) + })?; + + let updated = Self::ensure_group_details(group)?; + let name_bytes: Vec = updated.name.clone().into(); + Self::deposit_event(Event::GroupUpdated { + group, + name: name_bytes, + sharing: updated.sharing, + }); + + Ok(()) + } + + /// Deletes an existing group. The group must be empty and unused. + #[pallet::call_index(7)] + #[pallet::weight(T::DbWeight::get().reads_writes(3, 3))] + pub fn delete_group( + origin: OriginFor, + group: >::GroupId, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + Self::ensure_group_deletable(group)?; + + let details = Groups::::take(group).ok_or(Error::::UnknownGroup)?; + GroupNameIndex::::remove(&details.name); + GroupMembers::::remove(group); + + Self::deposit_event(Event::GroupDeleted { group }); + + Ok(()) + } + + /// Deregisters a call or removes a scoped entry from its configuration. + #[pallet::call_index(8)] + #[pallet::weight(T::DbWeight::get().reads_writes(4, 4))] + pub fn deregister_call( + origin: OriginFor, + transaction: TransactionIdentifier, + scope: Option<>::LimitScope>, + clear_usage: bool, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + Self::ensure_call_registered(&transaction)?; + let target = Self::config_target(&transaction)?; + let tx_target = RateLimitTarget::Transaction(transaction); + let usage_target = Self::usage_target(&transaction)?; + + match &scope { + Some(sc) => { + let mut removed = false; + Limits::::mutate_exists(target, |maybe_config| { + if let Some(RateLimit::Scoped(map)) = maybe_config { + if map.remove(sc).is_some() { + removed = true; + if map.is_empty() { + *maybe_config = None; + } + } + } + }); + ensure!(removed, Error::::MissingRateLimit); + + if let Some(group) = CallGroups::::take(&transaction) { + Self::detach_call_from_group(&transaction, group); + Self::deposit_event(Event::CallGroupUpdated { + transaction, + group: None, + }); + } + } + None => { + Limits::::remove(target); + if target != tx_target { + Limits::::remove(tx_target); + } + + if let Some(group) = CallGroups::::take(&transaction) { + Self::detach_call_from_group(&transaction, group); + Self::deposit_event(Event::CallGroupUpdated { + transaction, + group: None, + }); + } + } + } + + if clear_usage { + let _ = LastSeen::::clear_prefix(&usage_target, u32::MAX, None); + } + + let (pallet, extrinsic) = Self::call_metadata(&transaction)?; + Self::deposit_event(Event::CallDeregistered { + target, + transaction: Some(transaction), + scope, + pallet: Some(pallet), + extrinsic: Some(extrinsic), + }); Ok(()) } diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index fb7de0a400..b643dec64d 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -57,6 +57,7 @@ impl frame_system::Config for Test { pub type LimitScope = u16; pub type UsageKey = u16; +pub type GroupId = u32; pub struct TestScopeResolver; pub struct TestUsageResolver; @@ -77,14 +78,14 @@ impl pallet_rate_limiting::RateLimitScopeResolver bool { matches!( call, - RuntimeCall::RateLimiting(RateLimitingCall::clear_rate_limit { .. }) + RuntimeCall::RateLimiting(RateLimitingCall::remove_call_from_group { .. }) ) } fn adjust_span(_origin: &RuntimeOrigin, call: &RuntimeCall, span: u64) -> u64 { if matches!( call, - RuntimeCall::RateLimiting(RateLimitingCall::clear_all_rate_limits { .. }) + RuntimeCall::RateLimiting(RateLimitingCall::deregister_call { .. }) ) { span.saturating_mul(2) } else { @@ -114,6 +115,9 @@ impl pallet_rate_limiting::Config for Test { type UsageKey = UsageKey; type UsageResolver = TestUsageResolver; type AdminOrigin = EnsureRoot; + type GroupId = GroupId; + type MaxGroupMembers = ConstU32<32>; + type MaxGroupNameLength = ConstU32<64>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = BenchHelper; } diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index a377d71656..5027909b67 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -1,592 +1,656 @@ -use frame_support::{assert_noop, assert_ok, error::BadOrigin}; -use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; +use frame_support::{assert_noop, assert_ok}; +use sp_std::vec::Vec; -use crate::{DefaultLimit, LastSeen, Limits, RateLimit, RateLimitKind, mock::*, pallet::Error}; +use crate::{ + CallGroups, Config, GroupMembers, GroupSharing, LastSeen, Limits, RateLimit, RateLimitKind, + RateLimitTarget, TransactionIdentifier, mock::*, pallet::Error, +}; +use frame_support::traits::Get; -#[test] -fn limit_for_call_names_returns_none_if_not_set() { - new_test_ext().execute_with(|| { - assert!( - RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", None) - .is_none() - ); - }); +fn target(identifier: TransactionIdentifier) -> RateLimitTarget { + RateLimitTarget::Transaction(identifier) } -#[test] -fn limit_for_call_names_returns_stored_limit() { - new_test_ext().execute_with(|| { - let call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(7))); +fn remark_call() -> RuntimeCall { + RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }) +} - let fetched = - RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", None) - .expect("limit should exist"); - assert_eq!(fetched, RateLimitKind::Exact(7)); - }); +fn scoped_call() -> RuntimeCall { + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 1 }) } -#[test] -fn limit_for_call_names_prefers_scope_specific_limit() { - new_test_ext().execute_with(|| { - let call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&call); - Limits::::insert( - identifier, - RateLimit::scoped_single(5u16, RateLimitKind::Exact(8)), - ); +fn register(call: RuntimeCall, group: Option) -> TransactionIdentifier { + let identifier = identifier_for(&call); + assert_ok!(RateLimiting::register_call( + RuntimeOrigin::root(), + Box::new(call), + group + )); + identifier +} - let fetched = - RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", Some(5)) - .expect("limit should exist"); - assert_eq!(fetched, RateLimitKind::Exact(8)); +fn create_group(name: &[u8], sharing: GroupSharing) -> GroupId { + assert_ok!(RateLimiting::create_group( + RuntimeOrigin::root(), + name.to_vec(), + sharing, + )); + RateLimiting::next_group_id().saturating_sub(1) +} - assert!( - RateLimiting::limit_for_call_names("RateLimiting", "set_default_rate_limit", Some(1)) - .is_none() - ); - }); +fn last_event() -> RuntimeEvent { + pop_last_event() } #[test] -fn resolved_limit_for_call_names_resolves_default_value() { +fn register_call_seeds_global_limit() { new_test_ext().execute_with(|| { - DefaultLimit::::put(3); - let call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::global(RateLimitKind::Default)); - - let resolved = RateLimiting::resolved_limit_for_call_names( - "RateLimiting", - "set_default_rate_limit", - None, - ) - .expect("resolved limit"); - assert_eq!(resolved, 3); + let identifier = register(remark_call(), None); + let tx_target = target(identifier); + let stored = Limits::::get(tx_target).expect("limit"); + assert!(matches!(stored, RateLimit::Global(RateLimitKind::Default))); + + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::CallRegistered { transaction, .. }) + if transaction == identifier + )); }); } #[test] -fn resolved_limit_for_call_names_prefers_scope_specific_value() { +fn register_call_seeds_scoped_limit() { new_test_ext().execute_with(|| { - let call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&call); - let mut map = BTreeMap::new(); - map.insert(6u16, RateLimitKind::Exact(9)); - map.insert(2u16, RateLimitKind::Exact(4)); - Limits::::insert(identifier, RateLimit::Scoped(map)); - - let resolved = RateLimiting::resolved_limit_for_call_names( - "RateLimiting", - "set_default_rate_limit", - Some(6), - ) - .expect("resolved limit"); - assert_eq!(resolved, 9); + let identifier = register(scoped_call(), None); + let tx_target = target(identifier); + let stored = Limits::::get(tx_target).expect("limit"); + match stored { + RateLimit::Scoped(map) => { + assert_eq!(map.get(&1u16), Some(&RateLimitKind::Default)); + } + _ => panic!("expected scoped entry"), + } - assert!( - RateLimiting::resolved_limit_for_call_names( - "RateLimiting", - "set_default_rate_limit", - Some(1), - ) - .is_none() - ); + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::CallRegistered { transaction, scope, .. }) + if transaction == identifier && scope == Some(1u16) + )); }); } #[test] -fn resolved_limit_for_call_names_returns_none_when_unset() { +fn set_rate_limit_updates_transaction_target() { new_test_ext().execute_with(|| { - assert!( - RateLimiting::resolved_limit_for_call_names( - "RateLimiting", - "set_default_rate_limit", - None, - ) - .is_none() - ); + let identifier = register(remark_call(), None); + let tx_target = target(identifier); + let limit = RateLimitKind::Exact(9); + assert_ok!(RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + tx_target, + None, + limit, + )); + let stored = Limits::::get(tx_target).expect("limit"); + assert!(matches!(stored, RateLimit::Global(RateLimitKind::Exact(9)))); + + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::RateLimitSet { + target: RateLimitTarget::Transaction(t), + limit: RateLimitKind::Exact(9), + .. + }) if t == identifier + )); }); } #[test] -fn is_within_limit_is_true_when_no_limit() { +fn set_rate_limit_requires_registration_and_group_targeting() { new_test_ext().execute_with(|| { - let call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&call); + let identifier = register(remark_call(), None); + let target = target(identifier); + + // Unregistered call. + let unknown = TransactionIdentifier::new(99, 0); + assert_noop!( + RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + RateLimitTarget::Transaction(unknown), + None, + RateLimitKind::Exact(1), + ), + Error::::CallNotRegistered + ); - let origin = RuntimeOrigin::signed(1); - let result = RateLimiting::is_within_limit(&origin, &call, &identifier, &None, &None); - assert_eq!(result.expect("no error expected"), true); + // Group requires targeting the group. + let group = create_group(b"cfg", GroupSharing::ConfigAndUsage); + assert_ok!(RateLimiting::assign_call_to_group( + RuntimeOrigin::root(), + identifier, + group, + )); + assert_noop!( + RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + target, + None, + RateLimitKind::Exact(2), + ), + Error::::MustTargetGroup + ); }); } #[test] -fn is_within_limit_false_when_rate_limited() { +fn set_rate_limit_respects_group_config_sharing() { new_test_ext().execute_with(|| { - let call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&call); - Limits::::insert( + let identifier = register(remark_call(), None); + let group = create_group(b"test", GroupSharing::ConfigAndUsage); + assert_ok!(RateLimiting::assign_call_to_group( + RuntimeOrigin::root(), identifier, - RateLimit::scoped_single(1 as LimitScope, RateLimitKind::Exact(5)), + group, + )); + assert_noop!( + RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + RateLimitTarget::Transaction(identifier), + None, + RateLimitKind::Exact(5), + ), + Error::::MustTargetGroup ); - LastSeen::::insert(identifier, Some(1 as UsageKey), 9); - - System::set_block_number(13); - let origin = RuntimeOrigin::signed(1); - let within = RateLimiting::is_within_limit( - &origin, - &call, - &identifier, - &Some(1 as LimitScope), - &Some(1 as UsageKey), - ) - .expect("call succeeds"); - assert!(!within); + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::CallGroupUpdated { + transaction, + group: Some(g), + }) if transaction == identifier && g == group + )); }); } #[test] -fn is_within_limit_true_after_required_span() { +fn assign_and_remove_group_membership() { new_test_ext().execute_with(|| { - let call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&call); - Limits::::insert( + let identifier = register(remark_call(), None); + let group = create_group(b"team", GroupSharing::UsageOnly); + assert_ok!(RateLimiting::assign_call_to_group( + RuntimeOrigin::root(), identifier, - RateLimit::scoped_single(2 as LimitScope, RateLimitKind::Exact(5)), - ); - LastSeen::::insert(identifier, Some(2 as UsageKey), 10); - - System::set_block_number(20); - - let origin = RuntimeOrigin::signed(1); - let within = RateLimiting::is_within_limit( - &origin, - &call, - &identifier, - &Some(2 as LimitScope), - &Some(2 as UsageKey), - ) - .expect("call succeeds"); - assert!(within); + group, + )); + assert_eq!(CallGroups::::get(identifier), Some(group)); + assert!(GroupMembers::::get(group).contains(&identifier)); + assert_ok!(RateLimiting::remove_call_from_group( + RuntimeOrigin::root(), + identifier, + )); + assert!(CallGroups::::get(identifier).is_none()); + + // Last event should signal removal. + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::CallGroupUpdated { transaction, group: None }) + if transaction == identifier + )); }); } #[test] -fn migrate_limit_scope_global_to_scoped() { +fn set_rate_limit_on_group_updates_storage() { new_test_ext().execute_with(|| { - let target_call = - RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }); - let identifier = identifier_for(&target_call); - - Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(3))); - - assert!(RateLimiting::migrate_limit_scope( - &identifier, + let group = create_group(b"grp", GroupSharing::ConfigOnly); + let target = RateLimitTarget::Group(group); + assert_ok!(RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + target, None, - Some(9) + RateLimitKind::Exact(3), + )); + assert!(matches!( + Limits::::get(target), + Some(RateLimit::Global(RateLimitKind::Exact(3))) )); - match RateLimiting::limits(identifier).expect("config") { - RateLimit::Scoped(map) => { - assert_eq!(map.len(), 1); - assert_eq!(map.get(&9), Some(&RateLimitKind::Exact(3))); - } - other => panic!("unexpected config: {:?}", other), - } + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::RateLimitSet { + target: RateLimitTarget::Group(g), + limit: RateLimitKind::Exact(3), + .. + }) if g == group + )); }); } #[test] -fn migrate_limit_scope_scoped_to_scoped() { +fn create_and_delete_group_emit_events() { new_test_ext().execute_with(|| { - let target_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&target_call); - - let mut map = sp_std::collections::btree_map::BTreeMap::new(); - map.insert(1u16, RateLimitKind::Exact(4)); - map.insert(2u16, RateLimitKind::Exact(6)); - Limits::::insert(identifier, RateLimit::Scoped(map)); - - assert!(RateLimiting::migrate_limit_scope( - &identifier, - Some(1), - Some(3) + assert_ok!(RateLimiting::create_group( + RuntimeOrigin::root(), + b"ev".to_vec(), + GroupSharing::UsageOnly, + )); + let group = RateLimiting::next_group_id().saturating_sub(1); + let created = last_event(); + assert!(matches!( + created, + RuntimeEvent::RateLimiting(crate::Event::GroupCreated { group: g, .. }) if g == group )); - match RateLimiting::limits(identifier).expect("config") { - RateLimit::Scoped(map) => { - assert!(map.get(&1).is_none()); - assert_eq!(map.get(&3), Some(&RateLimitKind::Exact(4))); - assert_eq!(map.get(&2), Some(&RateLimitKind::Exact(6))); - } - other => panic!("unexpected config: {:?}", other), - } + assert_ok!(RateLimiting::delete_group(RuntimeOrigin::root(), group)); + let deleted = last_event(); + assert!(matches!( + deleted, + RuntimeEvent::RateLimiting(crate::Event::GroupDeleted { group: g }) if g == group + )); }); } #[test] -fn migrate_limit_scope_scoped_to_global() { +fn deregister_call_scope_removes_entry() { new_test_ext().execute_with(|| { - let target_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&target_call); - - let mut map = sp_std::collections::btree_map::BTreeMap::new(); - map.insert(7u16, RateLimitKind::Exact(8)); - Limits::::insert(identifier, RateLimit::Scoped(map)); - - assert!(RateLimiting::migrate_limit_scope( - &identifier, - Some(7), - None + let identifier = register(scoped_call(), None); + let tx_target = target(identifier); + assert_ok!(RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + tx_target, + Some(2u16), + RateLimitKind::Exact(4), )); - - match RateLimiting::limits(identifier).expect("config") { - RateLimit::Global(kind) => assert_eq!(kind, RateLimitKind::Exact(8)), + LastSeen::::insert(tx_target, Some(9u16), 10); + assert_ok!(RateLimiting::deregister_call( + RuntimeOrigin::root(), + identifier, + Some(2u16), + false, + )); + match Limits::::get(tx_target) { + Some(RateLimit::Scoped(map)) => { + assert!(map.contains_key(&1u16)); + assert!(!map.contains_key(&2u16)); + } other => panic!("unexpected config: {:?}", other), } + // usage remains intact when clear_usage is false + assert_eq!(LastSeen::::get(tx_target, Some(9u16)), Some(10)); + + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::CallDeregistered { + target, + transaction: Some(t), + scope: Some(sc), + .. + }) if target == tx_target && t == identifier && sc == 2u16 + )); + + // No group assigned in this test. + assert!(CallGroups::::get(identifier).is_none()); }); } #[test] -fn migrate_usage_key_moves_entry() { +fn register_call_rejects_duplicates_and_unknown_group() { new_test_ext().execute_with(|| { - let target_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&target_call); - - LastSeen::::insert(identifier, Some(5u16), 11); - - assert!(RateLimiting::migrate_usage_key( - &identifier, - Some(5), - Some(6) - )); - assert!(LastSeen::::get(identifier, Some(5u16)).is_none()); - assert_eq!(LastSeen::::get(identifier, Some(6u16)), Some(11)); + let identifier = register(remark_call(), None); + // Duplicate should fail. + assert_noop!( + RateLimiting::register_call(RuntimeOrigin::root(), Box::new(remark_call()), None), + Error::::CallAlreadyRegistered + ); - assert!(RateLimiting::migrate_usage_key(&identifier, Some(6), None)); - assert!(LastSeen::::get(identifier, Some(6u16)).is_none()); - assert_eq!( - LastSeen::::get(identifier, None::), - Some(11) + // Unknown group should fail. + assert_noop!( + RateLimiting::register_call(RuntimeOrigin::root(), Box::new(scoped_call()), Some(99)), + Error::::UnknownGroup ); + + assert!(Limits::::contains_key(target(identifier))); }); } #[test] -fn set_rate_limit_updates_storage_and_emits_event() { +fn group_name_limits_and_uniqueness_enforced() { new_test_ext().execute_with(|| { - System::reset_events(); + // Overlong name. + let max_name = <::MaxGroupNameLength as Get>::get() as usize; + let long_name = vec![0u8; max_name + 1]; + assert_noop!( + RateLimiting::create_group(RuntimeOrigin::root(), long_name, GroupSharing::UsageOnly), + Error::::GroupNameTooLong + ); - let target_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let limit = RateLimitKind::Exact(9); + // Duplicate names rejected on create and update. + let first = create_group(b"alpha", GroupSharing::UsageOnly); + let second = create_group(b"beta", GroupSharing::UsageOnly); - assert_ok!(RateLimiting::set_rate_limit( - RuntimeOrigin::root(), - Box::new(target_call.clone()), - limit, - )); + assert_noop!( + RateLimiting::create_group( + RuntimeOrigin::root(), + b"alpha".to_vec(), + GroupSharing::UsageOnly + ), + Error::::DuplicateGroupName + ); + + assert_noop!( + RateLimiting::update_group( + RuntimeOrigin::root(), + second, + Some(b"alpha".to_vec()), + None + ), + Error::::DuplicateGroupName + ); + + // Unknown group update. + assert_noop!( + RateLimiting::update_group(RuntimeOrigin::root(), 99, None, None), + Error::::UnknownGroup + ); - let identifier = identifier_for(&target_call); assert_eq!( - Limits::::get(identifier), - Some(RateLimit::scoped_single(0, limit)) + RateLimiting::groups(first).unwrap().name.into_inner(), + b"alpha".to_vec() ); - match pop_last_event() { - RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitSet { - transaction, - scope, - limit: emitted_limit, - pallet, - extrinsic, - }) => { - assert_eq!(transaction, identifier); - assert_eq!(scope, Some(0)); - assert_eq!(emitted_limit, limit); - assert_eq!(pallet, b"RateLimiting".to_vec()); - assert_eq!(extrinsic, b"set_default_rate_limit".to_vec()); - } - other => panic!("unexpected event: {:?}", other), - } + // Updating first group emits event. + assert_ok!(RateLimiting::update_group( + RuntimeOrigin::root(), + first, + Some(b"gamma".to_vec()), + None, + )); + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::GroupUpdated { group, .. }) if group == first + )); }); } #[test] -fn set_rate_limit_stores_global_when_scope_absent() { +fn group_member_limit_and_removal_errors() { new_test_ext().execute_with(|| { - System::reset_events(); - - let target_call = - RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }); - let limit = RateLimitKind::Exact(11); + let group = create_group(b"cap", GroupSharing::UsageOnly); - assert_ok!(RateLimiting::set_rate_limit( - RuntimeOrigin::root(), - Box::new(target_call.clone()), - limit, - )); + let max_members = <::MaxGroupMembers as Get>::get(); + GroupMembers::::mutate(group, |members| { + for i in 0..max_members { + let _ = members.try_insert(TransactionIdentifier::new(0, (i + 1) as u8)); + } + }); - let identifier = identifier_for(&target_call); - assert_eq!( - Limits::::get(identifier), - Some(RateLimit::global(limit)) + // Next insert should fail. + let extra = register(remark_call(), None); + assert_noop!( + RateLimiting::assign_call_to_group(RuntimeOrigin::root(), extra, group), + Error::::GroupMemberLimitExceeded ); - match pop_last_event() { - RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitSet { - transaction, - scope, - limit: emitted_limit, - pallet, - extrinsic, - }) => { - assert_eq!(transaction, identifier); - assert_eq!(scope, None); - assert_eq!(emitted_limit, limit); - assert_eq!(pallet, b"System".to_vec()); - assert_eq!(extrinsic, b"remark".to_vec()); - } - other => panic!("unexpected event: {:?}", other), - } + // Removing a call not in a group errors. + assert_noop!( + RateLimiting::remove_call_from_group(RuntimeOrigin::root(), extra), + Error::::CallNotInGroup + ); }); } #[test] -fn set_rate_limit_requires_root() { +fn cannot_delete_group_in_use_or_unknown() { new_test_ext().execute_with(|| { - let target_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); + let group = create_group(b"busy", GroupSharing::ConfigOnly); + let identifier = register(remark_call(), Some(group)); + let target = RateLimitTarget::Group(group); + Limits::::insert(target, RateLimit::global(RateLimitKind::Exact(1))); + LastSeen::::insert(target, None::, 10); + + // Remove member so only config/usage keep the group in-use. + assert_ok!(RateLimiting::remove_call_from_group( + RuntimeOrigin::root(), + identifier + )); + // Cannot delete when in use. assert_noop!( - RateLimiting::set_rate_limit( - RuntimeOrigin::signed(1), - Box::new(target_call), - RateLimitKind::Exact(1), - ), - BadOrigin + RateLimiting::delete_group(RuntimeOrigin::root(), group), + Error::::GroupInUse + ); + + // Clear state then delete. + Limits::::remove(target); + let _ = LastSeen::::clear_prefix(&target, u32::MAX, None); + assert_ok!(RateLimiting::delete_group(RuntimeOrigin::root(), group)); + + // Unknown group. + assert_noop!( + RateLimiting::delete_group(RuntimeOrigin::root(), 999), + Error::::UnknownGroup ); }); } #[test] -fn set_rate_limit_accepts_default_variant() { +fn deregister_call_clears_registration() { new_test_ext().execute_with(|| { - let target_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - - assert_ok!(RateLimiting::set_rate_limit( + let identifier = register(remark_call(), None); + let tx_target = target(identifier); + LastSeen::::insert(tx_target, None::, 5); + assert_ok!(RateLimiting::deregister_call( RuntimeOrigin::root(), - Box::new(target_call.clone()), - RateLimitKind::Default, + identifier, + None, + true, + )); + assert!(Limits::::get(tx_target).is_none()); + assert!(LastSeen::::get(tx_target, None::).is_none()); + assert!(CallGroups::::get(identifier).is_none()); + + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::CallDeregistered { + target, + transaction: Some(t), + scope: None, + .. + }) if target == tx_target && t == identifier )); - - let identifier = identifier_for(&target_call); - assert_eq!( - Limits::::get(identifier), - Some(RateLimit::scoped_single(0, RateLimitKind::Default)) - ); }); } #[test] -fn clear_rate_limit_removes_entry_and_emits_event() { +fn deregister_errors_for_unknown_or_missing_scope() { new_test_ext().execute_with(|| { - System::reset_events(); + let unknown = TransactionIdentifier::new(10, 1); + assert_noop!( + RateLimiting::deregister_call(RuntimeOrigin::root(), unknown, None, true), + Error::::CallNotRegistered + ); - let target_call = - RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }); - let identifier = identifier_for(&target_call); - Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(4))); - LastSeen::::insert(identifier, None::, 7); - LastSeen::::insert(identifier, Some(88u16), 9); + let identifier = register(scoped_call(), None); + let tx_target = target(identifier); + // Removing a non-existent scoped entry fails. + assert_noop!( + RateLimiting::deregister_call(RuntimeOrigin::root(), identifier, Some(99u16), false), + Error::::MissingRateLimit + ); - assert_ok!(RateLimiting::clear_rate_limit( + // Removing the last scoped entry clears Limits and LastSeen. + LastSeen::::insert(tx_target, Some(1u16), 5); + assert_ok!(RateLimiting::deregister_call( RuntimeOrigin::root(), - Box::new(target_call.clone()), + identifier, + Some(1u16), + true, )); - - assert!(Limits::::get(identifier).is_none()); - assert!(LastSeen::::get(identifier, None::).is_none()); - assert!(LastSeen::::get(identifier, Some(88u16)).is_none()); - - match pop_last_event() { - RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitCleared { - transaction, - scope, - pallet, - extrinsic, - }) => { - assert_eq!(transaction, identifier); - assert_eq!(scope, None); - assert_eq!(pallet, b"System".to_vec()); - assert_eq!(extrinsic, b"remark".to_vec()); - } - other => panic!("unexpected event: {:?}", other), - } + assert!(Limits::::get(tx_target).is_none()); + assert!(LastSeen::::get(tx_target, Some(1u16)).is_none()); }); } #[test] -fn clear_rate_limit_fails_when_missing() { +fn is_within_limit_detects_rate_limited_scope() { new_test_ext().execute_with(|| { - let target_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - - assert_noop!( - RateLimiting::clear_rate_limit(RuntimeOrigin::root(), Box::new(target_call)), - Error::::MissingRateLimit + let call = scoped_call(); + let identifier = identifier_for(&call); + let tx_target = target(identifier); + Limits::::insert( + tx_target, + RateLimit::scoped_single(7u16, RateLimitKind::Exact(3)), ); + LastSeen::::insert(tx_target, Some(1u16), 9); + System::set_block_number(11); + let result = RateLimiting::is_within_limit( + &RuntimeOrigin::signed(1), + &call, + &identifier, + &Some(7u16), + &Some(1u16), + ) + .expect("ok"); + assert!(!result); }); } #[test] -fn clear_rate_limit_removes_only_selected_scope() { +fn migrate_usage_key_tracks_scope() { new_test_ext().execute_with(|| { - System::reset_events(); - - let base_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&base_call); - let mut map = BTreeMap::new(); - map.insert(9u16, RateLimitKind::Exact(7)); - map.insert(10u16, RateLimitKind::Exact(5)); - Limits::::insert(identifier, RateLimit::Scoped(map)); - LastSeen::::insert(identifier, Some(9u16), 11); - LastSeen::::insert(identifier, Some(10u16), 12); - LastSeen::::insert(identifier, None::, 13); - - let scoped_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 9 }); - - assert_ok!(RateLimiting::clear_rate_limit( - RuntimeOrigin::root(), - Box::new(scoped_call.clone()), + let call = scoped_call(); + let identifier = identifier_for(&call); + let tx_target = target(identifier); + LastSeen::::insert(tx_target, Some(6u16), 10); + assert!(RateLimiting::migrate_usage_key( + tx_target, + Some(6u16), + Some(7u16) )); - - let config = Limits::::get(identifier).expect("config remains"); - assert!(config.kind_for(Some(&9u16)).is_none()); - assert_eq!( - config.kind_for(Some(&10u16)).copied(), - Some(RateLimitKind::Exact(5)) - ); - assert!(LastSeen::::get(identifier, Some(9u16)).is_none()); - assert_eq!(LastSeen::::get(identifier, Some(10u16)), Some(12)); - assert_eq!( - LastSeen::::get(identifier, None::), - Some(13) - ); - - match pop_last_event() { - RuntimeEvent::RateLimiting(crate::pallet::Event::RateLimitCleared { - transaction, - scope, - .. - }) => { - assert_eq!(transaction, identifier); - assert_eq!(scope, Some(9)); - } - other => panic!("unexpected event: {:?}", other), - } + assert_eq!(LastSeen::::get(tx_target, Some(7u16)), Some(10)); }); } #[test] -fn clear_all_rate_limits_removes_entire_configuration() { +fn migrate_limit_scope_covers_transitions() { new_test_ext().execute_with(|| { - System::reset_events(); + let identifier = register(remark_call(), None); + let tx_target = target(identifier); - let target_call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = identifier_for(&target_call); - - let mut map = BTreeMap::new(); - map.insert(3u16, RateLimitKind::Exact(6)); - map.insert(4u16, RateLimitKind::Exact(7)); - Limits::::insert(identifier, RateLimit::Scoped(map)); - - LastSeen::::insert(identifier, Some(3u16), 11); - LastSeen::::insert(identifier, None::, 12); - - assert_ok!(RateLimiting::clear_all_rate_limits( - RuntimeOrigin::root(), - Box::new(target_call.clone()), + // global -> scoped + assert!(RateLimiting::migrate_limit_scope( + tx_target, + None, + Some(42u16) )); + match Limits::::get(tx_target) { + Some(RateLimit::Scoped(map)) => { + assert_eq!(map.get(&42u16), Some(&RateLimitKind::Default)) + } + other => panic!("unexpected config: {:?}", other), + } - assert!(Limits::::get(identifier).is_none()); - assert!(LastSeen::::get(identifier, Some(3u16)).is_none()); - assert!(LastSeen::::get(identifier, None::).is_none()); - - match pop_last_event() { - RuntimeEvent::RateLimiting(crate::pallet::Event::AllRateLimitsCleared { - transaction, - pallet, - extrinsic, - }) => { - assert_eq!(transaction, identifier); - assert_eq!(pallet, b"RateLimiting".to_vec()); - assert_eq!(extrinsic, b"set_default_rate_limit".to_vec()); + // scoped -> scoped + assert!(RateLimiting::migrate_limit_scope( + tx_target, + Some(42u16), + Some(43u16) + )); + match Limits::::get(tx_target) { + Some(RateLimit::Scoped(map)) => { + assert_eq!(map.get(&43u16), Some(&RateLimitKind::Default)) } - other => panic!("unexpected event: {:?}", other), + other => panic!("unexpected config: {:?}", other), } - }); -} -#[test] -fn clear_all_rate_limits_fails_when_missing() { - new_test_ext().execute_with(|| { - let target_call = - RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }); + // scoped -> global (only entry) + assert!(RateLimiting::migrate_limit_scope( + tx_target, + Some(43u16), + None + )); + assert!(matches!( + Limits::::get(tx_target), + Some(RateLimit::Global(RateLimitKind::Default)) + )); - assert_noop!( - RateLimiting::clear_all_rate_limits(RuntimeOrigin::root(), Box::new(target_call)), - Error::::MissingRateLimit - ); + // no-op when scopes identical + assert!(RateLimiting::migrate_limit_scope(tx_target, None, None)); }); } #[test] -fn set_default_rate_limit_updates_storage_and_emits_event() { +fn set_default_limit_updates_span_and_resolves_in_enforcement() { new_test_ext().execute_with(|| { - System::reset_events(); - + assert_eq!(RateLimiting::default_limit(), 0); assert_ok!(RateLimiting::set_default_rate_limit( RuntimeOrigin::root(), - 42 + 5 )); + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::DefaultRateLimitSet { block_span: 5 }) + )); + assert_eq!(RateLimiting::default_limit(), 5); - assert_eq!(DefaultLimit::::get(), 42); + let call = remark_call(); + let identifier = register(call.clone(), None); + let tx_target = target(identifier); - match pop_last_event() { - RuntimeEvent::RateLimiting(crate::pallet::Event::DefaultRateLimitSet { - block_span, - }) => { - assert_eq!(block_span, 42); - } - other => panic!("unexpected event: {:?}", other), - } + System::set_block_number(10); + // No last-seen yet, first call passes. + assert!( + RateLimiting::is_within_limit( + &RuntimeOrigin::signed(1), + &call, + &identifier, + &None, + &None, + ) + .unwrap() + ); + + LastSeen::::insert(tx_target, None::, 12); + System::set_block_number(15); + // Span 5 should block when delta < 5. + assert!( + !RateLimiting::is_within_limit( + &RuntimeOrigin::signed(1), + &call, + &identifier, + &None, + &None, + ) + .unwrap() + ); }); } #[test] -fn set_default_rate_limit_requires_root() { +fn limit_for_call_names_prefers_scoped_value() { new_test_ext().execute_with(|| { - assert_noop!( - RateLimiting::set_default_rate_limit(RuntimeOrigin::signed(1), 5), - BadOrigin + let call = scoped_call(); + let identifier = identifier_for(&call); + Limits::::insert( + target(identifier), + RateLimit::scoped_single(9u16, RateLimitKind::Exact(8)), ); + let fetched = RateLimiting::limit_for_call_names( + "RateLimiting", + "set_default_rate_limit", + Some(9u16), + ) + .expect("limit"); + assert_eq!(fetched, RateLimitKind::Exact(8)); }); } diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index c6c3eb745c..41b4add270 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -17,7 +17,9 @@ use sp_std::{marker::PhantomData, result::Result}; use crate::{ Config, LastSeen, Pallet, - types::{RateLimitScopeResolver, RateLimitUsageResolver, TransactionIdentifier}, + types::{ + RateLimitScopeResolver, RateLimitTarget, RateLimitUsageResolver, TransactionIdentifier, + }, }; /// Identifier returned in the transaction metadata for the rate limiting extension. @@ -80,8 +82,14 @@ where const IDENTIFIER: &'static str = IDENTIFIER; type Implicit = (); - type Val = Option<(TransactionIdentifier, Option<>::UsageKey>)>; - type Pre = Option<(TransactionIdentifier, Option<>::UsageKey>)>; + type Val = Option<( + RateLimitTarget<>::GroupId>, + Option<>::UsageKey>, + )>; + type Pre = Option<( + RateLimitTarget<>::GroupId>, + Option<>::UsageKey>, + )>; fn weight(&self, _call: &>::RuntimeCall) -> Weight { Weight::zero() @@ -109,7 +117,13 @@ where let scope = >::LimitScopeResolver::context(&origin, call); let usage = >::UsageResolver::context(&origin, call); - let Some(block_span) = Pallet::::effective_span(&origin, call, &identifier, &scope) + let config_target = Pallet::::config_target(&identifier) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + let usage_target = Pallet::::usage_target(&identifier) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + + let Some(block_span) = + Pallet::::effective_span(&origin, call, &config_target, &scope) else { return Ok((ValidTransaction::default(), None, origin)); }; @@ -118,7 +132,7 @@ where return Ok((ValidTransaction::default(), None, origin)); } - let within_limit = Pallet::::within_span(&identifier, &usage, block_span); + let within_limit = Pallet::::within_span(&usage_target, &usage, block_span); if !within_limit { return Err(TransactionValidityError::Invalid( @@ -128,7 +142,7 @@ where Ok(( ValidTransaction::default(), - Some((identifier, usage)), + Some((usage_target, usage)), origin, )) } @@ -152,9 +166,9 @@ where result: &DispatchResult, ) -> Result<(), TransactionValidityError> { if result.is_ok() { - if let Some((identifier, usage)) = pre { + if let Some((target, usage)) = pre { let block_number = frame_system::Pallet::::block_number(); - LastSeen::::insert(&identifier, usage, block_number); + LastSeen::::insert(target, usage, block_number); } } Ok(()) @@ -164,15 +178,18 @@ where #[cfg(test)] mod tests { use codec::Encode; - use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo}; + use frame_support::{ + assert_ok, + dispatch::{GetDispatchInfo, PostDispatchInfo}, + }; use sp_runtime::{ traits::{TransactionExtension, TxBaseImplication}, transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError}, }; use crate::{ - LastSeen, Limits, - types::{RateLimit, RateLimitKind, TransactionIdentifier}, + GroupSharing, LastSeen, Limits, + types::{RateLimit, RateLimitKind}, }; use super::*; @@ -183,14 +200,16 @@ mod tests { } fn bypass_call() -> RuntimeCall { - RuntimeCall::RateLimiting(RateLimitingCall::clear_rate_limit { - call: Box::new(remark_call()), + RuntimeCall::RateLimiting(RateLimitingCall::remove_call_from_group { + transaction: TransactionIdentifier::new(0, 0), }) } fn adjustable_call() -> RuntimeCall { - RuntimeCall::RateLimiting(RateLimitingCall::clear_all_rate_limits { - call: Box::new(remark_call()), + RuntimeCall::RateLimiting(RateLimitingCall::deregister_call { + transaction: TransactionIdentifier::new(0, 0), + scope: None, + clear_usage: false, }) } @@ -198,13 +217,17 @@ mod tests { RateLimitTransactionExtension(Default::default()) } + fn target_for_call(call: &RuntimeCall) -> RateLimitTarget { + RateLimitTarget::Transaction(identifier_for(call)) + } + fn validate_with_tx_extension( extension: &RateLimitTransactionExtension, call: &RuntimeCall, ) -> Result< ( sp_runtime::transaction_validity::ValidTransaction, - Option<(TransactionIdentifier, Option)>, + Option<(RateLimitTarget, Option)>, RuntimeOrigin, ), TransactionValidityError, @@ -250,11 +273,8 @@ mod tests { ) .expect("post_dispatch succeeds"); - let identifier = identifier_for(&call); - assert_eq!( - LastSeen::::get(identifier, None::), - None - ); + let target = target_for_call(&call); + assert_eq!(LastSeen::::get(target, None::), None); }); } @@ -270,8 +290,9 @@ mod tests { assert!(val.is_none()); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(3))); - LastSeen::::insert(identifier, None::, 1); + let target = RateLimitTarget::Transaction(identifier); + Limits::::insert(target, RateLimit::global(RateLimitKind::Exact(3))); + LastSeen::::insert(target, None::, 1); let (_valid, post_val, _) = validate_with_tx_extension(&extension, &call).expect("still bypassed"); @@ -285,8 +306,9 @@ mod tests { let extension = new_tx_extension(); let call = adjustable_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(4))); - LastSeen::::insert(identifier, Some(1u16), 10); + let target = RateLimitTarget::Transaction(identifier); + Limits::::insert(target, RateLimit::global(RateLimitKind::Exact(4))); + LastSeen::::insert(target, Some(1u16), 10); System::set_block_number(14); @@ -308,7 +330,8 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(5))); + let target = RateLimitTarget::Transaction(identifier); + Limits::::insert(target, RateLimit::global(RateLimitKind::Exact(5))); System::set_block_number(10); @@ -334,7 +357,7 @@ mod tests { .expect("post_dispatch succeeds"); assert_eq!( - LastSeen::::get(identifier, None::), + LastSeen::::get(target, None::), Some(10) ); }); @@ -346,8 +369,9 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(5))); - LastSeen::::insert(identifier, None::, 20); + let target = RateLimitTarget::Transaction(identifier); + Limits::::insert(target, RateLimit::global(RateLimitKind::Exact(5))); + LastSeen::::insert(target, None::, 20); System::set_block_number(22); @@ -368,7 +392,8 @@ mod tests { let extension = new_tx_extension(); let call = remark_call(); let identifier = identifier_for(&call); - Limits::::insert(identifier, RateLimit::global(RateLimitKind::Exact(0))); + let target = RateLimitTarget::Transaction(identifier); + Limits::::insert(target, RateLimit::global(RateLimitKind::Exact(0))); System::set_block_number(30); @@ -393,10 +418,80 @@ mod tests { ) .expect("post_dispatch succeeds"); - assert_eq!( - LastSeen::::get(identifier, None::), - None - ); + assert_eq!(LastSeen::::get(target, None::), None); + }); + } + + #[test] + fn tx_extension_respects_usage_group_sharing() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + assert_ok!(RateLimiting::create_group( + RuntimeOrigin::root(), + b"use".to_vec(), + GroupSharing::UsageOnly, + )); + let group = RateLimiting::next_group_id().saturating_sub(1); + + let call = remark_call(); + let identifier = identifier_for(&call); + assert_ok!(RateLimiting::register_call( + RuntimeOrigin::root(), + Box::new(call.clone()), + Some(group), + )); + + let tx_target = RateLimitTarget::Transaction(identifier); + let usage_target = RateLimitTarget::Group(group); + Limits::::insert(tx_target, RateLimit::global(RateLimitKind::Exact(5))); + LastSeen::::insert(usage_target, None::, 10); + System::set_block_number(12); + + let err = validate_with_tx_extension(&extension, &call) + .expect_err("usage grouping should rate limit"); + match err { + TransactionValidityError::Invalid(InvalidTransaction::Custom(code)) => { + assert_eq!(code, RATE_LIMIT_DENIED); + } + other => panic!("unexpected error: {:?}", other), + } + }); + } + + #[test] + fn tx_extension_respects_config_group_sharing() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + assert_ok!(RateLimiting::create_group( + RuntimeOrigin::root(), + b"cfg".to_vec(), + GroupSharing::ConfigOnly, + )); + let group = RateLimiting::next_group_id().saturating_sub(1); + + let call = remark_call(); + let identifier = identifier_for(&call); + assert_ok!(RateLimiting::register_call( + RuntimeOrigin::root(), + Box::new(call.clone()), + Some(group), + )); + + let tx_target = RateLimitTarget::Transaction(identifier); + let group_target = RateLimitTarget::Group(group); + Limits::::remove(tx_target); + Limits::::insert(group_target, RateLimit::global(RateLimitKind::Exact(5))); + LastSeen::::insert(tx_target, None::, 10); + System::set_block_number(12); + + let err = validate_with_tx_extension(&extension, &call) + .expect_err("config grouping should rate limit"); + match err { + TransactionValidityError::Invalid(InvalidTransaction::Custom(code)) => { + assert_eq!(code, RATE_LIMIT_DENIED); + } + other => panic!("unexpected error: {:?}", other), + } }); } } diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 4748f1576e..1faff7c300 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -39,6 +39,8 @@ pub trait RateLimitUsageResolver { Copy, PartialEq, Eq, + PartialOrd, + Ord, Encode, Decode, DecodeWithMemTracking, @@ -53,6 +55,108 @@ pub struct TransactionIdentifier { pub extrinsic_index: u8, } +/// Target identifier for rate limit and usage configuration. +#[derive( + Serialize, + Deserialize, + Clone, + Copy, + PartialEq, + Eq, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + Debug, +)] +pub enum RateLimitTarget { + /// Per-transaction configuration keyed by pallet/extrinsic indices. + Transaction(TransactionIdentifier), + /// Shared configuration for a named group. + Group(GroupId), +} + +impl RateLimitTarget { + /// Returns the transaction identifier when the target represents a single extrinsic. + pub fn as_transaction(&self) -> Option<&TransactionIdentifier> { + match self { + RateLimitTarget::Transaction(identifier) => Some(identifier), + RateLimitTarget::Group(_) => None, + } + } + + /// Returns the group identifier when the target represents a group configuration. + pub fn as_group(&self) -> Option<&GroupId> { + match self { + RateLimitTarget::Transaction(_) => None, + RateLimitTarget::Group(id) => Some(id), + } + } +} + +/// Sharing mode configured for a group. +#[derive( + Serialize, + Deserialize, + Clone, + Copy, + PartialEq, + Eq, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + Debug, +)] +pub enum GroupSharing { + /// Limits remain per transaction; usage is shared by the group. + UsageOnly, + /// Limits are shared by the group; usage remains per transaction. + ConfigOnly, + /// Both limits and usage are shared by the group. + ConfigAndUsage, +} + +impl GroupSharing { + /// Returns `true` when configuration for this group should use the group target key. + pub fn config_uses_group(self) -> bool { + matches!( + self, + GroupSharing::ConfigOnly | GroupSharing::ConfigAndUsage + ) + } + + /// Returns `true` when usage tracking for this group should use the group target key. + pub fn usage_uses_group(self) -> bool { + matches!(self, GroupSharing::UsageOnly | GroupSharing::ConfigAndUsage) + } +} + +/// Metadata describing a configured group. +#[derive( + Serialize, + Deserialize, + Clone, + PartialEq, + Eq, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + Debug, +)] +pub struct RateLimitGroup { + /// Stable identifier assigned to the group. + pub id: GroupId, + /// Human readable group name. + pub name: Name, + /// Sharing configuration enforced for the group. + pub sharing: GroupSharing, +} + impl TransactionIdentifier { /// Builds a new identifier from pallet/extrinsic indices. pub const fn new(pallet_index: u8, extrinsic_index: u8) -> Self { @@ -218,8 +322,8 @@ mod tests { // System is the first pallet in the mock runtime, RateLimiting is second. assert_eq!(identifier.pallet_index, 1); - // set_default_rate_limit has call_index 2. - assert_eq!(identifier.extrinsic_index, 3); + // set_default_rate_limit has call_index 4. + assert_eq!(identifier.extrinsic_index, 4); } #[test] diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index 243c43b7ea..6582db52ed 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -14,13 +14,12 @@ use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; use subtensor_runtime_common::{MechId, NetUid, RateLimitScope, RateLimitUsageKey}; use pallet_subtensor::{ - self, + self, AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, + LastRateLimitedBlock, LastUpdate, MaxUidsTrimmingRateLimit, MechanismCountCurrent, + MechanismCountSetRateLimit, MechanismEmissionRateLimit, NetworkRateLimit, + OwnerHyperparamRateLimit, Pallet, Prometheus, RateLimitKey, TransactionKeyLastBlock, + TxChildkeyTakeRateLimit, TxDelegateTakeRateLimit, TxRateLimit, WeightsVersionKeyRateLimit, utils::rate_limiting::{Hyperparameter, TransactionType}, - AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, LastRateLimitedBlock, - LastUpdate, MaxUidsTrimmingRateLimit, MechanismCountCurrent, MechanismCountSetRateLimit, - MechanismEmissionRateLimit, NetworkRateLimit, OwnerHyperparamRateLimit, Pallet, Prometheus, - RateLimitKey, TransactionKeyLastBlock, TxChildkeyTakeRateLimit, TxDelegateTakeRateLimit, - TxRateLimit, WeightsVersionKeyRateLimit, }; /// Pallet index assigned to `pallet_subtensor` in `construct_runtime!`. @@ -139,12 +138,12 @@ fn gather_simple_limits(limits: &mut LimitEntries) -> u64 set_global_limit::(limits, subtensor_identifier(70), span); } - reads += 1; - if let Some(span) = block_number::(TxDelegateTakeRateLimit::::get()) { - // TODO(grouped-rate-limits): `decrease_take` shares the same timestamp but - // does not have its own ID here yet. - set_global_limit::(limits, subtensor_identifier(66), span); - } + reads += 1; + if let Some(span) = block_number::(TxDelegateTakeRateLimit::::get()) { + // TODO(grouped-rate-limits): `decrease_take` shares the same timestamp but + // does not have its own ID here yet. + set_global_limit::(limits, subtensor_identifier(66), span); + } reads += 1; if let Some(span) = block_number::(TxChildkeyTakeRateLimit::::get()) { @@ -468,11 +467,11 @@ fn write_limits(limits: &LimitEntries) -> u64 { if limits.is_empty() { return 0; } - let prefix = storage_prefix("RateLimiting", "Limits"); + let limits_prefix = storage_prefix("RateLimiting", "Limits"); let mut writes = 0; for (identifier, limit) in limits.iter() { - let key = map_storage_key(&prefix, identifier); - storage::set(&key, &limit.encode()); + let limit_key = map_storage_key(&limits_prefix, identifier); + storage::set(&limit_key, &limit.encode()); writes += 1; } writes From 434f7e31faac27f19c9288a31465ec54a3ddee44 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 21 Nov 2025 18:44:40 +0300 Subject: [PATCH 25/95] Add groups to rate-limiting migration --- runtime/src/rate_limiting/migration.rs | 510 ++++++++++++++++++++----- 1 file changed, 408 insertions(+), 102 deletions(-) diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index 6582db52ed..c4217b6dd4 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -4,15 +4,9 @@ use codec::Encode; use frame_support::{pallet_prelude::Parameter, traits::Get, weights::Weight}; use frame_system::pallet_prelude::BlockNumberFor; use log::info; -use pallet_rate_limiting::{RateLimit, RateLimitKind, TransactionIdentifier}; -use sp_io::{ - hashing::{blake2_128, twox_128}, - storage, +use pallet_rate_limiting::{ + GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitTarget, TransactionIdentifier, }; -use sp_runtime::traits::SaturatedConversion; -use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; -use subtensor_runtime_common::{MechId, NetUid, RateLimitScope, RateLimitUsageKey}; - use pallet_subtensor::{ self, AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, LastRateLimitedBlock, LastUpdate, MaxUidsTrimmingRateLimit, MechanismCountCurrent, @@ -21,6 +15,27 @@ use pallet_subtensor::{ TxChildkeyTakeRateLimit, TxDelegateTakeRateLimit, TxRateLimit, WeightsVersionKeyRateLimit, utils::rate_limiting::{Hyperparameter, TransactionType}, }; +use sp_io::{ + hashing::{blake2_128, twox_128}, + storage, +}; +use sp_runtime::traits::SaturatedConversion; +use sp_std::{ + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + vec, + vec::Vec, +}; +use subtensor_runtime_common::{MechId, NetUid, RateLimitScope, RateLimitUsageKey}; + +type RateLimitConfigOf = RateLimit>; +type RateLimitTargetOf = RateLimitTarget; +type RateLimitGroupOf = RateLimitGroup>; +type LimitEntries = Vec<(RateLimitTargetOf, RateLimitConfigOf)>; +type LastSeenKey = ( + RateLimitTargetOf, + Option::AccountId>>, +); +type LastSeenEntries = Vec<(LastSeenKey, BlockNumberFor)>; /// Pallet index assigned to `pallet_subtensor` in `construct_runtime!`. const SUBTENSOR_PALLET_INDEX: u8 = 7; @@ -35,22 +50,79 @@ const SET_CHILDREN_RATE_LIMIT: u64 = 150; /// `set_sn_owner_hotkey` default interval (blocks). const DEFAULT_SET_SN_OWNER_HOTKEY_LIMIT: u64 = 50_400; -/// Subtensor call indices that reuse the serving rate-limit configuration. -/// TODO(grouped-rate-limits): `serve_axon` (4), `serve_axon_tls` (40), and -/// `serve_prometheus` (5) share one cooldown today. The new pallet still misses -/// grouped identifiers, so we simply port the timers as-is. -const SERVE_CALLS: [u8; 3] = [4, 40, 5]; -/// Subtensor call indices that reuse the per-subnet weight limit. -/// TODO(grouped-rate-limits): Weight commits via call 100 still touch the same -/// `LastUpdate` entries but cannot be expressed here until grouping exists. -const WEIGHT_CALLS_SUBNET: [u8; 3] = [0, 96, 113]; -/// Subtensor call indices that reuse the per-mechanism weight limit. -const WEIGHT_CALLS_MECHANISM: [u8; 4] = [119, 115, 117, 118]; -/// Subtensor call indices for register-network extrinsics. -/// TODO(grouped-rate-limits): `register_network` (59) and -/// `register_network_with_identity` (79) still share the same helper and should -/// remain grouped once pallet-rate-limiting supports aliases. -const REGISTER_NETWORK_CALLS: [u8; 2] = [59, 79]; +type GroupId = u32; + +struct GroupDefinition { + id: GroupId, + name: &'static [u8], + sharing: GroupSharing, + members: Vec, +} + +const GROUP_SERVE_AXON: GroupId = 0; +const GROUP_DELEGATE_TAKE: GroupId = 1; +const GROUP_WEIGHTS_SUBNET: GroupId = 2; +const GROUP_WEIGHTS_MECHANISM: GroupId = 3; +const GROUP_REGISTER_NETWORK: GroupId = 4; +const GROUP_OWNER_HPARAMS: GroupId = 5; + +fn hyperparameter_identifiers() -> Vec { + HYPERPARAMETERS + .iter() + .filter_map(|h| identifier_for_hyperparameter(*h)) + .collect() +} + +fn group_definitions() -> Vec { + vec![ + GroupDefinition { + id: GROUP_SERVE_AXON, + name: b"serve-axon", + sharing: GroupSharing::ConfigAndUsage, + members: vec![subtensor_identifier(4), subtensor_identifier(40)], + }, + GroupDefinition { + id: GROUP_DELEGATE_TAKE, + name: b"delegate-take", + sharing: GroupSharing::ConfigAndUsage, + members: vec![subtensor_identifier(66), subtensor_identifier(65)], + }, + GroupDefinition { + id: GROUP_WEIGHTS_SUBNET, + name: b"weights-subnet", + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + subtensor_identifier(0), + subtensor_identifier(96), + subtensor_identifier(100), + subtensor_identifier(113), + ], + }, + GroupDefinition { + id: GROUP_WEIGHTS_MECHANISM, + name: b"weights-mechanism", + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + subtensor_identifier(119), + subtensor_identifier(115), + subtensor_identifier(117), + subtensor_identifier(118), + ], + }, + GroupDefinition { + id: GROUP_REGISTER_NETWORK, + name: b"register-network", + sharing: GroupSharing::ConfigAndUsage, + members: vec![subtensor_identifier(59), subtensor_identifier(79)], + }, + GroupDefinition { + id: GROUP_OWNER_HPARAMS, + name: b"owner-hparams", + sharing: GroupSharing::ConfigOnly, + members: hyperparameter_identifiers(), + }, + ] +} /// Hyperparameter extrinsics routed through owner-or-root rate limiting. const HYPERPARAMETERS: &[Hyperparameter] = &[ @@ -80,13 +152,111 @@ const HYPERPARAMETERS: &[Hyperparameter] = &[ Hyperparameter::RecycleOrBurn, ]; -type RateLimitConfigOf = RateLimit>; -type LimitEntries = Vec<(TransactionIdentifier, RateLimitConfigOf)>; -type LastSeenKey = ( - TransactionIdentifier, - Option::AccountId>>, -); -type LastSeenEntries = Vec<(LastSeenKey, BlockNumberFor)>; +#[derive(Clone, Copy)] +struct GroupInfo { + id: GroupId, + sharing: GroupSharing, +} + +#[derive(Default)] +struct Grouping { + assignments: BTreeMap, + members: BTreeMap>, + details: Vec, + next_group_id: GroupId, + max_group_id: Option, +} + +impl Grouping { + fn members(&self, id: GroupId) -> Option<&BTreeSet> { + self.members.get(&id) + } + + fn insert_group( + &mut self, + id: GroupId, + name: &[u8], + sharing: GroupSharing, + members: &[TransactionIdentifier], + ) { + let entry = self.members.entry(id).or_insert_with(BTreeSet::new); + for member in members { + self.assignments.insert(*member, GroupInfo { id, sharing }); + entry.insert(*member); + } + + self.details.push(RateLimitGroup { + id, + name: name.to_vec(), + sharing, + }); + + self.max_group_id = Some(self.max_group_id.map_or(id, |current| current.max(id))); + } + + fn finalize_next_id(&mut self) { + self.next_group_id = self.max_group_id.map_or(0, |id| id.saturating_add(1)); + } + + fn config_target(&self, identifier: TransactionIdentifier) -> RateLimitTargetOf { + if let Some(info) = self.assignments.get(&identifier) { + if info.sharing.config_uses_group() { + return RateLimitTarget::Group(info.id); + } + } + RateLimitTarget::Transaction(identifier) + } + + fn usage_target(&self, identifier: TransactionIdentifier) -> RateLimitTargetOf { + if let Some(info) = self.assignments.get(&identifier) { + if info.sharing.usage_uses_group() { + return RateLimitTarget::Group(info.id); + } + } + RateLimitTarget::Transaction(identifier) + } +} + +const SERVE_PROM_IDENTIFIER: TransactionIdentifier = subtensor_identifier(5); + +fn serve_calls(grouping: &Grouping) -> Vec { + let mut calls = Vec::new(); + if let Some(members) = grouping.members(GROUP_SERVE_AXON) { + calls.extend(members.iter().copied()); + } + calls.push(SERVE_PROM_IDENTIFIER); + calls +} + +fn weight_calls_subnet(grouping: &Grouping) -> Vec { + grouping + .members(GROUP_WEIGHTS_SUBNET) + .map(|m| m.iter().copied().collect()) + .unwrap_or_default() +} + +fn weight_calls_mechanism(grouping: &Grouping) -> Vec { + grouping + .members(GROUP_WEIGHTS_MECHANISM) + .map(|m| m.iter().copied().collect()) + .unwrap_or_default() +} + +fn build_grouping() -> Grouping { + let mut grouping = Grouping::default(); + + for definition in group_definitions() { + grouping.insert_group( + definition.id, + definition.name, + definition.sharing, + &definition.members, + ); + } + + grouping.finalize_next_id(); + grouping +} pub fn migrate_rate_limiting() -> Weight { let mut weight = T::DbWeight::get().reads(1); @@ -95,108 +265,162 @@ pub fn migrate_rate_limiting() -> Weight { return weight; } - let (limits, limit_reads) = build_limits::(); - let (last_seen, seen_reads) = build_last_seen::(); + let grouping = build_grouping(); + let (limits, limit_reads) = build_limits::(&grouping); + let (last_seen, seen_reads) = build_last_seen::(&grouping); let limit_writes = write_limits::(&limits); let seen_writes = write_last_seen::(&last_seen); + let group_writes = write_groups::(&grouping); HasMigrationRun::::insert(MIGRATION_NAME, true); weight = weight .saturating_add(T::DbWeight::get().reads(limit_reads.saturating_add(seen_reads))) .saturating_add( - T::DbWeight::get().writes(limit_writes.saturating_add(seen_writes).saturating_add(1)), + T::DbWeight::get().writes( + limit_writes + .saturating_add(seen_writes) + .saturating_add(group_writes) + .saturating_add(1), + ), ); info!( - "Migrated {} rate-limit configs and {} last-seen entries into pallet-rate-limiting", + "Migrated {} rate-limit configs, {} last-seen entries, and {} groups into pallet-rate-limiting", limits.len(), - last_seen.len() + last_seen.len(), + grouping.details.len() ); weight } -fn build_limits() -> (LimitEntries, u64) { +fn build_limits(grouping: &Grouping) -> (LimitEntries, u64) { let mut limits = LimitEntries::::new(); let mut reads: u64 = 0; - reads += gather_simple_limits::(&mut limits); - reads += gather_owner_hparam_limits::(&mut limits); - reads += gather_serving_limits::(&mut limits); - reads += gather_weight_limits::(&mut limits); + reads += gather_simple_limits::(&mut limits, grouping); + reads += gather_owner_hparam_limits::(&mut limits, grouping); + reads += gather_serving_limits::(&mut limits, grouping); + reads += gather_weight_limits::(&mut limits, grouping); (limits, reads) } -fn gather_simple_limits(limits: &mut LimitEntries) -> u64 { +fn gather_simple_limits( + limits: &mut LimitEntries, + grouping: &Grouping, +) -> u64 { let mut reads: u64 = 0; reads += 1; if let Some(span) = block_number::(TxRateLimit::::get()) { - set_global_limit::(limits, subtensor_identifier(70), span); + set_global_limit::( + limits, + grouping.config_target(subtensor_identifier(70)), + span, + ); } reads += 1; if let Some(span) = block_number::(TxDelegateTakeRateLimit::::get()) { - // TODO(grouped-rate-limits): `decrease_take` shares the same timestamp but - // does not have its own ID here yet. - set_global_limit::(limits, subtensor_identifier(66), span); + if let Some(members) = grouping.members(GROUP_DELEGATE_TAKE) { + for call in members { + set_global_limit::(limits, grouping.config_target(*call), span); + } + } } reads += 1; if let Some(span) = block_number::(TxChildkeyTakeRateLimit::::get()) { - set_global_limit::(limits, subtensor_identifier(75), span); + set_global_limit::( + limits, + grouping.config_target(subtensor_identifier(75)), + span, + ); } reads += 1; if let Some(span) = block_number::(NetworkRateLimit::::get()) { - for call in REGISTER_NETWORK_CALLS { - set_global_limit::(limits, subtensor_identifier(call), span); + if let Some(members) = grouping.members(GROUP_REGISTER_NETWORK) { + for call in members { + set_global_limit::(limits, grouping.config_target(*call), span); + } } } reads += 1; if let Some(span) = block_number::(WeightsVersionKeyRateLimit::::get()) { - set_global_limit::(limits, admin_utils_identifier(6), span); + set_global_limit::( + limits, + grouping.config_target(admin_utils_identifier(6)), + span, + ); } if let Some(span) = block_number::(DEFAULT_SET_SN_OWNER_HOTKEY_LIMIT) { - set_global_limit::(limits, admin_utils_identifier(67), span); + set_global_limit::( + limits, + grouping.config_target(admin_utils_identifier(67)), + span, + ); } if let Some(span) = block_number::(::EvmKeyAssociateRateLimit::get()) { - set_global_limit::(limits, subtensor_identifier(93), span); + set_global_limit::( + limits, + grouping.config_target(subtensor_identifier(93)), + span, + ); } if let Some(span) = block_number::(MechanismCountSetRateLimit::::get()) { - set_global_limit::(limits, admin_utils_identifier(76), span); + set_global_limit::( + limits, + grouping.config_target(admin_utils_identifier(76)), + span, + ); } if let Some(span) = block_number::(MechanismEmissionRateLimit::::get()) { - set_global_limit::(limits, admin_utils_identifier(77), span); + set_global_limit::( + limits, + grouping.config_target(admin_utils_identifier(77)), + span, + ); } if let Some(span) = block_number::(MaxUidsTrimmingRateLimit::::get()) { - set_global_limit::(limits, admin_utils_identifier(78), span); + set_global_limit::( + limits, + grouping.config_target(admin_utils_identifier(78)), + span, + ); } if let Some(span) = block_number::(SET_CHILDREN_RATE_LIMIT) { - set_global_limit::(limits, subtensor_identifier(67), span); + set_global_limit::( + limits, + grouping.config_target(subtensor_identifier(67)), + span, + ); } reads } -fn gather_owner_hparam_limits(limits: &mut LimitEntries) -> u64 { +fn gather_owner_hparam_limits( + limits: &mut LimitEntries, + grouping: &Grouping, +) -> u64 { let mut reads: u64 = 0; reads += 1; if let Some(span) = block_number::(u64::from(OwnerHyperparamRateLimit::::get())) { for hparam in HYPERPARAMETERS { if let Some(identifier) = identifier_for_hyperparameter(*hparam) { - set_global_limit::(limits, identifier, span); + set_global_limit::(limits, grouping.config_target(identifier), span); } } } @@ -204,17 +428,20 @@ fn gather_owner_hparam_limits(limits: &mut LimitEntries) reads } -fn gather_serving_limits(limits: &mut LimitEntries) -> u64 { +fn gather_serving_limits( + limits: &mut LimitEntries, + grouping: &Grouping, +) -> u64 { let mut reads: u64 = 0; let netuids = Pallet::::get_all_subnet_netuids(); for netuid in netuids { reads += 1; if let Some(span) = block_number::(Pallet::::get_serving_rate_limit(netuid)) { - for call in SERVE_CALLS { + for call in serve_calls(grouping) { set_scoped_limit::( limits, - subtensor_identifier(call), + grouping.config_target(call), RateLimitScope::Subnet(netuid), span, ); @@ -225,19 +452,24 @@ fn gather_serving_limits(limits: &mut LimitEntries) -> u6 reads } -fn gather_weight_limits(limits: &mut LimitEntries) -> u64 { +fn gather_weight_limits( + limits: &mut LimitEntries, + grouping: &Grouping, +) -> u64 { let mut reads: u64 = 0; let netuids = Pallet::::get_all_subnet_netuids(); let mut subnet_limits = BTreeMap::>::new(); + let subnet_calls = weight_calls_subnet(grouping); + let mechanism_calls = weight_calls_mechanism(grouping); for netuid in &netuids { reads += 1; if let Some(span) = block_number::(Pallet::::get_weights_set_rate_limit(*netuid)) { subnet_limits.insert(*netuid, span); - for call in WEIGHT_CALLS_SUBNET { + for call in &subnet_calls { set_scoped_limit::( limits, - subtensor_identifier(call), + grouping.config_target(*call), RateLimitScope::Subnet(*netuid), span, ); @@ -259,8 +491,8 @@ fn gather_weight_limits(limits: &mut LimitEntries) -> u64 netuid: *netuid, mecid: MechId::from(mecid), }; - for call in WEIGHT_CALLS_MECHANISM { - set_scoped_limit::(limits, subtensor_identifier(call), scope.clone(), span); + for call in &mechanism_calls { + set_scoped_limit::(limits, grouping.config_target(*call), scope.clone(), span); } } } @@ -268,20 +500,23 @@ fn gather_weight_limits(limits: &mut LimitEntries) -> u64 reads } -fn build_last_seen() -> (LastSeenEntries, u64) { +fn build_last_seen(grouping: &Grouping) -> (LastSeenEntries, u64) { let mut last_seen = LastSeenEntries::::new(); let mut reads: u64 = 0; - reads += import_last_rate_limited_blocks::(&mut last_seen); - reads += import_transaction_key_last_blocks::(&mut last_seen); - reads += import_last_update_entries::(&mut last_seen); - reads += import_serving_entries::(&mut last_seen); - reads += import_evm_entries::(&mut last_seen); + reads += import_last_rate_limited_blocks::(&mut last_seen, grouping); + reads += import_transaction_key_last_blocks::(&mut last_seen, grouping); + reads += import_last_update_entries::(&mut last_seen, grouping); + reads += import_serving_entries::(&mut last_seen, grouping); + reads += import_evm_entries::(&mut last_seen, grouping); (last_seen, reads) } -fn import_last_rate_limited_blocks(entries: &mut LastSeenEntries) -> u64 { +fn import_last_rate_limited_blocks( + entries: &mut LastSeenEntries, + grouping: &Grouping, +) -> u64 { let mut reads: u64 = 0; for (key, block) in LastRateLimitedBlock::::iter() { reads += 1; @@ -295,7 +530,7 @@ fn import_last_rate_limited_blocks(entries: &mut LastSeenEnt { record_last_seen_entry::( entries, - identifier, + grouping.usage_target(identifier), Some(RateLimitUsageKey::Subnet(netuid)), block, ); @@ -305,7 +540,7 @@ fn import_last_rate_limited_blocks(entries: &mut LastSeenEnt if let Some(identifier) = identifier_for_hyperparameter(hyper) { record_last_seen_entry::( entries, - identifier, + grouping.usage_target(identifier), Some(RateLimitUsageKey::Subnet(netuid)), block, ); @@ -314,7 +549,7 @@ fn import_last_rate_limited_blocks(entries: &mut LastSeenEnt RateLimitKey::LastTxBlock(account) => { record_last_seen_entry::( entries, - subtensor_identifier(70), + grouping.usage_target(subtensor_identifier(70)), Some(RateLimitUsageKey::Account(account.clone())), block, ); @@ -322,21 +557,31 @@ fn import_last_rate_limited_blocks(entries: &mut LastSeenEnt RateLimitKey::LastTxBlockDelegateTake(account) => { record_last_seen_entry::( entries, - subtensor_identifier(66), + grouping.usage_target(subtensor_identifier(66)), Some(RateLimitUsageKey::Account(account.clone())), block, ); } - RateLimitKey::NetworkLastRegistered | RateLimitKey::LastTxBlockChildKeyTake(_) => { - // TODO(grouped-rate-limits): Global network registration lock is still outside - // pallet-rate-limiting. We will migrate it once grouped identifiers land. + RateLimitKey::NetworkLastRegistered => { + record_last_seen_entry::( + entries, + grouping.usage_target(subtensor_identifier(59)), + None, + block, + ); + } + RateLimitKey::LastTxBlockChildKeyTake(_) => { + // Deprecated storage; ignored. } } } reads } -fn import_transaction_key_last_blocks(entries: &mut LastSeenEntries) -> u64 { +fn import_transaction_key_last_blocks( + entries: &mut LastSeenEntries, + grouping: &Grouping, +) -> u64 { let mut reads: u64 = 0; for ((account, netuid, tx_kind), block) in TransactionKeyLastBlock::::iter() { reads += 1; @@ -350,13 +595,23 @@ fn import_transaction_key_last_blocks(entries: &mut LastSeen let Some(usage) = usage_key_from_transaction_type(tx_type, &account, netuid) else { continue; }; - record_last_seen_entry::(entries, identifier, Some(usage), block); + record_last_seen_entry::( + entries, + grouping.usage_target(identifier), + Some(usage), + block, + ); } reads } -fn import_last_update_entries(entries: &mut LastSeenEntries) -> u64 { +fn import_last_update_entries( + entries: &mut LastSeenEntries, + grouping: &Grouping, +) -> u64 { let mut reads: u64 = 0; + let subnet_calls = weight_calls_subnet(grouping); + let mechanism_calls = weight_calls_mechanism(grouping); for (index, blocks) in LastUpdate::::iter() { reads += 1; let netuid = Pallet::::get_netuid(index); @@ -389,16 +644,16 @@ fn import_last_update_entries(entries: &mut LastSeenEntries< } }; - let call_set: &[u8] = if is_mechanism { - &WEIGHT_CALLS_MECHANISM + let call_set: &[TransactionIdentifier] = if is_mechanism { + mechanism_calls.as_slice() } else { - &WEIGHT_CALLS_SUBNET + subnet_calls.as_slice() }; for call in call_set { record_last_seen_entry::( entries, - subtensor_identifier(*call), + grouping.usage_target(*call), Some(usage.clone()), last_block, ); @@ -408,7 +663,10 @@ fn import_last_update_entries(entries: &mut LastSeenEntries< reads } -fn import_serving_entries(entries: &mut LastSeenEntries) -> u64 { +fn import_serving_entries( + entries: &mut LastSeenEntries, + grouping: &Grouping, +) -> u64 { let mut reads: u64 = 0; for (netuid, hotkey, axon) in Axons::::iter() { reads += 1; @@ -419,10 +677,14 @@ fn import_serving_entries(entries: &mut LastSeenEntries) account: hotkey.clone(), netuid, }; - for call in [4u8, 40u8] { + let axon_calls: Vec<_> = grouping + .members(GROUP_SERVE_AXON) + .map(|m| m.iter().copied().collect()) + .unwrap_or_else(|| vec![subtensor_identifier(4), subtensor_identifier(40)]); + for call in axon_calls { record_last_seen_entry::( entries, - subtensor_identifier(call), + grouping.usage_target(call), Some(usage.clone()), axon.block, ); @@ -438,13 +700,21 @@ fn import_serving_entries(entries: &mut LastSeenEntries) account: hotkey, netuid, }; - record_last_seen_entry::(entries, subtensor_identifier(5), Some(usage), prom.block); + record_last_seen_entry::( + entries, + grouping.usage_target(SERVE_PROM_IDENTIFIER), + Some(usage), + prom.block, + ); } reads } -fn import_evm_entries(entries: &mut LastSeenEntries) -> u64 { +fn import_evm_entries( + entries: &mut LastSeenEntries, + grouping: &Grouping, +) -> u64 { let mut reads: u64 = 0; for (netuid, uid, (_, block)) in AssociatedEvmAddress::::iter() { reads += 1; @@ -453,7 +723,7 @@ fn import_evm_entries(entries: &mut LastSeenEntries) -> u } record_last_seen_entry::( entries, - subtensor_identifier(93), + grouping.usage_target(subtensor_identifier(93)), Some(RateLimitUsageKey::SubnetNeuron { netuid, uid }), block, ); @@ -491,6 +761,42 @@ fn write_last_seen(entries: &LastSeenEntries) -> u64 { writes } +fn write_groups(grouping: &Grouping) -> u64 { + let mut writes = 0; + let groups_prefix = storage_prefix("RateLimiting", "Groups"); + let members_prefix = storage_prefix("RateLimiting", "GroupMembers"); + let name_index_prefix = storage_prefix("RateLimiting", "GroupNameIndex"); + let call_groups_prefix = storage_prefix("RateLimiting", "CallGroups"); + let next_group_id_prefix = storage_prefix("RateLimiting", "NextGroupId"); + + for detail in &grouping.details { + let group_key = map_storage_key(&groups_prefix, detail.id); + storage::set(&group_key, &detail.encode()); + writes += 1; + + let name_key = map_storage_key(&name_index_prefix, detail.name.clone()); + storage::set(&name_key, &detail.id.encode()); + writes += 1; + } + + for (group, members) in &grouping.members { + let members_key = map_storage_key(&members_prefix, *group); + storage::set(&members_key, &members.encode()); + writes += 1; + } + + for (identifier, info) in &grouping.assignments { + let call_key = map_storage_key(&call_groups_prefix, *identifier); + storage::set(&call_key, &info.id.encode()); + writes += 1; + } + + storage::set(&next_group_id_prefix, &grouping.next_group_id.encode()); + writes += 1; + + writes +} + fn block_number(value: u64) -> Option> { if value == 0 { return None; @@ -500,23 +806,23 @@ fn block_number(value: u64) -> Option> { fn set_global_limit( limits: &mut LimitEntries, - identifier: TransactionIdentifier, + target: RateLimitTargetOf, span: BlockNumberFor, ) { - if let Some((_, config)) = limits.iter_mut().find(|(id, _)| *id == identifier) { + if let Some((_, config)) = limits.iter_mut().find(|(id, _)| *id == target) { *config = RateLimit::global(RateLimitKind::Exact(span)); } else { - limits.push((identifier, RateLimit::global(RateLimitKind::Exact(span)))); + limits.push((target, RateLimit::global(RateLimitKind::Exact(span)))); } } fn set_scoped_limit( limits: &mut LimitEntries, - identifier: TransactionIdentifier, + target: RateLimitTargetOf, scope: RateLimitScope, span: BlockNumberFor, ) { - if let Some((_, config)) = limits.iter_mut().find(|(id, _)| *id == identifier) { + if let Some((_, config)) = limits.iter_mut().find(|(id, _)| *id == target) { match config { RateLimit::Global(_) => { *config = RateLimit::scoped_single(scope, RateLimitKind::Exact(span)); @@ -527,7 +833,7 @@ fn set_scoped_limit( } } else { limits.push(( - identifier, + target, RateLimit::scoped_single(scope, RateLimitKind::Exact(span)), )); } @@ -535,7 +841,7 @@ fn set_scoped_limit( fn record_last_seen_entry( entries: &mut LastSeenEntries, - identifier: TransactionIdentifier, + target: RateLimitTargetOf, usage: Option>, block: u64, ) { @@ -543,7 +849,7 @@ fn record_last_seen_entry( return; }; - let key = (identifier, usage); + let key = (target, usage); if let Some((_, existing)) = entries.iter_mut().find(|(entry_key, _)| *entry_key == key) { if block_number > *existing { *existing = block_number; @@ -692,7 +998,7 @@ where netuid, }), TransactionType::OwnerHyperparamUpdate(_) => Some(RateLimitUsageKey::Subnet(netuid)), - TransactionType::RegisterNetwork => Some(RateLimitUsageKey::Account(account.clone())), + TransactionType::RegisterNetwork => None, TransactionType::SetSNOwnerHotkey => Some(RateLimitUsageKey::Subnet(netuid)), TransactionType::Unknown => None, _ => None, From eaca7aaa6ec8c4b945e477cb25a472dd54d3be75 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 24 Nov 2025 16:41:43 +0300 Subject: [PATCH 26/95] Configure pallet-rate-limiting for the runtime --- Cargo.lock | 1 + pallets/rate-limiting/src/lib.rs | 12 +- pallets/rate-limiting/src/tx_extension.rs | 10 + runtime/Cargo.toml | 2 + runtime/src/lib.rs | 77 ++++- runtime/src/rate_limiting/migration.rs | 354 +++++++++++++++------- 6 files changed, 341 insertions(+), 115 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04686202fa..c714894796 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8298,6 +8298,7 @@ dependencies = [ "pallet-offences", "pallet-preimage", "pallet-rate-limiting", + "pallet-rate-limiting-runtime-api", "pallet-registry", "pallet-safe-mode", "pallet-scheduler", diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 8b7cc1072f..b40976f018 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -440,7 +440,6 @@ pub mod pallet { pub groups: Vec<(>::GroupId, Vec, GroupSharing)>, } - #[cfg(feature = "std")] impl, I: 'static> Default for GenesisConfig { fn default() -> Self { Self { @@ -541,7 +540,9 @@ pub mod pallet { Ok(Self::within_span(&usage_target, usage_key, block_span)) } - pub(crate) fn resolved_limit( + /// Resolves the configured span for the provided target/scope, applying the pallet default + /// when the stored value uses [`RateLimitKind::Default`]. + pub fn resolved_limit( target: &RateLimitTarget<>::GroupId>, scope: &Option<>::LimitScope>, ) -> Option> { @@ -689,7 +690,8 @@ pub mod pallet { Self::resolved_limit(&target, &scope) } - fn identifier_for_call_names( + /// Looks up the transaction identifier for a pallet/extrinsic name pair. + pub fn identifier_for_call_names( pallet_name: &str, extrinsic_name: &str, ) -> Option { @@ -730,7 +732,9 @@ pub mod pallet { )) } - pub(crate) fn config_target( + /// Returns the storage target used to store configuration for the provided identifier, + /// respecting any configured group assignment. + pub fn config_target( identifier: &TransactionIdentifier, ) -> Result>::GroupId>, DispatchError> { Self::target_for(identifier, GroupSharing::config_uses_group) diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index 41b4add270..3d400e6918 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -35,6 +35,16 @@ where T: Config + Send + Sync + TypeInfo, I: 'static + TypeInfo; +impl RateLimitTransactionExtension +where + T: Config + Send + Sync + TypeInfo, + I: 'static + TypeInfo, +{ + pub fn new() -> Self { + Self(PhantomData) + } +} + impl Clone for RateLimitTransactionExtension where T: Config + Send + Sync + TypeInfo, diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 5d40215c49..e3b926e859 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -39,6 +39,7 @@ frame-try-runtime = { workspace = true, optional = true } pallet-timestamp.workspace = true pallet-transaction-payment.workspace = true pallet-rate-limiting.workspace = true +pallet-rate-limiting-runtime-api.workspace = true pallet-subtensor-utility.workspace = true frame-executive.workspace = true frame-metadata-hash-extension.workspace = true @@ -189,6 +190,7 @@ std = [ "pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment/std", "pallet-rate-limiting/std", + "pallet-rate-limiting-runtime-api/std", "pallet-subtensor-utility/std", "pallet-sudo/std", "pallet-multisig/std", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 2c642c9af5..6bddb38350 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -24,7 +24,9 @@ use frame_support::{ dispatch::DispatchResult, genesis_builder_helper::{build_state, get_preset}, pallet_prelude::Get, - traits::{Contains, InsideBoth, LinearStoragePrice, fungible::HoldConsideration}, + traits::{ + Contains, GetCallMetadata, InsideBoth, LinearStoragePrice, fungible::HoldConsideration, + }, }; use frame_system::{EnsureRoot, EnsureRootWithSuccess, EnsureSigned}; use pallet_commitments::{CanCommit, OnMetadataCommitment}; @@ -74,6 +76,8 @@ use subtensor_swap_interface::{Order, SwapHandler}; pub use rate_limiting::{ ScopeResolver as RuntimeScopeResolver, UsageResolver as RuntimeUsageResolver, }; +pub type RateLimitingInstance = (); +pub type RateLimitGroupId = u32; // A few exports that help ease life for downstream crates. pub use frame_support::{ @@ -158,6 +162,7 @@ impl frame_system::offchain::CreateSignedTransaction frame_system::CheckEra::::from(Era::Immortal), check_nonce::CheckNonce::::from(nonce).into(), frame_system::CheckWeight::::new(), + pallet_rate_limiting::RateLimitTransactionExtension::::new(), ChargeTransactionPaymentWrapper::new( pallet_transaction_payment::ChargeTransactionPayment::::from(0), ), @@ -225,10 +230,10 @@ 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: 347, + spec_version: 348, impl_version: 1, apis: RUNTIME_API_VERSIONS, - transaction_version: 1, + transaction_version: 2, system_version: 1, }; @@ -1121,6 +1126,25 @@ impl pallet_subtensor::Config for Runtime { type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; } +parameter_types! { + pub const RateLimitingMaxGroupMembers: u32 = 64; + pub const RateLimitingMaxGroupNameLength: u32 = 64; +} + +impl pallet_rate_limiting::Config for Runtime { + type RuntimeCall = RuntimeCall; + type AdminOrigin = EnsureRoot; + type LimitScope = RateLimitScope; + type LimitScopeResolver = RuntimeScopeResolver; + type UsageKey = RateLimitUsageKey; + type UsageResolver = RuntimeUsageResolver; + type GroupId = u32; + type MaxGroupMembers = RateLimitingMaxGroupMembers; + type MaxGroupNameLength = RateLimitingMaxGroupNameLength; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const SwapMaxFeeRate: u16 = 10000; // 15.26% @@ -1574,6 +1598,7 @@ construct_runtime!( Crowdloan: pallet_crowdloan = 27, Swap: pallet_subtensor_swap = 28, Contracts: pallet_contracts = 29, + RateLimiting: pallet_rate_limiting = 30, } ); @@ -1592,6 +1617,7 @@ pub type TransactionExtensions = ( frame_system::CheckEra, check_nonce::CheckNonce, frame_system::CheckWeight, + pallet_rate_limiting::RateLimitTransactionExtension, ChargeTransactionPaymentWrapper, pallet_subtensor::transaction_extension::SubtensorTransactionExtension, pallet_drand::drand_priority::DrandPriority, @@ -1610,6 +1636,7 @@ type Migrations = ( pallet_subtensor::migrations::migrate_init_total_issuance::initialise_total_issuance::Migration< Runtime, >, + rate_limiting::migration::Migration, // Remove storage from removed governance pallets frame_support::migrations::RemovePallet, frame_support::migrations::RemovePallet, @@ -2162,6 +2189,50 @@ impl_runtime_apis! { } } + impl pallet_rate_limiting_runtime_api::RateLimitingRuntimeApi for Runtime { + fn get_rate_limit( + pallet: Vec, + extrinsic: Vec, + ) -> Option { + use pallet_rate_limiting::{Pallet as RateLimiting, RateLimit}; + use pallet_rate_limiting_runtime_api::RateLimitRpcResponse; + + let pallet_name = sp_std::str::from_utf8(&pallet).ok()?; + let extrinsic_name = sp_std::str::from_utf8(&extrinsic).ok()?; + + let identifier = RateLimiting::::identifier_for_call_names( + pallet_name, + extrinsic_name, + )?; + let target = + RateLimiting::::config_target(&identifier).ok()?; + let limits = + pallet_rate_limiting::Limits::::get(target)?; + let default_limit = + pallet_rate_limiting::DefaultLimit::::get(); + let resolved = + RateLimiting::::resolved_limit(&target, &None); + + let (global, contextual) = match limits { + RateLimit::Global(kind) => (Some(kind), sp_std::vec::Vec::new()), + RateLimit::Scoped(entries) => ( + None, + entries + .into_iter() + .map(|(scope, kind)| (scope.encode(), kind)) + .collect(), + ), + }; + + Some(RateLimitRpcResponse { + global, + contextual, + default_limit, + resolved, + }) + } + } + impl pallet_contracts::ContractsApi for Runtime { diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index c4217b6dd4..57bfde1319 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -1,9 +1,10 @@ -use core::convert::TryFrom; +use core::{convert::TryFrom, marker::PhantomData}; -use codec::Encode; -use frame_support::{pallet_prelude::Parameter, traits::Get, weights::Weight}; +use frame_support::{ + BoundedBTreeSet, BoundedVec, pallet_prelude::Parameter, traits::Get, weights::Weight, +}; use frame_system::pallet_prelude::BlockNumberFor; -use log::info; +use log::{info, warn}; use pallet_rate_limiting::{ GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitTarget, TransactionIdentifier, }; @@ -15,10 +16,6 @@ use pallet_subtensor::{ TxChildkeyTakeRateLimit, TxDelegateTakeRateLimit, TxRateLimit, WeightsVersionKeyRateLimit, utils::rate_limiting::{Hyperparameter, TransactionType}, }; -use sp_io::{ - hashing::{blake2_128, twox_128}, - storage, -}; use sp_runtime::traits::SaturatedConversion; use sp_std::{ collections::{btree_map::BTreeMap, btree_set::BTreeSet}, @@ -27,15 +24,26 @@ use sp_std::{ }; use subtensor_runtime_common::{MechId, NetUid, RateLimitScope, RateLimitUsageKey}; -type RateLimitConfigOf = RateLimit>; -type RateLimitTargetOf = RateLimitTarget; -type RateLimitGroupOf = RateLimitGroup>; -type LimitEntries = Vec<(RateLimitTargetOf, RateLimitConfigOf)>; -type LastSeenKey = ( - RateLimitTargetOf, - Option::AccountId>>, -); -type LastSeenEntries = Vec<(LastSeenKey, BlockNumberFor)>; +use crate::RateLimitingInstance; + +type GroupIdOf = >::GroupId; +type LimitEntries = Vec<( + RateLimitTarget, + RateLimit>, +)>; +type LastSeenEntries = Vec<( + ( + RateLimitTarget, + Option::AccountId>>, + ), + BlockNumberFor, +)>; +type GroupNameOf = + BoundedVec>::MaxGroupNameLength>; +type GroupMembersOf = BoundedBTreeSet< + TransactionIdentifier, + >::MaxGroupMembers, +>; /// Pallet index assigned to `pallet_subtensor` in `construct_runtime!`. const SUBTENSOR_PALLET_INDEX: u8 = 7; @@ -162,7 +170,7 @@ struct GroupInfo { struct Grouping { assignments: BTreeMap, members: BTreeMap>, - details: Vec, + details: Vec>>, next_group_id: GroupId, max_group_id: Option, } @@ -198,7 +206,7 @@ impl Grouping { self.next_group_id = self.max_group_id.map_or(0, |id| id.saturating_add(1)); } - fn config_target(&self, identifier: TransactionIdentifier) -> RateLimitTargetOf { + fn config_target(&self, identifier: TransactionIdentifier) -> RateLimitTarget { if let Some(info) = self.assignments.get(&identifier) { if info.sharing.config_uses_group() { return RateLimitTarget::Group(info.id); @@ -207,7 +215,7 @@ impl Grouping { RateLimitTarget::Transaction(identifier) } - fn usage_target(&self, identifier: TransactionIdentifier) -> RateLimitTargetOf { + fn usage_target(&self, identifier: TransactionIdentifier) -> RateLimitTarget { if let Some(info) = self.assignments.get(&identifier) { if info.sharing.usage_uses_group() { return RateLimitTarget::Group(info.id); @@ -258,7 +266,17 @@ fn build_grouping() -> Grouping { grouping } -pub fn migrate_rate_limiting() -> Weight { +pub fn migrate_rate_limiting() -> Weight +where + T: SubtensorConfig + + pallet_rate_limiting::Config< + RateLimitingInstance, + LimitScope = RateLimitScope, + GroupId = GroupId, + >, + RateLimitUsageKey: + Into<>::UsageKey>, +{ let mut weight = T::DbWeight::get().reads(1); if HasMigrationRun::::get(MIGRATION_NAME) { info!("Rate-limiting migration already executed. Skipping."); @@ -731,67 +749,120 @@ fn import_evm_entries( reads } -/// TODO(rate-limiting-storage): Swap these manual writes for -/// `pallet_rate_limiting::Pallet` APIs once the runtime wires the pallet in. -fn write_limits(limits: &LimitEntries) -> u64 { - if limits.is_empty() { - return 0; +fn convert_target(target: &RateLimitTarget) -> RateLimitTarget> +where + T: SubtensorConfig + + pallet_rate_limiting::Config< + RateLimitingInstance, + LimitScope = RateLimitScope, + GroupId = GroupId, + >, + RateLimitUsageKey: + Into<>::UsageKey>, +{ + match target { + RateLimitTarget::Transaction(identifier) => RateLimitTarget::Transaction(*identifier), + RateLimitTarget::Group(id) => RateLimitTarget::Group((*id).saturated_into()), } - let limits_prefix = storage_prefix("RateLimiting", "Limits"); - let mut writes = 0; +} + +fn write_limits(limits: &LimitEntries) -> u64 +where + T: SubtensorConfig + + pallet_rate_limiting::Config< + RateLimitingInstance, + LimitScope = RateLimitScope, + GroupId = GroupId, + >, + RateLimitUsageKey: + Into<>::UsageKey>, +{ + let mut writes: u64 = 0; for (identifier, limit) in limits.iter() { - let limit_key = map_storage_key(&limits_prefix, identifier); - storage::set(&limit_key, &limit.encode()); + let target = convert_target::(identifier); + pallet_rate_limiting::Limits::::insert(target, limit.clone()); writes += 1; } writes } -fn write_last_seen(entries: &LastSeenEntries) -> u64 { - if entries.is_empty() { - return 0; - } - let prefix = storage_prefix("RateLimiting", "LastSeen"); - let mut writes = 0; +fn write_last_seen(entries: &LastSeenEntries) -> u64 +where + T: SubtensorConfig + + pallet_rate_limiting::Config< + RateLimitingInstance, + LimitScope = RateLimitScope, + GroupId = GroupId, + >, + RateLimitUsageKey: + Into<>::UsageKey>, +{ + let mut writes: u64 = 0; for ((identifier, usage), block) in entries.iter() { - let key = double_map_storage_key(&prefix, identifier, usage); - storage::set(&key, &block.encode()); + let target = convert_target::(identifier); + let usage_key = usage.clone().map(Into::into); + pallet_rate_limiting::LastSeen::::insert( + target, usage_key, *block, + ); writes += 1; } writes } -fn write_groups(grouping: &Grouping) -> u64 { - let mut writes = 0; - let groups_prefix = storage_prefix("RateLimiting", "Groups"); - let members_prefix = storage_prefix("RateLimiting", "GroupMembers"); - let name_index_prefix = storage_prefix("RateLimiting", "GroupNameIndex"); - let call_groups_prefix = storage_prefix("RateLimiting", "CallGroups"); - let next_group_id_prefix = storage_prefix("RateLimiting", "NextGroupId"); +fn write_groups(grouping: &Grouping) -> u64 +where + T: SubtensorConfig + + pallet_rate_limiting::Config< + RateLimitingInstance, + LimitScope = RateLimitScope, + GroupId = GroupId, + >, + RateLimitUsageKey: + Into<>::UsageKey>, +{ + let mut writes: u64 = 0; for detail in &grouping.details { - let group_key = map_storage_key(&groups_prefix, detail.id); - storage::set(&group_key, &detail.encode()); - writes += 1; + let Ok(name) = GroupNameOf::::try_from(detail.name.clone()) else { + warn!( + "rate-limiting migration: group name exceeds bounds, skipping id {}", + detail.id + ); + continue; + }; + let group_id = detail.id.saturated_into::>(); + let stored = RateLimitGroup { + id: group_id, + name: name.clone(), + sharing: detail.sharing, + }; - let name_key = map_storage_key(&name_index_prefix, detail.name.clone()); - storage::set(&name_key, &detail.id.encode()); - writes += 1; + pallet_rate_limiting::Groups::::insert(group_id, stored); + pallet_rate_limiting::GroupNameIndex::::insert(name, group_id); + writes += 2; } for (group, members) in &grouping.members { - let members_key = map_storage_key(&members_prefix, *group); - storage::set(&members_key, &members.encode()); + let group_id = (*group).saturated_into::>(); + let Ok(bounded) = GroupMembersOf::::try_from(members.clone()) else { + warn!( + "rate-limiting migration: group {} has too many members, skipping assignment", + group + ); + continue; + }; + pallet_rate_limiting::GroupMembers::::insert(group_id, bounded); writes += 1; } for (identifier, info) in &grouping.assignments { - let call_key = map_storage_key(&call_groups_prefix, *identifier); - storage::set(&call_key, &info.id.encode()); + let group_id = info.id.saturated_into::>(); + pallet_rate_limiting::CallGroups::::insert(*identifier, group_id); writes += 1; } - storage::set(&next_group_id_prefix, &grouping.next_group_id.encode()); + let next_group_id = grouping.next_group_id.saturated_into::>(); + pallet_rate_limiting::NextGroupId::::put(next_group_id); writes += 1; writes @@ -806,7 +877,7 @@ fn block_number(value: u64) -> Option> { fn set_global_limit( limits: &mut LimitEntries, - target: RateLimitTargetOf, + target: RateLimitTarget, span: BlockNumberFor, ) { if let Some((_, config)) = limits.iter_mut().find(|(id, _)| *id == target) { @@ -818,7 +889,7 @@ fn set_global_limit( fn set_scoped_limit( limits: &mut LimitEntries, - target: RateLimitTargetOf, + target: RateLimitTarget, scope: RateLimitScope, span: BlockNumberFor, ) { @@ -841,7 +912,7 @@ fn set_scoped_limit( fn record_last_seen_entry( entries: &mut LastSeenEntries, - target: RateLimitTargetOf, + target: RateLimitTarget, usage: Option>, block: u64, ) { @@ -859,31 +930,23 @@ fn record_last_seen_entry( } } -fn storage_prefix(pallet: &str, storage: &str) -> Vec { - let mut out = Vec::with_capacity(32); - out.extend_from_slice(&twox_128(pallet.as_bytes())); - out.extend_from_slice(&twox_128(storage.as_bytes())); - out -} +/// Runtime hook that executes the rate-limiting migration. +pub struct Migration(PhantomData); -fn map_storage_key(prefix: &[u8], key: impl Encode) -> Vec { - let mut final_key = Vec::with_capacity(prefix.len() + 32); - final_key.extend_from_slice(prefix); - let encoded = key.encode(); - let hash = blake2_128(&encoded); - final_key.extend_from_slice(&hash); - final_key.extend_from_slice(&encoded); - final_key -} - -fn double_map_storage_key(prefix: &[u8], key1: impl Encode, key2: impl Encode) -> Vec { - let mut final_key = Vec::with_capacity(prefix.len() + 64); - final_key.extend_from_slice(prefix); - let first = map_storage_key(&[], key1); - final_key.extend_from_slice(&first); - let second = map_storage_key(&[], key2); - final_key.extend_from_slice(&second); - final_key +impl frame_support::traits::OnRuntimeUpgrade for Migration +where + T: SubtensorConfig + + pallet_rate_limiting::Config< + RateLimitingInstance, + LimitScope = RateLimitScope, + GroupId = GroupId, + >, + RateLimitUsageKey: + Into<>::UsageKey>, +{ + fn on_runtime_upgrade() -> Weight { + migrate_rate_limiting::() + } } const fn admin_utils_identifier(call_index: u8) -> TransactionIdentifier { @@ -955,25 +1018,6 @@ pub fn identifier_for_transaction_type(tx: TransactionType) -> Option( - key: &RateLimitKey, -) -> Option> -where - AccountId: Parameter + Clone, -{ - match key { - RateLimitKey::SetSNOwnerHotkey(netuid) => Some(RateLimitUsageKey::Subnet(*netuid)), - RateLimitKey::OwnerHyperparamUpdate(netuid, _) => Some(RateLimitUsageKey::Subnet(*netuid)), - RateLimitKey::NetworkLastRegistered => None, - RateLimitKey::LastTxBlock(account) - | RateLimitKey::LastTxBlockChildKeyTake(account) - | RateLimitKey::LastTxBlockDelegateTake(account) => { - Some(RateLimitUsageKey::Account(account.clone())) - } - } -} - /// Produces the usage key for a `TransactionType` that was stored in `TransactionKeyLastBlock`. pub fn usage_key_from_transaction_type( tx: TransactionType, @@ -1008,6 +1052,23 @@ where #[cfg(test)] mod tests { use super::*; + use crate::{AccountId, BuildStorage, RateLimitingInstance, Runtime}; + use sp_io::TestExternalities; + use sp_runtime::traits::{SaturatedConversion, Zero}; + use subtensor_runtime_common::RateLimitUsageKey; + + const ACCOUNT: [u8; 32] = [7u8; 32]; + const DELEGATE_TAKE_GROUP_ID: GroupId = GROUP_DELEGATE_TAKE; + + fn new_test_ext() -> TestExternalities { + sp_tracing::try_init_simple(); + let mut ext: TestExternalities = crate::RuntimeGenesisConfig::default() + .build_storage() + .expect("runtime storage") + .into(); + ext.execute_with(|| crate::System::set_block_number(1)); + ext + } #[test] fn maps_hyperparameters() { @@ -1028,11 +1089,88 @@ mod tests { } #[test] - fn maps_usage_keys() { - let acct = 42u64; - assert!(matches!( - usage_key_from_legacy_key(&RateLimitKey::LastTxBlock(acct)), - Some(RateLimitUsageKey::Account(42)) - )); + fn migration_populates_limits_last_seen_and_groups() { + new_test_ext().execute_with(|| { + let account: AccountId = ACCOUNT.into(); + pallet_subtensor::HasMigrationRun::::remove(MIGRATION_NAME); + + pallet_subtensor::TxRateLimit::::put(10); + pallet_subtensor::TxDelegateTakeRateLimit::::put(3); + pallet_subtensor::LastRateLimitedBlock::::insert( + RateLimitKey::LastTxBlock(account.clone()), + 5, + ); + + let weight = migrate_rate_limiting::(); + assert!(!weight.is_zero()); + assert!(pallet_subtensor::HasMigrationRun::::get( + MIGRATION_NAME + )); + + let tx_target = RateLimitTarget::Transaction(subtensor_identifier(70)); + let delegate_group = RateLimitTarget::Group(DELEGATE_TAKE_GROUP_ID); + + assert_eq!( + pallet_rate_limiting::Limits::::get(tx_target), + Some(RateLimit::Global(RateLimitKind::Exact( + 10u64.saturated_into() + ))) + ); + assert_eq!( + pallet_rate_limiting::Limits::::get(delegate_group), + Some(RateLimit::Global(RateLimitKind::Exact( + 3u64.saturated_into() + ))) + ); + + let usage_key = RateLimitUsageKey::Account(account.clone()); + assert_eq!( + pallet_rate_limiting::LastSeen::::get( + tx_target, + Some(usage_key.clone()) + ), + Some(5u64.saturated_into()) + ); + + let group = pallet_rate_limiting::Groups::::get( + DELEGATE_TAKE_GROUP_ID, + ) + .expect("group stored"); + assert_eq!(group.id, DELEGATE_TAKE_GROUP_ID); + assert_eq!(group.name.as_slice(), b"delegate-take"); + assert_eq!( + pallet_rate_limiting::CallGroups::::get( + subtensor_identifier(66) + ), + Some(DELEGATE_TAKE_GROUP_ID) + ); + assert_eq!( + pallet_rate_limiting::NextGroupId::::get(), + 6 + ); + }); + } + + #[test] + fn migration_skips_when_already_run() { + new_test_ext().execute_with(|| { + pallet_subtensor::HasMigrationRun::::insert(MIGRATION_NAME, true); + pallet_subtensor::TxRateLimit::::put(99); + + let base_weight = ::DbWeight::get().reads(1); + let weight = migrate_rate_limiting::(); + + assert_eq!(weight, base_weight); + assert!( + pallet_rate_limiting::Limits::::iter() + .next() + .is_none() + ); + assert!( + pallet_rate_limiting::LastSeen::::iter() + .next() + .is_none() + ); + }); } } From f7050e14d14ef9e928843a8e5c1300fbe50c228f Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 25 Nov 2025 12:46:08 +0300 Subject: [PATCH 27/95] Clean up --- Cargo.lock | 1 + common/src/lib.rs | 2 - common/src/rate_limiting.rs | 66 -------------- runtime/Cargo.toml | 2 + runtime/src/lib.rs | 43 ++++----- runtime/src/rate_limiting/migration.rs | 119 ++++++++----------------- runtime/src/rate_limiting/mod.rs | 75 ++++++++++++++-- 7 files changed, 128 insertions(+), 180 deletions(-) delete mode 100644 common/src/rate_limiting.rs diff --git a/Cargo.lock b/Cargo.lock index c714894796..4a9a31b68c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8321,6 +8321,7 @@ dependencies = [ "precompile-utils", "rand_chacha 0.3.1", "scale-info", + "serde", "serde_json", "sha2 0.10.9", "smallvec", diff --git a/common/src/lib.rs b/common/src/lib.rs index b08bed0696..28a33c2ae6 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -15,10 +15,8 @@ use sp_runtime::{ use subtensor_macros::freeze_struct; pub use currency::*; -pub use rate_limiting::{RateLimitScope, RateLimitUsageKey}; mod currency; -mod rate_limiting; /// Balance of an account. pub type Balance = u64; diff --git a/common/src/rate_limiting.rs b/common/src/rate_limiting.rs deleted file mode 100644 index 3c88758943..0000000000 --- a/common/src/rate_limiting.rs +++ /dev/null @@ -1,66 +0,0 @@ -use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use frame_support::pallet_prelude::Parameter; -use scale_info::TypeInfo; -use serde::{Deserialize, Serialize}; - -use crate::{MechId, NetUid}; - -#[derive( - Serialize, - Deserialize, - Encode, - Decode, - DecodeWithMemTracking, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Debug, - TypeInfo, - MaxEncodedLen, -)] -pub enum RateLimitScope { - Subnet(NetUid), - SubnetMechanism { netuid: NetUid, mecid: MechId }, -} - -#[derive( - Serialize, - Deserialize, - Encode, - Decode, - DecodeWithMemTracking, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Debug, - TypeInfo, - MaxEncodedLen, -)] -#[scale_info(skip_type_params(AccountId))] -pub enum RateLimitUsageKey { - Account(AccountId), - Subnet(NetUid), - AccountSubnet { - account: AccountId, - netuid: NetUid, - }, - ColdkeyHotkeySubnet { - coldkey: AccountId, - hotkey: AccountId, - netuid: NetUid, - }, - SubnetNeuron { - netuid: NetUid, - uid: u16, - }, - SubnetMechanismNeuron { - netuid: NetUid, - mecid: MechId, - uid: u16, - }, -} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index e3b926e859..aaede7537e 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -43,6 +43,7 @@ pallet-rate-limiting-runtime-api.workspace = true pallet-subtensor-utility.workspace = true frame-executive.workspace = true frame-metadata-hash-extension.workspace = true +serde.workspace = true sp-api.workspace = true sp-block-builder.workspace = true sp-consensus-aura.workspace = true @@ -199,6 +200,7 @@ std = [ "pallet-preimage/std", "pallet-commitments/std", "precompile-utils/std", + "serde/std", "sp-api/std", "sp-block-builder/std", "sp-core/std", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7a7431a7da..99a10e8198 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -8,6 +8,7 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +use core::marker::PhantomData; use core::num::NonZeroU64; pub mod check_nonce; @@ -24,9 +25,7 @@ use frame_support::{ dispatch::DispatchResult, genesis_builder_helper::{build_state, get_preset}, pallet_prelude::Get, - traits::{ - Contains, GetCallMetadata, InsideBoth, LinearStoragePrice, fungible::HoldConsideration, - }, + traits::{Contains, InsideBoth, LinearStoragePrice, fungible::HoldConsideration}, }; use frame_system::{EnsureRoot, EnsureRootWithSuccess, EnsureSigned}; use pallet_commitments::{CanCommit, OnMetadataCommitment}; @@ -46,6 +45,7 @@ use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_swap_runtime_api::SimSwapResult; use pallet_subtensor_utility as pallet_utility; use runtime_common::prod_or_fast; +use scale_info::TypeInfo; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_babe::BabeConfiguration; @@ -72,12 +72,13 @@ use sp_version::RuntimeVersion; use subtensor_precompiles::Precompiles; use subtensor_runtime_common::{AlphaCurrency, TaoCurrency, time::*, *}; use subtensor_swap_interface::{Order, SwapHandler}; - -pub use rate_limiting::{ - ScopeResolver as RuntimeScopeResolver, UsageResolver as RuntimeUsageResolver, +use subtensor_transaction_fee::{SubtensorTxFeeHandler, TransactionFeeHandler}; +// Frontier +use fp_rpc::TransactionStatus; +use pallet_ethereum::{Call::transact, PostLogContent, Transaction as EthereumTransaction}; +use pallet_evm::{ + Account as EVMAccount, BalanceConverter, EvmBalance, FeeCalculator, Runner, SubstrateBalance, }; -pub type RateLimitingInstance = (); -pub type RateLimitGroupId = u32; // A few exports that help ease life for downstream crates. pub use frame_support::{ @@ -99,21 +100,13 @@ pub use pallet_balances::Call as BalancesCall; use pallet_commitments::GetCommitments; pub use pallet_timestamp::Call as TimestampCall; use pallet_transaction_payment::{ConstFeeMultiplier, Multiplier}; +pub use rate_limiting::{ + RateLimitScope, RateLimitUsageKey, ScopeResolver as RuntimeScopeResolver, + UsageResolver as RuntimeUsageResolver, +}; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; pub use sp_runtime::{Perbill, Permill}; -use subtensor_transaction_fee::{SubtensorTxFeeHandler, TransactionFeeHandler}; - -use core::marker::PhantomData; - -use scale_info::TypeInfo; - -// Frontier -use fp_rpc::TransactionStatus; -use pallet_ethereum::{Call::transact, PostLogContent, Transaction as EthereumTransaction}; -use pallet_evm::{ - Account as EVMAccount, BalanceConverter, EvmBalance, FeeCalculator, Runner, SubstrateBalance, -}; // Drand impl pallet_drand::Config for Runtime { @@ -2200,18 +2193,18 @@ impl_runtime_apis! { let pallet_name = sp_std::str::from_utf8(&pallet).ok()?; let extrinsic_name = sp_std::str::from_utf8(&extrinsic).ok()?; - let identifier = RateLimiting::::identifier_for_call_names( + let identifier = RateLimiting::::identifier_for_call_names( pallet_name, extrinsic_name, )?; let target = - RateLimiting::::config_target(&identifier).ok()?; + RateLimiting::::config_target(&identifier).ok()?; let limits = - pallet_rate_limiting::Limits::::get(target)?; + pallet_rate_limiting::Limits::::get(target)?; let default_limit = - pallet_rate_limiting::DefaultLimit::::get(); + pallet_rate_limiting::DefaultLimit::::get(); let resolved = - RateLimiting::::resolved_limit(&target, &None); + RateLimiting::::resolved_limit(&target, &None); let (global, contextual) = match limits { RateLimit::Global(kind) => (Some(kind), sp_std::vec::Vec::new()), diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index 57bfde1319..549e847606 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -22,11 +22,11 @@ use sp_std::{ vec, vec::Vec, }; -use subtensor_runtime_common::{MechId, NetUid, RateLimitScope, RateLimitUsageKey}; +use subtensor_runtime_common::{MechId, NetUid}; -use crate::RateLimitingInstance; +use super::{RateLimitScope, RateLimitUsageKey}; -type GroupIdOf = >::GroupId; +type GroupIdOf = ::GroupId; type LimitEntries = Vec<( RateLimitTarget, RateLimit>, @@ -38,12 +38,9 @@ type LastSeenEntries = Vec<( ), BlockNumberFor, )>; -type GroupNameOf = - BoundedVec>::MaxGroupNameLength>; -type GroupMembersOf = BoundedBTreeSet< - TransactionIdentifier, - >::MaxGroupMembers, ->; +type GroupNameOf = BoundedVec::MaxGroupNameLength>; +type GroupMembersOf = + BoundedBTreeSet::MaxGroupMembers>; /// Pallet index assigned to `pallet_subtensor` in `construct_runtime!`. const SUBTENSOR_PALLET_INDEX: u8 = 7; @@ -269,13 +266,8 @@ fn build_grouping() -> Grouping { pub fn migrate_rate_limiting() -> Weight where T: SubtensorConfig - + pallet_rate_limiting::Config< - RateLimitingInstance, - LimitScope = RateLimitScope, - GroupId = GroupId, - >, - RateLimitUsageKey: - Into<>::UsageKey>, + + pallet_rate_limiting::Config, + RateLimitUsageKey: Into<::UsageKey>, { let mut weight = T::DbWeight::get().reads(1); if HasMigrationRun::::get(MIGRATION_NAME) { @@ -752,13 +744,8 @@ fn import_evm_entries( fn convert_target(target: &RateLimitTarget) -> RateLimitTarget> where T: SubtensorConfig - + pallet_rate_limiting::Config< - RateLimitingInstance, - LimitScope = RateLimitScope, - GroupId = GroupId, - >, - RateLimitUsageKey: - Into<>::UsageKey>, + + pallet_rate_limiting::Config, + RateLimitUsageKey: Into<::UsageKey>, { match target { RateLimitTarget::Transaction(identifier) => RateLimitTarget::Transaction(*identifier), @@ -769,18 +756,13 @@ where fn write_limits(limits: &LimitEntries) -> u64 where T: SubtensorConfig - + pallet_rate_limiting::Config< - RateLimitingInstance, - LimitScope = RateLimitScope, - GroupId = GroupId, - >, - RateLimitUsageKey: - Into<>::UsageKey>, + + pallet_rate_limiting::Config, + RateLimitUsageKey: Into<::UsageKey>, { let mut writes: u64 = 0; for (identifier, limit) in limits.iter() { let target = convert_target::(identifier); - pallet_rate_limiting::Limits::::insert(target, limit.clone()); + pallet_rate_limiting::Limits::::insert(target, limit.clone()); writes += 1; } writes @@ -789,21 +771,14 @@ where fn write_last_seen(entries: &LastSeenEntries) -> u64 where T: SubtensorConfig - + pallet_rate_limiting::Config< - RateLimitingInstance, - LimitScope = RateLimitScope, - GroupId = GroupId, - >, - RateLimitUsageKey: - Into<>::UsageKey>, + + pallet_rate_limiting::Config, + RateLimitUsageKey: Into<::UsageKey>, { let mut writes: u64 = 0; for ((identifier, usage), block) in entries.iter() { let target = convert_target::(identifier); let usage_key = usage.clone().map(Into::into); - pallet_rate_limiting::LastSeen::::insert( - target, usage_key, *block, - ); + pallet_rate_limiting::LastSeen::::insert(target, usage_key, *block); writes += 1; } writes @@ -812,13 +787,8 @@ where fn write_groups(grouping: &Grouping) -> u64 where T: SubtensorConfig - + pallet_rate_limiting::Config< - RateLimitingInstance, - LimitScope = RateLimitScope, - GroupId = GroupId, - >, - RateLimitUsageKey: - Into<>::UsageKey>, + + pallet_rate_limiting::Config, + RateLimitUsageKey: Into<::UsageKey>, { let mut writes: u64 = 0; @@ -837,8 +807,8 @@ where sharing: detail.sharing, }; - pallet_rate_limiting::Groups::::insert(group_id, stored); - pallet_rate_limiting::GroupNameIndex::::insert(name, group_id); + pallet_rate_limiting::Groups::::insert(group_id, stored); + pallet_rate_limiting::GroupNameIndex::::insert(name, group_id); writes += 2; } @@ -851,18 +821,18 @@ where ); continue; }; - pallet_rate_limiting::GroupMembers::::insert(group_id, bounded); + pallet_rate_limiting::GroupMembers::::insert(group_id, bounded); writes += 1; } for (identifier, info) in &grouping.assignments { let group_id = info.id.saturated_into::>(); - pallet_rate_limiting::CallGroups::::insert(*identifier, group_id); + pallet_rate_limiting::CallGroups::::insert(*identifier, group_id); writes += 1; } let next_group_id = grouping.next_group_id.saturated_into::>(); - pallet_rate_limiting::NextGroupId::::put(next_group_id); + pallet_rate_limiting::NextGroupId::::put(next_group_id); writes += 1; writes @@ -936,13 +906,8 @@ pub struct Migration(PhantomData); impl frame_support::traits::OnRuntimeUpgrade for Migration where T: SubtensorConfig - + pallet_rate_limiting::Config< - RateLimitingInstance, - LimitScope = RateLimitScope, - GroupId = GroupId, - >, - RateLimitUsageKey: - Into<>::UsageKey>, + + pallet_rate_limiting::Config, + RateLimitUsageKey: Into<::UsageKey>, { fn on_runtime_upgrade() -> Weight { migrate_rate_limiting::() @@ -1051,11 +1016,11 @@ where #[cfg(test)] mod tests { - use super::*; - use crate::{AccountId, BuildStorage, RateLimitingInstance, Runtime}; use sp_io::TestExternalities; use sp_runtime::traits::{SaturatedConversion, Zero}; - use subtensor_runtime_common::RateLimitUsageKey; + + use super::*; + use crate::{AccountId, BuildStorage, Runtime}; const ACCOUNT: [u8; 32] = [7u8; 32]; const DELEGATE_TAKE_GROUP_ID: GroupId = GROUP_DELEGATE_TAKE; @@ -1111,13 +1076,13 @@ mod tests { let delegate_group = RateLimitTarget::Group(DELEGATE_TAKE_GROUP_ID); assert_eq!( - pallet_rate_limiting::Limits::::get(tx_target), + pallet_rate_limiting::Limits::::get(tx_target), Some(RateLimit::Global(RateLimitKind::Exact( 10u64.saturated_into() ))) ); assert_eq!( - pallet_rate_limiting::Limits::::get(delegate_group), + pallet_rate_limiting::Limits::::get(delegate_group), Some(RateLimit::Global(RateLimitKind::Exact( 3u64.saturated_into() ))) @@ -1125,29 +1090,19 @@ mod tests { let usage_key = RateLimitUsageKey::Account(account.clone()); assert_eq!( - pallet_rate_limiting::LastSeen::::get( - tx_target, - Some(usage_key.clone()) - ), + pallet_rate_limiting::LastSeen::::get(tx_target, Some(usage_key.clone())), Some(5u64.saturated_into()) ); - let group = pallet_rate_limiting::Groups::::get( - DELEGATE_TAKE_GROUP_ID, - ) - .expect("group stored"); + let group = pallet_rate_limiting::Groups::::get(DELEGATE_TAKE_GROUP_ID) + .expect("group stored"); assert_eq!(group.id, DELEGATE_TAKE_GROUP_ID); assert_eq!(group.name.as_slice(), b"delegate-take"); assert_eq!( - pallet_rate_limiting::CallGroups::::get( - subtensor_identifier(66) - ), + pallet_rate_limiting::CallGroups::::get(subtensor_identifier(66)), Some(DELEGATE_TAKE_GROUP_ID) ); - assert_eq!( - pallet_rate_limiting::NextGroupId::::get(), - 6 - ); + assert_eq!(pallet_rate_limiting::NextGroupId::::get(), 6); }); } @@ -1162,12 +1117,12 @@ mod tests { assert_eq!(weight, base_weight); assert!( - pallet_rate_limiting::Limits::::iter() + pallet_rate_limiting::Limits::::iter() .next() .is_none() ); assert!( - pallet_rate_limiting::LastSeen::::iter() + pallet_rate_limiting::LastSeen::::iter() .next() .is_none() ); diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 713c8bacf6..fb8109202f 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -1,13 +1,77 @@ +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::pallet_prelude::Parameter; use frame_system::RawOrigin; use pallet_admin_utils::Call as AdminUtilsCall; use pallet_rate_limiting::{RateLimitScopeResolver, RateLimitUsageResolver}; use pallet_subtensor::{Call as SubtensorCall, Tempo}; -use subtensor_runtime_common::{BlockNumber, NetUid, RateLimitScope, RateLimitUsageKey}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use subtensor_runtime_common::{BlockNumber, MechId, NetUid}; use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; pub(crate) mod migration; +#[derive( + Serialize, + Deserialize, + Encode, + Decode, + DecodeWithMemTracking, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + TypeInfo, + MaxEncodedLen, +)] +pub enum RateLimitScope { + Subnet(NetUid), + SubnetMechanism { netuid: NetUid, mecid: MechId }, +} + +#[derive( + Serialize, + Deserialize, + Encode, + Decode, + DecodeWithMemTracking, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + TypeInfo, + MaxEncodedLen, +)] +#[scale_info(skip_type_params(AccountId))] +pub enum RateLimitUsageKey { + Account(AccountId), + Subnet(NetUid), + AccountSubnet { + account: AccountId, + netuid: NetUid, + }, + ColdkeyHotkeySubnet { + coldkey: AccountId, + hotkey: AccountId, + netuid: NetUid, + }, + SubnetNeuron { + netuid: NetUid, + uid: u16, + }, + SubnetMechanismNeuron { + netuid: NetUid, + mecid: MechId, + uid: u16, + }, +} + fn signed_origin(origin: &RuntimeOrigin) -> Option { match origin.clone().into() { Ok(RawOrigin::Signed(who)) => Some(who), @@ -53,7 +117,6 @@ fn owner_hparam_netuid(call: &AdminUtilsCall) -> Option { | AdminUtilsCall::sudo_set_recycle_or_burn { netuid, .. } | AdminUtilsCall::sudo_set_rho { netuid, .. } | AdminUtilsCall::sudo_set_serving_rate_limit { netuid, .. } - | AdminUtilsCall::sudo_set_sn_owner_hotkey { netuid, .. } | AdminUtilsCall::sudo_set_toggle_transfer { netuid, .. } | AdminUtilsCall::sudo_set_weights_version_key { netuid, .. } | AdminUtilsCall::sudo_set_yuma3_enabled { netuid, .. } => Some(*netuid), @@ -65,6 +128,7 @@ fn admin_scope_netuid(call: &AdminUtilsCall) -> Option { owner_hparam_netuid(call).or_else(|| match call { AdminUtilsCall::sudo_set_mechanism_count { netuid, .. } | AdminUtilsCall::sudo_set_mechanism_emission_split { netuid, .. } + | AdminUtilsCall::sudo_set_sn_owner_hotkey { netuid, .. } | AdminUtilsCall::sudo_trim_to_max_allowed_uids { netuid, .. } => Some(*netuid), _ => None, }) @@ -83,9 +147,7 @@ impl RateLimitUsageResolver::Account) } SubtensorCall::register_network { .. } - | SubtensorCall::register_network_with_identity { .. } => { - signed_origin(origin).map(RateLimitUsageKey::::Account) - } + | SubtensorCall::register_network_with_identity { .. } => None, SubtensorCall::increase_take { hotkey, .. } => { Some(RateLimitUsageKey::::Account(hotkey.clone())) } @@ -181,6 +243,9 @@ impl RateLimitUsageResolver::Subnet(netuid)) } else { match inner { + AdminUtilsCall::sudo_set_sn_owner_hotkey { netuid, .. } => { + Some(RateLimitUsageKey::::Subnet(*netuid)) + } AdminUtilsCall::sudo_set_mechanism_count { netuid, .. } | AdminUtilsCall::sudo_set_mechanism_emission_split { netuid, .. } | AdminUtilsCall::sudo_trim_to_max_allowed_uids { netuid, .. } => { From 42ea14e4f8f43b40cea0f2da3762736ced83bf3b Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 25 Nov 2025 17:25:07 +0300 Subject: [PATCH 28/95] Add more tests for rate-limiting migration --- runtime/src/lib.rs | 2 +- runtime/src/rate_limiting/mod.rs | 2 +- runtime/tests/rate_limiting_migration.rs | 98 ++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 runtime/tests/rate_limiting_migration.rs diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 99a10e8198..aea33bacca 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -13,7 +13,7 @@ use core::num::NonZeroU64; pub mod check_nonce; mod migrations; -mod rate_limiting; +pub mod rate_limiting; pub mod transaction_payment_wrapper; extern crate alloc; diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index fb8109202f..7b86d87dad 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -10,7 +10,7 @@ use subtensor_runtime_common::{BlockNumber, MechId, NetUid}; use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; -pub(crate) mod migration; +pub mod migration; #[derive( Serialize, diff --git a/runtime/tests/rate_limiting_migration.rs b/runtime/tests/rate_limiting_migration.rs new file mode 100644 index 0000000000..23a16e3fe4 --- /dev/null +++ b/runtime/tests/rate_limiting_migration.rs @@ -0,0 +1,98 @@ +#![allow(clippy::unwrap_used)] + +use frame_support::traits::OnRuntimeUpgrade; +use frame_system::pallet_prelude::BlockNumberFor; +use node_subtensor_runtime::{ + rate_limiting, + rate_limiting::migration::{identifier_for_transaction_type, Migration}, + BuildStorage, Runtime, RuntimeGenesisConfig, SubtensorModule, System, +}; +use pallet_rate_limiting::{RateLimit, RateLimitKind, RateLimitTarget}; +use pallet_subtensor::{HasMigrationRun, LastRateLimitedBlock, RateLimitKey, utils::rate_limiting::TransactionType}; +use sp_runtime::traits::SaturatedConversion; +use subtensor_runtime_common::NetUid; + +type GroupId = ::GroupId; +const MIGRATION_NAME: &[u8] = b"migrate_rate_limiting"; +type AccountId = ::AccountId; +type UsageKey = rate_limiting::RateLimitUsageKey; + +fn new_test_ext() -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig::default() + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +fn resolve_target(identifier: pallet_rate_limiting::TransactionIdentifier) -> RateLimitTarget { + if let Some(group) = pallet_rate_limiting::CallGroups::::get(identifier) { + RateLimitTarget::Group(group) + } else { + RateLimitTarget::Transaction(identifier) + } +} + +fn exact_span(span: u64) -> RateLimitKind> { + RateLimitKind::Exact(span.saturated_into()) +} + +#[test] +fn migrates_global_register_network_last_seen() { + new_test_ext().execute_with(|| { + HasMigrationRun::::remove(MIGRATION_NAME); + + // Seed legacy global register rate-limit state. + LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, 10u64); + System::set_block_number(12); + + // Run migration. + Migration::::on_runtime_upgrade(); + + let identifier = + identifier_for_transaction_type(TransactionType::RegisterNetwork).expect("identifier"); + let target = resolve_target(identifier); + + // LastSeen preserved globally (usage = None). + let stored = pallet_rate_limiting::LastSeen::::get(target, None::) + .expect("last seen entry"); + assert_eq!( + stored, + 10u64.saturated_into::>() + ); + }); +} + +#[test] +fn sn_owner_hotkey_limit_not_tempo_scaled_and_last_seen_preserved() { + new_test_ext().execute_with(|| { + HasMigrationRun::::remove(MIGRATION_NAME); + + let netuid = NetUid::from(1); + // Give the subnet a non-1 tempo to catch accidental scaling. + SubtensorModule::set_tempo(netuid, 5); + LastRateLimitedBlock::::insert(RateLimitKey::SetSNOwnerHotkey(netuid), 100u64); + + Migration::::on_runtime_upgrade(); + + let identifier = + identifier_for_transaction_type(TransactionType::SetSNOwnerHotkey).expect("identifier"); + let target = resolve_target(identifier); + + // Limit should remain the fixed default (50400 blocks), not tempo-scaled. + let limit = pallet_rate_limiting::Limits::::get(target).expect("limit stored"); + assert!(matches!(limit, RateLimit::Global(kind) if kind == exact_span(50_400))); + + // LastSeen preserved per subnet. + let usage: Option<::UsageKey> = + Some(UsageKey::Subnet(netuid).into()); + let stored = pallet_rate_limiting::LastSeen::::get(target, usage) + .expect("last seen entry"); + assert_eq!( + stored, + 100u64.saturated_into::>() + ); + }); +} From 662a5ed4f0353bc0492bd838e35d4fc6e0e3f47a Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 26 Nov 2025 18:25:42 +0300 Subject: [PATCH 29/95] Fix rate limit scope resolver implementation --- runtime/src/lib.rs | 5 +- runtime/src/rate_limiting/migration.rs | 109 ++---- runtime/src/rate_limiting/mod.rs | 212 +++++------- runtime/tests/rate_limiting_behavior.rs | 420 +++++++++++++++++++++++ runtime/tests/rate_limiting_migration.rs | 27 +- 5 files changed, 535 insertions(+), 238 deletions(-) create mode 100644 runtime/tests/rate_limiting_behavior.rs diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index aea33bacca..95f943731f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -101,8 +101,7 @@ use pallet_commitments::GetCommitments; pub use pallet_timestamp::Call as TimestampCall; use pallet_transaction_payment::{ConstFeeMultiplier, Multiplier}; pub use rate_limiting::{ - RateLimitScope, RateLimitUsageKey, ScopeResolver as RuntimeScopeResolver, - UsageResolver as RuntimeUsageResolver, + RateLimitUsageKey, ScopeResolver as RuntimeScopeResolver, UsageResolver as RuntimeUsageResolver, }; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; @@ -1127,7 +1126,7 @@ parameter_types! { impl pallet_rate_limiting::Config for Runtime { type RuntimeCall = RuntimeCall; type AdminOrigin = EnsureRoot; - type LimitScope = RateLimitScope; + type LimitScope = NetUid; type LimitScopeResolver = RuntimeScopeResolver; type UsageKey = RateLimitUsageKey; type UsageResolver = RuntimeUsageResolver; diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index 549e847606..54cc99d109 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -10,10 +10,10 @@ use pallet_rate_limiting::{ }; use pallet_subtensor::{ self, AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, - LastRateLimitedBlock, LastUpdate, MaxUidsTrimmingRateLimit, MechanismCountCurrent, - MechanismCountSetRateLimit, MechanismEmissionRateLimit, NetworkRateLimit, - OwnerHyperparamRateLimit, Pallet, Prometheus, RateLimitKey, TransactionKeyLastBlock, - TxChildkeyTakeRateLimit, TxDelegateTakeRateLimit, TxRateLimit, WeightsVersionKeyRateLimit, + LastRateLimitedBlock, LastUpdate, MaxUidsTrimmingRateLimit, MechanismCountSetRateLimit, + MechanismEmissionRateLimit, NetworkRateLimit, OwnerHyperparamRateLimit, Pallet, Prometheus, + RateLimitKey, TransactionKeyLastBlock, TxChildkeyTakeRateLimit, TxDelegateTakeRateLimit, + TxRateLimit, WeightsVersionKeyRateLimit, utils::rate_limiting::{Hyperparameter, TransactionType}, }; use sp_runtime::traits::SaturatedConversion; @@ -22,14 +22,14 @@ use sp_std::{ vec, vec::Vec, }; -use subtensor_runtime_common::{MechId, NetUid}; +use subtensor_runtime_common::NetUid; -use super::{RateLimitScope, RateLimitUsageKey}; +use super::RateLimitUsageKey; type GroupIdOf = ::GroupId; type LimitEntries = Vec<( RateLimitTarget, - RateLimit>, + RateLimit>, )>; type LastSeenEntries = Vec<( ( @@ -240,13 +240,6 @@ fn weight_calls_subnet(grouping: &Grouping) -> Vec { .unwrap_or_default() } -fn weight_calls_mechanism(grouping: &Grouping) -> Vec { - grouping - .members(GROUP_WEIGHTS_MECHANISM) - .map(|m| m.iter().copied().collect()) - .unwrap_or_default() -} - fn build_grouping() -> Grouping { let mut grouping = Grouping::default(); @@ -265,8 +258,7 @@ fn build_grouping() -> Grouping { pub fn migrate_rate_limiting() -> Weight where - T: SubtensorConfig - + pallet_rate_limiting::Config, + T: SubtensorConfig + pallet_rate_limiting::Config, RateLimitUsageKey: Into<::UsageKey>, { let mut weight = T::DbWeight::get().reads(1); @@ -449,12 +441,7 @@ fn gather_serving_limits( reads += 1; if let Some(span) = block_number::(Pallet::::get_serving_rate_limit(netuid)) { for call in serve_calls(grouping) { - set_scoped_limit::( - limits, - grouping.config_target(call), - RateLimitScope::Subnet(netuid), - span, - ); + set_scoped_limit::(limits, grouping.config_target(call), netuid, span); } } } @@ -469,40 +456,12 @@ fn gather_weight_limits( let mut reads: u64 = 0; let netuids = Pallet::::get_all_subnet_netuids(); - let mut subnet_limits = BTreeMap::>::new(); let subnet_calls = weight_calls_subnet(grouping); - let mechanism_calls = weight_calls_mechanism(grouping); for netuid in &netuids { reads += 1; if let Some(span) = block_number::(Pallet::::get_weights_set_rate_limit(*netuid)) { - subnet_limits.insert(*netuid, span); for call in &subnet_calls { - set_scoped_limit::( - limits, - grouping.config_target(*call), - RateLimitScope::Subnet(*netuid), - span, - ); - } - } - } - - for netuid in &netuids { - reads += 1; - let mech_count: u8 = MechanismCountCurrent::::get(*netuid).into(); - if mech_count <= 1 { - continue; - } - let Some(span) = subnet_limits.get(netuid).copied() else { - continue; - }; - for mecid in 1..mech_count { - let scope = RateLimitScope::SubnetMechanism { - netuid: *netuid, - mecid: MechId::from(mecid), - }; - for call in &mechanism_calls { - set_scoped_limit::(limits, grouping.config_target(*call), scope.clone(), span); + set_scoped_limit::(limits, grouping.config_target(*call), *netuid, span); } } } @@ -621,18 +580,9 @@ fn import_last_update_entries( ) -> u64 { let mut reads: u64 = 0; let subnet_calls = weight_calls_subnet(grouping); - let mechanism_calls = weight_calls_mechanism(grouping); for (index, blocks) in LastUpdate::::iter() { reads += 1; let netuid = Pallet::::get_netuid(index); - let sub_id = u16::from(index) - .checked_div(pallet_subtensor::subnets::mechanism::GLOBAL_MAX_SUBNET_COUNT) - .unwrap_or_default(); - let is_mechanism = sub_id != 0; - let Ok(sub_id) = u8::try_from(sub_id) else { - continue; - }; - let mecid = MechId::from(sub_id); for (uid, last_block) in blocks.into_iter().enumerate() { if last_block == 0 { @@ -641,26 +591,12 @@ fn import_last_update_entries( let Ok(uid_u16) = u16::try_from(uid) else { continue; }; - let usage = if is_mechanism { - RateLimitUsageKey::SubnetMechanismNeuron { - netuid, - mecid, - uid: uid_u16, - } - } else { - RateLimitUsageKey::SubnetNeuron { - netuid, - uid: uid_u16, - } - }; - - let call_set: &[TransactionIdentifier] = if is_mechanism { - mechanism_calls.as_slice() - } else { - subnet_calls.as_slice() + let usage = RateLimitUsageKey::SubnetNeuron { + netuid, + uid: uid_u16, }; - for call in call_set { + for call in &subnet_calls { record_last_seen_entry::( entries, grouping.usage_target(*call), @@ -743,8 +679,7 @@ fn import_evm_entries( fn convert_target(target: &RateLimitTarget) -> RateLimitTarget> where - T: SubtensorConfig - + pallet_rate_limiting::Config, + T: SubtensorConfig + pallet_rate_limiting::Config, RateLimitUsageKey: Into<::UsageKey>, { match target { @@ -755,8 +690,7 @@ where fn write_limits(limits: &LimitEntries) -> u64 where - T: SubtensorConfig - + pallet_rate_limiting::Config, + T: SubtensorConfig + pallet_rate_limiting::Config, RateLimitUsageKey: Into<::UsageKey>, { let mut writes: u64 = 0; @@ -770,8 +704,7 @@ where fn write_last_seen(entries: &LastSeenEntries) -> u64 where - T: SubtensorConfig - + pallet_rate_limiting::Config, + T: SubtensorConfig + pallet_rate_limiting::Config, RateLimitUsageKey: Into<::UsageKey>, { let mut writes: u64 = 0; @@ -786,8 +719,7 @@ where fn write_groups(grouping: &Grouping) -> u64 where - T: SubtensorConfig - + pallet_rate_limiting::Config, + T: SubtensorConfig + pallet_rate_limiting::Config, RateLimitUsageKey: Into<::UsageKey>, { let mut writes: u64 = 0; @@ -860,7 +792,7 @@ fn set_global_limit( fn set_scoped_limit( limits: &mut LimitEntries, target: RateLimitTarget, - scope: RateLimitScope, + scope: NetUid, span: BlockNumberFor, ) { if let Some((_, config)) = limits.iter_mut().find(|(id, _)| *id == target) { @@ -905,8 +837,7 @@ pub struct Migration(PhantomData); impl frame_support::traits::OnRuntimeUpgrade for Migration where - T: SubtensorConfig - + pallet_rate_limiting::Config, + T: SubtensorConfig + pallet_rate_limiting::Config, RateLimitUsageKey: Into<::UsageKey>, { fn on_runtime_upgrade() -> Weight { diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 7b86d87dad..5b4a97248a 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -12,27 +12,6 @@ use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; pub mod migration; -#[derive( - Serialize, - Deserialize, - Encode, - Decode, - DecodeWithMemTracking, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Debug, - TypeInfo, - MaxEncodedLen, -)] -pub enum RateLimitScope { - Subnet(NetUid), - SubnetMechanism { netuid: NetUid, mecid: MechId }, -} - #[derive( Serialize, Deserialize, @@ -72,66 +51,54 @@ pub enum RateLimitUsageKey { }, } -fn signed_origin(origin: &RuntimeOrigin) -> Option { - match origin.clone().into() { - Ok(RawOrigin::Signed(who)) => Some(who), - _ => None, - } -} +#[derive(Default)] +pub struct ScopeResolver; -fn tempo_scaled(netuid: NetUid, span: BlockNumber) -> BlockNumber { - if span == 0 { - return span; +impl RateLimitScopeResolver for ScopeResolver { + fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option { + match call { + RuntimeCall::SubtensorModule(inner) => match inner { + SubtensorCall::serve_axon { netuid, .. } + | SubtensorCall::serve_axon_tls { netuid, .. } + | SubtensorCall::serve_prometheus { netuid, .. } + | SubtensorCall::set_weights { netuid, .. } + | SubtensorCall::commit_weights { netuid, .. } + | SubtensorCall::reveal_weights { netuid, .. } + | SubtensorCall::batch_reveal_weights { netuid, .. } + | SubtensorCall::commit_timelocked_weights { netuid, .. } + | SubtensorCall::set_mechanism_weights { netuid, .. } + | SubtensorCall::commit_mechanism_weights { netuid, .. } + | SubtensorCall::reveal_mechanism_weights { netuid, .. } + | SubtensorCall::commit_crv3_mechanism_weights { netuid, .. } + | SubtensorCall::commit_timelocked_mechanism_weights { netuid, .. } => { + Some(*netuid) + } + _ => None, + }, + _ => None, + } } - let tempo = BlockNumber::from(Tempo::::get(netuid) as u32); - span.saturating_mul(tempo) -} -fn neuron_identity(origin: &RuntimeOrigin, netuid: NetUid) -> Option<(AccountId, u16)> { - let hotkey = signed_origin(origin)?; - let uid = - pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey(netuid, &hotkey).ok()?; - Some((hotkey, uid)) -} - -fn owner_hparam_netuid(call: &AdminUtilsCall) -> Option { - match call { - AdminUtilsCall::sudo_set_activity_cutoff { netuid, .. } - | AdminUtilsCall::sudo_set_adjustment_alpha { netuid, .. } - | AdminUtilsCall::sudo_set_alpha_sigmoid_steepness { netuid, .. } - | AdminUtilsCall::sudo_set_alpha_values { netuid, .. } - | AdminUtilsCall::sudo_set_bonds_moving_average { netuid, .. } - | AdminUtilsCall::sudo_set_bonds_penalty { netuid, .. } - | AdminUtilsCall::sudo_set_bonds_reset_enabled { netuid, .. } - | AdminUtilsCall::sudo_set_commit_reveal_weights_enabled { netuid, .. } - | AdminUtilsCall::sudo_set_commit_reveal_weights_interval { netuid, .. } - | AdminUtilsCall::sudo_set_immunity_period { netuid, .. } - | AdminUtilsCall::sudo_set_liquid_alpha_enabled { netuid, .. } - | AdminUtilsCall::sudo_set_max_allowed_uids { netuid, .. } - | AdminUtilsCall::sudo_set_max_burn { netuid, .. } - | AdminUtilsCall::sudo_set_max_difficulty { netuid, .. } - | AdminUtilsCall::sudo_set_min_allowed_weights { netuid, .. } - | AdminUtilsCall::sudo_set_min_burn { netuid, .. } - | AdminUtilsCall::sudo_set_network_pow_registration_allowed { netuid, .. } - | AdminUtilsCall::sudo_set_owner_immune_neuron_limit { netuid, .. } - | AdminUtilsCall::sudo_set_recycle_or_burn { netuid, .. } - | AdminUtilsCall::sudo_set_rho { netuid, .. } - | AdminUtilsCall::sudo_set_serving_rate_limit { netuid, .. } - | AdminUtilsCall::sudo_set_toggle_transfer { netuid, .. } - | AdminUtilsCall::sudo_set_weights_version_key { netuid, .. } - | AdminUtilsCall::sudo_set_yuma3_enabled { netuid, .. } => Some(*netuid), - _ => None, + fn should_bypass(origin: &RuntimeOrigin, _call: &RuntimeCall) -> bool { + matches!(origin.clone().into(), Ok(RawOrigin::Root)) } -} -fn admin_scope_netuid(call: &AdminUtilsCall) -> Option { - owner_hparam_netuid(call).or_else(|| match call { - AdminUtilsCall::sudo_set_mechanism_count { netuid, .. } - | AdminUtilsCall::sudo_set_mechanism_emission_split { netuid, .. } - | AdminUtilsCall::sudo_set_sn_owner_hotkey { netuid, .. } - | AdminUtilsCall::sudo_trim_to_max_allowed_uids { netuid, .. } => Some(*netuid), - _ => None, - }) + fn adjust_span(_origin: &RuntimeOrigin, call: &RuntimeCall, span: BlockNumber) -> BlockNumber { + match call { + RuntimeCall::AdminUtils(inner) => { + if let Some(netuid) = owner_hparam_netuid(inner) { + if span == 0 { + return span; + } + let tempo = BlockNumber::from(Tempo::::get(netuid) as u32); + span.saturating_mul(tempo) + } else { + span + } + } + _ => span, + } + } } #[derive(Default)] @@ -264,63 +231,46 @@ impl RateLimitUsageResolver - for ScopeResolver -{ - fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option { - match call { - RuntimeCall::SubtensorModule(inner) => match inner { - SubtensorCall::serve_axon { netuid, .. } - | SubtensorCall::serve_axon_tls { netuid, .. } - | SubtensorCall::serve_prometheus { netuid, .. } - | SubtensorCall::set_weights { netuid, .. } - | SubtensorCall::commit_weights { netuid, .. } - | SubtensorCall::reveal_weights { netuid, .. } - | SubtensorCall::batch_reveal_weights { netuid, .. } - | SubtensorCall::commit_timelocked_weights { netuid, .. } => { - Some(RateLimitScope::Subnet(*netuid)) - } - SubtensorCall::set_mechanism_weights { netuid, mecid, .. } - | SubtensorCall::commit_mechanism_weights { netuid, mecid, .. } - | SubtensorCall::reveal_mechanism_weights { netuid, mecid, .. } - | SubtensorCall::commit_crv3_mechanism_weights { netuid, mecid, .. } - | SubtensorCall::commit_timelocked_mechanism_weights { netuid, mecid, .. } => { - Some(RateLimitScope::SubnetMechanism { - netuid: *netuid, - mecid: *mecid, - }) - } - _ => None, - }, - RuntimeCall::AdminUtils(inner) => { - if owner_hparam_netuid(inner).is_some() { - // Hyperparameter setters share a global limit span; usage is tracked per subnet. - None - } else { - admin_scope_netuid(inner).map(RateLimitScope::Subnet) - } - } - _ => None, - } - } +fn neuron_identity(origin: &RuntimeOrigin, netuid: NetUid) -> Option<(AccountId, u16)> { + let hotkey = signed_origin(origin)?; + let uid = + pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey(netuid, &hotkey).ok()?; + Some((hotkey, uid)) +} - fn should_bypass(origin: &RuntimeOrigin, _call: &RuntimeCall) -> bool { - matches!(origin.clone().into(), Ok(RawOrigin::Root)) +fn signed_origin(origin: &RuntimeOrigin) -> Option { + match origin.clone().into() { + Ok(RawOrigin::Signed(who)) => Some(who), + _ => None, } +} - fn adjust_span(_origin: &RuntimeOrigin, call: &RuntimeCall, span: BlockNumber) -> BlockNumber { - match call { - RuntimeCall::AdminUtils(inner) => { - if let Some(netuid) = owner_hparam_netuid(inner) { - tempo_scaled(netuid, span) - } else { - span - } - } - _ => span, - } +fn owner_hparam_netuid(call: &AdminUtilsCall) -> Option { + match call { + AdminUtilsCall::sudo_set_activity_cutoff { netuid, .. } + | AdminUtilsCall::sudo_set_adjustment_alpha { netuid, .. } + | AdminUtilsCall::sudo_set_alpha_sigmoid_steepness { netuid, .. } + | AdminUtilsCall::sudo_set_alpha_values { netuid, .. } + | AdminUtilsCall::sudo_set_bonds_moving_average { netuid, .. } + | AdminUtilsCall::sudo_set_bonds_penalty { netuid, .. } + | AdminUtilsCall::sudo_set_bonds_reset_enabled { netuid, .. } + | AdminUtilsCall::sudo_set_commit_reveal_weights_enabled { netuid, .. } + | AdminUtilsCall::sudo_set_commit_reveal_weights_interval { netuid, .. } + | AdminUtilsCall::sudo_set_immunity_period { netuid, .. } + | AdminUtilsCall::sudo_set_liquid_alpha_enabled { netuid, .. } + | AdminUtilsCall::sudo_set_max_allowed_uids { netuid, .. } + | AdminUtilsCall::sudo_set_max_burn { netuid, .. } + | AdminUtilsCall::sudo_set_max_difficulty { netuid, .. } + | AdminUtilsCall::sudo_set_min_allowed_weights { netuid, .. } + | AdminUtilsCall::sudo_set_min_burn { netuid, .. } + | AdminUtilsCall::sudo_set_network_pow_registration_allowed { netuid, .. } + | AdminUtilsCall::sudo_set_owner_immune_neuron_limit { netuid, .. } + | AdminUtilsCall::sudo_set_recycle_or_burn { netuid, .. } + | AdminUtilsCall::sudo_set_rho { netuid, .. } + | AdminUtilsCall::sudo_set_serving_rate_limit { netuid, .. } + | AdminUtilsCall::sudo_set_toggle_transfer { netuid, .. } + | AdminUtilsCall::sudo_set_weights_version_key { netuid, .. } + | AdminUtilsCall::sudo_set_yuma3_enabled { netuid, .. } => Some(*netuid), + _ => None, } } diff --git a/runtime/tests/rate_limiting_behavior.rs b/runtime/tests/rate_limiting_behavior.rs new file mode 100644 index 0000000000..c513409db7 --- /dev/null +++ b/runtime/tests/rate_limiting_behavior.rs @@ -0,0 +1,420 @@ +#![allow(clippy::unwrap_used)] + +use frame_support::traits::OnRuntimeUpgrade; +use frame_system::pallet_prelude::BlockNumberFor; +use node_subtensor_runtime::{ + BuildStorage, Runtime, RuntimeCall, RuntimeGenesisConfig, RuntimeOrigin, RuntimeScopeResolver, + RuntimeUsageResolver, SubtensorModule, System, rate_limiting::RateLimitUsageKey, + rate_limiting::migration::Migration, +}; +use pallet_rate_limiting::{RateLimitScopeResolver, RateLimitUsageResolver}; +use pallet_rate_limiting::{RateLimitTarget, TransactionIdentifier}; +use pallet_subtensor::Call as SubtensorCall; +use pallet_subtensor::{ + AxonInfo, HasMigrationRun, LastRateLimitedBlock, LastUpdate, NetworksAdded, PrometheusInfo, + RateLimitKey, ServingRateLimit, TransactionKeyLastBlock, WeightsSetRateLimit, + WeightsVersionKeyRateLimit, utils::rate_limiting::TransactionType, +}; +use sp_core::{H160, ecdsa}; +use sp_runtime::traits::SaturatedConversion; +use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; + +type AccountId = ::AccountId; +type GroupId = ::GroupId; +type UsageKey = RateLimitUsageKey; + +const MIGRATION_NAME: &[u8] = b"migrate_rate_limiting"; + +fn new_ext() -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig::default() + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +fn account(n: u8) -> AccountId { + AccountId::from([n; 32]) +} + +fn resolve_target(identifier: TransactionIdentifier) -> RateLimitTarget { + if let Some(group) = pallet_rate_limiting::CallGroups::::get(identifier) { + RateLimitTarget::Group(group) + } else { + RateLimitTarget::Transaction(identifier) + } +} + +fn exact_span(span: u64) -> BlockNumberFor { + span.saturated_into::>() +} + +fn clear_rate_limiting_storage() { + let limit = u32::MAX; + let _ = pallet_rate_limiting::Limits::::clear(limit, None); + let _ = pallet_rate_limiting::LastSeen::::clear(limit, None); + let _ = pallet_rate_limiting::Groups::::clear(limit, None); + let _ = pallet_rate_limiting::GroupMembers::::clear(limit, None); + let _ = pallet_rate_limiting::GroupNameIndex::::clear(limit, None); + let _ = pallet_rate_limiting::CallGroups::::clear(limit, None); + pallet_rate_limiting::NextGroupId::::kill(); +} + +fn parity_check( + now: u64, + call: RuntimeCall, + origin: RuntimeOrigin, + usage_override: Option, + scope_override: Option, + legacy_check: F, +) where + F: Fn() -> bool, +{ + System::set_block_number(now.saturated_into()); + HasMigrationRun::::remove(MIGRATION_NAME); + clear_rate_limiting_storage(); + + // Run migration to hydrate pallet-rate-limiting state. + Migration::::on_runtime_upgrade(); + + let identifier = + TransactionIdentifier::from_call::(&call).expect("identifier for call"); + let scope = scope_override.or_else(|| RuntimeScopeResolver::context(&origin, &call)); + let usage: Option<::UsageKey> = usage_override + .map(Into::into) + .or_else(|| RuntimeUsageResolver::context(&origin, &call).map(Into::into)); + let target = resolve_target(identifier); + + let span = pallet_rate_limiting::Pallet::::resolved_limit(&target, &scope) + .unwrap_or_default(); + let span_u64: u64 = span.saturated_into(); + + let within = pallet_rate_limiting::Pallet::::is_within_limit( + &origin.clone().into(), + &call, + &identifier, + &scope, + &usage, + ) + .expect("pallet rate limit result"); + assert_eq!(within, legacy_check(), "parity at now for {:?}", identifier); + + // Advance beyond the span and re-check (span==0 treated as allow). + let advance: BlockNumberFor = span.saturating_add(exact_span(1)); + System::set_block_number(System::block_number().saturating_add(advance)); + + let within_after = pallet_rate_limiting::Pallet::::is_within_limit( + &origin.into(), + &call, + &identifier, + &scope, + &usage, + ) + .expect("pallet rate limit result (after)"); + assert!( + within_after || span_u64 == 0, + "parity after window for {:?}", + identifier + ); +} + +#[test] +fn register_network_parity() { + new_ext().execute_with(|| { + let now = 100u64; + let cold = account(1); + let hot = account(2); + let span = 5u64; + LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, now - 1); + pallet_subtensor::NetworkRateLimit::::put(span); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::register_network { hotkey: hot }); + let origin = RuntimeOrigin::signed(cold.clone()); + let legacy = || TransactionType::RegisterNetwork.passes_rate_limit::(&cold); + parity_check(now, call, origin, None, None, legacy); + }); +} + +#[test] +fn swap_hotkey_parity() { + new_ext().execute_with(|| { + let now = 200u64; + let cold = account(10); + let old_hot = account(11); + let new_hot = account(12); + let span = 10u64; + LastRateLimitedBlock::::insert(RateLimitKey::LastTxBlock(cold.clone()), now - 1); + pallet_subtensor::TxRateLimit::::put(span); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::swap_hotkey { + hotkey: old_hot, + new_hotkey: new_hot, + netuid: None, + }); + let origin = RuntimeOrigin::signed(cold.clone()); + let legacy = || !SubtensorModule::exceeds_tx_rate_limit(now - 1, now); + parity_check(now, call, origin, None, None, legacy); + }); +} + +#[test] +fn increase_take_parity() { + new_ext().execute_with(|| { + let now = 300u64; + let hot = account(20); + let span = 3u64; + LastRateLimitedBlock::::insert( + RateLimitKey::LastTxBlockDelegateTake(hot.clone()), + now - 1, + ); + pallet_subtensor::TxDelegateTakeRateLimit::::put(span); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::increase_take { + hotkey: hot.clone(), + take: 5, + }); + let origin = RuntimeOrigin::signed(account(21)); + let legacy = || !SubtensorModule::exceeds_tx_delegate_take_rate_limit(now - 1, now); + parity_check(now, call, origin, None, None, legacy); + }); +} + +#[test] +fn set_childkey_take_parity() { + new_ext().execute_with(|| { + let now = 400u64; + let hot = account(30); + let netuid = NetUid::from(1u16); + let span = 7u64; + let tx_kind: u16 = TransactionType::SetChildkeyTake.into(); + TransactionKeyLastBlock::::insert((hot.clone(), netuid, tx_kind), now - 1); + pallet_subtensor::TxChildkeyTakeRateLimit::::put(span); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::set_childkey_take { + hotkey: hot.clone(), + netuid, + take: 1, + }); + let origin = RuntimeOrigin::signed(account(31)); + let legacy = || { + TransactionType::SetChildkeyTake.passes_rate_limit_on_subnet::(&hot, netuid) + }; + parity_check(now, call, origin, None, None, legacy); + }); +} + +#[test] +fn set_children_parity() { + new_ext().execute_with(|| { + let now = 500u64; + let hot = account(40); + let netuid = NetUid::from(2u16); + let tx_kind: u16 = TransactionType::SetChildren.into(); + TransactionKeyLastBlock::::insert((hot.clone(), netuid, tx_kind), now - 1); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::set_children { + hotkey: hot.clone(), + netuid, + children: Vec::new(), + }); + let origin = RuntimeOrigin::signed(account(41)); + let legacy = + || TransactionType::SetChildren.passes_rate_limit_on_subnet::(&hot, netuid); + parity_check(now, call, origin, None, None, legacy); + }); +} + +#[test] +fn serving_parity() { + new_ext().execute_with(|| { + let now = 600u64; + let hot = account(50); + let netuid = NetUid::from(3u16); + let span = 5u64; + ServingRateLimit::::insert(netuid, span); + pallet_subtensor::Axons::::insert( + netuid, + hot.clone(), + AxonInfo { + block: now - 1, + ..Default::default() + }, + ); + pallet_subtensor::Prometheus::::insert( + netuid, + hot.clone(), + PrometheusInfo { + block: now - 1, + ..Default::default() + }, + ); + + // Axon + let axon_call = RuntimeCall::SubtensorModule(SubtensorCall::serve_axon { + netuid, + version: 1, + ip: 0, + port: 0, + ip_type: 4, + protocol: 0, + placeholder1: 0, + placeholder2: 0, + }); + let origin = RuntimeOrigin::signed(hot.clone()); + let legacy_axon = || { + SubtensorModule::axon_passes_rate_limit( + netuid, + &AxonInfo { + block: now - 1, + ..Default::default() + }, + now, + ) + }; + parity_check(now, axon_call, origin.clone(), None, None, legacy_axon); + + // Prometheus + let prom_call = RuntimeCall::SubtensorModule(SubtensorCall::serve_prometheus { + netuid, + version: 1, + ip: 0, + port: 0, + ip_type: 4, + }); + let legacy_prom = || { + SubtensorModule::prometheus_passes_rate_limit( + netuid, + &PrometheusInfo { + block: now - 1, + ..Default::default() + }, + now, + ) + }; + parity_check(now, prom_call, origin, None, None, legacy_prom); + }); +} + +#[test] +fn weights_and_hparam_parity() { + new_ext().execute_with(|| { + let now = 700u64; + let hot = account(60); + let netuid = NetUid::from(4u16); + let uid: u16 = 0; + let weights_span = 4u64; + let tempo = 3u16; + // Ensure subnet exists so LastUpdate is imported. + NetworksAdded::::insert(netuid, true); + SubtensorModule::set_tempo(netuid, tempo); + WeightsSetRateLimit::::insert(netuid, weights_span); + LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![now - 1]); + + let weights_call = RuntimeCall::SubtensorModule(SubtensorCall::set_weights { + netuid, + dests: Vec::new(), + weights: Vec::new(), + version_key: 0, + }); + let origin = RuntimeOrigin::signed(hot.clone()); + let scope = Some(netuid); + let usage = Some(UsageKey::SubnetNeuron { netuid, uid }); + let legacy_weights = || SubtensorModule::check_rate_limit(netuid.into(), uid, now); + parity_check( + now, + weights_call, + origin.clone(), + usage, + scope, + legacy_weights, + ); + + // Hyperparam (activity_cutoff) with tempo scaling. + let hparam_span_epochs = 2u16; + pallet_subtensor::OwnerHyperparamRateLimit::::put(hparam_span_epochs); + LastRateLimitedBlock::::insert( + RateLimitKey::OwnerHyperparamUpdate( + netuid, + pallet_subtensor::utils::rate_limiting::Hyperparameter::ActivityCutoff, + ), + now - 1, + ); + let hparam_call = + RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_activity_cutoff { + netuid, + activity_cutoff: 1, + }); + let hparam_origin = RuntimeOrigin::signed(hot); + let legacy_hparam = || { + let span = (tempo as u64) * (hparam_span_epochs as u64); + let last = now - 1; + // same logic as TransactionType::OwnerHyperparamUpdate in legacy: passes if delta >= span. + let delta = now.saturating_sub(last); + delta >= span + }; + parity_check(now, hparam_call, hparam_origin, None, None, legacy_hparam); + }); +} + +#[test] +fn weights_version_parity() { + new_ext().execute_with(|| { + let now = 800u64; + let hot = account(70); + let netuid = NetUid::from(5u16); + NetworksAdded::::insert(netuid, true); + SubtensorModule::set_tempo(netuid, 4); + WeightsVersionKeyRateLimit::::put(2u64); + let tx_kind_wvk: u16 = TransactionType::SetWeightsVersionKey.into(); + TransactionKeyLastBlock::::insert((hot.clone(), netuid, tx_kind_wvk), now - 1); + + let wvk_call = + RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_weights_version_key { + netuid, + weights_version_key: 0, + }); + let origin = RuntimeOrigin::signed(hot.clone()); + let legacy_wvk = || { + let limit = SubtensorModule::get_tempo(netuid) as u64 + * WeightsVersionKeyRateLimit::::get(); + let delta = now.saturating_sub(now - 1); + delta >= limit + }; + parity_check(now, wvk_call, origin, None, None, legacy_wvk); + }); +} + +#[test] +fn associate_evm_key_parity() { + new_ext().execute_with(|| { + let now = 900u64; + let hot = account(80); + let netuid = NetUid::from(6u16); + let uid: u16 = 0; + NetworksAdded::::insert(netuid, true); + pallet_subtensor::AssociatedEvmAddress::::insert( + netuid, + uid, + (H160::zero(), now - 1), + ); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::associate_evm_key { + netuid, + evm_key: H160::zero(), + block_number: now, + signature: ecdsa::Signature::from_raw([0u8; 65]), + }); + let origin = RuntimeOrigin::signed(hot.clone()); + let usage = Some(UsageKey::SubnetNeuron { netuid, uid }); + let scope = Some(netuid); + let limit = ::EvmKeyAssociateRateLimit::get(); + let legacy = || { + let last = now - 1; + let delta = now.saturating_sub(last); + delta >= limit + }; + parity_check(now, call, origin, usage, scope, legacy); + }); +} diff --git a/runtime/tests/rate_limiting_migration.rs b/runtime/tests/rate_limiting_migration.rs index 23a16e3fe4..40f68151ff 100644 --- a/runtime/tests/rate_limiting_migration.rs +++ b/runtime/tests/rate_limiting_migration.rs @@ -3,12 +3,13 @@ use frame_support::traits::OnRuntimeUpgrade; use frame_system::pallet_prelude::BlockNumberFor; use node_subtensor_runtime::{ - rate_limiting, - rate_limiting::migration::{identifier_for_transaction_type, Migration}, - BuildStorage, Runtime, RuntimeGenesisConfig, SubtensorModule, System, + BuildStorage, Runtime, RuntimeGenesisConfig, SubtensorModule, System, rate_limiting, + rate_limiting::migration::{Migration, identifier_for_transaction_type}, }; use pallet_rate_limiting::{RateLimit, RateLimitKind, RateLimitTarget}; -use pallet_subtensor::{HasMigrationRun, LastRateLimitedBlock, RateLimitKey, utils::rate_limiting::TransactionType}; +use pallet_subtensor::{ + HasMigrationRun, LastRateLimitedBlock, RateLimitKey, utils::rate_limiting::TransactionType, +}; use sp_runtime::traits::SaturatedConversion; use subtensor_runtime_common::NetUid; @@ -27,7 +28,9 @@ fn new_test_ext() -> sp_io::TestExternalities { ext } -fn resolve_target(identifier: pallet_rate_limiting::TransactionIdentifier) -> RateLimitTarget { +fn resolve_target( + identifier: pallet_rate_limiting::TransactionIdentifier, +) -> RateLimitTarget { if let Some(group) = pallet_rate_limiting::CallGroups::::get(identifier) { RateLimitTarget::Group(group) } else { @@ -58,10 +61,7 @@ fn migrates_global_register_network_last_seen() { // LastSeen preserved globally (usage = None). let stored = pallet_rate_limiting::LastSeen::::get(target, None::) .expect("last seen entry"); - assert_eq!( - stored, - 10u64.saturated_into::>() - ); + assert_eq!(stored, 10u64.saturated_into::>()); }); } @@ -88,11 +88,8 @@ fn sn_owner_hotkey_limit_not_tempo_scaled_and_last_seen_preserved() { // LastSeen preserved per subnet. let usage: Option<::UsageKey> = Some(UsageKey::Subnet(netuid).into()); - let stored = pallet_rate_limiting::LastSeen::::get(target, usage) - .expect("last seen entry"); - assert_eq!( - stored, - 100u64.saturated_into::>() - ); + let stored = + pallet_rate_limiting::LastSeen::::get(target, usage).expect("last seen entry"); + assert_eq!(stored, 100u64.saturated_into::>()); }); } From 34d5f26509cb4a40a9c66349c7e6c72bd97f67ee Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 1 Dec 2025 16:23:17 +0300 Subject: [PATCH 30/95] Remove register-network calls from rate limit usage resolver --- runtime/src/rate_limiting/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 5b4a97248a..2e5bc85d24 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -113,8 +113,6 @@ impl RateLimitUsageResolver { signed_origin(origin).map(RateLimitUsageKey::::Account) } - SubtensorCall::register_network { .. } - | SubtensorCall::register_network_with_identity { .. } => None, SubtensorCall::increase_take { hotkey, .. } => { Some(RateLimitUsageKey::::Account(hotkey.clone())) } @@ -136,6 +134,8 @@ impl RateLimitUsageResolver Date: Mon, 1 Dec 2025 16:56:04 +0300 Subject: [PATCH 31/95] Add read-only flag to grouped calls in pallet-rate-limiting --- pallets/rate-limiting/src/benchmarking.rs | 4 +- pallets/rate-limiting/src/lib.rs | 82 ++++++++++++++++++-- pallets/rate-limiting/src/tests.rs | 93 ++++++++++++++++++++--- pallets/rate-limiting/src/tx_extension.rs | 67 +++++++++++++++- 4 files changed, 222 insertions(+), 24 deletions(-) diff --git a/pallets/rate-limiting/src/benchmarking.rs b/pallets/rate-limiting/src/benchmarking.rs index 23dfecec85..38568dea28 100644 --- a/pallets/rate-limiting/src/benchmarking.rs +++ b/pallets/rate-limiting/src/benchmarking.rs @@ -8,6 +8,7 @@ use frame_system::{RawOrigin, pallet_prelude::BlockNumberFor}; use sp_runtime::traits::{One, Saturating}; use super::*; +use crate::CallReadOnly; pub trait BenchmarkHelper { fn sample_call() -> Call; @@ -85,9 +86,10 @@ mod benchmarks { let identifier = register_call_with_group::(None); #[extrinsic_call] - _(RawOrigin::Root, identifier, group); + _(RawOrigin::Root, identifier, group, false); assert_eq!(CallGroups::::get(identifier), Some(group)); + assert_eq!(CallReadOnly::::get(identifier), Some(false)); assert!(GroupMembers::::get(group).contains(&identifier)); } diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index b40976f018..af9149f8dd 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -20,6 +20,8 @@ //! - [`assign_call_to_group`](pallet::Pallet::assign_call_to_group) and //! [`remove_call_from_group`](pallet::Pallet::remove_call_from_group): manage group membership for //! registered calls. +//! - [`set_call_read_only`](pallet::Pallet::set_call_read_only): for grouped calls, choose whether +//! successful dispatches should update the shared usage row (`false` by default). //! - [`deregister_call`](pallet::Pallet::deregister_call): remove scoped configuration or wipe the //! registration entirely. //! - [`set_default_rate_limit`](pallet::Pallet::set_default_rate_limit): set the global default @@ -280,6 +282,12 @@ pub mod pallet { OptionQuery, >; + /// Tracks whether a grouped call should skip writing usage metadata on success. + #[pallet::storage] + #[pallet::getter(fn call_read_only)] + pub type CallReadOnly, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, TransactionIdentifier, bool, OptionQuery>; + /// Metadata for each configured group. #[pallet::storage] #[pallet::getter(fn groups)] @@ -394,6 +402,15 @@ pub mod pallet { /// Updated group assignment (None when cleared). group: Option<>::GroupId>, }, + /// A grouped call toggled whether it writes usage after enforcement. + CallReadOnlyUpdated { + /// Identifier of the transaction. + transaction: TransactionIdentifier, + /// Group to which the call belongs. + group: >::GroupId, + /// Current read-only flag. + read_only: bool, + }, } /// Errors that can occur while configuring rate limits. @@ -586,6 +603,18 @@ pub mod pallet { true } + pub(crate) fn should_record_usage( + identifier: &TransactionIdentifier, + usage_target: &RateLimitTarget<>::GroupId>, + ) -> bool { + match usage_target { + RateLimitTarget::Group(_) => { + !CallReadOnly::::get(identifier).unwrap_or(false) + } + RateLimitTarget::Transaction(_) => true, + } + } + /// Inserts or updates the cached usage timestamp for a rate-limited call. /// /// This is primarily intended for migrations that need to hydrate the new tracking storage @@ -893,6 +922,7 @@ pub mod pallet { Self::ensure_group_details(group_id)?; Self::insert_call_into_group(&identifier, group_id)?; CallGroups::::insert(&identifier, group_id); + CallReadOnly::::insert(&identifier, false); assigned_group = Some(group_id); } @@ -910,6 +940,11 @@ pub mod pallet { transaction: identifier, group: Some(group_id), }); + Self::deposit_event(Event::CallReadOnlyUpdated { + transaction: identifier, + group: group_id, + read_only: false, + }); } Ok(()) @@ -965,13 +1000,15 @@ pub mod pallet { Ok(()) } - /// Assigns a registered call to the specified group. + /// Assigns a registered call to the specified group and optionally marks it as read-only + /// for usage tracking. #[pallet::call_index(2)] #[pallet::weight(T::DbWeight::get().reads_writes(3, 3))] pub fn assign_call_to_group( origin: OriginFor, transaction: TransactionIdentifier, group: >::GroupId, + read_only: bool, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; @@ -979,20 +1016,20 @@ pub mod pallet { Self::ensure_group_details(group)?; let current = CallGroups::::get(&transaction); - if current == Some(group) { - return Err(Error::::CallAlreadyInGroup.into()); - } - + ensure!(current.is_none(), Error::::CallAlreadyInGroup); Self::insert_call_into_group(&transaction, group)?; - if let Some(existing) = current { - Self::detach_call_from_group(&transaction, existing); - } CallGroups::::insert(&transaction, group); + CallReadOnly::::insert(&transaction, read_only); Self::deposit_event(Event::CallGroupUpdated { transaction, group: Some(group), }); + Self::deposit_event(Event::CallReadOnlyUpdated { + transaction, + group, + read_only, + }); Ok(()) } @@ -1010,6 +1047,7 @@ pub mod pallet { let Some(group) = CallGroups::::take(&transaction) else { return Err(Error::::CallNotInGroup.into()); }; + CallReadOnly::::remove(&transaction); Self::detach_call_from_group(&transaction, group); Self::deposit_event(Event::CallGroupUpdated { @@ -1164,6 +1202,7 @@ pub mod pallet { ensure!(removed, Error::::MissingRateLimit); if let Some(group) = CallGroups::::take(&transaction) { + CallReadOnly::::remove(&transaction); Self::detach_call_from_group(&transaction, group); Self::deposit_event(Event::CallGroupUpdated { transaction, @@ -1178,6 +1217,7 @@ pub mod pallet { } if let Some(group) = CallGroups::::take(&transaction) { + CallReadOnly::::remove(&transaction); Self::detach_call_from_group(&transaction, group); Self::deposit_event(Event::CallGroupUpdated { transaction, @@ -1202,5 +1242,31 @@ pub mod pallet { Ok(()) } + + /// Updates whether a grouped call should skip writing usage metadata after enforcement. + /// + /// The call must already be assigned to a group. + #[pallet::call_index(9)] + #[pallet::weight(T::DbWeight::get().reads_writes(2, 1))] + pub fn set_call_read_only( + origin: OriginFor, + transaction: TransactionIdentifier, + read_only: bool, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + Self::ensure_call_registered(&transaction)?; + let group = + CallGroups::::get(&transaction).ok_or(Error::::CallNotInGroup)?; + CallReadOnly::::insert(&transaction, read_only); + + Self::deposit_event(Event::CallReadOnlyUpdated { + transaction, + group, + read_only, + }); + + Ok(()) + } } } diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index 5027909b67..1b4fc170b4 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -2,8 +2,8 @@ use frame_support::{assert_noop, assert_ok}; use sp_std::vec::Vec; use crate::{ - CallGroups, Config, GroupMembers, GroupSharing, LastSeen, Limits, RateLimit, RateLimitKind, - RateLimitTarget, TransactionIdentifier, mock::*, pallet::Error, + CallGroups, CallReadOnly, Config, GroupMembers, GroupSharing, LastSeen, Limits, RateLimit, + RateLimitKind, RateLimitTarget, TransactionIdentifier, mock::*, pallet::Error, }; use frame_support::traits::Get; @@ -132,6 +132,7 @@ fn set_rate_limit_requires_registration_and_group_targeting() { RuntimeOrigin::root(), identifier, group, + false, )); assert_noop!( RateLimiting::set_rate_limit( @@ -150,11 +151,42 @@ fn set_rate_limit_respects_group_config_sharing() { new_test_ext().execute_with(|| { let identifier = register(remark_call(), None); let group = create_group(b"test", GroupSharing::ConfigAndUsage); + // Consume group creation event to keep ordering predictable. + let created = last_event(); + assert!(matches!( + created, + RuntimeEvent::RateLimiting(crate::Event::GroupCreated { group: g, .. }) if g == group + )); assert_ok!(RateLimiting::assign_call_to_group( RuntimeOrigin::root(), identifier, group, + false, )); + let events: Vec<_> = System::events() + .into_iter() + .map(|e| e.event) + .filter(|evt| matches!(evt, RuntimeEvent::RateLimiting(_))) + .collect(); + assert!(events.iter().any(|evt| { + matches!( + evt, + RuntimeEvent::RateLimiting(crate::Event::CallReadOnlyUpdated { + transaction, + group: g, + read_only: false, + }) if *transaction == identifier && *g == group + ) + })); + assert!(events.iter().any(|evt| { + matches!( + evt, + RuntimeEvent::RateLimiting(crate::Event::CallGroupUpdated { + transaction, + group: Some(g), + }) if *transaction == identifier && *g == group + ) + })); assert_noop!( RateLimiting::set_rate_limit( RuntimeOrigin::root(), @@ -164,15 +196,6 @@ fn set_rate_limit_respects_group_config_sharing() { ), Error::::MustTargetGroup ); - - let event = last_event(); - assert!(matches!( - event, - RuntimeEvent::RateLimiting(crate::Event::CallGroupUpdated { - transaction, - group: Some(g), - }) if transaction == identifier && g == group - )); }); } @@ -185,8 +208,10 @@ fn assign_and_remove_group_membership() { RuntimeOrigin::root(), identifier, group, + false, )); assert_eq!(CallGroups::::get(identifier), Some(group)); + assert_eq!(CallReadOnly::::get(identifier), Some(false)); assert!(GroupMembers::::get(group).contains(&identifier)); assert_ok!(RateLimiting::remove_call_from_group( RuntimeOrigin::root(), @@ -395,7 +420,7 @@ fn group_member_limit_and_removal_errors() { // Next insert should fail. let extra = register(remark_call(), None); assert_noop!( - RateLimiting::assign_call_to_group(RuntimeOrigin::root(), extra, group), + RateLimiting::assign_call_to_group(RuntimeOrigin::root(), extra, group, false), Error::::GroupMemberLimitExceeded ); @@ -407,6 +432,50 @@ fn group_member_limit_and_removal_errors() { }); } +#[test] +fn set_call_read_only_requires_group() { + new_test_ext().execute_with(|| { + let identifier = register(remark_call(), None); + assert_noop!( + RateLimiting::set_call_read_only(RuntimeOrigin::root(), identifier, true), + Error::::CallNotInGroup + ); + }); +} + +#[test] +fn set_call_read_only_updates_assignment_and_emits_event() { + new_test_ext().execute_with(|| { + let group = create_group(b"ro", GroupSharing::UsageOnly); + let identifier = register(remark_call(), None); + assert_ok!(RateLimiting::assign_call_to_group( + RuntimeOrigin::root(), + identifier, + group, + false, + )); + + assert_ok!(RateLimiting::set_call_read_only( + RuntimeOrigin::root(), + identifier, + true + )); + + assert_eq!(CallGroups::::get(identifier), Some(group)); + assert_eq!(CallReadOnly::::get(identifier), Some(true)); + + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::CallReadOnlyUpdated { + transaction, + group: g, + read_only: true, + }) if transaction == identifier && g == group + )); + }); +} + #[test] fn cannot_delete_group_in_use_or_unknown() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index 3d400e6918..e1ffd4f14f 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -95,10 +95,12 @@ where type Val = Option<( RateLimitTarget<>::GroupId>, Option<>::UsageKey>, + bool, )>; type Pre = Option<( RateLimitTarget<>::GroupId>, Option<>::UsageKey>, + bool, )>; fn weight(&self, _call: &>::RuntimeCall) -> Weight { @@ -131,6 +133,7 @@ where .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; let usage_target = Pallet::::usage_target(&identifier) .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + let should_record = Pallet::::should_record_usage(&identifier, &usage_target); let Some(block_span) = Pallet::::effective_span(&origin, call, &config_target, &scope) @@ -152,7 +155,7 @@ where Ok(( ValidTransaction::default(), - Some((usage_target, usage)), + Some((usage_target, usage, should_record)), origin, )) } @@ -176,7 +179,10 @@ where result: &DispatchResult, ) -> Result<(), TransactionValidityError> { if result.is_ok() { - if let Some((target, usage)) = pre { + if let Some((target, usage, should_record)) = pre { + if !should_record { + return Ok(()); + } let block_number = frame_system::Pallet::::block_number(); LastSeen::::insert(target, usage, block_number); } @@ -237,7 +243,7 @@ mod tests { ) -> Result< ( sp_runtime::transaction_validity::ValidTransaction, - Option<(RateLimitTarget, Option)>, + Option<(RateLimitTarget, Option, bool)>, RuntimeOrigin, ), TransactionValidityError, @@ -432,6 +438,61 @@ mod tests { }); } + #[test] + fn tx_extension_skips_write_for_read_only_group_member() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + assert_ok!(RateLimiting::create_group( + RuntimeOrigin::root(), + b"use-ro".to_vec(), + GroupSharing::UsageOnly, + )); + let group = RateLimiting::next_group_id().saturating_sub(1); + + let call = remark_call(); + let identifier = identifier_for(&call); + assert_ok!(RateLimiting::register_call( + RuntimeOrigin::root(), + Box::new(call.clone()), + Some(group), + )); + assert_ok!(RateLimiting::set_call_read_only( + RuntimeOrigin::root(), + identifier, + true + )); + + let tx_target = RateLimitTarget::Transaction(identifier); + let usage_target = RateLimitTarget::Group(group); + Limits::::insert(tx_target, RateLimit::global(RateLimitKind::Exact(2))); + LastSeen::::insert(usage_target, Some(1u16), 2); + + System::set_block_number(5); + + let (_valid, val, _) = validate_with_tx_extension(&extension, &call).expect("valid"); + let info = call.get_dispatch_info(); + let len = call.encode().len(); + let origin_for_prepare = RuntimeOrigin::signed(42); + let pre = extension + .clone() + .prepare(val.clone(), &origin_for_prepare, &call, &info, len) + .expect("prepare succeeds"); + + let mut post = PostDispatchInfo::default(); + RateLimitTransactionExtension::::post_dispatch( + pre, + &info, + &mut post, + len, + &Ok(()), + ) + .expect("post_dispatch succeeds"); + + // Usage key should remain untouched because the call is read-only. + assert_eq!(LastSeen::::get(usage_target, Some(1u16)), Some(2)); + }); + } + #[test] fn tx_extension_respects_usage_group_sharing() { new_test_ext().execute_with(|| { From b4fef3223367aea77f2ad138bd5fa07d1fbc0143 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 1 Dec 2025 17:29:58 +0300 Subject: [PATCH 32/95] Fix staking ops rate limiting migration --- runtime/src/rate_limiting/migration.rs | 46 +++++++++++++++++++++++++- runtime/src/rate_limiting/mod.rs | 6 ++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index 54cc99d109..f02be4e422 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -70,6 +70,7 @@ const GROUP_WEIGHTS_SUBNET: GroupId = 2; const GROUP_WEIGHTS_MECHANISM: GroupId = 3; const GROUP_REGISTER_NETWORK: GroupId = 4; const GROUP_OWNER_HPARAMS: GroupId = 5; +const GROUP_STAKING_OPS: GroupId = 6; fn hyperparameter_identifiers() -> Vec { HYPERPARAMETERS @@ -126,6 +127,22 @@ fn group_definitions() -> Vec { sharing: GroupSharing::ConfigOnly, members: hyperparameter_identifiers(), }, + GroupDefinition { + id: GROUP_STAKING_OPS, + name: b"staking-ops", + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + subtensor_identifier(2), // add_stake + subtensor_identifier(88), // add_stake_limit + subtensor_identifier(3), // remove_stake + subtensor_identifier(89), // remove_stake_limit + subtensor_identifier(103), // remove_stake_full_limit + subtensor_identifier(85), // move_stake + subtensor_identifier(86), // transfer_stake + subtensor_identifier(87), // swap_stake + subtensor_identifier(90), // swap_stake_limit + ], + }, ] } @@ -166,6 +183,7 @@ struct GroupInfo { #[derive(Default)] struct Grouping { assignments: BTreeMap, + read_only: BTreeMap, members: BTreeMap>, details: Vec>>, next_group_id: GroupId, @@ -177,6 +195,10 @@ impl Grouping { self.members.get(&id) } + fn set_read_only(&mut self, id: TransactionIdentifier, read_only: bool) { + self.read_only.insert(id, read_only); + } + fn insert_group( &mut self, id: GroupId, @@ -188,6 +210,7 @@ impl Grouping { for member in members { self.assignments.insert(*member, GroupInfo { id, sharing }); entry.insert(*member); + self.read_only.entry(*member).or_insert(false); } self.details.push(RateLimitGroup { @@ -252,6 +275,15 @@ fn build_grouping() -> Grouping { ); } + // Mark staking operations that should not update usage after enforcement. + for readonly in [ + subtensor_identifier(3), + subtensor_identifier(89), + subtensor_identifier(103), + ] { + grouping.set_read_only(readonly, true); + } + grouping.finalize_next_id(); grouping } @@ -409,6 +441,13 @@ fn gather_simple_limits( ); } + // Staking operations use a 1-block lock shared by the group. + set_global_limit::( + limits, + grouping.config_target(subtensor_identifier(2)), + BlockNumberFor::::from(1u32), + ); + reads } @@ -761,6 +800,11 @@ where let group_id = info.id.saturated_into::>(); pallet_rate_limiting::CallGroups::::insert(*identifier, group_id); writes += 1; + + if grouping.read_only.get(identifier).copied().unwrap_or(false) { + pallet_rate_limiting::CallReadOnly::::insert(*identifier, true); + writes += 1; + } } let next_group_id = grouping.next_group_id.saturated_into::>(); @@ -1033,7 +1077,7 @@ mod tests { pallet_rate_limiting::CallGroups::::get(subtensor_identifier(66)), Some(DELEGATE_TAKE_GROUP_ID) ); - assert_eq!(pallet_rate_limiting::NextGroupId::::get(), 6); + assert_eq!(pallet_rate_limiting::NextGroupId::::get(), 7); }); } diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 2e5bc85d24..87a565b4c9 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -168,6 +168,8 @@ impl RateLimitUsageResolver { + } => { let coldkey = signed_origin(origin)?; Some(RateLimitUsageKey::::ColdkeyHotkeySubnet { coldkey, From cd091537eaed5ad2a196039213b8eba0c031beee Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 3 Dec 2025 16:42:49 +0300 Subject: [PATCH 33/95] Fix sudo_set_weights_version_key rate-limiting migration - fix scope resolver --- runtime/src/rate_limiting/migration.rs | 19 +++++++------------ runtime/src/rate_limiting/mod.rs | 19 ++++++++++++++++--- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index f02be4e422..e4a73d484d 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -968,23 +968,18 @@ where AccountId: Parameter + Clone, { match tx { - TransactionType::SetChildren | TransactionType::SetChildkeyTake => { - Some(RateLimitUsageKey::AccountSubnet { - account: account.clone(), - netuid, - }) - } - TransactionType::SetWeightsVersionKey => Some(RateLimitUsageKey::Subnet(netuid)), TransactionType::MechanismCountUpdate + | TransactionType::MaxUidsTrimming | TransactionType::MechanismEmission - | TransactionType::MaxUidsTrimming => Some(RateLimitUsageKey::AccountSubnet { + | TransactionType::SetChildkeyTake + | TransactionType::SetChildren + | TransactionType::SetWeightsVersionKey => Some(RateLimitUsageKey::AccountSubnet { account: account.clone(), netuid, }), - TransactionType::OwnerHyperparamUpdate(_) => Some(RateLimitUsageKey::Subnet(netuid)), - TransactionType::RegisterNetwork => None, - TransactionType::SetSNOwnerHotkey => Some(RateLimitUsageKey::Subnet(netuid)), - TransactionType::Unknown => None, + TransactionType::SetSNOwnerHotkey | TransactionType::OwnerHyperparamUpdate(_) => { + Some(RateLimitUsageKey::Subnet(netuid)) + } _ => None, } } diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 87a565b4c9..535b8e9b60 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -92,10 +92,24 @@ impl RateLimitScopeResolver for } let tempo = BlockNumber::from(Tempo::::get(netuid) as u32); span.saturating_mul(tempo) + } else if let AdminUtilsCall::sudo_set_weights_version_key { netuid, .. } = inner { + if span == 0 { + return span; + } + let tempo = BlockNumber::from(Tempo::::get(netuid) as u32); + span.saturating_mul(tempo) } else { span } } + RuntimeCall::SubtensorModule(inner) => match inner { + // Marker-only staking ops: allow but still record usage. + pallet_subtensor::Call::add_stake { .. } + | pallet_subtensor::Call::add_stake_limit { .. } => BlockNumber::from(0u32), + // Decrease take is marker-only; increase uses configured span. + pallet_subtensor::Call::decrease_take { .. } => BlockNumber::from(0u32), + _ => span, + }, _ => span, } } @@ -206,14 +220,14 @@ impl RateLimitUsageResolver { if let Some(netuid) = owner_hparam_netuid(inner) { - // Hyperparameter setters share a global span but are tracked per subnet. Some(RateLimitUsageKey::::Subnet(netuid)) } else { match inner { AdminUtilsCall::sudo_set_sn_owner_hotkey { netuid, .. } => { Some(RateLimitUsageKey::::Subnet(*netuid)) } - AdminUtilsCall::sudo_set_mechanism_count { netuid, .. } + AdminUtilsCall::sudo_set_weights_version_key { netuid, .. } + | AdminUtilsCall::sudo_set_mechanism_count { netuid, .. } | AdminUtilsCall::sudo_set_mechanism_emission_split { netuid, .. } | AdminUtilsCall::sudo_trim_to_max_allowed_uids { netuid, .. } => { let who = signed_origin(origin)?; @@ -269,7 +283,6 @@ fn owner_hparam_netuid(call: &AdminUtilsCall) -> Option { | AdminUtilsCall::sudo_set_rho { netuid, .. } | AdminUtilsCall::sudo_set_serving_rate_limit { netuid, .. } | AdminUtilsCall::sudo_set_toggle_transfer { netuid, .. } - | AdminUtilsCall::sudo_set_weights_version_key { netuid, .. } | AdminUtilsCall::sudo_set_yuma3_enabled { netuid, .. } => Some(*netuid), _ => None, } From 0d3a1d5d99ceeae0bb2798990abae8ad60cdd101 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 4 Dec 2025 16:15:50 +0300 Subject: [PATCH 34/95] Refactor rate-limiting migration --- pallets/rate-limiting/src/lib.rs | 4 +- runtime/src/rate_limiting/migration.rs | 565 +++++++++++++++++------- runtime/tests/rate_limiting_behavior.rs | 10 +- 3 files changed, 421 insertions(+), 158 deletions(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index af9149f8dd..8760ce7269 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -571,7 +571,9 @@ pub mod pallet { }) } - pub(crate) fn effective_span( + /// Resolves the span for a target/scope and applies the configured span adjustment + /// (e.g., tempo scaling) using the pallet's scope resolver. + pub fn effective_span( origin: &DispatchOriginOf<>::RuntimeCall>, call: &>::RuntimeCall, target: &RateLimitTarget<>::GroupId>, diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index e4a73d484d..47446650aa 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -12,8 +12,8 @@ use pallet_subtensor::{ self, AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, LastRateLimitedBlock, LastUpdate, MaxUidsTrimmingRateLimit, MechanismCountSetRateLimit, MechanismEmissionRateLimit, NetworkRateLimit, OwnerHyperparamRateLimit, Pallet, Prometheus, - RateLimitKey, TransactionKeyLastBlock, TxChildkeyTakeRateLimit, TxDelegateTakeRateLimit, - TxRateLimit, WeightsVersionKeyRateLimit, + RateLimitKey, ServingRateLimit, TransactionKeyLastBlock, TxChildkeyTakeRateLimit, + TxDelegateTakeRateLimit, TxRateLimit, WeightsVersionKeyRateLimit, utils::rate_limiting::{Hyperparameter, TransactionType}, }; use sp_runtime::traits::SaturatedConversion; @@ -24,9 +24,9 @@ use sp_std::{ }; use subtensor_runtime_common::NetUid; -use super::RateLimitUsageKey; +use super::{RateLimitUsageKey, Runtime}; -type GroupIdOf = ::GroupId; +type GroupId = ::GroupId; type LimitEntries = Vec<( RateLimitTarget, RateLimit>, @@ -47,22 +47,13 @@ const SUBTENSOR_PALLET_INDEX: u8 = 7; /// Pallet index assigned to `pallet_admin_utils` in `construct_runtime!`. const ADMIN_UTILS_PALLET_INDEX: u8 = 19; -/// Marker stored in `HasMigrationRun` once the migration finishes. +const SERVE_PROM_IDENTIFIER: TransactionIdentifier = subtensor_identifier(5); + +/// Marker stored in `pallet_subtensor::HasMigrationRun` once the migration finishes. const MIGRATION_NAME: &[u8] = b"migrate_rate_limiting"; -/// `set_children` is rate-limited to once every 150 blocks. +/// `set_children` is rate-limited to once every 150 blocks, it's hard-coded in the legacy code. const SET_CHILDREN_RATE_LIMIT: u64 = 150; -/// `set_sn_owner_hotkey` default interval (blocks). -const DEFAULT_SET_SN_OWNER_HOTKEY_LIMIT: u64 = 50_400; - -type GroupId = u32; - -struct GroupDefinition { - id: GroupId, - name: &'static [u8], - sharing: GroupSharing, - members: Vec, -} const GROUP_SERVE_AXON: GroupId = 0; const GROUP_DELEGATE_TAKE: GroupId = 1; @@ -72,80 +63,6 @@ const GROUP_REGISTER_NETWORK: GroupId = 4; const GROUP_OWNER_HPARAMS: GroupId = 5; const GROUP_STAKING_OPS: GroupId = 6; -fn hyperparameter_identifiers() -> Vec { - HYPERPARAMETERS - .iter() - .filter_map(|h| identifier_for_hyperparameter(*h)) - .collect() -} - -fn group_definitions() -> Vec { - vec![ - GroupDefinition { - id: GROUP_SERVE_AXON, - name: b"serve-axon", - sharing: GroupSharing::ConfigAndUsage, - members: vec![subtensor_identifier(4), subtensor_identifier(40)], - }, - GroupDefinition { - id: GROUP_DELEGATE_TAKE, - name: b"delegate-take", - sharing: GroupSharing::ConfigAndUsage, - members: vec![subtensor_identifier(66), subtensor_identifier(65)], - }, - GroupDefinition { - id: GROUP_WEIGHTS_SUBNET, - name: b"weights-subnet", - sharing: GroupSharing::ConfigAndUsage, - members: vec![ - subtensor_identifier(0), - subtensor_identifier(96), - subtensor_identifier(100), - subtensor_identifier(113), - ], - }, - GroupDefinition { - id: GROUP_WEIGHTS_MECHANISM, - name: b"weights-mechanism", - sharing: GroupSharing::ConfigAndUsage, - members: vec![ - subtensor_identifier(119), - subtensor_identifier(115), - subtensor_identifier(117), - subtensor_identifier(118), - ], - }, - GroupDefinition { - id: GROUP_REGISTER_NETWORK, - name: b"register-network", - sharing: GroupSharing::ConfigAndUsage, - members: vec![subtensor_identifier(59), subtensor_identifier(79)], - }, - GroupDefinition { - id: GROUP_OWNER_HPARAMS, - name: b"owner-hparams", - sharing: GroupSharing::ConfigOnly, - members: hyperparameter_identifiers(), - }, - GroupDefinition { - id: GROUP_STAKING_OPS, - name: b"staking-ops", - sharing: GroupSharing::ConfigAndUsage, - members: vec![ - subtensor_identifier(2), // add_stake - subtensor_identifier(88), // add_stake_limit - subtensor_identifier(3), // remove_stake - subtensor_identifier(89), // remove_stake_limit - subtensor_identifier(103), // remove_stake_full_limit - subtensor_identifier(85), // move_stake - subtensor_identifier(86), // transfer_stake - subtensor_identifier(87), // swap_stake - subtensor_identifier(90), // swap_stake_limit - ], - }, - ] -} - /// Hyperparameter extrinsics routed through owner-or-root rate limiting. const HYPERPARAMETERS: &[Hyperparameter] = &[ Hyperparameter::ServingRateLimit, @@ -174,6 +91,60 @@ const HYPERPARAMETERS: &[Hyperparameter] = &[ Hyperparameter::RecycleOrBurn, ]; +/// Identifies whether a rate-limited entry applies to a single call or a named group. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TargetKind { + Standalone(TransactionIdentifier), + Group { + id: GroupId, + name: Vec, + sharing: GroupSharing, + members: Vec, + }, +} + +/// Describes how a limit is scoped in storage. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LimitScopeKind { + Global, + Netuid, +} + +/// Describes the shape of the usage key recorded after a call executes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UsageKind { + None, + Account, + Subnet, + AccountSubnet, + ColdkeyHotkeySubnet, + SubnetNeuron, + SubnetMechanismNeuron, +} + +/// Human-friendly description of a rate-limited call or group. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RateLimitedCall { + pub target: TargetKind, + pub scope: LimitScopeKind, + pub usage: UsageKind, + /// Calls that should not record usage when dispatched (only relevant for groups). + pub read_only: Vec, + /// Legacy storage sources used by the migration. + pub legacy: LegacySources, +} + +/// Summarizes where legacy limits and last-seen data are sourced from. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LegacySources { + pub limits: &'static [&'static str], + pub last_seen: &'static [&'static str], +} + +fn sources(limits: &'static [&'static str], last_seen: &'static [&'static str]) -> LegacySources { + LegacySources { limits, last_seen } +} + #[derive(Clone, Copy)] struct GroupInfo { id: GroupId, @@ -245,8 +216,6 @@ impl Grouping { } } -const SERVE_PROM_IDENTIFIER: TransactionIdentifier = subtensor_identifier(5); - fn serve_calls(grouping: &Grouping) -> Vec { let mut calls = Vec::new(); if let Some(members) = grouping.members(GROUP_SERVE_AXON) { @@ -263,31 +232,276 @@ fn weight_calls_subnet(grouping: &Grouping) -> Vec { .unwrap_or_default() } +fn weight_calls_mechanism(grouping: &Grouping) -> Vec { + grouping + .members(GROUP_WEIGHTS_MECHANISM) + .map(|m| m.iter().copied().collect()) + .unwrap_or_default() +} + fn build_grouping() -> Grouping { let mut grouping = Grouping::default(); - for definition in group_definitions() { - grouping.insert_group( - definition.id, - definition.name, - definition.sharing, - &definition.members, - ); - } - - // Mark staking operations that should not update usage after enforcement. - for readonly in [ - subtensor_identifier(3), - subtensor_identifier(89), - subtensor_identifier(103), - ] { - grouping.set_read_only(readonly, true); + for entry in rate_limited_calls() { + match entry.target { + TargetKind::Group { + id, + name, + sharing, + members, + } => { + grouping.insert_group(id, &name, sharing, &members); + for member in members { + if entry.read_only.contains(&member) { + grouping.set_read_only(member, true); + } + } + } + TargetKind::Standalone(call) => { + if entry.read_only.contains(&call) { + grouping.set_read_only(call, true); + } + } + } } grouping.finalize_next_id(); grouping } +/// Returns a readable listing of all calls covered by the migration. +/// Each entry captures how the call is grouped, scoped, and which legacy +/// storage sources feed its limits and last-seen values. +pub fn rate_limited_calls() -> Vec { + vec![ + RateLimitedCall { + target: TargetKind::Group { + id: GROUP_SERVE_AXON, + name: b"serve-axon".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + subtensor_identifier(4), // serve_axon + subtensor_identifier(40), // serve_axon_tls + ], + }, + scope: LimitScopeKind::Netuid, + usage: UsageKind::AccountSubnet, + read_only: Vec::new(), + legacy: sources(&["ServingRateLimit (per netuid)"], &["Axons"]), + }, + RateLimitedCall { + target: TargetKind::Standalone(SERVE_PROM_IDENTIFIER), // serve_prometheus + scope: LimitScopeKind::Netuid, + usage: UsageKind::AccountSubnet, + read_only: Vec::new(), + legacy: sources(&["ServingRateLimit (per netuid)"], &["Prometheus"]), + }, + RateLimitedCall { + target: TargetKind::Group { + id: GROUP_DELEGATE_TAKE, + name: b"delegate-take".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + subtensor_identifier(66), // increase_take + subtensor_identifier(65), // decrease_take + ], + }, + scope: LimitScopeKind::Global, + usage: UsageKind::Account, + read_only: Vec::new(), + legacy: sources(&["TxDelegateTakeRateLimit"], &["LastTxBlockDelegateTake"]), + }, + RateLimitedCall { + target: TargetKind::Group { + id: GROUP_WEIGHTS_SUBNET, + name: b"weights-subnet".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + subtensor_identifier(0), // set_weights + subtensor_identifier(96), // commit_weights + subtensor_identifier(100), // batch_commit_weights + subtensor_identifier(113), // commit_timelocked_weights + subtensor_identifier(97), // reveal_weights + subtensor_identifier(98), // batch_reveal_weights + ], + }, + scope: LimitScopeKind::Netuid, + usage: UsageKind::SubnetNeuron, + read_only: Vec::new(), + legacy: sources(&["SubnetWeightsSetRateLimit"], &["LastUpdate (subnet)"]), + }, + RateLimitedCall { + target: TargetKind::Group { + id: GROUP_WEIGHTS_MECHANISM, + name: b"weights-mechanism".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + subtensor_identifier(119), // set_mechanism_weights + subtensor_identifier(115), // commit_mechanism_weights + subtensor_identifier(117), // commit_crv3_mechanism_weights + subtensor_identifier(118), // commit_timelocked_mechanism_weights + subtensor_identifier(116), // reveal_mechanism_weights + ], + }, + scope: LimitScopeKind::Netuid, + usage: UsageKind::SubnetMechanismNeuron, + read_only: Vec::new(), + legacy: sources(&["SubnetWeightsSetRateLimit"], &["LastUpdate (mechanism)"]), + }, + RateLimitedCall { + target: TargetKind::Group { + id: GROUP_REGISTER_NETWORK, + name: b"register-network".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + subtensor_identifier(59), // register_network + subtensor_identifier(79), // register_network_with_identity + ], + }, + scope: LimitScopeKind::Global, + usage: UsageKind::None, + read_only: Vec::new(), + legacy: sources(&["NetworkRateLimit"], &["NetworkLastRegistered"]), + }, + RateLimitedCall { + target: TargetKind::Group { + id: GROUP_OWNER_HPARAMS, + name: b"owner-hparams".to_vec(), + sharing: GroupSharing::ConfigOnly, + members: HYPERPARAMETERS + .iter() + .filter_map(|h| identifier_for_hyperparameter(*h)) + .collect(), + }, + scope: LimitScopeKind::Netuid, + usage: UsageKind::Subnet, + read_only: Vec::new(), + legacy: sources( + &["OwnerHyperparamRateLimit * tempo"], + &["LastRateLimitedBlock::OwnerHyperparamUpdate"], + ), + }, + RateLimitedCall { + target: TargetKind::Group { + id: GROUP_STAKING_OPS, + name: b"staking-ops".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + subtensor_identifier(2), // add_stake + subtensor_identifier(88), // add_stake_limit + subtensor_identifier(3), // remove_stake + subtensor_identifier(89), // remove_stake_limit + subtensor_identifier(103), // remove_stake_full_limit + subtensor_identifier(85), // move_stake + subtensor_identifier(86), // transfer_stake + subtensor_identifier(87), // swap_stake + subtensor_identifier(90), // swap_stake_limit + ], + }, + scope: LimitScopeKind::Global, + usage: UsageKind::ColdkeyHotkeySubnet, + read_only: vec![ + subtensor_identifier(3), // remove_stake + subtensor_identifier(89), // remove_stake_limit + subtensor_identifier(103), // remove_stake_full_limit + subtensor_identifier(86), // transfer_stake + subtensor_identifier(85), // move_stake + subtensor_identifier(87), // swap_stake + subtensor_identifier(90), // swap_stake_limit + ], + legacy: sources(&["TxRateLimit"], &[]), + }, + RateLimitedCall { + target: TargetKind::Standalone(subtensor_identifier(70)), // swap_hotkey + scope: LimitScopeKind::Global, + usage: UsageKind::Account, + read_only: Vec::new(), + legacy: sources(&["TxRateLimit"], &["LastRateLimitedBlock::LastTxBlock"]), + }, + RateLimitedCall { + target: TargetKind::Standalone(subtensor_identifier(75)), // set_childkey_take + scope: LimitScopeKind::Global, + usage: UsageKind::AccountSubnet, + read_only: Vec::new(), + legacy: sources(&["TxChildkeyTakeRateLimit"], &["TransactionKeyLastBlock::SetChildkeyTake"]), + }, + RateLimitedCall { + target: TargetKind::Standalone(subtensor_identifier(67)), // set_children + scope: LimitScopeKind::Global, + usage: UsageKind::AccountSubnet, + read_only: Vec::new(), + legacy: sources( + &["SET_CHILDREN_RATE_LIMIT (constant 150)"], + &["TransactionKeyLastBlock::SetChildren"], + ), + }, + RateLimitedCall { + // sudo_set_weights_version_key + target: TargetKind::Standalone(admin_utils_identifier(6)), + scope: LimitScopeKind::Netuid, + usage: UsageKind::AccountSubnet, + read_only: Vec::new(), + legacy: sources( + &["WeightsVersionKeyRateLimit * tempo"], + &["TransactionKeyLastBlock::SetWeightsVersionKey"], + ), + }, + RateLimitedCall { + // sudo_set_sn_owner_hotkey + target: TargetKind::Standalone(admin_utils_identifier(67)), + scope: LimitScopeKind::Global, + usage: UsageKind::Subnet, + read_only: Vec::new(), + legacy: sources( + &["DefaultSetSNOwnerHotkeyRateLimit"], + &["LastRateLimitedBlock::SetSNOwnerHotkey"], + ), + }, + RateLimitedCall { + target: TargetKind::Standalone(subtensor_identifier(93)), // associate_evm_key + scope: LimitScopeKind::Global, + usage: UsageKind::SubnetNeuron, + read_only: Vec::new(), + legacy: sources( + &["EvmKeyAssociateRateLimit"], + &["AssociatedEvmAddress"], + ), + }, + RateLimitedCall { + target: TargetKind::Standalone(admin_utils_identifier(76)), // sudo_set_mechanism_count + scope: LimitScopeKind::Global, + usage: UsageKind::AccountSubnet, + read_only: Vec::new(), + legacy: sources( + &["MechanismCountSetRateLimit"], + &["TransactionKeyLastBlock::MechanismCountUpdate"], + ), + }, + RateLimitedCall { + // sudo_set_mechanism_emission_split + target: TargetKind::Standalone(admin_utils_identifier(77)), + scope: LimitScopeKind::Global, + usage: UsageKind::AccountSubnet, + read_only: Vec::new(), + legacy: sources( + &["MechanismEmissionRateLimit"], + &["TransactionKeyLastBlock::MechanismEmission"], + ), + }, + RateLimitedCall { + // sudo_trim_to_max_allowed_uids + target: TargetKind::Standalone(admin_utils_identifier(78)), + scope: LimitScopeKind::Global, + usage: UsageKind::AccountSubnet, + read_only: Vec::new(), + legacy: sources( + &["MaxUidsTrimmingRateLimit"], + &["TransactionKeyLastBlock::MaxUidsTrimming"], + ), + }, + ] +} + pub fn migrate_rate_limiting() -> Weight where T: SubtensorConfig + pallet_rate_limiting::Config, @@ -330,21 +544,31 @@ where weight } +type LimitImporter = fn(&Grouping, &mut LimitEntries) -> u64; + +fn limit_importers() -> [LimitImporter; 4] { + [ + import_simple_limits::, // Tx/childkey/delegate/staking lock, register, sudo, evm, children + import_owner_hparam_limits::, // Owner hyperparams + import_serving_limits::, // Axon/prometheus serving rate limit per subnet + import_weight_limits::, // Weight/commit/reveal per subnet and mechanism + ] +} + fn build_limits(grouping: &Grouping) -> (LimitEntries, u64) { let mut limits = LimitEntries::::new(); let mut reads: u64 = 0; - reads += gather_simple_limits::(&mut limits, grouping); - reads += gather_owner_hparam_limits::(&mut limits, grouping); - reads += gather_serving_limits::(&mut limits, grouping); - reads += gather_weight_limits::(&mut limits, grouping); + for importer in limit_importers::() { + reads += importer(grouping, &mut limits); + } (limits, reads) } -fn gather_simple_limits( - limits: &mut LimitEntries, +fn import_simple_limits( grouping: &Grouping, + limits: &mut LimitEntries, ) -> u64 { let mut reads: u64 = 0; @@ -357,15 +581,24 @@ fn gather_simple_limits( ); } - reads += 1; - if let Some(span) = block_number::(TxDelegateTakeRateLimit::::get()) { - if let Some(members) = grouping.members(GROUP_DELEGATE_TAKE) { + // Share the TxRateLimit span across staking operations; add_* are marker-only via span tweaks. + if let Some(span) = block_number::(TxRateLimit::::get()) { + if let Some(members) = grouping.members(GROUP_STAKING_OPS) { for call in members { set_global_limit::(limits, grouping.config_target(*call), span); } } } + reads += 1; + if let Some(span) = block_number::(TxDelegateTakeRateLimit::::get()) { + set_global_limit::( + limits, + grouping.config_target(subtensor_identifier(66)), + span, + ); + } + reads += 1; if let Some(span) = block_number::(TxChildkeyTakeRateLimit::::get()) { set_global_limit::( @@ -384,7 +617,6 @@ fn gather_simple_limits( } } - reads += 1; if let Some(span) = block_number::(WeightsVersionKeyRateLimit::::get()) { set_global_limit::( limits, @@ -393,7 +625,9 @@ fn gather_simple_limits( ); } - if let Some(span) = block_number::(DEFAULT_SET_SN_OWNER_HOTKEY_LIMIT) { + if let Some(span) = + block_number::(pallet_subtensor::pallet::DefaultSetSNOwnerHotkeyRateLimit::::get()) + { set_global_limit::( limits, grouping.config_target(admin_utils_identifier(67)), @@ -441,19 +675,12 @@ fn gather_simple_limits( ); } - // Staking operations use a 1-block lock shared by the group. - set_global_limit::( - limits, - grouping.config_target(subtensor_identifier(2)), - BlockNumberFor::::from(1u32), - ); - reads } -fn gather_owner_hparam_limits( - limits: &mut LimitEntries, +fn import_owner_hparam_limits( grouping: &Grouping, + limits: &mut LimitEntries, ) -> u64 { let mut reads: u64 = 0; @@ -469,12 +696,17 @@ fn gather_owner_hparam_limits( reads } -fn gather_serving_limits( - limits: &mut LimitEntries, +fn import_serving_limits( grouping: &Grouping, + limits: &mut LimitEntries, ) -> u64 { let mut reads: u64 = 0; - let netuids = Pallet::::get_all_subnet_netuids(); + let mut netuids = Pallet::::get_all_subnet_netuids(); + for (netuid, _) in ServingRateLimit::::iter() { + if !netuids.contains(&netuid) { + netuids.push(netuid); + } + } for netuid in netuids { reads += 1; @@ -488,20 +720,24 @@ fn gather_serving_limits( reads } -fn gather_weight_limits( - limits: &mut LimitEntries, +fn import_weight_limits( grouping: &Grouping, + limits: &mut LimitEntries, ) -> u64 { let mut reads: u64 = 0; let netuids = Pallet::::get_all_subnet_netuids(); let subnet_calls = weight_calls_subnet(grouping); + let mechanism_calls = weight_calls_mechanism(grouping); for netuid in &netuids { reads += 1; if let Some(span) = block_number::(Pallet::::get_weights_set_rate_limit(*netuid)) { for call in &subnet_calls { set_scoped_limit::(limits, grouping.config_target(*call), *netuid, span); } + for call in &mechanism_calls { + set_scoped_limit::(limits, grouping.config_target(*call), *netuid, span); + } } } @@ -512,18 +748,28 @@ fn build_last_seen(grouping: &Grouping) -> (LastSeenEntries< let mut last_seen = LastSeenEntries::::new(); let mut reads: u64 = 0; - reads += import_last_rate_limited_blocks::(&mut last_seen, grouping); - reads += import_transaction_key_last_blocks::(&mut last_seen, grouping); - reads += import_last_update_entries::(&mut last_seen, grouping); - reads += import_serving_entries::(&mut last_seen, grouping); - reads += import_evm_entries::(&mut last_seen, grouping); + for importer in last_seen_importers::() { + reads += importer(grouping, &mut last_seen); + } (last_seen, reads) } +type LastSeenImporter = fn(&Grouping, &mut LastSeenEntries) -> u64; + +fn last_seen_importers() -> [LastSeenImporter; 5] { + [ + import_last_rate_limited_blocks::, // LastRateLimitedBlock (tx, delegate, owner hyperparams, sn owner) + import_transaction_key_last_blocks::, // TransactionKeyLastBlock (children, version key, mechanisms) + import_last_update_entries::, // LastUpdate (weights/mechanism weights) + import_serving_entries::, // Axons/Prometheus + import_evm_entries::, // AssociatedEvmAddress + ] +} + fn import_last_rate_limited_blocks( - entries: &mut LastSeenEntries, grouping: &Grouping, + entries: &mut LastSeenEntries, ) -> u64 { let mut reads: u64 = 0; for (key, block) in LastRateLimitedBlock::::iter() { @@ -587,8 +833,8 @@ fn import_last_rate_limited_blocks( } fn import_transaction_key_last_blocks( - entries: &mut LastSeenEntries, grouping: &Grouping, + entries: &mut LastSeenEntries, ) -> u64 { let mut reads: u64 = 0; for ((account, netuid, tx_kind), block) in TransactionKeyLastBlock::::iter() { @@ -614,14 +860,19 @@ fn import_transaction_key_last_blocks( } fn import_last_update_entries( - entries: &mut LastSeenEntries, grouping: &Grouping, + entries: &mut LastSeenEntries, ) -> u64 { let mut reads: u64 = 0; - let subnet_calls = weight_calls_subnet(grouping); for (index, blocks) in LastUpdate::::iter() { reads += 1; - let netuid = Pallet::::get_netuid(index); + let (netuid, mecid) = Pallet::::get_netuid_and_subid(index) + .unwrap_or((NetUid::ROOT, 0.into())); + let subnet_calls = if mecid == 0.into() { + weight_calls_subnet(grouping) + } else { + weight_calls_mechanism(grouping) + }; for (uid, last_block) in blocks.into_iter().enumerate() { if last_block == 0 { @@ -630,9 +881,14 @@ fn import_last_update_entries( let Ok(uid_u16) = u16::try_from(uid) else { continue; }; - let usage = RateLimitUsageKey::SubnetNeuron { - netuid, - uid: uid_u16, + let usage = if mecid == 0.into() { + RateLimitUsageKey::SubnetNeuron { netuid, uid: uid_u16 } + } else { + RateLimitUsageKey::SubnetMechanismNeuron { + netuid, + mecid, + uid: uid_u16, + } }; for call in &subnet_calls { @@ -649,8 +905,8 @@ fn import_last_update_entries( } fn import_serving_entries( - entries: &mut LastSeenEntries, grouping: &Grouping, + entries: &mut LastSeenEntries, ) -> u64 { let mut reads: u64 = 0; for (netuid, hotkey, axon) in Axons::::iter() { @@ -697,8 +953,8 @@ fn import_serving_entries( } fn import_evm_entries( - entries: &mut LastSeenEntries, grouping: &Grouping, + entries: &mut LastSeenEntries, ) -> u64 { let mut reads: u64 = 0; for (netuid, uid, (_, block)) in AssociatedEvmAddress::::iter() { @@ -716,7 +972,7 @@ fn import_evm_entries( reads } -fn convert_target(target: &RateLimitTarget) -> RateLimitTarget> +fn convert_target(target: &RateLimitTarget) -> RateLimitTarget where T: SubtensorConfig + pallet_rate_limiting::Config, RateLimitUsageKey: Into<::UsageKey>, @@ -771,7 +1027,7 @@ where ); continue; }; - let group_id = detail.id.saturated_into::>(); + let group_id = detail.id.saturated_into::(); let stored = RateLimitGroup { id: group_id, name: name.clone(), @@ -784,7 +1040,7 @@ where } for (group, members) in &grouping.members { - let group_id = (*group).saturated_into::>(); + let group_id = (*group).saturated_into::(); let Ok(bounded) = GroupMembersOf::::try_from(members.clone()) else { warn!( "rate-limiting migration: group {} has too many members, skipping assignment", @@ -797,7 +1053,7 @@ where } for (identifier, info) in &grouping.assignments { - let group_id = info.id.saturated_into::>(); + let group_id = info.id.saturated_into::(); pallet_rate_limiting::CallGroups::::insert(*identifier, group_id); writes += 1; @@ -807,7 +1063,7 @@ where } } - let next_group_id = grouping.next_group_id.saturated_into::>(); + let next_group_id = grouping.next_group_id.saturated_into::(); pallet_rate_limiting::NextGroupId::::put(next_group_id); writes += 1; @@ -905,7 +1161,6 @@ pub fn identifier_for_hyperparameter(hparam: Hyperparameter) -> Option return None, ServingRateLimit => admin_utils_identifier(3), MaxDifficulty => admin_utils_identifier(5), AdjustmentAlpha => admin_utils_identifier(9), diff --git a/runtime/tests/rate_limiting_behavior.rs b/runtime/tests/rate_limiting_behavior.rs index c513409db7..557fcdd63f 100644 --- a/runtime/tests/rate_limiting_behavior.rs +++ b/runtime/tests/rate_limiting_behavior.rs @@ -87,8 +87,14 @@ fn parity_check( .or_else(|| RuntimeUsageResolver::context(&origin, &call).map(Into::into)); let target = resolve_target(identifier); - let span = pallet_rate_limiting::Pallet::::resolved_limit(&target, &scope) - .unwrap_or_default(); + // Use the runtime-adjusted span (handles tempo scaling for admin-utils). + let span = pallet_rate_limiting::Pallet::::effective_span( + &origin.clone().into(), + &call, + &target, + &scope, + ) + .unwrap_or_default(); let span_u64: u64 = span.saturated_into(); let within = pallet_rate_limiting::Pallet::::is_within_limit( From 09ba8786c1bf06a4aa9f2744c18ff481daf1d8e6 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 4 Dec 2025 18:58:16 +0300 Subject: [PATCH 35/95] Introduce BypassDesicion in pallet-rate-limiting - fix bypassing in pallet-rate-limiting - fix conditional bypassing for legacy rate-limited calls --- pallets/rate-limiting/src/lib.rs | 30 +++++++---- pallets/rate-limiting/src/mock.rs | 18 +++++-- pallets/rate-limiting/src/tests.rs | 19 +++---- pallets/rate-limiting/src/tx_extension.rs | 66 +++++++++++++++++++++-- pallets/rate-limiting/src/types.rs | 35 ++++++++++-- runtime/src/rate_limiting/migration.rs | 41 +++++++------- runtime/src/rate_limiting/mod.rs | 42 +++++++++++---- 7 files changed, 188 insertions(+), 63 deletions(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 8760ce7269..c93397c425 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -97,8 +97,12 @@ //! } //! } //! -//! fn should_bypass(origin: &RuntimeOrigin, _call: &RuntimeCall) -> bool { -//! matches!(origin, RuntimeOrigin::Root) +//! fn should_bypass(origin: &RuntimeOrigin, _call: &RuntimeCall) -> BypassDecision { +//! if matches!(origin, RuntimeOrigin::Root) { +//! BypassDecision::bypass_and_skip() +//! } else { +//! BypassDecision::enforce_and_record() +//! } //! } //! //! fn adjust_span(_origin: &RuntimeOrigin, _call: &RuntimeCall, span: BlockNumber) -> BlockNumber { @@ -140,7 +144,7 @@ pub use benchmarking::BenchmarkHelper; pub use pallet::*; pub use tx_extension::RateLimitTransactionExtension; pub use types::{ - GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitScopeResolver, + BypassDecision, GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitScopeResolver, RateLimitTarget, RateLimitUsageResolver, TransactionIdentifier, }; @@ -172,8 +176,8 @@ pub mod pallet { #[cfg(feature = "runtime-benchmarks")] use crate::benchmarking::BenchmarkHelper as BenchmarkHelperTrait; use crate::types::{ - GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitScopeResolver, - RateLimitTarget, RateLimitUsageResolver, TransactionIdentifier, + BypassDecision, GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, + RateLimitScopeResolver, RateLimitTarget, RateLimitUsageResolver, TransactionIdentifier, }; type GroupNameOf = BoundedVec>::MaxGroupNameLength>; @@ -542,7 +546,9 @@ pub mod pallet { scope: &Option<>::LimitScope>, usage_key: &Option<>::UsageKey>, ) -> Result { - if >::LimitScopeResolver::should_bypass(origin, call) { + let bypass: BypassDecision = + >::LimitScopeResolver::should_bypass(origin, call); + if bypass.bypass_enforcement { return Ok(true); } @@ -571,8 +577,8 @@ pub mod pallet { }) } - /// Resolves the span for a target/scope and applies the configured span adjustment - /// (e.g., tempo scaling) using the pallet's scope resolver. + /// Resolves the span for a target/scope and applies the configured span adjustment (e.g., + /// tempo scaling) using the pallet's scope resolver. pub fn effective_span( origin: &DispatchOriginOf<>::RuntimeCall>, call: &>::RuntimeCall, @@ -594,7 +600,7 @@ pub mod pallet { return true; } - if let Some(last) = LastSeen::::get(target, usage_key.clone()) { + if let Some(last) = LastSeen::::get(target, usage_key) { let current = frame_system::Pallet::::block_number(); let delta = current.saturating_sub(last); if delta < block_span { @@ -753,6 +759,12 @@ pub mod pallet { Ok(()) } + /// Returns true when the call has been registered (either directly or via a group). + pub fn is_registered(identifier: &TransactionIdentifier) -> bool { + let tx_target = RateLimitTarget::Transaction(*identifier); + Limits::::contains_key(tx_target) || CallGroups::::contains_key(identifier) + } + fn call_metadata( identifier: &TransactionIdentifier, ) -> Result<(Vec, Vec), DispatchError> { diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index b643dec64d..a4aefd4357 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -75,11 +75,19 @@ impl pallet_rate_limiting::RateLimitScopeResolver bool { - matches!( - call, - RuntimeCall::RateLimiting(RateLimitingCall::remove_call_from_group { .. }) - ) + fn should_bypass( + _origin: &RuntimeOrigin, + call: &RuntimeCall, + ) -> pallet_rate_limiting::types::BypassDecision { + match call { + RuntimeCall::RateLimiting(RateLimitingCall::remove_call_from_group { .. }) => { + pallet_rate_limiting::types::BypassDecision::bypass_and_skip() + } + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { .. }) => { + pallet_rate_limiting::types::BypassDecision::bypass_and_record() + } + _ => pallet_rate_limiting::types::BypassDecision::enforce_and_record(), + } } fn adjust_span(_origin: &RuntimeOrigin, call: &RuntimeCall, span: u64) -> u64 { diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index 1b4fc170b4..ecad8a5da8 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -16,7 +16,11 @@ fn remark_call() -> RuntimeCall { } fn scoped_call() -> RuntimeCall { - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 1 }) + RuntimeCall::RateLimiting(RateLimitingCall::set_rate_limit { + target: RateLimitTarget::Transaction(TransactionIdentifier::new(0, 0)), + scope: Some(1), + limit: RateLimitKind::Default, + }) } fn register(call: RuntimeCall, group: Option) -> TransactionIdentifier { @@ -577,7 +581,7 @@ fn is_within_limit_detects_rate_limited_scope() { let tx_target = target(identifier); Limits::::insert( tx_target, - RateLimit::scoped_single(7u16, RateLimitKind::Exact(3)), + RateLimit::scoped_single(1u16, RateLimitKind::Exact(3)), ); LastSeen::::insert(tx_target, Some(1u16), 9); System::set_block_number(11); @@ -585,7 +589,7 @@ fn is_within_limit_detects_rate_limited_scope() { &RuntimeOrigin::signed(1), &call, &identifier, - &Some(7u16), + &Some(1u16), &Some(1u16), ) .expect("ok"); @@ -714,12 +718,9 @@ fn limit_for_call_names_prefers_scoped_value() { target(identifier), RateLimit::scoped_single(9u16, RateLimitKind::Exact(8)), ); - let fetched = RateLimiting::limit_for_call_names( - "RateLimiting", - "set_default_rate_limit", - Some(9u16), - ) - .expect("limit"); + let fetched = + RateLimiting::limit_for_call_names("RateLimiting", "set_rate_limit", Some(9u16)) + .expect("limit"); assert_eq!(fetched, RateLimitKind::Exact(8)); }); } diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index e1ffd4f14f..42737e6fd4 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -117,15 +117,15 @@ where _inherited_implication: &impl Implication, _source: TransactionSource, ) -> ValidateResult>::RuntimeCall> { - if >::LimitScopeResolver::should_bypass(&origin, call) { - return Ok((ValidTransaction::default(), None, origin)); - } - let identifier = match TransactionIdentifier::from_call::(call) { Ok(identifier) => identifier, Err(_) => return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), }; + if !Pallet::::is_registered(&identifier) { + return Ok((ValidTransaction::default(), None, origin)); + } + let scope = >::LimitScopeResolver::context(&origin, call); let usage = >::UsageResolver::context(&origin, call); @@ -133,7 +133,9 @@ where .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; let usage_target = Pallet::::usage_target(&identifier) .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; - let should_record = Pallet::::should_record_usage(&identifier, &usage_target); + let bypass = >::LimitScopeResolver::should_bypass(&origin, call); + let should_record = + bypass.record_usage && Pallet::::should_record_usage(&identifier, &usage_target); let Some(block_span) = Pallet::::effective_span(&origin, call, &config_target, &scope) @@ -141,6 +143,14 @@ where return Ok((ValidTransaction::default(), None, origin)); }; + if bypass.bypass_enforcement { + return Ok(( + ValidTransaction::default(), + should_record.then_some((usage_target, usage, true)), + origin, + )); + } + if block_span.is_zero() { return Ok((ValidTransaction::default(), None, origin)); } @@ -340,6 +350,52 @@ mod tests { }); } + #[test] + fn tx_extension_records_usage_on_bypass() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + let call = RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { + block_span: 2, + }); + let identifier = identifier_for(&call); + let target = RateLimitTarget::Transaction(identifier); + + assert_ok!(RateLimiting::register_call( + RuntimeOrigin::root(), + Box::new(call.clone()), + None, + )); + + System::set_block_number(5); + + let (_valid, val, origin) = + validate_with_tx_extension(&extension, &call).expect("bypass should succeed"); + assert!(val.is_some(), "bypass decision should still record usage"); + + let info = call.get_dispatch_info(); + let len = call.encode().len(); + let pre = extension + .clone() + .prepare(val.clone(), &origin, &call, &info, len) + .expect("prepare succeeds"); + + let mut post = PostDispatchInfo::default(); + RateLimitTransactionExtension::::post_dispatch( + pre, + &info, + &mut post, + len, + &Ok(()), + ) + .expect("post_dispatch succeeds"); + + assert_eq!( + LastSeen::::get(target, Some(2u16)), + Some(5u64.into()) + ); + }); + } + #[test] fn tx_extension_records_last_seen_for_successful_call() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 1faff7c300..deb27598ec 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -11,10 +11,9 @@ pub trait RateLimitScopeResolver { /// limits. fn context(origin: &Origin, call: &Call) -> Option; - /// Returns `true` when the rate limit should be bypassed for the provided origin/call pair. - /// Defaults to `false`. - fn should_bypass(_origin: &Origin, _call: &Call) -> bool { - false + /// Returns how the call should interact with enforcement and usage tracking. + fn should_bypass(_origin: &Origin, _call: &Call) -> BypassDecision { + BypassDecision::enforce_and_record() } /// Optionally adjusts the effective span used during enforcement. Defaults to the original @@ -24,6 +23,34 @@ pub trait RateLimitScopeResolver { } } +/// Controls whether enforcement should run and whether usage should be recorded for a call. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BypassDecision { + pub bypass_enforcement: bool, + pub record_usage: bool, +} + +impl BypassDecision { + pub const fn new(bypass_enforcement: bool, record_usage: bool) -> Self { + Self { + bypass_enforcement, + record_usage, + } + } + + pub const fn enforce_and_record() -> Self { + Self::new(false, true) + } + + pub const fn bypass_and_record() -> Self { + Self::new(true, true) + } + + pub const fn bypass_and_skip() -> Self { + Self::new(true, false) + } +} + /// Resolves the optional usage tracking key applied when enforcing limits. pub trait RateLimitUsageResolver { /// Returns `Some(usage)` when usage should be tracked per-key, or `None` for global usage diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index 47446650aa..9879097561 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -404,10 +404,6 @@ pub fn rate_limited_calls() -> Vec { subtensor_identifier(3), // remove_stake subtensor_identifier(89), // remove_stake_limit subtensor_identifier(103), // remove_stake_full_limit - subtensor_identifier(86), // transfer_stake - subtensor_identifier(85), // move_stake - subtensor_identifier(87), // swap_stake - subtensor_identifier(90), // swap_stake_limit ], legacy: sources(&["TxRateLimit"], &[]), }, @@ -423,7 +419,10 @@ pub fn rate_limited_calls() -> Vec { scope: LimitScopeKind::Global, usage: UsageKind::AccountSubnet, read_only: Vec::new(), - legacy: sources(&["TxChildkeyTakeRateLimit"], &["TransactionKeyLastBlock::SetChildkeyTake"]), + legacy: sources( + &["TxChildkeyTakeRateLimit"], + &["TransactionKeyLastBlock::SetChildkeyTake"], + ), }, RateLimitedCall { target: TargetKind::Standalone(subtensor_identifier(67)), // set_children @@ -462,10 +461,7 @@ pub fn rate_limited_calls() -> Vec { scope: LimitScopeKind::Global, usage: UsageKind::SubnetNeuron, read_only: Vec::new(), - legacy: sources( - &["EvmKeyAssociateRateLimit"], - &["AssociatedEvmAddress"], - ), + legacy: sources(&["EvmKeyAssociateRateLimit"], &["AssociatedEvmAddress"]), }, RateLimitedCall { target: TargetKind::Standalone(admin_utils_identifier(76)), // sudo_set_mechanism_count @@ -548,10 +544,10 @@ type LimitImporter = fn(&Grouping, &mut LimitEntries) -> u64; fn limit_importers() -> [LimitImporter; 4] { [ - import_simple_limits::, // Tx/childkey/delegate/staking lock, register, sudo, evm, children + import_simple_limits::, // Tx/childkey/delegate/staking lock, register, sudo, evm, children import_owner_hparam_limits::, // Owner hyperparams - import_serving_limits::, // Axon/prometheus serving rate limit per subnet - import_weight_limits::, // Weight/commit/reveal per subnet and mechanism + import_serving_limits::, // Axon/prometheus serving rate limit per subnet + import_weight_limits::, // Weight/commit/reveal per subnet and mechanism ] } @@ -581,9 +577,9 @@ fn import_simple_limits( ); } - // Share the TxRateLimit span across staking operations; add_* are marker-only via span tweaks. - if let Some(span) = block_number::(TxRateLimit::::get()) { - if let Some(members) = grouping.members(GROUP_STAKING_OPS) { + // Staking ops are gated to one operation per block in legacy (marker cleared each block). + if let Some(members) = grouping.members(GROUP_STAKING_OPS) { + if let Some(span) = block_number::(1) { for call in members { set_global_limit::(limits, grouping.config_target(*call), span); } @@ -761,9 +757,9 @@ fn last_seen_importers() -> [LastSeenImporter; 5] { [ import_last_rate_limited_blocks::, // LastRateLimitedBlock (tx, delegate, owner hyperparams, sn owner) import_transaction_key_last_blocks::, // TransactionKeyLastBlock (children, version key, mechanisms) - import_last_update_entries::, // LastUpdate (weights/mechanism weights) - import_serving_entries::, // Axons/Prometheus - import_evm_entries::, // AssociatedEvmAddress + import_last_update_entries::, // LastUpdate (weights/mechanism weights) + import_serving_entries::, // Axons/Prometheus + import_evm_entries::, // AssociatedEvmAddress ] } @@ -866,8 +862,8 @@ fn import_last_update_entries( let mut reads: u64 = 0; for (index, blocks) in LastUpdate::::iter() { reads += 1; - let (netuid, mecid) = Pallet::::get_netuid_and_subid(index) - .unwrap_or((NetUid::ROOT, 0.into())); + let (netuid, mecid) = + Pallet::::get_netuid_and_subid(index).unwrap_or((NetUid::ROOT, 0.into())); let subnet_calls = if mecid == 0.into() { weight_calls_subnet(grouping) } else { @@ -882,7 +878,10 @@ fn import_last_update_entries( continue; }; let usage = if mecid == 0.into() { - RateLimitUsageKey::SubnetNeuron { netuid, uid: uid_u16 } + RateLimitUsageKey::SubnetNeuron { + netuid, + uid: uid_u16, + } } else { RateLimitUsageKey::SubnetMechanismNeuron { netuid, diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 535b8e9b60..06b7dbcf3e 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -2,6 +2,7 @@ use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use frame_support::pallet_prelude::Parameter; use frame_system::RawOrigin; use pallet_admin_utils::Call as AdminUtilsCall; +use pallet_rate_limiting::BypassDecision; use pallet_rate_limiting::{RateLimitScopeResolver, RateLimitUsageResolver}; use pallet_subtensor::{Call as SubtensorCall, Tempo}; use scale_info::TypeInfo; @@ -79,8 +80,37 @@ impl RateLimitScopeResolver for } } - fn should_bypass(origin: &RuntimeOrigin, _call: &RuntimeCall) -> bool { - matches!(origin.clone().into(), Ok(RawOrigin::Root)) + fn should_bypass(origin: &RuntimeOrigin, call: &RuntimeCall) -> BypassDecision { + if matches!(origin.clone().into(), Ok(RawOrigin::Root)) { + return BypassDecision::bypass_and_skip(); + } + + if let RuntimeCall::SubtensorModule(inner) = call { + match inner { + SubtensorCall::set_childkey_take { + hotkey, + netuid, + take, + .. + } => { + let current = + pallet_subtensor::Pallet::::get_childkey_take(hotkey, *netuid); + return if *take <= current { + BypassDecision::bypass_and_record() + } else { + BypassDecision::enforce_and_record() + }; + } + SubtensorCall::add_stake { .. } + | SubtensorCall::add_stake_limit { .. } + | SubtensorCall::decrease_take { .. } => { + return BypassDecision::bypass_and_record(); + } + _ => {} + } + } + + BypassDecision::enforce_and_record() } fn adjust_span(_origin: &RuntimeOrigin, call: &RuntimeCall, span: BlockNumber) -> BlockNumber { @@ -102,14 +132,6 @@ impl RateLimitScopeResolver for span } } - RuntimeCall::SubtensorModule(inner) => match inner { - // Marker-only staking ops: allow but still record usage. - pallet_subtensor::Call::add_stake { .. } - | pallet_subtensor::Call::add_stake_limit { .. } => BlockNumber::from(0u32), - // Decrease take is marker-only; increase uses configured span. - pallet_subtensor::Call::decrease_take { .. } => BlockNumber::from(0u32), - _ => span, - }, _ => span, } } From 95720cb5ba48cd1fce4023feb33531557ba8bcc8 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 5 Dec 2025 15:36:36 +0300 Subject: [PATCH 36/95] Fix reveal-weights bypassing and transfer stake ops rate-limiting - fix weights grouping (reflect the legacy behavior for config share) - fix serving calls should share config --- runtime/src/rate_limiting/migration.rs | 177 +++++++++++------------- runtime/src/rate_limiting/mod.rs | 50 ++++++- runtime/tests/rate_limiting_behavior.rs | 1 + 3 files changed, 127 insertions(+), 101 deletions(-) diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index 9879097561..be504985c7 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -42,28 +42,27 @@ type GroupNameOf = BoundedVec::MaxGro type GroupMembersOf = BoundedBTreeSet::MaxGroupMembers>; -/// Pallet index assigned to `pallet_subtensor` in `construct_runtime!`. +// Pallet index assigned to `pallet_subtensor` in `construct_runtime!`. const SUBTENSOR_PALLET_INDEX: u8 = 7; -/// Pallet index assigned to `pallet_admin_utils` in `construct_runtime!`. +// Pallet index assigned to `pallet_admin_utils` in `construct_runtime!`. const ADMIN_UTILS_PALLET_INDEX: u8 = 19; const SERVE_PROM_IDENTIFIER: TransactionIdentifier = subtensor_identifier(5); -/// Marker stored in `pallet_subtensor::HasMigrationRun` once the migration finishes. +// Marker stored in `pallet_subtensor::HasMigrationRun` once the migration finishes. const MIGRATION_NAME: &[u8] = b"migrate_rate_limiting"; -/// `set_children` is rate-limited to once every 150 blocks, it's hard-coded in the legacy code. +// `set_children` is rate-limited to once every 150 blocks, it's hard-coded in the legacy code. const SET_CHILDREN_RATE_LIMIT: u64 = 150; -const GROUP_SERVE_AXON: GroupId = 0; +const GROUP_SERVE: GroupId = 0; const GROUP_DELEGATE_TAKE: GroupId = 1; const GROUP_WEIGHTS_SUBNET: GroupId = 2; -const GROUP_WEIGHTS_MECHANISM: GroupId = 3; -const GROUP_REGISTER_NETWORK: GroupId = 4; -const GROUP_OWNER_HPARAMS: GroupId = 5; -const GROUP_STAKING_OPS: GroupId = 6; +const GROUP_REGISTER_NETWORK: GroupId = 3; +const GROUP_OWNER_HPARAMS: GroupId = 4; +const GROUP_STAKING_OPS: GroupId = 5; -/// Hyperparameter extrinsics routed through owner-or-root rate limiting. +// Hyperparameter extrinsics routed through owner-or-root rate limiting. const HYPERPARAMETERS: &[Hyperparameter] = &[ Hyperparameter::ServingRateLimit, Hyperparameter::MaxDifficulty, @@ -91,6 +90,23 @@ const HYPERPARAMETERS: &[Hyperparameter] = &[ Hyperparameter::RecycleOrBurn, ]; +const WEIGHT_CALLS_SUBNET: [TransactionIdentifier; 6] = [ + subtensor_identifier(0), // set_weights + subtensor_identifier(96), // commit_weights + subtensor_identifier(100), // batch_commit_weights + subtensor_identifier(113), // commit_timelocked_weights + subtensor_identifier(97), // reveal_weights + subtensor_identifier(98), // batch_reveal_weights +]; + +const WEIGHT_CALLS_MECHANISM: [TransactionIdentifier; 5] = [ + subtensor_identifier(119), // set_mechanism_weights + subtensor_identifier(115), // commit_mechanism_weights + subtensor_identifier(117), // commit_crv3_mechanism_weights + subtensor_identifier(118), // commit_timelocked_mechanism_weights + subtensor_identifier(116), // reveal_mechanism_weights +]; + /// Identifies whether a rate-limited entry applies to a single call or a named group. #[derive(Clone, Debug, PartialEq, Eq)] pub enum TargetKind { @@ -113,10 +129,11 @@ pub enum LimitScopeKind { /// Describes the shape of the usage key recorded after a call executes. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum UsageKind { - None, Account, Subnet, AccountSubnet, + AccountSubnetAxon, + AccountSubnetPrometheus, ColdkeyHotkeySubnet, SubnetNeuron, SubnetMechanismNeuron, @@ -127,7 +144,8 @@ pub enum UsageKind { pub struct RateLimitedCall { pub target: TargetKind, pub scope: LimitScopeKind, - pub usage: UsageKind, + /// One or more usage shapes this call maps to in the legacy pallet. + pub usage: Vec, /// Calls that should not record usage when dispatched (only relevant for groups). pub read_only: Vec, /// Legacy storage sources used by the migration. @@ -217,24 +235,8 @@ impl Grouping { } fn serve_calls(grouping: &Grouping) -> Vec { - let mut calls = Vec::new(); - if let Some(members) = grouping.members(GROUP_SERVE_AXON) { - calls.extend(members.iter().copied()); - } - calls.push(SERVE_PROM_IDENTIFIER); - calls -} - -fn weight_calls_subnet(grouping: &Grouping) -> Vec { - grouping - .members(GROUP_WEIGHTS_SUBNET) - .map(|m| m.iter().copied().collect()) - .unwrap_or_default() -} - -fn weight_calls_mechanism(grouping: &Grouping) -> Vec { grouping - .members(GROUP_WEIGHTS_MECHANISM) + .members(GROUP_SERVE) .map(|m| m.iter().copied().collect()) .unwrap_or_default() } @@ -270,31 +272,28 @@ fn build_grouping() -> Grouping { } /// Returns a readable listing of all calls covered by the migration. -/// Each entry captures how the call is grouped, scoped, and which legacy -/// storage sources feed its limits and last-seen values. +/// Each entry captures how the call is grouped, scoped, and which legacy storage sources feed its +/// limits and last-seen values. pub fn rate_limited_calls() -> Vec { vec![ RateLimitedCall { target: TargetKind::Group { - id: GROUP_SERVE_AXON, - name: b"serve-axon".to_vec(), + id: GROUP_SERVE, + name: b"serving".to_vec(), sharing: GroupSharing::ConfigAndUsage, members: vec![ subtensor_identifier(4), // serve_axon subtensor_identifier(40), // serve_axon_tls + SERVE_PROM_IDENTIFIER, // serve_prometheus ], }, scope: LimitScopeKind::Netuid, - usage: UsageKind::AccountSubnet, - read_only: Vec::new(), - legacy: sources(&["ServingRateLimit (per netuid)"], &["Axons"]), - }, - RateLimitedCall { - target: TargetKind::Standalone(SERVE_PROM_IDENTIFIER), // serve_prometheus - scope: LimitScopeKind::Netuid, - usage: UsageKind::AccountSubnet, + usage: vec![ + UsageKind::AccountSubnetAxon, + UsageKind::AccountSubnetPrometheus, + ], read_only: Vec::new(), - legacy: sources(&["ServingRateLimit (per netuid)"], &["Prometheus"]), + legacy: sources(&["ServingRateLimit (per netuid)"], &["Axons/Prometheus"]), }, RateLimitedCall { target: TargetKind::Group { @@ -307,46 +306,27 @@ pub fn rate_limited_calls() -> Vec { ], }, scope: LimitScopeKind::Global, - usage: UsageKind::Account, + usage: vec![UsageKind::Account], read_only: Vec::new(), legacy: sources(&["TxDelegateTakeRateLimit"], &["LastTxBlockDelegateTake"]), }, RateLimitedCall { target: TargetKind::Group { id: GROUP_WEIGHTS_SUBNET, - name: b"weights-subnet".to_vec(), + name: b"weights".to_vec(), sharing: GroupSharing::ConfigAndUsage, - members: vec![ - subtensor_identifier(0), // set_weights - subtensor_identifier(96), // commit_weights - subtensor_identifier(100), // batch_commit_weights - subtensor_identifier(113), // commit_timelocked_weights - subtensor_identifier(97), // reveal_weights - subtensor_identifier(98), // batch_reveal_weights - ], - }, - scope: LimitScopeKind::Netuid, - usage: UsageKind::SubnetNeuron, - read_only: Vec::new(), - legacy: sources(&["SubnetWeightsSetRateLimit"], &["LastUpdate (subnet)"]), - }, - RateLimitedCall { - target: TargetKind::Group { - id: GROUP_WEIGHTS_MECHANISM, - name: b"weights-mechanism".to_vec(), - sharing: GroupSharing::ConfigAndUsage, - members: vec![ - subtensor_identifier(119), // set_mechanism_weights - subtensor_identifier(115), // commit_mechanism_weights - subtensor_identifier(117), // commit_crv3_mechanism_weights - subtensor_identifier(118), // commit_timelocked_mechanism_weights - subtensor_identifier(116), // reveal_mechanism_weights - ], + members: WEIGHT_CALLS_SUBNET + .into_iter() + .chain(WEIGHT_CALLS_MECHANISM) + .collect(), }, scope: LimitScopeKind::Netuid, - usage: UsageKind::SubnetMechanismNeuron, + usage: vec![UsageKind::SubnetNeuron, UsageKind::SubnetMechanismNeuron], read_only: Vec::new(), - legacy: sources(&["SubnetWeightsSetRateLimit"], &["LastUpdate (mechanism)"]), + legacy: sources( + &["SubnetWeightsSetRateLimit"], + &["LastUpdate (subnet/mechanism)"], + ), }, RateLimitedCall { target: TargetKind::Group { @@ -359,7 +339,7 @@ pub fn rate_limited_calls() -> Vec { ], }, scope: LimitScopeKind::Global, - usage: UsageKind::None, + usage: Vec::new(), read_only: Vec::new(), legacy: sources(&["NetworkRateLimit"], &["NetworkLastRegistered"]), }, @@ -374,7 +354,7 @@ pub fn rate_limited_calls() -> Vec { .collect(), }, scope: LimitScopeKind::Netuid, - usage: UsageKind::Subnet, + usage: vec![UsageKind::Subnet], read_only: Vec::new(), legacy: sources( &["OwnerHyperparamRateLimit * tempo"], @@ -399,25 +379,26 @@ pub fn rate_limited_calls() -> Vec { ], }, scope: LimitScopeKind::Global, - usage: UsageKind::ColdkeyHotkeySubnet, + usage: vec![UsageKind::ColdkeyHotkeySubnet], read_only: vec![ subtensor_identifier(3), // remove_stake subtensor_identifier(89), // remove_stake_limit subtensor_identifier(103), // remove_stake_full_limit + subtensor_identifier(86), // transfer_stake ], legacy: sources(&["TxRateLimit"], &[]), }, RateLimitedCall { target: TargetKind::Standalone(subtensor_identifier(70)), // swap_hotkey scope: LimitScopeKind::Global, - usage: UsageKind::Account, + usage: vec![UsageKind::Account], read_only: Vec::new(), legacy: sources(&["TxRateLimit"], &["LastRateLimitedBlock::LastTxBlock"]), }, RateLimitedCall { target: TargetKind::Standalone(subtensor_identifier(75)), // set_childkey_take scope: LimitScopeKind::Global, - usage: UsageKind::AccountSubnet, + usage: vec![UsageKind::AccountSubnet], read_only: Vec::new(), legacy: sources( &["TxChildkeyTakeRateLimit"], @@ -427,7 +408,7 @@ pub fn rate_limited_calls() -> Vec { RateLimitedCall { target: TargetKind::Standalone(subtensor_identifier(67)), // set_children scope: LimitScopeKind::Global, - usage: UsageKind::AccountSubnet, + usage: vec![UsageKind::AccountSubnet], read_only: Vec::new(), legacy: sources( &["SET_CHILDREN_RATE_LIMIT (constant 150)"], @@ -438,7 +419,7 @@ pub fn rate_limited_calls() -> Vec { // sudo_set_weights_version_key target: TargetKind::Standalone(admin_utils_identifier(6)), scope: LimitScopeKind::Netuid, - usage: UsageKind::AccountSubnet, + usage: vec![UsageKind::AccountSubnet], read_only: Vec::new(), legacy: sources( &["WeightsVersionKeyRateLimit * tempo"], @@ -449,7 +430,7 @@ pub fn rate_limited_calls() -> Vec { // sudo_set_sn_owner_hotkey target: TargetKind::Standalone(admin_utils_identifier(67)), scope: LimitScopeKind::Global, - usage: UsageKind::Subnet, + usage: vec![UsageKind::Subnet], read_only: Vec::new(), legacy: sources( &["DefaultSetSNOwnerHotkeyRateLimit"], @@ -459,14 +440,14 @@ pub fn rate_limited_calls() -> Vec { RateLimitedCall { target: TargetKind::Standalone(subtensor_identifier(93)), // associate_evm_key scope: LimitScopeKind::Global, - usage: UsageKind::SubnetNeuron, + usage: vec![UsageKind::SubnetNeuron], read_only: Vec::new(), legacy: sources(&["EvmKeyAssociateRateLimit"], &["AssociatedEvmAddress"]), }, RateLimitedCall { target: TargetKind::Standalone(admin_utils_identifier(76)), // sudo_set_mechanism_count scope: LimitScopeKind::Global, - usage: UsageKind::AccountSubnet, + usage: vec![UsageKind::AccountSubnet], read_only: Vec::new(), legacy: sources( &["MechanismCountSetRateLimit"], @@ -477,7 +458,7 @@ pub fn rate_limited_calls() -> Vec { // sudo_set_mechanism_emission_split target: TargetKind::Standalone(admin_utils_identifier(77)), scope: LimitScopeKind::Global, - usage: UsageKind::AccountSubnet, + usage: vec![UsageKind::AccountSubnet], read_only: Vec::new(), legacy: sources( &["MechanismEmissionRateLimit"], @@ -488,7 +469,7 @@ pub fn rate_limited_calls() -> Vec { // sudo_trim_to_max_allowed_uids target: TargetKind::Standalone(admin_utils_identifier(78)), scope: LimitScopeKind::Global, - usage: UsageKind::AccountSubnet, + usage: vec![UsageKind::AccountSubnet], read_only: Vec::new(), legacy: sources( &["MaxUidsTrimmingRateLimit"], @@ -723,16 +704,14 @@ fn import_weight_limits( let mut reads: u64 = 0; let netuids = Pallet::::get_all_subnet_netuids(); - let subnet_calls = weight_calls_subnet(grouping); - let mechanism_calls = weight_calls_mechanism(grouping); for netuid in &netuids { reads += 1; if let Some(span) = block_number::(Pallet::::get_weights_set_rate_limit(*netuid)) { - for call in &subnet_calls { - set_scoped_limit::(limits, grouping.config_target(*call), *netuid, span); + for call in WEIGHT_CALLS_SUBNET { + set_scoped_limit::(limits, grouping.config_target(call), *netuid, span); } - for call in &mechanism_calls { - set_scoped_limit::(limits, grouping.config_target(*call), *netuid, span); + for call in WEIGHT_CALLS_MECHANISM { + set_scoped_limit::(limits, grouping.config_target(call), *netuid, span); } } } @@ -864,10 +843,10 @@ fn import_last_update_entries( reads += 1; let (netuid, mecid) = Pallet::::get_netuid_and_subid(index).unwrap_or((NetUid::ROOT, 0.into())); - let subnet_calls = if mecid == 0.into() { - weight_calls_subnet(grouping) + let subnet_calls: Vec<_> = if mecid == 0.into() { + WEIGHT_CALLS_SUBNET.to_vec() } else { - weight_calls_mechanism(grouping) + WEIGHT_CALLS_MECHANISM.to_vec() }; for (uid, last_block) in blocks.into_iter().enumerate() { @@ -890,7 +869,7 @@ fn import_last_update_entries( } }; - for call in &subnet_calls { + for call in subnet_calls.iter() { record_last_seen_entry::( entries, grouping.usage_target(*call), @@ -913,12 +892,13 @@ fn import_serving_entries( if axon.block == 0 { continue; } - let usage = RateLimitUsageKey::AccountSubnet { + let usage = RateLimitUsageKey::AccountSubnetServing { account: hotkey.clone(), netuid, + endpoint: crate::rate_limiting::ServingEndpoint::Axon, }; let axon_calls: Vec<_> = grouping - .members(GROUP_SERVE_AXON) + .members(GROUP_SERVE) .map(|m| m.iter().copied().collect()) .unwrap_or_else(|| vec![subtensor_identifier(4), subtensor_identifier(40)]); for call in axon_calls { @@ -936,9 +916,10 @@ fn import_serving_entries( if prom.block == 0 { continue; } - let usage = RateLimitUsageKey::AccountSubnet { + let usage = RateLimitUsageKey::AccountSubnetServing { account: hotkey, netuid, + endpoint: crate::rate_limiting::ServingEndpoint::Prometheus, }; record_last_seen_entry::( entries, @@ -1326,7 +1307,7 @@ mod tests { pallet_rate_limiting::CallGroups::::get(subtensor_identifier(66)), Some(DELEGATE_TAKE_GROUP_ID) ); - assert_eq!(pallet_rate_limiting::NextGroupId::::get(), 7); + assert_eq!(pallet_rate_limiting::NextGroupId::::get(), 6); }); } diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 06b7dbcf3e..1658f03a6b 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -50,6 +50,32 @@ pub enum RateLimitUsageKey { mecid: MechId, uid: u16, }, + AccountSubnetServing { + account: AccountId, + netuid: NetUid, + endpoint: ServingEndpoint, + }, +} + +#[derive( + Serialize, + Deserialize, + Encode, + Decode, + DecodeWithMemTracking, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + TypeInfo, + MaxEncodedLen, +)] +pub enum ServingEndpoint { + Axon, + Prometheus, } #[derive(Default)] @@ -106,6 +132,16 @@ impl RateLimitScopeResolver for | SubtensorCall::decrease_take { .. } => { return BypassDecision::bypass_and_record(); } + SubtensorCall::reveal_weights { netuid, .. } + | SubtensorCall::batch_reveal_weights { netuid, .. } + | SubtensorCall::reveal_mechanism_weights { netuid, .. } => { + if pallet_subtensor::Pallet::::get_commit_reveal_weights_enabled( + *netuid, + ) { + // Legacy: reveals are not rate-limited while commit-reveal is enabled. + return BypassDecision::bypass_and_skip(); + } + } _ => {} } } @@ -185,12 +221,20 @@ impl RateLimitUsageResolver { + | SubtensorCall::serve_axon_tls { netuid, .. } => { let hotkey = signed_origin(origin)?; - Some(RateLimitUsageKey::::AccountSubnet { + Some(RateLimitUsageKey::::AccountSubnetServing { + account: hotkey, + netuid: *netuid, + endpoint: ServingEndpoint::Axon, + }) + } + SubtensorCall::serve_prometheus { netuid, .. } => { + let hotkey = signed_origin(origin)?; + Some(RateLimitUsageKey::::AccountSubnetServing { account: hotkey, netuid: *netuid, + endpoint: ServingEndpoint::Prometheus, }) } SubtensorCall::associate_evm_key { netuid, .. } => { diff --git a/runtime/tests/rate_limiting_behavior.rs b/runtime/tests/rate_limiting_behavior.rs index 557fcdd63f..b9a39814c1 100644 --- a/runtime/tests/rate_limiting_behavior.rs +++ b/runtime/tests/rate_limiting_behavior.rs @@ -327,6 +327,7 @@ fn weights_and_hparam_parity() { let origin = RuntimeOrigin::signed(hot.clone()); let scope = Some(netuid); let usage = Some(UsageKey::SubnetNeuron { netuid, uid }); + let legacy_weights = || SubtensorModule::check_rate_limit(netuid.into(), uid, now); parity_check( now, From 56c1af991e9deca3a661a9cdb3fa72433f1372e3 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 8 Dec 2025 17:59:07 +0300 Subject: [PATCH 37/95] Refactor rate-limiting migration --- pallets/rate-limiting/src/types.rs | 2 + runtime/src/rate_limiting/migration.rs | 1771 ++++++++++------------ runtime/tests/rate_limiting_migration.rs | 39 +- 3 files changed, 787 insertions(+), 1025 deletions(-) diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index deb27598ec..cab93a6b1a 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -90,6 +90,8 @@ pub struct TransactionIdentifier { Copy, PartialEq, Eq, + PartialOrd, + Ord, Encode, Decode, DecodeWithMemTracking, diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index be504985c7..a8466fc782 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -1,8 +1,6 @@ use core::{convert::TryFrom, marker::PhantomData}; -use frame_support::{ - BoundedBTreeSet, BoundedVec, pallet_prelude::Parameter, traits::Get, weights::Weight, -}; +use frame_support::{BoundedBTreeSet, BoundedVec, traits::Get, weights::Weight}; use frame_system::pallet_prelude::BlockNumberFor; use log::{info, warn}; use pallet_rate_limiting::{ @@ -24,20 +22,9 @@ use sp_std::{ }; use subtensor_runtime_common::NetUid; -use super::{RateLimitUsageKey, Runtime}; +use super::{AccountId, RateLimitUsageKey, Runtime}; type GroupId = ::GroupId; -type LimitEntries = Vec<( - RateLimitTarget, - RateLimit>, -)>; -type LastSeenEntries = Vec<( - ( - RateLimitTarget, - Option::AccountId>>, - ), - BlockNumberFor, -)>; type GroupNameOf = BoundedVec::MaxGroupNameLength>; type GroupMembersOf = BoundedBTreeSet::MaxGroupMembers>; @@ -47,21 +34,19 @@ const SUBTENSOR_PALLET_INDEX: u8 = 7; // Pallet index assigned to `pallet_admin_utils` in `construct_runtime!`. const ADMIN_UTILS_PALLET_INDEX: u8 = 19; -const SERVE_PROM_IDENTIFIER: TransactionIdentifier = subtensor_identifier(5); - -// Marker stored in `pallet_subtensor::HasMigrationRun` once the migration finishes. -const MIGRATION_NAME: &[u8] = b"migrate_rate_limiting"; - -// `set_children` is rate-limited to once every 150 blocks, it's hard-coded in the legacy code. -const SET_CHILDREN_RATE_LIMIT: u64 = 150; +/// Marker stored in `pallet_subtensor::HasMigrationRun` once the migration finishes. +pub const MIGRATION_NAME: &[u8] = b"migrate_rate_limiting"; const GROUP_SERVE: GroupId = 0; const GROUP_DELEGATE_TAKE: GroupId = 1; const GROUP_WEIGHTS_SUBNET: GroupId = 2; -const GROUP_REGISTER_NETWORK: GroupId = 3; +pub const GROUP_REGISTER_NETWORK: GroupId = 3; const GROUP_OWNER_HPARAMS: GroupId = 4; const GROUP_STAKING_OPS: GroupId = 5; +// `set_children` is rate-limited to once every 150 blocks, it's hard-coded in the legacy code. +const SET_CHILDREN_RATE_LIMIT: u64 = 150; + // Hyperparameter extrinsics routed through owner-or-root rate limiting. const HYPERPARAMETERS: &[Hyperparameter] = &[ Hyperparameter::ServingRateLimit, @@ -90,769 +75,371 @@ const HYPERPARAMETERS: &[Hyperparameter] = &[ Hyperparameter::RecycleOrBurn, ]; -const WEIGHT_CALLS_SUBNET: [TransactionIdentifier; 6] = [ - subtensor_identifier(0), // set_weights - subtensor_identifier(96), // commit_weights - subtensor_identifier(100), // batch_commit_weights - subtensor_identifier(113), // commit_timelocked_weights - subtensor_identifier(97), // reveal_weights - subtensor_identifier(98), // batch_reveal_weights -]; - -const WEIGHT_CALLS_MECHANISM: [TransactionIdentifier; 5] = [ - subtensor_identifier(119), // set_mechanism_weights - subtensor_identifier(115), // commit_mechanism_weights - subtensor_identifier(117), // commit_crv3_mechanism_weights - subtensor_identifier(118), // commit_timelocked_mechanism_weights - subtensor_identifier(116), // reveal_mechanism_weights -]; - -/// Identifies whether a rate-limited entry applies to a single call or a named group. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum TargetKind { - Standalone(TransactionIdentifier), - Group { - id: GroupId, - name: Vec, - sharing: GroupSharing, - members: Vec, - }, -} - -/// Describes how a limit is scoped in storage. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum LimitScopeKind { - Global, - Netuid, -} - -/// Describes the shape of the usage key recorded after a call executes. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum UsageKind { - Account, - Subnet, - AccountSubnet, - AccountSubnetAxon, - AccountSubnetPrometheus, - ColdkeyHotkeySubnet, - SubnetNeuron, - SubnetMechanismNeuron, -} - -/// Human-friendly description of a rate-limited call or group. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RateLimitedCall { - pub target: TargetKind, - pub scope: LimitScopeKind, - /// One or more usage shapes this call maps to in the legacy pallet. - pub usage: Vec, - /// Calls that should not record usage when dispatched (only relevant for groups). - pub read_only: Vec, - /// Legacy storage sources used by the migration. - pub legacy: LegacySources, -} - -/// Summarizes where legacy limits and last-seen data are sourced from. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct LegacySources { - pub limits: &'static [&'static str], - pub last_seen: &'static [&'static str], -} - -fn sources(limits: &'static [&'static str], last_seen: &'static [&'static str]) -> LegacySources { - LegacySources { limits, last_seen } -} - -#[derive(Clone, Copy)] -struct GroupInfo { - id: GroupId, - sharing: GroupSharing, -} - -#[derive(Default)] -struct Grouping { - assignments: BTreeMap, - read_only: BTreeMap, - members: BTreeMap>, - details: Vec>>, - next_group_id: GroupId, - max_group_id: Option, -} - -impl Grouping { - fn members(&self, id: GroupId) -> Option<&BTreeSet> { - self.members.get(&id) - } - - fn set_read_only(&mut self, id: TransactionIdentifier, read_only: bool) { - self.read_only.insert(id, read_only); - } - - fn insert_group( - &mut self, - id: GroupId, - name: &[u8], - sharing: GroupSharing, - members: &[TransactionIdentifier], - ) { - let entry = self.members.entry(id).or_insert_with(BTreeSet::new); - for member in members { - self.assignments.insert(*member, GroupInfo { id, sharing }); - entry.insert(*member); - self.read_only.entry(*member).or_insert(false); - } - - self.details.push(RateLimitGroup { - id, - name: name.to_vec(), - sharing, - }); - - self.max_group_id = Some(self.max_group_id.map_or(id, |current| current.max(id))); - } - - fn finalize_next_id(&mut self) { - self.next_group_id = self.max_group_id.map_or(0, |id| id.saturating_add(1)); - } - - fn config_target(&self, identifier: TransactionIdentifier) -> RateLimitTarget { - if let Some(info) = self.assignments.get(&identifier) { - if info.sharing.config_uses_group() { - return RateLimitTarget::Group(info.id); - } - } - RateLimitTarget::Transaction(identifier) - } - - fn usage_target(&self, identifier: TransactionIdentifier) -> RateLimitTarget { - if let Some(info) = self.assignments.get(&identifier) { - if info.sharing.usage_uses_group() { - return RateLimitTarget::Group(info.id); - } - } - RateLimitTarget::Transaction(identifier) - } -} - -fn serve_calls(grouping: &Grouping) -> Vec { - grouping - .members(GROUP_SERVE) - .map(|m| m.iter().copied().collect()) - .unwrap_or_default() -} - -fn build_grouping() -> Grouping { - let mut grouping = Grouping::default(); - - for entry in rate_limited_calls() { - match entry.target { - TargetKind::Group { - id, - name, - sharing, - members, - } => { - grouping.insert_group(id, &name, sharing, &members); - for member in members { - if entry.read_only.contains(&member) { - grouping.set_read_only(member, true); - } - } - } - TargetKind::Standalone(call) => { - if entry.read_only.contains(&call) { - grouping.set_read_only(call, true); - } - } - } - } - - grouping.finalize_next_id(); - grouping -} - -/// Returns a readable listing of all calls covered by the migration. -/// Each entry captures how the call is grouped, scoped, and which legacy storage sources feed its -/// limits and last-seen values. -pub fn rate_limited_calls() -> Vec { - vec![ - RateLimitedCall { - target: TargetKind::Group { - id: GROUP_SERVE, - name: b"serving".to_vec(), - sharing: GroupSharing::ConfigAndUsage, - members: vec![ - subtensor_identifier(4), // serve_axon - subtensor_identifier(40), // serve_axon_tls - SERVE_PROM_IDENTIFIER, // serve_prometheus - ], - }, - scope: LimitScopeKind::Netuid, - usage: vec![ - UsageKind::AccountSubnetAxon, - UsageKind::AccountSubnetPrometheus, - ], - read_only: Vec::new(), - legacy: sources(&["ServingRateLimit (per netuid)"], &["Axons/Prometheus"]), - }, - RateLimitedCall { - target: TargetKind::Group { - id: GROUP_DELEGATE_TAKE, - name: b"delegate-take".to_vec(), - sharing: GroupSharing::ConfigAndUsage, - members: vec![ - subtensor_identifier(66), // increase_take - subtensor_identifier(65), // decrease_take - ], - }, - scope: LimitScopeKind::Global, - usage: vec![UsageKind::Account], - read_only: Vec::new(), - legacy: sources(&["TxDelegateTakeRateLimit"], &["LastTxBlockDelegateTake"]), - }, - RateLimitedCall { - target: TargetKind::Group { - id: GROUP_WEIGHTS_SUBNET, - name: b"weights".to_vec(), - sharing: GroupSharing::ConfigAndUsage, - members: WEIGHT_CALLS_SUBNET - .into_iter() - .chain(WEIGHT_CALLS_MECHANISM) - .collect(), - }, - scope: LimitScopeKind::Netuid, - usage: vec![UsageKind::SubnetNeuron, UsageKind::SubnetMechanismNeuron], - read_only: Vec::new(), - legacy: sources( - &["SubnetWeightsSetRateLimit"], - &["LastUpdate (subnet/mechanism)"], - ), - }, - RateLimitedCall { - target: TargetKind::Group { - id: GROUP_REGISTER_NETWORK, - name: b"register-network".to_vec(), - sharing: GroupSharing::ConfigAndUsage, - members: vec![ - subtensor_identifier(59), // register_network - subtensor_identifier(79), // register_network_with_identity - ], - }, - scope: LimitScopeKind::Global, - usage: Vec::new(), - read_only: Vec::new(), - legacy: sources(&["NetworkRateLimit"], &["NetworkLastRegistered"]), - }, - RateLimitedCall { - target: TargetKind::Group { - id: GROUP_OWNER_HPARAMS, - name: b"owner-hparams".to_vec(), - sharing: GroupSharing::ConfigOnly, - members: HYPERPARAMETERS - .iter() - .filter_map(|h| identifier_for_hyperparameter(*h)) - .collect(), - }, - scope: LimitScopeKind::Netuid, - usage: vec![UsageKind::Subnet], - read_only: Vec::new(), - legacy: sources( - &["OwnerHyperparamRateLimit * tempo"], - &["LastRateLimitedBlock::OwnerHyperparamUpdate"], - ), - }, - RateLimitedCall { - target: TargetKind::Group { - id: GROUP_STAKING_OPS, - name: b"staking-ops".to_vec(), - sharing: GroupSharing::ConfigAndUsage, - members: vec![ - subtensor_identifier(2), // add_stake - subtensor_identifier(88), // add_stake_limit - subtensor_identifier(3), // remove_stake - subtensor_identifier(89), // remove_stake_limit - subtensor_identifier(103), // remove_stake_full_limit - subtensor_identifier(85), // move_stake - subtensor_identifier(86), // transfer_stake - subtensor_identifier(87), // swap_stake - subtensor_identifier(90), // swap_stake_limit - ], - }, - scope: LimitScopeKind::Global, - usage: vec![UsageKind::ColdkeyHotkeySubnet], - read_only: vec![ - subtensor_identifier(3), // remove_stake - subtensor_identifier(89), // remove_stake_limit - subtensor_identifier(103), // remove_stake_full_limit - subtensor_identifier(86), // transfer_stake - ], - legacy: sources(&["TxRateLimit"], &[]), - }, - RateLimitedCall { - target: TargetKind::Standalone(subtensor_identifier(70)), // swap_hotkey - scope: LimitScopeKind::Global, - usage: vec![UsageKind::Account], - read_only: Vec::new(), - legacy: sources(&["TxRateLimit"], &["LastRateLimitedBlock::LastTxBlock"]), - }, - RateLimitedCall { - target: TargetKind::Standalone(subtensor_identifier(75)), // set_childkey_take - scope: LimitScopeKind::Global, - usage: vec![UsageKind::AccountSubnet], - read_only: Vec::new(), - legacy: sources( - &["TxChildkeyTakeRateLimit"], - &["TransactionKeyLastBlock::SetChildkeyTake"], - ), - }, - RateLimitedCall { - target: TargetKind::Standalone(subtensor_identifier(67)), // set_children - scope: LimitScopeKind::Global, - usage: vec![UsageKind::AccountSubnet], - read_only: Vec::new(), - legacy: sources( - &["SET_CHILDREN_RATE_LIMIT (constant 150)"], - &["TransactionKeyLastBlock::SetChildren"], - ), - }, - RateLimitedCall { - // sudo_set_weights_version_key - target: TargetKind::Standalone(admin_utils_identifier(6)), - scope: LimitScopeKind::Netuid, - usage: vec![UsageKind::AccountSubnet], - read_only: Vec::new(), - legacy: sources( - &["WeightsVersionKeyRateLimit * tempo"], - &["TransactionKeyLastBlock::SetWeightsVersionKey"], - ), - }, - RateLimitedCall { - // sudo_set_sn_owner_hotkey - target: TargetKind::Standalone(admin_utils_identifier(67)), - scope: LimitScopeKind::Global, - usage: vec![UsageKind::Subnet], - read_only: Vec::new(), - legacy: sources( - &["DefaultSetSNOwnerHotkeyRateLimit"], - &["LastRateLimitedBlock::SetSNOwnerHotkey"], - ), - }, - RateLimitedCall { - target: TargetKind::Standalone(subtensor_identifier(93)), // associate_evm_key - scope: LimitScopeKind::Global, - usage: vec![UsageKind::SubnetNeuron], - read_only: Vec::new(), - legacy: sources(&["EvmKeyAssociateRateLimit"], &["AssociatedEvmAddress"]), - }, - RateLimitedCall { - target: TargetKind::Standalone(admin_utils_identifier(76)), // sudo_set_mechanism_count - scope: LimitScopeKind::Global, - usage: vec![UsageKind::AccountSubnet], - read_only: Vec::new(), - legacy: sources( - &["MechanismCountSetRateLimit"], - &["TransactionKeyLastBlock::MechanismCountUpdate"], - ), - }, - RateLimitedCall { - // sudo_set_mechanism_emission_split - target: TargetKind::Standalone(admin_utils_identifier(77)), - scope: LimitScopeKind::Global, - usage: vec![UsageKind::AccountSubnet], - read_only: Vec::new(), - legacy: sources( - &["MechanismEmissionRateLimit"], - &["TransactionKeyLastBlock::MechanismEmission"], - ), - }, - RateLimitedCall { - // sudo_trim_to_max_allowed_uids - target: TargetKind::Standalone(admin_utils_identifier(78)), - scope: LimitScopeKind::Global, - usage: vec![UsageKind::AccountSubnet], - read_only: Vec::new(), - legacy: sources( - &["MaxUidsTrimmingRateLimit"], - &["TransactionKeyLastBlock::MaxUidsTrimming"], - ), - }, - ] -} +/// Runtime hook that executes the rate-limiting migration. +pub struct Migration(PhantomData); -pub fn migrate_rate_limiting() -> Weight +impl frame_support::traits::OnRuntimeUpgrade for Migration where T: SubtensorConfig + pallet_rate_limiting::Config, RateLimitUsageKey: Into<::UsageKey>, { - let mut weight = T::DbWeight::get().reads(1); - if HasMigrationRun::::get(MIGRATION_NAME) { + fn on_runtime_upgrade() -> Weight { + migrate_rate_limiting() + } +} + +pub fn migrate_rate_limiting() -> Weight { + let mut weight = ::DbWeight::get().reads(1); + if HasMigrationRun::::get(MIGRATION_NAME) { info!("Rate-limiting migration already executed. Skipping."); return weight; } - let grouping = build_grouping(); - let (limits, limit_reads) = build_limits::(&grouping); - let (last_seen, seen_reads) = build_last_seen::(&grouping); - - let limit_writes = write_limits::(&limits); - let seen_writes = write_last_seen::(&last_seen); - let group_writes = write_groups::(&grouping); - - HasMigrationRun::::insert(MIGRATION_NAME, true); - - weight = weight - .saturating_add(T::DbWeight::get().reads(limit_reads.saturating_add(seen_reads))) - .saturating_add( - T::DbWeight::get().writes( - limit_writes - .saturating_add(seen_writes) - .saturating_add(group_writes) - .saturating_add(1), - ), - ); + let (groups, commits, reads) = commits(); + weight = weight.saturating_add(::DbWeight::get().reads(reads)); - info!( - "Migrated {} rate-limit configs, {} last-seen entries, and {} groups into pallet-rate-limiting", - limits.len(), - last_seen.len(), - grouping.details.len() + let (limit_commits, last_seen_commits) = commits.into_iter().fold( + (Vec::new(), Vec::new()), + |(mut limits, mut seen), commit| { + match commit.kind { + CommitKind::Limit(limit) => limits.push((commit.target, limit)), + CommitKind::LastSeen(ls) => seen.push((commit.target, ls)), + } + (limits, seen) + }, ); - weight -} + let (group_writes, group_count) = migrate_grouping(&groups); + let (limit_writes, limits_len) = migrate_limits(limit_commits); + let (last_seen_writes, last_seen_len) = migrate_last_seen(last_seen_commits); -type LimitImporter = fn(&Grouping, &mut LimitEntries) -> u64; + let mut writes = group_writes + .saturating_add(limit_writes) + .saturating_add(last_seen_writes); -fn limit_importers() -> [LimitImporter; 4] { - [ - import_simple_limits::, // Tx/childkey/delegate/staking lock, register, sudo, evm, children - import_owner_hparam_limits::, // Owner hyperparams - import_serving_limits::, // Axon/prometheus serving rate limit per subnet - import_weight_limits::, // Weight/commit/reveal per subnet and mechanism - ] -} + HasMigrationRun::::insert(MIGRATION_NAME, true); + writes += 1; -fn build_limits(grouping: &Grouping) -> (LimitEntries, u64) { - let mut limits = LimitEntries::::new(); - let mut reads: u64 = 0; + weight = + weight.saturating_add(::DbWeight::get().writes(writes)); - for importer in limit_importers::() { - reads += importer(grouping, &mut limits); - } + info!( + "New migration wrote {} limits, {} last-seen entries, and {} groups into pallet-rate-limiting", + limits_len, last_seen_len, group_count + ); - (limits, reads) + weight } -fn import_simple_limits( - grouping: &Grouping, - limits: &mut LimitEntries, -) -> u64 { +// Main entrypoint: build all groups and commits, along with storage reads. +fn commits() -> (Vec, Vec, u64) { + let mut groups = Vec::new(); + let mut commits = Vec::new(); let mut reads: u64 = 0; - reads += 1; - if let Some(span) = block_number::(TxRateLimit::::get()) { - set_global_limit::( - limits, - grouping.config_target(subtensor_identifier(70)), - span, - ); - } + // grouped + reads += build_serving(&mut groups, &mut commits); + reads += build_delegate_take(&mut groups, &mut commits); + reads += build_weights(&mut groups, &mut commits); + reads += build_register_network(&mut groups, &mut commits); + reads += build_owner_hparams(&mut groups, &mut commits); + reads += build_staking_ops(&mut groups, &mut commits); + + // standalone + reads += build_swap_hotkey(&mut commits); + reads += build_childkey_take(&mut commits); + reads += build_set_children(&mut commits); + reads += build_weights_version_key(&mut commits); + reads += build_sn_owner_hotkey(&mut commits); + reads += build_associate_evm(&mut commits); + reads += build_mechanism_count(&mut commits); + reads += build_mechanism_emission(&mut commits); + reads += build_trim_max_uids(&mut commits); + + (groups, commits, reads) +} - // Staking ops are gated to one operation per block in legacy (marker cleared each block). - if let Some(members) = grouping.members(GROUP_STAKING_OPS) { - if let Some(span) = block_number::(1) { - for call in members { - set_global_limit::(limits, grouping.config_target(*call), span); - } - } - } +fn migrate_grouping(groups: &[GroupConfig]) -> (u64, usize) { + let mut writes: u64 = 0; + let mut max_group_id: Option = None; - reads += 1; - if let Some(span) = block_number::(TxDelegateTakeRateLimit::::get()) { - set_global_limit::( - limits, - grouping.config_target(subtensor_identifier(66)), - span, - ); - } + for group in groups { + let Ok(name) = GroupNameOf::::try_from(group.name.clone()) else { + warn!( + "rate-limiting migration: group name exceeds bounds, skipping id {}", + group.id + ); + continue; + }; - reads += 1; - if let Some(span) = block_number::(TxChildkeyTakeRateLimit::::get()) { - set_global_limit::( - limits, - grouping.config_target(subtensor_identifier(75)), - span, + pallet_rate_limiting::Groups::::insert( + group.id, + RateLimitGroup { + id: group.id, + name: name.clone(), + sharing: group.sharing, + }, ); - } + pallet_rate_limiting::GroupNameIndex::::insert(name, group.id); + writes += 2; - reads += 1; - if let Some(span) = block_number::(NetworkRateLimit::::get()) { - if let Some(members) = grouping.members(GROUP_REGISTER_NETWORK) { - for call in members { - set_global_limit::(limits, grouping.config_target(*call), span); + let mut member_set = BTreeSet::new(); + for call in &group.members { + member_set.insert(call.identifier()); + pallet_rate_limiting::CallGroups::::insert(call.identifier(), group.id); + writes += 1; + if call.read_only { + pallet_rate_limiting::CallReadOnly::::insert(call.identifier(), true); + writes += 1; } } - } + let Ok(bounded) = GroupMembersOf::::try_from(member_set) else { + warn!( + "rate-limiting migration: group {} has too many members, skipping assignment", + group.id + ); + continue; + }; + pallet_rate_limiting::GroupMembers::::insert(group.id, bounded); + writes += 1; - if let Some(span) = block_number::(WeightsVersionKeyRateLimit::::get()) { - set_global_limit::( - limits, - grouping.config_target(admin_utils_identifier(6)), - span, - ); + max_group_id = Some(max_group_id.map_or(group.id, |current| current.max(group.id))); } - if let Some(span) = - block_number::(pallet_subtensor::pallet::DefaultSetSNOwnerHotkeyRateLimit::::get()) - { - set_global_limit::( - limits, - grouping.config_target(admin_utils_identifier(67)), - span, - ); - } + let next_group_id = max_group_id.map_or(0, |id| id.saturating_add(1)); + pallet_rate_limiting::NextGroupId::::put(next_group_id); + writes += 1; - if let Some(span) = block_number::(::EvmKeyAssociateRateLimit::get()) { - set_global_limit::( - limits, - grouping.config_target(subtensor_identifier(93)), - span, - ); - } + (writes, groups.len()) +} - if let Some(span) = block_number::(MechanismCountSetRateLimit::::get()) { - set_global_limit::( - limits, - grouping.config_target(admin_utils_identifier(76)), - span, - ); - } +fn migrate_limits(limit_commits: Vec<(RateLimitTarget, MigratedLimit)>) -> (u64, usize) { + let mut writes: u64 = 0; + let mut limits: BTreeMap, RateLimit>> = + BTreeMap::new(); - if let Some(span) = block_number::(MechanismEmissionRateLimit::::get()) { - set_global_limit::( - limits, - grouping.config_target(admin_utils_identifier(77)), - span, - ); - } + for (target, MigratedLimit { span, scope }) in limit_commits { + let entry = limits.entry(target).or_insert_with(|| match scope { + Some(s) => RateLimit::scoped_single(s, RateLimitKind::Exact(span)), + None => RateLimit::global(RateLimitKind::Exact(span)), + }); - if let Some(span) = block_number::(MaxUidsTrimmingRateLimit::::get()) { - set_global_limit::( - limits, - grouping.config_target(admin_utils_identifier(78)), - span, - ); + if let Some(netuid) = scope { + match entry { + RateLimit::Global(_) => { + *entry = RateLimit::scoped_single(netuid, RateLimitKind::Exact(span)); + } + RateLimit::Scoped(map) => { + map.insert(netuid, RateLimitKind::Exact(span)); + } + } + } else { + *entry = RateLimit::global(RateLimitKind::Exact(span)); + } } - if let Some(span) = block_number::(SET_CHILDREN_RATE_LIMIT) { - set_global_limit::( - limits, - grouping.config_target(subtensor_identifier(67)), - span, - ); + let len = limits.len(); + for (target, limit) in limits { + pallet_rate_limiting::Limits::::insert(target, limit); + writes += 1; } - reads + (writes, len) } -fn import_owner_hparam_limits( - grouping: &Grouping, - limits: &mut LimitEntries, -) -> u64 { - let mut reads: u64 = 0; +fn migrate_last_seen( + last_seen_commits: Vec<(RateLimitTarget, MigratedLastSeen)>, +) -> (u64, usize) { + let mut writes: u64 = 0; + let mut last_seen: BTreeMap< + ( + RateLimitTarget, + Option>, + ), + BlockNumberFor, + > = BTreeMap::new(); + + for (target, MigratedLastSeen { block, usage }) in last_seen_commits { + let key = (target, usage); + last_seen + .entry(key) + .and_modify(|existing| { + if block > *existing { + *existing = block; + } + }) + .or_insert(block); + } - reads += 1; - if let Some(span) = block_number::(u64::from(OwnerHyperparamRateLimit::::get())) { - for hparam in HYPERPARAMETERS { - if let Some(identifier) = identifier_for_hyperparameter(*hparam) { - set_global_limit::(limits, grouping.config_target(identifier), span); - } - } + let len = last_seen.len(); + for ((target, usage), block) in last_seen { + pallet_rate_limiting::LastSeen::::insert(target, usage, block); + writes += 1; } - reads + (writes, len) } -fn import_serving_limits( - grouping: &Grouping, - limits: &mut LimitEntries, -) -> u64 { +// Serving group (config+usage shared). +// scope: netuid +// usage: account+netuid, but different keys (endpoint value) for axon/prometheus +// legacy sources: ServingRateLimit (per netuid), Axons/Prometheus +fn build_serving(groups: &mut Vec, commits: &mut Vec) -> u64 { let mut reads: u64 = 0; - let mut netuids = Pallet::::get_all_subnet_netuids(); - for (netuid, _) in ServingRateLimit::::iter() { + // Create the group with all its members. + groups.push(GroupConfig { + id: GROUP_SERVE, + name: b"serving".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + MigratedCall::subtensor(4, false), // serve_axon + MigratedCall::subtensor(40, false), // serve_axon_tls + MigratedCall::subtensor(5, false), // serve_prometheus + ], + }); + + // Limits per netuid (written to the group target). + reads += 1; + // Merge live subnets (which may rely on default rate-limit values) with any legacy entries that + // exist only in storage, so we migrate both current and previously stored netuids without + // duplicates. + let mut netuids = Pallet::::get_all_subnet_netuids(); + for (netuid, _) in ServingRateLimit::::iter() { if !netuids.contains(&netuid) { netuids.push(netuid); } } - for netuid in netuids { reads += 1; - if let Some(span) = block_number::(Pallet::::get_serving_rate_limit(netuid)) { - for call in serve_calls(grouping) { - set_scoped_limit::(limits, grouping.config_target(call), netuid, span); - } - } + push_limit_commit_if_non_zero( + commits, + RateLimitTarget::Group(GROUP_SERVE), + Pallet::::get_serving_rate_limit(netuid), + Some(netuid), + ); } - reads -} - -fn import_weight_limits( - grouping: &Grouping, - limits: &mut LimitEntries, -) -> u64 { - let mut reads: u64 = 0; - let netuids = Pallet::::get_all_subnet_netuids(); + // Axon last-seen (group-shared usage). + for (netuid, hotkey, axon) in Axons::::iter() { + reads += 1; + if let Some(block) = block_number::(axon.block) { + commits.push(Commit { + target: RateLimitTarget::Group(GROUP_SERVE), + kind: CommitKind::LastSeen(MigratedLastSeen { + block, + usage: Some(RateLimitUsageKey::AccountSubnetServing { + account: hotkey.clone(), + netuid, + endpoint: crate::rate_limiting::ServingEndpoint::Axon, + }), + }), + }); + } + } - for netuid in &netuids { + // Prometheus last-seen (group-shared usage). + for (netuid, hotkey, prom) in Prometheus::::iter() { reads += 1; - if let Some(span) = block_number::(Pallet::::get_weights_set_rate_limit(*netuid)) { - for call in WEIGHT_CALLS_SUBNET { - set_scoped_limit::(limits, grouping.config_target(call), *netuid, span); - } - for call in WEIGHT_CALLS_MECHANISM { - set_scoped_limit::(limits, grouping.config_target(call), *netuid, span); - } + if let Some(block) = block_number::(prom.block) { + commits.push(Commit { + target: RateLimitTarget::Group(GROUP_SERVE), + kind: CommitKind::LastSeen(MigratedLastSeen { + block, + usage: Some(RateLimitUsageKey::AccountSubnetServing { + account: hotkey, + netuid, + endpoint: crate::rate_limiting::ServingEndpoint::Prometheus, + }), + }), + }); } } reads } -fn build_last_seen(grouping: &Grouping) -> (LastSeenEntries, u64) { - let mut last_seen = LastSeenEntries::::new(); +// Delegate take group (config + usage shared). +// usage: account +// legacy sources: TxDelegateTakeRateLimit, LastTxBlockDelegateTake +fn build_delegate_take(groups: &mut Vec, commits: &mut Vec) -> u64 { let mut reads: u64 = 0; + groups.push(GroupConfig { + id: GROUP_DELEGATE_TAKE, + name: b"delegate-take".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + MigratedCall::subtensor(66, false), // increase_take + MigratedCall::subtensor(65, false), // decrease_take + ], + }); + + let target = RateLimitTarget::Group(GROUP_DELEGATE_TAKE); + reads += 1; + push_limit_commit_if_non_zero( + commits, + target, + TxDelegateTakeRateLimit::::get(), + None, + ); - for importer in last_seen_importers::() { - reads += importer(grouping, &mut last_seen); - } - - (last_seen, reads) -} - -type LastSeenImporter = fn(&Grouping, &mut LastSeenEntries) -> u64; - -fn last_seen_importers() -> [LastSeenImporter; 5] { - [ - import_last_rate_limited_blocks::, // LastRateLimitedBlock (tx, delegate, owner hyperparams, sn owner) - import_transaction_key_last_blocks::, // TransactionKeyLastBlock (children, version key, mechanisms) - import_last_update_entries::, // LastUpdate (weights/mechanism weights) - import_serving_entries::, // Axons/Prometheus - import_evm_entries::, // AssociatedEvmAddress - ] -} - -fn import_last_rate_limited_blocks( - grouping: &Grouping, - entries: &mut LastSeenEntries, -) -> u64 { - let mut reads: u64 = 0; - for (key, block) in LastRateLimitedBlock::::iter() { - reads += 1; - if block == 0 { - continue; - } - match key { - RateLimitKey::SetSNOwnerHotkey(netuid) => { - if let Some(identifier) = - identifier_for_transaction_type(TransactionType::SetSNOwnerHotkey) - { - record_last_seen_entry::( - entries, - grouping.usage_target(identifier), - Some(RateLimitUsageKey::Subnet(netuid)), - block, - ); - } - } - RateLimitKey::OwnerHyperparamUpdate(netuid, hyper) => { - if let Some(identifier) = identifier_for_hyperparameter(hyper) { - record_last_seen_entry::( - entries, - grouping.usage_target(identifier), - Some(RateLimitUsageKey::Subnet(netuid)), - block, - ); + reads += + last_seen_helpers::collect_last_seen_from_last_rate_limited_block( + commits, + |key| match key { + RateLimitKey::LastTxBlockDelegateTake(account) => { + Some((target, Some(RateLimitUsageKey::Account(account)))) } - } - RateLimitKey::LastTxBlock(account) => { - record_last_seen_entry::( - entries, - grouping.usage_target(subtensor_identifier(70)), - Some(RateLimitUsageKey::Account(account.clone())), - block, - ); - } - RateLimitKey::LastTxBlockDelegateTake(account) => { - record_last_seen_entry::( - entries, - grouping.usage_target(subtensor_identifier(66)), - Some(RateLimitUsageKey::Account(account.clone())), - block, - ); - } - RateLimitKey::NetworkLastRegistered => { - record_last_seen_entry::( - entries, - grouping.usage_target(subtensor_identifier(59)), - None, - block, - ); - } - RateLimitKey::LastTxBlockChildKeyTake(_) => { - // Deprecated storage; ignored. - } - } - } + _ => None, + }, + ); + reads } -fn import_transaction_key_last_blocks( - grouping: &Grouping, - entries: &mut LastSeenEntries, -) -> u64 { +// Weights group (config + usage shared). +// scope: netuid +// usage: netuid+neuron/netuid+mechanism+neuron +// legacy source: SubnetWeightsSetRateLimit, LastUpdate (subnet/mechanism) +fn build_weights(groups: &mut Vec, commits: &mut Vec) -> u64 { let mut reads: u64 = 0; - for ((account, netuid, tx_kind), block) in TransactionKeyLastBlock::::iter() { + groups.push(GroupConfig { + id: GROUP_WEIGHTS_SUBNET, + name: b"weights".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + MigratedCall::subtensor(0, false), // set_weights + MigratedCall::subtensor(96, false), // commit_weights + MigratedCall::subtensor(100, false), // batch_commit_weights + MigratedCall::subtensor(113, false), // commit_timelocked_weights + MigratedCall::subtensor(97, false), // reveal_weights + MigratedCall::subtensor(98, false), // batch_reveal_weights + MigratedCall::subtensor(119, false), // set_mechanism_weights + MigratedCall::subtensor(115, false), // commit_mechanism_weights + MigratedCall::subtensor(117, false), // commit_crv3_mechanism_weights + MigratedCall::subtensor(118, false), // commit_timelocked_mechanism_weights + MigratedCall::subtensor(116, false), // reveal_mechanism_weights + ], + }); + + reads += 1; + for netuid in Pallet::::get_all_subnet_netuids() { reads += 1; - if block == 0 { - continue; - } - let tx_type = TransactionType::from(tx_kind); - let Some(identifier) = identifier_for_transaction_type(tx_type) else { - continue; - }; - let Some(usage) = usage_key_from_transaction_type(tx_type, &account, netuid) else { - continue; - }; - record_last_seen_entry::( - entries, - grouping.usage_target(identifier), - Some(usage), - block, + push_limit_commit_if_non_zero( + commits, + RateLimitTarget::Group(GROUP_WEIGHTS_SUBNET), + Pallet::::get_weights_set_rate_limit(netuid), + Some(netuid), ); } - reads -} -fn import_last_update_entries( - grouping: &Grouping, - entries: &mut LastSeenEntries, -) -> u64 { - let mut reads: u64 = 0; - for (index, blocks) in LastUpdate::::iter() { + for (index, blocks) in LastUpdate::::iter() { reads += 1; let (netuid, mecid) = - Pallet::::get_netuid_and_subid(index).unwrap_or((NetUid::ROOT, 0.into())); - let subnet_calls: Vec<_> = if mecid == 0.into() { - WEIGHT_CALLS_SUBNET.to_vec() - } else { - WEIGHT_CALLS_MECHANISM.to_vec() - }; - + Pallet::::get_netuid_and_subid(index).unwrap_or((NetUid::ROOT, 0.into())); for (uid, last_block) in blocks.into_iter().enumerate() { - if last_block == 0 { + let Some(block) = block_number::(last_block) else { continue; - } + }; let Ok(uid_u16) = u16::try_from(uid) else { continue; }; @@ -868,340 +455,495 @@ fn import_last_update_entries( uid: uid_u16, } }; - - for call in subnet_calls.iter() { - record_last_seen_entry::( - entries, - grouping.usage_target(*call), - Some(usage.clone()), - last_block, - ); - } + commits.push(Commit { + target: RateLimitTarget::Group(GROUP_WEIGHTS_SUBNET), + kind: CommitKind::LastSeen(MigratedLastSeen { + block, + usage: Some(usage), + }), + }); } } + reads } -fn import_serving_entries( - grouping: &Grouping, - entries: &mut LastSeenEntries, -) -> u64 { +// Register network group (config + usage shared). +// legacy sources: NetworkRateLimit, NetworkLastRegistered +fn build_register_network(groups: &mut Vec, commits: &mut Vec) -> u64 { let mut reads: u64 = 0; - for (netuid, hotkey, axon) in Axons::::iter() { - reads += 1; - if axon.block == 0 { - continue; - } - let usage = RateLimitUsageKey::AccountSubnetServing { - account: hotkey.clone(), - netuid, - endpoint: crate::rate_limiting::ServingEndpoint::Axon, - }; - let axon_calls: Vec<_> = grouping - .members(GROUP_SERVE) - .map(|m| m.iter().copied().collect()) - .unwrap_or_else(|| vec![subtensor_identifier(4), subtensor_identifier(40)]); - for call in axon_calls { - record_last_seen_entry::( - entries, - grouping.usage_target(call), - Some(usage.clone()), - axon.block, - ); - } - } + groups.push(GroupConfig { + id: GROUP_REGISTER_NETWORK, + name: b"register-network".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + MigratedCall::subtensor(59, false), // register_network + MigratedCall::subtensor(79, false), // register_network_with_identity + ], + }); + + let target = RateLimitTarget::Group(GROUP_REGISTER_NETWORK); + reads += 1; + push_limit_commit_if_non_zero(commits, target, NetworkRateLimit::::get(), None); + + reads += + last_seen_helpers::collect_last_seen_from_last_rate_limited_block( + commits, + |key| match key { + RateLimitKey::NetworkLastRegistered => Some((target, None)), + _ => None, + }, + ); - for (netuid, hotkey, prom) in Prometheus::::iter() { - reads += 1; - if prom.block == 0 { - continue; - } - let usage = RateLimitUsageKey::AccountSubnetServing { - account: hotkey, - netuid, - endpoint: crate::rate_limiting::ServingEndpoint::Prometheus, - }; - record_last_seen_entry::( - entries, - grouping.usage_target(SERVE_PROM_IDENTIFIER), - Some(usage), - prom.block, + reads +} + +// Owner hyperparameter group (config shared, usage per call). +// usage: netuid +// legacy sources: OwnerHyperparamRateLimit * tempo, +// LastRateLimitedBlock per OwnerHyperparamUpdate +fn build_owner_hparams(groups: &mut Vec, commits: &mut Vec) -> u64 { + let mut reads: u64 = 0; + groups.push(GroupConfig { + id: GROUP_OWNER_HPARAMS, + name: b"owner-hparams".to_vec(), + sharing: GroupSharing::ConfigOnly, + members: HYPERPARAMETERS + .iter() + .filter_map(|h| identifier_for_hyperparameter(*h)) + .collect(), + }); + + let group_target = RateLimitTarget::Group(GROUP_OWNER_HPARAMS); + reads += 1; + push_limit_commit_if_non_zero( + commits, + group_target, + u64::from(OwnerHyperparamRateLimit::::get()), + None, + ); + + reads += + last_seen_helpers::collect_last_seen_from_last_rate_limited_block( + commits, + |key| match key { + RateLimitKey::OwnerHyperparamUpdate(netuid, hyper) => { + let Some(identifier) = identifier_for_hyperparameter(hyper) else { + return None; + }; + Some(( + RateLimitTarget::Transaction(identifier.identifier()), + Some(RateLimitUsageKey::Subnet(netuid)), + )) + } + _ => None, + }, ); - } reads } -fn import_evm_entries( - grouping: &Grouping, - entries: &mut LastSeenEntries, -) -> u64 { +// Staking ops group (config + usage shared, all ops 1 block). +// usage: coldkey+hotkey+netuid +// legacy sources: TxRateLimit (reset every block for staking ops), StakingOperationRateLimiter +fn build_staking_ops(groups: &mut Vec, commits: &mut Vec) -> u64 { + groups.push(GroupConfig { + id: GROUP_STAKING_OPS, + name: b"staking-ops".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + MigratedCall::subtensor(2, false), // add_stake + MigratedCall::subtensor(88, false), // add_stake_limit + MigratedCall::subtensor(3, true), // remove_stake + MigratedCall::subtensor(89, true), // remove_stake_limit + MigratedCall::subtensor(103, true), // remove_stake_full_limit + MigratedCall::subtensor(85, false), // move_stake + MigratedCall::subtensor(86, true), // transfer_stake + MigratedCall::subtensor(87, false), // swap_stake + MigratedCall::subtensor(90, false), // swap_stake_limit + ], + }); + + push_limit_commit_if_non_zero(commits, RateLimitTarget::Group(GROUP_STAKING_OPS), 1, None); + + // we don't need to migrate last-seen since the limiter is reset every block. + + 0 +} + +// Standalone swap_hotkey. +// usage: account +// legacy sources: TxRateLimit, LastRateLimitedBlock per LastTxBlock +fn build_swap_hotkey(commits: &mut Vec) -> u64 { let mut reads: u64 = 0; - for (netuid, uid, (_, block)) in AssociatedEvmAddress::::iter() { - reads += 1; - if block == 0 { - continue; - } - record_last_seen_entry::( - entries, - grouping.usage_target(subtensor_identifier(93)), - Some(RateLimitUsageKey::SubnetNeuron { netuid, uid }), - block, + let target = + RateLimitTarget::Transaction(TransactionIdentifier::new(SUBTENSOR_PALLET_INDEX, 70)); + + reads += 1; + push_limit_commit_if_non_zero(commits, target, TxRateLimit::::get(), None); + + reads += + last_seen_helpers::collect_last_seen_from_last_rate_limited_block( + commits, + |key| match key { + RateLimitKey::LastTxBlock(account) => { + Some((target, Some(RateLimitUsageKey::Account(account)))) + } + _ => None, + }, ); - } + reads } -fn convert_target(target: &RateLimitTarget) -> RateLimitTarget -where - T: SubtensorConfig + pallet_rate_limiting::Config, - RateLimitUsageKey: Into<::UsageKey>, -{ - match target { - RateLimitTarget::Transaction(identifier) => RateLimitTarget::Transaction(*identifier), - RateLimitTarget::Group(id) => RateLimitTarget::Group((*id).saturated_into()), - } +// Standalone set_childkey_take. +// usage: account+netuid +// legacy sources: TxChildkeyTakeRateLimit, TransactionKeyLastBlock per SetChildkeyTake +fn build_childkey_take(commits: &mut Vec) -> u64 { + let mut reads: u64 = 0; + let target = + RateLimitTarget::Transaction(TransactionIdentifier::new(SUBTENSOR_PALLET_INDEX, 75)); + reads += 1; + push_limit_commit_if_non_zero( + commits, + target, + TxChildkeyTakeRateLimit::::get(), + None, + ); + + reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::SetChildkeyTake, + ); + + reads } -fn write_limits(limits: &LimitEntries) -> u64 -where - T: SubtensorConfig + pallet_rate_limiting::Config, - RateLimitUsageKey: Into<::UsageKey>, -{ - let mut writes: u64 = 0; - for (identifier, limit) in limits.iter() { - let target = convert_target::(identifier); - pallet_rate_limiting::Limits::::insert(target, limit.clone()); - writes += 1; - } - writes +// Standalone set_children. +// usage: account+netuid +// legacy sources: SET_CHILDREN_RATE_LIMIT (constant 150), +// TransactionKeyLastBlock per SetChildren +fn build_set_children(commits: &mut Vec) -> u64 { + let mut reads: u64 = 0; + let target = + RateLimitTarget::Transaction(TransactionIdentifier::new(SUBTENSOR_PALLET_INDEX, 67)); + push_limit_commit_if_non_zero(commits, target, SET_CHILDREN_RATE_LIMIT, None); + + reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::SetChildren, + ); + + reads } -fn write_last_seen(entries: &LastSeenEntries) -> u64 -where - T: SubtensorConfig + pallet_rate_limiting::Config, - RateLimitUsageKey: Into<::UsageKey>, -{ - let mut writes: u64 = 0; - for ((identifier, usage), block) in entries.iter() { - let target = convert_target::(identifier); - let usage_key = usage.clone().map(Into::into); - pallet_rate_limiting::LastSeen::::insert(target, usage_key, *block); - writes += 1; - } - writes +// Standalone set_weights_version_key. +// scope: netuid +// usage: account+netuid +// legacy sources: WeightsVersionKeyRateLimit * tempo, +// TransactionKeyLastBlock per SetWeightsVersionKey +fn build_weights_version_key(commits: &mut Vec) -> u64 { + let mut reads: u64 = 0; + let target = + RateLimitTarget::Transaction(TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, 6)); + reads += 1; + push_limit_commit_if_non_zero( + commits, + target, + WeightsVersionKeyRateLimit::::get(), + None, + ); + + reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::SetWeightsVersionKey, + ); + + reads } -fn write_groups(grouping: &Grouping) -> u64 -where - T: SubtensorConfig + pallet_rate_limiting::Config, - RateLimitUsageKey: Into<::UsageKey>, -{ - let mut writes: u64 = 0; +// Standalone set_sn_owner_hotkey. +// usage: netuid +// legacy sources: DefaultSetSNOwnerHotkeyRateLimit, +// LastRateLimitedBlock per SetSNOwnerHotkey +fn build_sn_owner_hotkey(commits: &mut Vec) -> u64 { + let mut reads: u64 = 0; + let target = + RateLimitTarget::Transaction(TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, 67)); + reads += 1; + push_limit_commit_if_non_zero( + commits, + target, + pallet_subtensor::pallet::DefaultSetSNOwnerHotkeyRateLimit::::get(), + None, + ); - for detail in &grouping.details { - let Ok(name) = GroupNameOf::::try_from(detail.name.clone()) else { - warn!( - "rate-limiting migration: group name exceeds bounds, skipping id {}", - detail.id - ); - continue; - }; - let group_id = detail.id.saturated_into::(); - let stored = RateLimitGroup { - id: group_id, - name: name.clone(), - sharing: detail.sharing, - }; + reads += + last_seen_helpers::collect_last_seen_from_last_rate_limited_block( + commits, + |key| match key { + RateLimitKey::SetSNOwnerHotkey(netuid) => { + Some((target, Some(RateLimitUsageKey::Subnet(netuid)))) + } + _ => None, + }, + ); - pallet_rate_limiting::Groups::::insert(group_id, stored); - pallet_rate_limiting::GroupNameIndex::::insert(name, group_id); - writes += 2; - } + reads +} - for (group, members) in &grouping.members { - let group_id = (*group).saturated_into::(); - let Ok(bounded) = GroupMembersOf::::try_from(members.clone()) else { - warn!( - "rate-limiting migration: group {} has too many members, skipping assignment", - group - ); +// Standalone associate_evm_key. +// usage: netuid+neuron +// legacy sources: EvmKeyAssociateRateLimit, AssociatedEvmAddress +fn build_associate_evm(commits: &mut Vec) -> u64 { + let mut reads: u64 = 0; + let target = + RateLimitTarget::Transaction(TransactionIdentifier::new(SUBTENSOR_PALLET_INDEX, 93)); + reads += 1; + push_limit_commit_if_non_zero( + commits, + target, + ::EvmKeyAssociateRateLimit::get(), + None, + ); + + for (netuid, uid, (_, block)) in AssociatedEvmAddress::::iter() { + reads += 1; + let Some(block) = block_number::(block) else { continue; }; - pallet_rate_limiting::GroupMembers::::insert(group_id, bounded); - writes += 1; + commits.push(Commit { + target, + kind: CommitKind::LastSeen(MigratedLastSeen { + block, + usage: Some(RateLimitUsageKey::SubnetNeuron { netuid, uid }), + }), + }); } - for (identifier, info) in &grouping.assignments { - let group_id = info.id.saturated_into::(); - pallet_rate_limiting::CallGroups::::insert(*identifier, group_id); - writes += 1; + reads +} - if grouping.read_only.get(identifier).copied().unwrap_or(false) { - pallet_rate_limiting::CallReadOnly::::insert(*identifier, true); - writes += 1; - } - } +// Standalone mechanism count. +// usage: account+netuid +// legacy sources: MechanismCountSetRateLimit, +// TransactionKeyLastBlock per MechanismCountUpdate +// sudo_set_mechanism_count +fn build_mechanism_count(commits: &mut Vec) -> u64 { + let mut reads: u64 = 0; + let target = + RateLimitTarget::Transaction(TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, 76)); + reads += 1; + push_limit_commit_if_non_zero( + commits, + target, + MechanismCountSetRateLimit::::get(), + None, + ); - let next_group_id = grouping.next_group_id.saturated_into::(); - pallet_rate_limiting::NextGroupId::::put(next_group_id); - writes += 1; + reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::MechanismCountUpdate, + ); - writes + reads } -fn block_number(value: u64) -> Option> { - if value == 0 { - return None; - } - Some(value.saturated_into::>()) +// Standalone mechanism emission. +// usage: account+netuid +// legacy sources: MechanismEmissionRateLimit, +// TransactionKeyLastBlock per MechanismEmission +// sudo_set_mechanism_emission_split +fn build_mechanism_emission(commits: &mut Vec) -> u64 { + let mut reads: u64 = 0; + let target = + RateLimitTarget::Transaction(TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, 77)); + reads += 1; + push_limit_commit_if_non_zero( + commits, + target, + MechanismEmissionRateLimit::::get(), + None, + ); + + reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::MechanismEmission, + ); + + reads } -fn set_global_limit( - limits: &mut LimitEntries, - target: RateLimitTarget, - span: BlockNumberFor, -) { - if let Some((_, config)) = limits.iter_mut().find(|(id, _)| *id == target) { - *config = RateLimit::global(RateLimitKind::Exact(span)); - } else { - limits.push((target, RateLimit::global(RateLimitKind::Exact(span)))); - } +// Standalone trim_to_max_allowed_uids. +// usage: account+netuid +// legacy sources: MaxUidsTrimmingRateLimit, +// TransactionKeyLastBlock per MaxUidsTrimming +// sudo_trim_to_max_allowed_uids +fn build_trim_max_uids(commits: &mut Vec) -> u64 { + let mut reads: u64 = 0; + let target = + RateLimitTarget::Transaction(TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, 78)); + reads += 1; + push_limit_commit_if_non_zero( + commits, + target, + MaxUidsTrimmingRateLimit::::get(), + None, + ); + + reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::MaxUidsTrimming, + ); + + reads } -fn set_scoped_limit( - limits: &mut LimitEntries, +struct Commit { target: RateLimitTarget, - scope: NetUid, - span: BlockNumberFor, -) { - if let Some((_, config)) = limits.iter_mut().find(|(id, _)| *id == target) { - match config { - RateLimit::Global(_) => { - *config = RateLimit::scoped_single(scope, RateLimitKind::Exact(span)); - } - RateLimit::Scoped(map) => { - map.insert(scope, RateLimitKind::Exact(span)); - } - } - } else { - limits.push(( - target, - RateLimit::scoped_single(scope, RateLimitKind::Exact(span)), - )); - } + kind: CommitKind, } -fn record_last_seen_entry( - entries: &mut LastSeenEntries, - target: RateLimitTarget, - usage: Option>, - block: u64, -) { - let Some(block_number) = block_number::(block) else { - return; - }; +enum CommitKind { + Limit(MigratedLimit), + LastSeen(MigratedLastSeen), +} + +struct MigratedLimit { + span: BlockNumberFor, + scope: Option, +} - let key = (target, usage); - if let Some((_, existing)) = entries.iter_mut().find(|(entry_key, _)| *entry_key == key) { - if block_number > *existing { - *existing = block_number; +struct MigratedLastSeen { + block: BlockNumberFor, + usage: Option>, +} + +struct GroupConfig { + id: GroupId, + name: Vec, + sharing: GroupSharing, + members: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct MigratedCall { + identifier: TransactionIdentifier, + read_only: bool, +} + +impl MigratedCall { + const fn new(pallet_index: u8, call_index: u8, read_only: bool) -> Self { + Self { + identifier: TransactionIdentifier::new(pallet_index, call_index), + read_only, } - } else { - entries.push((key, block_number)); } -} -/// Runtime hook that executes the rate-limiting migration. -pub struct Migration(PhantomData); + const fn subtensor(call_index: u8, read_only: bool) -> Self { + Self::new(SUBTENSOR_PALLET_INDEX, call_index, read_only) + } -impl frame_support::traits::OnRuntimeUpgrade for Migration -where - T: SubtensorConfig + pallet_rate_limiting::Config, - RateLimitUsageKey: Into<::UsageKey>, -{ - fn on_runtime_upgrade() -> Weight { - migrate_rate_limiting::() + const fn admin(call_index: u8, read_only: bool) -> Self { + Self::new(ADMIN_UTILS_PALLET_INDEX, call_index, read_only) } -} -const fn admin_utils_identifier(call_index: u8) -> TransactionIdentifier { - TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, call_index) + pub fn identifier(&self) -> TransactionIdentifier { + self.identifier + } } -const fn subtensor_identifier(call_index: u8) -> TransactionIdentifier { - TransactionIdentifier::new(SUBTENSOR_PALLET_INDEX, call_index) +fn push_limit_commit_if_non_zero( + commits: &mut Vec, + target: RateLimitTarget, + span: u64, + scope: Option, +) { + if let Some(span) = block_number::(span) { + commits.push(Commit { + target, + kind: CommitKind::Limit(MigratedLimit { span, scope }), + }); + } } -/// Returns the `TransactionIdentifier` for the admin-utils extrinsic that controls `hparam`. -/// -/// Only hyperparameters that are currently rate-limited (i.e. routed through -/// `ensure_sn_owner_or_root_with_limits`) are mapped; others return `None`. -pub fn identifier_for_hyperparameter(hparam: Hyperparameter) -> Option { - use Hyperparameter::*; +mod last_seen_helpers { + use core::mem::discriminant; - let identifier = match hparam { - ServingRateLimit => admin_utils_identifier(3), - MaxDifficulty => admin_utils_identifier(5), - AdjustmentAlpha => admin_utils_identifier(9), - ImmunityPeriod => admin_utils_identifier(13), - MinAllowedWeights => admin_utils_identifier(14), - MaxAllowedUids => admin_utils_identifier(15), - Kappa => admin_utils_identifier(16), - Rho => admin_utils_identifier(17), - ActivityCutoff => admin_utils_identifier(18), - PowRegistrationAllowed => admin_utils_identifier(20), - MinBurn => admin_utils_identifier(22), - MaxBurn => admin_utils_identifier(23), - BondsMovingAverage => admin_utils_identifier(26), - BondsPenalty => admin_utils_identifier(60), - CommitRevealEnabled => admin_utils_identifier(49), - LiquidAlphaEnabled => admin_utils_identifier(50), - AlphaValues => admin_utils_identifier(51), - WeightCommitInterval => admin_utils_identifier(57), - TransferEnabled => admin_utils_identifier(61), - AlphaSigmoidSteepness => admin_utils_identifier(68), - Yuma3Enabled => admin_utils_identifier(69), - BondsResetEnabled => admin_utils_identifier(70), - ImmuneNeuronLimit => admin_utils_identifier(72), - RecycleOrBurn => admin_utils_identifier(80), - _ => return None, - }; + use super::*; - Some(identifier) -} + pub(super) fn collect_last_seen_from_last_rate_limited_block( + commits: &mut Vec, + map: impl Fn( + RateLimitKey, + ) -> Option<( + RateLimitTarget, + Option>, + )>, + ) -> u64 { + let mut reads: u64 = 0; + + for (key, block) in LastRateLimitedBlock::::iter() { + reads += 1; + let Some((target, usage)) = map(key) else { + continue; + }; + let Some(block) = block_number::(block) else { + continue; + }; + commits.push(Commit { + target, + kind: CommitKind::LastSeen(MigratedLastSeen { block, usage }), + }); + } -/// Returns the `TransactionIdentifier` for the extrinsic associated with the given transaction -/// type, mirroring current rate-limit enforcement. -pub fn identifier_for_transaction_type(tx: TransactionType) -> Option { - use TransactionType::*; - - let identifier = match tx { - SetChildren => subtensor_identifier(67), - SetChildkeyTake => subtensor_identifier(75), - RegisterNetwork => subtensor_identifier(59), - SetWeightsVersionKey => admin_utils_identifier(6), - SetSNOwnerHotkey => admin_utils_identifier(67), - OwnerHyperparamUpdate(hparam) => return identifier_for_hyperparameter(hparam), - MechanismCountUpdate => admin_utils_identifier(76), - MechanismEmission => admin_utils_identifier(77), - MaxUidsTrimming => admin_utils_identifier(78), - Unknown => return None, - _ => return None, - }; + reads + } - Some(identifier) + pub(super) fn collect_last_seen_from_transaction_key_last_block( + commits: &mut Vec, + target: RateLimitTarget, + tx_filter: TransactionType, + ) -> u64 { + let mut reads: u64 = 0; + + for ((account, netuid, tx_kind), block) in TransactionKeyLastBlock::::iter() { + reads += 1; + let tx = TransactionType::from(tx_kind); + if discriminant(&tx) != discriminant(&tx_filter) { + continue; + } + let Some(usage) = usage_key_from_transaction_type(tx, &account, netuid) else { + continue; + }; + let Some(block) = block_number::(block) else { + continue; + }; + commits.push(Commit { + target, + kind: CommitKind::LastSeen(MigratedLastSeen { + block, + usage: Some(usage), + }), + }); + } + + reads + } } -/// Produces the usage key for a `TransactionType` that was stored in `TransactionKeyLastBlock`. -pub fn usage_key_from_transaction_type( +// Produces the usage key for a `TransactionType` that was stored in `TransactionKeyLastBlock`. +fn usage_key_from_transaction_type( tx: TransactionType, account: &AccountId, netuid: NetUid, -) -> Option> -where - AccountId: Parameter + Clone, -{ +) -> Option> { match tx { TransactionType::MechanismCountUpdate | TransactionType::MaxUidsTrimming @@ -1219,13 +961,58 @@ where } } +// Returns the migrated call wrapper for the admin-utils extrinsic that controls `hparam`. +// +// Only hyperparameters that are currently rate-limited (i.e. routed through +// `ensure_sn_owner_or_root_with_limits`) are mapped; others return `None`. +fn identifier_for_hyperparameter(hparam: Hyperparameter) -> Option { + use Hyperparameter::*; + + let identifier = match hparam { + ServingRateLimit => MigratedCall::admin(3, false), + MaxDifficulty => MigratedCall::admin(5, false), + AdjustmentAlpha => MigratedCall::admin(9, false), + ImmunityPeriod => MigratedCall::admin(13, false), + MinAllowedWeights => MigratedCall::admin(14, false), + MaxAllowedUids => MigratedCall::admin(15, false), + Kappa => MigratedCall::admin(16, false), + Rho => MigratedCall::admin(17, false), + ActivityCutoff => MigratedCall::admin(18, false), + PowRegistrationAllowed => MigratedCall::admin(20, false), + MinBurn => MigratedCall::admin(22, false), + MaxBurn => MigratedCall::admin(23, false), + BondsMovingAverage => MigratedCall::admin(26, false), + BondsPenalty => MigratedCall::admin(60, false), + CommitRevealEnabled => MigratedCall::admin(49, false), + LiquidAlphaEnabled => MigratedCall::admin(50, false), + AlphaValues => MigratedCall::admin(51, false), + WeightCommitInterval => MigratedCall::admin(57, false), + TransferEnabled => MigratedCall::admin(61, false), + AlphaSigmoidSteepness => MigratedCall::admin(68, false), + Yuma3Enabled => MigratedCall::admin(69, false), + BondsResetEnabled => MigratedCall::admin(70, false), + ImmuneNeuronLimit => MigratedCall::admin(72, false), + RecycleOrBurn => MigratedCall::admin(80, false), + _ => return None, + }; + + Some(identifier) +} + +fn block_number(value: u64) -> Option> { + if value == 0 { + return None; + } + Some(value.saturated_into::>()) +} + #[cfg(test)] mod tests { use sp_io::TestExternalities; use sp_runtime::traits::{SaturatedConversion, Zero}; use super::*; - use crate::{AccountId, BuildStorage, Runtime}; + use crate::BuildStorage; const ACCOUNT: [u8; 32] = [7u8; 32]; const DELEGATE_TAKE_GROUP_ID: GroupId = GROUP_DELEGATE_TAKE; @@ -1244,20 +1031,11 @@ mod tests { fn maps_hyperparameters() { assert_eq!( identifier_for_hyperparameter(Hyperparameter::ServingRateLimit), - Some(admin_utils_identifier(3)) + Some(MigratedCall::admin(3, false)) ); assert!(identifier_for_hyperparameter(Hyperparameter::MaxWeightLimit).is_none()); } - #[test] - fn maps_transaction_types() { - assert_eq!( - identifier_for_transaction_type(TransactionType::SetChildren), - Some(subtensor_identifier(67)) - ); - assert!(identifier_for_transaction_type(TransactionType::Unknown).is_none()); - } - #[test] fn migration_populates_limits_last_seen_and_groups() { new_test_ext().execute_with(|| { @@ -1271,13 +1049,14 @@ mod tests { 5, ); - let weight = migrate_rate_limiting::(); + let weight = migrate_rate_limiting(); assert!(!weight.is_zero()); assert!(pallet_subtensor::HasMigrationRun::::get( MIGRATION_NAME )); - let tx_target = RateLimitTarget::Transaction(subtensor_identifier(70)); + let tx_target = + RateLimitTarget::Transaction(MigratedCall::subtensor(70, false).identifier()); let delegate_group = RateLimitTarget::Group(DELEGATE_TAKE_GROUP_ID); assert_eq!( @@ -1304,7 +1083,9 @@ mod tests { assert_eq!(group.id, DELEGATE_TAKE_GROUP_ID); assert_eq!(group.name.as_slice(), b"delegate-take"); assert_eq!( - pallet_rate_limiting::CallGroups::::get(subtensor_identifier(66)), + pallet_rate_limiting::CallGroups::::get( + MigratedCall::subtensor(66, false).identifier() + ), Some(DELEGATE_TAKE_GROUP_ID) ); assert_eq!(pallet_rate_limiting::NextGroupId::::get(), 6); @@ -1318,7 +1099,7 @@ mod tests { pallet_subtensor::TxRateLimit::::put(99); let base_weight = ::DbWeight::get().reads(1); - let weight = migrate_rate_limiting::(); + let weight = migrate_rate_limiting(); assert_eq!(weight, base_weight); assert!( diff --git a/runtime/tests/rate_limiting_migration.rs b/runtime/tests/rate_limiting_migration.rs index 40f68151ff..e97beb8fc1 100644 --- a/runtime/tests/rate_limiting_migration.rs +++ b/runtime/tests/rate_limiting_migration.rs @@ -2,19 +2,16 @@ use frame_support::traits::OnRuntimeUpgrade; use frame_system::pallet_prelude::BlockNumberFor; +use pallet_rate_limiting::{RateLimit, RateLimitKind, RateLimitTarget, TransactionIdentifier}; +use pallet_subtensor::{HasMigrationRun, LastRateLimitedBlock, RateLimitKey}; +use sp_runtime::traits::SaturatedConversion; +use subtensor_runtime_common::NetUid; + use node_subtensor_runtime::{ BuildStorage, Runtime, RuntimeGenesisConfig, SubtensorModule, System, rate_limiting, - rate_limiting::migration::{Migration, identifier_for_transaction_type}, + rate_limiting::migration::{GROUP_REGISTER_NETWORK, MIGRATION_NAME, Migration}, }; -use pallet_rate_limiting::{RateLimit, RateLimitKind, RateLimitTarget}; -use pallet_subtensor::{ - HasMigrationRun, LastRateLimitedBlock, RateLimitKey, utils::rate_limiting::TransactionType, -}; -use sp_runtime::traits::SaturatedConversion; -use subtensor_runtime_common::NetUid; -type GroupId = ::GroupId; -const MIGRATION_NAME: &[u8] = b"migrate_rate_limiting"; type AccountId = ::AccountId; type UsageKey = rate_limiting::RateLimitUsageKey; @@ -28,20 +25,6 @@ fn new_test_ext() -> sp_io::TestExternalities { ext } -fn resolve_target( - identifier: pallet_rate_limiting::TransactionIdentifier, -) -> RateLimitTarget { - if let Some(group) = pallet_rate_limiting::CallGroups::::get(identifier) { - RateLimitTarget::Group(group) - } else { - RateLimitTarget::Transaction(identifier) - } -} - -fn exact_span(span: u64) -> RateLimitKind> { - RateLimitKind::Exact(span.saturated_into()) -} - #[test] fn migrates_global_register_network_last_seen() { new_test_ext().execute_with(|| { @@ -54,9 +37,7 @@ fn migrates_global_register_network_last_seen() { // Run migration. Migration::::on_runtime_upgrade(); - let identifier = - identifier_for_transaction_type(TransactionType::RegisterNetwork).expect("identifier"); - let target = resolve_target(identifier); + let target = RateLimitTarget::Group(GROUP_REGISTER_NETWORK); // LastSeen preserved globally (usage = None). let stored = pallet_rate_limiting::LastSeen::::get(target, None::) @@ -77,13 +58,11 @@ fn sn_owner_hotkey_limit_not_tempo_scaled_and_last_seen_preserved() { Migration::::on_runtime_upgrade(); - let identifier = - identifier_for_transaction_type(TransactionType::SetSNOwnerHotkey).expect("identifier"); - let target = resolve_target(identifier); + let target = RateLimitTarget::Transaction(TransactionIdentifier::new(19, 67)); // Limit should remain the fixed default (50400 blocks), not tempo-scaled. let limit = pallet_rate_limiting::Limits::::get(target).expect("limit stored"); - assert!(matches!(limit, RateLimit::Global(kind) if kind == exact_span(50_400))); + assert!(matches!(limit, RateLimit::Global(kind) if kind == RateLimitKind::Exact(50_400))); // LastSeen preserved per subnet. let usage: Option<::UsageKey> = From 953288fd8aec8e752031a6bb10e8908eb727eba1 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 10 Dec 2025 17:28:55 +0300 Subject: [PATCH 38/95] Fix swap-keys rate-limiting migration --- pallets/rate-limiting/src/mock.rs | 6 +- pallets/rate-limiting/src/tx_extension.rs | 30 ++++++-- pallets/rate-limiting/src/types.rs | 10 +-- runtime/src/rate_limiting/migration.rs | 27 +++++--- runtime/src/rate_limiting/mod.rs | 84 +++++++++++++++-------- runtime/tests/rate_limiting_behavior.rs | 52 ++++++++------ 6 files changed, 135 insertions(+), 74 deletions(-) diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index a4aefd4357..98769bf6a6 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -105,12 +105,12 @@ impl pallet_rate_limiting::RateLimitScopeResolver for TestUsageResolver { - fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option { + fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option> { match call { RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { - (*block_span).try_into().ok() + (*block_span).try_into().ok().map(|key| vec![key]) } - RuntimeCall::RateLimiting(_) => Some(1), + RuntimeCall::RateLimiting(_) => Some(vec![1]), _ => None, } } diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index 42737e6fd4..5e5cc45f0c 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -13,7 +13,7 @@ use frame_support::{ }, }; use scale_info::TypeInfo; -use sp_std::{marker::PhantomData, result::Result}; +use sp_std::{marker::PhantomData, result::Result, vec, vec::Vec}; use crate::{ Config, LastSeen, Pallet, @@ -94,12 +94,12 @@ where type Implicit = (); type Val = Option<( RateLimitTarget<>::GroupId>, - Option<>::UsageKey>, + Option>::UsageKey>>, bool, )>; type Pre = Option<( RateLimitTarget<>::GroupId>, - Option<>::UsageKey>, + Option>::UsageKey>>, bool, )>; @@ -155,7 +155,14 @@ where return Ok((ValidTransaction::default(), None, origin)); } - let within_limit = Pallet::::within_span(&usage_target, &usage, block_span); + let usage_keys: Vec>::UsageKey>> = match usage.clone() { + None => vec![None], + Some(keys) => keys.into_iter().map(Some).collect(), + }; + + let within_limit = usage_keys + .iter() + .all(|key| Pallet::::within_span(&usage_target, key, block_span)); if !within_limit { return Err(TransactionValidityError::Invalid( @@ -194,7 +201,18 @@ where return Ok(()); } let block_number = frame_system::Pallet::::block_number(); - LastSeen::::insert(target, usage, block_number); + match usage { + None => LastSeen::::insert( + target, + None::<>::UsageKey>, + block_number, + ), + Some(keys) => { + for key in keys { + LastSeen::::insert(target, Some(key), block_number); + } + } + } } } Ok(()) @@ -253,7 +271,7 @@ mod tests { ) -> Result< ( sp_runtime::transaction_validity::ValidTransaction, - Option<(RateLimitTarget, Option, bool)>, + Option<(RateLimitTarget, Option>, bool)>, RuntimeOrigin, ), TransactionValidityError, diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index cab93a6b1a..c7bc2f91f7 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -2,7 +2,7 @@ use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; -use sp_std::collections::btree_map::BTreeMap; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; /// Resolves the optional identifier within which a rate limit applies and can optionally adjust /// enforcement behaviour. @@ -53,9 +53,11 @@ impl BypassDecision { /// Resolves the optional usage tracking key applied when enforcing limits. pub trait RateLimitUsageResolver { - /// Returns `Some(usage)` when usage should be tracked per-key, or `None` for global usage - /// tracking. - fn context(origin: &Origin, call: &Call) -> Option; + /// Returns `Some(keys)` to track usage per key, or `None` for global usage tracking. + /// + /// When multiple keys are returned, the rate limit is enforced against each key and all are + /// recorded on success. + fn context(origin: &Origin, call: &Call) -> Option>; } /// Identifies a runtime call by pallet and extrinsic indices. diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index a8466fc782..3e53916deb 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -43,6 +43,7 @@ const GROUP_WEIGHTS_SUBNET: GroupId = 2; pub const GROUP_REGISTER_NETWORK: GroupId = 3; const GROUP_OWNER_HPARAMS: GroupId = 4; const GROUP_STAKING_OPS: GroupId = 5; +const GROUP_SWAP_KEYS: GroupId = 6; // `set_children` is rate-limited to once every 150 blocks, it's hard-coded in the legacy code. const SET_CHILDREN_RATE_LIMIT: u64 = 150; @@ -144,9 +145,9 @@ fn commits() -> (Vec, Vec, u64) { reads += build_register_network(&mut groups, &mut commits); reads += build_owner_hparams(&mut groups, &mut commits); reads += build_staking_ops(&mut groups, &mut commits); + reads += build_swap_keys(&mut groups, &mut commits); // standalone - reads += build_swap_hotkey(&mut commits); reads += build_childkey_take(&mut commits); reads += build_set_children(&mut commits); reads += build_weights_version_key(&mut commits); @@ -571,14 +572,23 @@ fn build_staking_ops(groups: &mut Vec, commits: &mut Vec) - 0 } -// Standalone swap_hotkey. -// usage: account +// Swap hotkey/coldkey share the lock and usage; swap_coldkey bypasses enforcement but records +// usage. +// usage: account (coldkey) // legacy sources: TxRateLimit, LastRateLimitedBlock per LastTxBlock -fn build_swap_hotkey(commits: &mut Vec) -> u64 { +fn build_swap_keys(groups: &mut Vec, commits: &mut Vec) -> u64 { let mut reads: u64 = 0; - let target = - RateLimitTarget::Transaction(TransactionIdentifier::new(SUBTENSOR_PALLET_INDEX, 70)); + groups.push(GroupConfig { + id: GROUP_SWAP_KEYS, + name: b"swap-keys".to_vec(), + sharing: GroupSharing::ConfigAndUsage, + members: vec![ + MigratedCall::subtensor(70, false), // swap_hotkey + MigratedCall::subtensor(71, false), // swap_coldkey + ], + }); + let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); reads += 1; push_limit_commit_if_non_zero(commits, target, TxRateLimit::::get(), None); @@ -1055,8 +1065,7 @@ mod tests { MIGRATION_NAME )); - let tx_target = - RateLimitTarget::Transaction(MigratedCall::subtensor(70, false).identifier()); + let tx_target = RateLimitTarget::Group(GROUP_SWAP_KEYS); let delegate_group = RateLimitTarget::Group(DELEGATE_TAKE_GROUP_ID); assert_eq!( @@ -1088,7 +1097,7 @@ mod tests { ), Some(DELEGATE_TAKE_GROUP_ID) ); - assert_eq!(pallet_rate_limiting::NextGroupId::::get(), 6); + assert_eq!(pallet_rate_limiting::NextGroupId::::get(), 7); }); } diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 1658f03a6b..8b4a07a926 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -7,6 +7,7 @@ use pallet_rate_limiting::{RateLimitScopeResolver, RateLimitUsageResolver}; use pallet_subtensor::{Call as SubtensorCall, Tempo}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; +use sp_std::{vec, vec::Vec}; use subtensor_runtime_common::{BlockNumber, MechId, NetUid}; use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; @@ -107,11 +108,15 @@ impl RateLimitScopeResolver for } fn should_bypass(origin: &RuntimeOrigin, call: &RuntimeCall) -> BypassDecision { - if matches!(origin.clone().into(), Ok(RawOrigin::Root)) { - return BypassDecision::bypass_and_skip(); - } - if let RuntimeCall::SubtensorModule(inner) = call { + if matches!(origin.clone().into(), Ok(RawOrigin::Root)) { + // swap_coldkey should record last-seen but never fail; other root calls skip. + if matches!(inner, SubtensorCall::swap_coldkey { .. }) { + return BypassDecision::bypass_and_record(); + } + return BypassDecision::bypass_and_skip(); + } + match inner { SubtensorCall::set_childkey_take { hotkey, @@ -129,7 +134,8 @@ impl RateLimitScopeResolver for } SubtensorCall::add_stake { .. } | SubtensorCall::add_stake_limit { .. } - | SubtensorCall::decrease_take { .. } => { + | SubtensorCall::decrease_take { .. } + | SubtensorCall::swap_coldkey { .. } => { return BypassDecision::bypass_and_record(); } SubtensorCall::reveal_weights { netuid, .. } @@ -179,21 +185,37 @@ pub struct UsageResolver; impl RateLimitUsageResolver> for UsageResolver { - fn context(origin: &RuntimeOrigin, call: &RuntimeCall) -> Option> { + fn context( + origin: &RuntimeOrigin, + call: &RuntimeCall, + ) -> Option>> { match call { RuntimeCall::SubtensorModule(inner) => match inner { - SubtensorCall::swap_hotkey { .. } => { - signed_origin(origin).map(RateLimitUsageKey::::Account) + SubtensorCall::swap_coldkey { new_coldkey, .. } => { + Some(vec![RateLimitUsageKey::::Account( + new_coldkey.clone(), + )]) + } + SubtensorCall::swap_hotkey { new_hotkey, .. } => { + // Record against the coldkey (enforcement) and the new hotkey to mirror legacy + // writes. + let coldkey = signed_origin(origin)?; + Some(vec![ + RateLimitUsageKey::::Account(coldkey), + RateLimitUsageKey::::Account(new_hotkey.clone()), + ]) } SubtensorCall::increase_take { hotkey, .. } => { - Some(RateLimitUsageKey::::Account(hotkey.clone())) + Some(vec![RateLimitUsageKey::::Account( + hotkey.clone(), + )]) } SubtensorCall::set_childkey_take { hotkey, netuid, .. } | SubtensorCall::set_children { hotkey, netuid, .. } => { - Some(RateLimitUsageKey::::AccountSubnet { + Some(vec![RateLimitUsageKey::::AccountSubnet { account: hotkey.clone(), netuid: *netuid, - }) + }]) } SubtensorCall::set_weights { netuid, .. } | SubtensorCall::commit_weights { netuid, .. } @@ -201,10 +223,10 @@ impl RateLimitUsageResolver { let (_, uid) = neuron_identity(origin, *netuid)?; - Some(RateLimitUsageKey::::SubnetNeuron { + Some(vec![RateLimitUsageKey::::SubnetNeuron { netuid: *netuid, uid, - }) + }]) } // legacy implementation still used netuid only, but it was recalculating it using // mecid, so switching to netuid AND mecid is logical here @@ -214,28 +236,30 @@ impl RateLimitUsageResolver { let (_, uid) = neuron_identity(origin, *netuid)?; - Some(RateLimitUsageKey::::SubnetMechanismNeuron { - netuid: *netuid, - mecid: *mecid, - uid, - }) + Some(vec![ + RateLimitUsageKey::::SubnetMechanismNeuron { + netuid: *netuid, + mecid: *mecid, + uid, + }, + ]) } SubtensorCall::serve_axon { netuid, .. } | SubtensorCall::serve_axon_tls { netuid, .. } => { let hotkey = signed_origin(origin)?; - Some(RateLimitUsageKey::::AccountSubnetServing { + Some(vec![RateLimitUsageKey::::AccountSubnetServing { account: hotkey, netuid: *netuid, endpoint: ServingEndpoint::Axon, - }) + }]) } SubtensorCall::serve_prometheus { netuid, .. } => { let hotkey = signed_origin(origin)?; - Some(RateLimitUsageKey::::AccountSubnetServing { + Some(vec![RateLimitUsageKey::::AccountSubnetServing { account: hotkey, netuid: *netuid, endpoint: ServingEndpoint::Prometheus, - }) + }]) } SubtensorCall::associate_evm_key { netuid, .. } => { let hotkey = signed_origin(origin)?; @@ -243,10 +267,10 @@ impl RateLimitUsageResolver::SubnetNeuron { + Some(vec![RateLimitUsageKey::::SubnetNeuron { netuid: *netuid, uid, - }) + }]) } // Staking calls share a group lock; only add_* write usage, the rest are read-only. // Keep the usage key granular so the lock applies per (coldkey, hotkey, netuid). @@ -276,31 +300,31 @@ impl RateLimitUsageResolver { let coldkey = signed_origin(origin)?; - Some(RateLimitUsageKey::::ColdkeyHotkeySubnet { + Some(vec![RateLimitUsageKey::::ColdkeyHotkeySubnet { coldkey, hotkey: hotkey.clone(), netuid: *netuid, - }) + }]) } _ => None, }, RuntimeCall::AdminUtils(inner) => { if let Some(netuid) = owner_hparam_netuid(inner) { - Some(RateLimitUsageKey::::Subnet(netuid)) + Some(vec![RateLimitUsageKey::::Subnet(netuid)]) } else { match inner { AdminUtilsCall::sudo_set_sn_owner_hotkey { netuid, .. } => { - Some(RateLimitUsageKey::::Subnet(*netuid)) + Some(vec![RateLimitUsageKey::::Subnet(*netuid)]) } AdminUtilsCall::sudo_set_weights_version_key { netuid, .. } | AdminUtilsCall::sudo_set_mechanism_count { netuid, .. } | AdminUtilsCall::sudo_set_mechanism_emission_split { netuid, .. } | AdminUtilsCall::sudo_trim_to_max_allowed_uids { netuid, .. } => { let who = signed_origin(origin)?; - Some(RateLimitUsageKey::::AccountSubnet { + Some(vec![RateLimitUsageKey::::AccountSubnet { account: who, netuid: *netuid, - }) + }]) } _ => None, } diff --git a/runtime/tests/rate_limiting_behavior.rs b/runtime/tests/rate_limiting_behavior.rs index b9a39814c1..e9a76acf1c 100644 --- a/runtime/tests/rate_limiting_behavior.rs +++ b/runtime/tests/rate_limiting_behavior.rs @@ -66,7 +66,7 @@ fn parity_check( now: u64, call: RuntimeCall, origin: RuntimeOrigin, - usage_override: Option, + usage_override: Option>, scope_override: Option, legacy_check: F, ) where @@ -82,9 +82,8 @@ fn parity_check( let identifier = TransactionIdentifier::from_call::(&call).expect("identifier for call"); let scope = scope_override.or_else(|| RuntimeScopeResolver::context(&origin, &call)); - let usage: Option<::UsageKey> = usage_override - .map(Into::into) - .or_else(|| RuntimeUsageResolver::context(&origin, &call).map(Into::into)); + let usage: Option::UsageKey>> = + usage_override.or_else(|| RuntimeUsageResolver::context(&origin, &call)); let target = resolve_target(identifier); // Use the runtime-adjusted span (handles tempo scaling for admin-utils). @@ -97,28 +96,37 @@ fn parity_check( .unwrap_or_default(); let span_u64: u64 = span.saturated_into(); - let within = pallet_rate_limiting::Pallet::::is_within_limit( - &origin.clone().into(), - &call, - &identifier, - &scope, - &usage, - ) - .expect("pallet rate limit result"); + let usage_keys: Vec::UsageKey>> = match usage { + None => vec![None], + Some(keys) => keys.into_iter().map(Some).collect(), + }; + + let within = usage_keys.iter().all(|key| { + pallet_rate_limiting::Pallet::::is_within_limit( + &origin.clone().into(), + &call, + &identifier, + &scope, + key, + ) + .expect("pallet rate limit result") + }); assert_eq!(within, legacy_check(), "parity at now for {:?}", identifier); // Advance beyond the span and re-check (span==0 treated as allow). let advance: BlockNumberFor = span.saturating_add(exact_span(1)); System::set_block_number(System::block_number().saturating_add(advance)); - let within_after = pallet_rate_limiting::Pallet::::is_within_limit( - &origin.into(), - &call, - &identifier, - &scope, - &usage, - ) - .expect("pallet rate limit result (after)"); + let within_after = usage_keys.iter().all(|key| { + pallet_rate_limiting::Pallet::::is_within_limit( + &origin.clone().into(), + &call, + &identifier, + &scope, + key, + ) + .expect("pallet rate limit result (after)") + }); assert!( within_after || span_u64 == 0, "parity after window for {:?}", @@ -326,7 +334,7 @@ fn weights_and_hparam_parity() { }); let origin = RuntimeOrigin::signed(hot.clone()); let scope = Some(netuid); - let usage = Some(UsageKey::SubnetNeuron { netuid, uid }); + let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); let legacy_weights = || SubtensorModule::check_rate_limit(netuid.into(), uid, now); parity_check( @@ -414,7 +422,7 @@ fn associate_evm_key_parity() { signature: ecdsa::Signature::from_raw([0u8; 65]), }); let origin = RuntimeOrigin::signed(hot.clone()); - let usage = Some(UsageKey::SubnetNeuron { netuid, uid }); + let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); let scope = Some(netuid); let limit = ::EvmKeyAssociateRateLimit::get(); let legacy = || { From 30d5e324e3cda2baf158b9e988ebd4cb2c8605a2 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 11 Dec 2025 17:24:08 +0300 Subject: [PATCH 39/95] Decouple rate-limiting migration from legacy types --- runtime/src/rate_limiting/legacy.rs | 305 ++++++++++++++++++++++++ runtime/src/rate_limiting/migration.rs | 309 +++++++++++++------------ runtime/src/rate_limiting/mod.rs | 1 + 3 files changed, 464 insertions(+), 151 deletions(-) create mode 100644 runtime/src/rate_limiting/legacy.rs diff --git a/runtime/src/rate_limiting/legacy.rs b/runtime/src/rate_limiting/legacy.rs new file mode 100644 index 0000000000..5311a982ab --- /dev/null +++ b/runtime/src/rate_limiting/legacy.rs @@ -0,0 +1,305 @@ +use codec::{Decode, Encode}; +use frame_support::{Identity, migration::storage_key_iter}; +use runtime_common::prod_or_fast; +use scale_info::TypeInfo; +use sp_io::{ + hashing::twox_128, + storage::{self as io_storage, next_key}, +}; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; +use subtensor_runtime_common::NetUid; + +use super::AccountId; +use crate::{ + SubtensorInitialNetworkRateLimit, SubtensorInitialServingRateLimit, + SubtensorInitialTxChildKeyTakeRateLimit, SubtensorInitialTxDelegateTakeRateLimit, + SubtensorInitialTxRateLimit, +}; + +pub use types::{Hyperparameter, RateLimitKey, TransactionType}; + +const PALLET_PREFIX: &[u8] = b"SubtensorModule"; +const BLAKE2_128_PREFIX_LEN: usize = 16; + +pub mod storage { + use super::*; + + pub fn serving_rate_limits() -> (BTreeMap, u64) { + let items: Vec<_> = + storage_key_iter::(PALLET_PREFIX, b"ServingRateLimit").collect(); + let reads = items.len() as u64; + (items.into_iter().collect(), reads) + } + + pub fn weights_set_rate_limits() -> (BTreeMap, u64) { + let items: Vec<_> = + storage_key_iter::(PALLET_PREFIX, b"WeightsSetRateLimit") + .collect(); + let reads = items.len() as u64; + (items.into_iter().collect(), reads) + } + + pub fn tx_rate_limit() -> (u64, u64) { + value_with_default(b"TxRateLimit", defaults::tx_rate_limit()) + } + + pub fn tx_delegate_take_rate_limit() -> (u64, u64) { + value_with_default( + b"TxDelegateTakeRateLimit", + defaults::tx_delegate_take_rate_limit(), + ) + } + + pub fn tx_childkey_take_rate_limit() -> (u64, u64) { + value_with_default( + b"TxChildkeyTakeRateLimit", + defaults::tx_childkey_take_rate_limit(), + ) + } + + pub fn network_rate_limit() -> (u64, u64) { + value_with_default(b"NetworkRateLimit", defaults::network_rate_limit()) + } + + pub fn owner_hyperparam_rate_limit() -> (u64, u64) { + let (value, reads) = value_with_default::( + b"OwnerHyperparamRateLimit", + defaults::owner_hyperparam_rate_limit(), + ); + (u64::from(value), reads) + } + + pub fn weights_version_key_rate_limit() -> (u64, u64) { + value_with_default( + b"WeightsVersionKeyRateLimit", + defaults::weights_version_key_rate_limit(), + ) + } + + pub fn last_rate_limited_blocks() -> (Vec<(RateLimitKey, u64)>, u64) { + let entries: Vec<_> = storage_key_iter::, u64, Identity>( + PALLET_PREFIX, + b"LastRateLimitedBlock", + ) + .collect(); + let reads = entries.len() as u64; + (entries, reads) + } + + pub fn transaction_key_last_block() -> (Vec<((AccountId, NetUid, u16), u64)>, u64) { + let prefix = storage_prefix(PALLET_PREFIX, b"TransactionKeyLastBlock"); + let mut cursor = prefix.clone(); + let mut entries = Vec::new(); + + while let Some(next) = next_key(&cursor) { + if !next.starts_with(&prefix) { + break; + } + if let Some(value) = io_storage::get(&next) { + let key_bytes = &next[prefix.len()..]; + if let (Some(key), Some(decoded_value)) = ( + decode_transaction_key(key_bytes), + decode_value::(&value), + ) { + entries.push((key, decoded_value)); + } + } + cursor = next; + } + + let reads = entries.len() as u64; + (entries, reads) + } + + fn storage_prefix(pallet: &[u8], storage: &[u8]) -> Vec { + [twox_128(pallet), twox_128(storage)].concat() + } + + fn value_with_default(storage_name: &[u8], default: V) -> (V, u64) { + let key = storage_prefix(PALLET_PREFIX, storage_name); + let value = io_storage::get(&key) + .and_then(|bytes| Decode::decode(&mut &bytes[..]).ok()) + .unwrap_or(default); + (value, 1) + } + + fn decode_value(bytes: &[u8]) -> Option { + Decode::decode(&mut &bytes[..]).ok() + } + + fn decode_transaction_key( + encoded: &[u8], + ) -> Option<(AccountId, NetUid, u16)> { + if encoded.len() < BLAKE2_128_PREFIX_LEN { + return None; + } + let mut slice = &encoded[BLAKE2_128_PREFIX_LEN..]; + let account = AccountId::decode(&mut slice).ok()?; + let netuid = NetUid::decode(&mut slice).ok()?; + let tx_kind = u16::decode(&mut slice).ok()?; + + Some((account, netuid, tx_kind)) + } +} + +pub mod defaults { + use super::*; + + pub fn serving_rate_limit() -> u64 { + SubtensorInitialServingRateLimit::get() + } + + pub fn weights_set_rate_limit() -> u64 { + 100 + } + + pub fn tx_rate_limit() -> u64 { + SubtensorInitialTxRateLimit::get() + } + + pub fn tx_delegate_take_rate_limit() -> u64 { + SubtensorInitialTxDelegateTakeRateLimit::get() + } + + pub fn tx_childkey_take_rate_limit() -> u64 { + SubtensorInitialTxChildKeyTakeRateLimit::get() + } + + pub fn network_rate_limit() -> u64 { + if cfg!(feature = "pow-faucet") { + 0 + } else { + SubtensorInitialNetworkRateLimit::get() + } + } + + pub fn owner_hyperparam_rate_limit() -> u16 { + 2 + } + + pub fn weights_version_key_rate_limit() -> u64 { + 5 + } + + pub fn sn_owner_hotkey_rate_limit() -> u64 { + 50_400 + } + + pub fn mechanism_count_rate_limit() -> u64 { + prod_or_fast!(7_200, 1) + } + + pub fn mechanism_emission_rate_limit() -> u64 { + prod_or_fast!(7_200, 1) + } + + pub fn max_uids_trimming_rate_limit() -> u64 { + prod_or_fast!(30 * 7_200, 1) + } +} + +pub mod types { + use super::*; + + #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo)] + pub enum RateLimitKey { + #[codec(index = 0)] + SetSNOwnerHotkey(NetUid), + #[codec(index = 1)] + OwnerHyperparamUpdate(NetUid, Hyperparameter), + #[codec(index = 2)] + NetworkLastRegistered, + #[codec(index = 3)] + LastTxBlock(AccountId), + #[codec(index = 4)] + LastTxBlockChildKeyTake(AccountId), + #[codec(index = 5)] + LastTxBlockDelegateTake(AccountId), + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + #[non_exhaustive] + pub enum TransactionType { + SetChildren, + SetChildkeyTake, + Unknown, + RegisterNetwork, + SetWeightsVersionKey, + SetSNOwnerHotkey, + OwnerHyperparamUpdate(Hyperparameter), + MechanismCountUpdate, + MechanismEmission, + MaxUidsTrimming, + } + + impl From for TransactionType { + fn from(value: u16) -> Self { + match value { + 0 => TransactionType::SetChildren, + 1 => TransactionType::SetChildkeyTake, + 3 => TransactionType::RegisterNetwork, + 4 => TransactionType::SetWeightsVersionKey, + 5 => TransactionType::SetSNOwnerHotkey, + 6 => TransactionType::OwnerHyperparamUpdate(Hyperparameter::Unknown), + 7 => TransactionType::MechanismCountUpdate, + 8 => TransactionType::MechanismEmission, + 9 => TransactionType::MaxUidsTrimming, + _ => TransactionType::Unknown, + } + } + } + + impl From for u16 { + fn from(tx_type: TransactionType) -> Self { + match tx_type { + TransactionType::SetChildren => 0, + TransactionType::SetChildkeyTake => 1, + TransactionType::Unknown => 2, + TransactionType::RegisterNetwork => 3, + TransactionType::SetWeightsVersionKey => 4, + TransactionType::SetSNOwnerHotkey => 5, + TransactionType::OwnerHyperparamUpdate(_) => 6, + TransactionType::MechanismCountUpdate => 7, + TransactionType::MechanismEmission => 8, + TransactionType::MaxUidsTrimming => 9, + } + } + } + + #[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, Debug, TypeInfo)] + #[non_exhaustive] + pub enum Hyperparameter { + Unknown = 0, + ServingRateLimit = 1, + MaxDifficulty = 2, + AdjustmentAlpha = 3, + MaxWeightLimit = 4, + ImmunityPeriod = 5, + MinAllowedWeights = 6, + Kappa = 7, + Rho = 8, + ActivityCutoff = 9, + PowRegistrationAllowed = 10, + MinBurn = 11, + MaxBurn = 12, + BondsMovingAverage = 13, + BondsPenalty = 14, + CommitRevealEnabled = 15, + LiquidAlphaEnabled = 16, + AlphaValues = 17, + WeightCommitInterval = 18, + TransferEnabled = 19, + AlphaSigmoidSteepness = 20, + Yuma3Enabled = 21, + BondsResetEnabled = 22, + ImmuneNeuronLimit = 23, + RecycleOrBurn = 24, + MaxAllowedUids = 25, + } + + impl From for TransactionType { + fn from(param: Hyperparameter) -> Self { + Self::OwnerHyperparamUpdate(param) + } + } +} diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index 3e53916deb..ccd3e6402f 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -1,18 +1,14 @@ use core::{convert::TryFrom, marker::PhantomData}; -use frame_support::{BoundedBTreeSet, BoundedVec, traits::Get, weights::Weight}; +use frame_support::{BoundedBTreeSet, BoundedVec, weights::Weight}; use frame_system::pallet_prelude::BlockNumberFor; use log::{info, warn}; use pallet_rate_limiting::{ GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitTarget, TransactionIdentifier, }; use pallet_subtensor::{ - self, AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, - LastRateLimitedBlock, LastUpdate, MaxUidsTrimmingRateLimit, MechanismCountSetRateLimit, - MechanismEmissionRateLimit, NetworkRateLimit, OwnerHyperparamRateLimit, Pallet, Prometheus, - RateLimitKey, ServingRateLimit, TransactionKeyLastBlock, TxChildkeyTakeRateLimit, - TxDelegateTakeRateLimit, TxRateLimit, WeightsVersionKeyRateLimit, - utils::rate_limiting::{Hyperparameter, TransactionType}, + self, AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, LastUpdate, + Pallet, Prometheus, }; use sp_runtime::traits::SaturatedConversion; use sp_std::{ @@ -22,7 +18,13 @@ use sp_std::{ }; use subtensor_runtime_common::NetUid; -use super::{AccountId, RateLimitUsageKey, Runtime}; +use super::{ + AccountId, RateLimitUsageKey, Runtime, + legacy::{ + Hyperparameter, RateLimitKey, TransactionType, defaults as legacy_defaults, + storage as legacy_storage, + }, +}; type GroupId = ::GroupId; type GroupNameOf = BoundedVec::MaxGroupNameLength>; @@ -136,26 +138,25 @@ pub fn migrate_rate_limiting() -> Weight { fn commits() -> (Vec, Vec, u64) { let mut groups = Vec::new(); let mut commits = Vec::new(); - let mut reads: u64 = 0; // grouped - reads += build_serving(&mut groups, &mut commits); - reads += build_delegate_take(&mut groups, &mut commits); - reads += build_weights(&mut groups, &mut commits); - reads += build_register_network(&mut groups, &mut commits); - reads += build_owner_hparams(&mut groups, &mut commits); - reads += build_staking_ops(&mut groups, &mut commits); - reads += build_swap_keys(&mut groups, &mut commits); + let mut reads = build_serving(&mut groups, &mut commits); + reads = reads.saturating_add(build_delegate_take(&mut groups, &mut commits)); + reads = reads.saturating_add(build_weights(&mut groups, &mut commits)); + reads = reads.saturating_add(build_register_network(&mut groups, &mut commits)); + reads = reads.saturating_add(build_owner_hparams(&mut groups, &mut commits)); + reads = reads.saturating_add(build_staking_ops(&mut groups, &mut commits)); + reads = reads.saturating_add(build_swap_keys(&mut groups, &mut commits)); // standalone - reads += build_childkey_take(&mut commits); - reads += build_set_children(&mut commits); - reads += build_weights_version_key(&mut commits); - reads += build_sn_owner_hotkey(&mut commits); - reads += build_associate_evm(&mut commits); - reads += build_mechanism_count(&mut commits); - reads += build_mechanism_emission(&mut commits); - reads += build_trim_max_uids(&mut commits); + reads = reads.saturating_add(build_childkey_take(&mut commits)); + reads = reads.saturating_add(build_set_children(&mut commits)); + reads = reads.saturating_add(build_weights_version_key(&mut commits)); + reads = reads.saturating_add(build_sn_owner_hotkey(&mut commits)); + reads = reads.saturating_add(build_associate_evm(&mut commits)); + reads = reads.saturating_add(build_mechanism_count(&mut commits)); + reads = reads.saturating_add(build_mechanism_emission(&mut commits)); + reads = reads.saturating_add(build_trim_max_uids(&mut commits)); (groups, commits, reads) } @@ -299,30 +300,35 @@ fn build_serving(groups: &mut Vec, commits: &mut Vec) -> u6 ], }); + let (serving_limits, serving_reads) = legacy_storage::serving_rate_limits(); + reads = reads.saturating_add(serving_reads); // Limits per netuid (written to the group target). - reads += 1; // Merge live subnets (which may rely on default rate-limit values) with any legacy entries that // exist only in storage, so we migrate both current and previously stored netuids without // duplicates. let mut netuids = Pallet::::get_all_subnet_netuids(); - for (netuid, _) in ServingRateLimit::::iter() { + for (&netuid, _) in &serving_limits { if !netuids.contains(&netuid) { netuids.push(netuid); } } + let default_limit = legacy_defaults::serving_rate_limit(); for netuid in netuids { - reads += 1; + reads = reads.saturating_add(1); push_limit_commit_if_non_zero( commits, RateLimitTarget::Group(GROUP_SERVE), - Pallet::::get_serving_rate_limit(netuid), + serving_limits + .get(&netuid) + .copied() + .unwrap_or(default_limit), Some(netuid), ); } // Axon last-seen (group-shared usage). for (netuid, hotkey, axon) in Axons::::iter() { - reads += 1; + reads = reads.saturating_add(1); if let Some(block) = block_number::(axon.block) { commits.push(Commit { target: RateLimitTarget::Group(GROUP_SERVE), @@ -340,7 +346,7 @@ fn build_serving(groups: &mut Vec, commits: &mut Vec) -> u6 // Prometheus last-seen (group-shared usage). for (netuid, hotkey, prom) in Prometheus::::iter() { - reads += 1; + reads = reads.saturating_add(1); if let Some(block) = block_number::(prom.block) { commits.push(Commit { target: RateLimitTarget::Group(GROUP_SERVE), @@ -375,15 +381,11 @@ fn build_delegate_take(groups: &mut Vec, commits: &mut Vec) }); let target = RateLimitTarget::Group(GROUP_DELEGATE_TAKE); - reads += 1; - push_limit_commit_if_non_zero( - commits, - target, - TxDelegateTakeRateLimit::::get(), - None, - ); + let (delegate_take_limit, delegate_reads) = legacy_storage::tx_delegate_take_rate_limit(); + reads = reads.saturating_add(delegate_reads); + push_limit_commit_if_non_zero(commits, target, delegate_take_limit, None); - reads += + reads = reads.saturating_add( last_seen_helpers::collect_last_seen_from_last_rate_limited_block( commits, |key| match key { @@ -392,7 +394,8 @@ fn build_delegate_take(groups: &mut Vec, commits: &mut Vec) } _ => None, }, - ); + ), + ); reads } @@ -422,19 +425,24 @@ fn build_weights(groups: &mut Vec, commits: &mut Vec) -> u6 ], }); - reads += 1; + let (weights_limits, weights_reads) = legacy_storage::weights_set_rate_limits(); + reads = reads.saturating_add(weights_reads); + let default_limit = legacy_defaults::weights_set_rate_limit(); for netuid in Pallet::::get_all_subnet_netuids() { - reads += 1; + reads = reads.saturating_add(1); push_limit_commit_if_non_zero( commits, RateLimitTarget::Group(GROUP_WEIGHTS_SUBNET), - Pallet::::get_weights_set_rate_limit(netuid), + weights_limits + .get(&netuid) + .copied() + .unwrap_or(default_limit), Some(netuid), ); } for (index, blocks) in LastUpdate::::iter() { - reads += 1; + reads = reads.saturating_add(1); let (netuid, mecid) = Pallet::::get_netuid_and_subid(index).unwrap_or((NetUid::ROOT, 0.into())); for (uid, last_block) in blocks.into_iter().enumerate() { @@ -484,25 +492,26 @@ fn build_register_network(groups: &mut Vec, commits: &mut Vec::get(), None); + let (network_rate_limit, network_reads) = legacy_storage::network_rate_limit(); + reads = reads.saturating_add(network_reads); + push_limit_commit_if_non_zero(commits, target, network_rate_limit, None); - reads += + reads = reads.saturating_add( last_seen_helpers::collect_last_seen_from_last_rate_limited_block( commits, |key| match key { RateLimitKey::NetworkLastRegistered => Some((target, None)), _ => None, }, - ); + ), + ); reads } // Owner hyperparameter group (config shared, usage per call). // usage: netuid -// legacy sources: OwnerHyperparamRateLimit * tempo, -// LastRateLimitedBlock per OwnerHyperparamUpdate +// legacy sources: OwnerHyperparamRateLimit * tempo, LastRateLimitedBlock per OwnerHyperparamUpdate fn build_owner_hparams(groups: &mut Vec, commits: &mut Vec) -> u64 { let mut reads: u64 = 0; groups.push(GroupConfig { @@ -516,15 +525,11 @@ fn build_owner_hparams(groups: &mut Vec, commits: &mut Vec) }); let group_target = RateLimitTarget::Group(GROUP_OWNER_HPARAMS); - reads += 1; - push_limit_commit_if_non_zero( - commits, - group_target, - u64::from(OwnerHyperparamRateLimit::::get()), - None, - ); + let (owner_limit, owner_reads) = legacy_storage::owner_hyperparam_rate_limit(); + reads = reads.saturating_add(owner_reads); + push_limit_commit_if_non_zero(commits, group_target, owner_limit, None); - reads += + reads = reads.saturating_add( last_seen_helpers::collect_last_seen_from_last_rate_limited_block( commits, |key| match key { @@ -539,7 +544,8 @@ fn build_owner_hparams(groups: &mut Vec, commits: &mut Vec) } _ => None, }, - ); + ), + ); reads } @@ -589,10 +595,11 @@ fn build_swap_keys(groups: &mut Vec, commits: &mut Vec) -> }); let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); - reads += 1; - push_limit_commit_if_non_zero(commits, target, TxRateLimit::::get(), None); + let (tx_rate_limit, tx_reads) = legacy_storage::tx_rate_limit(); + reads = reads.saturating_add(tx_reads); + push_limit_commit_if_non_zero(commits, target, tx_rate_limit, None); - reads += + reads = reads.saturating_add( last_seen_helpers::collect_last_seen_from_last_rate_limited_block( commits, |key| match key { @@ -601,7 +608,8 @@ fn build_swap_keys(groups: &mut Vec, commits: &mut Vec) -> } _ => None, }, - ); + ), + ); reads } @@ -613,18 +621,16 @@ fn build_childkey_take(commits: &mut Vec) -> u64 { let mut reads: u64 = 0; let target = RateLimitTarget::Transaction(TransactionIdentifier::new(SUBTENSOR_PALLET_INDEX, 75)); - reads += 1; - push_limit_commit_if_non_zero( - commits, - target, - TxChildkeyTakeRateLimit::::get(), - None, - ); + let (childkey_limit, childkey_reads) = legacy_storage::tx_childkey_take_rate_limit(); + reads = reads.saturating_add(childkey_reads); + push_limit_commit_if_non_zero(commits, target, childkey_limit, None); - reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( - commits, - target, - TransactionType::SetChildkeyTake, + reads = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::SetChildkeyTake, + ), ); reads @@ -632,18 +638,19 @@ fn build_childkey_take(commits: &mut Vec) -> u64 { // Standalone set_children. // usage: account+netuid -// legacy sources: SET_CHILDREN_RATE_LIMIT (constant 150), -// TransactionKeyLastBlock per SetChildren +// legacy sources: SET_CHILDREN_RATE_LIMIT (constant 150), TransactionKeyLastBlock per SetChildren fn build_set_children(commits: &mut Vec) -> u64 { let mut reads: u64 = 0; let target = RateLimitTarget::Transaction(TransactionIdentifier::new(SUBTENSOR_PALLET_INDEX, 67)); push_limit_commit_if_non_zero(commits, target, SET_CHILDREN_RATE_LIMIT, None); - reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( - commits, - target, - TransactionType::SetChildren, + reads = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::SetChildren, + ), ); reads @@ -658,18 +665,17 @@ fn build_weights_version_key(commits: &mut Vec) -> u64 { let mut reads: u64 = 0; let target = RateLimitTarget::Transaction(TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, 6)); - reads += 1; - push_limit_commit_if_non_zero( - commits, - target, - WeightsVersionKeyRateLimit::::get(), - None, - ); + let (weights_version_limit, weights_version_reads) = + legacy_storage::weights_version_key_rate_limit(); + reads = reads.saturating_add(weights_version_reads); + push_limit_commit_if_non_zero(commits, target, weights_version_limit, None); - reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( - commits, - target, - TransactionType::SetWeightsVersionKey, + reads = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::SetWeightsVersionKey, + ), ); reads @@ -677,21 +683,16 @@ fn build_weights_version_key(commits: &mut Vec) -> u64 { // Standalone set_sn_owner_hotkey. // usage: netuid -// legacy sources: DefaultSetSNOwnerHotkeyRateLimit, -// LastRateLimitedBlock per SetSNOwnerHotkey +// legacy sources: DefaultSetSNOwnerHotkeyRateLimit, LastRateLimitedBlock per SetSNOwnerHotkey fn build_sn_owner_hotkey(commits: &mut Vec) -> u64 { let mut reads: u64 = 0; let target = RateLimitTarget::Transaction(TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, 67)); + let sn_owner_limit = legacy_defaults::sn_owner_hotkey_rate_limit(); reads += 1; - push_limit_commit_if_non_zero( - commits, - target, - pallet_subtensor::pallet::DefaultSetSNOwnerHotkeyRateLimit::::get(), - None, - ); + push_limit_commit_if_non_zero(commits, target, sn_owner_limit, None); - reads += + reads = reads.saturating_add( last_seen_helpers::collect_last_seen_from_last_rate_limited_block( commits, |key| match key { @@ -700,7 +701,8 @@ fn build_sn_owner_hotkey(commits: &mut Vec) -> u64 { } _ => None, }, - ); + ), + ); reads } @@ -721,7 +723,7 @@ fn build_associate_evm(commits: &mut Vec) -> u64 { ); for (netuid, uid, (_, block)) in AssociatedEvmAddress::::iter() { - reads += 1; + reads = reads.saturating_add(1); let Some(block) = block_number::(block) else { continue; }; @@ -739,25 +741,21 @@ fn build_associate_evm(commits: &mut Vec) -> u64 { // Standalone mechanism count. // usage: account+netuid -// legacy sources: MechanismCountSetRateLimit, -// TransactionKeyLastBlock per MechanismCountUpdate +// legacy sources: MechanismCountSetRateLimit, TransactionKeyLastBlock per MechanismCountUpdate // sudo_set_mechanism_count fn build_mechanism_count(commits: &mut Vec) -> u64 { let mut reads: u64 = 0; let target = RateLimitTarget::Transaction(TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, 76)); - reads += 1; - push_limit_commit_if_non_zero( - commits, - target, - MechanismCountSetRateLimit::::get(), - None, - ); + let mechanism_limit = legacy_defaults::mechanism_count_rate_limit(); + push_limit_commit_if_non_zero(commits, target, mechanism_limit, None); - reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( - commits, - target, - TransactionType::MechanismCountUpdate, + reads = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::MechanismCountUpdate, + ), ); reads @@ -765,25 +763,21 @@ fn build_mechanism_count(commits: &mut Vec) -> u64 { // Standalone mechanism emission. // usage: account+netuid -// legacy sources: MechanismEmissionRateLimit, -// TransactionKeyLastBlock per MechanismEmission +// legacy sources: MechanismEmissionRateLimit, TransactionKeyLastBlock per MechanismEmission // sudo_set_mechanism_emission_split fn build_mechanism_emission(commits: &mut Vec) -> u64 { let mut reads: u64 = 0; let target = RateLimitTarget::Transaction(TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, 77)); - reads += 1; - push_limit_commit_if_non_zero( - commits, - target, - MechanismEmissionRateLimit::::get(), - None, - ); + let emission_limit = legacy_defaults::mechanism_emission_rate_limit(); + push_limit_commit_if_non_zero(commits, target, emission_limit, None); - reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( - commits, - target, - TransactionType::MechanismEmission, + reads = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::MechanismEmission, + ), ); reads @@ -791,25 +785,21 @@ fn build_mechanism_emission(commits: &mut Vec) -> u64 { // Standalone trim_to_max_allowed_uids. // usage: account+netuid -// legacy sources: MaxUidsTrimmingRateLimit, -// TransactionKeyLastBlock per MaxUidsTrimming +// legacy sources: MaxUidsTrimmingRateLimit, TransactionKeyLastBlock per MaxUidsTrimming // sudo_trim_to_max_allowed_uids fn build_trim_max_uids(commits: &mut Vec) -> u64 { let mut reads: u64 = 0; let target = RateLimitTarget::Transaction(TransactionIdentifier::new(ADMIN_UTILS_PALLET_INDEX, 78)); - reads += 1; - push_limit_commit_if_non_zero( - commits, - target, - MaxUidsTrimmingRateLimit::::get(), - None, - ); + let trim_limit = legacy_defaults::max_uids_trimming_rate_limit(); + push_limit_commit_if_non_zero(commits, target, trim_limit, None); - reads += last_seen_helpers::collect_last_seen_from_transaction_key_last_block( - commits, - target, - TransactionType::MaxUidsTrimming, + reads = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::MaxUidsTrimming, + ), ); reads @@ -899,8 +889,9 @@ mod last_seen_helpers { ) -> u64 { let mut reads: u64 = 0; - for (key, block) in LastRateLimitedBlock::::iter() { - reads += 1; + let (entries, iter_reads) = legacy_storage::last_rate_limited_blocks(); + reads = reads.saturating_add(iter_reads); + for (key, block) in entries { let Some((target, usage)) = map(key) else { continue; }; @@ -923,8 +914,9 @@ mod last_seen_helpers { ) -> u64 { let mut reads: u64 = 0; - for ((account, netuid, tx_kind), block) in TransactionKeyLastBlock::::iter() { - reads += 1; + let (entries, iter_reads) = legacy_storage::transaction_key_last_block(); + reads = reads.saturating_add(iter_reads); + for ((account, netuid, tx_kind), block) in entries { let tx = TransactionType::from(tx_kind); if discriminant(&tx) != discriminant(&tx_filter) { continue; @@ -1018,7 +1010,9 @@ fn block_number(value: u64) -> Option> { #[cfg(test)] mod tests { + use codec::Encode; use sp_io::TestExternalities; + use sp_io::{hashing::twox_128, storage}; use sp_runtime::traits::{SaturatedConversion, Zero}; use super::*; @@ -1026,6 +1020,7 @@ mod tests { const ACCOUNT: [u8; 32] = [7u8; 32]; const DELEGATE_TAKE_GROUP_ID: GroupId = GROUP_DELEGATE_TAKE; + const PALLET_PREFIX: &[u8] = b"SubtensorModule"; fn new_test_ext() -> TestExternalities { sp_tracing::try_init_simple(); @@ -1052,12 +1047,9 @@ mod tests { let account: AccountId = ACCOUNT.into(); pallet_subtensor::HasMigrationRun::::remove(MIGRATION_NAME); - pallet_subtensor::TxRateLimit::::put(10); - pallet_subtensor::TxDelegateTakeRateLimit::::put(3); - pallet_subtensor::LastRateLimitedBlock::::insert( - RateLimitKey::LastTxBlock(account.clone()), - 5, - ); + put_legacy_value(b"TxRateLimit", 10u64); + put_legacy_value(b"TxDelegateTakeRateLimit", 3u64); + put_last_rate_limited_block(RateLimitKey::LastTxBlock(account.clone()), 5); let weight = migrate_rate_limiting(); assert!(!weight.is_zero()); @@ -1105,7 +1097,7 @@ mod tests { fn migration_skips_when_already_run() { new_test_ext().execute_with(|| { pallet_subtensor::HasMigrationRun::::insert(MIGRATION_NAME, true); - pallet_subtensor::TxRateLimit::::put(99); + put_legacy_value(b"TxRateLimit", 99u64); let base_weight = ::DbWeight::get().reads(1); let weight = migrate_rate_limiting(); @@ -1123,4 +1115,19 @@ mod tests { ); }); } + + fn put_legacy_value(storage_name: &[u8], value: impl Encode) { + let key = storage_key(storage_name); + storage::set(&key, &value.encode()); + } + + fn put_last_rate_limited_block(key: RateLimitKey, block: u64) { + let mut storage_key = storage_key(b"LastRateLimitedBlock"); + storage_key.extend(key.encode()); + storage::set(&storage_key, &block.encode()); + } + + fn storage_key(storage_name: &[u8]) -> Vec { + [twox_128(PALLET_PREFIX), twox_128(storage_name)].concat() + } } diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 8b4a07a926..5c0b7a8ec1 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -12,6 +12,7 @@ use subtensor_runtime_common::{BlockNumber, MechId, NetUid}; use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; +mod legacy; pub mod migration; #[derive( From 483be0ca1d312262861184111ff246c3c8864cb4 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 12 Dec 2025 17:34:55 +0300 Subject: [PATCH 40/95] Add rate limiting interface --- Cargo.lock | 12 + Cargo.toml | 1 + common/src/lib.rs | 1 + common/src/rate_limiting.rs | 19 ++ pallets/rate-limiting-interface/Cargo.toml | 24 ++ pallets/rate-limiting-interface/README.md | 3 + pallets/rate-limiting-interface/src/lib.rs | 267 +++++++++++++++++++++ pallets/rate-limiting/Cargo.toml | 2 + pallets/rate-limiting/src/benchmarking.rs | 6 +- pallets/rate-limiting/src/lib.rs | 58 ++++- pallets/rate-limiting/src/mock.rs | 2 +- pallets/rate-limiting/src/tx_extension.rs | 5 +- pallets/rate-limiting/src/types.rs | 158 +----------- runtime/src/lib.rs | 2 +- runtime/src/rate_limiting/migration.rs | 17 +- runtime/tests/rate_limiting_behavior.rs | 6 +- runtime/tests/rate_limiting_migration.rs | 4 +- 17 files changed, 401 insertions(+), 186 deletions(-) create mode 100644 common/src/rate_limiting.rs create mode 100644 pallets/rate-limiting-interface/Cargo.toml create mode 100644 pallets/rate-limiting-interface/README.md create mode 100644 pallets/rate-limiting-interface/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5a8f830542..2c85cc1aad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10314,6 +10314,7 @@ dependencies = [ "frame-support", "frame-system", "parity-scale-codec", + "rate-limiting-interface", "scale-info", "serde", "sp-core", @@ -13727,6 +13728,17 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rate-limiting-interface" +version = "0.1.0" +dependencies = [ + "frame-support", + "parity-scale-codec", + "scale-info", + "serde", + "sp-std", +] + [[package]] name = "raw-cpuid" version = "11.6.0" diff --git a/Cargo.toml b/Cargo.toml index 8bc3f488e6..48630c5f1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ subtensor-runtime-common = { default-features = false, path = "common" } subtensor-swap-interface = { default-features = false, path = "pallets/swap-interface" } subtensor-transaction-fee = { default-features = false, path = "pallets/transaction-fee" } subtensor-chain-extensions = { default-features = false, path = "chain-extensions" } +rate-limiting-interface = { default-features = false, path = "pallets/rate-limiting-interface" } ed25519-dalek = { version = "2.1.0", default-features = false } async-trait = "0.1" diff --git a/common/src/lib.rs b/common/src/lib.rs index 28a33c2ae6..0e4a500d69 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -17,6 +17,7 @@ use subtensor_macros::freeze_struct; pub use currency::*; mod currency; +pub mod rate_limiting; /// Balance of an account. pub type Balance = u64; diff --git a/common/src/rate_limiting.rs b/common/src/rate_limiting.rs new file mode 100644 index 0000000000..7051e5aeaa --- /dev/null +++ b/common/src/rate_limiting.rs @@ -0,0 +1,19 @@ +//! Shared rate-limiting types. + +/// Identifier type for rate-limiting groups. +pub type GroupId = u32; + +/// Group id for serving-related calls. +pub const GROUP_SERVE: GroupId = 0; +/// Group id for delegate-take related calls. +pub const GROUP_DELEGATE_TAKE: GroupId = 1; +/// Group id for subnet weight-setting calls. +pub const GROUP_WEIGHTS_SUBNET: GroupId = 2; +/// Group id for network registration calls. +pub const GROUP_REGISTER_NETWORK: GroupId = 3; +/// Group id for owner hyperparameter calls. +pub const GROUP_OWNER_HPARAMS: GroupId = 4; +/// Group id for staking operations. +pub const GROUP_STAKING_OPS: GroupId = 5; +/// Group id for key swap calls. +pub const GROUP_SWAP_KEYS: GroupId = 6; diff --git a/pallets/rate-limiting-interface/Cargo.toml b/pallets/rate-limiting-interface/Cargo.toml new file mode 100644 index 0000000000..8f352d0c58 --- /dev/null +++ b/pallets/rate-limiting-interface/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "rate-limiting-interface" +version = "0.1.0" +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +codec = { workspace = true, features = ["derive"], default-features = false } +frame-support = { workspace = true, default-features = false } +scale-info = { workspace = true, features = ["derive"], default-features = false } +serde = { workspace = true, features = ["derive"], default-features = false } +sp-std = { workspace = true, default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "scale-info/std", + "serde/std", + "sp-std/std", +] diff --git a/pallets/rate-limiting-interface/README.md b/pallets/rate-limiting-interface/README.md new file mode 100644 index 0000000000..d8671fc0ba --- /dev/null +++ b/pallets/rate-limiting-interface/README.md @@ -0,0 +1,3 @@ +# `rate-limiting-interface` + +Small, `no_std`-friendly interface crate that defines [`RateLimitingInfo`](src/lib.rs). diff --git a/pallets/rate-limiting-interface/src/lib.rs b/pallets/rate-limiting-interface/src/lib.rs new file mode 100644 index 0000000000..4bf0dab22f --- /dev/null +++ b/pallets/rate-limiting-interface/src/lib.rs @@ -0,0 +1,267 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +//! Read-only interface for querying rate limits and last-seen usage. + +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::traits::GetCallMetadata; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_std::vec::Vec; + +/// Read-only queries for rate-limiting configuration and usage tracking. +pub trait RateLimitingInfo { + /// Group id type used by rate-limiting targets. + type GroupId; + /// Call type used for name/index resolution. + type CallMetadata: GetCallMetadata; + /// Numeric type used for returned values (commonly a block number / block span type). + type Limit; + /// Optional configuration scope (for example per-network `netuid`). + type Scope; + /// Optional usage key used to refine "last seen" tracking. + type UsageKey; + + /// Returns the configured limit for `target` and optional `scope`. + fn rate_limit(target: TargetArg, scope: Option) -> Option + where + TargetArg: TryIntoRateLimitTarget; + + /// Returns when `target` was last observed for the optional `usage_key`. + fn last_seen( + target: TargetArg, + usage_key: Option, + ) -> Option + where + TargetArg: TryIntoRateLimitTarget; +} + +/// Target identifier for rate limit and usage configuration. +#[derive( + Serialize, + Deserialize, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + Debug, +)] +pub enum RateLimitTarget { + /// Per-transaction configuration keyed by pallet/extrinsic indices. + Transaction(TransactionIdentifier), + /// Shared configuration for a named group. + Group(GroupId), +} + +impl RateLimitTarget { + /// Returns the transaction identifier when the target represents a single extrinsic. + pub fn as_transaction(&self) -> Option<&TransactionIdentifier> { + match self { + RateLimitTarget::Transaction(identifier) => Some(identifier), + RateLimitTarget::Group(_) => None, + } + } + + /// Returns the group identifier when the target represents a group configuration. + pub fn as_group(&self) -> Option<&GroupId> { + match self { + RateLimitTarget::Transaction(_) => None, + RateLimitTarget::Group(id) => Some(id), + } + } +} + +impl From for RateLimitTarget { + fn from(identifier: TransactionIdentifier) -> Self { + Self::Transaction(identifier) + } +} + +/// Identifies a runtime call by pallet and extrinsic indices. +#[derive( + Serialize, + Deserialize, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + Debug, +)] +pub struct TransactionIdentifier { + /// Pallet variant index. + pub pallet_index: u8, + /// Call variant index within the pallet. + pub extrinsic_index: u8, +} + +impl TransactionIdentifier { + /// Builds a new identifier from pallet/extrinsic indices. + pub const fn new(pallet_index: u8, extrinsic_index: u8) -> Self { + Self { + pallet_index, + extrinsic_index, + } + } + + /// Attempts to build an identifier from a SCALE-encoded call by reading the first two bytes. + pub fn from_call(call: &Call) -> Option { + call.using_encoded(|encoded| { + let pallet_index = *encoded.get(0)?; + let extrinsic_index = *encoded.get(1)?; + Some(Self::new(pallet_index, extrinsic_index)) + }) + } + + /// Resolves pallet/extrinsic names for this identifier using call metadata. + pub fn names(&self) -> Option<(&'static str, &'static str)> { + let modules = Call::get_module_names(); + let pallet_name = *modules.get(self.pallet_index as usize)?; + let call_names = Call::get_call_names(pallet_name); + let extrinsic_name = *call_names.get(self.extrinsic_index as usize)?; + Some((pallet_name, extrinsic_name)) + } + + /// Resolves a pallet/extrinsic name pair into a transaction identifier. + pub fn for_call_names( + pallet_name: &str, + extrinsic_name: &str, + ) -> Option { + let modules = Call::get_module_names(); + let pallet_pos = modules.iter().position(|name| *name == pallet_name)?; + let call_names = Call::get_call_names(pallet_name); + let extrinsic_pos = call_names.iter().position(|name| *name == extrinsic_name)?; + let pallet_index = u8::try_from(pallet_pos).ok()?; + let extrinsic_index = u8::try_from(extrinsic_pos).ok()?; + Some(Self::new(pallet_index, extrinsic_index)) + } +} + +/// Conversion into a concrete [`RateLimitTarget`]. +pub trait TryIntoRateLimitTarget { + type Error; + + fn try_into_rate_limit_target( + self, + ) -> Result, Self::Error>; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RateLimitTargetConversionError { + InvalidUtf8, + UnknownCall, +} + +impl TryIntoRateLimitTarget for RateLimitTarget { + type Error = core::convert::Infallible; + + fn try_into_rate_limit_target( + self, + ) -> Result, Self::Error> { + Ok(self) + } +} + +impl TryIntoRateLimitTarget for GroupId { + type Error = core::convert::Infallible; + + fn try_into_rate_limit_target( + self, + ) -> Result, Self::Error> { + Ok(RateLimitTarget::Group(self)) + } +} + +impl TryIntoRateLimitTarget for (Vec, Vec) { + type Error = RateLimitTargetConversionError; + + fn try_into_rate_limit_target( + self, + ) -> Result, Self::Error> { + let (pallet, extrinsic) = self; + let pallet_name = sp_std::str::from_utf8(&pallet) + .map_err(|_| RateLimitTargetConversionError::InvalidUtf8)?; + let extrinsic_name = sp_std::str::from_utf8(&extrinsic) + .map_err(|_| RateLimitTargetConversionError::InvalidUtf8)?; + + let identifier = TransactionIdentifier::for_call_names::(pallet_name, extrinsic_name) + .ok_or(RateLimitTargetConversionError::UnknownCall)?; + + Ok(RateLimitTarget::Transaction(identifier)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use codec::Encode; + use frame_support::traits::CallMetadata; + + #[derive(Clone, Copy, Debug, Encode)] + struct DummyCall(u8, u8); + + impl GetCallMetadata for DummyCall { + fn get_module_names() -> &'static [&'static str] { + &["P0", "P1"] + } + + fn get_call_names(module: &str) -> &'static [&'static str] { + match module { + "P0" => &["C0"], + "P1" => &["C0", "C1", "C2", "C3", "C4"], + _ => &[], + } + } + + fn get_call_metadata(&self) -> CallMetadata { + CallMetadata { + function_name: "unused", + pallet_name: "unused", + } + } + } + + #[test] + fn transaction_identifier_from_call_reads_first_two_bytes() { + let id = TransactionIdentifier::from_call(&DummyCall(1, 4)).expect("identifier"); + assert_eq!(id, TransactionIdentifier::new(1, 4)); + } + + #[test] + fn transaction_identifier_names_resolves_metadata() { + let id = TransactionIdentifier::new(1, 4); + assert_eq!(id.names::(), Some(("P1", "C4"))); + } + + #[test] + fn transaction_identifier_for_call_names_resolves_indices() { + let id = TransactionIdentifier::for_call_names::("P1", "C4").expect("id"); + assert_eq!(id, TransactionIdentifier::new(1, 4)); + } + + #[test] + fn rate_limit_target_accessors_work() { + let tx = RateLimitTarget::::Transaction(TransactionIdentifier::new(1, 4)); + assert!(tx.as_group().is_none()); + assert_eq!( + tx.as_transaction().copied(), + Some(TransactionIdentifier::new(1, 4)) + ); + + let group = RateLimitTarget::::Group(7); + assert!(group.as_transaction().is_none()); + assert_eq!(group.as_group().copied(), Some(7)); + } +} diff --git a/pallets/rate-limiting/Cargo.toml b/pallets/rate-limiting/Cargo.toml index 67e2710f4b..00bae918dd 100644 --- a/pallets/rate-limiting/Cargo.toml +++ b/pallets/rate-limiting/Cargo.toml @@ -18,6 +18,7 @@ scale-info = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] } sp-std.workspace = true sp-runtime.workspace = true +rate-limiting-interface.workspace = true subtensor-runtime-common.workspace = true [dev-dependencies] @@ -32,6 +33,7 @@ std = [ "frame-benchmarking?/std", "frame-support/std", "frame-system/std", + "rate-limiting-interface/std", "scale-info/std", "serde/std", "sp-std/std", diff --git a/pallets/rate-limiting/src/benchmarking.rs b/pallets/rate-limiting/src/benchmarking.rs index 38568dea28..265733e113 100644 --- a/pallets/rate-limiting/src/benchmarking.rs +++ b/pallets/rate-limiting/src/benchmarking.rs @@ -40,7 +40,7 @@ fn register_call_with_group( group: Option<::GroupId>, ) -> TransactionIdentifier { let call = sample_call::(); - let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("id"); + let identifier = TransactionIdentifier::from_call(call.as_ref()).expect("id"); Pallet::::register_call(RawOrigin::Root.into(), call, group).expect("registered"); identifier } @@ -53,7 +53,7 @@ mod benchmarks { #[benchmark] fn register_call() { let call = sample_call::(); - let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("id"); + let identifier = TransactionIdentifier::from_call(call.as_ref()).expect("id"); let target = RateLimitTarget::Transaction(identifier); #[extrinsic_call] @@ -65,7 +65,7 @@ mod benchmarks { #[benchmark] fn set_rate_limit() { let call = sample_call::(); - let identifier = TransactionIdentifier::from_call::(call.as_ref()).expect("id"); + let identifier = TransactionIdentifier::from_call(call.as_ref()).expect("id"); let target = RateLimitTarget::Transaction(identifier); Limits::::insert(target, RateLimit::global(RateLimitKind::Default)); diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index c93397c425..339ae3d359 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -6,8 +6,8 @@ //! //! `pallet-rate-limiting` lets a runtime restrict how frequently particular calls can execute. //! Limits are stored on-chain, keyed by explicit [`RateLimitTarget`] values. A target is either a -//! single [`TransactionIdentifier`] (the pallet/extrinsic indices) or a named *group* managed by the -//! admin APIs. Groups provide a way to give multiple calls the same configuration and/or usage +//! single [`TransactionIdentifier`] (the pallet/extrinsic indices) or a named *group* managed by +//! the admin APIs. Groups provide a way to give multiple calls the same configuration and/or usage //! tracking without duplicating storage. Each target entry stores either a global span or a set of //! scoped spans resolved at runtime. The pallet exposes a handful of extrinsics, restricted by //! [`Config::AdminOrigin`], to manage this data: @@ -142,12 +142,57 @@ #[cfg(feature = "runtime-benchmarks")] pub use benchmarking::BenchmarkHelper; pub use pallet::*; +pub use rate_limiting_interface::{RateLimitTarget, TransactionIdentifier}; +pub use rate_limiting_interface::{RateLimitingInfo, TryIntoRateLimitTarget}; pub use tx_extension::RateLimitTransactionExtension; pub use types::{ BypassDecision, GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitScopeResolver, - RateLimitTarget, RateLimitUsageResolver, TransactionIdentifier, + RateLimitUsageResolver, }; +impl, I: 'static> RateLimitingInfo for pallet::Pallet { + type GroupId = >::GroupId; + type CallMetadata = >::RuntimeCall; + type Limit = frame_system::pallet_prelude::BlockNumberFor; + type Scope = >::LimitScope; + type UsageKey = >::UsageKey; + + fn rate_limit(target: TargetArg, scope: Option) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + let raw_target = target + .try_into_rate_limit_target::() + .ok()?; + let config_target = match raw_target { + // A transaction identifier may be assigned to a group; resolve the effective storage + // target. + RateLimitTarget::Transaction(identifier) => Self::config_target(&identifier).ok()?, + _ => raw_target, + }; + Self::resolved_limit(&config_target, &scope) + } + + fn last_seen( + target: TargetArg, + usage_key: Option, + ) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + let raw_target = target + .try_into_rate_limit_target::() + .ok()?; + let usage_target = match raw_target { + // A transaction identifier may be assigned to a group; resolve the effective storage + // target. + RateLimitTarget::Transaction(identifier) => Self::usage_target(&identifier).ok()?, + _ => raw_target, + }; + pallet::LastSeen::::get(usage_target, usage_key) + } +} + #[cfg(feature = "runtime-benchmarks")] mod benchmarking; mod tx_extension; @@ -768,7 +813,9 @@ pub mod pallet { fn call_metadata( identifier: &TransactionIdentifier, ) -> Result<(Vec, Vec), DispatchError> { - let (pallet_name, extrinsic_name) = identifier.names::()?; + let (pallet_name, extrinsic_name) = identifier + .names::<>::RuntimeCall>() + .ok_or(Error::::InvalidRuntimeCall)?; Ok(( Vec::from(pallet_name.as_bytes()), Vec::from(extrinsic_name.as_bytes()), @@ -917,7 +964,8 @@ pub mod pallet { T::AdminOrigin::ensure_origin(origin)?; - let identifier = TransactionIdentifier::from_call::(call.as_ref())?; + let identifier = TransactionIdentifier::from_call(call.as_ref()) + .ok_or(Error::::InvalidRuntimeCall)?; Self::ensure_call_unregistered(&identifier)?; let target = RateLimitTarget::Transaction(identifier); diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index 98769bf6a6..d3d486f6a1 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -153,7 +153,7 @@ pub fn new_test_ext() -> TestExternalities { } pub(crate) fn identifier_for(call: &RuntimeCall) -> TransactionIdentifier { - TransactionIdentifier::from_call::(call).expect("identifier for call") + TransactionIdentifier::from_call(call).expect("identifier for call") } pub(crate) fn pop_last_event() -> RuntimeEvent { diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index 5e5cc45f0c..303649c9c9 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -117,9 +117,8 @@ where _inherited_implication: &impl Implication, _source: TransactionSource, ) -> ValidateResult>::RuntimeCall> { - let identifier = match TransactionIdentifier::from_call::(call) { - Ok(identifier) => identifier, - Err(_) => return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + let Some(identifier) = TransactionIdentifier::from_call(call) else { + return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)); }; if !Pallet::::is_registered(&identifier) { diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index c7bc2f91f7..166c471d7e 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -1,5 +1,5 @@ use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata}; +pub use rate_limiting_interface::{RateLimitTarget, TransactionIdentifier}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; @@ -60,72 +60,6 @@ pub trait RateLimitUsageResolver { fn context(origin: &Origin, call: &Call) -> Option>; } -/// Identifies a runtime call by pallet and extrinsic indices. -#[derive( - Serialize, - Deserialize, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Encode, - Decode, - DecodeWithMemTracking, - TypeInfo, - MaxEncodedLen, - Debug, -)] -pub struct TransactionIdentifier { - /// Pallet variant index. - pub pallet_index: u8, - /// Call variant index within the pallet. - pub extrinsic_index: u8, -} - -/// Target identifier for rate limit and usage configuration. -#[derive( - Serialize, - Deserialize, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Encode, - Decode, - DecodeWithMemTracking, - TypeInfo, - MaxEncodedLen, - Debug, -)] -pub enum RateLimitTarget { - /// Per-transaction configuration keyed by pallet/extrinsic indices. - Transaction(TransactionIdentifier), - /// Shared configuration for a named group. - Group(GroupId), -} - -impl RateLimitTarget { - /// Returns the transaction identifier when the target represents a single extrinsic. - pub fn as_transaction(&self) -> Option<&TransactionIdentifier> { - match self { - RateLimitTarget::Transaction(identifier) => Some(identifier), - RateLimitTarget::Group(_) => None, - } - } - - /// Returns the group identifier when the target represents a group configuration. - pub fn as_group(&self) -> Option<&GroupId> { - match self { - RateLimitTarget::Transaction(_) => None, - RateLimitTarget::Group(id) => Some(id), - } - } -} - /// Sharing mode configured for a group. #[derive( Serialize, @@ -188,55 +122,6 @@ pub struct RateLimitGroup { pub sharing: GroupSharing, } -impl TransactionIdentifier { - /// Builds a new identifier from pallet/extrinsic indices. - pub const fn new(pallet_index: u8, extrinsic_index: u8) -> Self { - Self { - pallet_index, - extrinsic_index, - } - } - - /// Returns the pallet and extrinsic names associated with this identifier. - pub fn names(&self) -> Result<(&'static str, &'static str), DispatchError> - where - T: crate::pallet::Config, - I: 'static, - >::RuntimeCall: GetCallMetadata, - { - let modules = >::RuntimeCall::get_module_names(); - let pallet_name = modules - .get(self.pallet_index as usize) - .copied() - .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; - let call_names = >::RuntimeCall::get_call_names(pallet_name); - let extrinsic_name = call_names - .get(self.extrinsic_index as usize) - .copied() - .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; - Ok((pallet_name, extrinsic_name)) - } - - /// Builds an identifier from a runtime call by extracting pallet/extrinsic indices. - pub fn from_call( - call: &>::RuntimeCall, - ) -> Result - where - T: crate::pallet::Config, - I: 'static, - { - call.using_encoded(|encoded| { - let pallet_index = *encoded - .get(0) - .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; - let extrinsic_index = *encoded - .get(1) - .ok_or(crate::pallet::Error::::InvalidRuntimeCall)?; - Ok(TransactionIdentifier::new(pallet_index, extrinsic_index)) - }) - } -} - /// Policy describing the block span enforced by a rate limit. #[derive( Serialize, @@ -336,44 +221,3 @@ where matches!(self, RateLimit::Scoped(map) if map.is_empty()) } } - -#[cfg(test)] -mod tests { - use sp_runtime::DispatchError; - - use super::*; - use crate::{mock::*, pallet::Error}; - - #[test] - fn transaction_identifier_from_call_matches_expected_indices() { - let call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - - let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); - - // System is the first pallet in the mock runtime, RateLimiting is second. - assert_eq!(identifier.pallet_index, 1); - // set_default_rate_limit has call_index 4. - assert_eq!(identifier.extrinsic_index, 4); - } - - #[test] - fn transaction_identifier_names_matches_call_metadata() { - let call = - RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 }); - let identifier = TransactionIdentifier::from_call::(&call).expect("identifier"); - - let (pallet, extrinsic) = identifier.names::().expect("call metadata"); - assert_eq!(pallet, "RateLimiting"); - assert_eq!(extrinsic, "set_default_rate_limit"); - } - - #[test] - fn transaction_identifier_names_error_for_unknown_indices() { - let identifier = TransactionIdentifier::new(99, 0); - - let err = identifier.names::().expect_err("should fail"); - let expected: DispatchError = Error::::InvalidRuntimeCall.into(); - assert_eq!(err, expected); - } -} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 25943cf4e9..be6617f671 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1150,7 +1150,7 @@ impl pallet_rate_limiting::Config for Runtime { type LimitScopeResolver = RuntimeScopeResolver; type UsageKey = RateLimitUsageKey; type UsageResolver = RuntimeUsageResolver; - type GroupId = u32; + type GroupId = subtensor_runtime_common::rate_limiting::GroupId; type MaxGroupMembers = RateLimitingMaxGroupMembers; type MaxGroupNameLength = RateLimitingMaxGroupNameLength; #[cfg(feature = "runtime-benchmarks")] diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index ccd3e6402f..f77f6ec2dc 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -16,7 +16,13 @@ use sp_std::{ vec, vec::Vec, }; -use subtensor_runtime_common::NetUid; +use subtensor_runtime_common::{ + NetUid, + rate_limiting::{ + GROUP_DELEGATE_TAKE, GROUP_OWNER_HPARAMS, GROUP_REGISTER_NETWORK, GROUP_SERVE, + GROUP_STAKING_OPS, GROUP_SWAP_KEYS, GROUP_WEIGHTS_SUBNET, GroupId, + }, +}; use super::{ AccountId, RateLimitUsageKey, Runtime, @@ -26,7 +32,6 @@ use super::{ }, }; -type GroupId = ::GroupId; type GroupNameOf = BoundedVec::MaxGroupNameLength>; type GroupMembersOf = BoundedBTreeSet::MaxGroupMembers>; @@ -39,14 +44,6 @@ const ADMIN_UTILS_PALLET_INDEX: u8 = 19; /// Marker stored in `pallet_subtensor::HasMigrationRun` once the migration finishes. pub const MIGRATION_NAME: &[u8] = b"migrate_rate_limiting"; -const GROUP_SERVE: GroupId = 0; -const GROUP_DELEGATE_TAKE: GroupId = 1; -const GROUP_WEIGHTS_SUBNET: GroupId = 2; -pub const GROUP_REGISTER_NETWORK: GroupId = 3; -const GROUP_OWNER_HPARAMS: GroupId = 4; -const GROUP_STAKING_OPS: GroupId = 5; -const GROUP_SWAP_KEYS: GroupId = 6; - // `set_children` is rate-limited to once every 150 blocks, it's hard-coded in the legacy code. const SET_CHILDREN_RATE_LIMIT: u64 = 150; diff --git a/runtime/tests/rate_limiting_behavior.rs b/runtime/tests/rate_limiting_behavior.rs index e9a76acf1c..38208a3e3c 100644 --- a/runtime/tests/rate_limiting_behavior.rs +++ b/runtime/tests/rate_limiting_behavior.rs @@ -17,10 +17,9 @@ use pallet_subtensor::{ }; use sp_core::{H160, ecdsa}; use sp_runtime::traits::SaturatedConversion; -use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; +use subtensor_runtime_common::{NetUid, NetUidStorageIndex, rate_limiting::GroupId}; type AccountId = ::AccountId; -type GroupId = ::GroupId; type UsageKey = RateLimitUsageKey; const MIGRATION_NAME: &[u8] = b"migrate_rate_limiting"; @@ -79,8 +78,7 @@ fn parity_check( // Run migration to hydrate pallet-rate-limiting state. Migration::::on_runtime_upgrade(); - let identifier = - TransactionIdentifier::from_call::(&call).expect("identifier for call"); + let identifier = TransactionIdentifier::from_call(&call).expect("identifier for call"); let scope = scope_override.or_else(|| RuntimeScopeResolver::context(&origin, &call)); let usage: Option::UsageKey>> = usage_override.or_else(|| RuntimeUsageResolver::context(&origin, &call)); diff --git a/runtime/tests/rate_limiting_migration.rs b/runtime/tests/rate_limiting_migration.rs index e97beb8fc1..378f4c7a73 100644 --- a/runtime/tests/rate_limiting_migration.rs +++ b/runtime/tests/rate_limiting_migration.rs @@ -5,11 +5,11 @@ use frame_system::pallet_prelude::BlockNumberFor; use pallet_rate_limiting::{RateLimit, RateLimitKind, RateLimitTarget, TransactionIdentifier}; use pallet_subtensor::{HasMigrationRun, LastRateLimitedBlock, RateLimitKey}; use sp_runtime::traits::SaturatedConversion; -use subtensor_runtime_common::NetUid; +use subtensor_runtime_common::{NetUid, rate_limiting::GROUP_REGISTER_NETWORK}; use node_subtensor_runtime::{ BuildStorage, Runtime, RuntimeGenesisConfig, SubtensorModule, System, rate_limiting, - rate_limiting::migration::{GROUP_REGISTER_NETWORK, MIGRATION_NAME, Migration}, + rate_limiting::migration::{MIGRATION_NAME, Migration}, }; type AccountId = ::AccountId; From 1228b8a3ed573c13f225c149a018446a4a797239 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 15 Dec 2025 15:05:25 +0300 Subject: [PATCH 41/95] Add rate limiting interface for pallet-subtensor::Config --- Cargo.lock | 4 + chain-extensions/Cargo.toml | 3 + chain-extensions/src/mock.rs | 33 +++++++- common/src/rate_limiting.rs | 91 +++++++++++++++++++++++ pallets/admin-utils/Cargo.toml | 2 + pallets/admin-utils/src/tests/mock.rs | 31 +++++++- pallets/rate-limiting/src/lib.rs | 86 ++++++++++----------- pallets/subtensor/Cargo.toml | 2 + pallets/subtensor/src/macros/config.rs | 12 +++ pallets/subtensor/src/tests/mock.rs | 31 +++++++- pallets/transaction-fee/Cargo.toml | 2 + pallets/transaction-fee/src/tests/mock.rs | 33 +++++++- runtime/src/lib.rs | 4 +- runtime/src/rate_limiting/mod.rs | 90 +++++----------------- runtime/tests/rate_limiting_behavior.rs | 8 +- runtime/tests/rate_limiting_migration.rs | 9 ++- 16 files changed, 317 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0cefd7d14..351e156aad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8774,6 +8774,7 @@ dependencies = [ "pallet-subtensor", "pallet-subtensor-swap", "parity-scale-codec", + "rate-limiting-interface", "scale-info", "sp-consensus-aura", "sp-consensus-grandpa", @@ -10836,6 +10837,7 @@ dependencies = [ "polkadot-runtime-common", "rand 0.8.5", "rand_chacha 0.3.1", + "rate-limiting-interface", "safe-math", "scale-info", "serde", @@ -18091,6 +18093,7 @@ dependencies = [ "pallet-subtensor-utility", "pallet-timestamp", "parity-scale-codec", + "rate-limiting-interface", "scale-info", "sp-core", "sp-io", @@ -18234,6 +18237,7 @@ dependencies = [ "pallet-subtensor-swap", "pallet-transaction-payment", "parity-scale-codec", + "rate-limiting-interface", "scale-info", "smallvec", "sp-consensus-aura", diff --git a/chain-extensions/Cargo.toml b/chain-extensions/Cargo.toml index 9727439e7a..61ec5b12e6 100644 --- a/chain-extensions/Cargo.toml +++ b/chain-extensions/Cargo.toml @@ -36,6 +36,9 @@ subtensor-swap-interface.workspace = true num_enum.workspace = true substrate-fixed.workspace = true +[dev-dependencies] +rate-limiting-interface.workspace = true + [lints] workspace = true diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 98ea096199..53195c6cd9 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -18,6 +18,7 @@ use pallet_contracts::HoldReason as ContractsHoldReason; use pallet_subtensor::*; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_utility as pallet_utility; +use rate_limiting_interface::{RateLimitingInfo, TryIntoRateLimitTarget}; use sp_core::{ConstU64, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; use sp_runtime::{ @@ -25,7 +26,9 @@ use sp_runtime::{ traits::{BlakeTwo256, Convert, IdentityLookup}, }; use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; -use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; +use subtensor_runtime_common::{ + AlphaCurrency, NetUid, TaoCurrency, rate_limiting::RateLimitUsageKey, +}; type Block = frame_system::mocking::MockBlock; @@ -411,6 +414,7 @@ impl pallet_subtensor::Config for Test { type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; + type RateLimiting = NoRateLimiting; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; } @@ -449,6 +453,33 @@ impl CommitmentsInterface for CommitmentsI { fn purge_netuid(_netuid: NetUid) {} } +pub struct NoRateLimiting; + +impl RateLimitingInfo for NoRateLimiting { + type GroupId = subtensor_runtime_common::rate_limiting::GroupId; + type CallMetadata = RuntimeCall; + type Limit = BlockNumber; + type Scope = subtensor_runtime_common::NetUid; + type UsageKey = RateLimitUsageKey; + + fn rate_limit(_target: TargetArg, _scope: Option) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + None + } + + fn last_seen( + _target: TargetArg, + _usage_key: Option, + ) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + None + } +} + parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; diff --git a/common/src/rate_limiting.rs b/common/src/rate_limiting.rs index 7051e5aeaa..20c9e2d629 100644 --- a/common/src/rate_limiting.rs +++ b/common/src/rate_limiting.rs @@ -1,4 +1,26 @@ //! Shared rate-limiting types. +//! +//! Note: `pallet-rate-limiting` supports multiple independent instances, and is intended to be used +//! as “one instance per pallet” with pallet-specific scope/usage-key types and resolvers. +//! +//! The scope/usage-key types in this module are centralized today due to the current state of +//! `pallet-subtensor` (a large, centralized pallet) and its coupling with `pallet-admin-utils`, +//! which share a single `pallet-rate-limiting` instance and resolver implementation in the runtime. +//! +//! For new pallets, it is strongly recommended to: +//! - define their own `LimitScope` and `UsageKey` types (do not extend `RateLimitUsageKey` here), +//! - provide pallet-local scope/usage resolvers, +//! - and use a dedicated `pallet-rate-limiting` instance. +//! +//! Long-term, we should move away from these shared types by refactoring `pallet-subtensor` into +//! smaller pallets with dedicated `pallet-rate-limiting` instances. + +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::pallet_prelude::Parameter; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; + +use crate::{MechId, NetUid}; /// Identifier type for rate-limiting groups. pub type GroupId = u32; @@ -17,3 +39,72 @@ pub const GROUP_OWNER_HPARAMS: GroupId = 4; pub const GROUP_STAKING_OPS: GroupId = 5; /// Group id for key swap calls. pub const GROUP_SWAP_KEYS: GroupId = 6; + +/// Usage-key type currently shared by the centralized `pallet-subtensor` rate-limiting instance. +/// +/// Do not add new variants for new pallets. Prefer defining pallet-specific types and using a +/// dedicated `pallet-rate-limiting` instance per pallet. +#[derive( + Serialize, + Deserialize, + Encode, + Decode, + DecodeWithMemTracking, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + TypeInfo, + MaxEncodedLen, +)] +#[scale_info(skip_type_params(AccountId))] +pub enum RateLimitUsageKey { + Account(AccountId), + Subnet(NetUid), + AccountSubnet { + account: AccountId, + netuid: NetUid, + }, + ColdkeyHotkeySubnet { + coldkey: AccountId, + hotkey: AccountId, + netuid: NetUid, + }, + SubnetNeuron { + netuid: NetUid, + uid: u16, + }, + SubnetMechanismNeuron { + netuid: NetUid, + mecid: MechId, + uid: u16, + }, + AccountSubnetServing { + account: AccountId, + netuid: NetUid, + endpoint: ServingEndpoint, + }, +} + +#[derive( + Serialize, + Deserialize, + Encode, + Decode, + DecodeWithMemTracking, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + TypeInfo, + MaxEncodedLen, +)] +pub enum ServingEndpoint { + Axon, + Prometheus, +} diff --git a/pallets/admin-utils/Cargo.toml b/pallets/admin-utils/Cargo.toml index 61cdba4cbf..aa6d7e593c 100644 --- a/pallets/admin-utils/Cargo.toml +++ b/pallets/admin-utils/Cargo.toml @@ -38,6 +38,7 @@ sp-core.workspace = true sp-io.workspace = true sp-tracing.workspace = true sp-consensus-aura.workspace = true +rate-limiting-interface.workspace = true pallet-balances = { workspace = true, features = ["std"] } pallet-scheduler.workspace = true pallet-grandpa.workspace = true @@ -75,6 +76,7 @@ std = [ "substrate-fixed/std", "subtensor-swap-interface/std", "subtensor-runtime-common/std", + "rate-limiting-interface/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 0140808baa..0633109caa 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -8,6 +8,7 @@ use frame_support::{ }; use frame_system::{self as system, offchain::CreateTransactionBase}; use frame_system::{EnsureRoot, limits}; +use rate_limiting_interface::{RateLimitingInfo, TryIntoRateLimitTarget}; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_grandpa::AuthorityList as GrandpaAuthorityList; use sp_core::U256; @@ -19,7 +20,7 @@ use sp_runtime::{ }; use sp_std::cmp::Ordering; use sp_weights::Weight; -use subtensor_runtime_common::{NetUid, TaoCurrency}; +use subtensor_runtime_common::{NetUid, TaoCurrency, rate_limiting::RateLimitUsageKey}; type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. @@ -224,6 +225,7 @@ impl pallet_subtensor::Config for Test { type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; + type RateLimiting = NoRateLimiting; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; } @@ -356,6 +358,33 @@ impl pallet_subtensor::CommitmentsInterface for CommitmentsI { fn purge_netuid(_netuid: NetUid) {} } +pub struct NoRateLimiting; + +impl RateLimitingInfo for NoRateLimiting { + type GroupId = subtensor_runtime_common::rate_limiting::GroupId; + type CallMetadata = RuntimeCall; + type Limit = u64; + type Scope = subtensor_runtime_common::NetUid; + type UsageKey = RateLimitUsageKey; + + fn rate_limit(_target: TargetArg, _scope: Option) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + None + } + + fn last_seen( + _target: TargetArg, + _usage_key: Option, + ) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + None + } +} + pub struct GrandpaInterfaceImpl; impl crate::GrandpaInterface for GrandpaInterfaceImpl { fn schedule_change( diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 339ae3d359..a4af7fc3fd 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -150,49 +150,6 @@ pub use types::{ RateLimitUsageResolver, }; -impl, I: 'static> RateLimitingInfo for pallet::Pallet { - type GroupId = >::GroupId; - type CallMetadata = >::RuntimeCall; - type Limit = frame_system::pallet_prelude::BlockNumberFor; - type Scope = >::LimitScope; - type UsageKey = >::UsageKey; - - fn rate_limit(target: TargetArg, scope: Option) -> Option - where - TargetArg: TryIntoRateLimitTarget, - { - let raw_target = target - .try_into_rate_limit_target::() - .ok()?; - let config_target = match raw_target { - // A transaction identifier may be assigned to a group; resolve the effective storage - // target. - RateLimitTarget::Transaction(identifier) => Self::config_target(&identifier).ok()?, - _ => raw_target, - }; - Self::resolved_limit(&config_target, &scope) - } - - fn last_seen( - target: TargetArg, - usage_key: Option, - ) -> Option - where - TargetArg: TryIntoRateLimitTarget, - { - let raw_target = target - .try_into_rate_limit_target::() - .ok()?; - let usage_target = match raw_target { - // A transaction identifier may be assigned to a group; resolve the effective storage - // target. - RateLimitTarget::Transaction(identifier) => Self::usage_target(&identifier).ok()?, - _ => raw_target, - }; - pallet::LastSeen::::get(usage_target, usage_key) - } -} - #[cfg(feature = "runtime-benchmarks")] mod benchmarking; mod tx_extension; @@ -1332,3 +1289,46 @@ pub mod pallet { } } } + +impl, I: 'static> RateLimitingInfo for pallet::Pallet { + type GroupId = >::GroupId; + type CallMetadata = >::RuntimeCall; + type Limit = frame_system::pallet_prelude::BlockNumberFor; + type Scope = >::LimitScope; + type UsageKey = >::UsageKey; + + fn rate_limit(target: TargetArg, scope: Option) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + let raw_target = target + .try_into_rate_limit_target::() + .ok()?; + let config_target = match raw_target { + // A transaction identifier may be assigned to a group; resolve the effective storage + // target. + RateLimitTarget::Transaction(identifier) => Self::config_target(&identifier).ok()?, + _ => raw_target, + }; + Self::resolved_limit(&config_target, &scope) + } + + fn last_seen( + target: TargetArg, + usage_key: Option, + ) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + let raw_target = target + .try_into_rate_limit_target::() + .ok()?; + let usage_target = match raw_target { + // A transaction identifier may be assigned to a group; resolve the effective storage + // target. + RateLimitTarget::Transaction(identifier) => Self::usage_target(&identifier).ok()?, + _ => raw_target, + }; + pallet::LastSeen::::get(usage_target, usage_key) + } +} diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index a7885e32a8..cd9410de8b 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -56,6 +56,7 @@ rand_chacha.workspace = true pallet-crowdloan.workspace = true pallet-subtensor-proxy.workspace = true pallet-rate-limiting.workspace = true +rate-limiting-interface.workspace = true [dev-dependencies] pallet-balances = { workspace = true, features = ["std"] } @@ -116,6 +117,7 @@ std = [ "pallet-drand/std", "pallet-subtensor-proxy/std", "pallet-rate-limiting/std", + "rate-limiting-interface/std", "pallet-subtensor-swap/std", "subtensor-swap-interface/std", "pallet-subtensor-utility/std", diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index a735bde1e1..f8742087ff 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -8,6 +8,7 @@ mod config { use crate::{CommitmentsInterface, GetAlphaForTao, GetTaoForAlpha}; use pallet_commitments::GetCommitments; + use rate_limiting_interface::RateLimitingInfo; use subtensor_swap_interface::{SwapEngine, SwapHandler}; /// Configure the pallet by specifying the parameters and types on which it depends. @@ -56,6 +57,17 @@ mod config { /// Interface to clean commitments on network dissolution. type CommitmentsInterface: CommitmentsInterface; + /// Read-only interface for querying rate limiting configuration and usage. + type RateLimiting: RateLimitingInfo< + GroupId = subtensor_runtime_common::rate_limiting::GroupId, + CallMetadata = ::RuntimeCall, + Limit = BlockNumberFor, + Scope = subtensor_runtime_common::NetUid, + UsageKey = subtensor_runtime_common::rate_limiting::RateLimitUsageKey< + Self::AccountId, + >, + >; + /// Rate limit for associating an EVM key. type EvmKeyAssociateRateLimit: Get; diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 090fcf8f75..161e0e372b 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -20,6 +20,7 @@ use frame_system as system; use frame_system::{EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_utility as pallet_utility; +use rate_limiting_interface::{RateLimitingInfo, TryIntoRateLimitTarget}; use sp_core::{ConstU64, Get, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; use sp_runtime::{ @@ -28,7 +29,7 @@ use sp_runtime::{ }; use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; use sp_tracing::tracing_subscriber; -use subtensor_runtime_common::{NetUid, TaoCurrency}; +use subtensor_runtime_common::{NetUid, TaoCurrency, rate_limiting::RateLimitUsageKey}; use subtensor_swap_interface::{Order, SwapHandler}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; type Block = frame_system::mocking::MockBlock; @@ -298,6 +299,7 @@ impl crate::Config for Test { type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; + type RateLimiting = NoRateLimiting; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; } @@ -336,6 +338,33 @@ impl CommitmentsInterface for CommitmentsI { fn purge_netuid(_netuid: NetUid) {} } +pub struct NoRateLimiting; + +impl RateLimitingInfo for NoRateLimiting { + type GroupId = subtensor_runtime_common::rate_limiting::GroupId; + type CallMetadata = RuntimeCall; + type Limit = BlockNumber; + type Scope = subtensor_runtime_common::NetUid; + type UsageKey = RateLimitUsageKey; + + fn rate_limit(_target: TargetArg, _scope: Option) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + None + } + + fn last_seen( + _target: TargetArg, + _usage_key: Option, + ) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + None + } +} + parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; diff --git a/pallets/transaction-fee/Cargo.toml b/pallets/transaction-fee/Cargo.toml index b5f08ac7c3..86bea7c843 100644 --- a/pallets/transaction-fee/Cargo.toml +++ b/pallets/transaction-fee/Cargo.toml @@ -27,6 +27,7 @@ pallet-scheduler = { workspace = true, default-features = false, optional = true [dev-dependencies] frame-executive.workspace = true pallet-evm-chain-id.workspace = true +rate-limiting-interface.workspace = true scale-info.workspace = true sp-consensus-aura.workspace = true sp-consensus-grandpa.workspace = true @@ -56,6 +57,7 @@ std = [ "pallet-subtensor/std", "pallet-subtensor-swap/std", "pallet-transaction-payment/std", + "rate-limiting-interface/std", "scale-info/std", "sp-runtime/std", "sp-std/std", diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index ee5b1693ba..2b25273104 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -12,6 +12,7 @@ use frame_system::{ self as system, EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase, }; pub use pallet_subtensor::*; +use rate_limiting_interface::{RateLimitingInfo, TryIntoRateLimitTarget}; pub use sp_core::U256; use sp_core::{ConstU64, H256}; use sp_runtime::{ @@ -21,7 +22,9 @@ use sp_runtime::{ }; use sp_std::cmp::Ordering; use sp_weights::Weight; -pub use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; +pub use subtensor_runtime_common::{ + AlphaCurrency, Currency, NetUid, TaoCurrency, rate_limiting::RateLimitUsageKey, +}; use subtensor_swap_interface::{Order, SwapHandler}; use crate::SubtensorTxFeeHandler; @@ -289,6 +292,7 @@ impl pallet_subtensor::Config for Test { type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; + type RateLimiting = NoRateLimiting; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; } @@ -421,6 +425,33 @@ impl pallet_subtensor::CommitmentsInterface for CommitmentsI { fn purge_netuid(_netuid: NetUid) {} } +pub struct NoRateLimiting; + +impl RateLimitingInfo for NoRateLimiting { + type GroupId = subtensor_runtime_common::rate_limiting::GroupId; + type CallMetadata = RuntimeCall; + type Limit = u64; + type Scope = subtensor_runtime_common::NetUid; + type UsageKey = RateLimitUsageKey; + + fn rate_limit(_target: TargetArg, _scope: Option) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + None + } + + fn last_seen( + _target: TargetArg, + _usage_key: Option, + ) -> Option + where + TargetArg: TryIntoRateLimitTarget, + { + None + } +} + parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 4a122ef951..8cde7d8999 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -102,11 +102,12 @@ use pallet_commitments::GetCommitments; pub use pallet_timestamp::Call as TimestampCall; use pallet_transaction_payment::{ConstFeeMultiplier, Multiplier}; pub use rate_limiting::{ - RateLimitUsageKey, ScopeResolver as RuntimeScopeResolver, UsageResolver as RuntimeUsageResolver, + ScopeResolver as RuntimeScopeResolver, UsageResolver as RuntimeUsageResolver, }; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; pub use sp_runtime::{Perbill, Permill}; +pub use subtensor_runtime_common::rate_limiting::RateLimitUsageKey; // Drand impl pallet_drand::Config for Runtime { @@ -1135,6 +1136,7 @@ impl pallet_subtensor::Config for Runtime { type GetCommitments = GetCommitmentsStruct; type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; + type RateLimiting = RateLimiting; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; } diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 5c0b7a8ec1..d0b91c34a7 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -1,85 +1,35 @@ -use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use frame_support::pallet_prelude::Parameter; +//! Runtime-level rate limiting wiring and resolvers. +//! +//! `pallet-rate-limiting` supports multiple independent instances, and is intended to be deployed +//! as “one instance per pallet” with pallet-specific scope/usage-key types and resolvers. +//! +//! This runtime module is centralized today because `pallet-subtensor` is currently centralized and +//! coupled with `pallet-admin-utils`; both share a single `pallet-rate-limiting` instance and a +//! single resolver implementation. +//! +//! For new pallets, do not reuse or extend the centralized scope/usage-key types or resolvers. +//! Prefer defining pallet-local types/resolvers and using a dedicated `pallet-rate-limiting` +//! instance. +//! +//! Long-term, we should refactor `pallet-subtensor` into smaller pallets and move to dedicated +//! `pallet-rate-limiting` instances per pallet. + use frame_system::RawOrigin; use pallet_admin_utils::Call as AdminUtilsCall; use pallet_rate_limiting::BypassDecision; use pallet_rate_limiting::{RateLimitScopeResolver, RateLimitUsageResolver}; use pallet_subtensor::{Call as SubtensorCall, Tempo}; -use scale_info::TypeInfo; -use serde::{Deserialize, Serialize}; use sp_std::{vec, vec::Vec}; -use subtensor_runtime_common::{BlockNumber, MechId, NetUid}; +use subtensor_runtime_common::{ + BlockNumber, NetUid, + rate_limiting::{RateLimitUsageKey, ServingEndpoint}, +}; use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; mod legacy; pub mod migration; -#[derive( - Serialize, - Deserialize, - Encode, - Decode, - DecodeWithMemTracking, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Debug, - TypeInfo, - MaxEncodedLen, -)] -#[scale_info(skip_type_params(AccountId))] -pub enum RateLimitUsageKey { - Account(AccountId), - Subnet(NetUid), - AccountSubnet { - account: AccountId, - netuid: NetUid, - }, - ColdkeyHotkeySubnet { - coldkey: AccountId, - hotkey: AccountId, - netuid: NetUid, - }, - SubnetNeuron { - netuid: NetUid, - uid: u16, - }, - SubnetMechanismNeuron { - netuid: NetUid, - mecid: MechId, - uid: u16, - }, - AccountSubnetServing { - account: AccountId, - netuid: NetUid, - endpoint: ServingEndpoint, - }, -} - -#[derive( - Serialize, - Deserialize, - Encode, - Decode, - DecodeWithMemTracking, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Debug, - TypeInfo, - MaxEncodedLen, -)] -pub enum ServingEndpoint { - Axon, - Prometheus, -} - #[derive(Default)] pub struct ScopeResolver; diff --git a/runtime/tests/rate_limiting_behavior.rs b/runtime/tests/rate_limiting_behavior.rs index 38208a3e3c..3066125ef6 100644 --- a/runtime/tests/rate_limiting_behavior.rs +++ b/runtime/tests/rate_limiting_behavior.rs @@ -4,8 +4,7 @@ use frame_support::traits::OnRuntimeUpgrade; use frame_system::pallet_prelude::BlockNumberFor; use node_subtensor_runtime::{ BuildStorage, Runtime, RuntimeCall, RuntimeGenesisConfig, RuntimeOrigin, RuntimeScopeResolver, - RuntimeUsageResolver, SubtensorModule, System, rate_limiting::RateLimitUsageKey, - rate_limiting::migration::Migration, + RuntimeUsageResolver, SubtensorModule, System, rate_limiting::migration::Migration, }; use pallet_rate_limiting::{RateLimitScopeResolver, RateLimitUsageResolver}; use pallet_rate_limiting::{RateLimitTarget, TransactionIdentifier}; @@ -17,7 +16,10 @@ use pallet_subtensor::{ }; use sp_core::{H160, ecdsa}; use sp_runtime::traits::SaturatedConversion; -use subtensor_runtime_common::{NetUid, NetUidStorageIndex, rate_limiting::GroupId}; +use subtensor_runtime_common::{ + NetUid, NetUidStorageIndex, + rate_limiting::{GroupId, RateLimitUsageKey}, +}; type AccountId = ::AccountId; type UsageKey = RateLimitUsageKey; diff --git a/runtime/tests/rate_limiting_migration.rs b/runtime/tests/rate_limiting_migration.rs index 378f4c7a73..9e08f489b9 100644 --- a/runtime/tests/rate_limiting_migration.rs +++ b/runtime/tests/rate_limiting_migration.rs @@ -5,15 +5,18 @@ use frame_system::pallet_prelude::BlockNumberFor; use pallet_rate_limiting::{RateLimit, RateLimitKind, RateLimitTarget, TransactionIdentifier}; use pallet_subtensor::{HasMigrationRun, LastRateLimitedBlock, RateLimitKey}; use sp_runtime::traits::SaturatedConversion; -use subtensor_runtime_common::{NetUid, rate_limiting::GROUP_REGISTER_NETWORK}; +use subtensor_runtime_common::{ + NetUid, + rate_limiting::{GROUP_REGISTER_NETWORK, RateLimitUsageKey}, +}; use node_subtensor_runtime::{ - BuildStorage, Runtime, RuntimeGenesisConfig, SubtensorModule, System, rate_limiting, + BuildStorage, Runtime, RuntimeGenesisConfig, SubtensorModule, System, rate_limiting::migration::{MIGRATION_NAME, Migration}, }; type AccountId = ::AccountId; -type UsageKey = rate_limiting::RateLimitUsageKey; +type UsageKey = RateLimitUsageKey; fn new_test_ext() -> sp_io::TestExternalities { sp_tracing::try_init_simple(); From 0c1c6cb4c1725dd4975507e473d226865d614ff4 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 15 Dec 2025 17:26:52 +0300 Subject: [PATCH 42/95] Add limit setting rule to pallet-rate-limiting --- pallets/rate-limiting/src/lib.rs | 71 +++++++++++++++++++++++++++--- pallets/rate-limiting/src/mock.rs | 54 ++++++++++++++++++++++- pallets/rate-limiting/src/tests.rs | 44 +++++++++++++++++- pallets/rate-limiting/src/types.rs | 13 ++++++ 4 files changed, 173 insertions(+), 9 deletions(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index a4af7fc3fd..217611c002 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -146,8 +146,8 @@ pub use rate_limiting_interface::{RateLimitTarget, TransactionIdentifier}; pub use rate_limiting_interface::{RateLimitingInfo, TryIntoRateLimitTarget}; pub use tx_extension::RateLimitTransactionExtension; pub use types::{ - BypassDecision, GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitScopeResolver, - RateLimitUsageResolver, + BypassDecision, EnsureLimitSettingRule, GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, + RateLimitScopeResolver, RateLimitUsageResolver, }; #[cfg(feature = "runtime-benchmarks")] @@ -178,8 +178,9 @@ pub mod pallet { #[cfg(feature = "runtime-benchmarks")] use crate::benchmarking::BenchmarkHelper as BenchmarkHelperTrait; use crate::types::{ - BypassDecision, GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, - RateLimitScopeResolver, RateLimitTarget, RateLimitUsageResolver, TransactionIdentifier, + BypassDecision, EnsureLimitSettingRule, GroupSharing, RateLimit, RateLimitGroup, + RateLimitKind, RateLimitScopeResolver, RateLimitTarget, RateLimitUsageResolver, + TransactionIdentifier, }; type GroupNameOf = BoundedVec>::MaxGroupNameLength>; @@ -205,6 +206,16 @@ pub mod pallet { /// Origin permitted to configure rate limits. type AdminOrigin: EnsureOrigin>; + /// Rule type that decides which origins may call [`Pallet::set_rate_limit`]. + type LimitSettingRule: Parameter + Member + MaxEncodedLen + MaybeSerializeDeserialize; + + /// Default rule applied when a target does not have an explicit entry in + /// [`LimitSettingRules`]. + type DefaultLimitSettingRule: Get; + + /// Origin checker invoked when setting a rate limit, parameterized by the stored rule. + type LimitSettingOrigin: EnsureLimitSettingRule, Self::LimitSettingRule, Self::LimitScope>; + /// Scope identifier used to namespace stored rate limits. type LimitScope: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize; @@ -259,6 +270,23 @@ pub mod pallet { OptionQuery, >; + #[pallet::type_value] + pub fn DefaultLimitSettingRuleFor, I: 'static>() -> T::LimitSettingRule { + T::DefaultLimitSettingRule::get() + } + + /// Stores the rule used to authorize [`Pallet::set_rate_limit`] per call/group target. + #[pallet::storage] + #[pallet::getter(fn limit_setting_rule)] + pub type LimitSettingRules, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + RateLimitTarget<>::GroupId>, + >::LimitSettingRule, + ValueQuery, + DefaultLimitSettingRuleFor, + >; + /// Tracks when a rate-limited target was last observed per usage key. #[pallet::storage] pub type LastSeen, I: 'static = ()> = StorageDoubleMap< @@ -360,6 +388,13 @@ pub mod pallet { /// Extrinsic name associated with the transaction, when available. extrinsic: Option>, }, + /// The rule that authorizes [`Pallet::set_rate_limit`] was updated for a target. + LimitSettingRuleUpdated { + /// Target whose limit-setting rule changed. + target: RateLimitTarget<>::GroupId>, + /// Updated rule. + rule: >::LimitSettingRule, + }, /// A rate-limited call was deregistered or had a scoped entry cleared. CallDeregistered { /// Target whose configuration changed. @@ -978,7 +1013,8 @@ pub mod pallet { scope: Option<>::LimitScope>, limit: RateLimitKind>, ) -> DispatchResult { - T::AdminOrigin::ensure_origin(origin)?; + let rule = LimitSettingRules::::get(&target); + T::LimitSettingOrigin::ensure_origin(origin, &rule, &scope)?; let (transaction, pallet, extrinsic) = match target { RateLimitTarget::Transaction(identifier) => { @@ -1019,6 +1055,31 @@ pub mod pallet { Ok(()) } + /// Sets the rule used to authorize [`Pallet::set_rate_limit`] for the provided target. + #[pallet::call_index(10)] + #[pallet::weight(T::DbWeight::get().reads_writes(2, 1))] + pub fn set_limit_setting_rule( + origin: OriginFor, + target: RateLimitTarget<>::GroupId>, + rule: >::LimitSettingRule, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + match target { + RateLimitTarget::Transaction(identifier) => { + Self::ensure_call_registered(&identifier)?; + } + RateLimitTarget::Group(group) => { + Self::ensure_group_details(group)?; + } + } + + LimitSettingRules::::insert(target, rule.clone()); + Self::deposit_event(Event::LimitSettingRuleUpdated { target, rule }); + + Ok(()) + } + /// Assigns a registered call to the specified group and optionally marks it as read-only /// for usage tracking. #[pallet::call_index(2)] diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index d3d486f6a1..16e470be3e 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -4,13 +4,15 @@ use core::convert::TryInto; use frame_support::{ derive_impl, + dispatch::DispatchResult, sp_runtime::{ BuildStorage, traits::{BlakeTwo256, IdentityLookup}, }, - traits::{ConstU16, ConstU32, ConstU64, Everything}, + traits::{ConstU16, ConstU32, ConstU64, EnsureOrigin, Everything}, }; -use frame_system::EnsureRoot; +use frame_system::{EnsureRoot, ensure_signed}; +use serde::{Deserialize, Serialize}; use sp_core::H256; use sp_io::TestExternalities; use sp_std::vec::Vec; @@ -59,6 +61,51 @@ pub type LimitScope = u16; pub type UsageKey = u16; pub type GroupId = u32; +#[derive( + codec::Encode, + codec::Decode, + codec::DecodeWithMemTracking, + Serialize, + Deserialize, + Clone, + Copy, + PartialEq, + Eq, + scale_info::TypeInfo, + codec::MaxEncodedLen, + Debug, +)] +pub enum LimitSettingRule { + RootOnly, + AnySigned, +} + +frame_support::parameter_types! { + pub const DefaultLimitSettingRule: LimitSettingRule = LimitSettingRule::RootOnly; +} + +pub struct LimitSettingOrigin; + +impl pallet_rate_limiting::EnsureLimitSettingRule + for LimitSettingOrigin +{ + fn ensure_origin( + origin: RuntimeOrigin, + rule: &LimitSettingRule, + _scope: &Option, + ) -> DispatchResult { + match rule { + LimitSettingRule::RootOnly => EnsureRoot::::ensure_origin(origin) + .map(|_| ()) + .map_err(Into::into), + LimitSettingRule::AnySigned => { + let _ = ensure_signed(origin)?; + Ok(()) + } + } + } +} + pub struct TestScopeResolver; pub struct TestUsageResolver; @@ -123,6 +170,9 @@ impl pallet_rate_limiting::Config for Test { type UsageKey = UsageKey; type UsageResolver = TestUsageResolver; type AdminOrigin = EnsureRoot; + type LimitSettingRule = LimitSettingRule; + type DefaultLimitSettingRule = DefaultLimitSettingRule; + type LimitSettingOrigin = LimitSettingOrigin; type GroupId = GroupId; type MaxGroupMembers = ConstU32<32>; type MaxGroupNameLength = ConstU32<64>; diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index ecad8a5da8..5fc79a9362 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -2,8 +2,9 @@ use frame_support::{assert_noop, assert_ok}; use sp_std::vec::Vec; use crate::{ - CallGroups, CallReadOnly, Config, GroupMembers, GroupSharing, LastSeen, Limits, RateLimit, - RateLimitKind, RateLimitTarget, TransactionIdentifier, mock::*, pallet::Error, + CallGroups, CallReadOnly, Config, GroupMembers, GroupSharing, LastSeen, LimitSettingRules, + Limits, RateLimit, RateLimitKind, RateLimitTarget, TransactionIdentifier, mock::*, + pallet::Error, }; use frame_support::traits::Get; @@ -46,6 +47,45 @@ fn last_event() -> RuntimeEvent { pop_last_event() } +#[test] +fn set_rate_limit_respects_limit_setting_rule() { + new_test_ext().execute_with(|| { + let identifier = register(remark_call(), None); + let tx_target = target(identifier); + + // Default rule is root-only. + assert_noop!( + RateLimiting::set_rate_limit( + RuntimeOrigin::signed(1), + tx_target, + None, + RateLimitKind::Exact(1), + ), + sp_runtime::DispatchError::BadOrigin + ); + + // Root updates the limit-setting rule for this transaction target. + assert_ok!(RateLimiting::set_limit_setting_rule( + RuntimeOrigin::root(), + tx_target, + LimitSettingRule::AnySigned, + )); + + assert_eq!( + LimitSettingRules::::get(tx_target), + LimitSettingRule::AnySigned + ); + + // Now any signed origin may set the limit for this target. + assert_ok!(RateLimiting::set_rate_limit( + RuntimeOrigin::signed(1), + tx_target, + None, + RateLimitKind::Exact(7), + )); + }); +} + #[test] fn register_call_seeds_global_limit() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index 166c471d7e..f6f54b472f 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -1,4 +1,5 @@ use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::dispatch::DispatchResult; pub use rate_limiting_interface::{RateLimitTarget, TransactionIdentifier}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; @@ -60,6 +61,18 @@ pub trait RateLimitUsageResolver { fn context(origin: &Origin, call: &Call) -> Option>; } +/// Origin check performed when configuring a rate limit. +/// +/// `pallet-rate-limiting` supports configuring a distinct "who may set limits" rule per call/group +/// target. This trait is invoked by [`pallet::Pallet::set_rate_limit`] after loading the rule from +/// storage, allowing runtimes to implement arbitrary permissioning logic. +/// +/// Note: the hook receives the provided `scope` (if any). Some policies (for example "subnet owner") +/// require a scope value (such as `netuid`) in order to validate the caller. +pub trait EnsureLimitSettingRule { + fn ensure_origin(origin: Origin, rule: &Rule, scope: &Option) -> DispatchResult; +} + /// Sharing mode configured for a group. #[derive( Serialize, From ee5bde91af15bf3c94077ac1df7718cdcc3dc480 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 16 Dec 2025 16:08:53 +0300 Subject: [PATCH 43/95] Deprecate serving rate limit --- pallets/admin-utils/src/benchmarking.rs | 12 +++- pallets/admin-utils/src/lib.rs | 31 ++++----- pallets/admin-utils/src/tests/mod.rs | 20 +++--- pallets/subtensor/src/lib.rs | 6 +- pallets/subtensor/src/macros/dispatches.rs | 8 --- pallets/subtensor/src/macros/errors.rs | 1 + pallets/subtensor/src/subnets/serving.rs | 38 ----------- .../subtensor/src/transaction_extension.rs | 3 - pallets/subtensor/src/utils/misc.rs | 12 +++- runtime/src/lib.rs | 3 + runtime/src/rate_limiting/migration.rs | 17 ++++- runtime/src/rate_limiting/mod.rs | 67 ++++++++++++++++++- runtime/tests/rate_limiting_behavior.rs | 26 +++---- 13 files changed, 140 insertions(+), 104 deletions(-) diff --git a/pallets/admin-utils/src/benchmarking.rs b/pallets/admin-utils/src/benchmarking.rs index 7b8124144d..e83d0e222a 100644 --- a/pallets/admin-utils/src/benchmarking.rs +++ b/pallets/admin-utils/src/benchmarking.rs @@ -64,11 +64,19 @@ mod benchmarks { } #[benchmark] + #[allow(deprecated)] fn sudo_set_serving_rate_limit() { // disable admin freeze window pallet_subtensor::Pallet::::set_admin_freeze_window(0); - #[extrinsic_call] - _(RawOrigin::Root, 1u16.into()/*netuid*/, 100u64/*serving_rate_limit*/)/*sudo_set_serving_rate_limit*/; + #[block] + { + #[allow(deprecated)] + let _ = AdminUtils::::sudo_set_serving_rate_limit( + RawOrigin::Root.into(), + 1u16.into(), /*netuid*/ + 100u64, /*serving_rate_limit*/ + ); + } } #[benchmark] diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index eebefcf3e9..9220c960d0 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -117,6 +117,8 @@ pub mod pallet { MaxAllowedUidsGreaterThanDefaultMaxAllowedUids, /// Bad parameter value InvalidValue, + /// The called extrinsic has been deprecated. + Deprecated, } /// Enum for specifying the type of precompile operation. #[derive( @@ -220,31 +222,22 @@ pub mod pallet { } /// The extrinsic sets the serving rate limit for a subnet. - /// It is only callable by the root account or subnet owner. - /// The extrinsic will call the Subtensor pallet to set the serving rate limit. + /// + /// Deprecated: serving rate limits are now configured via `pallet-rate-limiting` on the + /// serving group target (`GROUP_SERVE`) with `scope = Some(netuid)`. #[pallet::call_index(3)] #[pallet::weight(Weight::from_parts(22_980_000, 0) .saturating_add(::DbWeight::get().reads(2_u64)) .saturating_add(::DbWeight::get().writes(1_u64)))] + #[deprecated( + note = "deprecated: configure via pallet-rate-limiting::set_rate_limit(target=Group(GROUP_SERVE), scope=Some(netuid), ...)" + )] pub fn sudo_set_serving_rate_limit( - origin: OriginFor, - netuid: NetUid, - serving_rate_limit: u64, + _origin: OriginFor, + _netuid: NetUid, + _serving_rate_limit: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::ServingRateLimit.into()], - )?; - pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; - pallet_subtensor::Pallet::::set_serving_rate_limit(netuid, serving_rate_limit); - log::debug!("ServingRateLimitSet( serving_rate_limit: {serving_rate_limit:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::ServingRateLimit.into()], - ); - Ok(()) + Err(Error::::Deprecated.into()) } /// The extrinsic sets the minimum difficulty for a subnet. diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index 024871e60f..a5fb053806 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -44,26 +44,30 @@ fn test_sudo_set_default_take() { } #[test] +#[allow(deprecated)] fn test_sudo_set_serving_rate_limit() { new_test_ext().execute_with(|| { let netuid = NetUid::from(3); let to_be_set: u64 = 10; let init_value: u64 = SubtensorModule::get_serving_rate_limit(netuid); - assert_eq!( + assert_noop!( AdminUtils::sudo_set_serving_rate_limit( <::RuntimeOrigin>::signed(U256::from(1)), netuid, to_be_set ), - Err(DispatchError::BadOrigin) + Error::::Deprecated + ); + assert_eq!(SubtensorModule::get_serving_rate_limit(netuid), init_value); + assert_noop!( + AdminUtils::sudo_set_serving_rate_limit( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + ), + Error::::Deprecated ); assert_eq!(SubtensorModule::get_serving_rate_limit(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_serving_rate_limit( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_serving_rate_limit(netuid), to_be_set); }); } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index a85c5f37d4..c2c210d959 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1235,7 +1235,7 @@ pub mod pallet { /// ================== /// ==== Coinbase ==== /// ================== - /// --- ITEM ( global_block_emission ) + /// --- ITEM ( global_block_emission ) #[pallet::storage] pub type BlockEmission = StorageValue<_, u64, ValueQuery, DefaultBlockEmission>; @@ -1275,7 +1275,7 @@ pub mod pallet { #[pallet::storage] pub type TotalStake = StorageValue<_, TaoCurrency, ValueQuery, DefaultZeroTao>; - /// --- ITEM ( moving_alpha ) -- subnet moving alpha. + /// --- ITEM ( moving_alpha ) -- subnet moving alpha. #[pallet::storage] pub type SubnetMovingAlpha = StorageValue<_, I96F32, ValueQuery, DefaultMovingAlpha>; @@ -2434,7 +2434,6 @@ pub enum CustomTransactionError { TransferDisallowed, HotKeyNotRegisteredInNetwork, InvalidIpAddress, - ServingRateLimitExceeded, InvalidPort, BadRequest, ZeroMaxAmount, @@ -2461,7 +2460,6 @@ impl From for u8 { CustomTransactionError::TransferDisallowed => 9, CustomTransactionError::HotKeyNotRegisteredInNetwork => 10, CustomTransactionError::InvalidIpAddress => 11, - CustomTransactionError::ServingRateLimitExceeded => 12, CustomTransactionError::InvalidPort => 13, CustomTransactionError::BadRequest => 255, CustomTransactionError::ZeroMaxAmount => 14, diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 61d5523285..a17e257039 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -814,10 +814,6 @@ mod dispatches { /// /// * 'InvalidIpAddress': /// - The numerically encoded ip address does not resolve to a proper ip. - /// - /// * 'ServingRateLimitExceeded': - /// - Attempting to set prometheus information withing the rate limit min. - /// #[pallet::call_index(4)] #[pallet::weight((Weight::from_parts(33_010_000, 0) .saturating_add(T::DbWeight::get().reads(4)) @@ -898,10 +894,6 @@ mod dispatches { /// /// * 'InvalidIpAddress': /// - The numerically encoded ip address does not resolve to a proper ip. - /// - /// * 'ServingRateLimitExceeded': - /// - Attempting to set prometheus information withing the rate limit min. - /// #[pallet::call_index(40)] #[pallet::weight((Weight::from_parts(32_510_000, 0) .saturating_add(T::DbWeight::get().reads(4)) diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 5a15330075..1330b81738 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -79,6 +79,7 @@ mod errors { SettingWeightsTooFast, /// A validator is attempting to set weights from a validator with incorrect weight version. IncorrectWeightVersionKey, + /// DEPRECATED /// An axon or prometheus serving exceeded the rate limit for a registered neuron. ServingRateLimitExceeded, /// The caller is attempting to set weights with more UIDs than allowed. diff --git a/pallets/subtensor/src/subnets/serving.rs b/pallets/subtensor/src/subnets/serving.rs index cdaf39e51b..b46f0edf06 100644 --- a/pallets/subtensor/src/subnets/serving.rs +++ b/pallets/subtensor/src/subnets/serving.rs @@ -51,10 +51,6 @@ impl Pallet { /// /// * 'InvalidIpAddress': /// - The numerically encoded ip address does not resolve to a proper ip. - /// - /// * 'ServingRateLimitExceeded': - /// - Attempting to set prometheus information withing the rate limit min. - /// pub fn do_serve_axon( origin: T::RuntimeOrigin, netuid: NetUid, @@ -155,10 +151,6 @@ impl Pallet { /// /// * 'InvalidIpAddress': /// - The numerically encoded ip address does not resolve to a proper ip. - /// - /// * 'ServingRateLimitExceeded': - /// - Attempting to set prometheus information withing the rate limit min. - /// pub fn do_serve_prometheus( origin: T::RuntimeOrigin, netuid: NetUid, @@ -185,11 +177,6 @@ impl Pallet { // We get the previous axon info assoicated with this ( netuid, uid ) let mut prev_prometheus = Self::get_prometheus_info(netuid, &hotkey_id); - let current_block: u64 = Self::get_current_block_as_u64(); - ensure!( - Self::prometheus_passes_rate_limit(netuid, &prev_prometheus, current_block), - Error::::ServingRateLimitExceeded - ); // We insert the prometheus meta. prev_prometheus.block = Self::get_current_block_as_u64(); @@ -220,26 +207,6 @@ impl Pallet { --==[[ Helper functions ]]==-- *********************************/ - pub fn axon_passes_rate_limit( - netuid: NetUid, - prev_axon_info: &AxonInfoOf, - current_block: u64, - ) -> bool { - let rate_limit: u64 = Self::get_serving_rate_limit(netuid); - let last_serve = prev_axon_info.block; - rate_limit == 0 || last_serve == 0 || current_block.saturating_sub(last_serve) >= rate_limit - } - - pub fn prometheus_passes_rate_limit( - netuid: NetUid, - prev_prometheus_info: &PrometheusInfoOf, - current_block: u64, - ) -> bool { - let rate_limit: u64 = Self::get_serving_rate_limit(netuid); - let last_serve = prev_prometheus_info.block; - rate_limit == 0 || last_serve == 0 || current_block.saturating_sub(last_serve) >= rate_limit - } - pub fn get_axon_info(netuid: NetUid, hotkey: &T::AccountId) -> AxonInfoOf { if let Some(axons) = Axons::::get(netuid, hotkey) { axons @@ -345,11 +312,6 @@ impl Pallet { // Get the previous axon information. let mut prev_axon = Self::get_axon_info(netuid, hotkey_id); - let current_block: u64 = Self::get_current_block_as_u64(); - ensure!( - Self::axon_passes_rate_limit(netuid, &prev_axon, current_block), - Error::::ServingRateLimitExceeded - ); // Validate axon data with delegate func prev_axon.block = Self::get_current_block_as_u64(); diff --git a/pallets/subtensor/src/transaction_extension.rs b/pallets/subtensor/src/transaction_extension.rs index cf1d410ea9..e227e2d483 100644 --- a/pallets/subtensor/src/transaction_extension.rs +++ b/pallets/subtensor/src/transaction_extension.rs @@ -70,9 +70,6 @@ where CustomTransactionError::HotKeyNotRegisteredInNetwork.into() } Error::::InvalidIpAddress => CustomTransactionError::InvalidIpAddress.into(), - Error::::ServingRateLimitExceeded => { - CustomTransactionError::ServingRateLimitExceeded.into() - } Error::::InvalidPort => CustomTransactionError::InvalidPort.into(), _ => CustomTransactionError::BadRequest.into(), }) diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 10fc0535f0..70b1defcd7 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -1,12 +1,15 @@ use super::*; use crate::Error; use crate::system::{ensure_signed, ensure_signed_or_root, pallet_prelude::BlockNumberFor}; +use rate_limiting_interface::RateLimitingInfo; use safe_math::*; use sp_core::Get; use sp_core::U256; -use sp_runtime::Saturating; +use sp_runtime::{SaturatedConversion, Saturating}; use substrate_fixed::types::{I32F32, I64F64, U64F64, U96F32}; -use subtensor_runtime_common::{AlphaCurrency, NetUid, NetUidStorageIndex, TaoCurrency}; +use subtensor_runtime_common::{ + AlphaCurrency, NetUid, NetUidStorageIndex, TaoCurrency, rate_limiting, +}; impl Pallet { pub fn ensure_subnet_owner_or_root( @@ -428,8 +431,11 @@ impl Pallet { } pub fn get_serving_rate_limit(netuid: NetUid) -> u64 { - ServingRateLimit::::get(netuid) + T::RateLimiting::rate_limit(rate_limiting::GROUP_SERVE, Some(netuid)) + .unwrap_or_default() + .saturated_into() } + pub fn set_serving_rate_limit(netuid: NetUid, serving_rate_limit: u64) { ServingRateLimit::::insert(netuid, serving_rate_limit); Self::deposit_event(Event::ServingRateLimitSet(netuid, serving_rate_limit)); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 8cde7d8999..5a659146ed 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1148,6 +1148,9 @@ parameter_types! { impl pallet_rate_limiting::Config for Runtime { type RuntimeCall = RuntimeCall; type AdminOrigin = EnsureRoot; + type LimitSettingRule = rate_limiting::LimitSettingRule; + type DefaultLimitSettingRule = rate_limiting::DefaultLimitSettingRule; + type LimitSettingOrigin = rate_limiting::LimitSettingOrigin; type LimitScope = NetUid; type LimitScopeResolver = RuntimeScopeResolver; type UsageKey = RateLimitUsageKey; diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/rate_limiting/migration.rs index f77f6ec2dc..360933bb48 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/rate_limiting/migration.rs @@ -25,7 +25,7 @@ use subtensor_runtime_common::{ }; use super::{ - AccountId, RateLimitUsageKey, Runtime, + AccountId, LimitSettingRule, RateLimitUsageKey, Runtime, legacy::{ Hyperparameter, RateLimitKey, TransactionType, defaults as legacy_defaults, storage as legacy_storage, @@ -117,6 +117,14 @@ pub fn migrate_rate_limiting() -> Weight { .saturating_add(limit_writes) .saturating_add(last_seen_writes); + // Legacy parity: serving-rate-limit configuration is allowed for root OR subnet owner. + // Everything else remains default (`AdminOrigin` / root in this runtime). + pallet_rate_limiting::LimitSettingRules::::insert( + RateLimitTarget::Group(GROUP_SERVE), + LimitSettingRule::RootOrSubnetOwnerAdminWindow, + ); + writes += 1; + HasMigrationRun::::insert(MIGRATION_NAME, true); writes += 1; @@ -1087,6 +1095,13 @@ mod tests { Some(DELEGATE_TAKE_GROUP_ID) ); assert_eq!(pallet_rate_limiting::NextGroupId::::get(), 7); + + let serve_target = RateLimitTarget::Group(GROUP_SERVE); + assert!(pallet_rate_limiting::LimitSettingRules::::contains_key(serve_target)); + assert_eq!( + pallet_rate_limiting::LimitSettingRules::::get(serve_target), + crate::rate_limiting::LimitSettingRule::RootOrSubnetOwnerAdminWindow + ); }); } diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index d0b91c34a7..9b370229fe 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -14,11 +14,17 @@ //! Long-term, we should refactor `pallet-subtensor` into smaller pallets and move to dedicated //! `pallet-rate-limiting` instances per pallet. +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::traits::Get; use frame_system::RawOrigin; use pallet_admin_utils::Call as AdminUtilsCall; -use pallet_rate_limiting::BypassDecision; -use pallet_rate_limiting::{RateLimitScopeResolver, RateLimitUsageResolver}; +use pallet_rate_limiting::{ + BypassDecision, EnsureLimitSettingRule, RateLimitScopeResolver, RateLimitUsageResolver, +}; use pallet_subtensor::{Call as SubtensorCall, Tempo}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_runtime::DispatchError; use sp_std::{vec, vec::Vec}; use subtensor_runtime_common::{ BlockNumber, NetUid, @@ -30,6 +36,63 @@ use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; mod legacy; pub mod migration; +/// Authorization rules for configuring rate limits via `pallet-rate-limiting::set_rate_limit`. +/// +/// Legacy note: historically, all rate-limit setters were `Root`-only except +/// `admin-utils::sudo_set_serving_rate_limit` (subnet-owner-or-root). We preserve that behavior by +/// requiring a `scope` value when using the [`LimitSettingRule::RootOrSubnetOwnerAdminWindow`] rule and +/// validating subnet ownership against that `scope` (`netuid`). +#[derive( + Encode, + Decode, + DecodeWithMemTracking, + Serialize, + Deserialize, + Clone, + PartialEq, + Eq, + TypeInfo, + MaxEncodedLen, + Debug, +)] +pub enum LimitSettingRule { + /// Require `Root`. + Root, + /// Allow `Root` or the subnet owner for the provided `netuid` scope. + /// + /// This rule requires `scope == Some(netuid)`. + RootOrSubnetOwnerAdminWindow, +} + +pub struct DefaultLimitSettingRule; + +impl Get for DefaultLimitSettingRule { + fn get() -> LimitSettingRule { + LimitSettingRule::Root + } +} + +pub struct LimitSettingOrigin; + +impl EnsureLimitSettingRule for LimitSettingOrigin { + fn ensure_origin( + origin: RuntimeOrigin, + rule: &LimitSettingRule, + scope: &Option, + ) -> frame_support::dispatch::DispatchResult { + match rule { + LimitSettingRule::Root => frame_system::ensure_root(origin).map_err(Into::into), + LimitSettingRule::RootOrSubnetOwnerAdminWindow => { + let netuid = scope.ok_or(DispatchError::BadOrigin)?; + pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; + pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid) + .map(|_| ()) + .map_err(Into::into) + } + } + } +} + #[derive(Default)] pub struct ScopeResolver; diff --git a/runtime/tests/rate_limiting_behavior.rs b/runtime/tests/rate_limiting_behavior.rs index 3066125ef6..0350fa9f11 100644 --- a/runtime/tests/rate_limiting_behavior.rs +++ b/runtime/tests/rate_limiting_behavior.rs @@ -278,14 +278,11 @@ fn serving_parity() { }); let origin = RuntimeOrigin::signed(hot.clone()); let legacy_axon = || { - SubtensorModule::axon_passes_rate_limit( - netuid, - &AxonInfo { - block: now - 1, - ..Default::default() - }, - now, - ) + let info = AxonInfo { + block: now.saturating_sub(1), + ..Default::default() + }; + now.saturating_sub(info.block) >= ServingRateLimit::::get(netuid) }; parity_check(now, axon_call, origin.clone(), None, None, legacy_axon); @@ -298,14 +295,11 @@ fn serving_parity() { ip_type: 4, }); let legacy_prom = || { - SubtensorModule::prometheus_passes_rate_limit( - netuid, - &PrometheusInfo { - block: now - 1, - ..Default::default() - }, - now, - ) + let info = PrometheusInfo { + block: now.saturating_sub(1), + ..Default::default() + }; + now.saturating_sub(info.block) >= ServingRateLimit::::get(netuid) }; parity_check(now, prom_call, origin, None, None, legacy_prom); }); From f1504c650a438bbb0f2c4f2b6d309e239593e978 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 23 Dec 2025 12:28:27 +0100 Subject: [PATCH 44/95] Fix TX-extension type definition --- node/Cargo.toml | 2 ++ node/src/benchmarking.rs | 8 ++++++-- node/src/mev_shield/author.rs | 16 ++++++++++++---- pallets/admin-utils/src/benchmarking.rs | 4 ++-- runtime/src/lib.rs | 17 ++++++++++++----- 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/node/Cargo.toml b/node/Cargo.toml index 1d2351c265..06b96c0eba 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -70,6 +70,7 @@ frame-system.workspace = true pallet-transaction-payment.workspace = true pallet-commitments.workspace = true pallet-drand.workspace = true +pallet-rate-limiting.workspace = true sp-crypto-ec-utils = { workspace = true, default-features = true, features = [ "bls12-381", ] } @@ -173,6 +174,7 @@ runtime-benchmarks = [ "polkadot-sdk/runtime-benchmarks", "pallet-subtensor/runtime-benchmarks", "pallet-shield/runtime-benchmarks", + "pallet-rate-limiting/runtime-benchmarks", ] pow-faucet = [] diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index 5430a75d9e..c8dcc4cef0 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -143,8 +143,12 @@ pub fn create_benchmark_extrinsic( pallet_subtensor::transaction_extension::SubtensorTransactionExtension::< runtime::Runtime, >::new(), - pallet_drand::drand_priority::DrandPriority::::new(), - frame_metadata_hash_extension::CheckMetadataHash::::new(true), + // Keep the same order while staying under the 12-item tuple limit. + ( + pallet_drand::drand_priority::DrandPriority::::new(), + frame_metadata_hash_extension::CheckMetadataHash::::new(true), + ), + pallet_rate_limiting::RateLimitTransactionExtension::::new(), ); let raw_payload = runtime::SignedPayload::from_raw( diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index 99000d4ac6..2501bcec20 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -396,8 +396,12 @@ where pallet_subtensor::transaction_extension::SubtensorTransactionExtension::< runtime::Runtime, >::new(), - pallet_drand::drand_priority::DrandPriority::::new(), - frame_metadata_hash_extension::CheckMetadataHash::::new(false), + // Keep the same order while staying under the 12-item tuple limit. + ( + pallet_drand::drand_priority::DrandPriority::::new(), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), + ), + pallet_rate_limiting::RateLimitTransactionExtension::::new(), ); // 3) Manually construct the `Implicit` tuple that the runtime will also derive. @@ -431,8 +435,12 @@ where (), // ChargeTransactionPaymentWrapper::Implicit = () (), // SudoTransactionExtension::Implicit = () (), // SubtensorTransactionExtension::Implicit = () - (), // DrandPriority::Implicit = () - None, // CheckMetadataHash::Implicit = Option<[u8; 32]> + // Match the nested tuple shape used by TransactionExtensions. + ( + (), // DrandPriority::Implicit = () + None, // CheckMetadataHash::Implicit = Option<[u8; 32]> + ), + (), // RateLimitTransactionExtension::Implicit = () ); // 4) Build the exact signable payload from call + extra + implicit. diff --git a/pallets/admin-utils/src/benchmarking.rs b/pallets/admin-utils/src/benchmarking.rs index e83d0e222a..105a8b8ddb 100644 --- a/pallets/admin-utils/src/benchmarking.rs +++ b/pallets/admin-utils/src/benchmarking.rs @@ -73,8 +73,8 @@ mod benchmarks { #[allow(deprecated)] let _ = AdminUtils::::sudo_set_serving_rate_limit( RawOrigin::Root.into(), - 1u16.into(), /*netuid*/ - 100u64, /*serving_rate_limit*/ + 1u16.into(), /*netuid*/ + 100u64, /*serving_rate_limit*/ ); } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ee2dda35c6..10a0670a0f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -175,15 +175,17 @@ impl frame_system::offchain::CreateSignedTransaction frame_system::CheckEra::::from(Era::Immortal), check_nonce::CheckNonce::::from(nonce).into(), frame_system::CheckWeight::::new(), - pallet_rate_limiting::RateLimitTransactionExtension::::new(), ChargeTransactionPaymentWrapper::new( pallet_transaction_payment::ChargeTransactionPayment::::from(0), ), SudoTransactionExtension::::new(), pallet_subtensor::transaction_extension::SubtensorTransactionExtension::::new( ), - pallet_drand::drand_priority::DrandPriority::::new(), - frame_metadata_hash_extension::CheckMetadataHash::::new(true), + ( + pallet_drand::drand_priority::DrandPriority::::new(), + frame_metadata_hash_extension::CheckMetadataHash::::new(true), + ), + pallet_rate_limiting::RateLimitTransactionExtension::::new(), ); let raw_payload = SignedPayload::new(call.clone(), extra.clone()).ok()?; @@ -1679,6 +1681,8 @@ pub type Header = generic::Header; // Block type as expected by this runtime. pub type Block = generic::Block; // The extensions to the basic transaction logic. +// Note: The SDK only implements TransactionExtension for tuples up to 12 items, so we nest the last +// two extensions to keep order/encoding while staying under the limit. pub type TransactionExtensions = ( frame_system::CheckNonZeroSender, frame_system::CheckSpecVersion, @@ -1691,8 +1695,11 @@ pub type TransactionExtensions = ( ChargeTransactionPaymentWrapper, SudoTransactionExtension, pallet_subtensor::transaction_extension::SubtensorTransactionExtension, - pallet_drand::drand_priority::DrandPriority, - frame_metadata_hash_extension::CheckMetadataHash, + ( + pallet_drand::drand_priority::DrandPriority, + frame_metadata_hash_extension::CheckMetadataHash, + ), + pallet_rate_limiting::RateLimitTransactionExtension, ); type Migrations = ( From 913ee9724f40ec92ac39a3453e5bc17c06c1a3c0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 23 Dec 2025 13:47:14 +0100 Subject: [PATCH 45/95] Deprecate serving rate limit --- Cargo.lock | 2 + chain-extensions/src/mock.rs | 2 - node/src/benchmarking.rs | 2 +- pallets/admin-utils/src/tests/mock.rs | 2 - pallets/subtensor/src/benchmarks.rs | 2 - pallets/subtensor/src/coinbase/root.rs | 1 - pallets/subtensor/src/lib.rs | 11 ----- pallets/subtensor/src/macros/config.rs | 3 -- pallets/subtensor/src/macros/events.rs | 2 +- pallets/subtensor/src/tests/mock.rs | 2 - pallets/subtensor/src/tests/networks.rs | 2 - pallets/subtensor/src/tests/serving.rs | 50 ++++++++++------------- pallets/subtensor/src/utils/misc.rs | 5 --- pallets/transaction-fee/src/tests/mock.rs | 2 - precompiles/Cargo.toml | 2 + precompiles/src/lib.rs | 21 ++++++++-- precompiles/src/subnet.rs | 30 ++++++++++---- runtime/src/lib.rs | 3 -- runtime/src/rate_limiting/legacy.rs | 8 ++-- runtime/tests/rate_limiting_behavior.rs | 19 ++++++--- 20 files changed, 84 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 351e156aad..8bea2f5c3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8222,6 +8222,7 @@ dependencies = [ "num-traits", "pallet-commitments", "pallet-drand", + "pallet-rate-limiting", "pallet-shield", "pallet-subtensor", "pallet-subtensor-swap-rpc", @@ -18166,6 +18167,7 @@ dependencies = [ "pallet-evm-precompile-modexp", "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", + "pallet-rate-limiting", "pallet-subtensor", "pallet-subtensor-proxy", "pallet-subtensor-swap", diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 53195c6cd9..ae99c4031e 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -294,7 +294,6 @@ parameter_types! { pub const InitialMinChildKeyTake: u16 = 0; // 0 %; pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; - pub const InitialServingRateLimit: u64 = 0; // No limit. pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxDelegateTakeRateLimit: u64 = 1; // 1 block take rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 1; // 1 block take rate limit for testing @@ -380,7 +379,6 @@ impl pallet_subtensor::Config for Test { type InitialWeightsVersionKey = InitialWeightsVersionKey; type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; - type InitialServingRateLimit = InitialServingRateLimit; type InitialTxRateLimit = InitialTxRateLimit; type InitialTxDelegateTakeRateLimit = InitialTxDelegateTakeRateLimit; type InitialBurn = InitialBurn; diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index c8dcc4cef0..2f401e3e95 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -165,8 +165,8 @@ pub fn create_benchmark_extrinsic( (), (), (), + ((), None), (), - None, ), ); let signature = raw_payload.using_encoded(|e| sender.sign(e)); diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 0633109caa..211a72ef08 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -104,7 +104,6 @@ parameter_types! { pub const InitialMinChildKeyTake: u16 = 0; // Allow 0 % pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; - pub const InitialServingRateLimit: u64 = 0; // No limit. pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxDelegateTakeRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 0; // Disable rate limit for testing @@ -190,7 +189,6 @@ impl pallet_subtensor::Config for Test { type InitialWeightsVersionKey = InitialWeightsVersionKey; type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; - type InitialServingRateLimit = InitialServingRateLimit; type InitialTxRateLimit = InitialTxRateLimit; type InitialTxDelegateTakeRateLimit = InitialTxDelegateTakeRateLimit; type InitialTxChildKeyTakeRateLimit = InitialTxChildKeyTakeRateLimit; diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index 476be905e9..67b7f473b0 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -157,7 +157,6 @@ mod pallet_benchmarks { netuid, caller.clone() )); - Subtensor::::set_serving_rate_limit(netuid, 0); #[extrinsic_call] _( @@ -195,7 +194,6 @@ mod pallet_benchmarks { netuid, caller.clone() )); - Subtensor::::set_serving_rate_limit(netuid, 0); #[extrinsic_call] _( diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 328ce3805c..83ff49abbf 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -325,7 +325,6 @@ impl Pallet { LastAdjustmentBlock::::remove(netuid); // --- 16. Serving / rho / curves, and other per-net controls. - ServingRateLimit::::remove(netuid); Rho::::remove(netuid); AlphaSigmoidSteepness::::remove(netuid); diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 2954b6a7c5..c44381dc2b 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -908,12 +908,6 @@ pub mod pallet { 0 } - /// Default value for serving rate limit. - #[pallet::type_value] - pub fn DefaultServingRateLimit() -> u64 { - T::InitialServingRateLimit::get() - } - /// Default value for weight commit/reveal enabled. #[pallet::type_value] pub fn DefaultCommitRevealWeightsEnabled() -> bool { @@ -1674,11 +1668,6 @@ pub mod pallet { pub type RecycleOrBurn = StorageMap<_, Identity, NetUid, RecycleOrBurnEnum, ValueQuery, DefaultRecycleOrBurn>; - /// --- MAP ( netuid ) --> serving_rate_limit - #[pallet::storage] - pub type ServingRateLimit = - StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultServingRateLimit>; - /// --- MAP ( netuid ) --> Rho #[pallet::storage] pub type Rho = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultRho>; diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index f8742087ff..b6dc13059d 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -183,9 +183,6 @@ mod config { /// Initial weights version key. #[pallet::constant] type InitialWeightsVersionKey: Get; - /// Initial serving rate limit. - #[pallet::constant] - type InitialServingRateLimit: Get; /// Initial transaction rate limit. #[pallet::constant] type InitialTxRateLimit: Get; diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index a06e035d86..baf39b2994 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -101,7 +101,7 @@ mod events { MinDifficultySet(NetUid, u64), /// setting max difficulty on a network. MaxDifficultySet(NetUid, u64), - /// setting the prometheus serving rate limit. + /// [DEPRECATED] setting the prometheus serving rate limit. ServingRateLimitSet(NetUid, u64), /// setting burn on a network. BurnSet(NetUid, TaoCurrency), diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 161e0e372b..8cb3478b34 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -179,7 +179,6 @@ parameter_types! { pub const InitialMinChildKeyTake: u16 = 0; // 0 %; pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; - pub const InitialServingRateLimit: u64 = 0; // No limit. pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxDelegateTakeRateLimit: u64 = 1; // 1 block take rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 1; // 1 block take rate limit for testing @@ -265,7 +264,6 @@ impl crate::Config for Test { type InitialWeightsVersionKey = InitialWeightsVersionKey; type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; - type InitialServingRateLimit = InitialServingRateLimit; type InitialTxRateLimit = InitialTxRateLimit; type InitialTxDelegateTakeRateLimit = InitialTxDelegateTakeRateLimit; type InitialBurn = InitialBurn; diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 8214d58be0..8a8850b178 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -388,7 +388,6 @@ fn dissolve_clears_all_per_subnet_storages() { PendingOwnerCut::::insert(net, AlphaCurrency::from(1)); BlocksSinceLastStep::::insert(net, 1u64); LastMechansimStepBlock::::insert(net, 1u64); - ServingRateLimit::::insert(net, 1u64); Rho::::insert(net, 1u16); AlphaSigmoidSteepness::::insert(net, 1i16); @@ -548,7 +547,6 @@ fn dissolve_clears_all_per_subnet_storages() { assert!(!PendingOwnerCut::::contains_key(net)); assert!(!BlocksSinceLastStep::::contains_key(net)); assert!(!LastMechansimStepBlock::::contains_key(net)); - assert!(!ServingRateLimit::::contains_key(net)); assert!(!Rho::::contains_key(net)); assert!(!AlphaSigmoidSteepness::::contains_key(net)); diff --git a/pallets/subtensor/src/tests/serving.rs b/pallets/subtensor/src/tests/serving.rs index b52666bf26..d56787cccd 100644 --- a/pallets/subtensor/src/tests/serving.rs +++ b/pallets/subtensor/src/tests/serving.rs @@ -281,23 +281,19 @@ fn test_axon_serving_rate_limit_exceeded() { placeholder1, placeholder2 )); - SubtensorModule::set_serving_rate_limit(netuid, 2); run_to_block(2); // Go to block 2 - // Needs to be 2 blocks apart, we are only 1 block apart - assert_eq!( - SubtensorModule::serve_axon( - <::RuntimeOrigin>::signed(hotkey_account_id), - netuid, - version, - ip, - port, - ip_type, - protocol, - placeholder1, - placeholder2 - ), - Err(Error::::ServingRateLimitExceeded.into()) - ); + // Rate limiting is enforced by the transaction extension, not the pallet call. + assert_ok!(SubtensorModule::serve_axon( + <::RuntimeOrigin>::signed(hotkey_account_id), + netuid, + version, + ip, + port, + ip_type, + protocol, + placeholder1, + placeholder2 + )); }); } @@ -479,19 +475,15 @@ fn test_prometheus_serving_rate_limit_exceeded() { port, ip_type )); - SubtensorModule::set_serving_rate_limit(netuid, 1); - // Same block, need 1 block to pass - assert_eq!( - SubtensorModule::serve_prometheus( - <::RuntimeOrigin>::signed(hotkey_account_id), - netuid, - version, - ip, - port, - ip_type - ), - Err(Error::::ServingRateLimitExceeded.into()) - ); + // Rate limiting is enforced by the transaction extension, not the pallet call. + assert_ok!(SubtensorModule::serve_prometheus( + <::RuntimeOrigin>::signed(hotkey_account_id), + netuid, + version, + ip, + port, + ip_type + )); }); } diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 70b1defcd7..05c1af7d6f 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -436,11 +436,6 @@ impl Pallet { .saturated_into() } - pub fn set_serving_rate_limit(netuid: NetUid, serving_rate_limit: u64) { - ServingRateLimit::::insert(netuid, serving_rate_limit); - Self::deposit_event(Event::ServingRateLimitSet(netuid, serving_rate_limit)); - } - pub fn get_min_difficulty(netuid: NetUid) -> u64 { MinDifficulty::::get(netuid) } diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 2b25273104..c5f25b54e8 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -171,7 +171,6 @@ parameter_types! { pub const InitialMinChildKeyTake: u16 = 0; // Allow 0 % pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; - pub const InitialServingRateLimit: u64 = 0; // No limit. pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxDelegateTakeRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 0; // Disable rate limit for testing @@ -257,7 +256,6 @@ impl pallet_subtensor::Config for Test { type InitialWeightsVersionKey = InitialWeightsVersionKey; type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; - type InitialServingRateLimit = InitialServingRateLimit; type InitialTxRateLimit = InitialTxRateLimit; type InitialTxDelegateTakeRateLimit = InitialTxDelegateTakeRateLimit; type InitialTxChildKeyTakeRateLimit = InitialTxChildKeyTakeRateLimit; diff --git a/precompiles/Cargo.toml b/precompiles/Cargo.toml index 12460dcfbb..599b404350 100644 --- a/precompiles/Cargo.toml +++ b/precompiles/Cargo.toml @@ -34,6 +34,7 @@ substrate-fixed.workspace = true pallet-subtensor.workspace = true pallet-subtensor-swap.workspace = true pallet-admin-utils.workspace = true +pallet-rate-limiting.workspace = true subtensor-swap-interface.workspace = true pallet-crowdloan.workspace = true @@ -50,6 +51,7 @@ std = [ "log/std", "pallet-admin-utils/std", "pallet-balances/std", + "pallet-rate-limiting/std", "pallet-evm-precompile-dispatch/std", "pallet-evm-precompile-modexp/std", "pallet-evm-precompile-sha3fips/std", diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index 8069a1eb92..fe08fbec5a 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -66,12 +66,17 @@ where + pallet_subtensor::Config + pallet_subtensor_swap::Config + pallet_proxy::Config - + pallet_crowdloan::Config, + + pallet_crowdloan::Config + + pallet_rate_limiting::Config< + LimitScope = subtensor_runtime_common::NetUid, + GroupId = subtensor_runtime_common::rate_limiting::GroupId, + >, R::AccountId: From<[u8; 32]> + ByteArray + Into<[u8; 32]>, ::RuntimeCall: From> + From> + From> + From> + + From> + From> + GetDispatchInfo + Dispatchable, @@ -93,12 +98,17 @@ where + pallet_subtensor::Config + pallet_subtensor_swap::Config + pallet_proxy::Config - + pallet_crowdloan::Config, + + pallet_crowdloan::Config + + pallet_rate_limiting::Config< + LimitScope = subtensor_runtime_common::NetUid, + GroupId = subtensor_runtime_common::rate_limiting::GroupId, + >, R::AccountId: From<[u8; 32]> + ByteArray + Into<[u8; 32]>, ::RuntimeCall: From> + From> + From> + From> + + From> + From> + GetDispatchInfo + Dispatchable, @@ -149,12 +159,17 @@ where + pallet_subtensor::Config + pallet_subtensor_swap::Config + pallet_proxy::Config - + pallet_crowdloan::Config, + + pallet_crowdloan::Config + + pallet_rate_limiting::Config< + LimitScope = subtensor_runtime_common::NetUid, + GroupId = subtensor_runtime_common::rate_limiting::GroupId, + >, R::AccountId: From<[u8; 32]> + ByteArray + Into<[u8; 32]>, ::RuntimeCall: From> + From> + From> + From> + + From> + From> + GetDispatchInfo + Dispatchable diff --git a/precompiles/src/subnet.rs b/precompiles/src/subnet.rs index b7f5cdb098..bdda32a6ce 100644 --- a/precompiles/src/subnet.rs +++ b/precompiles/src/subnet.rs @@ -4,9 +4,10 @@ use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo}; use frame_support::traits::ConstU32; use frame_system::RawOrigin; use pallet_evm::{AddressMapping, PrecompileHandle}; +use pallet_rate_limiting::{RateLimitKind, RateLimitTarget}; use precompile_utils::{EvmResult, prelude::BoundedString}; use sp_core::H256; -use sp_runtime::traits::Dispatchable; +use sp_runtime::traits::{Dispatchable, SaturatedConversion}; use sp_std::vec; use subtensor_runtime_common::{Currency, NetUid}; @@ -19,10 +20,15 @@ where R: frame_system::Config + pallet_evm::Config + pallet_subtensor::Config - + pallet_admin_utils::Config, + + pallet_admin_utils::Config + + pallet_rate_limiting::Config< + LimitScope = NetUid, + GroupId = subtensor_runtime_common::rate_limiting::GroupId, + >, R::AccountId: From<[u8; 32]>, ::RuntimeCall: From> + From> + + From> + GetDispatchInfo + Dispatchable, ::AddressMapping: AddressMapping, @@ -36,10 +42,15 @@ where R: frame_system::Config + pallet_evm::Config + pallet_subtensor::Config - + pallet_admin_utils::Config, + + pallet_admin_utils::Config + + pallet_rate_limiting::Config< + LimitScope = NetUid, + GroupId = subtensor_runtime_common::rate_limiting::GroupId, + >, R::AccountId: From<[u8; 32]>, ::RuntimeCall: From> + From> + + From> + GetDispatchInfo + Dispatchable, ::AddressMapping: AddressMapping, @@ -141,9 +152,9 @@ where #[precompile::public("getServingRateLimit(uint16)")] #[precompile::view] fn get_serving_rate_limit(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { - Ok(pallet_subtensor::ServingRateLimit::::get(NetUid::from( - netuid, - ))) + Ok(pallet_subtensor::Pallet::::get_serving_rate_limit( + NetUid::from(netuid), + )) } #[precompile::public("setServingRateLimit(uint16,uint64)")] @@ -153,9 +164,10 @@ where netuid: u16, serving_rate_limit: u64, ) -> EvmResult<()> { - let call = pallet_admin_utils::Call::::sudo_set_serving_rate_limit { - netuid: netuid.into(), - serving_rate_limit, + let call = pallet_rate_limiting::Call::::set_rate_limit { + target: RateLimitTarget::Group(subtensor_runtime_common::rate_limiting::GROUP_SERVE), + scope: Some(netuid.into()), + limit: RateLimitKind::Exact(serving_rate_limit.saturated_into()), }; handle.try_dispatch_runtime_call::( diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 10a0670a0f..65a61ebfd8 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1033,7 +1033,6 @@ parameter_types! { pub const SubtensorInitialWeightsVersionKey: u64 = 0; pub const SubtensorInitialMinDifficulty: u64 = 10_000_000; pub const SubtensorInitialMaxDifficulty: u64 = u64::MAX / 4; - pub const SubtensorInitialServingRateLimit: u64 = 50; pub const SubtensorInitialBurn: u64 = 100_000_000; // 0.1 tao pub const SubtensorInitialMinBurn: u64 = 500_000; // 500k RAO pub const SubtensorInitialMaxBurn: u64 = 100_000_000_000; // 100 tao @@ -1106,7 +1105,6 @@ impl pallet_subtensor::Config for Runtime { type InitialWeightsVersionKey = SubtensorInitialWeightsVersionKey; type InitialMaxDifficulty = SubtensorInitialMaxDifficulty; type InitialMinDifficulty = SubtensorInitialMinDifficulty; - type InitialServingRateLimit = SubtensorInitialServingRateLimit; type InitialBurn = SubtensorInitialBurn; type InitialMaxBurn = SubtensorInitialMaxBurn; type InitialMinBurn = SubtensorInitialMinBurn; @@ -1691,7 +1689,6 @@ pub type TransactionExtensions = ( frame_system::CheckEra, check_nonce::CheckNonce, frame_system::CheckWeight, - pallet_rate_limiting::RateLimitTransactionExtension, ChargeTransactionPaymentWrapper, SudoTransactionExtension, pallet_subtensor::transaction_extension::SubtensorTransactionExtension, diff --git a/runtime/src/rate_limiting/legacy.rs b/runtime/src/rate_limiting/legacy.rs index 5311a982ab..cab3d37719 100644 --- a/runtime/src/rate_limiting/legacy.rs +++ b/runtime/src/rate_limiting/legacy.rs @@ -11,9 +11,8 @@ use subtensor_runtime_common::NetUid; use super::AccountId; use crate::{ - SubtensorInitialNetworkRateLimit, SubtensorInitialServingRateLimit, - SubtensorInitialTxChildKeyTakeRateLimit, SubtensorInitialTxDelegateTakeRateLimit, - SubtensorInitialTxRateLimit, + SubtensorInitialNetworkRateLimit, SubtensorInitialTxChildKeyTakeRateLimit, + SubtensorInitialTxDelegateTakeRateLimit, SubtensorInitialTxRateLimit, }; pub use types::{Hyperparameter, RateLimitKey, TransactionType}; @@ -146,7 +145,8 @@ pub mod defaults { use super::*; pub fn serving_rate_limit() -> u64 { - SubtensorInitialServingRateLimit::get() + // SubtensorInitialServingRateLimit::get() + 50 } pub fn weights_set_rate_limit() -> u64 { diff --git a/runtime/tests/rate_limiting_behavior.rs b/runtime/tests/rate_limiting_behavior.rs index 0350fa9f11..132fa1bbe3 100644 --- a/runtime/tests/rate_limiting_behavior.rs +++ b/runtime/tests/rate_limiting_behavior.rs @@ -1,5 +1,6 @@ #![allow(clippy::unwrap_used)] +use codec::Encode; use frame_support::traits::OnRuntimeUpgrade; use frame_system::pallet_prelude::BlockNumberFor; use node_subtensor_runtime::{ @@ -11,10 +12,11 @@ use pallet_rate_limiting::{RateLimitTarget, TransactionIdentifier}; use pallet_subtensor::Call as SubtensorCall; use pallet_subtensor::{ AxonInfo, HasMigrationRun, LastRateLimitedBlock, LastUpdate, NetworksAdded, PrometheusInfo, - RateLimitKey, ServingRateLimit, TransactionKeyLastBlock, WeightsSetRateLimit, - WeightsVersionKeyRateLimit, utils::rate_limiting::TransactionType, + RateLimitKey, TransactionKeyLastBlock, WeightsSetRateLimit, WeightsVersionKeyRateLimit, + utils::rate_limiting::TransactionType, }; use sp_core::{H160, ecdsa}; +use sp_io::{hashing::twox_128, storage}; use sp_runtime::traits::SaturatedConversion; use subtensor_runtime_common::{ NetUid, NetUidStorageIndex, @@ -63,6 +65,13 @@ fn clear_rate_limiting_storage() { pallet_rate_limiting::NextGroupId::::kill(); } +fn set_legacy_serving_rate_limit(netuid: NetUid, span: u64) { + let mut key = twox_128(b"SubtensorModule").to_vec(); + key.extend(twox_128(b"ServingRateLimit")); + key.extend(netuid.encode()); + storage::set(&key, &span.encode()); +} + fn parity_check( now: u64, call: RuntimeCall, @@ -247,7 +256,7 @@ fn serving_parity() { let hot = account(50); let netuid = NetUid::from(3u16); let span = 5u64; - ServingRateLimit::::insert(netuid, span); + set_legacy_serving_rate_limit(netuid, span); pallet_subtensor::Axons::::insert( netuid, hot.clone(), @@ -282,7 +291,7 @@ fn serving_parity() { block: now.saturating_sub(1), ..Default::default() }; - now.saturating_sub(info.block) >= ServingRateLimit::::get(netuid) + now.saturating_sub(info.block) >= span }; parity_check(now, axon_call, origin.clone(), None, None, legacy_axon); @@ -299,7 +308,7 @@ fn serving_parity() { block: now.saturating_sub(1), ..Default::default() }; - now.saturating_sub(info.block) >= ServingRateLimit::::get(netuid) + now.saturating_sub(info.block) >= span }; parity_check(now, prom_call, origin, None, None, legacy_prom); }); From cdd90c17fa60cdcee364b6cc9f8d5bb1f01208de Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 23 Dec 2025 15:26:44 +0100 Subject: [PATCH 46/95] Update contract tests --- contract-tests/README.md | 3 + contract-tests/package.json | 2 +- .../subnet.precompile.hyperparameter.test.ts | 60 ++++++++++++++++++- contract-tests/test/wasm.contract.test.ts | 18 +++++- 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/contract-tests/README.md b/contract-tests/README.md index 78294603d3..90068e5553 100644 --- a/contract-tests/README.md +++ b/contract-tests/README.md @@ -36,6 +36,9 @@ npx papi add devnet -w ws://localhost:9944 If the runtime is upgrade, need to get the metadata again. ```bash +cd contract-tests/bittensor +cargo contract build --release +cd .. sh get-metadata.sh ``` diff --git a/contract-tests/package.json b/contract-tests/package.json index 26136346bb..6af55b1d9e 100644 --- a/contract-tests/package.json +++ b/contract-tests/package.json @@ -1,6 +1,6 @@ { "scripts": { - "test": "mocha --timeout 999999 --retries 3 --file src/setup.ts --require ts-node/register test/*test.ts" + "test": "TS_NODE_PREFER_TS_EXTS=1 TS_NODE_TRANSPILE_ONLY=1 mocha --timeout 999999 --retries 3 --file src/setup.ts --require ts-node/register --extension ts \"test/**/*.ts\"" }, "keywords": [], "author": "", diff --git a/contract-tests/test/subnet.precompile.hyperparameter.test.ts b/contract-tests/test/subnet.precompile.hyperparameter.test.ts index 87968b6e9f..aee28708b0 100644 --- a/contract-tests/test/subnet.precompile.hyperparameter.test.ts +++ b/contract-tests/test/subnet.precompile.hyperparameter.test.ts @@ -91,7 +91,65 @@ describe("Test the Subnet precompile contract", () => { const tx = await contract.setServingRateLimit(netuid, newValue); await tx.wait(); - let onchainValue = await api.query.SubtensorModule.ServingRateLimit.getValue(netuid) + const unwrapEnum = (value: unknown): { tag: string; value: unknown } | null => { + if (!value || typeof value !== "object") { + return null; + } + if ("type" in value && "value" in value) { + return { tag: (value as { type: string }).type, value: (value as { value: unknown }).value }; + } + const keys = Object.keys(value); + if (keys.length === 1) { + const key = keys[0]; + return { tag: key, value: (value as Record)[key] }; + } + return null; + }; + + const toNumber = (value: unknown): number | undefined => { + if (typeof value === "number") { + return value; + } + if (typeof value === "bigint") { + return Number(value); + } + if (typeof value === "string") { + const parsed = Number(value); + return Number.isNaN(parsed) ? undefined : parsed; + } + return undefined; + }; + + const extractRateLimit = (limits: unknown, scope: number): number | undefined => { + const decoded = unwrapEnum(limits); + if (!decoded) { + return undefined; + } + if (decoded.tag === "Global") { + const kind = unwrapEnum(decoded.value); + return kind?.tag === "Exact" ? toNumber(kind.value) : undefined; + } + if (decoded.tag !== "Scoped") { + return undefined; + } + const scopedEntries = decoded.value instanceof Map + ? Array.from(decoded.value.entries()) + : Array.isArray(decoded.value) + ? decoded.value + : []; + const entry = scopedEntries.find( + (item: unknown) => + Array.isArray(item) && item.length === 2 && toNumber(item[0]) === scope, + ) as [unknown, unknown] | undefined; + if (!entry) { + return undefined; + } + const kind = unwrapEnum(entry[1]); + return kind?.tag === "Exact" ? toNumber(kind.value) : undefined; + }; + + const limits = await api.query.RateLimiting.Limits.getValue({ Group: 0 } as any); + const onchainValue = extractRateLimit(limits, netuid); let valueFromContract = Number( diff --git a/contract-tests/test/wasm.contract.test.ts b/contract-tests/test/wasm.contract.test.ts index 26d5c87924..c42dbf5555 100644 --- a/contract-tests/test/wasm.contract.test.ts +++ b/contract-tests/test/wasm.contract.test.ts @@ -6,12 +6,23 @@ import { contracts } from "../.papi/descriptors"; import { getInkClient, InkClient, } from "@polkadot-api/ink-contracts" import { forceSetBalanceToSs58Address, startCall, burnedRegister } from "../src/subtensor"; import fs from "fs" +import path from "path"; import { convertPublicKeyToSs58 } from "../src/address-utils"; import { addNewSubnetwork, sendWasmContractExtrinsic } from "../src/subtensor"; import { tao } from "../src/balance-math"; -const bittensorWasmPath = "./bittensor/target/ink/bittensor.wasm" -const bittensorBytecode = fs.readFileSync(bittensorWasmPath) +const bittensorWasmPath = path.resolve(__dirname, "../bittensor/target/ink/bittensor.wasm") +const loadBittensorBytecode = () => { + if (!fs.existsSync(bittensorWasmPath)) { + throw new Error( + `Missing Ink wasm at ${bittensorWasmPath}. Run ` + + "`cd contract-tests/bittensor && cargo contract build --release` to generate it." + ) + } + + return fs.readFileSync(bittensorWasmPath) +} +let bittensorBytecode: Buffer; describe("Test wasm contract", () => { @@ -60,6 +71,7 @@ describe("Test wasm contract", () => { before(async () => { + bittensorBytecode = loadBittensorBytecode() // init variables got from await and async api = await getDevnetApi() @@ -584,4 +596,4 @@ describe("Test wasm contract", () => { assert.ok(result !== undefined) }) -}); \ No newline at end of file +}); From e7ed870b54ed1c64a3ee2d6153abdcf8929c5d3d Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 24 Dec 2025 16:33:43 +0100 Subject: [PATCH 47/95] Add genesis config for rate-limiting - fix get-metadata script --- contract-tests/get-metadata.sh | 7 ++++++- contract-tests/src/subtensor.ts | 4 ++-- .../test/subnet.precompile.hyperparameter.test.ts | 7 +++++++ node/src/chain_spec/devnet.rs | 11 +++++++++++ node/src/chain_spec/localnet.rs | 11 +++++++++++ pallets/rate-limiting/src/lib.rs | 9 +++++++++ 6 files changed, 46 insertions(+), 3 deletions(-) diff --git a/contract-tests/get-metadata.sh b/contract-tests/get-metadata.sh index 64d76bff29..90cb97e162 100755 --- a/contract-tests/get-metadata.sh +++ b/contract-tests/get-metadata.sh @@ -1,3 +1,8 @@ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +cd "$SCRIPT_DIR" + rm -rf .papi npx papi add devnet -w ws://localhost:9944 -npx papi ink add ./bittensor/target/ink/bittensor.json \ No newline at end of file +npx papi ink add ./bittensor/target/ink/bittensor.json +# Yarn copies file: dependencies into node_modules, so reinstall to pick up new .papi/descriptors. +yarn install diff --git a/contract-tests/src/subtensor.ts b/contract-tests/src/subtensor.ts index 12d652a9a3..ce2f32acc2 100644 --- a/contract-tests/src/subtensor.ts +++ b/contract-tests/src/subtensor.ts @@ -1,6 +1,6 @@ import * as assert from "assert"; import { devnet, MultiAddress } from '@polkadot-api/descriptors'; -import { TypedApi, TxCallData, Binary, Enum } from 'polkadot-api'; +import { TypedApi, TxCallData, Binary } from 'polkadot-api'; import { KeyPair } from "@polkadot-labs/hdkd-helpers" import { getAliceSigner, waitForTransactionCompletion, getSignerFromKeypair, waitForTransactionWithRetry } from './substrate' import { convertH160ToSS58, convertPublicKeyToSs58, ethAddressToH160 } from './address-utils' @@ -398,4 +398,4 @@ export async function sendWasmContractExtrinsic(api: TypedApi, co storage_deposit_limit: BigInt(1000000000) }) await waitForTransactionWithRetry(api, tx, signer) -} \ No newline at end of file +} diff --git a/contract-tests/test/subnet.precompile.hyperparameter.test.ts b/contract-tests/test/subnet.precompile.hyperparameter.test.ts index aee28708b0..12e0bba57a 100644 --- a/contract-tests/test/subnet.precompile.hyperparameter.test.ts +++ b/contract-tests/test/subnet.precompile.hyperparameter.test.ts @@ -16,6 +16,7 @@ describe("Test the Subnet precompile contract", () => { const hotkey1 = getRandomSubstrateKeypair(); const hotkey2 = getRandomSubstrateKeypair(); + const hotkey3 = getRandomSubstrateKeypair(); let api: TypedApi before(async () => { @@ -24,9 +25,15 @@ describe("Test the Subnet precompile contract", () => { await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey1.publicKey)) await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey2.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey3.publicKey)) await forceSetBalanceToEthAddress(api, wallet.address) await disableAdminFreezeWindowAndOwnerHyperparamRateLimit(api) + + // Ensure the EVM wallet owns a subnet so owner-only calls pass when tests run in isolation. + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const tx = await contract.registerNetwork(hotkey3.publicKey); + await tx.wait(); }) it("Can register network without identity info", async () => { diff --git a/node/src/chain_spec/devnet.rs b/node/src/chain_spec/devnet.rs index cb3bc66924..ac3afc7c94 100644 --- a/node/src/chain_spec/devnet.rs +++ b/node/src/chain_spec/devnet.rs @@ -2,6 +2,7 @@ #![allow(clippy::unwrap_used)] use super::*; +use subtensor_runtime_common::rate_limiting::GROUP_SERVE; pub fn devnet_config() -> Result { let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; @@ -89,5 +90,15 @@ fn devnet_genesis( "sudo": { "key": Some(root_key), }, + "rateLimiting": { + "defaultLimit": 0, + "limits": [], + "groups": vec![ + (GROUP_SERVE, b"serving".to_vec(), "ConfigAndUsage"), + ], + "limitSettingRules": vec![ + (serde_json::json!({ "Group": GROUP_SERVE }), "RootOrSubnetOwnerAdminWindow"), + ], + }, }) } diff --git a/node/src/chain_spec/localnet.rs b/node/src/chain_spec/localnet.rs index 02ea8896b5..269f5f661b 100644 --- a/node/src/chain_spec/localnet.rs +++ b/node/src/chain_spec/localnet.rs @@ -2,6 +2,7 @@ #![allow(clippy::unwrap_used)] use super::*; +use subtensor_runtime_common::rate_limiting::GROUP_SERVE; pub fn localnet_config(single_authority: bool) -> Result { let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; @@ -124,5 +125,15 @@ fn localnet_genesis( "evmChainId": { "chainId": 42, }, + "rateLimiting": { + "defaultLimit": 0, + "limits": [], + "groups": vec![ + (GROUP_SERVE, b"serving".to_vec(), "ConfigAndUsage"), + ], + "limitSettingRules": vec![ + (serde_json::json!({ "Group": GROUP_SERVE }), "RootOrSubnetOwnerAdminWindow"), + ], + }, }) } diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 217611c002..34504187a6 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -496,6 +496,10 @@ pub mod pallet { RateLimitKind>, )>, pub groups: Vec<(>::GroupId, Vec, GroupSharing)>, + pub limit_setting_rules: Vec<( + RateLimitTarget<>::GroupId>, + >::LimitSettingRule, + )>, } impl, I: 'static> Default for GenesisConfig { @@ -504,6 +508,7 @@ pub mod pallet { default_limit: Zero::zero(), limits: Vec::new(), groups: Vec::new(), + limit_setting_rules: Vec::new(), } } } @@ -566,6 +571,10 @@ pub mod pallet { } }); } + + for (target, rule) in &self.limit_setting_rules { + LimitSettingRules::::insert(target, rule.clone()); + } } } From caaa6425f05c11babe1f113e171e1b8d945abdb6 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 2 Jan 2026 14:40:01 +0100 Subject: [PATCH 48/95] Add a warning about tx extension in precompiles --- runtime/src/lib.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 20b5940024..2892cfcd23 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1679,6 +1679,11 @@ pub type Header = generic::Header; // Block type as expected by this runtime. pub type Block = generic::Block; // The extensions to the basic transaction logic. +// ================================================================================================= +// IMPORTANT: If you add a new TransactionExtension here, review precompile runtime-call dispatch +// paths (currently `PrecompileHandleExt::try_dispatch_runtime_call`) and decide whether the new +// extension must be triggered manually (not all extensions apply to precompiles). +// ================================================================================================= // Note: The SDK only implements TransactionExtension for tuples up to 12 items, so we nest the last // two extensions to keep order/encoding while staying under the limit. pub type TransactionExtensions = ( @@ -1700,8 +1705,9 @@ pub type TransactionExtensions = ( ); type Migrations = ( - // Leave this migration in the runtime, so every runtime upgrade tiny rounding errors (fractions of fractions - // of a cent) are cleaned up. These tiny rounding errors occur due to floating point coversion. + // Leave this migration in the runtime, so every runtime upgrade tiny rounding errors (fractions + // of fractions of a cent) are cleaned up. These tiny rounding errors occur due to floating + // point coversion. pallet_subtensor::migrations::migrate_init_total_issuance::initialise_total_issuance::Migration< Runtime, >, From cb6ab63757b3a10f4cc6583a79b72c0a2c73f212 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 2 Jan 2026 15:32:59 +0100 Subject: [PATCH 49/95] Trigger pallet-rate-limiting tx extension in precompiles --- precompiles/src/balance_transfer.rs | 2 ++ precompiles/src/crowdloan.rs | 2 ++ precompiles/src/extensions.rs | 49 ++++++++++++++++++++--------- precompiles/src/leasing.rs | 2 ++ precompiles/src/lib.rs | 3 ++ precompiles/src/neuron.rs | 2 ++ precompiles/src/proxy.rs | 2 ++ precompiles/src/staking.rs | 4 +++ precompiles/src/subnet.rs | 2 ++ 9 files changed, 54 insertions(+), 14 deletions(-) diff --git a/precompiles/src/balance_transfer.rs b/precompiles/src/balance_transfer.rs index c1cdab6ca5..e8eb6d2e6e 100644 --- a/precompiles/src/balance_transfer.rs +++ b/precompiles/src/balance_transfer.rs @@ -17,6 +17,7 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + Send + Sync @@ -42,6 +43,7 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + Send + Sync diff --git a/precompiles/src/crowdloan.rs b/precompiles/src/crowdloan.rs index f0971015d9..5fda02e0c1 100644 --- a/precompiles/src/crowdloan.rs +++ b/precompiles/src/crowdloan.rs @@ -24,6 +24,7 @@ where + pallet_crowdloan::Config + pallet_evm::Config + pallet_proxy::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + Send + Sync @@ -48,6 +49,7 @@ where + pallet_crowdloan::Config + pallet_evm::Config + pallet_proxy::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + Send + Sync diff --git a/precompiles/src/extensions.rs b/precompiles/src/extensions.rs index df5ebf3fa2..fbd5815d78 100644 --- a/precompiles/src/extensions.rs +++ b/precompiles/src/extensions.rs @@ -10,6 +10,7 @@ use pallet_evm::{ AddressMapping, BalanceConverter, EvmBalance, ExitError, GasWeightMapping, Precompile, PrecompileFailure, PrecompileHandle, PrecompileResult, }; +use pallet_rate_limiting::RateLimitTransactionExtension; use pallet_subtensor::transaction_extension::SubtensorTransactionExtension; use precompile_utils::EvmResult; use scale_info::TypeInfo; @@ -24,6 +25,12 @@ use sp_runtime::{ }; use sp_std::vec::Vec; +type RuntimeCallOf = ::RuntimeCall; +type Extensions = ( + SubtensorTransactionExtension, + RateLimitTransactionExtension, +); + pub(crate) trait PrecompileHandleExt: PrecompileHandle { fn caller_account_id(&self) -> R::AccountId where @@ -57,24 +64,29 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config> + pallet_subtensor::Config + Send + Sync + TypeInfo, - ::RuntimeCall: From, - ::RuntimeCall: GetDispatchInfo + RuntimeCallOf: From, + RuntimeCallOf: GetDispatchInfo + Dispatchable + IsSubType> + IsSubType>, ::RuntimeOrigin: From> + AsSystemOriginSigner + Clone, { - let call = ::RuntimeCall::from(call); + let call = RuntimeCallOf::::from(call); let mut info = GetDispatchInfo::get_dispatch_info(&call); - let subtensor_extension = SubtensorTransactionExtension::::new(); + + let extensions = ( + SubtensorTransactionExtension::::new(), + RateLimitTransactionExtension::::new(), + ); info.extension_weight = info .extension_weight - .saturating_add(subtensor_extension.weight(&call)); + .saturating_add(extensions.weight(&call)); let target_gas = self.gas_limit(); if let Some(gas) = target_gas { @@ -94,18 +106,19 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { )?; let origin = ::RuntimeOrigin::from(origin); - let (_, val, origin) = subtensor_extension + let implicit = extensions.implicit().map_err(extension_error)?; + let (_, val, origin) = extensions .validate( origin, &call, &info, 0, - (), + implicit, &TxBaseImplication(()), TransactionSource::External, ) .map_err(extension_error)?; - subtensor_extension + let pre = extensions .prepare(val, &origin, &call, &info, 0) .map_err(extension_error)?; @@ -113,9 +126,13 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { Ok(mut post_info) => { post_info.set_extension_weight(&info); let result: DispatchResult = Ok(()); - as TransactionExtension< - ::RuntimeCall, - >>::post_dispatch((), &info, &mut post_info, 0, &result) + as TransactionExtension>>::post_dispatch( + pre, + &info, + &mut post_info, + 0, + &result, + ) .map_err(extension_error)?; log::debug!("Dispatch succeeded. Post info: {post_info:?}"); self.charge_and_refund_after_dispatch::(&info, &post_info)?; @@ -127,9 +144,13 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { let mut post_info = e.post_info; post_info.set_extension_weight(&info); let result: DispatchResult = Err(e.error); - as TransactionExtension< - ::RuntimeCall, - >>::post_dispatch((), &info, &mut post_info, 0, &result) + as TransactionExtension>>::post_dispatch( + pre, + &info, + &mut post_info, + 0, + &result, + ) .map_err(extension_error)?; log::error!("Dispatch failed. Error: {e:?}"); log::warn!("Returning error PrecompileFailure::Error"); diff --git a/precompiles/src/leasing.rs b/precompiles/src/leasing.rs index 01a8db4354..d4951a658f 100644 --- a/precompiles/src/leasing.rs +++ b/precompiles/src/leasing.rs @@ -24,6 +24,7 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_crowdloan::Config + Send @@ -48,6 +49,7 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_crowdloan::Config + Send diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index c7a29f1a46..c7ba7a32f1 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -71,6 +71,7 @@ where + pallet_rate_limiting::Config< LimitScope = subtensor_runtime_common::NetUid, GroupId = subtensor_runtime_common::rate_limiting::GroupId, + RuntimeCall = ::RuntimeCall, > + Send + Sync + scale_info::TypeInfo, @@ -108,6 +109,7 @@ where + pallet_rate_limiting::Config< LimitScope = subtensor_runtime_common::NetUid, GroupId = subtensor_runtime_common::rate_limiting::GroupId, + RuntimeCall = ::RuntimeCall, > + Send + Sync + scale_info::TypeInfo, @@ -174,6 +176,7 @@ where + pallet_rate_limiting::Config< LimitScope = subtensor_runtime_common::NetUid, GroupId = subtensor_runtime_common::rate_limiting::GroupId, + RuntimeCall = ::RuntimeCall, > + Send + Sync + scale_info::TypeInfo, diff --git a/precompiles/src/neuron.rs b/precompiles/src/neuron.rs index 0b998b3c07..e7411cd2b2 100644 --- a/precompiles/src/neuron.rs +++ b/precompiles/src/neuron.rs @@ -18,6 +18,7 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + Send + Sync @@ -40,6 +41,7 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + Send + Sync diff --git a/precompiles/src/proxy.rs b/precompiles/src/proxy.rs index 5139477f00..a366590f53 100644 --- a/precompiles/src/proxy.rs +++ b/precompiles/src/proxy.rs @@ -28,6 +28,7 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_proxy::Config + Send @@ -54,6 +55,7 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_proxy::Config + Send diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index 862c59219c..4484c12fcc 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -55,6 +55,7 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_proxy::Config + Send @@ -80,6 +81,7 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_proxy::Config + Send @@ -431,6 +433,7 @@ impl PrecompileExt for StakingPrecompile where R: frame_system::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_proxy::Config + pallet_balances::Config @@ -458,6 +461,7 @@ impl StakingPrecompile where R: frame_system::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_proxy::Config + pallet_balances::Config diff --git a/precompiles/src/subnet.rs b/precompiles/src/subnet.rs index 6da4012bd1..55524ee1ba 100644 --- a/precompiles/src/subnet.rs +++ b/precompiles/src/subnet.rs @@ -26,6 +26,7 @@ where + pallet_rate_limiting::Config< LimitScope = NetUid, GroupId = subtensor_runtime_common::rate_limiting::GroupId, + RuntimeCall = ::RuntimeCall, > + Send + Sync + scale_info::TypeInfo, @@ -54,6 +55,7 @@ where + pallet_rate_limiting::Config< LimitScope = NetUid, GroupId = subtensor_runtime_common::rate_limiting::GroupId, + RuntimeCall = ::RuntimeCall, > + Send + Sync + scale_info::TypeInfo, From fcd1fc2ad680afc03dd2c63d4d2d3ba6c4da7327 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 5 Jan 2026 14:08:12 +0100 Subject: [PATCH 50/95] Move network-lock and rate-limiting migrations into migrations module of runtime --- pallets/subtensor/src/macros/hooks.rs | 6 - .../migrate_network_lock_cost_2500.rs | 48 -- ...migrate_network_lock_reduction_interval.rs | 55 -- .../migrate_rate_limiting_last_blocks.rs | 161 ------ pallets/subtensor/src/migrations/mod.rs | 3 - pallets/subtensor/src/tests/migration.rs | 371 ------------- runtime/src/lib.rs | 3 +- runtime/src/migrations/mod.rs | 5 +- .../rate_limiting.rs} | 500 +++++++++++++++++- runtime/src/migrations/subtensor_module.rs | 319 +++++++++++ runtime/src/rate_limiting/mod.rs | 3 +- runtime/tests/rate_limiting_behavior.rs | 438 --------------- runtime/tests/rate_limiting_migration.rs | 77 --- 13 files changed, 817 insertions(+), 1172 deletions(-) delete mode 100644 pallets/subtensor/src/migrations/migrate_network_lock_cost_2500.rs delete mode 100644 pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs delete mode 100644 pallets/subtensor/src/migrations/migrate_rate_limiting_last_blocks.rs rename runtime/src/{rate_limiting/migration.rs => migrations/rate_limiting.rs} (68%) create mode 100644 runtime/src/migrations/subtensor_module.rs delete mode 100644 runtime/tests/rate_limiting_behavior.rs delete mode 100644 runtime/tests/rate_limiting_migration.rs diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 31be3e5e4f..21821f646d 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -127,8 +127,6 @@ mod hooks { .saturating_add(migrations::migrate_crv3_v2_to_timelocked::migrate_crv3_v2_to_timelocked::()) // Migrate to fix root counters .saturating_add(migrations::migrate_fix_root_tao_and_alpha_in::migrate_fix_root_tao_and_alpha_in::()) - // Migrate last block rate limiting storage items - .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::()) // Migrate remove network modality @@ -137,12 +135,8 @@ mod hooks { .saturating_add(migrations::migrate_network_immunity_period::migrate_network_immunity_period::()) // Migrate Subnet Limit .saturating_add(migrations::migrate_subnet_limit_to_default::migrate_subnet_limit_to_default::()) - // Migrate Lock Reduction Interval - .saturating_add(migrations::migrate_network_lock_reduction_interval::migrate_network_lock_reduction_interval::()) // Migrate subnet locked balances .saturating_add(migrations::migrate_subnet_locked::migrate_restore_subnet_locked::()) - // Migrate subnet burn cost to 2500 - .saturating_add(migrations::migrate_network_lock_cost_2500::migrate_network_lock_cost_2500::()) // Cleanup child/parent keys .saturating_add(migrations::migrate_fix_childkeys::migrate_fix_childkeys::()) // Migrate AutoStakeDestinationColdkeys diff --git a/pallets/subtensor/src/migrations/migrate_network_lock_cost_2500.rs b/pallets/subtensor/src/migrations/migrate_network_lock_cost_2500.rs deleted file mode 100644 index e12356f6ba..0000000000 --- a/pallets/subtensor/src/migrations/migrate_network_lock_cost_2500.rs +++ /dev/null @@ -1,48 +0,0 @@ -use super::*; -use frame_support::{traits::Get, weights::Weight}; -use log; -use scale_info::prelude::string::String; - -pub fn migrate_network_lock_cost_2500() -> Weight { - const RAO_PER_TAO: u64 = 1_000_000_000; - const TARGET_COST_TAO: u64 = 2_500; - const NEW_LAST_LOCK_RAO: u64 = (TARGET_COST_TAO / 2) * RAO_PER_TAO; // 1,250 TAO - - let migration_name = b"migrate_network_lock_cost_2500".to_vec(); - let mut weight = T::DbWeight::get().reads(1); - - // Skip if already executed - if HasMigrationRun::::get(&migration_name) { - log::info!( - target: "runtime", - "Migration '{}' already run - skipping.", - String::from_utf8_lossy(&migration_name) - ); - return weight; - } - - // Use the current block; ensure it's non-zero so mult == 2 in get_network_lock_cost() - let current_block = Pallet::::get_current_block_as_u64(); - let block_to_set = if current_block == 0 { 1 } else { current_block }; - - // Set last_lock so that price = 2 * last_lock = 2,500 TAO at this block - Pallet::::set_network_last_lock(TaoCurrency::from(NEW_LAST_LOCK_RAO)); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); - - // Start decay from "now" (no backdated decay) - Pallet::::set_network_last_lock_block(block_to_set); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); - - // Mark migration done - HasMigrationRun::::insert(&migration_name, true); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); - - log::info!( - target: "runtime", - "Migration '{}' completed. lock_cost set to 2,500 TAO at block {}.", - String::from_utf8_lossy(&migration_name), - block_to_set - ); - - weight -} diff --git a/pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs b/pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs deleted file mode 100644 index 99bb5b6e97..0000000000 --- a/pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs +++ /dev/null @@ -1,55 +0,0 @@ -use super::*; -use frame_support::{traits::Get, weights::Weight}; -use log; -use scale_info::prelude::string::String; - -pub fn migrate_network_lock_reduction_interval() -> Weight { - const FOUR_DAYS: u64 = 28_800; - const EIGHT_DAYS: u64 = 57_600; - const ONE_WEEK_BLOCKS: u64 = 50_400; - - let migration_name = b"migrate_network_lock_reduction_interval".to_vec(); - let mut weight = T::DbWeight::get().reads(1); - - // Skip if already executed - if HasMigrationRun::::get(&migration_name) { - log::info!( - target: "runtime", - "Migration '{}' already run - skipping.", - String::from_utf8_lossy(&migration_name) - ); - return weight; - } - - let current_block = Pallet::::get_current_block_as_u64(); - - // ── 1) Set new values ───────────────────────────────────────────────── - NetworkLockReductionInterval::::put(EIGHT_DAYS); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); - - NetworkRateLimit::::put(FOUR_DAYS); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); - - Pallet::::set_network_last_lock(TaoCurrency::from(1_000_000_000_000)); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); - - // Hold price at 2000 TAO until day 7, then begin linear decay - Pallet::::set_network_last_lock_block(current_block.saturating_add(ONE_WEEK_BLOCKS)); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); - - // Allow registrations starting at day 7 - NetworkRegistrationStartBlock::::put(current_block.saturating_add(ONE_WEEK_BLOCKS)); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); - - // ── 2) Mark migration done ─────────────────────────────────────────── - HasMigrationRun::::insert(&migration_name, true); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); - - log::info!( - target: "runtime", - "Migration '{}' completed.", - String::from_utf8_lossy(&migration_name), - ); - - weight -} diff --git a/pallets/subtensor/src/migrations/migrate_rate_limiting_last_blocks.rs b/pallets/subtensor/src/migrations/migrate_rate_limiting_last_blocks.rs deleted file mode 100644 index 99ce1e3077..0000000000 --- a/pallets/subtensor/src/migrations/migrate_rate_limiting_last_blocks.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::Vec; -use crate::{Config, HasMigrationRun, Pallet}; -use alloc::string::String; -use codec::Decode; -use frame_support::traits::Get; -use frame_support::weights::Weight; -use sp_io::hashing::twox_128; -use sp_io::storage::{clear, get}; - -pub fn migrate_obsolete_rate_limiting_last_blocks_storage() -> Weight { - migrate_network_last_registered::() - .saturating_add(migrate_last_tx_block::()) - .saturating_add(migrate_last_tx_block_childkey_take::()) - .saturating_add(migrate_last_tx_block_delegate_take::()) -} - -pub fn migrate_network_last_registered() -> Weight { - let migration_name = b"migrate_network_last_registered".to_vec(); - let pallet_name = "SubtensorModule"; - let storage_name = "NetworkLastRegistered"; - - migrate_value::(migration_name, pallet_name, storage_name, |limit| { - Pallet::::set_network_last_lock_block(limit); - }) -} - -#[allow(deprecated)] -pub fn migrate_last_tx_block() -> Weight { - let migration_name = b"migrate_last_tx_block".to_vec(); - - migrate_last_block_map::( - migration_name, - || crate::LastTxBlock::::drain().collect::>(), - |account, block| { - Pallet::::set_last_tx_block(&account, block); - }, - ) -} - -#[allow(deprecated)] -pub fn migrate_last_tx_block_childkey_take() -> Weight { - let migration_name = b"migrate_last_tx_block_childkey_take".to_vec(); - - migrate_last_block_map::( - migration_name, - || crate::LastTxBlockChildKeyTake::::drain().collect::>(), - |account, block| { - Pallet::::set_last_tx_block_childkey(&account, block); - }, - ) -} - -#[allow(deprecated)] -pub fn migrate_last_tx_block_delegate_take() -> Weight { - let migration_name = b"migrate_last_tx_block_delegate_take".to_vec(); - - migrate_last_block_map::( - migration_name, - || crate::LastTxBlockDelegateTake::::drain().collect::>(), - |account, block| { - Pallet::::set_last_tx_block_delegate_take(&account, block); - }, - ) -} - -fn migrate_value( - migration_name: Vec, - pallet_name: &str, - storage_name: &str, - set_value: SetValueFunction, -) -> Weight -where - T: Config, - SetValueFunction: Fn(u64 /*limit in blocks*/), -{ - // 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 '{migration_name:?}' has already run. Skipping.",); - return weight; - } - log::info!( - "Running migration '{}'", - String::from_utf8_lossy(&migration_name) - ); - - let pallet_name_hash = twox_128(pallet_name.as_bytes()); - let storage_name_hash = twox_128(storage_name.as_bytes()); - let full_key = [pallet_name_hash, storage_name_hash].concat(); - - if let Some(value_bytes) = get(&full_key) { - if let Ok(rate_limit) = Decode::decode(&mut &value_bytes[..]) { - set_value(rate_limit); - } - - clear(&full_key); - } - - weight = weight.saturating_add(T::DbWeight::get().writes(2)); - weight = weight.saturating_add(T::DbWeight::get().reads(1)); - - // Mark the migration as completed - HasMigrationRun::::insert(&migration_name, true); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); - - log::info!( - "Migration '{:?}' completed.", - String::from_utf8_lossy(&migration_name) - ); - - // Return the migration weight. - weight -} - -fn migrate_last_block_map( - migration_name: Vec, - get_values: GetValuesFunction, - set_value: SetValueFunction, -) -> Weight -where - T: Config, - GetValuesFunction: Fn() -> Vec<(T::AccountId, u64)>, // (account, limit in blocks) - SetValueFunction: Fn(T::AccountId, u64), -{ - // 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 '{migration_name:?}' has already run. Skipping.",); - return weight; - } - log::info!( - "Running migration '{}'", - String::from_utf8_lossy(&migration_name) - ); - - let key_values = get_values(); - weight = weight.saturating_add(T::DbWeight::get().reads(key_values.len() as u64)); - - for (account, block) in key_values.into_iter() { - set_value(account, block); - - weight = weight.saturating_add(T::DbWeight::get().writes(2)); - weight = weight.saturating_add(T::DbWeight::get().reads(1)); - } - - // Mark the migration as completed - HasMigrationRun::::insert(&migration_name, true); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); - - log::info!( - "Migration '{:?}' completed.", - 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 4c9d5f01d1..da0e06ab19 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -23,14 +23,11 @@ pub mod migrate_init_tao_flow; pub mod migrate_init_total_issuance; pub mod migrate_kappa_map_to_default; pub mod migrate_network_immunity_period; -pub mod migrate_network_lock_cost_2500; -pub mod migrate_network_lock_reduction_interval; pub mod migrate_orphaned_storage_items; pub mod migrate_pending_emissions; 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_commitments_rate_limit; pub mod migrate_remove_network_modality; pub mod migrate_remove_old_identity_maps; diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 1d78b4dffd..5736a54efb 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -827,206 +827,6 @@ fn test_migrate_remove_commitments_rate_limit() { }); } -#[test] -fn test_migrate_network_last_registered() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // Step 1: Simulate Old Storage Entry - // ------------------------------ - const MIGRATION_NAME: &str = "migrate_network_last_registered"; - - let pallet_name = "SubtensorModule"; - let storage_name = "NetworkLastRegistered"; - let pallet_name_hash = twox_128(pallet_name.as_bytes()); - let storage_name_hash = twox_128(storage_name.as_bytes()); - let prefix = [pallet_name_hash, storage_name_hash].concat(); - - let mut full_key = prefix.clone(); - - let original_value: u64 = 123; - put_raw(&full_key, &original_value.encode()); - - let stored_before = get_raw(&full_key).expect("Expected RateLimit to exist"); - assert_eq!( - u64::decode(&mut &stored_before[..]).expect("Failed to decode RateLimit"), - original_value - ); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // Step 2: Run the Migration - // ------------------------------ - let weight = crate::migrations::migrate_rate_limiting_last_blocks:: - migrate_obsolete_rate_limiting_last_blocks_storage::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as completed" - ); - - // ------------------------------ - // Step 3: Verify Migration Effects - // ------------------------------ - - assert_eq!( - SubtensorModule::get_network_last_lock_block(), - original_value - ); - assert_eq!( - get_raw(&full_key), - None, - "RateLimit storage should have been cleared" - ); - - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -#[allow(deprecated)] -#[test] -fn test_migrate_last_block_tx() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // Step 1: Simulate Old Storage Entry - // ------------------------------ - const MIGRATION_NAME: &str = "migrate_last_tx_block"; - - let test_account: U256 = U256::from(1); - let original_value: u64 = 123; - - LastTxBlock::::insert(test_account, original_value); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // Step 2: Run the Migration - // ------------------------------ - let weight = crate::migrations::migrate_rate_limiting_last_blocks:: - migrate_obsolete_rate_limiting_last_blocks_storage::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as completed" - ); - - // ------------------------------ - // Step 3: Verify Migration Effects - // ------------------------------ - - assert_eq!( - SubtensorModule::get_last_tx_block(&test_account), - original_value - ); - assert!( - !LastTxBlock::::contains_key(test_account), - "RateLimit storage should have been cleared" - ); - - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -#[allow(deprecated)] -#[test] -fn test_migrate_last_tx_block_childkey_take() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // Step 1: Simulate Old Storage Entry - // ------------------------------ - const MIGRATION_NAME: &str = "migrate_last_tx_block_childkey_take"; - - let test_account: U256 = U256::from(1); - let original_value: u64 = 123; - - LastTxBlockChildKeyTake::::insert(test_account, original_value); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // Step 2: Run the Migration - // ------------------------------ - let weight = crate::migrations::migrate_rate_limiting_last_blocks:: - migrate_obsolete_rate_limiting_last_blocks_storage::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as completed" - ); - - // ------------------------------ - // Step 3: Verify Migration Effects - // ------------------------------ - - assert_eq!( - SubtensorModule::get_last_tx_block_childkey_take(&test_account), - original_value - ); - assert!( - !LastTxBlockChildKeyTake::::contains_key(test_account), - "RateLimit storage should have been cleared" - ); - - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -#[allow(deprecated)] -#[test] -fn test_migrate_last_tx_block_delegate_take() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // Step 1: Simulate Old Storage Entry - // ------------------------------ - const MIGRATION_NAME: &str = "migrate_last_tx_block_delegate_take"; - - let test_account: U256 = U256::from(1); - let original_value: u64 = 123; - - LastTxBlockDelegateTake::::insert(test_account, original_value); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // Step 2: Run the Migration - // ------------------------------ - let weight = crate::migrations::migrate_rate_limiting_last_blocks:: - migrate_last_tx_block_delegate_take::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as completed" - ); - - // ------------------------------ - // Step 3: Verify Migration Effects - // ------------------------------ - - assert_eq!( - SubtensorModule::get_last_tx_block_delegate_take(&test_account), - original_value - ); - assert!( - !LastTxBlockDelegateTake::::contains_key(test_account), - "RateLimit storage should have been cleared" - ); - - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - #[test] fn test_migrate_rate_limit_keys() { new_test_ext(1).execute_with(|| { @@ -1984,67 +1784,6 @@ fn test_migrate_subnet_limit_to_default() { }); } -#[test] -fn test_migrate_network_lock_reduction_interval_and_decay() { - new_test_ext(0).execute_with(|| { - const FOUR_DAYS: u64 = 28_800; - const EIGHT_DAYS: u64 = 57_600; - const ONE_WEEK_BLOCKS: u64 = 50_400; - - // ── pre ────────────────────────────────────────────────────────────── - assert!( - !HasMigrationRun::::get(b"migrate_network_lock_reduction_interval".to_vec()), - "HasMigrationRun should be false before migration" - ); - - // ensure current_block > 0 - step_block(1); - let current_block_before = Pallet::::get_current_block_as_u64(); - - // ── run migration ──────────────────────────────────────────────────── - let weight = crate::migrations::migrate_network_lock_reduction_interval::migrate_network_lock_reduction_interval::(); - assert!(!weight.is_zero(), "migration weight should be > 0"); - - // ── params & flags ─────────────────────────────────────────────────── - assert_eq!(NetworkLockReductionInterval::::get(), EIGHT_DAYS); - assert_eq!(NetworkRateLimit::::get(), FOUR_DAYS); - assert_eq!( - Pallet::::get_network_last_lock(), - 1_000_000_000_000u64.into(), // 1000 TAO in rao - "last_lock should be 1_000_000_000_000 rao" - ); - - // last_lock_block should be set one week in the future - let last_lock_block = Pallet::::get_network_last_lock_block(); - let expected_block = current_block_before + ONE_WEEK_BLOCKS; - assert_eq!( - last_lock_block, - expected_block, - "last_lock_block should be current + ONE_WEEK_BLOCKS" - ); - - // registration start block should match the same future block - assert_eq!( - NetworkRegistrationStartBlock::::get(), - expected_block, - "NetworkRegistrationStartBlock should equal last_lock_block" - ); - - // lock cost should be 2000 TAO immediately after migration - let lock_cost_now = Pallet::::get_network_lock_cost(); - assert_eq!( - lock_cost_now, - 2_000_000_000_000u64.into(), - "lock cost should be 2000 TAO right after migration" - ); - - assert!( - HasMigrationRun::::get(b"migrate_network_lock_reduction_interval".to_vec()), - "HasMigrationRun should be true after migration" - ); - }); -} - #[test] fn test_migrate_restore_subnet_locked_65_128() { use sp_runtime::traits::SaturatedConversion; @@ -2171,116 +1910,6 @@ fn test_migrate_restore_subnet_locked_65_128() { }); } -#[test] -fn test_migrate_network_lock_cost_2500_sets_price_and_decay() { - new_test_ext(0).execute_with(|| { - // ── constants ─────────────────────────────────────────────────────── - const RAO_PER_TAO: u64 = 1_000_000_000; - const TARGET_COST_TAO: u64 = 2_500; - const TARGET_COST_RAO: u64 = TARGET_COST_TAO * RAO_PER_TAO; - const NEW_LAST_LOCK_RAO: u64 = (TARGET_COST_TAO / 2) * RAO_PER_TAO; - - let migration_key = b"migrate_network_lock_cost_2500".to_vec(); - - // ── pre ────────────────────────────────────────────────────────────── - assert!( - !HasMigrationRun::::get(migration_key.clone()), - "HasMigrationRun should be false before migration" - ); - - // Ensure current_block > 0 so mult == 2 in get_network_lock_cost() - step_block(1); - let current_block_before = Pallet::::get_current_block_as_u64(); - - // Snapshot interval to ensure migration doesn't change it - let interval_before = NetworkLockReductionInterval::::get(); - - // ── run migration ──────────────────────────────────────────────────── - let weight = crate::migrations::migrate_network_lock_cost_2500::migrate_network_lock_cost_2500::(); - assert!(!weight.is_zero(), "migration weight should be > 0"); - - // ── asserts: params & flags ───────────────────────────────────────── - assert_eq!( - Pallet::::get_network_last_lock(), - NEW_LAST_LOCK_RAO.into(), - "last_lock should be set to 1,250 TAO (in rao)" - ); - assert_eq!( - Pallet::::get_network_last_lock_block(), - current_block_before, - "last_lock_block should be set to the current block" - ); - - // Lock cost should be exactly 2,500 TAO immediately after migration - let lock_cost_now = Pallet::::get_network_lock_cost(); - assert_eq!( - lock_cost_now, - TARGET_COST_RAO.into(), - "lock cost should be 2,500 TAO right after migration" - ); - - // Interval should be unchanged by this migration - assert_eq!( - NetworkLockReductionInterval::::get(), - interval_before, - "lock reduction interval should not be modified by this migration" - ); - - assert!( - HasMigrationRun::::get(migration_key.clone()), - "HasMigrationRun should be true after migration" - ); - - // ── decay check (1 block later) ───────────────────────────────────── - // Expected: cost = max(min_lock, 2*L - floor(L / eff_interval) * delta_blocks) - let eff_interval = Pallet::::get_lock_reduction_interval(); - let per_block_decrement: u64 = if eff_interval == 0 { - 0 - } else { - NEW_LAST_LOCK_RAO / eff_interval - }; - - let min_lock_rao: u64 = Pallet::::get_network_min_lock().to_u64(); - - step_block(1); - let expected_after_1: u64 = - core::cmp::max(min_lock_rao, TARGET_COST_RAO - per_block_decrement); - let lock_cost_after_1 = Pallet::::get_network_lock_cost(); - assert_eq!( - lock_cost_after_1, - expected_after_1.into(), - "lock cost should decay by one per-block step after 1 block" - ); - - // ── idempotency: running the migration again should do nothing ────── - let last_lock_before_rerun = Pallet::::get_network_last_lock(); - let last_lock_block_before_rerun = Pallet::::get_network_last_lock_block(); - let cost_before_rerun = Pallet::::get_network_lock_cost(); - - let _weight2 = crate::migrations::migrate_network_lock_cost_2500::migrate_network_lock_cost_2500::(); - - assert!( - HasMigrationRun::::get(migration_key.clone()), - "HasMigrationRun remains true on second run" - ); - assert_eq!( - Pallet::::get_network_last_lock(), - last_lock_before_rerun, - "second run should not modify last_lock" - ); - assert_eq!( - Pallet::::get_network_last_lock_block(), - last_lock_block_before_rerun, - "second run should not modify last_lock_block" - ); - assert_eq!( - Pallet::::get_network_lock_cost(), - cost_before_rerun, - "second run should not change current lock cost" - ); - }); -} - #[test] fn test_migrate_kappa_map_to_default() { new_test_ext(1).execute_with(|| { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 2892cfcd23..8b2004bbfd 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1711,7 +1711,8 @@ type Migrations = ( pallet_subtensor::migrations::migrate_init_total_issuance::initialise_total_issuance::Migration< Runtime, >, - rate_limiting::migration::Migration, + migrations::rate_limiting::Migration, + migrations::subtensor_module::Migration, ); // Unchecked extrinsic type as expected by this runtime. diff --git a/runtime/src/migrations/mod.rs b/runtime/src/migrations/mod.rs index ecc48efcdb..c906c32874 100644 --- a/runtime/src/migrations/mod.rs +++ b/runtime/src/migrations/mod.rs @@ -1 +1,4 @@ -//! Export migrations from here. +//! Runtime-level migrations. + +pub mod rate_limiting; +pub mod subtensor_module; diff --git a/runtime/src/rate_limiting/migration.rs b/runtime/src/migrations/rate_limiting.rs similarity index 68% rename from runtime/src/rate_limiting/migration.rs rename to runtime/src/migrations/rate_limiting.rs index 360933bb48..8c42fbe622 100644 --- a/runtime/src/rate_limiting/migration.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -20,15 +20,19 @@ use subtensor_runtime_common::{ NetUid, rate_limiting::{ GROUP_DELEGATE_TAKE, GROUP_OWNER_HPARAMS, GROUP_REGISTER_NETWORK, GROUP_SERVE, - GROUP_STAKING_OPS, GROUP_SWAP_KEYS, GROUP_WEIGHTS_SUBNET, GroupId, + GROUP_STAKING_OPS, GROUP_SWAP_KEYS, GROUP_WEIGHTS_SUBNET, GroupId, RateLimitUsageKey, + ServingEndpoint, }, }; -use super::{ - AccountId, LimitSettingRule, RateLimitUsageKey, Runtime, - legacy::{ - Hyperparameter, RateLimitKey, TransactionType, defaults as legacy_defaults, - storage as legacy_storage, +use crate::{ + AccountId, Runtime, + rate_limiting::{ + LimitSettingRule, + legacy::{ + Hyperparameter, RateLimitKey, TransactionType, defaults as legacy_defaults, + storage as legacy_storage, + }, }, }; @@ -342,7 +346,7 @@ fn build_serving(groups: &mut Vec, commits: &mut Vec) -> u6 usage: Some(RateLimitUsageKey::AccountSubnetServing { account: hotkey.clone(), netuid, - endpoint: crate::rate_limiting::ServingEndpoint::Axon, + endpoint: ServingEndpoint::Axon, }), }), }); @@ -360,7 +364,7 @@ fn build_serving(groups: &mut Vec, commits: &mut Vec) -> u6 usage: Some(RateLimitUsageKey::AccountSubnetServing { account: hotkey, netuid, - endpoint: crate::rate_limiting::ServingEndpoint::Prometheus, + endpoint: ServingEndpoint::Prometheus, }), }), }); @@ -1014,18 +1018,36 @@ fn block_number(value: u64) -> Option> { } #[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { use codec::Encode; + use frame_support::traits::OnRuntimeUpgrade; + use frame_system::pallet_prelude::BlockNumberFor; + use pallet_rate_limiting::{ + RateLimit, RateLimitKind, RateLimitScopeResolver, RateLimitTarget, RateLimitUsageResolver, + TransactionIdentifier, + }; + use pallet_subtensor::{ + AxonInfo, Call as SubtensorCall, HasMigrationRun, LastRateLimitedBlock, LastUpdate, + NetworksAdded, PrometheusInfo, RateLimitKey, TransactionKeyLastBlock, WeightsSetRateLimit, + WeightsVersionKeyRateLimit, utils::rate_limiting::TransactionType, + }; + use sp_core::{H160, ecdsa}; use sp_io::TestExternalities; use sp_io::{hashing::twox_128, storage}; use sp_runtime::traits::{SaturatedConversion, Zero}; use super::*; - use crate::BuildStorage; + use crate::{ + BuildStorage, RuntimeCall, RuntimeOrigin, RuntimeScopeResolver, RuntimeUsageResolver, + SubtensorModule, System, + }; + use subtensor_runtime_common::NetUidStorageIndex; const ACCOUNT: [u8; 32] = [7u8; 32]; const DELEGATE_TAKE_GROUP_ID: GroupId = GROUP_DELEGATE_TAKE; const PALLET_PREFIX: &[u8] = b"SubtensorModule"; + type UsageKey = RateLimitUsageKey; fn new_test_ext() -> TestExternalities { sp_tracing::try_init_simple(); @@ -1037,6 +1059,116 @@ mod tests { ext } + fn new_ext() -> TestExternalities { + new_test_ext() + } + + fn account(n: u8) -> AccountId { + AccountId::from([n; 32]) + } + + fn resolve_target(identifier: TransactionIdentifier) -> RateLimitTarget { + if let Some(group) = pallet_rate_limiting::CallGroups::::get(identifier) { + RateLimitTarget::Group(group) + } else { + RateLimitTarget::Transaction(identifier) + } + } + + fn exact_span(span: u64) -> BlockNumberFor { + span.saturated_into::>() + } + + fn clear_rate_limiting_storage() { + let limit = u32::MAX; + let _ = pallet_rate_limiting::Limits::::clear(limit, None); + let _ = pallet_rate_limiting::LastSeen::::clear(limit, None); + let _ = pallet_rate_limiting::Groups::::clear(limit, None); + let _ = pallet_rate_limiting::GroupMembers::::clear(limit, None); + let _ = pallet_rate_limiting::GroupNameIndex::::clear(limit, None); + let _ = pallet_rate_limiting::CallGroups::::clear(limit, None); + pallet_rate_limiting::NextGroupId::::kill(); + } + + fn set_legacy_serving_rate_limit(netuid: NetUid, span: u64) { + let mut key = twox_128(b"SubtensorModule").to_vec(); + key.extend(twox_128(b"ServingRateLimit")); + key.extend(netuid.encode()); + storage::set(&key, &span.encode()); + } + + fn parity_check( + now: u64, + call: RuntimeCall, + origin: RuntimeOrigin, + usage_override: Option>, + scope_override: Option, + legacy_check: F, + ) where + F: Fn() -> bool, + { + System::set_block_number(now.saturated_into()); + HasMigrationRun::::remove(MIGRATION_NAME); + clear_rate_limiting_storage(); + + // Run migration to hydrate pallet-rate-limiting state. + Migration::::on_runtime_upgrade(); + + let identifier = TransactionIdentifier::from_call(&call).expect("identifier for call"); + let scope = scope_override.or_else(|| RuntimeScopeResolver::context(&origin, &call)); + let usage: Option::UsageKey>> = + usage_override.or_else(|| RuntimeUsageResolver::context(&origin, &call)); + let target = resolve_target(identifier); + + // Use the runtime-adjusted span (handles tempo scaling for admin-utils). + let span = pallet_rate_limiting::Pallet::::effective_span( + &origin.clone().into(), + &call, + &target, + &scope, + ) + .unwrap_or_default(); + let span_u64: u64 = span.saturated_into(); + + let usage_keys: Vec::UsageKey>> = + match usage { + None => vec![None], + Some(keys) => keys.into_iter().map(Some).collect(), + }; + + let within = usage_keys.iter().all(|key| { + pallet_rate_limiting::Pallet::::is_within_limit( + &origin.clone().into(), + &call, + &identifier, + &scope, + key, + ) + .expect("pallet rate limit result") + }); + assert_eq!(within, legacy_check(), "parity at now for {:?}", identifier); + + // Advance beyond the span and re-check (span==0 treated as allow). + let advance: BlockNumberFor = span.saturating_add(exact_span(1)); + System::set_block_number(System::block_number().saturating_add(advance)); + + let within_after = usage_keys.iter().all(|key| { + pallet_rate_limiting::Pallet::::is_within_limit( + &origin.clone().into(), + &call, + &identifier, + &scope, + key, + ) + .expect("pallet rate limit result (after)") + }); + assert!( + within_after || span_u64 == 0, + "parity after window for {:?}", + identifier + ); + } + #[test] fn maps_hyperparameters() { assert_eq!( @@ -1105,6 +1237,356 @@ mod tests { }); } + #[test] + fn migrates_global_register_network_last_seen() { + new_test_ext().execute_with(|| { + HasMigrationRun::::remove(MIGRATION_NAME); + + // Seed legacy global register rate-limit state. + LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, 10u64); + System::set_block_number(12); + + // Run migration. + Migration::::on_runtime_upgrade(); + + let target = RateLimitTarget::Group(GROUP_REGISTER_NETWORK); + + // LastSeen preserved globally (usage = None). + let stored = pallet_rate_limiting::LastSeen::::get(target, None::) + .expect("last seen entry"); + assert_eq!(stored, 10u64.saturated_into::>()); + }); + } + + #[test] + fn sn_owner_hotkey_limit_not_tempo_scaled_and_last_seen_preserved() { + new_test_ext().execute_with(|| { + HasMigrationRun::::remove(MIGRATION_NAME); + + let netuid = NetUid::from(1); + // Give the subnet a non-1 tempo to catch accidental scaling. + SubtensorModule::set_tempo(netuid, 5); + LastRateLimitedBlock::::insert(RateLimitKey::SetSNOwnerHotkey(netuid), 100u64); + + Migration::::on_runtime_upgrade(); + + let target = RateLimitTarget::Transaction(TransactionIdentifier::new(19, 67)); + + // Limit should remain the fixed default (50400 blocks), not tempo-scaled. + let limit = pallet_rate_limiting::Limits::::get(target).expect("limit stored"); + assert!( + matches!(limit, RateLimit::Global(kind) if kind == RateLimitKind::Exact(50_400)) + ); + + // LastSeen preserved per subnet. + let usage: Option<::UsageKey> = + Some(UsageKey::Subnet(netuid).into()); + let stored = pallet_rate_limiting::LastSeen::::get(target, usage) + .expect("last seen entry"); + assert_eq!(stored, 100u64.saturated_into::>()); + }); + } + + #[test] + fn register_network_parity() { + new_ext().execute_with(|| { + let now = 100u64; + let cold = account(1); + let hot = account(2); + let span = 5u64; + LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, now - 1); + pallet_subtensor::NetworkRateLimit::::put(span); + + let call = + RuntimeCall::SubtensorModule(SubtensorCall::register_network { hotkey: hot }); + let origin = RuntimeOrigin::signed(cold.clone()); + let legacy = || TransactionType::RegisterNetwork.passes_rate_limit::(&cold); + parity_check(now, call, origin, None, None, legacy); + }); + } + + #[test] + fn swap_hotkey_parity() { + new_ext().execute_with(|| { + let now = 200u64; + let cold = account(10); + let old_hot = account(11); + let new_hot = account(12); + let span = 10u64; + LastRateLimitedBlock::::insert( + RateLimitKey::LastTxBlock(cold.clone()), + now - 1, + ); + pallet_subtensor::TxRateLimit::::put(span); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::swap_hotkey { + hotkey: old_hot, + new_hotkey: new_hot, + netuid: None, + }); + let origin = RuntimeOrigin::signed(cold.clone()); + let legacy = || !SubtensorModule::exceeds_tx_rate_limit(now - 1, now); + parity_check(now, call, origin, None, None, legacy); + }); + } + + #[test] + fn increase_take_parity() { + new_ext().execute_with(|| { + let now = 300u64; + let hot = account(20); + let span = 3u64; + LastRateLimitedBlock::::insert( + RateLimitKey::LastTxBlockDelegateTake(hot.clone()), + now - 1, + ); + pallet_subtensor::TxDelegateTakeRateLimit::::put(span); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::increase_take { + hotkey: hot.clone(), + take: 5, + }); + let origin = RuntimeOrigin::signed(account(21)); + let legacy = || !SubtensorModule::exceeds_tx_delegate_take_rate_limit(now - 1, now); + parity_check(now, call, origin, None, None, legacy); + }); + } + + #[test] + fn set_childkey_take_parity() { + new_ext().execute_with(|| { + let now = 400u64; + let hot = account(30); + let netuid = NetUid::from(1u16); + let span = 7u64; + let tx_kind: u16 = TransactionType::SetChildkeyTake.into(); + TransactionKeyLastBlock::::insert((hot.clone(), netuid, tx_kind), now - 1); + pallet_subtensor::TxChildkeyTakeRateLimit::::put(span); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::set_childkey_take { + hotkey: hot.clone(), + netuid, + take: 1, + }); + let origin = RuntimeOrigin::signed(account(31)); + let legacy = || { + TransactionType::SetChildkeyTake + .passes_rate_limit_on_subnet::(&hot, netuid) + }; + parity_check(now, call, origin, None, None, legacy); + }); + } + + #[test] + fn set_children_parity() { + new_ext().execute_with(|| { + let now = 500u64; + let hot = account(40); + let netuid = NetUid::from(2u16); + let tx_kind: u16 = TransactionType::SetChildren.into(); + TransactionKeyLastBlock::::insert((hot.clone(), netuid, tx_kind), now - 1); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::set_children { + hotkey: hot.clone(), + netuid, + children: Vec::new(), + }); + let origin = RuntimeOrigin::signed(account(41)); + let legacy = || { + TransactionType::SetChildren.passes_rate_limit_on_subnet::(&hot, netuid) + }; + parity_check(now, call, origin, None, None, legacy); + }); + } + + #[test] + fn serving_parity() { + new_ext().execute_with(|| { + let now = 600u64; + let hot = account(50); + let netuid = NetUid::from(3u16); + let span = 5u64; + set_legacy_serving_rate_limit(netuid, span); + pallet_subtensor::Axons::::insert( + netuid, + hot.clone(), + AxonInfo { + block: now - 1, + ..Default::default() + }, + ); + pallet_subtensor::Prometheus::::insert( + netuid, + hot.clone(), + PrometheusInfo { + block: now - 1, + ..Default::default() + }, + ); + + // Axon + let axon_call = RuntimeCall::SubtensorModule(SubtensorCall::serve_axon { + netuid, + version: 1, + ip: 0, + port: 0, + ip_type: 4, + protocol: 0, + placeholder1: 0, + placeholder2: 0, + }); + let origin = RuntimeOrigin::signed(hot.clone()); + let legacy_axon = || { + let info = AxonInfo { + block: now.saturating_sub(1), + ..Default::default() + }; + now.saturating_sub(info.block) >= span + }; + parity_check(now, axon_call, origin.clone(), None, None, legacy_axon); + + // Prometheus + let prom_call = RuntimeCall::SubtensorModule(SubtensorCall::serve_prometheus { + netuid, + version: 1, + ip: 0, + port: 0, + ip_type: 4, + }); + let legacy_prom = || { + let info = PrometheusInfo { + block: now.saturating_sub(1), + ..Default::default() + }; + now.saturating_sub(info.block) >= span + }; + parity_check(now, prom_call, origin, None, None, legacy_prom); + }); + } + + #[test] + fn weights_and_hparam_parity() { + new_ext().execute_with(|| { + let now = 700u64; + let hot = account(60); + let netuid = NetUid::from(4u16); + let uid: u16 = 0; + let weights_span = 4u64; + let tempo = 3u16; + // Ensure subnet exists so LastUpdate is imported. + NetworksAdded::::insert(netuid, true); + SubtensorModule::set_tempo(netuid, tempo); + WeightsSetRateLimit::::insert(netuid, weights_span); + LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![now - 1]); + + let weights_call = RuntimeCall::SubtensorModule(SubtensorCall::set_weights { + netuid, + dests: Vec::new(), + weights: Vec::new(), + version_key: 0, + }); + let origin = RuntimeOrigin::signed(hot.clone()); + let scope = Some(netuid); + let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); + + let legacy_weights = || SubtensorModule::check_rate_limit(netuid.into(), uid, now); + parity_check( + now, + weights_call, + origin.clone(), + usage, + scope, + legacy_weights, + ); + + // Hyperparam (activity_cutoff) with tempo scaling. + let hparam_span_epochs = 2u16; + pallet_subtensor::OwnerHyperparamRateLimit::::put(hparam_span_epochs); + LastRateLimitedBlock::::insert( + RateLimitKey::OwnerHyperparamUpdate( + netuid, + pallet_subtensor::utils::rate_limiting::Hyperparameter::ActivityCutoff, + ), + now - 1, + ); + let hparam_call = + RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_activity_cutoff { + netuid, + activity_cutoff: 1, + }); + let hparam_origin = RuntimeOrigin::signed(hot); + let legacy_hparam = || { + let span = (tempo as u64) * (hparam_span_epochs as u64); + let last = now - 1; + // same logic as TransactionType::OwnerHyperparamUpdate in legacy: passes if delta >= span. + let delta = now.saturating_sub(last); + delta >= span + }; + parity_check(now, hparam_call, hparam_origin, None, None, legacy_hparam); + }); + } + + #[test] + fn weights_version_parity() { + new_ext().execute_with(|| { + let now = 800u64; + let hot = account(70); + let netuid = NetUid::from(5u16); + NetworksAdded::::insert(netuid, true); + SubtensorModule::set_tempo(netuid, 4); + WeightsVersionKeyRateLimit::::put(2u64); + let tx_kind_wvk: u16 = TransactionType::SetWeightsVersionKey.into(); + TransactionKeyLastBlock::::insert((hot.clone(), netuid, tx_kind_wvk), now - 1); + + let wvk_call = + RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_weights_version_key { + netuid, + weights_version_key: 0, + }); + let origin = RuntimeOrigin::signed(hot.clone()); + let legacy_wvk = || { + let limit = SubtensorModule::get_tempo(netuid) as u64 + * WeightsVersionKeyRateLimit::::get(); + let delta = now.saturating_sub(now - 1); + delta >= limit + }; + parity_check(now, wvk_call, origin, None, None, legacy_wvk); + }); + } + + #[test] + fn associate_evm_key_parity() { + new_ext().execute_with(|| { + let now = 900u64; + let hot = account(80); + let netuid = NetUid::from(6u16); + let uid: u16 = 0; + NetworksAdded::::insert(netuid, true); + pallet_subtensor::AssociatedEvmAddress::::insert( + netuid, + uid, + (H160::zero(), now - 1), + ); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::associate_evm_key { + netuid, + evm_key: H160::zero(), + block_number: now, + signature: ecdsa::Signature::from_raw([0u8; 65]), + }); + let origin = RuntimeOrigin::signed(hot.clone()); + let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); + let scope = Some(netuid); + let limit = ::EvmKeyAssociateRateLimit::get(); + let legacy = || { + let last = now - 1; + let delta = now.saturating_sub(last); + delta >= limit + }; + parity_check(now, call, origin, usage, scope, legacy); + }); + } + #[test] fn migration_skips_when_already_run() { new_test_ext().execute_with(|| { diff --git a/runtime/src/migrations/subtensor_module.rs b/runtime/src/migrations/subtensor_module.rs new file mode 100644 index 0000000000..926abf9015 --- /dev/null +++ b/runtime/src/migrations/subtensor_module.rs @@ -0,0 +1,319 @@ +use core::marker::PhantomData; + +use frame_support::{traits::Get, traits::OnRuntimeUpgrade, weights::Weight}; +use log; +use scale_info::prelude::string::String; +use subtensor_runtime_common::TaoCurrency; + +use pallet_subtensor::{ + Config as SubtensorConfig, HasMigrationRun, NetworkLockReductionInterval, NetworkRateLimit, + NetworkRegistrationStartBlock, Pallet as SubtensorPallet, +}; + +pub struct Migration(PhantomData); + +impl OnRuntimeUpgrade for Migration { + fn on_runtime_upgrade() -> Weight { + migrate_network_lock_reduction_interval::() + .saturating_add(migrate_network_lock_cost_2500::()) + } +} + +pub fn migrate_network_lock_reduction_interval() -> Weight { + const FOUR_DAYS: u64 = 28_800; + const EIGHT_DAYS: u64 = 57_600; + const ONE_WEEK_BLOCKS: u64 = 50_400; + + let migration_name = b"migrate_network_lock_reduction_interval".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + // Skip if already executed + if HasMigrationRun::::get(&migration_name) { + log::info!( + target: "runtime", + "Migration '{}' already run - skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + let current_block = SubtensorPallet::::get_current_block_as_u64(); + + // -- 1) Set new values -------------------------------------------------- + NetworkLockReductionInterval::::put(EIGHT_DAYS); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + NetworkRateLimit::::put(FOUR_DAYS); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + SubtensorPallet::::set_network_last_lock(TaoCurrency::from(1_000_000_000_000)); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + // Hold price at 2000 TAO until day 7, then begin linear decay + SubtensorPallet::::set_network_last_lock_block( + current_block.saturating_add(ONE_WEEK_BLOCKS), + ); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + // Allow registrations starting at day 7 + NetworkRegistrationStartBlock::::put(current_block.saturating_add(ONE_WEEK_BLOCKS)); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + // -- 2) Mark migration done -------------------------------------------- + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + target: "runtime", + "Migration '{}' completed.", + String::from_utf8_lossy(&migration_name), + ); + + weight +} + +pub fn migrate_network_lock_cost_2500() -> Weight { + const RAO_PER_TAO: u64 = 1_000_000_000; + const TARGET_COST_TAO: u64 = 2_500; + const NEW_LAST_LOCK_RAO: u64 = (TARGET_COST_TAO / 2) * RAO_PER_TAO; // 1,250 TAO + + let migration_name = b"migrate_network_lock_cost_2500".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + // Skip if already executed + if HasMigrationRun::::get(&migration_name) { + log::info!( + target: "runtime", + "Migration '{}' already run - skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + // Use the current block; ensure it's non-zero so mult == 2 in get_network_lock_cost() + let current_block = SubtensorPallet::::get_current_block_as_u64(); + let block_to_set = if current_block == 0 { 1 } else { current_block }; + + // Set last_lock so that price = 2 * last_lock = 2,500 TAO at this block + SubtensorPallet::::set_network_last_lock(TaoCurrency::from(NEW_LAST_LOCK_RAO)); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + // Start decay from "now" (no backdated decay) + SubtensorPallet::::set_network_last_lock_block(block_to_set); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + // Mark migration done + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + target: "runtime", + "Migration '{}' completed. lock_cost set to 2,500 TAO at block {}.", + String::from_utf8_lossy(&migration_name), + block_to_set + ); + + weight +} + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used)] +mod tests { + use frame_support::pallet_prelude::Zero; + use sp_io::TestExternalities; + use sp_runtime::traits::SaturatedConversion; + use subtensor_runtime_common::Currency; + + use super::*; + use crate::{BuildStorage, Runtime, System}; + + fn new_test_ext() -> TestExternalities { + sp_tracing::try_init_simple(); + let mut ext: TestExternalities = crate::RuntimeGenesisConfig::default() + .build_storage() + .expect("runtime storage") + .into(); + ext.execute_with(|| System::set_block_number(1u64.saturated_into())); + ext + } + + fn step_block(blocks: u64) { + let next = System::block_number().saturating_add(blocks.saturated_into()); + System::set_block_number(next); + } + + #[test] + fn test_migrate_network_lock_reduction_interval_and_decay() { + new_test_ext().execute_with(|| { + const FOUR_DAYS: u64 = 28_800; + const EIGHT_DAYS: u64 = 57_600; + const ONE_WEEK_BLOCKS: u64 = 50_400; + + // -- pre -------------------------------------------------------------- + assert!( + !HasMigrationRun::::get( + b"migrate_network_lock_reduction_interval".to_vec() + ), + "HasMigrationRun should be false before migration" + ); + + // ensure current_block > 0 + step_block(1); + let current_block_before = SubtensorPallet::::get_current_block_as_u64(); + + // -- run migration --------------------------------------------------- + let weight = migrate_network_lock_reduction_interval::(); + assert!(!weight.is_zero(), "migration weight should be > 0"); + + // -- params & flags -------------------------------------------------- + assert_eq!(NetworkLockReductionInterval::::get(), EIGHT_DAYS); + assert_eq!(NetworkRateLimit::::get(), FOUR_DAYS); + assert_eq!( + SubtensorPallet::::get_network_last_lock(), + 1_000_000_000_000u64.into(), // 1000 TAO in rao + "last_lock should be 1_000_000_000_000 rao" + ); + + // last_lock_block should be set one week in the future + let last_lock_block = SubtensorPallet::::get_network_last_lock_block(); + let expected_block = current_block_before + ONE_WEEK_BLOCKS; + assert_eq!( + last_lock_block, expected_block, + "last_lock_block should be current + ONE_WEEK_BLOCKS" + ); + + // registration start block should match the same future block + assert_eq!( + NetworkRegistrationStartBlock::::get(), + expected_block, + "NetworkRegistrationStartBlock should equal last_lock_block" + ); + + // lock cost should be 2000 TAO immediately after migration + let lock_cost_now = SubtensorPallet::::get_network_lock_cost(); + assert_eq!( + lock_cost_now, + 2_000_000_000_000u64.into(), + "lock cost should be 2000 TAO right after migration" + ); + + assert!( + HasMigrationRun::::get( + b"migrate_network_lock_reduction_interval".to_vec() + ), + "HasMigrationRun should be true after migration" + ); + }); + } + + #[test] + fn test_migrate_network_lock_cost_2500_sets_price_and_decay() { + new_test_ext().execute_with(|| { + // -- constants ------------------------------------------------------- + const RAO_PER_TAO: u64 = 1_000_000_000; + const TARGET_COST_TAO: u64 = 2_500; + const TARGET_COST_RAO: u64 = TARGET_COST_TAO * RAO_PER_TAO; + const NEW_LAST_LOCK_RAO: u64 = (TARGET_COST_TAO / 2) * RAO_PER_TAO; + + let migration_key = b"migrate_network_lock_cost_2500".to_vec(); + + // -- pre -------------------------------------------------------------- + assert!( + !HasMigrationRun::::get(migration_key.clone()), + "HasMigrationRun should be false before migration" + ); + + // Ensure current_block > 0 so mult == 2 in get_network_lock_cost() + step_block(1); + let current_block_before = SubtensorPallet::::get_current_block_as_u64(); + + // Snapshot interval to ensure migration doesn't change it + let interval_before = NetworkLockReductionInterval::::get(); + + // -- run migration --------------------------------------------------- + let weight = migrate_network_lock_cost_2500::(); + assert!(!weight.is_zero(), "migration weight should be > 0"); + + // -- asserts: params & flags ----------------------------------------- + assert_eq!( + SubtensorPallet::::get_network_last_lock(), + NEW_LAST_LOCK_RAO.into(), + "last_lock should be set to 1,250 TAO (in rao)" + ); + assert_eq!( + SubtensorPallet::::get_network_last_lock_block(), + current_block_before, + "last_lock_block should be set to the current block" + ); + + // Lock cost should be exactly 2,500 TAO immediately after migration + let lock_cost_now = SubtensorPallet::::get_network_lock_cost(); + assert_eq!( + lock_cost_now, + TARGET_COST_RAO.into(), + "lock cost should be 2,500 TAO right after migration" + ); + + // Interval should be unchanged by this migration + assert_eq!( + NetworkLockReductionInterval::::get(), + interval_before, + "lock reduction interval should not be modified by this migration" + ); + + assert!( + HasMigrationRun::::get(migration_key.clone()), + "HasMigrationRun should be true after migration" + ); + + // -- decay check (1 block later) ------------------------------------- + // Expected: cost = max(min_lock, 2*L - floor(L / eff_interval) * delta_blocks) + let eff_interval = SubtensorPallet::::get_lock_reduction_interval(); + let per_block_decrement: u64 = if eff_interval == 0 { + 0 + } else { + NEW_LAST_LOCK_RAO / eff_interval + }; + + let min_lock_rao: u64 = SubtensorPallet::::get_network_min_lock().to_u64(); + + step_block(1); + let expected_after_1: u64 = + core::cmp::max(min_lock_rao, TARGET_COST_RAO - per_block_decrement); + let lock_cost_after_1 = SubtensorPallet::::get_network_lock_cost(); + assert_eq!( + lock_cost_after_1, + expected_after_1.into(), + "lock cost should decay by one per-block step after 1 block" + ); + + // -- idempotency: running the migration again should do nothing ------ + let last_lock_before_rerun = SubtensorPallet::::get_network_last_lock(); + let last_lock_block_before_rerun = + SubtensorPallet::::get_network_last_lock_block(); + let cost_before_rerun = SubtensorPallet::::get_network_lock_cost(); + + let _weight2 = migrate_network_lock_cost_2500::(); + + assert!( + HasMigrationRun::::get(migration_key.clone()), + "HasMigrationRun remains true on second run" + ); + assert_eq!( + SubtensorPallet::::get_network_last_lock(), + last_lock_before_rerun, + "second run should not modify last_lock" + ); + assert_eq!( + SubtensorPallet::::get_network_last_lock_block(), + last_lock_block_before_rerun, + "second run should not modify last_lock_block" + ); + assert_eq!( + SubtensorPallet::::get_network_lock_cost(), + cost_before_rerun, + "second run should not change current lock cost" + ); + }); + } +} diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 9b370229fe..caf9c67779 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -33,8 +33,7 @@ use subtensor_runtime_common::{ use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; -mod legacy; -pub mod migration; +pub(crate) mod legacy; /// Authorization rules for configuring rate limits via `pallet-rate-limiting::set_rate_limit`. /// diff --git a/runtime/tests/rate_limiting_behavior.rs b/runtime/tests/rate_limiting_behavior.rs deleted file mode 100644 index 132fa1bbe3..0000000000 --- a/runtime/tests/rate_limiting_behavior.rs +++ /dev/null @@ -1,438 +0,0 @@ -#![allow(clippy::unwrap_used)] - -use codec::Encode; -use frame_support::traits::OnRuntimeUpgrade; -use frame_system::pallet_prelude::BlockNumberFor; -use node_subtensor_runtime::{ - BuildStorage, Runtime, RuntimeCall, RuntimeGenesisConfig, RuntimeOrigin, RuntimeScopeResolver, - RuntimeUsageResolver, SubtensorModule, System, rate_limiting::migration::Migration, -}; -use pallet_rate_limiting::{RateLimitScopeResolver, RateLimitUsageResolver}; -use pallet_rate_limiting::{RateLimitTarget, TransactionIdentifier}; -use pallet_subtensor::Call as SubtensorCall; -use pallet_subtensor::{ - AxonInfo, HasMigrationRun, LastRateLimitedBlock, LastUpdate, NetworksAdded, PrometheusInfo, - RateLimitKey, TransactionKeyLastBlock, WeightsSetRateLimit, WeightsVersionKeyRateLimit, - utils::rate_limiting::TransactionType, -}; -use sp_core::{H160, ecdsa}; -use sp_io::{hashing::twox_128, storage}; -use sp_runtime::traits::SaturatedConversion; -use subtensor_runtime_common::{ - NetUid, NetUidStorageIndex, - rate_limiting::{GroupId, RateLimitUsageKey}, -}; - -type AccountId = ::AccountId; -type UsageKey = RateLimitUsageKey; - -const MIGRATION_NAME: &[u8] = b"migrate_rate_limiting"; - -fn new_ext() -> sp_io::TestExternalities { - sp_tracing::try_init_simple(); - let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig::default() - .build_storage() - .unwrap() - .into(); - ext.execute_with(|| System::set_block_number(1)); - ext -} - -fn account(n: u8) -> AccountId { - AccountId::from([n; 32]) -} - -fn resolve_target(identifier: TransactionIdentifier) -> RateLimitTarget { - if let Some(group) = pallet_rate_limiting::CallGroups::::get(identifier) { - RateLimitTarget::Group(group) - } else { - RateLimitTarget::Transaction(identifier) - } -} - -fn exact_span(span: u64) -> BlockNumberFor { - span.saturated_into::>() -} - -fn clear_rate_limiting_storage() { - let limit = u32::MAX; - let _ = pallet_rate_limiting::Limits::::clear(limit, None); - let _ = pallet_rate_limiting::LastSeen::::clear(limit, None); - let _ = pallet_rate_limiting::Groups::::clear(limit, None); - let _ = pallet_rate_limiting::GroupMembers::::clear(limit, None); - let _ = pallet_rate_limiting::GroupNameIndex::::clear(limit, None); - let _ = pallet_rate_limiting::CallGroups::::clear(limit, None); - pallet_rate_limiting::NextGroupId::::kill(); -} - -fn set_legacy_serving_rate_limit(netuid: NetUid, span: u64) { - let mut key = twox_128(b"SubtensorModule").to_vec(); - key.extend(twox_128(b"ServingRateLimit")); - key.extend(netuid.encode()); - storage::set(&key, &span.encode()); -} - -fn parity_check( - now: u64, - call: RuntimeCall, - origin: RuntimeOrigin, - usage_override: Option>, - scope_override: Option, - legacy_check: F, -) where - F: Fn() -> bool, -{ - System::set_block_number(now.saturated_into()); - HasMigrationRun::::remove(MIGRATION_NAME); - clear_rate_limiting_storage(); - - // Run migration to hydrate pallet-rate-limiting state. - Migration::::on_runtime_upgrade(); - - let identifier = TransactionIdentifier::from_call(&call).expect("identifier for call"); - let scope = scope_override.or_else(|| RuntimeScopeResolver::context(&origin, &call)); - let usage: Option::UsageKey>> = - usage_override.or_else(|| RuntimeUsageResolver::context(&origin, &call)); - let target = resolve_target(identifier); - - // Use the runtime-adjusted span (handles tempo scaling for admin-utils). - let span = pallet_rate_limiting::Pallet::::effective_span( - &origin.clone().into(), - &call, - &target, - &scope, - ) - .unwrap_or_default(); - let span_u64: u64 = span.saturated_into(); - - let usage_keys: Vec::UsageKey>> = match usage { - None => vec![None], - Some(keys) => keys.into_iter().map(Some).collect(), - }; - - let within = usage_keys.iter().all(|key| { - pallet_rate_limiting::Pallet::::is_within_limit( - &origin.clone().into(), - &call, - &identifier, - &scope, - key, - ) - .expect("pallet rate limit result") - }); - assert_eq!(within, legacy_check(), "parity at now for {:?}", identifier); - - // Advance beyond the span and re-check (span==0 treated as allow). - let advance: BlockNumberFor = span.saturating_add(exact_span(1)); - System::set_block_number(System::block_number().saturating_add(advance)); - - let within_after = usage_keys.iter().all(|key| { - pallet_rate_limiting::Pallet::::is_within_limit( - &origin.clone().into(), - &call, - &identifier, - &scope, - key, - ) - .expect("pallet rate limit result (after)") - }); - assert!( - within_after || span_u64 == 0, - "parity after window for {:?}", - identifier - ); -} - -#[test] -fn register_network_parity() { - new_ext().execute_with(|| { - let now = 100u64; - let cold = account(1); - let hot = account(2); - let span = 5u64; - LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, now - 1); - pallet_subtensor::NetworkRateLimit::::put(span); - - let call = RuntimeCall::SubtensorModule(SubtensorCall::register_network { hotkey: hot }); - let origin = RuntimeOrigin::signed(cold.clone()); - let legacy = || TransactionType::RegisterNetwork.passes_rate_limit::(&cold); - parity_check(now, call, origin, None, None, legacy); - }); -} - -#[test] -fn swap_hotkey_parity() { - new_ext().execute_with(|| { - let now = 200u64; - let cold = account(10); - let old_hot = account(11); - let new_hot = account(12); - let span = 10u64; - LastRateLimitedBlock::::insert(RateLimitKey::LastTxBlock(cold.clone()), now - 1); - pallet_subtensor::TxRateLimit::::put(span); - - let call = RuntimeCall::SubtensorModule(SubtensorCall::swap_hotkey { - hotkey: old_hot, - new_hotkey: new_hot, - netuid: None, - }); - let origin = RuntimeOrigin::signed(cold.clone()); - let legacy = || !SubtensorModule::exceeds_tx_rate_limit(now - 1, now); - parity_check(now, call, origin, None, None, legacy); - }); -} - -#[test] -fn increase_take_parity() { - new_ext().execute_with(|| { - let now = 300u64; - let hot = account(20); - let span = 3u64; - LastRateLimitedBlock::::insert( - RateLimitKey::LastTxBlockDelegateTake(hot.clone()), - now - 1, - ); - pallet_subtensor::TxDelegateTakeRateLimit::::put(span); - - let call = RuntimeCall::SubtensorModule(SubtensorCall::increase_take { - hotkey: hot.clone(), - take: 5, - }); - let origin = RuntimeOrigin::signed(account(21)); - let legacy = || !SubtensorModule::exceeds_tx_delegate_take_rate_limit(now - 1, now); - parity_check(now, call, origin, None, None, legacy); - }); -} - -#[test] -fn set_childkey_take_parity() { - new_ext().execute_with(|| { - let now = 400u64; - let hot = account(30); - let netuid = NetUid::from(1u16); - let span = 7u64; - let tx_kind: u16 = TransactionType::SetChildkeyTake.into(); - TransactionKeyLastBlock::::insert((hot.clone(), netuid, tx_kind), now - 1); - pallet_subtensor::TxChildkeyTakeRateLimit::::put(span); - - let call = RuntimeCall::SubtensorModule(SubtensorCall::set_childkey_take { - hotkey: hot.clone(), - netuid, - take: 1, - }); - let origin = RuntimeOrigin::signed(account(31)); - let legacy = || { - TransactionType::SetChildkeyTake.passes_rate_limit_on_subnet::(&hot, netuid) - }; - parity_check(now, call, origin, None, None, legacy); - }); -} - -#[test] -fn set_children_parity() { - new_ext().execute_with(|| { - let now = 500u64; - let hot = account(40); - let netuid = NetUid::from(2u16); - let tx_kind: u16 = TransactionType::SetChildren.into(); - TransactionKeyLastBlock::::insert((hot.clone(), netuid, tx_kind), now - 1); - - let call = RuntimeCall::SubtensorModule(SubtensorCall::set_children { - hotkey: hot.clone(), - netuid, - children: Vec::new(), - }); - let origin = RuntimeOrigin::signed(account(41)); - let legacy = - || TransactionType::SetChildren.passes_rate_limit_on_subnet::(&hot, netuid); - parity_check(now, call, origin, None, None, legacy); - }); -} - -#[test] -fn serving_parity() { - new_ext().execute_with(|| { - let now = 600u64; - let hot = account(50); - let netuid = NetUid::from(3u16); - let span = 5u64; - set_legacy_serving_rate_limit(netuid, span); - pallet_subtensor::Axons::::insert( - netuid, - hot.clone(), - AxonInfo { - block: now - 1, - ..Default::default() - }, - ); - pallet_subtensor::Prometheus::::insert( - netuid, - hot.clone(), - PrometheusInfo { - block: now - 1, - ..Default::default() - }, - ); - - // Axon - let axon_call = RuntimeCall::SubtensorModule(SubtensorCall::serve_axon { - netuid, - version: 1, - ip: 0, - port: 0, - ip_type: 4, - protocol: 0, - placeholder1: 0, - placeholder2: 0, - }); - let origin = RuntimeOrigin::signed(hot.clone()); - let legacy_axon = || { - let info = AxonInfo { - block: now.saturating_sub(1), - ..Default::default() - }; - now.saturating_sub(info.block) >= span - }; - parity_check(now, axon_call, origin.clone(), None, None, legacy_axon); - - // Prometheus - let prom_call = RuntimeCall::SubtensorModule(SubtensorCall::serve_prometheus { - netuid, - version: 1, - ip: 0, - port: 0, - ip_type: 4, - }); - let legacy_prom = || { - let info = PrometheusInfo { - block: now.saturating_sub(1), - ..Default::default() - }; - now.saturating_sub(info.block) >= span - }; - parity_check(now, prom_call, origin, None, None, legacy_prom); - }); -} - -#[test] -fn weights_and_hparam_parity() { - new_ext().execute_with(|| { - let now = 700u64; - let hot = account(60); - let netuid = NetUid::from(4u16); - let uid: u16 = 0; - let weights_span = 4u64; - let tempo = 3u16; - // Ensure subnet exists so LastUpdate is imported. - NetworksAdded::::insert(netuid, true); - SubtensorModule::set_tempo(netuid, tempo); - WeightsSetRateLimit::::insert(netuid, weights_span); - LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![now - 1]); - - let weights_call = RuntimeCall::SubtensorModule(SubtensorCall::set_weights { - netuid, - dests: Vec::new(), - weights: Vec::new(), - version_key: 0, - }); - let origin = RuntimeOrigin::signed(hot.clone()); - let scope = Some(netuid); - let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); - - let legacy_weights = || SubtensorModule::check_rate_limit(netuid.into(), uid, now); - parity_check( - now, - weights_call, - origin.clone(), - usage, - scope, - legacy_weights, - ); - - // Hyperparam (activity_cutoff) with tempo scaling. - let hparam_span_epochs = 2u16; - pallet_subtensor::OwnerHyperparamRateLimit::::put(hparam_span_epochs); - LastRateLimitedBlock::::insert( - RateLimitKey::OwnerHyperparamUpdate( - netuid, - pallet_subtensor::utils::rate_limiting::Hyperparameter::ActivityCutoff, - ), - now - 1, - ); - let hparam_call = - RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_activity_cutoff { - netuid, - activity_cutoff: 1, - }); - let hparam_origin = RuntimeOrigin::signed(hot); - let legacy_hparam = || { - let span = (tempo as u64) * (hparam_span_epochs as u64); - let last = now - 1; - // same logic as TransactionType::OwnerHyperparamUpdate in legacy: passes if delta >= span. - let delta = now.saturating_sub(last); - delta >= span - }; - parity_check(now, hparam_call, hparam_origin, None, None, legacy_hparam); - }); -} - -#[test] -fn weights_version_parity() { - new_ext().execute_with(|| { - let now = 800u64; - let hot = account(70); - let netuid = NetUid::from(5u16); - NetworksAdded::::insert(netuid, true); - SubtensorModule::set_tempo(netuid, 4); - WeightsVersionKeyRateLimit::::put(2u64); - let tx_kind_wvk: u16 = TransactionType::SetWeightsVersionKey.into(); - TransactionKeyLastBlock::::insert((hot.clone(), netuid, tx_kind_wvk), now - 1); - - let wvk_call = - RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_weights_version_key { - netuid, - weights_version_key: 0, - }); - let origin = RuntimeOrigin::signed(hot.clone()); - let legacy_wvk = || { - let limit = SubtensorModule::get_tempo(netuid) as u64 - * WeightsVersionKeyRateLimit::::get(); - let delta = now.saturating_sub(now - 1); - delta >= limit - }; - parity_check(now, wvk_call, origin, None, None, legacy_wvk); - }); -} - -#[test] -fn associate_evm_key_parity() { - new_ext().execute_with(|| { - let now = 900u64; - let hot = account(80); - let netuid = NetUid::from(6u16); - let uid: u16 = 0; - NetworksAdded::::insert(netuid, true); - pallet_subtensor::AssociatedEvmAddress::::insert( - netuid, - uid, - (H160::zero(), now - 1), - ); - - let call = RuntimeCall::SubtensorModule(SubtensorCall::associate_evm_key { - netuid, - evm_key: H160::zero(), - block_number: now, - signature: ecdsa::Signature::from_raw([0u8; 65]), - }); - let origin = RuntimeOrigin::signed(hot.clone()); - let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); - let scope = Some(netuid); - let limit = ::EvmKeyAssociateRateLimit::get(); - let legacy = || { - let last = now - 1; - let delta = now.saturating_sub(last); - delta >= limit - }; - parity_check(now, call, origin, usage, scope, legacy); - }); -} diff --git a/runtime/tests/rate_limiting_migration.rs b/runtime/tests/rate_limiting_migration.rs deleted file mode 100644 index 9e08f489b9..0000000000 --- a/runtime/tests/rate_limiting_migration.rs +++ /dev/null @@ -1,77 +0,0 @@ -#![allow(clippy::unwrap_used)] - -use frame_support::traits::OnRuntimeUpgrade; -use frame_system::pallet_prelude::BlockNumberFor; -use pallet_rate_limiting::{RateLimit, RateLimitKind, RateLimitTarget, TransactionIdentifier}; -use pallet_subtensor::{HasMigrationRun, LastRateLimitedBlock, RateLimitKey}; -use sp_runtime::traits::SaturatedConversion; -use subtensor_runtime_common::{ - NetUid, - rate_limiting::{GROUP_REGISTER_NETWORK, RateLimitUsageKey}, -}; - -use node_subtensor_runtime::{ - BuildStorage, Runtime, RuntimeGenesisConfig, SubtensorModule, System, - rate_limiting::migration::{MIGRATION_NAME, Migration}, -}; - -type AccountId = ::AccountId; -type UsageKey = RateLimitUsageKey; - -fn new_test_ext() -> sp_io::TestExternalities { - sp_tracing::try_init_simple(); - let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig::default() - .build_storage() - .unwrap() - .into(); - ext.execute_with(|| System::set_block_number(1)); - ext -} - -#[test] -fn migrates_global_register_network_last_seen() { - new_test_ext().execute_with(|| { - HasMigrationRun::::remove(MIGRATION_NAME); - - // Seed legacy global register rate-limit state. - LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, 10u64); - System::set_block_number(12); - - // Run migration. - Migration::::on_runtime_upgrade(); - - let target = RateLimitTarget::Group(GROUP_REGISTER_NETWORK); - - // LastSeen preserved globally (usage = None). - let stored = pallet_rate_limiting::LastSeen::::get(target, None::) - .expect("last seen entry"); - assert_eq!(stored, 10u64.saturated_into::>()); - }); -} - -#[test] -fn sn_owner_hotkey_limit_not_tempo_scaled_and_last_seen_preserved() { - new_test_ext().execute_with(|| { - HasMigrationRun::::remove(MIGRATION_NAME); - - let netuid = NetUid::from(1); - // Give the subnet a non-1 tempo to catch accidental scaling. - SubtensorModule::set_tempo(netuid, 5); - LastRateLimitedBlock::::insert(RateLimitKey::SetSNOwnerHotkey(netuid), 100u64); - - Migration::::on_runtime_upgrade(); - - let target = RateLimitTarget::Transaction(TransactionIdentifier::new(19, 67)); - - // Limit should remain the fixed default (50400 blocks), not tempo-scaled. - let limit = pallet_rate_limiting::Limits::::get(target).expect("limit stored"); - assert!(matches!(limit, RateLimit::Global(kind) if kind == RateLimitKind::Exact(50_400))); - - // LastSeen preserved per subnet. - let usage: Option<::UsageKey> = - Some(UsageKey::Subnet(netuid).into()); - let stored = - pallet_rate_limiting::LastSeen::::get(target, usage).expect("last seen entry"); - assert_eq!(stored, 100u64.saturated_into::>()); - }); -} From 72c9369e4a7207ff16d1a2cb234e3655f73df6a0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 5 Jan 2026 15:44:41 +0100 Subject: [PATCH 51/95] Remove legacy network registered rate limits get/set --- pallets/subtensor/src/coinbase/root.rs | 17 ++-- pallets/subtensor/src/subnets/subnet.rs | 1 - pallets/subtensor/src/tests/migration.rs | 14 ++- pallets/subtensor/src/utils/rate_limiting.rs | 18 +++- runtime/src/migrations/subtensor_module.rs | 95 ++++++++++++++++---- 5 files changed, 109 insertions(+), 36 deletions(-) diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 83ff49abbf..b821a1be34 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -17,9 +17,13 @@ use super::*; use crate::CommitmentsInterface; +use rate_limiting_interface::RateLimitingInfo; use safe_math::*; +use sp_runtime::SaturatedConversion; use substrate_fixed::types::{I64F64, U96F32}; -use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, NetUidStorageIndex, TaoCurrency}; +use subtensor_runtime_common::{ + AlphaCurrency, Currency, NetUid, NetUidStorageIndex, TaoCurrency, rate_limiting, +}; use subtensor_swap_interface::SwapHandler; impl Pallet { @@ -507,7 +511,10 @@ impl Pallet { pub fn get_network_lock_cost() -> TaoCurrency { let last_lock = Self::get_network_last_lock(); let min_lock = Self::get_network_min_lock(); - let last_lock_block = Self::get_network_last_lock_block(); + let last_lock_block: u64 = + T::RateLimiting::last_seen(rate_limiting::GROUP_REGISTER_NETWORK, None) + .unwrap_or_default() + .saturated_into(); let current_block = Self::get_current_block_as_u64(); let lock_reduction_interval = Self::get_lock_reduction_interval(); let mult: TaoCurrency = if last_lock_block == 0 { 1 } else { 2 }.into(); @@ -554,12 +561,6 @@ impl Pallet { pub fn get_network_last_lock() -> TaoCurrency { NetworkLastLockCost::::get() } - pub fn get_network_last_lock_block() -> u64 { - Self::get_rate_limited_last_block(&RateLimitKey::NetworkLastRegistered) - } - pub fn set_network_last_lock_block(block: u64) { - Self::set_rate_limited_last_block(&RateLimitKey::NetworkLastRegistered, block); - } pub fn set_lock_reduction_interval(interval: u64) { NetworkLockReductionInterval::::set(interval); Self::deposit_event(Event::NetworkLockCostReductionIntervalSet(interval)); diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index 4185aee624..8719e679b6 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -171,7 +171,6 @@ impl Pallet { // --- 8. Set the lock amount for use to determine pricing. Self::set_network_last_lock(actual_tao_lock_amount); - Self::set_network_last_lock_block(current_block); // --- 9. If we identified a subnet to prune, do it now. if let Some(prune_netuid) = recycle_netuid { diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 5736a54efb..5b44f19dd3 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -22,13 +22,15 @@ use frame_support::{ use crate::migrations::migrate_storage; use frame_system::Config; use pallet_drand::types::RoundNumber; +use rate_limiting_interface::RateLimitingInfo; use scale_info::prelude::collections::VecDeque; use sp_core::{H256, U256, crypto::Ss58Codec}; use sp_io::hashing::twox_128; +use sp_runtime::SaturatedConversion; use sp_runtime::traits::Zero; use substrate_fixed::types::extra::U2; use substrate_fixed::types::{I96F32, U64F64}; -use subtensor_runtime_common::{NetUidStorageIndex, TaoCurrency}; +use subtensor_runtime_common::{NetUidStorageIndex, TaoCurrency, rate_limiting}; #[allow(clippy::arithmetic_side_effects)] fn close(value: u64, target: u64, eps: u64) { @@ -884,9 +886,14 @@ fn test_migrate_rate_limit_keys() { assert!(!weight.is_zero(), "Migration weight should be non-zero"); // Legacy entries were migrated and cleared. + let network_last_lock_block: u64 = ::RateLimiting::last_seen( + rate_limiting::GROUP_REGISTER_NETWORK, + None, + ) + .unwrap_or_default() + .saturated_into(); assert_eq!( - SubtensorModule::get_network_last_lock_block(), - 111u64, + network_last_lock_block, 111, "Network last lock block should match migrated value" ); assert!( @@ -1786,7 +1793,6 @@ fn test_migrate_subnet_limit_to_default() { #[test] fn test_migrate_restore_subnet_locked_65_128() { - use sp_runtime::traits::SaturatedConversion; new_test_ext(0).execute_with(|| { let name = b"migrate_restore_subnet_locked".to_vec(); assert!( diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index 468aecd1c1..2fae748379 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -1,6 +1,8 @@ use codec::{Decode, Encode}; +use rate_limiting_interface::RateLimitingInfo; use scale_info::TypeInfo; -use subtensor_runtime_common::NetUid; +use sp_runtime::SaturatedConversion; +use subtensor_runtime_common::{NetUid, rate_limiting}; use super::*; @@ -80,7 +82,11 @@ impl TransactionType { /// Get the block number of the last transaction for a specific key, and transaction type pub fn last_block(&self, key: &T::AccountId) -> u64 { match self { - Self::RegisterNetwork => Pallet::::get_network_last_lock_block(), + Self::RegisterNetwork => { + T::RateLimiting::last_seen(rate_limiting::GROUP_REGISTER_NETWORK, None) + .unwrap_or_default() + .saturated_into() + } _ => self.last_block_on_subnet::(key, NetUid::ROOT), } } @@ -89,7 +95,11 @@ impl TransactionType { /// type pub fn last_block_on_subnet(&self, hotkey: &T::AccountId, netuid: NetUid) -> u64 { match self { - Self::RegisterNetwork => Pallet::::get_network_last_lock_block(), + Self::RegisterNetwork => { + T::RateLimiting::last_seen(rate_limiting::GROUP_REGISTER_NETWORK, None) + .unwrap_or_default() + .saturated_into() + } Self::SetSNOwnerHotkey => { Pallet::::get_rate_limited_last_block(&RateLimitKey::SetSNOwnerHotkey(netuid)) } @@ -112,7 +122,7 @@ impl TransactionType { block: u64, ) { match self { - Self::RegisterNetwork => Pallet::::set_network_last_lock_block(block), + Self::RegisterNetwork => { /*DEPRECATED*/ } Self::SetSNOwnerHotkey => Pallet::::set_rate_limited_last_block( &RateLimitKey::SetSNOwnerHotkey(netuid), block, diff --git a/runtime/src/migrations/subtensor_module.rs b/runtime/src/migrations/subtensor_module.rs index 926abf9015..0ff54370d2 100644 --- a/runtime/src/migrations/subtensor_module.rs +++ b/runtime/src/migrations/subtensor_module.rs @@ -1,25 +1,43 @@ use core::marker::PhantomData; use frame_support::{traits::Get, traits::OnRuntimeUpgrade, weights::Weight}; +use frame_system::pallet_prelude::BlockNumberFor; use log; +use pallet_rate_limiting::{RateLimit, RateLimitKind, RateLimitTarget}; use scale_info::prelude::string::String; +use sp_runtime::traits::SaturatedConversion; use subtensor_runtime_common::TaoCurrency; +use subtensor_runtime_common::rate_limiting::{GROUP_REGISTER_NETWORK, GroupId}; use pallet_subtensor::{ - Config as SubtensorConfig, HasMigrationRun, NetworkLockReductionInterval, NetworkRateLimit, + Config as SubtensorConfig, HasMigrationRun, NetworkLockReductionInterval, NetworkRegistrationStartBlock, Pallet as SubtensorPallet, }; pub struct Migration(PhantomData); -impl OnRuntimeUpgrade for Migration { +impl OnRuntimeUpgrade for Migration +where + T: SubtensorConfig + + pallet_rate_limiting::Config< + LimitScope = subtensor_runtime_common::NetUid, + GroupId = GroupId, + >, +{ fn on_runtime_upgrade() -> Weight { migrate_network_lock_reduction_interval::() .saturating_add(migrate_network_lock_cost_2500::()) } } -pub fn migrate_network_lock_reduction_interval() -> Weight { +pub fn migrate_network_lock_reduction_interval() -> Weight +where + T: SubtensorConfig + + pallet_rate_limiting::Config< + LimitScope = subtensor_runtime_common::NetUid, + GroupId = GroupId, + >, +{ const FOUR_DAYS: u64 = 28_800; const EIGHT_DAYS: u64 = 57_600; const ONE_WEEK_BLOCKS: u64 = 50_400; @@ -43,20 +61,29 @@ pub fn migrate_network_lock_reduction_interval() -> Weight { NetworkLockReductionInterval::::put(EIGHT_DAYS); weight = weight.saturating_add(T::DbWeight::get().writes(1)); - NetworkRateLimit::::put(FOUR_DAYS); + pallet_rate_limiting::Limits::::insert( + RateLimitTarget::Group(GROUP_REGISTER_NETWORK), + RateLimit::Global(RateLimitKind::Exact(FOUR_DAYS.saturated_into())), + ); weight = weight.saturating_add(T::DbWeight::get().writes(1)); SubtensorPallet::::set_network_last_lock(TaoCurrency::from(1_000_000_000_000)); weight = weight.saturating_add(T::DbWeight::get().writes(1)); // Hold price at 2000 TAO until day 7, then begin linear decay - SubtensorPallet::::set_network_last_lock_block( - current_block.saturating_add(ONE_WEEK_BLOCKS), - ); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); + let last_lock_block = current_block.saturating_add(ONE_WEEK_BLOCKS); // Allow registrations starting at day 7 - NetworkRegistrationStartBlock::::put(current_block.saturating_add(ONE_WEEK_BLOCKS)); + NetworkRegistrationStartBlock::::put(last_lock_block); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + // Mirror the register-network last seen in pallet-rate-limiting. + let last_seen_block: BlockNumberFor = last_lock_block.saturated_into(); + pallet_rate_limiting::LastSeen::::insert( + RateLimitTarget::Group(GROUP_REGISTER_NETWORK), + None::, + last_seen_block, + ); weight = weight.saturating_add(T::DbWeight::get().writes(1)); // -- 2) Mark migration done -------------------------------------------- @@ -72,7 +99,14 @@ pub fn migrate_network_lock_reduction_interval() -> Weight { weight } -pub fn migrate_network_lock_cost_2500() -> Weight { +pub fn migrate_network_lock_cost_2500() -> Weight +where + T: SubtensorConfig + + pallet_rate_limiting::Config< + LimitScope = subtensor_runtime_common::NetUid, + GroupId = GroupId, + >, +{ const RAO_PER_TAO: u64 = 1_000_000_000; const TARGET_COST_TAO: u64 = 2_500; const NEW_LAST_LOCK_RAO: u64 = (TARGET_COST_TAO / 2) * RAO_PER_TAO; // 1,250 TAO @@ -98,8 +132,13 @@ pub fn migrate_network_lock_cost_2500() -> Weight { SubtensorPallet::::set_network_last_lock(TaoCurrency::from(NEW_LAST_LOCK_RAO)); weight = weight.saturating_add(T::DbWeight::get().writes(1)); - // Start decay from "now" (no backdated decay) - SubtensorPallet::::set_network_last_lock_block(block_to_set); + // Mirror the register-network last seen in pallet-rate-limiting. + let last_seen_block: BlockNumberFor = block_to_set.saturated_into(); + pallet_rate_limiting::LastSeen::::insert( + RateLimitTarget::Group(GROUP_REGISTER_NETWORK), + None::, + last_seen_block, + ); weight = weight.saturating_add(T::DbWeight::get().writes(1)); // Mark migration done @@ -120,9 +159,12 @@ pub fn migrate_network_lock_cost_2500() -> Weight { #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { use frame_support::pallet_prelude::Zero; + use frame_system::pallet_prelude::BlockNumberFor; + use pallet_rate_limiting::{RateLimit, RateLimitKind, RateLimitTarget}; use sp_io::TestExternalities; use sp_runtime::traits::SaturatedConversion; use subtensor_runtime_common::Currency; + use subtensor_runtime_common::rate_limiting::GROUP_REGISTER_NETWORK; use super::*; use crate::{BuildStorage, Runtime, System}; @@ -142,6 +184,13 @@ mod tests { System::set_block_number(next); } + fn register_network_last_seen() -> Option> { + pallet_rate_limiting::LastSeen::::get( + RateLimitTarget::Group(GROUP_REGISTER_NETWORK), + None::<::UsageKey>, + ) + } + #[test] fn test_migrate_network_lock_reduction_interval_and_decay() { new_test_ext().execute_with(|| { @@ -167,7 +216,14 @@ mod tests { // -- params & flags -------------------------------------------------- assert_eq!(NetworkLockReductionInterval::::get(), EIGHT_DAYS); - assert_eq!(NetworkRateLimit::::get(), FOUR_DAYS); + assert_eq!( + pallet_rate_limiting::Limits::::get(RateLimitTarget::Group( + GROUP_REGISTER_NETWORK + )), + Some(RateLimit::Global(RateLimitKind::Exact( + FOUR_DAYS.saturated_into() + ))) + ); assert_eq!( SubtensorPallet::::get_network_last_lock(), 1_000_000_000_000u64.into(), // 1000 TAO in rao @@ -175,10 +231,11 @@ mod tests { ); // last_lock_block should be set one week in the future - let last_lock_block = SubtensorPallet::::get_network_last_lock_block(); + let last_lock_block = register_network_last_seen().expect("last seen entry"); let expected_block = current_block_before + ONE_WEEK_BLOCKS; assert_eq!( - last_lock_block, expected_block, + last_lock_block, + expected_block.saturated_into::>(), "last_lock_block should be current + ONE_WEEK_BLOCKS" ); @@ -241,8 +298,8 @@ mod tests { "last_lock should be set to 1,250 TAO (in rao)" ); assert_eq!( - SubtensorPallet::::get_network_last_lock_block(), - current_block_before, + register_network_last_seen().expect("last seen entry"), + current_block_before.saturated_into::>(), "last_lock_block should be set to the current block" ); @@ -290,7 +347,7 @@ mod tests { // -- idempotency: running the migration again should do nothing ------ let last_lock_before_rerun = SubtensorPallet::::get_network_last_lock(); let last_lock_block_before_rerun = - SubtensorPallet::::get_network_last_lock_block(); + register_network_last_seen().expect("last seen entry"); let cost_before_rerun = SubtensorPallet::::get_network_lock_cost(); let _weight2 = migrate_network_lock_cost_2500::(); @@ -305,7 +362,7 @@ mod tests { "second run should not modify last_lock" ); assert_eq!( - SubtensorPallet::::get_network_last_lock_block(), + register_network_last_seen().expect("last seen entry"), last_lock_block_before_rerun, "second run should not modify last_lock_block" ); From 4fb2da5bd1d848394888d96bd3b8eb324ab7df60 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 6 Jan 2026 13:23:49 +0100 Subject: [PATCH 52/95] Deprecate network rate limit storage, config and extrinsic --- chain-extensions/src/mock.rs | 2 -- pallets/admin-utils/src/lib.rs | 17 +++++++++-------- pallets/admin-utils/src/tests/mock.rs | 2 -- pallets/subtensor/src/benchmarks.rs | 2 -- pallets/subtensor/src/lib.rs | 13 ------------- pallets/subtensor/src/macros/config.rs | 3 --- pallets/subtensor/src/macros/events.rs | 4 ---- pallets/subtensor/src/subnets/subnet.rs | 13 ------------- pallets/subtensor/src/tests/mock.rs | 2 -- pallets/subtensor/src/utils/rate_limiting.rs | 18 +++++++++--------- pallets/transaction-fee/src/tests/mock.rs | 2 -- runtime/src/lib.rs | 1 - 12 files changed, 18 insertions(+), 61 deletions(-) diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index ae99c4031e..490d6f40fa 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -321,7 +321,6 @@ parameter_types! { pub const InitialNetworkMinLockCost: u64 = 100_000_000_000; pub const InitialSubnetOwnerCut: u16 = 0; // 0%. 100% of rewards go to validators + miners. pub const InitialNetworkLockReductionInterval: u64 = 2; // 2 blocks. - pub const InitialNetworkRateLimit: u64 = 0; pub const InitialKeySwapCost: u64 = 1_000_000_000; pub const InitialAlphaHigh: u16 = 58982; // Represents 0.9 as per the production default pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default @@ -391,7 +390,6 @@ impl pallet_subtensor::Config for Test { type InitialNetworkMinLockCost = InitialNetworkMinLockCost; type InitialSubnetOwnerCut = InitialSubnetOwnerCut; type InitialNetworkLockReductionInterval = InitialNetworkLockReductionInterval; - type InitialNetworkRateLimit = InitialNetworkRateLimit; type KeySwapCost = InitialKeySwapCost; type AlphaHigh = InitialAlphaHigh; type AlphaLow = InitialAlphaLow; diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 18cddb0d42..82517c49e2 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -984,8 +984,9 @@ pub mod pallet { } /// The extrinsic sets the network rate limit for the network. - /// It is only callable by the root account. - /// The extrinsic will call the Subtensor pallet to set the network rate limit. + /// + /// Deprecated: network rate limits are now configured via `pallet-rate-limiting` on the + /// register-network group target (`GROUP_REGISTER_NETWORK`) with `scope = None`. #[pallet::call_index(29)] #[pallet::weight(( Weight::from_parts(14_000_000, 0) @@ -993,14 +994,14 @@ pub mod pallet { DispatchClass::Operational, Pays::Yes ))] + #[deprecated( + note = "deprecated: configure via pallet-rate-limiting::set_rate_limit(target=Group(GROUP_REGISTER_NETWORK), scope=None, ...)" + )] pub fn sudo_set_network_rate_limit( - origin: OriginFor, - rate_limit: u64, + _origin: OriginFor, + _rate_limit: u64, ) -> DispatchResult { - ensure_root(origin)?; - pallet_subtensor::Pallet::::set_network_rate_limit(rate_limit); - log::debug!("NetworkRateLimit( rate_limit: {rate_limit:?} ) "); - Ok(()) + Err(Error::::Deprecated.into()) } /// The extrinsic sets the tempo for a subnet. diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 211a72ef08..75cce3f03d 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -132,7 +132,6 @@ parameter_types! { pub const InitialSubnetOwnerCut: u16 = 0; // 0%. 100% of rewards go to validators + miners. pub const InitialNetworkLockReductionInterval: u64 = 2; // 2 blocks. // pub const InitialSubnetLimit: u16 = 10; // (DEPRECATED) - pub const InitialNetworkRateLimit: u64 = 0; pub const InitialKeySwapCost: u64 = 1_000_000_000; pub const InitialAlphaHigh: u16 = 58982; // Represents 0.9 as per the production default pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default @@ -202,7 +201,6 @@ impl pallet_subtensor::Config for Test { type InitialNetworkMinLockCost = InitialNetworkMinLockCost; type InitialSubnetOwnerCut = InitialSubnetOwnerCut; type InitialNetworkLockReductionInterval = InitialNetworkLockReductionInterval; - type InitialNetworkRateLimit = InitialNetworkRateLimit; type KeySwapCost = InitialKeySwapCost; type AlphaHigh = InitialAlphaHigh; type AlphaLow = InitialAlphaLow; diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index 67b7f473b0..edc6358a02 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -257,7 +257,6 @@ mod pallet_benchmarks { let coldkey: T::AccountId = account("Test", 0, seed); let hotkey: T::AccountId = account("TestHotkey", 0, seed); - Subtensor::::set_network_rate_limit(1); let amount: u64 = 100_000_000_000_000u64.saturating_mul(2); Subtensor::::add_balance_to_coldkey_account(&coldkey, amount); @@ -1087,7 +1086,6 @@ mod pallet_benchmarks { let identity: Option = None; Subtensor::::set_network_registration_allowed(1.into(), true); - Subtensor::::set_network_rate_limit(1); let amount: u64 = 9_999_999_999_999; Subtensor::::add_balance_to_coldkey_account(&coldkey, amount); diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index c44381dc2b..778cde0083 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -646,15 +646,6 @@ pub mod pallet { RecycleOrBurnEnum::Burn // default to burn } - /// Default value for network rate limit. - #[pallet::type_value] - pub fn DefaultNetworkRateLimit() -> u64 { - if cfg!(feature = "pow-faucet") { - return 0; - } - T::InitialNetworkRateLimit::get() - } - /// Default value for network rate limit. #[pallet::type_value] pub fn DefaultNetworkRegistrationStartBlock() -> u64 { @@ -1527,10 +1518,6 @@ pub mod pallet { #[pallet::storage] pub type SubnetOwnerCut = StorageValue<_, u16, ValueQuery, DefaultSubnetOwnerCut>; - /// ITEM( network_rate_limit ) - #[pallet::storage] - pub type NetworkRateLimit = StorageValue<_, u64, ValueQuery, DefaultNetworkRateLimit>; - /// --- ITEM( nominator_min_required_stake ) --- Factor of DefaultMinStake in per-mill format. #[pallet::storage] pub type NominatorMinRequiredStake = StorageValue<_, u64, ValueQuery, DefaultZeroU64>; diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index b6dc13059d..6c7ea04bd4 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -207,9 +207,6 @@ mod config { /// Initial lock reduction interval. #[pallet::constant] type InitialNetworkLockReductionInterval: Get; - /// Initial network creation rate limit - #[pallet::constant] - type InitialNetworkRateLimit: Get; /// Cost of swapping a hotkey. #[pallet::constant] type KeySwapCost: Get; diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index baf39b2994..49cf8dce6f 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -101,8 +101,6 @@ mod events { MinDifficultySet(NetUid, u64), /// setting max difficulty on a network. MaxDifficultySet(NetUid, u64), - /// [DEPRECATED] setting the prometheus serving rate limit. - ServingRateLimitSet(NetUid, u64), /// setting burn on a network. BurnSet(NetUid, TaoCurrency), /// setting max burn on a network. @@ -143,8 +141,6 @@ mod events { Faucet(T::AccountId, u64), /// the subnet owner cut is set. SubnetOwnerCutSet(u16), - /// the network creation rate limit is set. - NetworkRateLimitSet(u64), /// the network immunity period is set. NetworkImmunityPeriodSet(u64), /// the network minimum locking cost is set. diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index 8719e679b6..440f1821de 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -61,13 +61,6 @@ impl Pallet { } } - /// Sets the network rate limit and emit the `NetworkRateLimitSet` event - /// - pub fn set_network_rate_limit(limit: u64) { - NetworkRateLimit::::set(limit); - Self::deposit_event(Event::NetworkRateLimitSet(limit)); - } - /// Checks if registrations are allowed for a given subnet. /// /// This function retrieves the subnet hyperparameters for the specified subnet and checks the @@ -134,12 +127,6 @@ impl Pallet { Error::::SubNetRegistrationDisabled ); - // --- 4. Rate limit for network registrations. - ensure!( - TransactionType::RegisterNetwork.passes_rate_limit::(&coldkey), - Error::::NetworkTxRateLimitExceeded - ); - // --- 5. Check if we need to prune a subnet (if at SubnetLimit). // But do not prune yet; we only do it after all checks pass. let subnet_limit = Self::get_max_subnets(); diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 8cb3478b34..89b2eb4a76 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -206,7 +206,6 @@ parameter_types! { pub const InitialNetworkMinLockCost: u64 = 100_000_000_000; pub const InitialSubnetOwnerCut: u16 = 0; // 0%. 100% of rewards go to validators + miners. pub const InitialNetworkLockReductionInterval: u64 = 2; // 2 blocks. - pub const InitialNetworkRateLimit: u64 = 0; pub const InitialKeySwapCost: u64 = 1_000_000_000; pub const InitialAlphaHigh: u16 = 58982; // Represents 0.9 as per the production default pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default @@ -276,7 +275,6 @@ impl crate::Config for Test { type InitialNetworkMinLockCost = InitialNetworkMinLockCost; type InitialSubnetOwnerCut = InitialSubnetOwnerCut; type InitialNetworkLockReductionInterval = InitialNetworkLockReductionInterval; - type InitialNetworkRateLimit = InitialNetworkRateLimit; type KeySwapCost = InitialKeySwapCost; type AlphaHigh = InitialAlphaHigh; type AlphaLow = InitialAlphaLow; diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index 2fae748379..4409e61ce3 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -1,8 +1,7 @@ use codec::{Decode, Encode}; use rate_limiting_interface::RateLimitingInfo; use scale_info::TypeInfo; -use sp_runtime::SaturatedConversion; -use subtensor_runtime_common::{NetUid, rate_limiting}; +use subtensor_runtime_common::NetUid; use super::*; @@ -28,7 +27,10 @@ impl TransactionType { match self { Self::SetChildren => 150, // 30 minutes Self::SetChildkeyTake => TxChildkeyTakeRateLimit::::get(), - Self::RegisterNetwork => NetworkRateLimit::::get(), + Self::RegisterNetwork => { + /*DEPRECATED*/ + 0 + } Self::MechanismCountUpdate => MechanismCountSetRateLimit::::get(), Self::MechanismEmission => MechanismEmissionRateLimit::::get(), Self::MaxUidsTrimming => MaxUidsTrimmingRateLimit::::get(), @@ -83,9 +85,8 @@ impl TransactionType { pub fn last_block(&self, key: &T::AccountId) -> u64 { match self { Self::RegisterNetwork => { - T::RateLimiting::last_seen(rate_limiting::GROUP_REGISTER_NETWORK, None) - .unwrap_or_default() - .saturated_into() + /*DEPRECATED*/ + 0 } _ => self.last_block_on_subnet::(key, NetUid::ROOT), } @@ -96,9 +97,8 @@ impl TransactionType { pub fn last_block_on_subnet(&self, hotkey: &T::AccountId, netuid: NetUid) -> u64 { match self { Self::RegisterNetwork => { - T::RateLimiting::last_seen(rate_limiting::GROUP_REGISTER_NETWORK, None) - .unwrap_or_default() - .saturated_into() + /*DEPRECATED*/ + 0 } Self::SetSNOwnerHotkey => { Pallet::::get_rate_limited_last_block(&RateLimitKey::SetSNOwnerHotkey(netuid)) diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index c5f25b54e8..2ebe15febe 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -199,7 +199,6 @@ parameter_types! { pub const InitialSubnetOwnerCut: u16 = 0; // 0%. 100% of rewards go to validators + miners. pub const InitialNetworkLockReductionInterval: u64 = 2; // 2 blocks. // pub const InitialSubnetLimit: u16 = 10; // (DEPRECATED) - pub const InitialNetworkRateLimit: u64 = 0; pub const InitialKeySwapCost: u64 = 1_000_000_000; pub const InitialAlphaHigh: u16 = 58982; // Represents 0.9 as per the production default pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default @@ -269,7 +268,6 @@ impl pallet_subtensor::Config for Test { type InitialNetworkMinLockCost = InitialNetworkMinLockCost; type InitialSubnetOwnerCut = InitialSubnetOwnerCut; type InitialNetworkLockReductionInterval = InitialNetworkLockReductionInterval; - type InitialNetworkRateLimit = InitialNetworkRateLimit; type KeySwapCost = InitialKeySwapCost; type AlphaHigh = InitialAlphaHigh; type AlphaLow = InitialAlphaLow; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 8b2004bbfd..c9336dee55 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1119,7 +1119,6 @@ impl pallet_subtensor::Config for Runtime { type InitialNetworkMinLockCost = SubtensorInitialMinLockCost; type InitialNetworkLockReductionInterval = SubtensorInitialNetworkLockReductionInterval; type InitialSubnetOwnerCut = SubtensorInitialSubnetOwnerCut; - type InitialNetworkRateLimit = SubtensorInitialNetworkRateLimit; type KeySwapCost = SubtensorInitialKeySwapCost; type AlphaHigh = InitialAlphaHigh; type AlphaLow = InitialAlphaLow; From d3f705fa0eb60204c607e34d6e2d84ffa5ef800d Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 6 Jan 2026 18:51:25 +0100 Subject: [PATCH 53/95] Update contract tests --- contract-tests/src/subtensor.ts | 15 ++++++++++++--- pallets/subtensor/src/tests/migration.rs | 1 + pallets/subtensor/src/utils/rate_limiting.rs | 1 - 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/contract-tests/src/subtensor.ts b/contract-tests/src/subtensor.ts index ce2f32acc2..0d8169929e 100644 --- a/contract-tests/src/subtensor.ts +++ b/contract-tests/src/subtensor.ts @@ -12,9 +12,18 @@ export async function addNewSubnetwork(api: TypedApi, hotkey: Key const alice = getAliceSigner() const totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue() - const rateLimit = await api.query.SubtensorModule.NetworkRateLimit.getValue() + const registerNetworkGroupId = 3; // GROUP_REGISTER_NETWORK constant + const target = { Group: registerNetworkGroupId } as const; + const limits = await api.query.RateLimiting.Limits.getValue(target as any) as any; + const rateLimit = limits?.tag === "Global" && limits?.value?.tag === "Exact" + ? BigInt(limits.value.value) + : BigInt(0); if (rateLimit !== BigInt(0)) { - const internalCall = api.tx.AdminUtils.sudo_set_network_rate_limit({ rate_limit: BigInt(0) }) + const internalCall = api.tx.RateLimiting.set_rate_limit({ + target: target as any, + scope: undefined, + limit: { Exact: BigInt(0) }, + }) const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) await waitForTransactionWithRetry(api, tx, alice) } @@ -398,4 +407,4 @@ export async function sendWasmContractExtrinsic(api: TypedApi, co storage_deposit_limit: BigInt(1000000000) }) await waitForTransactionWithRetry(api, tx, signer) -} +} diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 5b44f19dd3..356cf686b2 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -829,6 +829,7 @@ fn test_migrate_remove_commitments_rate_limit() { }); } +// TODO this must be removed after the legacy rate-limiting deprecation #[test] fn test_migrate_rate_limit_keys() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index 4409e61ce3..4a88c5fe73 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -1,5 +1,4 @@ use codec::{Decode, Encode}; -use rate_limiting_interface::RateLimitingInfo; use scale_info::TypeInfo; use subtensor_runtime_common::NetUid; From 4e1de28f87c33b124077b1b4a5e221b959568700 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 7 Jan 2026 15:18:22 +0100 Subject: [PATCH 54/95] Setup runtime integration tests --- runtime/Cargo.toml | 1 + runtime/tests/common/mock.rs | 52 ++++++++++++++++++++++++++++++++++++ runtime/tests/common/mod.rs | 3 +++ 3 files changed, 56 insertions(+) create mode 100644 runtime/tests/common/mock.rs create mode 100644 runtime/tests/common/mod.rs diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index baec2c6f30..558e36c17e 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -282,6 +282,7 @@ std = [ "ethereum/std", "pallet-shield/std", ] +integration-tests = ["std"] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", diff --git a/runtime/tests/common/mock.rs b/runtime/tests/common/mock.rs new file mode 100644 index 0000000000..d4a9d02cd5 --- /dev/null +++ b/runtime/tests/common/mock.rs @@ -0,0 +1,52 @@ +use frame_support::traits::BuildStorage; +use sp_io::TestExternalities; +use subtensor_runtime_common::{AccountId, Balance}; + +use node_subtensor_runtime::{RuntimeGenesisConfig, System}; + +pub struct ExtBuilder { + balances: Vec<(AccountId, Balance)>, + block_number: u64, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + balances: Vec::new(), + block_number: 1, + } + } +} + +impl ExtBuilder { + pub fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + + pub fn with_block_number(mut self, block_number: u64) -> Self { + self.block_number = block_number; + self + } + + pub fn build(self) -> TestExternalities { + let mut ext: TestExternalities = RuntimeGenesisConfig { + balances: pallet_balances::GenesisConfig { + balances: self.balances, + dev_accounts: None, + }, + ..Default::default() + } + .build_storage() + .expect("runtime genesis config builds") + .into(); + + let block_number = self.block_number; + ext.execute_with(|| System::set_block_number(block_number)); + ext + } +} + +pub fn new_test_ext() -> TestExternalities { + ExtBuilder::default().build() +} diff --git a/runtime/tests/common/mod.rs b/runtime/tests/common/mod.rs new file mode 100644 index 0000000000..50b365d88c --- /dev/null +++ b/runtime/tests/common/mod.rs @@ -0,0 +1,3 @@ +pub mod mock; + +pub use mock::{ExtBuilder, new_test_ext}; From dcd47494e5d3a21494192aab5e57423d3a2900b2 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 7 Jan 2026 18:53:08 +0100 Subject: [PATCH 55/95] Add integration test for network register rate-limiting --- pallets/rate-limiting/src/benchmarking.rs | 1 + runtime/Cargo.toml | 3 +- runtime/src/migrations/rate_limiting.rs | 28 ++++-- runtime/src/migrations/subtensor_module.rs | 12 +-- runtime/tests/common/mock.rs | 8 +- runtime/tests/rate_limiting.rs | 112 +++++++++++++++++++++ 6 files changed, 143 insertions(+), 21 deletions(-) create mode 100644 runtime/tests/rate_limiting.rs diff --git a/pallets/rate-limiting/src/benchmarking.rs b/pallets/rate-limiting/src/benchmarking.rs index 265733e113..4b1b2ed385 100644 --- a/pallets/rate-limiting/src/benchmarking.rs +++ b/pallets/rate-limiting/src/benchmarking.rs @@ -6,6 +6,7 @@ use codec::Decode; use frame_benchmarking::v2::*; use frame_system::{RawOrigin, pallet_prelude::BlockNumberFor}; use sp_runtime::traits::{One, Saturating}; +use sp_std::boxed::Box; use super::*; use crate::CallReadOnly; diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 558e36c17e..70e06f0468 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -282,7 +282,7 @@ std = [ "ethereum/std", "pallet-shield/std", ] -integration-tests = ["std"] +integration-tests = [] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", @@ -322,6 +322,7 @@ runtime-benchmarks = [ "pallet-hotfix-sufficients/runtime-benchmarks", "pallet-drand/runtime-benchmarks", "pallet-transaction-payment/runtime-benchmarks", + "pallet-rate-limiting/runtime-benchmarks", "pallet-subtensor-swap/runtime-benchmarks", # Smart Tx fees pallet diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 8c42fbe622..42856fcd6b 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -1097,6 +1097,12 @@ mod tests { storage::set(&key, &span.encode()); } + fn set_legacy_network_rate_limit(span: u64) { + let mut key = twox_128(b"SubtensorModule").to_vec(); + key.extend(twox_128(b"NetworkRateLimit")); + storage::set(&key, &span.encode()); + } + fn parity_check( now: u64, call: RuntimeCall, @@ -1290,18 +1296,24 @@ mod tests { #[test] fn register_network_parity() { new_ext().execute_with(|| { + HasMigrationRun::::remove(MIGRATION_NAME); let now = 100u64; - let cold = account(1); - let hot = account(2); let span = 5u64; + System::set_block_number(now.saturated_into()); LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, now - 1); - pallet_subtensor::NetworkRateLimit::::put(span); + set_legacy_network_rate_limit(span); - let call = - RuntimeCall::SubtensorModule(SubtensorCall::register_network { hotkey: hot }); - let origin = RuntimeOrigin::signed(cold.clone()); - let legacy = || TransactionType::RegisterNetwork.passes_rate_limit::(&cold); - parity_check(now, call, origin, None, None, legacy); + Migration::::on_runtime_upgrade(); + + let target = RateLimitTarget::Group(GROUP_REGISTER_NETWORK); + let limit = pallet_rate_limiting::Limits::::get(target).expect("limit stored"); + assert!( + matches!(limit, RateLimit::Global(kind) if kind == RateLimitKind::Exact(exact_span(span))) + ); + + let stored = pallet_rate_limiting::LastSeen::::get(target, None::) + .expect("last seen entry"); + assert_eq!(stored, (now - 1).saturated_into::>()); }); } diff --git a/runtime/src/migrations/subtensor_module.rs b/runtime/src/migrations/subtensor_module.rs index 0ff54370d2..54e2cdddd8 100644 --- a/runtime/src/migrations/subtensor_module.rs +++ b/runtime/src/migrations/subtensor_module.rs @@ -14,6 +14,10 @@ use pallet_subtensor::{ NetworkRegistrationStartBlock, Pallet as SubtensorPallet, }; +const FOUR_DAYS: u64 = 28_800; +const EIGHT_DAYS: u64 = 57_600; +const ONE_WEEK_BLOCKS: u64 = 50_400; + pub struct Migration(PhantomData); impl OnRuntimeUpgrade for Migration @@ -38,10 +42,6 @@ where GroupId = GroupId, >, { - const FOUR_DAYS: u64 = 28_800; - const EIGHT_DAYS: u64 = 57_600; - const ONE_WEEK_BLOCKS: u64 = 50_400; - let migration_name = b"migrate_network_lock_reduction_interval".to_vec(); let mut weight = T::DbWeight::get().reads(1); @@ -194,10 +194,6 @@ mod tests { #[test] fn test_migrate_network_lock_reduction_interval_and_decay() { new_test_ext().execute_with(|| { - const FOUR_DAYS: u64 = 28_800; - const EIGHT_DAYS: u64 = 57_600; - const ONE_WEEK_BLOCKS: u64 = 50_400; - // -- pre -------------------------------------------------------------- assert!( !HasMigrationRun::::get( diff --git a/runtime/tests/common/mock.rs b/runtime/tests/common/mock.rs index d4a9d02cd5..fe3bacfc80 100644 --- a/runtime/tests/common/mock.rs +++ b/runtime/tests/common/mock.rs @@ -1,9 +1,9 @@ -use frame_support::traits::BuildStorage; +use node_subtensor_runtime::{RuntimeGenesisConfig, System}; use sp_io::TestExternalities; +use sp_runtime::BuildStorage; +use sp_runtime::traits::SaturatedConversion; use subtensor_runtime_common::{AccountId, Balance}; -use node_subtensor_runtime::{RuntimeGenesisConfig, System}; - pub struct ExtBuilder { balances: Vec<(AccountId, Balance)>, block_number: u64, @@ -42,7 +42,7 @@ impl ExtBuilder { .into(); let block_number = self.block_number; - ext.execute_with(|| System::set_block_number(block_number)); + ext.execute_with(|| System::set_block_number(block_number.saturated_into())); ext } } diff --git a/runtime/tests/rate_limiting.rs b/runtime/tests/rate_limiting.rs new file mode 100644 index 0000000000..658558fca0 --- /dev/null +++ b/runtime/tests/rate_limiting.rs @@ -0,0 +1,112 @@ +#![cfg(feature = "integration-tests")] +#![allow(clippy::unwrap_used)] + +use codec::Encode; +use frame_support::assert_ok; +use node_subtensor_runtime::{ + Executive, Runtime, RuntimeCall, SignedPayload, System, TransactionExtensions, + UncheckedExtrinsic, check_nonce, sudo_wrapper, transaction_payment_wrapper, +}; +use sp_core::{Pair, sr25519}; +use sp_runtime::{ + MultiSignature, + generic::Era, + traits::SaturatedConversion, + transaction_validity::{InvalidTransaction, TransactionValidityError}, +}; +use subtensor_runtime_common::AccountId; + +use common::ExtBuilder; + +mod common; + +fn signed_extrinsic(call: RuntimeCall, pair: &sr25519::Pair, nonce: u32) -> UncheckedExtrinsic { + let check_metadata_hash = + frame_metadata_hash_extension::CheckMetadataHash::::new(false); + + let extra: TransactionExtensions = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(Era::Immortal), + check_nonce::CheckNonce::::from(nonce).into(), + frame_system::CheckWeight::::new(), + transaction_payment_wrapper::ChargeTransactionPaymentWrapper::new( + pallet_transaction_payment::ChargeTransactionPayment::::from(0), + ), + sudo_wrapper::SudoTransactionExtension::::new(), + pallet_subtensor::transaction_extension::SubtensorTransactionExtension::::new(), + ( + pallet_drand::drand_priority::DrandPriority::::new(), + check_metadata_hash, + ), + pallet_rate_limiting::RateLimitTransactionExtension::::new(), + ); + + let payload = SignedPayload::new(call.clone(), extra.clone()).expect("signed payload"); + let signature = MultiSignature::from(pair.sign(payload.encode().as_slice())); + let address = sp_runtime::MultiAddress::Id(AccountId::from(pair.public())); + UncheckedExtrinsic::new_signed(call, address, signature, extra) +} + +#[test] +fn register_network_is_rate_limited_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[1u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey_a = AccountId::from([2u8; 32]); + let hotkey_b = AccountId::from([3u8; 32]); + let balance = 10_000_000_000_000_u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + // Run runtime upgrades explicitly; ExtBuilder sets up genesis only. + Executive::execute_on_runtime_upgrade(); + + let call_a = RuntimeCall::SubtensorModule(pallet_subtensor::Call::register_network { + hotkey: hotkey_a, + }); + let call_b = RuntimeCall::SubtensorModule( + pallet_subtensor::Call::register_network_with_identity { + hotkey: hotkey_b, + identity: None, + }, + ); + + let start_block = + pallet_subtensor::NetworkRegistrationStartBlock::::get().saturated_into(); + System::set_block_number(start_block); + + let mut nonce = System::account(coldkey.clone()).nonce; + let xt_a = signed_extrinsic(call_a, &coldkey_pair, nonce); + assert_ok!(Executive::apply_extrinsic(xt_a)); + + nonce = System::account(coldkey.clone()).nonce; + let xt_b = signed_extrinsic(call_b.clone(), &coldkey_pair, nonce); + assert!(matches!( + Executive::apply_extrinsic(xt_b).expect_err("rate limit enforced"), + TransactionValidityError::Invalid(InvalidTransaction::Custom(1)) + )); + + // Migration sets register-network limit to 4 days (28_800 blocks). + let limit = start_block.saturating_add(28_800); + + // Should still be rate-limited. + System::set_block_number(limit - 1); + nonce = System::account(coldkey.clone()).nonce; + let xt_b = signed_extrinsic(call_b.clone(), &coldkey_pair, nonce); + assert!(matches!( + Executive::apply_extrinsic(xt_b).expect_err("rate limit enforced"), + TransactionValidityError::Invalid(InvalidTransaction::Custom(1)) + )); + + // Should pass now. + System::set_block_number(limit); + nonce = System::account(coldkey.clone()).nonce; + let xt_c = signed_extrinsic(call_b, &coldkey_pair, nonce); + assert_ok!(Executive::apply_extrinsic(xt_c)); + }); +} From 6c57dc12a67a41e19a72a1b2d4ee5935c5e0dee7 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 8 Jan 2026 17:46:03 +0100 Subject: [PATCH 56/95] Add integration test for serving rate limit --- runtime/tests/common/mod.rs | 3 +- runtime/tests/rate_limiting.rs | 140 +++++++++++++++++++++++++++------ 2 files changed, 118 insertions(+), 25 deletions(-) diff --git a/runtime/tests/common/mod.rs b/runtime/tests/common/mod.rs index 50b365d88c..27c914e371 100644 --- a/runtime/tests/common/mod.rs +++ b/runtime/tests/common/mod.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] pub mod mock; -pub use mock::{ExtBuilder, new_test_ext}; +pub use mock::*; diff --git a/runtime/tests/rate_limiting.rs b/runtime/tests/rate_limiting.rs index 658558fca0..ccd6182c8f 100644 --- a/runtime/tests/rate_limiting.rs +++ b/runtime/tests/rate_limiting.rs @@ -14,12 +14,27 @@ use sp_runtime::{ traits::SaturatedConversion, transaction_validity::{InvalidTransaction, TransactionValidityError}, }; -use subtensor_runtime_common::AccountId; +use subtensor_runtime_common::{AccountId, NetUid}; use common::ExtBuilder; mod common; +fn assert_extrinsic_ok(account_id: &AccountId, pair: &sr25519::Pair, call: RuntimeCall) { + let nonce = System::account(account_id).nonce; + let xt = signed_extrinsic(call, pair, nonce); + assert_ok!(Executive::apply_extrinsic(xt)); +} + +fn assert_extrinsic_rate_limited(account_id: &AccountId, pair: &sr25519::Pair, call: RuntimeCall) { + let nonce = System::account(account_id).nonce; + let xt = signed_extrinsic(call, pair, nonce); + assert!(matches!( + Executive::apply_extrinsic(xt).expect_err("rate limit enforced"), + TransactionValidityError::Invalid(InvalidTransaction::Custom(1)) + )); +} + fn signed_extrinsic(call: RuntimeCall, pair: &sr25519::Pair, nonce: u32) -> UncheckedExtrinsic { let check_metadata_hash = frame_metadata_hash_extension::CheckMetadataHash::::new(false); @@ -63,7 +78,8 @@ fn register_network_is_rate_limited_after_migration() { .build() .execute_with(|| { System::set_block_number(1); - // Run runtime upgrades explicitly; ExtBuilder sets up genesis only. + + // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. Executive::execute_on_runtime_upgrade(); let call_a = RuntimeCall::SubtensorModule(pallet_subtensor::Call::register_network { @@ -75,38 +91,114 @@ fn register_network_is_rate_limited_after_migration() { identity: None, }, ); - let start_block = pallet_subtensor::NetworkRegistrationStartBlock::::get().saturated_into(); + System::set_block_number(start_block); - let mut nonce = System::account(coldkey.clone()).nonce; - let xt_a = signed_extrinsic(call_a, &coldkey_pair, nonce); - assert_ok!(Executive::apply_extrinsic(xt_a)); + assert_extrinsic_ok(&coldkey, &coldkey_pair, call_a.clone()); - nonce = System::account(coldkey.clone()).nonce; - let xt_b = signed_extrinsic(call_b.clone(), &coldkey_pair, nonce); - assert!(matches!( - Executive::apply_extrinsic(xt_b).expect_err("rate limit enforced"), - TransactionValidityError::Invalid(InvalidTransaction::Custom(1)) - )); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, call_b.clone()); // Migration sets register-network limit to 4 days (28_800 blocks). let limit = start_block.saturating_add(28_800); - // Should still be rate-limited. + // Should still be rate-limited. System::set_block_number(limit - 1); - nonce = System::account(coldkey.clone()).nonce; - let xt_b = signed_extrinsic(call_b.clone(), &coldkey_pair, nonce); - assert!(matches!( - Executive::apply_extrinsic(xt_b).expect_err("rate limit enforced"), - TransactionValidityError::Invalid(InvalidTransaction::Custom(1)) - )); - - // Should pass now. + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, call_a.clone()); + + // Should pass now. System::set_block_number(limit); - nonce = System::account(coldkey.clone()).nonce; - let xt_c = signed_extrinsic(call_b, &coldkey_pair, nonce); - assert_ok!(Executive::apply_extrinsic(xt_c)); + assert_extrinsic_ok(&coldkey, &coldkey_pair, call_b); + + // Both calls share the same usage key and window. + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, call_a.clone()); + + System::set_block_number(limit.saturating_add(28_800)); + assert_extrinsic_ok(&coldkey, &coldkey_pair, call_a); + }); +} + +#[test] +fn serving_is_rate_limited_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[4u8; 32]); + let hotkey_pair = sr25519::Pair::from_seed(&[5u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from(hotkey_pair.public()); + let balance = 10_000_000_000_000_u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. + Executive::execute_on_runtime_upgrade(); + + assert_extrinsic_ok( + &coldkey, + &coldkey_pair, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { + hotkey: hotkey.clone(), + }), + ); + + let netuid = NetUid::ROOT; + let start_block = System::block_number(); + let serve_axon = RuntimeCall::SubtensorModule(pallet_subtensor::Call::serve_axon { + netuid, + version: 1, + ip: 0, + port: 3030, + ip_type: 4, + protocol: 0, + placeholder1: 0, + placeholder2: 0, + }); + let serve_axon_tls = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::serve_axon_tls { + netuid, + version: 1, + ip: 0, + port: 3030, + ip_type: 4, + protocol: 0, + placeholder1: 0, + placeholder2: 0, + certificate: b"cert".to_vec(), + }); + let serve_prometheus = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::serve_prometheus { + netuid, + version: 1, + ip: 1_676_056_785, + port: 3031, + ip_type: 4, + }); + + assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_axon.clone()); + + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_axon_tls.clone()); + + assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_prometheus.clone()); + + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_prometheus.clone()); + + // Migration sets serving limit to 50 blocks by default. + let limit = start_block.saturating_add(50); + + // Should still be rate-limited. + System::set_block_number(limit - 1); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_axon.clone()); + + // Should pass now. + System::set_block_number(limit); + assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_axon_tls); + + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_axon); + + assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_prometheus.clone()); + + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_prometheus); }); } From 45d6106c8d2b026e2507a123eee35cd494d58caa Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 9 Jan 2026 13:48:03 +0100 Subject: [PATCH 57/95] Deprecate legacy delegate take rate limits --- chain-extensions/src/mock.rs | 2 - pallets/admin-utils/src/lib.rs | 20 +-- pallets/admin-utils/src/tests/mock.rs | 2 - pallets/admin-utils/src/tests/mod.rs | 27 ---- pallets/subtensor/src/lib.rs | 11 -- pallets/subtensor/src/macros/config.rs | 3 - pallets/subtensor/src/macros/errors.rs | 2 - pallets/subtensor/src/macros/events.rs | 2 - .../subtensor/src/staking/decrease_take.rs | 4 - .../subtensor/src/staking/increase_take.rs | 13 -- pallets/subtensor/src/swap/swap_hotkey.rs | 5 - pallets/subtensor/src/tests/migration.rs | 12 -- pallets/subtensor/src/tests/mock.rs | 2 - pallets/subtensor/src/tests/staking.rs | 127 +----------------- pallets/subtensor/src/tests/swap_hotkey.rs | 7 - .../src/tests/swap_hotkey_with_subnet.rs | 7 - pallets/subtensor/src/utils/misc.rs | 7 - pallets/subtensor/src/utils/rate_limiting.rs | 17 --- pallets/transaction-fee/src/tests/mock.rs | 2 - runtime/src/lib.rs | 1 - runtime/src/migrations/rate_limiting.rs | 3 +- runtime/src/rate_limiting/mod.rs | 4 +- 22 files changed, 17 insertions(+), 263 deletions(-) diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 490d6f40fa..1f2a676d0b 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -295,7 +295,6 @@ parameter_types! { pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing - pub const InitialTxDelegateTakeRateLimit: u64 = 1; // 1 block take rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 1; // 1 block take rate limit for testing pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -379,7 +378,6 @@ impl pallet_subtensor::Config for Test { type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; type InitialTxRateLimit = InitialTxRateLimit; - type InitialTxDelegateTakeRateLimit = InitialTxDelegateTakeRateLimit; type InitialBurn = InitialBurn; type InitialMaxBurn = InitialMaxBurn; type InitialMinBurn = InitialMinBurn; diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 82517c49e2..074f3248a5 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -1203,7 +1203,11 @@ pub mod pallet { /// The extrinsic sets the rate limit for delegate take transactions. /// It is only callable by the root account. - /// The extrinsic will call the Subtensor pallet to set the rate limit for delegate take transactions. + /// The extrinsic will call the Subtensor pallet to set the rate limit for delegate take + /// transactions. + /// + /// Deprecated: delegate take rate limit is now configured via `pallet-rate-limiting` on the + /// delegate take group target (`GROUP_DELEGATE_TAKE`). #[pallet::call_index(45)] #[pallet::weight(( Weight::from_parts(5_019_000, 0) @@ -1212,16 +1216,14 @@ pub mod pallet { DispatchClass::Operational, Pays::Yes ))] + #[deprecated( + note = "deprecated: configure via pallet-rate-limiting::set_rate_limit(target=Group(GROUP_DELEGATE_TAKE), ...)" + )] pub fn sudo_set_tx_delegate_take_rate_limit( - origin: OriginFor, - tx_rate_limit: u64, + _origin: OriginFor, + _tx_rate_limit: u64, ) -> DispatchResult { - ensure_root(origin)?; - pallet_subtensor::Pallet::::set_tx_delegate_take_rate_limit(tx_rate_limit); - log::debug!( - "TxRateLimitDelegateTakeSet( tx_delegate_take_rate_limit: {tx_rate_limit:?} ) " - ); - Ok(()) + Err(Error::::Deprecated.into()) } /// The extrinsic sets the minimum delegate take. diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 75cce3f03d..ec46dc1027 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -105,7 +105,6 @@ parameter_types! { pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing - pub const InitialTxDelegateTakeRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -189,7 +188,6 @@ impl pallet_subtensor::Config for Test { type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; type InitialTxRateLimit = InitialTxRateLimit; - type InitialTxDelegateTakeRateLimit = InitialTxDelegateTakeRateLimit; type InitialTxChildKeyTakeRateLimit = InitialTxChildKeyTakeRateLimit; type InitialBurn = InitialBurn; type InitialMaxBurn = InitialMaxBurn; diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index a5fb053806..44a3a35df1 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -1079,33 +1079,6 @@ mod sudo_set_nominator_min_required_stake { } } -#[test] -fn test_sudo_set_tx_delegate_take_rate_limit() { - new_test_ext().execute_with(|| { - let to_be_set: u64 = 10; - let init_value: u64 = SubtensorModule::get_tx_delegate_take_rate_limit(); - assert_eq!( - AdminUtils::sudo_set_tx_delegate_take_rate_limit( - <::RuntimeOrigin>::signed(U256::from(1)), - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - SubtensorModule::get_tx_delegate_take_rate_limit(), - init_value - ); - assert_ok!(AdminUtils::sudo_set_tx_delegate_take_rate_limit( - <::RuntimeOrigin>::root(), - to_be_set - )); - assert_eq!( - SubtensorModule::get_tx_delegate_take_rate_limit(), - to_be_set - ); - }); -} - #[test] fn test_sudo_set_min_delegate_take() { new_test_ext().execute_with(|| { diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 778cde0083..8c7019ff6a 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -881,12 +881,6 @@ pub mod pallet { T::InitialTxRateLimit::get() } - /// Default value for delegate take rate limiting - #[pallet::type_value] - pub fn DefaultTxDelegateTakeRateLimit() -> u64 { - T::InitialTxDelegateTakeRateLimit::get() - } - /// Default value for chidlkey take rate limiting #[pallet::type_value] pub fn DefaultTxChildKeyTakeRateLimit() -> u64 { @@ -1836,11 +1830,6 @@ pub mod pallet { #[pallet::storage] pub type TxRateLimit = StorageValue<_, u64, ValueQuery, DefaultTxRateLimit>; - /// --- ITEM ( tx_delegate_take_rate_limit ) - #[pallet::storage] - pub type TxDelegateTakeRateLimit = - StorageValue<_, u64, ValueQuery, DefaultTxDelegateTakeRateLimit>; - /// --- ITEM ( tx_childkey_take_rate_limit ) #[pallet::storage] pub type TxChildkeyTakeRateLimit = diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index 6c7ea04bd4..706ac88560 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -186,9 +186,6 @@ mod config { /// Initial transaction rate limit. #[pallet::constant] type InitialTxRateLimit: Get; - /// Initial delegate take transaction rate limit. - #[pallet::constant] - type InitialTxDelegateTakeRateLimit: Get; /// Initial childkey take transaction rate limit. #[pallet::constant] type InitialTxChildKeyTakeRateLimit: Get; diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index f87388d49b..b5a1e8f37f 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -86,8 +86,6 @@ mod errors { UidsLengthExceedUidsInSubNet, // 32 /// A transactor exceeded the rate limit for add network transaction. NetworkTxRateLimitExceeded, - /// A transactor exceeded the rate limit for delegate transaction. - DelegateTxRateLimitExceeded, /// A transactor exceeded the rate limit for setting or swapping hotkey. HotKeySetTxRateLimitExceeded, /// A transactor exceeded the rate limit for staking. diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 49cf8dce6f..d3e5bb963a 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -109,8 +109,6 @@ mod events { MinBurnSet(NetUid, TaoCurrency), /// setting the transaction rate limit. TxRateLimitSet(u64), - /// setting the delegate take transaction rate limit. - TxDelegateTakeRateLimitSet(u64), /// setting the childkey take transaction rate limit. TxChildKeyTakeRateLimitSet(u64), /// setting the admin freeze window length (last N blocks of tempo) diff --git a/pallets/subtensor/src/staking/decrease_take.rs b/pallets/subtensor/src/staking/decrease_take.rs index 43bc633431..61c1131696 100644 --- a/pallets/subtensor/src/staking/decrease_take.rs +++ b/pallets/subtensor/src/staking/decrease_take.rs @@ -52,10 +52,6 @@ impl Pallet { // --- 4. Set the new take value. Delegates::::insert(hotkey.clone(), take); - // --- 5. Set last block for rate limiting - let block: u64 = Self::get_current_block_as_u64(); - Self::set_last_tx_block_delegate_take(&hotkey, block); - // --- 6. Emit the take value. log::debug!("TakeDecreased( coldkey:{coldkey:?}, hotkey:{hotkey:?}, take:{take:?} )"); Self::deposit_event(Event::TakeDecreased(coldkey, hotkey, take)); diff --git a/pallets/subtensor/src/staking/increase_take.rs b/pallets/subtensor/src/staking/increase_take.rs index 3a101c7e0f..4fbd446c89 100644 --- a/pallets/subtensor/src/staking/increase_take.rs +++ b/pallets/subtensor/src/staking/increase_take.rs @@ -52,19 +52,6 @@ impl Pallet { let max_take = MaxDelegateTake::::get(); ensure!(take <= max_take, Error::::DelegateTakeTooHigh); - // --- 5. Enforce the rate limit (independently on do_add_stake rate limits) - let block: u64 = Self::get_current_block_as_u64(); - ensure!( - !Self::exceeds_tx_delegate_take_rate_limit( - Self::get_last_tx_block_delegate_take(&hotkey), - block - ), - Error::::DelegateTxRateLimitExceeded - ); - - // Set last block for rate limiting - Self::set_last_tx_block_delegate_take(&hotkey, block); - // --- 6. Set the new take value. Delegates::::insert(hotkey.clone(), take); diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 4fdf87fb7b..597e8c489d 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -68,11 +68,6 @@ impl Pallet { Self::set_last_tx_block(new_hotkey, last_tx_block); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - // 9. Swap LastTxBlockDelegateTake - let last_tx_block_delegate_take: u64 = Self::get_last_tx_block_delegate_take(old_hotkey); - Self::set_last_tx_block_delegate_take(new_hotkey, last_tx_block_delegate_take); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - // 10. Swap LastTxBlockChildKeyTake let last_tx_block_child_key_take: u64 = Self::get_last_tx_block_childkey_take(old_hotkey); Self::set_last_tx_block_childkey(new_hotkey, last_tx_block_child_key_take); diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index e5c02e36f8..c5f74d70f6 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -845,8 +845,6 @@ fn test_migrate_rate_limit_keys() { SubtensorModule::set_last_tx_block(&new_last_account, 555); let new_child_account = U256::from(11); SubtensorModule::set_last_tx_block_childkey(&new_child_account, 777); - let new_delegate_account = U256::from(12); - SubtensorModule::set_last_tx_block_delegate_take(&new_delegate_account, 888); // Legacy NetworkLastRegistered entry (index 1) let mut legacy_network_key = prefix.clone(); @@ -922,11 +920,6 @@ fn test_migrate_rate_limit_keys() { "Legacy child take entry should be cleared" ); - assert_eq!( - SubtensorModule::get_last_tx_block_delegate_take(&legacy_delegate_account), - 444u64, - "Delegate take block should be migrated" - ); assert!( sp_io::storage::get(&legacy_delegate_key).is_none(), "Legacy delegate take entry should be cleared" @@ -938,11 +931,6 @@ fn test_migrate_rate_limit_keys() { 777u64, "Existing child take entry should be preserved" ); - assert_eq!( - SubtensorModule::get_last_tx_block_delegate_take(&new_delegate_account), - 888u64, - "Existing delegate take entry should be preserved" - ); }); } diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 89b2eb4a76..bc6f339988 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -180,7 +180,6 @@ parameter_types! { pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing - pub const InitialTxDelegateTakeRateLimit: u64 = 1; // 1 block take rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 1; // 1 block take rate limit for testing pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -264,7 +263,6 @@ impl crate::Config for Test { type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; type InitialTxRateLimit = InitialTxRateLimit; - type InitialTxDelegateTakeRateLimit = InitialTxDelegateTakeRateLimit; type InitialBurn = InitialBurn; type InitialMaxBurn = InitialMaxBurn; type InitialMinBurn = InitialMinBurn; diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 4146786709..47c438bbc5 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -1871,7 +1871,7 @@ fn test_delegate_take_can_be_increased() { SubtensorModule::get_min_delegate_take() ); - step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); + step_block(1); // Coldkey / hotkey 0 decreases take to 12.5% assert_ok!(SubtensorModule::do_increase_take( @@ -1945,7 +1945,7 @@ fn test_delegate_take_can_be_increased_to_limit() { SubtensorModule::get_min_delegate_take() ); - step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); + step_block(1); // Coldkey / hotkey 0 tries to increase take to InitialDefaultDelegateTake+1 assert_ok!(SubtensorModule::do_increase_take( @@ -2002,129 +2002,6 @@ fn test_delegate_take_can_not_be_increased_beyond_limit() { }); } -// Test rate-limiting on increase_take -#[test] -fn test_rate_limits_enforced_on_increase_take() { - new_test_ext(1).execute_with(|| { - // Make account - let hotkey0 = U256::from(1); - let coldkey0 = U256::from(3); - - // Add balance - SubtensorModule::add_balance_to_coldkey_account(&coldkey0, 100000); - - // Register the neuron to a new network - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey0, coldkey0, 124124); - - // Coldkey / hotkey 0 become delegates with 9% take - Delegates::::insert(hotkey0, SubtensorModule::get_min_delegate_take()); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - - // Increase take first time - assert_ok!(SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - SubtensorModule::get_min_delegate_take() + 1 - )); - - // Increase again - assert_eq!( - SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - SubtensorModule::get_min_delegate_take() + 2 - ), - Err(Error::::DelegateTxRateLimitExceeded.into()) - ); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() + 1 - ); - - step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); - - // Can increase after waiting - assert_ok!(SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - SubtensorModule::get_min_delegate_take() + 2 - )); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() + 2 - ); - }); -} - -// Test rate-limiting on an increase take just after a decrease take -// Prevents a Validator from decreasing take and then increasing it immediately after. -#[test] -fn test_rate_limits_enforced_on_decrease_before_increase_take() { - new_test_ext(1).execute_with(|| { - // Make account - let hotkey0 = U256::from(1); - let coldkey0 = U256::from(3); - - // Add balance - SubtensorModule::add_balance_to_coldkey_account(&coldkey0, 100000); - - // Register the neuron to a new network - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey0, coldkey0, 124124); - - // Coldkey / hotkey 0 become delegates with 9% take - Delegates::::insert(hotkey0, SubtensorModule::get_min_delegate_take() + 1); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() + 1 - ); - - // Decrease take - assert_ok!(SubtensorModule::do_decrease_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - SubtensorModule::get_min_delegate_take() - )); // Verify decrease - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - - // Increase take immediately after - assert_eq!( - SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - SubtensorModule::get_min_delegate_take() + 1 - ), - Err(Error::::DelegateTxRateLimitExceeded.into()) - ); // Verify no change - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - - step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); - - // Can increase after waiting - assert_ok!(SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - SubtensorModule::get_min_delegate_take() + 1 - )); // Verify increase - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() + 1 - ); - }); -} - // cargo test --package pallet-subtensor --lib -- tests::staking::test_get_total_delegated_stake_after_unstaking --exact --show-output #[test] fn test_get_total_delegated_stake_after_unstaking() { diff --git a/pallets/subtensor/src/tests/swap_hotkey.rs b/pallets/subtensor/src/tests/swap_hotkey.rs index 71191d1951..99a615ec00 100644 --- a/pallets/subtensor/src/tests/swap_hotkey.rs +++ b/pallets/subtensor/src/tests/swap_hotkey.rs @@ -1391,13 +1391,10 @@ fn test_swap_hotkey_swap_rate_limits() { SubtensorModule::add_balance_to_coldkey_account(&coldkey, u64::MAX); let last_tx_block = 123; - let delegate_take_block = 4567; let child_key_take_block = 8910; // Set the last tx block for the old hotkey SubtensorModule::set_last_tx_block(&old_hotkey, last_tx_block); - // Set the last delegate take block for the old hotkey - SubtensorModule::set_last_tx_block_delegate_take(&old_hotkey, delegate_take_block); // Set last childkey take block for the old hotkey SubtensorModule::set_last_tx_block_childkey(&old_hotkey, child_key_take_block); @@ -1414,10 +1411,6 @@ fn test_swap_hotkey_swap_rate_limits() { SubtensorModule::get_last_tx_block(&new_hotkey), last_tx_block ); - assert_eq!( - SubtensorModule::get_last_tx_block_delegate_take(&new_hotkey), - delegate_take_block - ); assert_eq!( SubtensorModule::get_last_tx_block_childkey_take(&new_hotkey), child_key_take_block diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index 6e423c1269..dc2af38cb6 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -1439,7 +1439,6 @@ fn test_swap_hotkey_swap_rate_limits() { let coldkey = U256::from(3); let last_tx_block = 123; - let delegate_take_block = 4567; let child_key_take_block = 8910; let netuid = add_dynamic_network(&old_hotkey, &coldkey); @@ -1447,8 +1446,6 @@ fn test_swap_hotkey_swap_rate_limits() { // Set the last tx block for the old hotkey SubtensorModule::set_last_tx_block(&old_hotkey, last_tx_block); - // Set the last delegate take block for the old hotkey - SubtensorModule::set_last_tx_block_delegate_take(&old_hotkey, delegate_take_block); // Set last childkey take block for the old hotkey SubtensorModule::set_last_tx_block_childkey(&old_hotkey, child_key_take_block); @@ -1466,10 +1463,6 @@ fn test_swap_hotkey_swap_rate_limits() { SubtensorModule::get_last_tx_block(&new_hotkey), last_tx_block ); - assert_eq!( - SubtensorModule::get_last_tx_block_delegate_take(&new_hotkey), - delegate_take_block - ); assert_eq!( SubtensorModule::get_last_tx_block_childkey_take(&new_hotkey), child_key_take_block diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 05c1af7d6f..d6c438a76c 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -377,13 +377,6 @@ impl Pallet { TxRateLimit::::put(tx_rate_limit); Self::deposit_event(Event::TxRateLimitSet(tx_rate_limit)); } - pub fn get_tx_delegate_take_rate_limit() -> u64 { - TxDelegateTakeRateLimit::::get() - } - pub fn set_tx_delegate_take_rate_limit(tx_rate_limit: u64) { - TxDelegateTakeRateLimit::::put(tx_rate_limit); - Self::deposit_event(Event::TxDelegateTakeRateLimitSet(tx_rate_limit)); - } pub fn set_min_delegate_take(take: u16) { MinDelegateTake::::put(take); Self::deposit_event(Event::MinDelegateTakeSet(take)); diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index 4a88c5fe73..18d0728bf8 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -229,15 +229,6 @@ impl Pallet { pub fn remove_last_tx_block_delegate_take(key: &T::AccountId) { Self::remove_rate_limited_last_block(&RateLimitKey::LastTxBlockDelegateTake(key.clone())) } - pub fn set_last_tx_block_delegate_take(key: &T::AccountId, block: u64) { - Self::set_rate_limited_last_block( - &RateLimitKey::LastTxBlockDelegateTake(key.clone()), - block, - ); - } - pub fn get_last_tx_block_delegate_take(key: &T::AccountId) -> u64 { - Self::get_rate_limited_last_block(&RateLimitKey::LastTxBlockDelegateTake(key.clone())) - } pub fn get_last_tx_block_childkey_take(key: &T::AccountId) -> u64 { Self::get_rate_limited_last_block(&RateLimitKey::LastTxBlockChildKeyTake(key.clone())) } @@ -256,14 +247,6 @@ impl Pallet { return false; } - current_block.saturating_sub(prev_tx_block) <= rate_limit - } - pub fn exceeds_tx_delegate_take_rate_limit(prev_tx_block: u64, current_block: u64) -> bool { - let rate_limit: u64 = Self::get_tx_delegate_take_rate_limit(); - if rate_limit == 0 || prev_tx_block == 0 { - return false; - } - current_block.saturating_sub(prev_tx_block) <= rate_limit } } diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 2ebe15febe..2117371456 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -172,7 +172,6 @@ parameter_types! { pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing - pub const InitialTxDelegateTakeRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -256,7 +255,6 @@ impl pallet_subtensor::Config for Test { type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; type InitialTxRateLimit = InitialTxRateLimit; - type InitialTxDelegateTakeRateLimit = InitialTxDelegateTakeRateLimit; type InitialTxChildKeyTakeRateLimit = InitialTxChildKeyTakeRateLimit; type InitialBurn = InitialBurn; type InitialMaxBurn = InitialMaxBurn; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index c9336dee55..c105e06b97 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1111,7 +1111,6 @@ impl pallet_subtensor::Config for Runtime { type MinBurnUpperBound = MinBurnUpperBound; type MaxBurnLowerBound = MaxBurnLowerBound; type InitialTxRateLimit = SubtensorInitialTxRateLimit; - type InitialTxDelegateTakeRateLimit = SubtensorInitialTxDelegateTakeRateLimit; type InitialTxChildKeyTakeRateLimit = SubtensorInitialTxChildKeyTakeRateLimit; type InitialMaxChildKeyTake = SubtensorInitialMaxChildKeyTake; type InitialRAORecycledForRegistration = SubtensorInitialRAORecycledForRegistration; diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 42856fcd6b..4723eb194d 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -1359,7 +1359,8 @@ mod tests { take: 5, }); let origin = RuntimeOrigin::signed(account(21)); - let legacy = || !SubtensorModule::exceeds_tx_delegate_take_rate_limit(now - 1, now); + // FIXME exceeds_tx_delegate_take_rate_limit is removed + // let legacy = || !SubtensorModule::exceeds_tx_delegate_take_rate_limit(now - 1, now); parity_check(now, call, origin, None, None, legacy); }); } diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index caf9c67779..dc7ed88f89 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -39,8 +39,8 @@ pub(crate) mod legacy; /// /// Legacy note: historically, all rate-limit setters were `Root`-only except /// `admin-utils::sudo_set_serving_rate_limit` (subnet-owner-or-root). We preserve that behavior by -/// requiring a `scope` value when using the [`LimitSettingRule::RootOrSubnetOwnerAdminWindow`] rule and -/// validating subnet ownership against that `scope` (`netuid`). +/// requiring a `scope` value when using the [`LimitSettingRule::RootOrSubnetOwnerAdminWindow`] rule +/// and validating subnet ownership against that `scope` (`netuid`). #[derive( Encode, Decode, From c2b9df055395173fa1aa075a42a66f73bd5af870 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 9 Jan 2026 15:35:40 +0100 Subject: [PATCH 58/95] Add integration tests for delegate take rate limits --- pallets/admin-utils/src/lib.rs | 6 +- runtime/src/migrations/rate_limiting.rs | 11 +- runtime/src/rate_limiting/mod.rs | 3 +- runtime/tests/rate_limiting.rs | 164 +++++++++++++++++++++++- 4 files changed, 172 insertions(+), 12 deletions(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 074f3248a5..f1e88871f1 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -1203,9 +1203,9 @@ pub mod pallet { /// The extrinsic sets the rate limit for delegate take transactions. /// It is only callable by the root account. - /// The extrinsic will call the Subtensor pallet to set the rate limit for delegate take - /// transactions. - /// + /// The extrinsic will call the Subtensor pallet to set the rate limit for delegate take + /// transactions. + /// /// Deprecated: delegate take rate limit is now configured via `pallet-rate-limiting` on the /// delegate take group target (`GROUP_DELEGATE_TAKE`). #[pallet::call_index(45)] diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 4723eb194d..b664d6aef5 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -1352,15 +1352,20 @@ mod tests { RateLimitKey::LastTxBlockDelegateTake(hot.clone()), now - 1, ); - pallet_subtensor::TxDelegateTakeRateLimit::::put(span); + put_legacy_value(b"TxDelegateTakeRateLimit", span); let call = RuntimeCall::SubtensorModule(SubtensorCall::increase_take { hotkey: hot.clone(), take: 5, }); let origin = RuntimeOrigin::signed(account(21)); - // FIXME exceeds_tx_delegate_take_rate_limit is removed - // let legacy = || !SubtensorModule::exceeds_tx_delegate_take_rate_limit(now - 1, now); + let legacy = || { + let last = now - 1; + if span == 0 || last == 0 { + return true; + } + now - last > span + }; parity_check(now, call, origin, None, None, legacy); }); } diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index dc7ed88f89..8eebfaf224 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -218,7 +218,8 @@ impl RateLimitUsageResolver::Account(new_hotkey.clone()), ]) } - SubtensorCall::increase_take { hotkey, .. } => { + SubtensorCall::increase_take { hotkey, .. } + | SubtensorCall::decrease_take { hotkey, .. } => { Some(vec![RateLimitUsageKey::::Account( hotkey.clone(), )]) diff --git a/runtime/tests/rate_limiting.rs b/runtime/tests/rate_limiting.rs index ccd6182c8f..68b73ead82 100644 --- a/runtime/tests/rate_limiting.rs +++ b/runtime/tests/rate_limiting.rs @@ -4,8 +4,9 @@ use codec::Encode; use frame_support::assert_ok; use node_subtensor_runtime::{ - Executive, Runtime, RuntimeCall, SignedPayload, System, TransactionExtensions, - UncheckedExtrinsic, check_nonce, sudo_wrapper, transaction_payment_wrapper, + Executive, Runtime, RuntimeCall, SignedPayload, SubtensorInitialTxDelegateTakeRateLimit, + System, TransactionExtensions, UncheckedExtrinsic, check_nonce, sudo_wrapper, + transaction_payment_wrapper, }; use sp_core::{Pair, sr25519}; use sp_runtime::{ @@ -101,7 +102,7 @@ fn register_network_is_rate_limited_after_migration() { assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, call_b.clone()); // Migration sets register-network limit to 4 days (28_800 blocks). - let limit = start_block.saturating_add(28_800); + let limit = start_block + 28_800; // Should still be rate-limited. System::set_block_number(limit - 1); @@ -114,7 +115,7 @@ fn register_network_is_rate_limited_after_migration() { // Both calls share the same usage key and window. assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, call_a.clone()); - System::set_block_number(limit.saturating_add(28_800)); + System::set_block_number(limit + 28_800); assert_extrinsic_ok(&coldkey, &coldkey_pair, call_a); }); } @@ -185,7 +186,7 @@ fn serving_is_rate_limited_after_migration() { assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_prometheus.clone()); // Migration sets serving limit to 50 blocks by default. - let limit = start_block.saturating_add(50); + let limit = start_block + 50; // Should still be rate-limited. System::set_block_number(limit - 1); @@ -202,3 +203,156 @@ fn serving_is_rate_limited_after_migration() { assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_prometheus); }); } + +#[test] +fn delegate_take_increase_is_rate_limited_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[6u8; 32]); + let hotkey_pair = sr25519::Pair::from_seed(&[7u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from(hotkey_pair.public()); + let balance = 10_000_000_000_000_u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. + Executive::execute_on_runtime_upgrade(); + + assert_extrinsic_ok( + &coldkey, + &coldkey_pair, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { + hotkey: hotkey.clone(), + }), + ); + + // Seed current take so increase_take passes take checks. + pallet_subtensor::Delegates::::insert(&hotkey, 1u16); + + let increase_once = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::increase_take { + hotkey: hotkey.clone(), + take: 2u16, + }); + let increase_twice = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::increase_take { + hotkey: hotkey.clone(), + take: 3u16, + }); + + let start_block = System::block_number(); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, increase_once); + + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase_twice.clone()); + + let limit = SubtensorInitialTxDelegateTakeRateLimit::get(); + let limit_block = start_block + limit.saturated_into::(); + let allowed_block = limit_block + 1; + + System::set_block_number(limit_block - 1); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase_twice.clone()); + + System::set_block_number(allowed_block); + assert_extrinsic_ok(&coldkey, &coldkey_pair, increase_twice); + }); +} + +#[test] +fn delegate_take_decrease_is_not_rate_limited_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[10u8; 32]); + let hotkey_pair = sr25519::Pair::from_seed(&[11u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from(hotkey_pair.public()); + let balance = 10_000_000_000_000_u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. + Executive::execute_on_runtime_upgrade(); + + assert_extrinsic_ok( + &coldkey, + &coldkey_pair, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { + hotkey: hotkey.clone(), + }), + ); + + // Seed current take so decreases are valid and deterministic. + pallet_subtensor::Delegates::::insert(&hotkey, 3u16); + + let decrease_once = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::decrease_take { + hotkey: hotkey.clone(), + take: 2u16, + }); + let decrease_twice = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::decrease_take { + hotkey: hotkey.clone(), + take: 1u16, + }); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, decrease_once); + assert_extrinsic_ok(&coldkey, &coldkey_pair, decrease_twice); + }); +} + +#[test] +fn delegate_take_decrease_blocks_immediate_increase_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[8u8; 32]); + let hotkey_pair = sr25519::Pair::from_seed(&[9u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from(hotkey_pair.public()); + let balance = 10_000_000_000_000_u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. + Executive::execute_on_runtime_upgrade(); + + assert_extrinsic_ok( + &coldkey, + &coldkey_pair, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { + hotkey: hotkey.clone(), + }), + ); + + // Seed current take so decrease then increase remains valid. + pallet_subtensor::Delegates::::insert(&hotkey, 2u16); + + let decrease = RuntimeCall::SubtensorModule(pallet_subtensor::Call::decrease_take { + hotkey: hotkey.clone(), + take: 1u16, + }); + let increase = RuntimeCall::SubtensorModule(pallet_subtensor::Call::increase_take { + hotkey: hotkey.clone(), + take: 2u16, + }); + + let start_block = System::block_number(); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, decrease); + + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase.clone()); + + let limit = SubtensorInitialTxDelegateTakeRateLimit::get(); + let limit_block = start_block + limit.saturated_into::(); + let allowed_block = limit_block + 1; + + System::set_block_number(limit_block - 1); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase.clone()); + + System::set_block_number(allowed_block); + assert_extrinsic_ok(&coldkey, &coldkey_pair, increase); + }); +} From c8018ca1ba89cc84e97b6bc59867a0879346e967 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 12 Jan 2026 14:19:00 +0100 Subject: [PATCH 59/95] Remove legacy weights-set rate limit setter --- pallets/admin-utils/src/lib.rs | 27 +- pallets/admin-utils/src/tests/mod.rs | 39 --- pallets/subtensor/src/benchmarks.rs | 1 - .../migrations/migrate_create_root_network.rs | 3 - pallets/subtensor/src/subnets/weights.rs | 96 ++---- pallets/subtensor/src/tests/children.rs | 8 - pallets/subtensor/src/tests/coinbase.rs | 9 - pallets/subtensor/src/tests/epoch.rs | 10 - pallets/subtensor/src/tests/epoch_logs.rs | 1 - pallets/subtensor/src/tests/mechanism.rs | 99 ------ pallets/subtensor/src/tests/mock.rs | 2 - pallets/subtensor/src/tests/networks.rs | 17 - pallets/subtensor/src/tests/staking.rs | 1 - pallets/subtensor/src/tests/weights.rs | 323 ------------------ pallets/subtensor/src/utils/misc.rs | 11 +- runtime/src/migrations/rate_limiting.rs | 5 +- 16 files changed, 42 insertions(+), 610 deletions(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index f1e88871f1..6abf1601d0 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -341,29 +341,22 @@ pub mod pallet { /// The extrinsic sets the weights set rate limit for a subnet. /// It is only callable by the root account. /// The extrinsic will call the Subtensor pallet to set the weights set rate limit. + /// + /// Deprecated: weights set rate limit is now configured via `pallet-rate-limiting` on the + /// weights set group target (`GROUP_WEIGHTS_SUBNET`) with `scope = Some(netuid)`. #[pallet::call_index(7)] #[pallet::weight(Weight::from_parts(15_060_000, 0) .saturating_add(::DbWeight::get().reads(1_u64)) .saturating_add(::DbWeight::get().writes(1_u64)))] + #[deprecated( + note = "deprecated: configure via pallet-rate-limiting::set_rate_limit(target=Group(GROUP_WEIGHTS_SUBNET), scope=Some(netuid), ...)" + )] pub fn sudo_set_weights_set_rate_limit( - origin: OriginFor, - netuid: NetUid, - weights_set_rate_limit: u64, + _origin: OriginFor, + _netuid: NetUid, + _weights_set_rate_limit: u64, ) -> DispatchResult { - ensure_root(origin)?; - - ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), - Error::::SubnetDoesNotExist - ); - pallet_subtensor::Pallet::::set_weights_set_rate_limit( - netuid, - weights_set_rate_limit, - ); - log::debug!( - "WeightsSetRateLimitSet( netuid: {netuid:?} weights_set_rate_limit: {weights_set_rate_limit:?} ) " - ); - Ok(()) + Err(Error::::Deprecated.into()) } /// The extrinsic sets the adjustment interval for a subnet. diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index 44a3a35df1..90b888bc02 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -269,45 +269,6 @@ fn test_sudo_set_weights_version_key_rate_limit_root() { }); } -#[test] -fn test_sudo_set_weights_set_rate_limit() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 10; - add_network(netuid, 10); - let init_value: u64 = SubtensorModule::get_weights_set_rate_limit(netuid); - assert_eq!( - AdminUtils::sudo_set_weights_set_rate_limit( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_weights_set_rate_limit( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!( - SubtensorModule::get_weights_set_rate_limit(netuid), - init_value - ); - assert_ok!(AdminUtils::sudo_set_weights_set_rate_limit( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!( - SubtensorModule::get_weights_set_rate_limit(netuid), - to_be_set - ); - }); -} - #[test] fn test_sudo_set_adjustment_interval() { new_test_ext().execute_with(|| { diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index edc6358a02..4fc206d6cf 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -476,7 +476,6 @@ mod pallet_benchmarks { Subtensor::::set_network_registration_allowed(netuid, true); Subtensor::::set_network_pow_registration_allowed(netuid, true); Subtensor::::set_commit_reveal_weights_enabled(netuid, true); - Subtensor::::set_weights_set_rate_limit(netuid, 0); let block_number: u64 = Subtensor::::get_current_block_as_u64(); let (nonce, work) = diff --git a/pallets/subtensor/src/migrations/migrate_create_root_network.rs b/pallets/subtensor/src/migrations/migrate_create_root_network.rs index 6cca34f815..599f7feb0e 100644 --- a/pallets/subtensor/src/migrations/migrate_create_root_network.rs +++ b/pallets/subtensor/src/migrations/migrate_create_root_network.rs @@ -73,9 +73,6 @@ pub fn migrate_create_root_network() -> Weight { // Set target registrations for validators as 1 per block TargetRegistrationsPerInterval::::insert(NetUid::ROOT, 1); - // TODO: Consider if WeightsSetRateLimit should be set - // WeightsSetRateLimit::::insert(NetUid::ROOT, 7200); - // Accrue weight for database writes weight.saturating_accrue(T::DbWeight::get().writes(7)); diff --git a/pallets/subtensor/src/subnets/weights.rs b/pallets/subtensor/src/subnets/weights.rs index 56acbef9c7..ac96f68720 100644 --- a/pallets/subtensor/src/subnets/weights.rs +++ b/pallets/subtensor/src/subnets/weights.rs @@ -87,14 +87,8 @@ impl Pallet { Error::::HotKeyNotRegisteredInSubNet ); - // 4. Check that the commit rate does not exceed the allowed frequency. let commit_block = Self::get_current_block_as_u64(); let neuron_uid = Self::get_uid_for_net_and_hotkey(netuid, &who)?; - ensure!( - // Rate limiting should happen per sub-subnet, so use netuid_index here - Self::check_rate_limit(netuid_index, neuron_uid, commit_block), - Error::::CommittingWeightsTooFast - ); // 5. Calculate the reveal blocks based on network tempo and reveal period. let (first_reveal_block, last_reveal_block) = Self::get_reveal_blocks(netuid, commit_block); @@ -216,43 +210,41 @@ impl Pallet { /// ---- Commits a timelocked, encrypted weight payload (Commit-Reveal v3). /// /// # Args - /// * `origin` (`::RuntimeOrigin`): + /// * `origin` (`::RuntimeOrigin`): /// The signed origin of the committing hotkey. - /// * `netuid` (`NetUid` = `u16`): + /// * `netuid` (`NetUid` = `u16`): /// Unique identifier for the subnet on which the commit is made. - /// * `commit` (`BoundedVec>`): - /// The encrypted weight payload, produced as follows: - /// 1. Build a [`WeightsPayload`] structure. - /// 2. SCALE-encode it (`parity_scale_codec::Encode`). - /// 3. Encrypt it following the steps - /// [here](https://github.com/ideal-lab5/tle/blob/f8e6019f0fb02c380ebfa6b30efb61786dede07b/timelock/src/tlock.rs#L283-L336) to - /// produce a [`TLECiphertext`]. + /// * `commit` (`BoundedVec>`): + /// The encrypted weight payload, produced as follows: + /// 1. Build a [`WeightsPayload`] structure. + /// 2. SCALE-encode it (`parity_scale_codec::Encode`). + /// 3. Encrypt it following the steps + /// [here](https://github.com/ideal-lab5/tle/blob/f8e6019f0fb02c380ebfa6b30efb61786dede07b/timelock/src/tlock.rs#L283-L336) to + /// produce a [`TLECiphertext`]. /// 4. Compress & serialise. - /// * `reveal_round` (`u64`): - /// DRAND round whose output becomes known during epoch `n + 1`; the payload + /// * `reveal_round` (`u64`): + /// DRAND round whose output becomes known during epoch `n + 1`; the payload /// must be revealed in that epoch. - /// * `commit_reveal_version` (`u16`): - /// Version tag that **must** match [`get_commit_reveal_weights_version`] for + /// * `commit_reveal_version` (`u16`): + /// Version tag that **must** match [`get_commit_reveal_weights_version`] for /// the call to succeed. Used to gate runtime upgrades. /// /// # Behaviour - /// 1. Verifies the caller’s signature and registration on `netuid`. + /// 1. Verifies the caller’s signature and registration on `netuid`. /// 2. Ensures commit-reveal is enabled **and** the supplied - /// `commit_reveal_version` is current. - /// 3. Enforces per-neuron rate-limiting via [`Pallet::check_rate_limit`]. - /// 4. Rejects the call when the hotkey already has ≥ 10 unrevealed commits in - /// the current epoch. - /// 5. Appends `(hotkey, commit_block, commit, reveal_round)` to - /// `TimelockedWeightCommits[netuid][epoch]`. - /// 6. Emits `TimelockedWeightsCommitted` with the Blake2 hash of `commit`. - /// 7. Updates `LastUpdateForUid` so subsequent rate-limit checks include this + /// `commit_reveal_version` is current. + /// 3. Rejects the call when the hotkey already has ≥ 10 unrevealed commits in + /// the current epoch. + /// 4. Appends `(hotkey, commit_block, commit, reveal_round)` to + /// `TimelockedWeightCommits[netuid][epoch]`. + /// 5. Emits `TimelockedWeightsCommitted` with the Blake2 hash of `commit`. + /// 6. Updates `LastUpdateForUid` so subsequent rate-limit checks include this /// commit. /// /// # Raises - /// * `CommitRevealDisabled` – Commit-reveal is disabled on `netuid`. - /// * `IncorrectCommitRevealVersion` – Provided version ≠ runtime version. - /// * `HotKeyNotRegisteredInSubNet` – Caller’s hotkey is not registered. - /// * `CommittingWeightsTooFast` – Caller exceeds commit-rate limit. + /// * `CommitRevealDisabled` – Commit-reveal is disabled on `netuid`. + /// * `IncorrectCommitRevealVersion` – Provided version ≠ runtime version. + /// * `HotKeyNotRegisteredInSubNet` – Caller’s hotkey is not registered. /// * `TooManyUnrevealedCommits` – Caller already has 10 unrevealed commits. /// /// # Events @@ -329,13 +321,8 @@ impl Pallet { Error::::HotKeyNotRegisteredInSubNet ); - // 5. Check that the commit rate does not exceed the allowed frequency. let commit_block = Self::get_current_block_as_u64(); let neuron_uid = Self::get_uid_for_net_and_hotkey(netuid, &who)?; - ensure!( - Self::check_rate_limit(netuid_index, neuron_uid, commit_block), - Error::::CommittingWeightsTooFast - ); // 6. Retrieve or initialize the VecDeque of commits for the hotkey. let cur_block = Self::get_current_block_as_u64(); @@ -798,16 +785,9 @@ impl Pallet { Error::::IncorrectWeightVersionKey ); - // --- 9. Ensure the uid is not setting weights faster than the weights_set_rate_limit. let neuron_uid = Self::get_uid_for_net_and_hotkey(netuid, &hotkey)?; - let current_block: u64 = Self::get_current_block_as_u64(); - if !Self::get_commit_reveal_weights_enabled(netuid) { - ensure!( - // Rate limit should apply per sub-subnet, so use netuid_index here - Self::check_rate_limit(netuid_index, neuron_uid, current_block), - Error::::SettingWeightsTooFast - ); - } + let current_block = Self::get_current_block_as_u64(); + // --- 10. Check that the neuron uid is an allowed validator permitted to set non-self weights. ensure!( Self::check_validator_permit(netuid, neuron_uid, &uids, &values), @@ -1101,30 +1081,6 @@ impl Pallet { network_version_key == 0 || version_key >= network_version_key } - /// Checks if the neuron has set weights within the weights_set_rate_limit. - /// - pub fn check_rate_limit( - netuid_index: NetUidStorageIndex, - neuron_uid: u16, - current_block: u64, - ) -> bool { - let maybe_netuid_and_subid = Self::get_netuid_and_subid(netuid_index); - if let Ok((netuid, _)) = maybe_netuid_and_subid - && Self::is_uid_exist_on_network(netuid, neuron_uid) - { - // --- 1. Ensure that the diff between current and last_set weights is greater than limit. - let last_set_weights: u64 = Self::get_last_update_for_uid(netuid_index, neuron_uid); - if last_set_weights == 0 { - return true; - } // (Storage default) Never set weights. - return current_block.saturating_sub(last_set_weights) - >= Self::get_weights_set_rate_limit(netuid); - } - - // --- 3. Non registered peers cant pass. Neither can non-existing mecid - false - } - /// Checks for any invalid uids on this network. pub fn contains_invalid_uids(netuid: NetUid, uids: &[u16]) -> bool { for uid in uids { diff --git a/pallets/subtensor/src/tests/children.rs b/pallets/subtensor/src/tests/children.rs index e8be57f021..146c94516a 100644 --- a/pallets/subtensor/src/tests/children.rs +++ b/pallets/subtensor/src/tests/children.rs @@ -2664,8 +2664,6 @@ fn test_childkey_set_weights_single_parent() { 1_000_000.into(), ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - // Set parent-child relationship mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX, child)]); @@ -2760,8 +2758,6 @@ fn test_set_weights_no_parent() { stake_to_give_child.into(), ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - // Has stake and no parent step_block(7200 + 1); @@ -2863,7 +2859,6 @@ fn test_childkey_take_drain() { &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); @@ -3659,9 +3654,6 @@ fn test_dynamic_parent_child_relationships() { let values: Vec = vec![65535, 65535, 65535]; // Set equal weights for all hotkeys let version_key = SubtensorModule::get_weights_version_key(netuid); - // Ensure we can set weights without rate limiting - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - assert_ok!(SubtensorModule::set_weights( origin, netuid, diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index f6c92c8079..99121f3353 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -2468,8 +2468,6 @@ fn test_distribute_emission_zero_emission() { let init_stake: u64 = 100_000_000_000_000; let tempo = 2; SubtensorModule::set_tempo(netuid, tempo); - // Set weight-set limit to 0. - SubtensorModule::set_weights_set_rate_limit(netuid, 0); register_ok_neuron(netuid, hotkey, coldkey, 0); register_ok_neuron(netuid, miner_hk, miner_ck, 0); @@ -2556,8 +2554,6 @@ fn test_run_coinbase_not_started() { let init_stake: u64 = 100_000_000_000_000; let tempo = 2; SubtensorModule::set_tempo(netuid, tempo); - // Set weight-set limit to 0. - SubtensorModule::set_weights_set_rate_limit(netuid, 0); let reserve = init_stake * 1000; mock::setup_reserves(netuid, reserve.into(), reserve.into()); @@ -2650,8 +2646,6 @@ fn test_run_coinbase_not_started_start_after() { let init_stake: u64 = 100_000_000_000_000; let tempo = 2; SubtensorModule::set_tempo(netuid, tempo); - // Set weight-set limit to 0. - SubtensorModule::set_weights_set_rate_limit(netuid, 0); register_ok_neuron(netuid, hotkey, coldkey, 0); register_ok_neuron(netuid, miner_hk, miner_ck, 0); @@ -3018,7 +3012,6 @@ fn test_mining_emission_distribution_with_no_root_sell() { &miner_coldkey, stake + ExistentialDeposit::get(), ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); step_block(subnet_tempo); SubnetOwnerCut::::set(u16::MAX / 10); // There are two validators and three neurons @@ -3213,7 +3206,6 @@ fn test_mining_emission_distribution_with_root_sell() { &miner_coldkey, stake + ExistentialDeposit::get(), ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); step_block(subnet_tempo); SubnetOwnerCut::::set(u16::MAX / 10); // There are two validators and three neurons @@ -3865,7 +3857,6 @@ fn test_pending_emission_start_call_not_done() { &validator_coldkey, stake + ExistentialDeposit::get(), ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); step_block(subnet_tempo); SubnetOwnerCut::::set(u16::MAX / 10); // There are two validators and three neurons diff --git a/pallets/subtensor/src/tests/epoch.rs b/pallets/subtensor/src/tests/epoch.rs index 32f754f78d..c0be00b0fd 100644 --- a/pallets/subtensor/src/tests/epoch.rs +++ b/pallets/subtensor/src/tests/epoch.rs @@ -568,7 +568,6 @@ fn test_1_graph() { stake_amount + ExistentialDeposit::get(), ); register_ok_neuron(netuid, hotkey, coldkey, 1); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); assert_ok!(SubtensorModule::add_stake( RuntimeOrigin::signed(coldkey), @@ -1017,7 +1016,6 @@ fn test_bonds() { assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); SubtensorModule::set_max_registrations_per_block( netuid, n ); SubtensorModule::set_target_registrations_per_interval(netuid, n); - SubtensorModule::set_weights_set_rate_limit( netuid, 0 ); SubtensorModule::set_min_allowed_weights( netuid, 1 ); SubtensorModule::set_bonds_penalty(netuid, u16::MAX); @@ -1576,7 +1574,6 @@ fn test_outdated_weights() { let stake: u64 = 1; add_network_disable_commit_reveal(netuid, tempo, 0); SubtensorModule::set_max_allowed_uids(netuid, n); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_max_registrations_per_block(netuid, n); SubtensorModule::set_target_registrations_per_interval(netuid, n); SubtensorModule::set_min_allowed_weights(netuid, 0); @@ -1765,7 +1762,6 @@ fn test_zero_weights() { let stake: u64 = 1; add_network_disable_commit_reveal(netuid, tempo, 0); SubtensorModule::set_max_allowed_uids(netuid, n); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_max_registrations_per_block(netuid, n); SubtensorModule::set_target_registrations_per_interval(netuid, n); SubtensorModule::set_min_allowed_weights(netuid, 0); @@ -1967,7 +1963,6 @@ fn test_deregistered_miner_bonds() { let stake: u64 = 1; add_network_disable_commit_reveal(netuid, high_tempo, 0); SubtensorModule::set_max_allowed_uids(netuid, n); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_max_registrations_per_block(netuid, n); SubtensorModule::set_target_registrations_per_interval(netuid, n); SubtensorModule::set_min_allowed_weights(netuid, 0); @@ -2688,7 +2683,6 @@ fn setup_yuma_3_scenario(netuid: NetUid, n: u16, sparse: bool, max_stake: u64, s assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); SubtensorModule::set_max_registrations_per_block(netuid, n); SubtensorModule::set_target_registrations_per_interval(netuid, n); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_min_allowed_weights(netuid, 1); SubtensorModule::set_bonds_penalty(netuid, 0); SubtensorModule::set_alpha_sigmoid_steepness(netuid, 1000); @@ -3624,7 +3618,6 @@ fn test_epoch_masks_incoming_to_sniped_uid_prevents_inheritance() { /* validator weights uid‑1 */ SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); assert_ok!(SubtensorModule::set_weights( RuntimeOrigin::signed(val_hot), netuid, @@ -3696,7 +3689,6 @@ fn test_epoch_no_mask_when_commit_reveal_disabled() { 1_000.into(), ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); assert_ok!(SubtensorModule::set_weights( RuntimeOrigin::signed(hot), netuid, @@ -3789,7 +3781,6 @@ fn test_epoch_does_not_mask_outside_window_but_masks_inside() { /* vote */ SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); assert_ok!(SubtensorModule::set_weights( RuntimeOrigin::signed(v_hot), netuid, @@ -3836,7 +3827,6 @@ fn test_last_update_size_mismatch() { stake_amount + ExistentialDeposit::get(), ); register_ok_neuron(netuid, hotkey, coldkey, 1); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); assert_ok!(SubtensorModule::add_stake( RuntimeOrigin::signed(coldkey), diff --git a/pallets/subtensor/src/tests/epoch_logs.rs b/pallets/subtensor/src/tests/epoch_logs.rs index 38e3a1b734..420a9e0eac 100644 --- a/pallets/subtensor/src/tests/epoch_logs.rs +++ b/pallets/subtensor/src/tests/epoch_logs.rs @@ -62,7 +62,6 @@ fn setup_epoch(neurons: Vec, mechanism_count: u8) { SubnetworkN::::insert(netuid, network_n); ActivityCutoff::::insert(netuid, ACTIVITY_CUTOFF); Tempo::::insert(netuid, TEMPO); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); MechanismCountCurrent::::insert(netuid, MechId::from(mechanism_count)); // Setup neurons diff --git a/pallets/subtensor/src/tests/mechanism.rs b/pallets/subtensor/src/tests/mechanism.rs index 9e6450e09c..d096cd0515 100644 --- a/pallets/subtensor/src/tests/mechanism.rs +++ b/pallets/subtensor/src/tests/mechanism.rs @@ -1062,7 +1062,6 @@ fn test_commit_reveal_mechanism_weights_ok() { // Enable commit-reveal path and make caller a validator with stake SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::set_validator_permit_for_uid(netuid, uid1, true); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); SubtensorModule::add_balance_to_coldkey_account(&ck1, 1); @@ -1146,7 +1145,6 @@ fn test_commit_reveal_above_mechanism_count_fails() { // Enable commit-reveal path and make caller a validator with stake SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::set_validator_permit_for_uid(netuid, uid1, true); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); SubtensorModule::add_balance_to_coldkey_account(&ck1, 1); @@ -1227,7 +1225,6 @@ fn test_reveal_crv3_commits_sub_success() { register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); @@ -1333,7 +1330,6 @@ fn test_crv3_above_mechanism_count_fails() { register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); @@ -1387,101 +1383,6 @@ fn test_crv3_above_mechanism_count_fails() { }); } -#[test] -fn test_do_commit_crv3_mechanism_weights_committing_too_fast() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let mecid = MechId::from(1u8); - let hotkey: AccountId = U256::from(1); - let commit_data_1: Vec = vec![1, 2, 3]; - let commit_data_2: Vec = vec![4, 5, 6]; - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - MechanismCountCurrent::::insert(netuid, MechId::from(2u8)); // allow subids {0,1} - - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).expect("uid"); - let idx1 = SubtensorModule::get_mechanism_storage_index(netuid, mecid); - SubtensorModule::set_last_update_for_uid(idx1, uid, 0); - - // make validator with stake - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_validator_permit_for_uid(netuid, uid, true); - SubtensorModule::add_balance_to_coldkey_account(&U256::from(2), 1); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &U256::from(2), - netuid, - 1.into(), - ); - - // first commit OK on mecid=1 - assert_ok!(SubtensorModule::commit_timelocked_mechanism_weights( - RuntimeOrigin::signed(hotkey), - netuid, - mecid, - commit_data_1.clone().try_into().expect("bounded"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - // immediate second commit on SAME mecid blocked - assert_noop!( - SubtensorModule::commit_timelocked_mechanism_weights( - RuntimeOrigin::signed(hotkey), - netuid, - mecid, - commit_data_2.clone().try_into().expect("bounded"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - ), - Error::::CommittingWeightsTooFast - ); - - // BUT committing too soon on a DIFFERENT mecid is allowed - let other_subid = MechId::from(0u8); - let idx0 = SubtensorModule::get_mechanism_storage_index(netuid, other_subid); - SubtensorModule::set_last_update_for_uid(idx0, uid, 0); // baseline like above - assert_ok!(SubtensorModule::commit_timelocked_mechanism_weights( - RuntimeOrigin::signed(hotkey), - netuid, - other_subid, - commit_data_2.clone().try_into().expect("bounded"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - // still too fast on original mecid after 2 blocks - step_block(2); - assert_noop!( - SubtensorModule::commit_timelocked_mechanism_weights( - RuntimeOrigin::signed(hotkey), - netuid, - mecid, - commit_data_2.clone().try_into().expect("bounded"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - ), - Error::::CommittingWeightsTooFast - ); - - // after enough blocks, OK again on original mecid - step_block(3); - assert_ok!(SubtensorModule::commit_timelocked_mechanism_weights( - RuntimeOrigin::signed(hotkey), - netuid, - mecid, - commit_data_2.try_into().expect("bounded"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - }); -} - #[test] fn epoch_mechanism_emergency_mode_distributes_by_stake() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index bc6f339988..b1db243a81 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -1019,8 +1019,6 @@ pub fn assert_last_event( #[allow(dead_code)] pub fn commit_dummy(who: U256, netuid: NetUid) { - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - // any 32‑byte value is fine; hash is never opened let hash = sp_core::H256::from_low_u64_be(0xDEAD_BEEF); assert_ok!(SubtensorModule::do_commit_weights( diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 8a8850b178..3ef476fa00 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -1755,23 +1755,6 @@ fn test_register_subnet_high_lock_cost() { }) } -#[test] -fn test_tempo_greater_than_weight_set_rate_limit() { - new_test_ext(1).execute_with(|| { - let subnet_owner_hotkey = U256::from(1); - let subnet_owner_coldkey = U256::from(2); - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - // Get tempo - let tempo = SubtensorModule::get_tempo(netuid); - - let weights_set_rate_limit = SubtensorModule::get_weights_set_rate_limit(netuid); - - assert!(tempo as u64 >= weights_set_rate_limit); - }) -} - #[allow(clippy::indexing_slicing)] #[test] fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state() { diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 47c438bbc5..a096c29723 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -2301,7 +2301,6 @@ fn test_mining_emission_distribution_validator_valiminer_miner() { &miner_coldkey, stake + ExistentialDeposit::get(), ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); step_block(subnet_tempo); SubnetOwnerCut::::set(0); // There are two validators and three neurons diff --git a/pallets/subtensor/src/tests/weights.rs b/pallets/subtensor/src/tests/weights.rs index 20ace5ee0d..4dce86d0ad 100644 --- a/pallets/subtensor/src/tests/weights.rs +++ b/pallets/subtensor/src/tests/weights.rs @@ -905,68 +905,6 @@ fn test_weights_version_key() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_setting_weights_too_fast --exact --show-output --nocapture -// Test ensures that uid has validator permit to set non-self weights. -#[test] -fn test_weights_err_setting_weights_too_fast() { - new_test_ext(0).execute_with(|| { - let hotkey_account_id = U256::from(55); - let netuid = NetUid::from(1); - let tempo: u16 = 13; - add_network_disable_commit_reveal(netuid, tempo, 0); - SubtensorModule::set_min_allowed_weights(netuid, 0); - SubtensorModule::set_max_allowed_uids(netuid, 3); - register_ok_neuron(netuid, hotkey_account_id, U256::from(66), 0); - register_ok_neuron(netuid, U256::from(1), U256::from(1), 65555); - register_ok_neuron(netuid, U256::from(2), U256::from(2), 75555); - - let neuron_uid: u16 = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_account_id) - .expect("Not registered."); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); - SubtensorModule::add_balance_to_coldkey_account(&U256::from(66), 1); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &(U256::from(66)), - netuid, - 1.into(), - ); - SubtensorModule::set_weights_set_rate_limit(netuid, 10); - assert_eq!(SubtensorModule::get_weights_set_rate_limit(netuid), 10); - - let weights_keys: Vec = vec![1, 2]; - let weight_values: Vec = vec![1, 2]; - - // Note that LastUpdate has default 0 for new uids, but if they have actually set weights on block 0 - // then they are allowed to set weights again once more without a wait restriction, to accommodate the default. - let result = SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey_account_id), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0, - ); - assert_ok!(result); - run_to_block(1); - - for i in 1..100 { - let result = SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey_account_id), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0, - ); - if i % 10 == 1 { - assert_ok!(result); - } else { - assert_eq!(result, Err(Error::::SettingWeightsTooFast.into())); - } - run_to_block(i + 1); - } - }); -} - // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_weights_vec_not_equal_size --exact --show-output --nocapture // Test ensures that uids -- weights must have the same size. #[test] @@ -1662,7 +1600,6 @@ fn test_reveal_weights_when_commit_reveal_disabled() { // Register neurons and set up configurations register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); @@ -1723,7 +1660,6 @@ fn test_commit_reveal_weights_ok() { register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); @@ -1791,7 +1727,6 @@ fn test_commit_reveal_tempo_interval() { register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); @@ -1927,7 +1862,6 @@ fn test_commit_reveal_hash() { register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); SubtensorModule::add_balance_to_coldkey_account(&U256::from(0), 1); @@ -2027,7 +1961,6 @@ fn test_commit_reveal_disabled_or_enabled() { register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); SubtensorModule::add_balance_to_coldkey_account(&U256::from(0), 1); @@ -2106,7 +2039,6 @@ fn test_toggle_commit_reveal_weights_and_set_weights() { SubtensorModule::set_stake_threshold(0); SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::add_balance_to_coldkey_account(&U256::from(0), 1); SubtensorModule::add_balance_to_coldkey_account(&U256::from(1), 1); SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( @@ -2189,7 +2121,6 @@ fn test_tempo_change_during_commit_reveal_process() { register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); @@ -2338,7 +2269,6 @@ fn test_commit_reveal_multiple_commits() { register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); @@ -2740,7 +2670,6 @@ fn test_expired_commits_handling_in_commit_and_reveal() { add_network(netuid, tempo, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); // Register neurons register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); @@ -2940,7 +2869,6 @@ fn test_reveal_at_exact_epoch() { add_network(netuid, tempo, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); @@ -3104,8 +3032,6 @@ fn test_tempo_and_reveal_period_change_during_commit_reveal_process() { assert_ok!(SubtensorModule::set_reveal_period(netuid, initial_reveal_period)); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); SubtensorModule::set_stake_threshold(0); @@ -3291,7 +3217,6 @@ fn test_commit_reveal_order_enforcement() { add_network(netuid, tempo, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); @@ -3394,7 +3319,6 @@ fn test_reveal_at_exact_block() { add_network_disable_commit_reveal(netuid, tempo, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); @@ -3550,7 +3474,6 @@ fn test_successful_batch_reveal() { add_network(netuid, tempo, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); @@ -3628,7 +3551,6 @@ fn test_batch_reveal_with_expired_commits() { add_network(netuid, tempo, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); @@ -3880,7 +3802,6 @@ fn test_batch_reveal_before_reveal_period() { register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); @@ -3938,7 +3859,6 @@ fn test_batch_reveal_after_commits_expired() { register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); @@ -4045,7 +3965,6 @@ fn test_batch_reveal_with_out_of_order_commits() { add_network(netuid, tempo, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); @@ -4156,7 +4075,6 @@ fn test_highly_concurrent_commits_and_reveals_with_multiple_hotkeys() { // ==== Setup Network ==== add_network(netuid, initial_tempo, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); assert_ok!(SubtensorModule::set_reveal_period(netuid, initial_reveal_period)); SubtensorModule::set_max_registrations_per_block(netuid, u16::MAX); SubtensorModule::set_target_registrations_per_interval(netuid, u16::MAX); @@ -4449,7 +4367,6 @@ fn test_get_reveal_blocks() { register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); @@ -4556,151 +4473,6 @@ fn test_get_reveal_blocks() { }) } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_weights_rate_limit --exact --show-output --nocapture -#[test] -fn test_commit_weights_rate_limit() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - System::set_block_number(11); - - let tempo: u16 = 5; - add_network(netuid, tempo, 0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 10); // Rate limit is 10 blocks - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::add_balance_to_coldkey_account(&U256::from(0), 1); - SubtensorModule::add_balance_to_coldkey_account(&U256::from(1), 1); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - let neuron_uid = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).expect("expected uid"); - SubtensorModule::set_last_update_for_uid(NetUidStorageIndex::from(netuid), neuron_uid, 0); - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - let new_salt: Vec = vec![9; 8]; - let new_commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - new_salt.clone(), - version_key, - )); - assert_err!( - SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, new_commit_hash), - Error::::CommittingWeightsTooFast - ); - - step_block(5); - assert_err!( - SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, new_commit_hash), - Error::::CommittingWeightsTooFast - ); - - step_block(5); // Current block is now 21 - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - new_commit_hash - )); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - let weights_keys: Vec = vec![0]; - let weight_values: Vec = vec![1]; - - assert_err!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0 - ), - Error::::SettingWeightsTooFast - ); - - step_block(10); - - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0 - )); - - assert_err!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0 - ), - Error::::SettingWeightsTooFast - ); - - step_block(5); - - assert_err!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0 - ), - Error::::SettingWeightsTooFast - ); - - step_block(5); - - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0 - )); - }); -} - // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::tlock_encrypt_decrypt_drand_quicknet_works --exact --show-output --nocapture #[test] pub fn tlock_encrypt_decrypt_drand_quicknet_works() { @@ -4764,7 +4536,6 @@ fn test_reveal_crv3_commits_success() { register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); @@ -4916,7 +4687,6 @@ fn test_reveal_crv3_commits_cannot_reveal_after_reveal_epoch() { add_network(netuid, 5, 0); register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); @@ -5042,7 +4812,6 @@ fn test_do_commit_crv3_weights_success() { add_network(netuid, 5, 0); register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::do_commit_timelocked_weights( @@ -5078,7 +4847,6 @@ fn test_do_commit_crv3_weights_disabled() { add_network(netuid, 5, 0); register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); assert_err!( @@ -5108,7 +4876,6 @@ fn test_do_commit_crv3_weights_hotkey_not_registered() { add_network(netuid, 5, 0); register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_err!( @@ -5126,79 +4893,6 @@ fn test_do_commit_crv3_weights_hotkey_not_registered() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_committing_too_fast --exact --show-output --nocapture -#[test] -fn test_do_commit_crv3_weights_committing_too_fast() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let commit_data_1: Vec = vec![1, 2, 3]; - let commit_data_2: Vec = vec![4, 5, 6]; - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - let neuron_uid = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).expect("Expected uid"); - SubtensorModule::set_last_update_for_uid(NetUidStorageIndex::from(netuid), neuron_uid, 0); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_data_1 - .clone() - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - assert_err!( - SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_data_2 - .clone() - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - ), - Error::::CommittingWeightsTooFast - ); - - step_block(2); - - assert_err!( - SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_data_2 - .clone() - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - ), - Error::::CommittingWeightsTooFast - ); - - step_block(3); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_data_2 - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - }); -} - // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_too_many_unrevealed_commits --exact --show-output --nocapture #[test] fn test_do_commit_crv3_weights_too_many_unrevealed_commits() { @@ -5212,7 +4906,6 @@ fn test_do_commit_crv3_weights_too_many_unrevealed_commits() { register_ok_neuron(netuid, hotkey1, U256::from(2), 100_000); register_ok_neuron(netuid, hotkey2, U256::from(3), 100_000); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); // Hotkey1 submits 10 commits successfully for i in 0..10 { @@ -5320,7 +5013,6 @@ fn test_reveal_crv3_commits_decryption_failure() { add_network(netuid, 5, 0); register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); let commit_bytes: Vec = vec![0xff; 100]; @@ -5376,7 +5068,6 @@ fn test_reveal_crv3_commits_multiple_commits_some_fail_some_succeed() { register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); // Prepare a valid payload for hotkey1 let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) @@ -5499,7 +5190,6 @@ fn test_reveal_crv3_commits_do_set_weights_failure() { register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); // Prepare payload with mismatched uids and values lengths let version_key = SubtensorModule::get_weights_version_key(netuid); @@ -5585,7 +5275,6 @@ fn test_reveal_crv3_commits_payload_decoding_failure() { register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); let invalid_payload = vec![0u8; 10]; // Not a valid encoding of WeightsTlockPayload @@ -5663,7 +5352,6 @@ fn test_reveal_crv3_commits_signature_deserialization_failure() { register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); let version_key = SubtensorModule::get_weights_version_key(netuid); let payload = WeightsTlockPayload { @@ -5744,7 +5432,6 @@ fn test_do_commit_crv3_weights_commit_size_exceeds_limit() { add_network(netuid, 5, 0); register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); let max_commit_size = MAX_CRV3_COMMIT_SIZE_BYTES as usize; let commit_data_exceeding: Vec = vec![0u8; max_commit_size + 1]; // Exceeds max size @@ -5785,7 +5472,6 @@ fn test_reveal_crv3_commits_with_empty_commit_queue() { add_network(netuid, 5, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); step_epochs(2, netuid); @@ -5809,7 +5495,6 @@ fn test_reveal_crv3_commits_with_incorrect_identity_message() { register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); // Prepare a valid payload but use incorrect identity message during encryption let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) @@ -5897,7 +5582,6 @@ fn test_multiple_commits_by_same_hotkey_within_limit() { register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); for i in 0..10 { let commit_data: Vec = vec![i; 5]; @@ -5936,7 +5620,6 @@ fn test_reveal_crv3_commits_removes_past_epoch_commits() { register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); // reveal_period = 1 epoch - SubtensorModule::set_weights_set_rate_limit(netuid, 0); // --------------------------------------------------------------------- // Put dummy commits into the two epochs immediately *before* current. @@ -6001,7 +5684,6 @@ fn test_reveal_crv3_commits_multiple_valid_commits_all_processed() { add_network(netuid, 5, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_stake_threshold(0); SubtensorModule::set_max_registrations_per_block(netuid, 100); SubtensorModule::set_target_registrations_per_interval(netuid, 100); @@ -6117,7 +5799,6 @@ fn test_reveal_crv3_commits_max_neurons() { add_network(netuid, 5, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_stake_threshold(0); SubtensorModule::set_max_registrations_per_block(netuid, 10_000); SubtensorModule::set_target_registrations_per_interval(netuid, 10_000); @@ -6343,7 +6024,6 @@ fn test_reveal_crv3_commits_hotkey_check() { register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); @@ -6460,7 +6140,6 @@ fn test_reveal_crv3_commits_hotkey_check() { register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); @@ -6612,7 +6291,6 @@ fn test_reveal_crv3_commits_retry_on_missing_pulse() { register_ok_neuron(netuid, hotkey, U256::from(3), 100_000); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_stake_threshold(0); let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).unwrap(); @@ -6727,7 +6405,6 @@ fn test_reveal_crv3_commits_legacy_payload_success() { register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index d6c438a76c..5faf38a7c5 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -454,14 +454,9 @@ impl Pallet { } pub fn get_weights_set_rate_limit(netuid: NetUid) -> u64 { - WeightsSetRateLimit::::get(netuid) - } - pub fn set_weights_set_rate_limit(netuid: NetUid, weights_set_rate_limit: u64) { - WeightsSetRateLimit::::insert(netuid, weights_set_rate_limit); - Self::deposit_event(Event::WeightsSetRateLimitSet( - netuid, - weights_set_rate_limit, - )); + T::RateLimiting::rate_limit(rate_limiting::GROUP_WEIGHTS_SUBNET, Some(netuid)) + .unwrap_or_default() + .saturated_into() } pub fn get_adjustment_interval(netuid: NetUid) -> u16 { diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index b664d6aef5..3a6b335548 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -412,7 +412,7 @@ fn build_delegate_take(groups: &mut Vec, commits: &mut Vec) // Weights group (config + usage shared). // scope: netuid // usage: netuid+neuron/netuid+mechanism+neuron -// legacy source: SubnetWeightsSetRateLimit, LastUpdate (subnet/mechanism) +// legacy source: WeightsSetRateLimit, LastUpdate (subnet/mechanism) fn build_weights(groups: &mut Vec, commits: &mut Vec) -> u64 { let mut reads: u64 = 0; groups.push(GroupConfig { @@ -1507,7 +1507,8 @@ mod tests { let scope = Some(netuid); let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); - let legacy_weights = || SubtensorModule::check_rate_limit(netuid.into(), uid, now); + // FIXME check_rate_limit is removed + // let legacy_weights = || SubtensorModule::check_rate_limit(netuid.into(), uid, now); parity_check( now, weights_call, From 6699000ab0d135f0010166da789cfc8134ee095a Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 12 Jan 2026 16:56:44 +0100 Subject: [PATCH 60/95] Make rate limit scope resolver to return a vec of scopes --- pallets/rate-limiting/src/lib.rs | 77 +++++++++----- pallets/rate-limiting/src/mock.rs | 36 ++++++- pallets/rate-limiting/src/tests.rs | 32 +++++- pallets/rate-limiting/src/tx_extension.rs | 123 ++++++++++++++++++---- pallets/rate-limiting/src/types.rs | 4 +- runtime/src/migrations/rate_limiting.rs | 2 +- 6 files changed, 218 insertions(+), 56 deletions(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 34504187a6..af91e70b9a 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -18,8 +18,8 @@ //! - [`set_rate_limit`](pallet::Pallet::set_rate_limit): assign or override the limit at a specific //! target/scope by supplying a [`RateLimitKind`] span. //! - [`assign_call_to_group`](pallet::Pallet::assign_call_to_group) and -//! [`remove_call_from_group`](pallet::Pallet::remove_call_from_group): manage group membership for -//! registered calls. +//! [`remove_call_from_group`](pallet::Pallet::remove_call_from_group): manage group membership +//! for registered calls. //! - [`set_call_read_only`](pallet::Pallet::set_call_read_only): for grouped calls, choose whether //! successful dispatches should update the shared usage row (`false` by default). //! - [`deregister_call`](pallet::Pallet::deregister_call): remove scoped configuration or wipe the @@ -88,10 +88,10 @@ //! NetUid, //! BlockNumber, //! > for ScopeResolver { -//! fn context(origin: &RuntimeOrigin, call: &RuntimeCall) -> Option { +//! fn context(origin: &RuntimeOrigin, call: &RuntimeCall) -> Option> { //! match call { //! RuntimeCall::Subtensor(pallet_subtensor::Call::set_weights { netuid, .. }) => { -//! Some(*netuid) +//! Some(vec![*netuid]) //! } //! _ => None, //! } @@ -173,14 +173,16 @@ pub mod pallet { use sp_runtime::traits::{ AtLeast32BitUnsigned, DispatchOriginOf, Dispatchable, Member, One, Saturating, Zero, }; - use sp_std::{boxed::Box, convert::TryFrom, marker::PhantomData, vec::Vec}; + use sp_std::{ + boxed::Box, collections::btree_map::BTreeMap, convert::TryFrom, marker::PhantomData, + vec::Vec, + }; #[cfg(feature = "runtime-benchmarks")] use crate::benchmarking::BenchmarkHelper as BenchmarkHelperTrait; use crate::types::{ - BypassDecision, EnsureLimitSettingRule, GroupSharing, RateLimit, RateLimitGroup, - RateLimitKind, RateLimitScopeResolver, RateLimitTarget, RateLimitUsageResolver, - TransactionIdentifier, + EnsureLimitSettingRule, GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, + RateLimitScopeResolver, RateLimitTarget, RateLimitUsageResolver, TransactionIdentifier, }; type GroupNameOf = BoundedVec>::MaxGroupNameLength>; @@ -365,7 +367,7 @@ pub mod pallet { /// Identifier of the registered transaction. transaction: TransactionIdentifier, /// Scope seeded during registration (if any). - scope: Option<>::LimitScope>, + scope: Option>::LimitScope>>, /// Optional group assignment applied at registration time. group: Option<>::GroupId>, /// Pallet name associated with the transaction. @@ -589,24 +591,34 @@ pub mod pallet { origin: &DispatchOriginOf<>::RuntimeCall>, call: &>::RuntimeCall, identifier: &TransactionIdentifier, - scope: &Option<>::LimitScope>, + scopes: &Option>::LimitScope>>, usage_key: &Option<>::UsageKey>, ) -> Result { - let bypass: BypassDecision = - >::LimitScopeResolver::should_bypass(origin, call); + let bypass = >::LimitScopeResolver::should_bypass(origin, call); if bypass.bypass_enforcement { return Ok(true); } let target = Self::config_target(identifier)?; - Self::ensure_scope_available(&target, scope)?; + Self::ensure_scope_available(&target, scopes)?; - let Some(block_span) = Self::effective_span(origin, call, &target, scope) else { - return Ok(true); + let usage_target = Self::usage_target(identifier)?; + let scope_list: Vec>::LimitScope>> = match scopes { + None => vec![None], + Some(resolved) if resolved.is_empty() => vec![None], + Some(resolved) => resolved.iter().cloned().map(Some).collect(), }; - let usage_target = Self::usage_target(identifier)?; - Ok(Self::within_span(&usage_target, usage_key, block_span)) + for scope in scope_list { + let Some(block_span) = Self::effective_span(origin, call, &target, &scope) else { + continue; + }; + if !Self::within_span(&usage_target, usage_key, block_span) { + return Ok(false); + } + } + + Ok(true) } /// Resolves the configured span for the provided target/scope, applying the pallet default @@ -880,9 +892,10 @@ pub mod pallet { fn ensure_scope_available( target: &RateLimitTarget<>::GroupId>, - scope: &Option<>::LimitScope>, + scopes: &Option>::LimitScope>>, ) -> Result<(), DispatchError> { - if scope.is_some() { + let has_scope = scopes.as_ref().map_or(false, |scopes| !scopes.is_empty()); + if has_scope { return Ok(()); } @@ -960,7 +973,7 @@ pub mod pallet { ) -> DispatchResult { let resolver_origin: DispatchOriginOf<>::RuntimeCall> = Into::>::RuntimeCall>>::into(origin.clone()); - let scope = + let scopes = >::LimitScopeResolver::context(&resolver_origin, call.as_ref()); T::AdminOrigin::ensure_origin(origin)?; @@ -971,11 +984,19 @@ pub mod pallet { let target = RateLimitTarget::Transaction(identifier); - if let Some(ref sc) = scope { - Limits::::insert( - target, - RateLimit::scoped_single(sc.clone(), RateLimitKind::Default), - ); + let scopes = scopes.and_then(|scopes| { + if scopes.is_empty() { + None + } else { + Some(scopes) + } + }); + if let Some(ref resolved) = scopes { + let mut map = BTreeMap::new(); + for scope in resolved { + map.insert(scope.clone(), RateLimitKind::Default); + } + Limits::::insert(target, RateLimit::Scoped(map)); } else { Limits::::insert(target, RateLimit::global(RateLimitKind::Default)); } @@ -992,10 +1013,10 @@ pub mod pallet { let (pallet, extrinsic) = Self::call_metadata(&identifier)?; Self::deposit_event(Event::CallRegistered { transaction: identifier, - scope: scope.clone(), + scope: scopes, group: assigned_group, - pallet: pallet.clone(), - extrinsic: extrinsic.clone(), + pallet: pallet, + extrinsic: extrinsic, }); if let Some(group_id) = assigned_group { diff --git a/pallets/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs index 16e470be3e..a29c719ce4 100644 --- a/pallets/rate-limiting/src/mock.rs +++ b/pallets/rate-limiting/src/mock.rs @@ -18,7 +18,7 @@ use sp_io::TestExternalities; use sp_std::vec::Vec; use crate as pallet_rate_limiting; -use crate::TransactionIdentifier; +use crate::{RateLimitKind, TransactionIdentifier}; pub type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; pub type Block = frame_system::mocking::MockBlock; @@ -112,12 +112,25 @@ pub struct TestUsageResolver; impl pallet_rate_limiting::RateLimitScopeResolver for TestScopeResolver { - fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option { + fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option> { match call { + RuntimeCall::RateLimiting(RateLimitingCall::set_rate_limit { limit, .. }) => { + let RateLimitKind::Exact(span) = limit else { + return Some(vec![1]); + }; + let scope = (*span).try_into().ok()?; + // Multi-scope path used by tests: Exact(42/43) returns two scopes. + if *span == 42 || *span == 43 { + Some(vec![scope, scope.saturating_add(1)]) + } else { + Some(vec![scope]) + } + } RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { - (*block_span).try_into().ok() + let scope = (*block_span).try_into().ok()?; + Some(vec![scope]) } - RuntimeCall::RateLimiting(_) => Some(1), + RuntimeCall::RateLimiting(_) => Some(vec![1]), _ => None, } } @@ -154,8 +167,21 @@ impl pallet_rate_limiting::RateLimitUsageResolver Option> { match call { + RuntimeCall::RateLimiting(RateLimitingCall::set_rate_limit { limit, .. }) => { + let RateLimitKind::Exact(span) = limit else { + return Some(vec![1]); + }; + let key = (*span).try_into().ok()?; + // Multi-usage path used by tests: Exact(42) returns two usage keys. + if *span == 42 { + Some(vec![key, key.saturating_add(1)]) + } else { + Some(vec![key]) + } + } RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { - (*block_span).try_into().ok().map(|key| vec![key]) + let key = (*block_span).try_into().ok()?; + Some(vec![key]) } RuntimeCall::RateLimiting(_) => Some(vec![1]), _ => None, diff --git a/pallets/rate-limiting/src/tests.rs b/pallets/rate-limiting/src/tests.rs index 5fc79a9362..874cc68241 100644 --- a/pallets/rate-limiting/src/tests.rs +++ b/pallets/rate-limiting/src/tests.rs @@ -120,7 +120,35 @@ fn register_call_seeds_scoped_limit() { assert!(matches!( event, RuntimeEvent::RateLimiting(crate::Event::CallRegistered { transaction, scope, .. }) - if transaction == identifier && scope == Some(1u16) + if transaction == identifier && scope == Some(vec![1u16]) + )); + }); +} + +#[test] +fn register_call_seeds_multi_scoped_limit() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::RateLimiting(RateLimitingCall::set_rate_limit { + target: RateLimitTarget::Transaction(TransactionIdentifier::new(0, 0)), + scope: None, + limit: RateLimitKind::Exact(42), + }); + let identifier = register(call, None); + let tx_target = target(identifier); + let stored = Limits::::get(tx_target).expect("limit"); + match stored { + RateLimit::Scoped(map) => { + assert_eq!(map.get(&42u16), Some(&RateLimitKind::Default)); + assert_eq!(map.get(&43u16), Some(&RateLimitKind::Default)); + } + _ => panic!("expected scoped entry"), + } + + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::CallRegistered { transaction, scope, .. }) + if transaction == identifier && scope == Some(vec![42u16, 43u16]) )); }); } @@ -629,7 +657,7 @@ fn is_within_limit_detects_rate_limited_scope() { &RuntimeOrigin::signed(1), &call, &identifier, - &Some(1u16), + &Some(vec![1u16]), &Some(1u16), ) .expect("ok"); diff --git a/pallets/rate-limiting/src/tx_extension.rs b/pallets/rate-limiting/src/tx_extension.rs index 303649c9c9..5136659a83 100644 --- a/pallets/rate-limiting/src/tx_extension.rs +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -125,7 +125,7 @@ where return Ok((ValidTransaction::default(), None, origin)); } - let scope = >::LimitScopeResolver::context(&origin, call); + let scopes = >::LimitScopeResolver::context(&origin, call); let usage = >::UsageResolver::context(&origin, call); let config_target = Pallet::::config_target(&identifier) @@ -136,12 +136,6 @@ where let should_record = bypass.record_usage && Pallet::::should_record_usage(&identifier, &usage_target); - let Some(block_span) = - Pallet::::effective_span(&origin, call, &config_target, &scope) - else { - return Ok((ValidTransaction::default(), None, origin)); - }; - if bypass.bypass_enforcement { return Ok(( ValidTransaction::default(), @@ -150,23 +144,40 @@ where )); } - if block_span.is_zero() { - return Ok((ValidTransaction::default(), None, origin)); - } - let usage_keys: Vec>::UsageKey>> = match usage.clone() { None => vec![None], Some(keys) => keys.into_iter().map(Some).collect(), }; - let within_limit = usage_keys - .iter() - .all(|key| Pallet::::within_span(&usage_target, key, block_span)); + let scope_list: Vec>::LimitScope>> = match scopes { + None => vec![None], + Some(resolved) if resolved.is_empty() => vec![None], + Some(resolved) => resolved.into_iter().map(Some).collect(), + }; - if !within_limit { - return Err(TransactionValidityError::Invalid( - InvalidTransaction::Custom(RATE_LIMIT_DENIED), - )); + let mut enforced = false; + for scope in scope_list { + let Some(block_span) = + Pallet::::effective_span(&origin, call, &config_target, &scope) + else { + continue; + }; + if block_span.is_zero() { + continue; + } + enforced = true; + let within_limit = usage_keys + .iter() + .all(|key| Pallet::::within_span(&usage_target, key, block_span)); + if !within_limit { + return Err(TransactionValidityError::Invalid( + InvalidTransaction::Custom(RATE_LIMIT_DENIED), + )); + } + } + + if !enforced { + return Ok((ValidTransaction::default(), None, origin)); } Ok(( @@ -237,6 +248,7 @@ mod tests { use super::*; use crate::mock::*; + use sp_std::collections::btree_map::BTreeMap; fn remark_call() -> RuntimeCall { RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }) @@ -256,6 +268,14 @@ mod tests { }) } + fn multi_scope_call(block_span: u64) -> RuntimeCall { + RuntimeCall::RateLimiting(RateLimitingCall::set_rate_limit { + target: RateLimitTarget::Transaction(TransactionIdentifier::new(0, 0)), + scope: None, + limit: RateLimitKind::Exact(block_span), + }) + } + fn new_tx_extension() -> RateLimitTransactionExtension { RateLimitTransactionExtension(Default::default()) } @@ -367,6 +387,73 @@ mod tests { }); } + #[test] + fn tx_extension_rejects_when_any_scope_fails() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + let call = multi_scope_call(43); + let identifier = identifier_for(&call); + let target = RateLimitTarget::Transaction(identifier); + + assert_ok!(RateLimiting::register_call( + RuntimeOrigin::root(), + Box::new(call.clone()), + None, + )); + + let mut scopes = BTreeMap::new(); + scopes.insert(43u16, RateLimitKind::Exact(5)); + scopes.insert(44u16, RateLimitKind::Exact(3)); + Limits::::insert(target, RateLimit::Scoped(scopes)); + LastSeen::::insert(target, Some(43u16), 10); + + System::set_block_number(14); + + let err = + validate_with_tx_extension(&extension, &call).expect_err("one scope should block"); + match err { + TransactionValidityError::Invalid(InvalidTransaction::Custom(code)) => { + assert_eq!(code, RATE_LIMIT_DENIED); + } + other => panic!("unexpected error: {:?}", other), + } + }); + } + + #[test] + fn tx_extension_rejects_when_any_usage_key_fails() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + let call = multi_scope_call(42); + let identifier = identifier_for(&call); + let target = RateLimitTarget::Transaction(identifier); + + assert_ok!(RateLimiting::register_call( + RuntimeOrigin::root(), + Box::new(call.clone()), + None, + )); + + let mut scopes = BTreeMap::new(); + scopes.insert(42u16, RateLimitKind::Exact(5)); + scopes.insert(43u16, RateLimitKind::Exact(5)); + Limits::::insert(target, RateLimit::Scoped(scopes)); + LastSeen::::insert(target, Some(42u16), 8); + LastSeen::::insert(target, Some(43u16), 12); + + System::set_block_number(14); + + let err = validate_with_tx_extension(&extension, &call) + .expect_err("one usage key should block"); + match err { + TransactionValidityError::Invalid(InvalidTransaction::Custom(code)) => { + assert_eq!(code, RATE_LIMIT_DENIED); + } + other => panic!("unexpected error: {:?}", other), + } + }); + } + #[test] fn tx_extension_records_usage_on_bypass() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting/src/types.rs b/pallets/rate-limiting/src/types.rs index f6f54b472f..f128433473 100644 --- a/pallets/rate-limiting/src/types.rs +++ b/pallets/rate-limiting/src/types.rs @@ -8,9 +8,9 @@ use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; /// Resolves the optional identifier within which a rate limit applies and can optionally adjust /// enforcement behaviour. pub trait RateLimitScopeResolver { - /// Returns `Some(scope)` when the limit should be applied per-scope, or `None` for global + /// Returns `Some(scopes)` when the limit should be applied per-scope, or `None` for global /// limits. - fn context(origin: &Origin, call: &Call) -> Option; + fn context(origin: &Origin, call: &Call) -> Option>; /// Returns how the call should interact with enforcement and usage tracking. fn should_bypass(_origin: &Origin, _call: &Call) -> BypassDecision { diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 3a6b335548..dcd96ce2b3 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -1507,7 +1507,7 @@ mod tests { let scope = Some(netuid); let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); - // FIXME check_rate_limit is removed + // FIXME check_rate_limit is removed // let legacy_weights = || SubtensorModule::check_rate_limit(netuid.into(), uid, now); parity_check( now, From b9be9eeafa86fdd2844362c4a874c2fc0734108e Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 12 Jan 2026 18:05:32 +0100 Subject: [PATCH 61/95] Fix batch weights rate-limiting migration --- pallets/rate-limiting/src/lib.rs | 2 +- runtime/src/migrations/rate_limiting.rs | 45 ++++++++++++++++++------- runtime/src/rate_limiting/mod.rs | 38 +++++++++++++++++---- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index af91e70b9a..2ba3cd16b7 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -174,7 +174,7 @@ pub mod pallet { AtLeast32BitUnsigned, DispatchOriginOf, Dispatchable, Member, One, Saturating, Zero, }; use sp_std::{ - boxed::Box, collections::btree_map::BTreeMap, convert::TryFrom, marker::PhantomData, + boxed::Box, collections::btree_map::BTreeMap, convert::TryFrom, marker::PhantomData, vec, vec::Vec, }; diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index dcd96ce2b3..4a5226a809 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -421,6 +421,7 @@ fn build_weights(groups: &mut Vec, commits: &mut Vec) -> u6 sharing: GroupSharing::ConfigAndUsage, members: vec![ MigratedCall::subtensor(0, false), // set_weights + MigratedCall::subtensor(80, false), // batch_set_weights MigratedCall::subtensor(96, false), // commit_weights MigratedCall::subtensor(100, false), // batch_commit_weights MigratedCall::subtensor(113, false), // commit_timelocked_weights @@ -1108,7 +1109,7 @@ mod tests { call: RuntimeCall, origin: RuntimeOrigin, usage_override: Option>, - scope_override: Option, + scope_override: Option>, legacy_check: F, ) where F: Fn() -> bool, @@ -1127,13 +1128,27 @@ mod tests { let target = resolve_target(identifier); // Use the runtime-adjusted span (handles tempo scaling for admin-utils). - let span = pallet_rate_limiting::Pallet::::effective_span( - &origin.clone().into(), - &call, - &target, - &scope, - ) - .unwrap_or_default(); + let span = match scope.as_ref() { + None => pallet_rate_limiting::Pallet::::effective_span( + &origin.clone().into(), + &call, + &target, + &None, + ) + .unwrap_or_default(), + Some(scopes) => scopes + .iter() + .filter_map(|scope| { + pallet_rate_limiting::Pallet::::effective_span( + &origin.clone().into(), + &call, + &target, + &Some(*scope), + ) + }) + .max() + .unwrap_or_default(), + }; let span_u64: u64 = span.saturated_into(); let usage_keys: Vec::UsageKey>> = @@ -1504,11 +1519,17 @@ mod tests { version_key: 0, }); let origin = RuntimeOrigin::signed(hot.clone()); - let scope = Some(netuid); + let scope = Some(vec![netuid]); let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); - // FIXME check_rate_limit is removed - // let legacy_weights = || SubtensorModule::check_rate_limit(netuid.into(), uid, now); + let legacy_weights = || { + let last = LastUpdate::::get(NetUidStorageIndex::from(netuid)) + .get(uid as usize) + .copied() + .unwrap_or_default(); + let limit = WeightsSetRateLimit::::get(netuid); + now.saturating_sub(last) >= limit + }; parity_check( now, weights_call, @@ -1595,7 +1616,7 @@ mod tests { }); let origin = RuntimeOrigin::signed(hot.clone()); let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); - let scope = Some(netuid); + let scope = Some(vec![netuid]); let limit = ::EvmKeyAssociateRateLimit::get(); let legacy = || { let last = now - 1; diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 8eebfaf224..5728895ca1 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -25,7 +25,7 @@ use pallet_subtensor::{Call as SubtensorCall, Tempo}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; use sp_runtime::DispatchError; -use sp_std::{vec, vec::Vec}; +use sp_std::{collections::btree_set::BTreeSet, vec, vec::Vec}; use subtensor_runtime_common::{ BlockNumber, NetUid, rate_limiting::{RateLimitUsageKey, ServingEndpoint}, @@ -96,7 +96,7 @@ impl EnsureLimitSettingRule for LimitSe pub struct ScopeResolver; impl RateLimitScopeResolver for ScopeResolver { - fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option { + fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option> { match call { RuntimeCall::SubtensorModule(inner) => match inner { SubtensorCall::serve_axon { netuid, .. } @@ -112,7 +112,17 @@ impl RateLimitScopeResolver for | SubtensorCall::reveal_mechanism_weights { netuid, .. } | SubtensorCall::commit_crv3_mechanism_weights { netuid, .. } | SubtensorCall::commit_timelocked_mechanism_weights { netuid, .. } => { - Some(*netuid) + Some(vec![*netuid]) + } + SubtensorCall::batch_set_weights { netuids, .. } + | SubtensorCall::batch_commit_weights { netuids, .. } => { + let scopes: BTreeSet = + netuids.iter().map(|netuid| (*netuid).into()).collect(); + if scopes.is_empty() { + None + } else { + Some(scopes.into_iter().collect()) + } } _ => None, }, @@ -231,12 +241,26 @@ impl RateLimitUsageResolver { + let mut usage = BTreeSet::new(); + for netuid in netuids { + let netuid: NetUid = (*netuid).into(); + let uid = neuron_identity(origin, netuid)?; + usage.insert(RateLimitUsageKey::::SubnetNeuron { netuid, uid }); + } + if usage.is_empty() { + None + } else { + Some(usage.into_iter().collect()) + } + } SubtensorCall::set_weights { netuid, .. } | SubtensorCall::commit_weights { netuid, .. } | SubtensorCall::reveal_weights { netuid, .. } | SubtensorCall::batch_reveal_weights { netuid, .. } | SubtensorCall::commit_timelocked_weights { netuid, .. } => { - let (_, uid) = neuron_identity(origin, *netuid)?; + let uid = neuron_identity(origin, *netuid)?; Some(vec![RateLimitUsageKey::::SubnetNeuron { netuid: *netuid, uid, @@ -249,7 +273,7 @@ impl RateLimitUsageResolver { - let (_, uid) = neuron_identity(origin, *netuid)?; + let uid = neuron_identity(origin, *netuid)?; Some(vec![ RateLimitUsageKey::::SubnetMechanismNeuron { netuid: *netuid, @@ -349,11 +373,11 @@ impl RateLimitUsageResolver Option<(AccountId, u16)> { +fn neuron_identity(origin: &RuntimeOrigin, netuid: NetUid) -> Option { let hotkey = signed_origin(origin)?; let uid = pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey(netuid, &hotkey).ok()?; - Some((hotkey, uid)) + Some(uid) } fn signed_origin(origin: &RuntimeOrigin) -> Option { From 3345812dc11bf0fcda4a8b44a17418d507399c57 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 13 Jan 2026 16:14:51 +0100 Subject: [PATCH 62/95] Extend RateLimitingInterface(Info) with set_last_seen --- chain-extensions/src/mock.rs | 13 ++++++-- pallets/admin-utils/src/tests/mock.rs | 13 ++++++-- pallets/rate-limiting-interface/README.md | 2 +- pallets/rate-limiting-interface/src/lib.rs | 16 ++++++++-- pallets/rate-limiting/src/lib.rs | 35 ++++++++++++++++++++-- pallets/subtensor/src/coinbase/root.rs | 2 +- pallets/subtensor/src/macros/config.rs | 4 +-- pallets/subtensor/src/tests/migration.rs | 2 +- pallets/subtensor/src/tests/mock.rs | 13 ++++++-- pallets/subtensor/src/utils/misc.rs | 2 +- pallets/transaction-fee/src/tests/mock.rs | 13 ++++++-- 11 files changed, 96 insertions(+), 19 deletions(-) diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index a1c37ff0a9..555934419f 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -18,7 +18,7 @@ use pallet_contracts::HoldReason as ContractsHoldReason; use pallet_subtensor::*; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_utility as pallet_utility; -use rate_limiting_interface::{RateLimitingInfo, TryIntoRateLimitTarget}; +use rate_limiting_interface::{RateLimitingInterface, TryIntoRateLimitTarget}; use sp_core::{ConstU64, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; use sp_runtime::{ @@ -449,7 +449,7 @@ impl CommitmentsInterface for CommitmentsI { pub struct NoRateLimiting; -impl RateLimitingInfo for NoRateLimiting { +impl RateLimitingInterface for NoRateLimiting { type GroupId = subtensor_runtime_common::rate_limiting::GroupId; type CallMetadata = RuntimeCall; type Limit = BlockNumber; @@ -472,6 +472,15 @@ impl RateLimitingInfo for NoRateLimiting { { None } + + fn set_last_seen( + _target: TargetArg, + _usage_key: Option, + _block: Option, + ) where + TargetArg: TryIntoRateLimitTarget, + { + } } parameter_types! { diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index e9bedb1023..08b05b3a20 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -8,7 +8,7 @@ use frame_support::{ }; use frame_system::{self as system, offchain::CreateTransactionBase}; use frame_system::{EnsureRoot, limits}; -use rate_limiting_interface::{RateLimitingInfo, TryIntoRateLimitTarget}; +use rate_limiting_interface::{RateLimitingInterface, TryIntoRateLimitTarget}; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_grandpa::AuthorityList as GrandpaAuthorityList; use sp_core::U256; @@ -354,7 +354,7 @@ impl pallet_subtensor::CommitmentsInterface for CommitmentsI { pub struct NoRateLimiting; -impl RateLimitingInfo for NoRateLimiting { +impl RateLimitingInterface for NoRateLimiting { type GroupId = subtensor_runtime_common::rate_limiting::GroupId; type CallMetadata = RuntimeCall; type Limit = u64; @@ -377,6 +377,15 @@ impl RateLimitingInfo for NoRateLimiting { { None } + + fn set_last_seen( + _target: TargetArg, + _usage_key: Option, + _block: Option, + ) where + TargetArg: TryIntoRateLimitTarget, + { + } } pub struct GrandpaInterfaceImpl; diff --git a/pallets/rate-limiting-interface/README.md b/pallets/rate-limiting-interface/README.md index d8671fc0ba..73011099c1 100644 --- a/pallets/rate-limiting-interface/README.md +++ b/pallets/rate-limiting-interface/README.md @@ -1,3 +1,3 @@ # `rate-limiting-interface` -Small, `no_std`-friendly interface crate that defines [`RateLimitingInfo`](src/lib.rs). +Small, `no_std`-friendly interface crate that defines [`RateLimitingInterface`](src/lib.rs). diff --git a/pallets/rate-limiting-interface/src/lib.rs b/pallets/rate-limiting-interface/src/lib.rs index 4bf0dab22f..ebed2883aa 100644 --- a/pallets/rate-limiting-interface/src/lib.rs +++ b/pallets/rate-limiting-interface/src/lib.rs @@ -1,6 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] -//! Read-only interface for querying rate limits and last-seen usage. +//! Interface for querying rate limits and last-seen usage, with optional write access. use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use frame_support::traits::GetCallMetadata; @@ -8,8 +8,8 @@ use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; use sp_std::vec::Vec; -/// Read-only queries for rate-limiting configuration and usage tracking. -pub trait RateLimitingInfo { +/// Interface for rate-limiting configuration and usage tracking. +pub trait RateLimitingInterface { /// Group id type used by rate-limiting targets. type GroupId; /// Call type used for name/index resolution. @@ -33,6 +33,16 @@ pub trait RateLimitingInfo { ) -> Option where TargetArg: TryIntoRateLimitTarget; + + /// Sets the last-seen block for `target` and optional `usage_key`. + /// + /// Passing `None` clears the value. + fn set_last_seen( + target: TargetArg, + usage_key: Option, + block: Option, + ) where + TargetArg: TryIntoRateLimitTarget; } /// Target identifier for rate limit and usage configuration. diff --git a/pallets/rate-limiting/src/lib.rs b/pallets/rate-limiting/src/lib.rs index 2ba3cd16b7..aa7e1765b2 100644 --- a/pallets/rate-limiting/src/lib.rs +++ b/pallets/rate-limiting/src/lib.rs @@ -143,7 +143,7 @@ pub use benchmarking::BenchmarkHelper; pub use pallet::*; pub use rate_limiting_interface::{RateLimitTarget, TransactionIdentifier}; -pub use rate_limiting_interface::{RateLimitingInfo, TryIntoRateLimitTarget}; +pub use rate_limiting_interface::{RateLimitingInterface, TryIntoRateLimitTarget}; pub use tx_extension::RateLimitTransactionExtension; pub use types::{ BypassDecision, EnsureLimitSettingRule, GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, @@ -1381,7 +1381,7 @@ pub mod pallet { } } -impl, I: 'static> RateLimitingInfo for pallet::Pallet { +impl, I: 'static> RateLimitingInterface for pallet::Pallet { type GroupId = >::GroupId; type CallMetadata = >::RuntimeCall; type Limit = frame_system::pallet_prelude::BlockNumberFor; @@ -1422,4 +1422,35 @@ impl, I: 'static> RateLimitingInfo for pallet::Pallet }; pallet::LastSeen::::get(usage_target, usage_key) } + + fn set_last_seen( + target: TargetArg, + usage_key: Option, + block: Option, + ) where + TargetArg: TryIntoRateLimitTarget, + { + let Some(raw_target) = target + .try_into_rate_limit_target::() + .ok() + else { + return; + }; + + let usage_target = match raw_target { + RateLimitTarget::Transaction(identifier) => { + if let Ok(resolved) = Self::usage_target(&identifier) { + resolved + } else { + return; + } + } + _ => raw_target, + }; + + match block { + Some(block) => pallet::LastSeen::::insert(usage_target, usage_key, block), + None => pallet::LastSeen::::remove(usage_target, usage_key), + } + } } diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 9048231a86..c01fc38442 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -17,7 +17,7 @@ use super::*; use crate::CommitmentsInterface; -use rate_limiting_interface::RateLimitingInfo; +use rate_limiting_interface::RateLimitingInterface; use safe_math::*; use sp_runtime::SaturatedConversion; use substrate_fixed::types::{I64F64, U96F32}; diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index d377dfcb47..3cbe846eb6 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -8,7 +8,7 @@ mod config { use crate::{CommitmentsInterface, GetAlphaForTao, GetTaoForAlpha}; use pallet_commitments::GetCommitments; - use rate_limiting_interface::RateLimitingInfo; + use rate_limiting_interface::RateLimitingInterface; use subtensor_swap_interface::{SwapEngine, SwapHandler}; /// Configure the pallet by specifying the parameters and types on which it depends. @@ -58,7 +58,7 @@ mod config { type CommitmentsInterface: CommitmentsInterface; /// Read-only interface for querying rate limiting configuration and usage. - type RateLimiting: RateLimitingInfo< + type RateLimiting: RateLimitingInterface< GroupId = subtensor_runtime_common::rate_limiting::GroupId, CallMetadata = ::RuntimeCall, Limit = BlockNumberFor, diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index c5f74d70f6..2b8cd0d061 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -22,7 +22,7 @@ use frame_support::{ use crate::migrations::migrate_storage; use frame_system::Config; use pallet_drand::types::RoundNumber; -use rate_limiting_interface::RateLimitingInfo; +use rate_limiting_interface::RateLimitingInterface; use scale_info::prelude::collections::VecDeque; use sp_core::{H256, U256, crypto::Ss58Codec}; use sp_io::hashing::twox_128; diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 2d05c50ec1..8f8615e79b 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -20,7 +20,7 @@ use frame_system as system; use frame_system::{EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_utility as pallet_utility; -use rate_limiting_interface::{RateLimitingInfo, TryIntoRateLimitTarget}; +use rate_limiting_interface::{RateLimitingInterface, TryIntoRateLimitTarget}; use sp_core::{ConstU64, Get, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; use sp_runtime::{ @@ -334,7 +334,7 @@ impl CommitmentsInterface for CommitmentsI { pub struct NoRateLimiting; -impl RateLimitingInfo for NoRateLimiting { +impl RateLimitingInterface for NoRateLimiting { type GroupId = subtensor_runtime_common::rate_limiting::GroupId; type CallMetadata = RuntimeCall; type Limit = BlockNumber; @@ -357,6 +357,15 @@ impl RateLimitingInfo for NoRateLimiting { { None } + + fn set_last_seen( + _target: TargetArg, + _usage_key: Option, + _block: Option, + ) where + TargetArg: TryIntoRateLimitTarget, + { + } } parameter_types! { diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 5faf38a7c5..89d5a1446a 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -1,7 +1,7 @@ use super::*; use crate::Error; use crate::system::{ensure_signed, ensure_signed_or_root, pallet_prelude::BlockNumberFor}; -use rate_limiting_interface::RateLimitingInfo; +use rate_limiting_interface::RateLimitingInterface; use safe_math::*; use sp_core::Get; use sp_core::U256; diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 6a7042220b..c67a5afa2d 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -12,7 +12,7 @@ use frame_system::{ self as system, EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase, }; pub use pallet_subtensor::*; -use rate_limiting_interface::{RateLimitingInfo, TryIntoRateLimitTarget}; +use rate_limiting_interface::{RateLimitingInterface, TryIntoRateLimitTarget}; pub use sp_core::U256; use sp_core::{ConstU64, H256}; use sp_runtime::{ @@ -421,7 +421,7 @@ impl pallet_subtensor::CommitmentsInterface for CommitmentsI { pub struct NoRateLimiting; -impl RateLimitingInfo for NoRateLimiting { +impl RateLimitingInterface for NoRateLimiting { type GroupId = subtensor_runtime_common::rate_limiting::GroupId; type CallMetadata = RuntimeCall; type Limit = u64; @@ -444,6 +444,15 @@ impl RateLimitingInfo for NoRateLimiting { { None } + + fn set_last_seen( + _target: TargetArg, + _usage_key: Option, + _block: Option, + ) where + TargetArg: TryIntoRateLimitTarget, + { + } } parameter_types! { From 1406a8ff6bb8e2249d3df0702cb4bfa0fefeefaf Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 13 Jan 2026 17:16:18 +0100 Subject: [PATCH 63/95] Remove LastUpdate storage --- pallets/admin-utils/src/tests/mod.rs | 27 +++++-- pallets/subtensor/src/coinbase/root.rs | 4 +- pallets/subtensor/src/epoch/run_epoch.rs | 13 ++- pallets/subtensor/src/lib.rs | 5 -- pallets/subtensor/src/macros/genesis.rs | 1 - .../migrations/migrate_delete_subnet_21.rs | 3 +- .../src/migrations/migrate_delete_subnet_3.rs | 3 +- pallets/subtensor/src/rpc_info/metagraph.rs | 45 +++++++---- pallets/subtensor/src/rpc_info/neuron_info.rs | 16 +++- pallets/subtensor/src/rpc_info/show_subnet.rs | 12 ++- pallets/subtensor/src/subnets/mechanism.rs | 10 ++- pallets/subtensor/src/subnets/uids.rs | 40 +++++++-- pallets/subtensor/src/subnets/weights.rs | 13 --- pallets/subtensor/src/tests/children.rs | 10 ++- pallets/subtensor/src/tests/coinbase.rs | 17 +++- pallets/subtensor/src/tests/epoch.rs | 25 ++++-- pallets/subtensor/src/tests/epoch_logs.rs | 15 +++- pallets/subtensor/src/tests/mechanism.rs | 81 +++++++++++++++---- pallets/subtensor/src/tests/networks.rs | 72 ++++++++++++++--- pallets/subtensor/src/tests/registration.rs | 35 +++++--- pallets/subtensor/src/tests/staking.rs | 10 ++- pallets/subtensor/src/utils/misc.rs | 65 +++++++++------ precompiles/src/metagraph.rs | 18 ++++- runtime/src/migrations/rate_limiting.rs | 67 +++++---------- runtime/src/rate_limiting/legacy.rs | 55 ++++++++++++- 25 files changed, 468 insertions(+), 194 deletions(-) diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index fdb800d710..3a7f4523b2 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -11,10 +11,12 @@ use pallet_subtensor::{ }; // use pallet_subtensor::{migrations, Event}; use pallet_subtensor::{Event, utils::rate_limiting::TransactionType}; +use rate_limiting_interface::RateLimitingInterface; use sp_consensus_grandpa::AuthorityId as GrandpaId; use sp_core::{Get, Pair, U256, ed25519}; +use sp_runtime::traits::SaturatedConversion; use substrate_fixed::types::I96F32; -use subtensor_runtime_common::{Currency, MechId, NetUid, TaoCurrency}; +use subtensor_runtime_common::{Currency, MechId, NetUid, TaoCurrency, rate_limiting}; use crate::Error; use crate::pallet::PrecompileEnable; @@ -2430,10 +2432,18 @@ fn test_trim_to_max_allowed_uids() { Active::::insert(netuid, bool_values); for mecid in 0..mechanism_count.into() { - let netuid_index = - SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); + let mecid = MechId::from(mecid); + let netuid_index = SubtensorModule::get_mechanism_storage_index(netuid, mecid); Incentive::::insert(netuid_index, values.clone()); - LastUpdate::::insert(netuid_index, u64_values.clone()); + for (uid, last_seen) in u64_values.iter().copied().enumerate() { + let usage = + SubtensorModule::weights_rl_usage_key_for_uid(netuid, mecid, uid as u16); + ::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + Some(last_seen.saturated_into()), + ); + } } // We set some owner immune uids @@ -2535,10 +2545,13 @@ fn test_trim_to_max_allowed_uids() { assert_eq!(StakeWeight::::get(netuid), expected_values); for mecid in 0..mechanism_count.into() { - let netuid_index = - SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); + let mecid = MechId::from(mecid); + let netuid_index = SubtensorModule::get_mechanism_storage_index(netuid, mecid); assert_eq!(Incentive::::get(netuid_index), expected_values); - assert_eq!(LastUpdate::::get(netuid_index), expected_u64_values); + assert_eq!( + SubtensorModule::weights_rl_last_seen_for_uids(netuid, mecid, new_max_n), + expected_u64_values, + ); } // Ensure trimmed uids related storage has been cleared diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index c01fc38442..8a19dbf343 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -237,6 +237,8 @@ impl Pallet { let owner_coldkey: T::AccountId = SubnetOwner::::get(netuid); SubnetOwner::::remove(netuid); + let subnetwork_n = SubnetworkN::::get(netuid); + // --- 2. Remove network count. SubnetworkN::::remove(netuid); @@ -379,7 +381,7 @@ impl Pallet { let mechanisms: u8 = MechanismCountCurrent::::get(netuid).into(); for subid in 0..mechanisms { let netuid_index = Self::get_mechanism_storage_index(netuid, subid.into()); - LastUpdate::::remove(netuid_index); + Self::set_weights_rl_last_seen_for_uids(netuid, subid.into(), subnetwork_n, None); Incentive::::remove(netuid_index); let _ = WeightCommits::::clear_prefix(netuid_index, u32::MAX, None); let _ = TimelockedWeightCommits::::clear_prefix(netuid_index, u32::MAX, None); diff --git a/pallets/subtensor/src/epoch/run_epoch.rs b/pallets/subtensor/src/epoch/run_epoch.rs index f56b8a89a4..363841ef1b 100644 --- a/pallets/subtensor/src/epoch/run_epoch.rs +++ b/pallets/subtensor/src/epoch/run_epoch.rs @@ -172,7 +172,7 @@ impl Pallet { log::trace!("activity_cutoff: {activity_cutoff:?}"); // Last update vector. - let last_update: Vec = Self::get_last_update(netuid_index); + let last_update = Self::weights_rl_last_seen_padded(netuid_index); log::trace!("Last update: {:?}", &last_update); // Inactive mask. @@ -598,7 +598,7 @@ impl Pallet { log::trace!("activity_cutoff: {activity_cutoff:?}"); // Last update vector. - let last_update: Vec = Self::get_last_update(netuid_index); + let last_update = Self::weights_rl_last_seen_padded(netuid_index); log::trace!("Last update: {:?}", &last_update); // Inactive mask. @@ -1582,4 +1582,13 @@ impl Pallet { } true } + + fn weights_rl_last_seen_padded(netuid_index: NetUidStorageIndex) -> Vec { + let netuid = Self::get_netuid(netuid_index); + let subnet_n = Self::get_subnetwork_n(netuid); + match Self::get_netuid_and_subid(netuid_index) { + Ok((_, mecid)) => Self::weights_rl_last_seen_for_uids(netuid, mecid, subnet_n), + Err(_) => vec![0; subnet_n as usize], + } + } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 8459d49369..1a5b640059 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1947,11 +1947,6 @@ pub mod pallet { #[pallet::storage] pub type Emission = StorageMap<_, Identity, NetUid, Vec, ValueQuery>; - /// --- MAP ( netuid ) --> last_update - #[pallet::storage] - pub type LastUpdate = - StorageMap<_, Identity, NetUidStorageIndex, Vec, ValueQuery, EmptyU64Vec>; - /// --- MAP ( netuid ) --> validator_trust #[pallet::storage] pub type ValidatorTrust = diff --git a/pallets/subtensor/src/macros/genesis.rs b/pallets/subtensor/src/macros/genesis.rs index 0014b63540..649e3ebf78 100644 --- a/pallets/subtensor/src/macros/genesis.rs +++ b/pallets/subtensor/src/macros/genesis.rs @@ -124,7 +124,6 @@ mod genesis { Consensus::::mutate(netuid, |v| v.push(0)); Incentive::::mutate(NetUidStorageIndex::from(netuid), |v| v.push(0)); Dividends::::mutate(netuid, |v| v.push(0)); - LastUpdate::::mutate(NetUidStorageIndex::from(netuid), |v| v.push(block_number)); PruningScores::::mutate(netuid, |v| v.push(0)); ValidatorTrust::::mutate(netuid, |v| v.push(0)); ValidatorPermit::::mutate(netuid, |v| v.push(false)); diff --git a/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs b/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs index 260904395d..932674808a 100644 --- a/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs +++ b/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs @@ -54,6 +54,7 @@ pub fn migrate_delete_subnet_21() -> Weight { info!(target: LOG_TARGET, ">>> Removing subnet 21 {onchain_version:?}"); let netuid = NetUid::from(21); + let subnetwork_n = SubnetworkN::::get(netuid); // We do this all manually as we don't want to call code related to giving subnet owner back their locked token cost. // Remove network count @@ -87,7 +88,7 @@ pub fn migrate_delete_subnet_21() -> Weight { Consensus::::remove(netuid); Dividends::::remove(netuid); PruningScores::::remove(netuid); - LastUpdate::::remove(NetUidStorageIndex::from(netuid)); + Pallet::::set_weights_rl_last_seen_for_uids(netuid, 0.into(), subnetwork_n, None); ValidatorPermit::::remove(netuid); ValidatorTrust::::remove(netuid); diff --git a/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs b/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs index 0cda0f0e06..30eba689af 100644 --- a/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs +++ b/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs @@ -57,6 +57,7 @@ pub fn migrate_delete_subnet_3() -> Weight { ); let netuid = NetUid::from(3); + let subnetwork_n = SubnetworkN::::get(netuid); // Remove network count SubnetworkN::::remove(netuid); @@ -89,7 +90,7 @@ pub fn migrate_delete_subnet_3() -> Weight { Consensus::::remove(netuid); Dividends::::remove(netuid); PruningScores::::remove(netuid); - LastUpdate::::remove(NetUidStorageIndex::from(netuid)); + Pallet::::set_weights_rl_last_seen_for_uids(netuid, 0.into(), subnetwork_n, None); ValidatorPermit::::remove(netuid); ValidatorTrust::::remove(netuid); diff --git a/pallets/subtensor/src/rpc_info/metagraph.rs b/pallets/subtensor/src/rpc_info/metagraph.rs index ea24657aeb..7388080b3e 100644 --- a/pallets/subtensor/src/rpc_info/metagraph.rs +++ b/pallets/subtensor/src/rpc_info/metagraph.rs @@ -752,10 +752,14 @@ impl Pallet { .into_iter() .map(Compact::from) .collect(), // Pruning per UID - last_update: LastUpdate::::get(NetUidStorageIndex::from(netuid)) - .into_iter() - .map(Compact::from) - .collect(), // Last update per UID + last_update: Self::weights_rl_last_seen_for_uids( + netuid, + MechId::from(0u8), + Self::get_subnetwork_n(netuid), + ) + .into_iter() + .map(Compact::from) + .collect(), // Last update per UID emission: Emission::::get(netuid) .into_iter() .map(Compact::from) @@ -820,10 +824,11 @@ impl Pallet { // Update with mechanism information meta.netuid = NetUid::from(u16::from(netuid_index)).into(); - meta.last_update = LastUpdate::::get(netuid_index) - .into_iter() - .map(Compact::from) - .collect(); + meta.last_update = + Self::weights_rl_last_seen_for_uids(netuid, mecid, Self::get_subnetwork_n(netuid)) + .into_iter() + .map(Compact::from) + .collect(); meta.incentives = Incentive::::get(netuid_index) .into_iter() .map(Compact::from) @@ -1274,10 +1279,14 @@ impl Pallet { Some(SelectiveMetagraphIndex::LastUpdate) => SelectiveMetagraph { netuid: netuid.into(), last_update: Some( - LastUpdate::::get(NetUidStorageIndex::from(netuid)) - .into_iter() - .map(Compact::from) - .collect(), + Self::weights_rl_last_seen_for_uids( + netuid, + MechId::from(0u8), + Self::get_subnetwork_n(netuid), + ) + .into_iter() + .map(Compact::from) + .collect(), ), ..Default::default() }, @@ -1476,10 +1485,14 @@ impl Pallet { Some(SelectiveMetagraphIndex::LastUpdate) => SelectiveMetagraph { netuid: netuid.into(), last_update: Some( - LastUpdate::::get(netuid_index) - .into_iter() - .map(Compact::from) - .collect(), + Self::weights_rl_last_seen_for_uids( + netuid, + mecid, + Self::get_subnetwork_n(netuid), + ) + .into_iter() + .map(Compact::from) + .collect(), ), ..Default::default() }, diff --git a/pallets/subtensor/src/rpc_info/neuron_info.rs b/pallets/subtensor/src/rpc_info/neuron_info.rs index 6e29a51ef5..d053b77a54 100644 --- a/pallets/subtensor/src/rpc_info/neuron_info.rs +++ b/pallets/subtensor/src/rpc_info/neuron_info.rs @@ -2,7 +2,9 @@ use super::*; use frame_support::pallet_prelude::{Decode, Encode}; extern crate alloc; use codec::Compact; -use subtensor_runtime_common::{AlphaCurrency, NetUid, NetUidStorageIndex}; +use rate_limiting_interface::RateLimitingInterface; +use sp_runtime::SaturatedConversion; +use subtensor_runtime_common::{AlphaCurrency, MechId, NetUid, NetUidStorageIndex, rate_limiting}; #[freeze_struct("9e5a291e7e71482d")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] @@ -93,7 +95,11 @@ impl Pallet { let validator_trust = Self::get_validator_trust_for_uid(netuid, uid); let dividends = Self::get_dividends_for_uid(netuid, uid); let pruning_score = Self::get_pruning_score_for_uid(netuid, uid); - let last_update = Self::get_last_update_for_uid(NetUidStorageIndex::from(netuid), uid); + let usage = Self::weights_rl_usage_key_for_uid(netuid, MechId::from(0u8), uid); + let last_update = + T::RateLimiting::last_seen(rate_limiting::GROUP_WEIGHTS_SUBNET, Some(usage)) + .map(|block| block.saturated_into::()) + .unwrap_or(0); let validator_permit = Self::get_validator_permit_for_uid(netuid, uid); let weights = Weights::::get(NetUidStorageIndex::from(netuid), uid) @@ -179,7 +185,11 @@ impl Pallet { let validator_trust = Self::get_validator_trust_for_uid(netuid, uid); let dividends = Self::get_dividends_for_uid(netuid, uid); let pruning_score = Self::get_pruning_score_for_uid(netuid, uid); - let last_update = Self::get_last_update_for_uid(NetUidStorageIndex::from(netuid), uid); + let usage = Self::weights_rl_usage_key_for_uid(netuid, MechId::from(0u8), uid); + let last_update = + T::RateLimiting::last_seen(rate_limiting::GROUP_WEIGHTS_SUBNET, Some(usage)) + .map(|block| block.saturated_into::()) + .unwrap_or(0); let validator_permit = Self::get_validator_permit_for_uid(netuid, uid); let stake: Vec<(T::AccountId, Compact)> = vec![( diff --git a/pallets/subtensor/src/rpc_info/show_subnet.rs b/pallets/subtensor/src/rpc_info/show_subnet.rs index abd9670bb8..a7cef1f147 100644 --- a/pallets/subtensor/src/rpc_info/show_subnet.rs +++ b/pallets/subtensor/src/rpc_info/show_subnet.rs @@ -103,10 +103,14 @@ impl Pallet { .into_iter() .map(Compact::from) .collect(); - let last_update: Vec> = LastUpdate::::get(NetUidStorageIndex::from(netuid)) - .into_iter() - .map(Compact::from) - .collect(); + let last_update: Vec> = Self::weights_rl_last_seen_for_uids( + netuid, + subtensor_runtime_common::MechId::from(0u8), + n, + ) + .into_iter() + .map(Compact::from) + .collect(); let emission = Emission::::get(netuid) .into_iter() .map(Compact::from) diff --git a/pallets/subtensor/src/subnets/mechanism.rs b/pallets/subtensor/src/subnets/mechanism.rs index 481974ef05..6de2e18011 100644 --- a/pallets/subtensor/src/subnets/mechanism.rs +++ b/pallets/subtensor/src/subnets/mechanism.rs @@ -132,6 +132,7 @@ impl Pallet { let new_count_u8 = u8::from(new_count); if old_count != new_count_u8 { if old_count > new_count_u8 { + let subnet_n = SubnetworkN::::get(netuid); for mecid in new_count_u8..old_count { let netuid_index = Self::get_mechanism_storage_index(netuid, MechId::from(mecid)); @@ -142,8 +143,13 @@ impl Pallet { // Cleanup Incentive Incentive::::remove(netuid_index); - // Cleanup LastUpdate - LastUpdate::::remove(netuid_index); + // Cleanup last-seen + Self::set_weights_rl_last_seen_for_uids( + netuid, + MechId::from(mecid), + subnet_n, + None, + ); // Cleanup Bonds let _ = Bonds::::clear_prefix(netuid_index, u32::MAX, None); diff --git a/pallets/subtensor/src/subnets/uids.rs b/pallets/subtensor/src/subnets/uids.rs index 0a09017e64..24f3d9f96c 100644 --- a/pallets/subtensor/src/subnets/uids.rs +++ b/pallets/subtensor/src/subnets/uids.rs @@ -1,9 +1,10 @@ use super::*; use frame_support::storage::IterableStorageDoubleMap; -use sp_runtime::Percent; +use rate_limiting_interface::RateLimitingInterface; +use sp_runtime::{Percent, SaturatedConversion}; use sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet}; use sp_std::{cmp, vec}; -use subtensor_runtime_common::NetUid; +use subtensor_runtime_common::{NetUid, rate_limiting}; impl Pallet { /// Returns the number of filled slots on a network. @@ -114,7 +115,12 @@ impl Pallet { for mecid in 0..MechanismCountCurrent::::get(netuid).into() { let netuid_index = Self::get_mechanism_storage_index(netuid, mecid.into()); Incentive::::mutate(netuid_index, |v| v.push(0)); - Self::set_last_update_for_uid(netuid_index, next_uid, block_number); + let usage = Self::weights_rl_usage_key_for_uid(netuid, mecid.into(), next_uid); + T::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + Some(block_number.saturated_into()), + ); } Dividends::::mutate(netuid, |v| v.push(0)); ValidatorTrust::::mutate(netuid, |v| v.push(0)); @@ -265,19 +271,37 @@ impl Pallet { // Update incentives/lastupdates for mechanisms for mecid in 0..mechanisms_count { - let netuid_index = Self::get_mechanism_storage_index(netuid, mecid.into()); + let mecid = mecid.into(); + let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); let incentive = Incentive::::get(netuid_index); - let lastupdate = LastUpdate::::get(netuid_index); let mut trimmed_incentive = Vec::with_capacity(trimmed_uids.len()); - let mut trimmed_lastupdate = Vec::with_capacity(trimmed_uids.len()); + let mut trimmed_last_seen = Vec::with_capacity(trimmed_uids.len()); for uid in &trimmed_uids { trimmed_incentive.push(incentive.get(*uid).cloned().unwrap_or_default()); - trimmed_lastupdate.push(lastupdate.get(*uid).cloned().unwrap_or_default()); + let usage = Self::weights_rl_usage_key_for_uid(netuid, mecid, *uid as u16); + let last_seen = T::RateLimiting::last_seen( + subtensor_runtime_common::rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + ); + trimmed_last_seen.push(last_seen); } Incentive::::insert(netuid_index, trimmed_incentive); - LastUpdate::::insert(netuid_index, trimmed_lastupdate); + + Self::set_weights_rl_last_seen_for_uids(netuid, mecid, current_n, None); + + for (uid, last_seen) in trimmed_last_seen.into_iter().enumerate() { + let Some(block) = last_seen else { + continue; + }; + let usage = Self::weights_rl_usage_key_for_uid(netuid, mecid, uid as u16); + T::RateLimiting::set_last_seen( + subtensor_runtime_common::rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + Some(block), + ); + } } // Create mapping from old uid to new compressed uid diff --git a/pallets/subtensor/src/subnets/weights.rs b/pallets/subtensor/src/subnets/weights.rs index ac96f68720..c8389f6639 100644 --- a/pallets/subtensor/src/subnets/weights.rs +++ b/pallets/subtensor/src/subnets/weights.rs @@ -128,9 +128,6 @@ impl Pallet { commit_hash, )); - // 12. Update the last commit block for the hotkey's UID. - Self::set_last_update_for_uid(netuid_index, neuron_uid, commit_block); - // 13. Return success. Ok(()) }) @@ -238,8 +235,6 @@ impl Pallet { /// 4. Appends `(hotkey, commit_block, commit, reveal_round)` to /// `TimelockedWeightCommits[netuid][epoch]`. /// 5. Emits `TimelockedWeightsCommitted` with the Blake2 hash of `commit`. - /// 6. Updates `LastUpdateForUid` so subsequent rate-limit checks include this - /// commit. /// /// # Raises /// * `CommitRevealDisabled` – Commit-reveal is disabled on `netuid`. @@ -359,9 +354,6 @@ impl Pallet { reveal_round, )); - // 10. Update the last commit block for the hotkey's UID. - Self::set_last_update_for_uid(netuid_index, neuron_uid, commit_block); - // 11. Return success. Ok(()) }, @@ -827,11 +819,6 @@ impl Pallet { // --- 17. Set weights under netuid_index (sub-subnet), uid double map entry. Weights::::insert(netuid_index, neuron_uid, zipped_weights); - // --- 18. Set the activity for the weights on this network. - if !Self::get_commit_reveal_weights_enabled(netuid) { - Self::set_last_update_for_uid(netuid_index, neuron_uid, current_block); - } - // --- 19. Emit the tracking event. log::debug!("WeightsSet( netuid:{netuid_index:?}, neuron_uid:{neuron_uid:?} )"); Self::deposit_event(Event::WeightsSet(netuid_index, neuron_uid)); diff --git a/pallets/subtensor/src/tests/children.rs b/pallets/subtensor/src/tests/children.rs index 146c94516a..c3d2a03ec9 100644 --- a/pallets/subtensor/src/tests/children.rs +++ b/pallets/subtensor/src/tests/children.rs @@ -5,8 +5,9 @@ use super::mock; use super::mock::*; use approx::assert_abs_diff_eq; use frame_support::{assert_err, assert_noop, assert_ok}; +use sp_runtime::traits::SaturatedConversion; use substrate_fixed::types::{I64F64, I96F32, U96F32}; -use subtensor_runtime_common::{AlphaCurrency, NetUidStorageIndex, TaoCurrency}; +use subtensor_runtime_common::{AlphaCurrency, MechId, NetUidStorageIndex, TaoCurrency}; use subtensor_swap_interface::SwapHandler; use crate::{utils::rate_limiting::TransactionType, *}; @@ -2906,7 +2907,12 @@ fn test_childkey_take_drain() { BlockAtRegistration::::set(netuid, 0, 1); BlockAtRegistration::::set(netuid, 1, 1); BlockAtRegistration::::set(netuid, 2, 1); - LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); + SubtensorModule::set_weights_rl_last_seen_for_uids( + netuid, + MechId::from(0u8), + 3, + Some(2u64.saturated_into()), + ); Kappa::::set(netuid, u16::MAX / 5); ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active ValidatorPermit::::insert(netuid, vec![true, true, false]); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index de42cf6b3b..83f3a25552 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -14,11 +14,12 @@ use approx::assert_abs_diff_eq; use frame_support::assert_ok; use pallet_subtensor_swap::position::PositionId; use sp_core::U256; +use sp_runtime::traits::SaturatedConversion; use substrate_fixed::{ transcendental::sqrt, types::{I64F64, I96F32, U64F64, U96F32}, }; -use subtensor_runtime_common::{AlphaCurrency, NetUidStorageIndex}; +use subtensor_runtime_common::{AlphaCurrency, MechId, NetUidStorageIndex}; use subtensor_swap_interface::{SwapEngine, SwapHandler}; #[allow(clippy::arithmetic_side_effects)] @@ -3040,7 +3041,12 @@ fn test_mining_emission_distribution_with_no_root_sell() { BlockAtRegistration::::set(netuid, 0, 1); BlockAtRegistration::::set(netuid, 1, 1); BlockAtRegistration::::set(netuid, 2, 1); - LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); + SubtensorModule::set_weights_rl_last_seen_for_uids( + netuid, + MechId::from(0u8), + 3, + Some(2u64.saturated_into()), + ); Kappa::::set(netuid, u16::MAX / 5); ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active ValidatorPermit::::insert(netuid, vec![true, true, false]); @@ -3234,7 +3240,12 @@ fn test_mining_emission_distribution_with_root_sell() { BlockAtRegistration::::set(netuid, 0, 1); BlockAtRegistration::::set(netuid, 1, 1); BlockAtRegistration::::set(netuid, 2, 1); - LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); + SubtensorModule::set_weights_rl_last_seen_for_uids( + netuid, + MechId::from(0u8), + 3, + Some(2u64.saturated_into()), + ); Kappa::::set(netuid, u16::MAX / 5); ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active ValidatorPermit::::insert(netuid, vec![true, true, false]); diff --git a/pallets/subtensor/src/tests/epoch.rs b/pallets/subtensor/src/tests/epoch.rs index c0be00b0fd..e64bbe5c4d 100644 --- a/pallets/subtensor/src/tests/epoch.rs +++ b/pallets/subtensor/src/tests/epoch.rs @@ -10,9 +10,13 @@ use std::time::Instant; use approx::assert_abs_diff_eq; use frame_support::{assert_err, assert_ok}; use rand::{Rng, SeedableRng, distributions::Uniform, rngs::StdRng, seq::SliceRandom, thread_rng}; +use rate_limiting_interface::RateLimitingInterface; use sp_core::{Get, U256}; +use sp_runtime::traits::SaturatedConversion; use substrate_fixed::types::I32F32; -use subtensor_runtime_common::{AlphaCurrency, NetUidStorageIndex, TaoCurrency}; +use subtensor_runtime_common::{ + AlphaCurrency, MechId, NetUidStorageIndex, TaoCurrency, rate_limiting, +}; use subtensor_swap_interface::SwapHandler; use super::mock::*; @@ -2491,7 +2495,15 @@ fn test_can_set_self_weight_as_subnet_owner() { step_block(1); // Set updated so weights are valid - LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![2, 0]); + let mecid = MechId::from(0u8); + for (uid, last_seen) in [2u64, 0u64].iter().copied().enumerate() { + let usage = SubtensorModule::weights_rl_usage_key_for_uid(netuid, mecid, uid as u16); + ::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + Some(last_seen.saturated_into()), + ); + } // Run epoch let hotkey_emission = SubtensorModule::epoch(netuid, to_emit.into()); @@ -3809,10 +3821,10 @@ fn test_epoch_does_not_mask_outside_window_but_masks_inside() { }); } -// Test an epoch doesn't panic when LastUpdate size doesn't match to Weights size. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::test_last_update_size_mismatch --exact --show-output --nocapture +// Test an epoch doesn't panic when weights last-seen entries are missing. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::test_missing_last_seen_does_not_panic --exact --show-output --nocapture #[test] -fn test_last_update_size_mismatch() { +fn test_missing_last_seen_does_not_panic() { new_test_ext(1).execute_with(|| { log::info!("test_1_graph:"); let netuid = NetUid::from(1); @@ -3845,9 +3857,6 @@ fn test_last_update_size_mismatch() { 0 )); - // Set mismatching LastUpdate vector - LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![1, 1, 1]); - SubtensorModule::epoch(netuid, 1_000_000_000.into()); assert_eq!( SubtensorModule::get_total_stake_for_hotkey(&hotkey), diff --git a/pallets/subtensor/src/tests/epoch_logs.rs b/pallets/subtensor/src/tests/epoch_logs.rs index 420a9e0eac..85bef59a1c 100644 --- a/pallets/subtensor/src/tests/epoch_logs.rs +++ b/pallets/subtensor/src/tests/epoch_logs.rs @@ -10,10 +10,12 @@ use super::mock::*; use crate::*; use frame_support::assert_ok; +use rate_limiting_interface::RateLimitingInterface; use sp_core::U256; +use sp_runtime::traits::SaturatedConversion; use std::io::{Result as IoResult, Write}; use std::sync::{Arc, Mutex}; -use subtensor_runtime_common::{AlphaCurrency, MechId}; +use subtensor_runtime_common::{AlphaCurrency, MechId, rate_limiting}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt}; const NETUID: u16 = 1; @@ -87,8 +89,15 @@ fn setup_epoch(neurons: Vec, mechanism_count: u8) { ValidatorPermit::::insert(netuid, permit_vec); for m in 0..mechanism_count { - let netuid_index = SubtensorModule::get_mechanism_storage_index(netuid, m.into()); - LastUpdate::::insert(netuid_index, last_update_vec.clone()); + let mecid = MechId::from(m); + for (uid, last_seen) in last_update_vec.iter().copied().enumerate() { + let usage = SubtensorModule::weights_rl_usage_key_for_uid(netuid, mecid, uid as u16); + ::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + Some(last_seen.saturated_into()), + ); + } } } diff --git a/pallets/subtensor/src/tests/mechanism.rs b/pallets/subtensor/src/tests/mechanism.rs index d096cd0515..4a183c99f6 100644 --- a/pallets/subtensor/src/tests/mechanism.rs +++ b/pallets/subtensor/src/tests/mechanism.rs @@ -27,7 +27,7 @@ // - [x] Incentives are per mechanism // - [x] Per-mechanism incentives are distributed proportionally to miner weights // - [x] Mechanism limit can be set up to 8 (with admin pallet) -// - [x] When reduction of mechanism limit occurs, Weights, Incentive, LastUpdate, Bonds, and WeightCommits are cleared +// - [x] When reduction of mechanism limit occurs, Weights, Incentive, last-seen, Bonds, and WeightCommits are cleared // - [x] Epoch terms of subnet are weighted sum (or logical OR) of all mechanism epoch terms // - [x] Subnet epoch terms persist in state // - [x] Mechanism epoch terms persist in state @@ -48,12 +48,13 @@ use frame_support::{assert_noop, assert_ok}; use frame_system::RawOrigin; use pallet_drand::types::Pulse; use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng}; +use rate_limiting_interface::RateLimitingInterface; use sha2::Digest; use sp_core::{H256, U256}; -use sp_runtime::traits::{BlakeTwo256, Hash}; +use sp_runtime::traits::{BlakeTwo256, Hash, SaturatedConversion}; use sp_std::collections::vec_deque::VecDeque; use substrate_fixed::types::{I32F32, U64F64}; -use subtensor_runtime_common::{MechId, NetUid, NetUidStorageIndex}; +use subtensor_runtime_common::{MechId, NetUid, NetUidStorageIndex, rate_limiting}; use tle::{ curves::drand::TinyBLS381, ibe::fullident::Identity, stream_ciphers::AESGCMStreamCipherProvider, tlock::tle, @@ -302,7 +303,13 @@ fn update_mechanism_counts_decreases_and_cleans() { Weights::::insert(idx_keep, 0u16, vec![(1u16, 1u16)]); Incentive::::insert(idx_keep, vec![1u16]); - LastUpdate::::insert(idx_keep, vec![123u64]); + let keep_usage = + SubtensorModule::weights_rl_usage_key_for_uid(netuid, MechId::from(1u8), 0); + ::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(keep_usage), + Some(123u64.saturated_into()), + ); Bonds::::insert(idx_keep, 0u16, vec![(1u16, 2u16)]); WeightCommits::::insert( idx_keep, @@ -317,7 +324,13 @@ fn update_mechanism_counts_decreases_and_cleans() { Weights::::insert(idx_rm3, 0u16, vec![(9u16, 9u16)]); Incentive::::insert(idx_rm3, vec![9u16]); - LastUpdate::::insert(idx_rm3, vec![999u64]); + let removed_usage = + SubtensorModule::weights_rl_usage_key_for_uid(netuid, MechId::from(2u8), 0); + ::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(removed_usage), + Some(999u64.saturated_into()), + ); Bonds::::insert(idx_rm3, 0u16, vec![(9u16, 9u16)]); WeightCommits::::insert( idx_rm3, @@ -339,7 +352,14 @@ fn update_mechanism_counts_decreases_and_cleans() { // Kept prefix intact assert_eq!(Incentive::::get(idx_keep), vec![1u16]); assert!(Weights::::iter_prefix(idx_keep).next().is_some()); - assert!(LastUpdate::::contains_key(idx_keep)); + let keep_usage = + SubtensorModule::weights_rl_usage_key_for_uid(netuid, MechId::from(1u8), 0); + let kept_last_seen = ::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(keep_usage), + ) + .map(|block| block.saturated_into::()); + assert_eq!(kept_last_seen, Some(123)); assert!(Bonds::::iter_prefix(idx_keep).next().is_some()); assert!(WeightCommits::::contains_key(idx_keep, hotkey)); assert!(TimelockedWeightCommits::::contains_key( @@ -349,7 +369,15 @@ fn update_mechanism_counts_decreases_and_cleans() { // Removed prefix (mecid 3) cleared assert!(Weights::::iter_prefix(idx_rm3).next().is_none()); assert_eq!(Incentive::::get(idx_rm3), Vec::::new()); - assert!(!LastUpdate::::contains_key(idx_rm3)); + let removed_usage = + SubtensorModule::weights_rl_usage_key_for_uid(netuid, MechId::from(2u8), 0); + assert!( + ::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(removed_usage), + ) + .is_none() + ); assert!(Bonds::::iter_prefix(idx_rm3).next().is_none()); assert!(!WeightCommits::::contains_key(idx_rm3, hotkey)); assert!(!TimelockedWeightCommits::::contains_key( @@ -454,8 +482,18 @@ pub fn mock_epoch_state(netuid: NetUid, ck0: U256, hk0: U256, ck1: U256, hk1: U2 // Make both ACTIVE: recent updates & old registrations. Tempo::::insert(netuid, 1u16); ActivityCutoff::::insert(netuid, u16::MAX); // large cutoff keeps them active - LastUpdate::::insert(idx0, vec![2, 2]); - LastUpdate::::insert(idx1, vec![2, 2]); + SubtensorModule::set_weights_rl_last_seen_for_uids( + netuid, + MechId::from(0u8), + 2, + Some(2u64.saturated_into()), + ); + SubtensorModule::set_weights_rl_last_seen_for_uids( + netuid, + MechId::from(1u8), + 2, + Some(2u64.saturated_into()), + ); BlockAtRegistration::::insert(netuid, 0, 1u64); // registered long ago BlockAtRegistration::::insert(netuid, 1, 1u64); @@ -490,13 +528,20 @@ pub fn mock_epoch_state(netuid: NetUid, ck0: U256, hk0: U256, ck1: U256, hk1: U2 } pub fn mock_3_neurons(netuid: NetUid, hk: U256) { - let idx0 = SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(0)); - let idx1 = SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(1)); - SubnetworkN::::insert(netuid, 3); Keys::::insert(netuid, 2u16, hk); - LastUpdate::::insert(idx0, vec![2, 2, 2]); - LastUpdate::::insert(idx1, vec![2, 2, 2]); + SubtensorModule::set_weights_rl_last_seen_for_uids( + netuid, + MechId::from(0u8), + 3, + Some(2u64.saturated_into()), + ); + SubtensorModule::set_weights_rl_last_seen_for_uids( + netuid, + MechId::from(1u8), + 3, + Some(2u64.saturated_into()), + ); BlockAtRegistration::::insert(netuid, 2, 1u64); } @@ -1389,7 +1434,6 @@ fn epoch_mechanism_emergency_mode_distributes_by_stake() { // setup a single sub-subnet where consensus sum becomes 0 let netuid = NetUid::from(1u16); let mecid = MechId::from(1u8); - let idx = SubtensorModule::get_mechanism_storage_index(netuid, mecid); let tempo: u16 = 5; add_network(netuid, tempo, 0); MechanismCountCurrent::::insert(netuid, MechId::from(2u8)); // allow subids {0,1} @@ -1413,7 +1457,12 @@ fn epoch_mechanism_emergency_mode_distributes_by_stake() { // active + recent updates so they're all active let now = SubtensorModule::get_current_block_as_u64(); ActivityCutoff::::insert(netuid, 1_000u16); - LastUpdate::::insert(idx, vec![now, now, now, now]); + SubtensorModule::set_weights_rl_last_seen_for_uids( + netuid, + mecid, + 4, + Some(now.saturated_into()), + ); // All staking validators permitted => active_stake = stake ValidatorPermit::::insert(netuid, vec![true, true, true, false]); diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 3ef476fa00..16ba1ee8db 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -5,10 +5,12 @@ use crate::migrations::migrate_network_immunity_period; use crate::*; use frame_support::{assert_err, assert_ok}; use frame_system::Config; +use rate_limiting_interface::RateLimitingInterface; use sp_core::U256; +use sp_runtime::traits::SaturatedConversion; use sp_std::collections::{btree_map::BTreeMap, vec_deque::VecDeque}; use substrate_fixed::types::{I96F32, U64F64, U96F32}; -use subtensor_runtime_common::{MechId, NetUidStorageIndex, TaoCurrency}; +use subtensor_runtime_common::{MechId, NetUidStorageIndex, TaoCurrency, rate_limiting}; use subtensor_swap_interface::{Order, SwapHandler}; #[test] @@ -325,7 +327,12 @@ fn dissolve_clears_all_per_subnet_storages() { Consensus::::insert(net, vec![1u16]); Dividends::::insert(net, vec![1u16]); PruningScores::::insert(net, vec![1u16]); - LastUpdate::::insert(NetUidStorageIndex::from(net), vec![0u64]); + let usage = SubtensorModule::weights_rl_usage_key_for_uid(net, MechId::from(0u8), 0); + ::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + Some(1u64.saturated_into()), + ); ValidatorPermit::::insert(net, vec![true]); ValidatorTrust::::insert(net, vec![1u16]); @@ -475,9 +482,14 @@ fn dissolve_clears_all_per_subnet_storages() { assert!(!Consensus::::contains_key(net)); assert!(!Dividends::::contains_key(net)); assert!(!PruningScores::::contains_key(net)); - assert!(!LastUpdate::::contains_key(NetUidStorageIndex::from( - net - ))); + let usage = SubtensorModule::weights_rl_usage_key_for_uid(net, MechId::from(0u8), 0); + assert!( + ::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + ) + .is_none() + ); assert!(!ValidatorPermit::::contains_key(net)); assert!(!ValidatorTrust::::contains_key(net)); @@ -2246,9 +2258,19 @@ fn dissolve_clears_all_mechanism_scoped_maps_for_all_mechanisms() { Incentive::::insert(idx0, vec![1u16, 2u16]); Incentive::::insert(idx1, vec![3u16, 4u16]); - // --- LastUpdate (MAP: netuid_index -> Vec) - LastUpdate::::insert(idx0, vec![42u64]); - LastUpdate::::insert(idx1, vec![84u64]); + // --- Last-seen (usage-key -> block) + let usage0 = SubtensorModule::weights_rl_usage_key_for_uid(net, m0, 0); + let usage1 = SubtensorModule::weights_rl_usage_key_for_uid(net, m1, 0); + ::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage0), + Some(42u64.saturated_into()), + ); + ::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage1), + Some(84u64.saturated_into()), + ); // Sanity: keys are present before dissolve. assert!(Weights::::contains_key(idx0, 0u16)); @@ -2259,8 +2281,20 @@ fn dissolve_clears_all_mechanism_scoped_maps_for_all_mechanisms() { assert!(TimelockedWeightCommits::::contains_key(idx1, 2u64)); assert!(Incentive::::contains_key(idx0)); assert!(Incentive::::contains_key(idx1)); - assert!(LastUpdate::::contains_key(idx0)); - assert!(LastUpdate::::contains_key(idx1)); + let usage0 = SubtensorModule::weights_rl_usage_key_for_uid(net, m0, 0); + let usage1 = SubtensorModule::weights_rl_usage_key_for_uid(net, m1, 0); + let last0 = ::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage0), + ) + .map(|block| block.saturated_into::()); + let last1 = ::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage1), + ) + .map(|block| block.saturated_into::()); + assert_eq!(last0, Some(42)); + assert_eq!(last1, Some(84)); assert!(MechanismCountCurrent::::contains_key(net)); // --- Dissolve the subnet --- @@ -2297,8 +2331,22 @@ fn dissolve_clears_all_mechanism_scoped_maps_for_all_mechanisms() { // Single-map per-mechanism vectors cleared. assert!(!Incentive::::contains_key(idx0)); assert!(!Incentive::::contains_key(idx1)); - assert!(!LastUpdate::::contains_key(idx0)); - assert!(!LastUpdate::::contains_key(idx1)); + let usage0 = SubtensorModule::weights_rl_usage_key_for_uid(net, m0, 0); + let usage1 = SubtensorModule::weights_rl_usage_key_for_uid(net, m1, 0); + assert!( + ::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage0), + ) + .is_none() + ); + assert!( + ::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage1), + ) + .is_none() + ); // MechanismCountCurrent cleared assert!(!MechanismCountCurrent::::contains_key(net)); diff --git a/pallets/subtensor/src/tests/registration.rs b/pallets/subtensor/src/tests/registration.rs index c82e173907..a2822c616a 100644 --- a/pallets/subtensor/src/tests/registration.rs +++ b/pallets/subtensor/src/tests/registration.rs @@ -7,9 +7,14 @@ use frame_support::sp_runtime::{DispatchError, transaction_validity::Transaction use frame_support::traits::Currency; use frame_support::{assert_err, assert_noop, assert_ok}; use frame_system::{Config, RawOrigin}; +use rate_limiting_interface::RateLimitingInterface; use sp_core::U256; -use sp_runtime::traits::{DispatchInfoOf, TransactionExtension, TxBaseImplication}; -use subtensor_runtime_common::{AlphaCurrency, Currency as CurrencyT, NetUid, NetUidStorageIndex}; +use sp_runtime::traits::{ + DispatchInfoOf, SaturatedConversion, TransactionExtension, TxBaseImplication, +}; +use subtensor_runtime_common::{ + AlphaCurrency, Currency as CurrencyT, MechId, NetUid, rate_limiting, +}; use super::mock; use super::mock::*; @@ -2142,8 +2147,16 @@ fn test_last_update_correctness() { let existing_neurons = 3; SubnetworkN::::insert(netuid, existing_neurons); - // Simulate no LastUpdate so far (can happen on mechanisms) - LastUpdate::::remove(NetUidStorageIndex::from(netuid)); + // Simulate no last-seen so far (can happen on mechanisms) + let mecid = MechId::from(0u8); + let existing_usage = SubtensorModule::weights_rl_usage_key_for_uid(netuid, mecid, 0); + assert!( + ::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(existing_usage), + ) + .is_none() + ); // Give some $$$ to coldkey SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 10000); @@ -2154,11 +2167,15 @@ fn test_last_update_correctness() { hotkey_account_id )); - // Check that LastUpdate has existing_neurons + 1 elements now - assert_eq!( - LastUpdate::::get(NetUidStorageIndex::from(netuid)).len(), - (existing_neurons + 1) as usize - ); + // Check that last-seen is set for the new uid + let new_uid = existing_neurons; + let usage = SubtensorModule::weights_rl_usage_key_for_uid(netuid, mecid, new_uid); + let last_seen = ::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + ) + .map(|block| block.saturated_into::()); + assert_eq!(last_seen, Some(SubtensorModule::get_current_block_as_u64())); }); } diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index a096c29723..dd63714f2e 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -11,10 +11,11 @@ use pallet_subtensor_swap::tick::TickIndex; use safe_math::FixedExt; use sp_core::{Get, H256, U256}; use sp_runtime::traits::Dispatchable; +use sp_runtime::traits::SaturatedConversion; use substrate_fixed::traits::FromFixed; use substrate_fixed::types::{I96F32, I110F18, U64F64, U96F32}; use subtensor_runtime_common::{ - AlphaCurrency, Currency as CurrencyT, NetUid, NetUidStorageIndex, TaoCurrency, + AlphaCurrency, Currency as CurrencyT, MechId, NetUid, NetUidStorageIndex, TaoCurrency, }; use subtensor_swap_interface::{Order, SwapHandler}; @@ -2329,7 +2330,12 @@ fn test_mining_emission_distribution_validator_valiminer_miner() { BlockAtRegistration::::set(netuid, 0, 1); BlockAtRegistration::::set(netuid, 1, 1); BlockAtRegistration::::set(netuid, 2, 1); - LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); + SubtensorModule::set_weights_rl_last_seen_for_uids( + netuid, + MechId::from(0u8), + 3, + Some(2u64.saturated_into()), + ); Kappa::::set(netuid, u16::MAX / 5); ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active ValidatorPermit::::insert(netuid, vec![true, true, false]); diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 89d5a1446a..3fb480508a 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -8,7 +8,7 @@ use sp_core::U256; use sp_runtime::{SaturatedConversion, Saturating}; use substrate_fixed::types::{I32F32, I64F64, U64F64, U96F32}; use subtensor_runtime_common::{ - AlphaCurrency, NetUid, NetUidStorageIndex, TaoCurrency, rate_limiting, + AlphaCurrency, MechId, NetUid, NetUidStorageIndex, TaoCurrency, rate_limiting, }; impl Pallet { @@ -173,17 +173,7 @@ impl Pallet { pub fn get_dividends(netuid: NetUid) -> Vec { Dividends::::get(netuid) } - /// Fetch LastUpdate for `netuid` and ensure its length is at least `get_subnetwork_n(netuid)`, - /// padding with zeros if needed. Returns the (possibly padded) vector. - pub fn get_last_update(netuid_index: NetUidStorageIndex) -> Vec { - let netuid = Self::get_netuid(netuid_index); - let target_len = Self::get_subnetwork_n(netuid) as usize; - let mut v = LastUpdate::::get(netuid_index); - if v.len() < target_len { - v.resize(target_len, 0); - } - v - } + pub fn get_pruning_score(netuid: NetUid) -> Vec { PruningScores::::get(netuid) } @@ -197,14 +187,47 @@ impl Pallet { // ================================== // ==== YumaConsensus UID params ==== // ================================== - pub fn set_last_update_for_uid(netuid: NetUidStorageIndex, uid: u16, last_update: u64) { - let mut updated_last_update_vec = Self::get_last_update(netuid); - let Some(updated_last_update) = updated_last_update_vec.get_mut(uid as usize) else { - return; - }; - *updated_last_update = last_update; - LastUpdate::::insert(netuid, updated_last_update_vec); + pub fn weights_rl_usage_key_for_uid( + netuid: NetUid, + mecid: MechId, + uid: u16, + ) -> rate_limiting::RateLimitUsageKey { + if mecid == 0.into() { + rate_limiting::RateLimitUsageKey::::SubnetNeuron { netuid, uid } + } else { + rate_limiting::RateLimitUsageKey::::SubnetMechanismNeuron { + netuid, + mecid, + uid, + } + } } + + pub fn weights_rl_last_seen_for_uids(netuid: NetUid, mecid: MechId, subnet_n: u16) -> Vec { + let mut last_seen = Vec::with_capacity(subnet_n as usize); + for uid in 0..subnet_n { + let usage = Self::weights_rl_usage_key_for_uid(netuid, mecid, uid); + let block = + T::RateLimiting::last_seen(rate_limiting::GROUP_WEIGHTS_SUBNET, Some(usage)) + .map(|block| block.saturated_into::()) + .unwrap_or(0); + last_seen.push(block); + } + last_seen + } + + pub(crate) fn set_weights_rl_last_seen_for_uids( + netuid: NetUid, + mecid: MechId, + subnet_n: u16, + block: Option>, + ) { + for uid in 0..subnet_n { + let usage = Self::weights_rl_usage_key_for_uid(netuid, mecid, uid); + T::RateLimiting::set_last_seen(rate_limiting::GROUP_WEIGHTS_SUBNET, Some(usage), block); + } + } + pub fn set_active_for_uid(netuid: NetUid, uid: u16, active: bool) { let mut updated_active_vec = Self::get_active(netuid); let Some(updated_active) = updated_active_vec.get_mut(uid as usize) else { @@ -254,10 +277,6 @@ impl Pallet { let vec = Dividends::::get(netuid); vec.get(uid as usize).copied().unwrap_or(0) } - pub fn get_last_update_for_uid(netuid: NetUidStorageIndex, uid: u16) -> u64 { - let vec = LastUpdate::::get(netuid); - vec.get(uid as usize).copied().unwrap_or(0) - } pub fn get_pruning_score_for_uid(netuid: NetUid, uid: u16) -> u16 { let vec = PruningScores::::get(netuid); vec.get(uid as usize).copied().unwrap_or(u16::MAX) diff --git a/precompiles/src/metagraph.rs b/precompiles/src/metagraph.rs index cc9652a74b..0bf4b3c9a3 100644 --- a/precompiles/src/metagraph.rs +++ b/precompiles/src/metagraph.rs @@ -2,10 +2,12 @@ use alloc::string::String; use core::marker::PhantomData; use fp_evm::{ExitError, PrecompileFailure, PrecompileHandle}; +use pallet_rate_limiting::RateLimitingInterface; use pallet_subtensor::AxonInfo as SubtensorModuleAxonInfo; use precompile_utils::{EvmResult, solidity::Codec}; use sp_core::{ByteArray, H256}; -use subtensor_runtime_common::{Currency, NetUid}; +use sp_runtime::SaturatedConversion; +use subtensor_runtime_common::{Currency, NetUid, rate_limiting}; use crate::PrecompileExt; @@ -120,10 +122,18 @@ where #[precompile::public("getLastUpdate(uint16,uint16)")] #[precompile::view] fn get_last_update(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { - Ok(pallet_subtensor::Pallet::::get_last_update_for_uid( - netuid.into(), + let usage = rate_limiting::RateLimitUsageKey::::SubnetNeuron { + netuid: netuid.into(), uid, - )) + }; + let block = ::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + ) + .map(|block| block.saturated_into::()) + .unwrap_or(0); + + Ok(block) } #[precompile::public("getIsActive(uint16,uint16)")] diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 4a5226a809..5cc12da72d 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -7,8 +7,8 @@ use pallet_rate_limiting::{ GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitTarget, TransactionIdentifier, }; use pallet_subtensor::{ - self, AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, LastUpdate, - Pallet, Prometheus, + self, AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, Pallet, + Prometheus, }; use sp_runtime::traits::SaturatedConversion; use sp_std::{ @@ -451,8 +451,9 @@ fn build_weights(groups: &mut Vec, commits: &mut Vec) -> u6 ); } - for (index, blocks) in LastUpdate::::iter() { - reads = reads.saturating_add(1); + let (last_updates, last_update_reads) = legacy_storage::last_updates(); + reads = reads.saturating_add(last_update_reads); + for (index, blocks) in last_updates { let (netuid, mecid) = Pallet::::get_netuid_and_subid(index).unwrap_or((NetUid::ROOT, 0.into())); for (uid, last_block) in blocks.into_iter().enumerate() { @@ -1021,7 +1022,6 @@ fn block_number(value: u64) -> Option> { #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { - use codec::Encode; use frame_support::traits::OnRuntimeUpgrade; use frame_system::pallet_prelude::BlockNumberFor; use pallet_rate_limiting::{ @@ -1029,13 +1029,12 @@ mod tests { TransactionIdentifier, }; use pallet_subtensor::{ - AxonInfo, Call as SubtensorCall, HasMigrationRun, LastRateLimitedBlock, LastUpdate, - NetworksAdded, PrometheusInfo, RateLimitKey, TransactionKeyLastBlock, WeightsSetRateLimit, + AxonInfo, Call as SubtensorCall, HasMigrationRun, LastRateLimitedBlock, NetworksAdded, + PrometheusInfo, RateLimitKey, TransactionKeyLastBlock, WeightsSetRateLimit, WeightsVersionKeyRateLimit, utils::rate_limiting::TransactionType, }; use sp_core::{H160, ecdsa}; use sp_io::TestExternalities; - use sp_io::{hashing::twox_128, storage}; use sp_runtime::traits::{SaturatedConversion, Zero}; use super::*; @@ -1047,7 +1046,6 @@ mod tests { const ACCOUNT: [u8; 32] = [7u8; 32]; const DELEGATE_TAKE_GROUP_ID: GroupId = GROUP_DELEGATE_TAKE; - const PALLET_PREFIX: &[u8] = b"SubtensorModule"; type UsageKey = RateLimitUsageKey; fn new_test_ext() -> TestExternalities { @@ -1091,19 +1089,6 @@ mod tests { pallet_rate_limiting::NextGroupId::::kill(); } - fn set_legacy_serving_rate_limit(netuid: NetUid, span: u64) { - let mut key = twox_128(b"SubtensorModule").to_vec(); - key.extend(twox_128(b"ServingRateLimit")); - key.extend(netuid.encode()); - storage::set(&key, &span.encode()); - } - - fn set_legacy_network_rate_limit(span: u64) { - let mut key = twox_128(b"SubtensorModule").to_vec(); - key.extend(twox_128(b"NetworkRateLimit")); - storage::set(&key, &span.encode()); - } - fn parity_check( now: u64, call: RuntimeCall, @@ -1205,9 +1190,12 @@ mod tests { let account: AccountId = ACCOUNT.into(); pallet_subtensor::HasMigrationRun::::remove(MIGRATION_NAME); - put_legacy_value(b"TxRateLimit", 10u64); - put_legacy_value(b"TxDelegateTakeRateLimit", 3u64); - put_last_rate_limited_block(RateLimitKey::LastTxBlock(account.clone()), 5); + legacy_storage::set_tx_rate_limit(10); + legacy_storage::set_tx_delegate_take_rate_limit(3); + legacy_storage::set_last_rate_limited_block( + crate::rate_limiting::legacy::RateLimitKey::LastTxBlock(account.clone()), + 5, + ); let weight = migrate_rate_limiting(); assert!(!weight.is_zero()); @@ -1316,7 +1304,7 @@ mod tests { let span = 5u64; System::set_block_number(now.saturated_into()); LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, now - 1); - set_legacy_network_rate_limit(span); + legacy_storage::set_network_rate_limit(span); Migration::::on_runtime_upgrade(); @@ -1367,7 +1355,7 @@ mod tests { RateLimitKey::LastTxBlockDelegateTake(hot.clone()), now - 1, ); - put_legacy_value(b"TxDelegateTakeRateLimit", span); + legacy_storage::set_tx_delegate_take_rate_limit(span); let call = RuntimeCall::SubtensorModule(SubtensorCall::increase_take { hotkey: hot.clone(), @@ -1439,7 +1427,7 @@ mod tests { let hot = account(50); let netuid = NetUid::from(3u16); let span = 5u64; - set_legacy_serving_rate_limit(netuid, span); + legacy_storage::set_serving_rate_limit(netuid, span); pallet_subtensor::Axons::::insert( netuid, hot.clone(), @@ -1506,11 +1494,11 @@ mod tests { let uid: u16 = 0; let weights_span = 4u64; let tempo = 3u16; - // Ensure subnet exists so LastUpdate is imported. + // Ensure subnet exists so legacy LastUpdate is imported. NetworksAdded::::insert(netuid, true); SubtensorModule::set_tempo(netuid, tempo); WeightsSetRateLimit::::insert(netuid, weights_span); - LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![now - 1]); + legacy_storage::set_last_update(NetUidStorageIndex::from(netuid), vec![now - 1]); let weights_call = RuntimeCall::SubtensorModule(SubtensorCall::set_weights { netuid, @@ -1523,7 +1511,7 @@ mod tests { let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); let legacy_weights = || { - let last = LastUpdate::::get(NetUidStorageIndex::from(netuid)) + let last = legacy_storage::get_last_update(NetUidStorageIndex::from(netuid)) .get(uid as usize) .copied() .unwrap_or_default(); @@ -1631,7 +1619,7 @@ mod tests { fn migration_skips_when_already_run() { new_test_ext().execute_with(|| { pallet_subtensor::HasMigrationRun::::insert(MIGRATION_NAME, true); - put_legacy_value(b"TxRateLimit", 99u64); + legacy_storage::set_tx_rate_limit(99); let base_weight = ::DbWeight::get().reads(1); let weight = migrate_rate_limiting(); @@ -1649,19 +1637,4 @@ mod tests { ); }); } - - fn put_legacy_value(storage_name: &[u8], value: impl Encode) { - let key = storage_key(storage_name); - storage::set(&key, &value.encode()); - } - - fn put_last_rate_limited_block(key: RateLimitKey, block: u64) { - let mut storage_key = storage_key(b"LastRateLimitedBlock"); - storage_key.extend(key.encode()); - storage::set(&storage_key, &block.encode()); - } - - fn storage_key(storage_name: &[u8]) -> Vec { - [twox_128(PALLET_PREFIX), twox_128(storage_name)].concat() - } } diff --git a/runtime/src/rate_limiting/legacy.rs b/runtime/src/rate_limiting/legacy.rs index cab3d37719..de67ca2039 100644 --- a/runtime/src/rate_limiting/legacy.rs +++ b/runtime/src/rate_limiting/legacy.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use codec::{Decode, Encode}; use frame_support::{Identity, migration::storage_key_iter}; use runtime_common::prod_or_fast; @@ -7,7 +9,7 @@ use sp_io::{ storage::{self as io_storage, next_key}, }; use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; -use subtensor_runtime_common::NetUid; +use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; use super::AccountId; use crate::{ @@ -38,10 +40,45 @@ pub mod storage { (items.into_iter().collect(), reads) } + pub fn last_updates() -> (Vec<(NetUidStorageIndex, Vec)>, u64) { + let items: Vec<_> = storage_key_iter::, Identity>( + PALLET_PREFIX, + b"LastUpdate", + ) + .collect(); + let reads = items.len() as u64; + (items, reads) + } + + pub fn set_last_update(netuid_index: NetUidStorageIndex, blocks: Vec) { + let mut key = storage_prefix(PALLET_PREFIX, b"LastUpdate"); + key.extend(netuid_index.encode()); + io_storage::set(&key, &blocks.encode()); + } + + pub fn get_last_update(netuid_index: NetUidStorageIndex) -> Vec { + let mut key = storage_prefix(PALLET_PREFIX, b"LastUpdate"); + key.extend(netuid_index.encode()); + io_storage::get(&key) + .and_then(|bytes| Decode::decode(&mut &bytes[..]).ok()) + .unwrap_or_default() + } + + pub fn set_serving_rate_limit(netuid: NetUid, span: u64) { + let mut key = storage_prefix(PALLET_PREFIX, b"ServingRateLimit"); + key.extend(netuid.encode()); + io_storage::set(&key, &span.encode()); + } + pub fn tx_rate_limit() -> (u64, u64) { value_with_default(b"TxRateLimit", defaults::tx_rate_limit()) } + pub fn set_tx_rate_limit(span: u64) { + let key = storage_prefix(PALLET_PREFIX, b"TxRateLimit"); + io_storage::set(&key, &span.encode()); + } + pub fn tx_delegate_take_rate_limit() -> (u64, u64) { value_with_default( b"TxDelegateTakeRateLimit", @@ -49,6 +86,11 @@ pub mod storage { ) } + pub fn set_tx_delegate_take_rate_limit(span: u64) { + let key = storage_prefix(PALLET_PREFIX, b"TxDelegateTakeRateLimit"); + io_storage::set(&key, &span.encode()); + } + pub fn tx_childkey_take_rate_limit() -> (u64, u64) { value_with_default( b"TxChildkeyTakeRateLimit", @@ -60,6 +102,11 @@ pub mod storage { value_with_default(b"NetworkRateLimit", defaults::network_rate_limit()) } + pub fn set_network_rate_limit(span: u64) { + let key = storage_prefix(PALLET_PREFIX, b"NetworkRateLimit"); + io_storage::set(&key, &span.encode()); + } + pub fn owner_hyperparam_rate_limit() -> (u64, u64) { let (value, reads) = value_with_default::( b"OwnerHyperparamRateLimit", @@ -85,6 +132,12 @@ pub mod storage { (entries, reads) } + pub fn set_last_rate_limited_block(key: RateLimitKey, block: u64) { + let mut storage_key = storage_prefix(PALLET_PREFIX, b"LastRateLimitedBlock"); + storage_key.extend(key.encode()); + io_storage::set(&storage_key, &block.encode()); + } + pub fn transaction_key_last_block() -> (Vec<((AccountId, NetUid, u16), u64)>, u64) { let prefix = storage_prefix(PALLET_PREFIX, b"TransactionKeyLastBlock"); let mut cursor = prefix.clone(); From 8ca7fb199ec3b1df6da13700374f45968bc305b1 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 14 Jan 2026 17:45:33 +0100 Subject: [PATCH 64/95] Fix pallet-subtensor tests --- pallets/subtensor/src/tests/mock.rs | 44 +++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 8f8615e79b..2a141dab9f 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -6,8 +6,7 @@ use core::num::NonZeroU64; -use crate::utils::rate_limiting::TransactionType; -use crate::*; +use codec::{Decode, Encode}; use frame_support::traits::{Contains, Everything, InherentBuilder, InsideBoth, InstanceFilter}; use frame_support::weights::Weight; use frame_support::weights::constants::RocksDbWeight; @@ -20,7 +19,7 @@ use frame_system as system; use frame_system::{EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_utility as pallet_utility; -use rate_limiting_interface::{RateLimitingInterface, TryIntoRateLimitTarget}; +use rate_limiting_interface::{RateLimitTarget, RateLimitingInterface, TryIntoRateLimitTarget}; use sp_core::{ConstU64, Get, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; use sp_runtime::{ @@ -32,6 +31,10 @@ use sp_tracing::tracing_subscriber; use subtensor_runtime_common::{NetUid, TaoCurrency, rate_limiting::RateLimitUsageKey}; use subtensor_swap_interface::{Order, SwapHandler}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; + +use crate::utils::rate_limiting::TransactionType; +use crate::*; + type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. @@ -293,7 +296,7 @@ impl crate::Config for Test { type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; - type RateLimiting = NoRateLimiting; + type RateLimiting = MockRateLimiting; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; } @@ -332,9 +335,9 @@ impl CommitmentsInterface for CommitmentsI { fn purge_netuid(_netuid: NetUid) {} } -pub struct NoRateLimiting; +pub struct MockRateLimiting; -impl RateLimitingInterface for NoRateLimiting { +impl RateLimitingInterface for MockRateLimiting { type GroupId = subtensor_runtime_common::rate_limiting::GroupId; type CallMetadata = RuntimeCall; type Limit = BlockNumber; @@ -349,22 +352,39 @@ impl RateLimitingInterface for NoRateLimiting { } fn last_seen( - _target: TargetArg, - _usage_key: Option, + target: TargetArg, + usage_key: Option, ) -> Option where TargetArg: TryIntoRateLimitTarget, { - None + let target = target + .try_into_rate_limit_target::() + .ok()?; + let mut key = b"mock_rate_limiting:last_seen".to_vec(); + key.extend_from_slice(&target.encode()); + key.extend_from_slice(&usage_key.encode()); + let raw = sp_io::storage::get(&key)?; + Decode::decode(&mut &raw[..]).ok() } fn set_last_seen( - _target: TargetArg, - _usage_key: Option, - _block: Option, + target: TargetArg, + usage_key: Option, + block: Option, ) where TargetArg: TryIntoRateLimitTarget, { + let Ok(target) = target.try_into_rate_limit_target::() else { + return; + }; + let mut key = b"mock_rate_limiting:last_seen".to_vec(); + key.extend_from_slice(&target.encode()); + key.extend_from_slice(&usage_key.encode()); + match block { + Some(value) => sp_io::storage::set(&key, &value.encode()), + None => sp_io::storage::clear(&key), + } } } From 7a5466b7ab78d65b345c25600b91b58c3f112eb5 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 14 Jan 2026 18:27:58 +0100 Subject: [PATCH 65/95] Revert "Fix pallet-subtensor tests" This reverts commit 8ca7fb199ec3b1df6da13700374f45968bc305b1. --- pallets/subtensor/src/tests/mock.rs | 44 ++++++++--------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 2a141dab9f..8f8615e79b 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -6,7 +6,8 @@ use core::num::NonZeroU64; -use codec::{Decode, Encode}; +use crate::utils::rate_limiting::TransactionType; +use crate::*; use frame_support::traits::{Contains, Everything, InherentBuilder, InsideBoth, InstanceFilter}; use frame_support::weights::Weight; use frame_support::weights::constants::RocksDbWeight; @@ -19,7 +20,7 @@ use frame_system as system; use frame_system::{EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_utility as pallet_utility; -use rate_limiting_interface::{RateLimitTarget, RateLimitingInterface, TryIntoRateLimitTarget}; +use rate_limiting_interface::{RateLimitingInterface, TryIntoRateLimitTarget}; use sp_core::{ConstU64, Get, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; use sp_runtime::{ @@ -31,10 +32,6 @@ use sp_tracing::tracing_subscriber; use subtensor_runtime_common::{NetUid, TaoCurrency, rate_limiting::RateLimitUsageKey}; use subtensor_swap_interface::{Order, SwapHandler}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; - -use crate::utils::rate_limiting::TransactionType; -use crate::*; - type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. @@ -296,7 +293,7 @@ impl crate::Config for Test { type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; - type RateLimiting = MockRateLimiting; + type RateLimiting = NoRateLimiting; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; } @@ -335,9 +332,9 @@ impl CommitmentsInterface for CommitmentsI { fn purge_netuid(_netuid: NetUid) {} } -pub struct MockRateLimiting; +pub struct NoRateLimiting; -impl RateLimitingInterface for MockRateLimiting { +impl RateLimitingInterface for NoRateLimiting { type GroupId = subtensor_runtime_common::rate_limiting::GroupId; type CallMetadata = RuntimeCall; type Limit = BlockNumber; @@ -352,39 +349,22 @@ impl RateLimitingInterface for MockRateLimiting { } fn last_seen( - target: TargetArg, - usage_key: Option, + _target: TargetArg, + _usage_key: Option, ) -> Option where TargetArg: TryIntoRateLimitTarget, { - let target = target - .try_into_rate_limit_target::() - .ok()?; - let mut key = b"mock_rate_limiting:last_seen".to_vec(); - key.extend_from_slice(&target.encode()); - key.extend_from_slice(&usage_key.encode()); - let raw = sp_io::storage::get(&key)?; - Decode::decode(&mut &raw[..]).ok() + None } fn set_last_seen( - target: TargetArg, - usage_key: Option, - block: Option, + _target: TargetArg, + _usage_key: Option, + _block: Option, ) where TargetArg: TryIntoRateLimitTarget, { - let Ok(target) = target.try_into_rate_limit_target::() else { - return; - }; - let mut key = b"mock_rate_limiting:last_seen".to_vec(); - key.extend_from_slice(&target.encode()); - key.extend_from_slice(&usage_key.encode()); - match block { - Some(value) => sp_io::storage::set(&key, &value.encode()), - None => sp_io::storage::clear(&key), - } } } From 683c84720d096c59c099e500e91b0ef46f491aab Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 14 Jan 2026 18:28:03 +0100 Subject: [PATCH 66/95] Revert "Remove LastUpdate storage" This reverts commit 1406a8ff6bb8e2249d3df0702cb4bfa0fefeefaf. --- pallets/admin-utils/src/tests/mod.rs | 27 ++----- pallets/subtensor/src/coinbase/root.rs | 4 +- pallets/subtensor/src/epoch/run_epoch.rs | 13 +-- pallets/subtensor/src/lib.rs | 5 ++ pallets/subtensor/src/macros/genesis.rs | 1 + .../migrations/migrate_delete_subnet_21.rs | 3 +- .../src/migrations/migrate_delete_subnet_3.rs | 3 +- pallets/subtensor/src/rpc_info/metagraph.rs | 45 ++++------- pallets/subtensor/src/rpc_info/neuron_info.rs | 16 +--- pallets/subtensor/src/rpc_info/show_subnet.rs | 12 +-- pallets/subtensor/src/subnets/mechanism.rs | 10 +-- pallets/subtensor/src/subnets/uids.rs | 40 ++------- pallets/subtensor/src/subnets/weights.rs | 13 +++ pallets/subtensor/src/tests/children.rs | 10 +-- pallets/subtensor/src/tests/coinbase.rs | 17 +--- pallets/subtensor/src/tests/epoch.rs | 25 ++---- pallets/subtensor/src/tests/epoch_logs.rs | 15 +--- pallets/subtensor/src/tests/mechanism.rs | 81 ++++--------------- pallets/subtensor/src/tests/networks.rs | 72 +++-------------- pallets/subtensor/src/tests/registration.rs | 35 +++----- pallets/subtensor/src/tests/staking.rs | 10 +-- pallets/subtensor/src/utils/misc.rs | 65 ++++++--------- precompiles/src/metagraph.rs | 18 +---- runtime/src/migrations/rate_limiting.rs | 67 ++++++++++----- runtime/src/rate_limiting/legacy.rs | 55 +------------ 25 files changed, 194 insertions(+), 468 deletions(-) diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index 3a7f4523b2..fdb800d710 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -11,12 +11,10 @@ use pallet_subtensor::{ }; // use pallet_subtensor::{migrations, Event}; use pallet_subtensor::{Event, utils::rate_limiting::TransactionType}; -use rate_limiting_interface::RateLimitingInterface; use sp_consensus_grandpa::AuthorityId as GrandpaId; use sp_core::{Get, Pair, U256, ed25519}; -use sp_runtime::traits::SaturatedConversion; use substrate_fixed::types::I96F32; -use subtensor_runtime_common::{Currency, MechId, NetUid, TaoCurrency, rate_limiting}; +use subtensor_runtime_common::{Currency, MechId, NetUid, TaoCurrency}; use crate::Error; use crate::pallet::PrecompileEnable; @@ -2432,18 +2430,10 @@ fn test_trim_to_max_allowed_uids() { Active::::insert(netuid, bool_values); for mecid in 0..mechanism_count.into() { - let mecid = MechId::from(mecid); - let netuid_index = SubtensorModule::get_mechanism_storage_index(netuid, mecid); + let netuid_index = + SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); Incentive::::insert(netuid_index, values.clone()); - for (uid, last_seen) in u64_values.iter().copied().enumerate() { - let usage = - SubtensorModule::weights_rl_usage_key_for_uid(netuid, mecid, uid as u16); - ::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage), - Some(last_seen.saturated_into()), - ); - } + LastUpdate::::insert(netuid_index, u64_values.clone()); } // We set some owner immune uids @@ -2545,13 +2535,10 @@ fn test_trim_to_max_allowed_uids() { assert_eq!(StakeWeight::::get(netuid), expected_values); for mecid in 0..mechanism_count.into() { - let mecid = MechId::from(mecid); - let netuid_index = SubtensorModule::get_mechanism_storage_index(netuid, mecid); + let netuid_index = + SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); assert_eq!(Incentive::::get(netuid_index), expected_values); - assert_eq!( - SubtensorModule::weights_rl_last_seen_for_uids(netuid, mecid, new_max_n), - expected_u64_values, - ); + assert_eq!(LastUpdate::::get(netuid_index), expected_u64_values); } // Ensure trimmed uids related storage has been cleared diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 8a19dbf343..c01fc38442 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -237,8 +237,6 @@ impl Pallet { let owner_coldkey: T::AccountId = SubnetOwner::::get(netuid); SubnetOwner::::remove(netuid); - let subnetwork_n = SubnetworkN::::get(netuid); - // --- 2. Remove network count. SubnetworkN::::remove(netuid); @@ -381,7 +379,7 @@ impl Pallet { let mechanisms: u8 = MechanismCountCurrent::::get(netuid).into(); for subid in 0..mechanisms { let netuid_index = Self::get_mechanism_storage_index(netuid, subid.into()); - Self::set_weights_rl_last_seen_for_uids(netuid, subid.into(), subnetwork_n, None); + LastUpdate::::remove(netuid_index); Incentive::::remove(netuid_index); let _ = WeightCommits::::clear_prefix(netuid_index, u32::MAX, None); let _ = TimelockedWeightCommits::::clear_prefix(netuid_index, u32::MAX, None); diff --git a/pallets/subtensor/src/epoch/run_epoch.rs b/pallets/subtensor/src/epoch/run_epoch.rs index 363841ef1b..f56b8a89a4 100644 --- a/pallets/subtensor/src/epoch/run_epoch.rs +++ b/pallets/subtensor/src/epoch/run_epoch.rs @@ -172,7 +172,7 @@ impl Pallet { log::trace!("activity_cutoff: {activity_cutoff:?}"); // Last update vector. - let last_update = Self::weights_rl_last_seen_padded(netuid_index); + let last_update: Vec = Self::get_last_update(netuid_index); log::trace!("Last update: {:?}", &last_update); // Inactive mask. @@ -598,7 +598,7 @@ impl Pallet { log::trace!("activity_cutoff: {activity_cutoff:?}"); // Last update vector. - let last_update = Self::weights_rl_last_seen_padded(netuid_index); + let last_update: Vec = Self::get_last_update(netuid_index); log::trace!("Last update: {:?}", &last_update); // Inactive mask. @@ -1582,13 +1582,4 @@ impl Pallet { } true } - - fn weights_rl_last_seen_padded(netuid_index: NetUidStorageIndex) -> Vec { - let netuid = Self::get_netuid(netuid_index); - let subnet_n = Self::get_subnetwork_n(netuid); - match Self::get_netuid_and_subid(netuid_index) { - Ok((_, mecid)) => Self::weights_rl_last_seen_for_uids(netuid, mecid, subnet_n), - Err(_) => vec![0; subnet_n as usize], - } - } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 1a5b640059..8459d49369 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1947,6 +1947,11 @@ pub mod pallet { #[pallet::storage] pub type Emission = StorageMap<_, Identity, NetUid, Vec, ValueQuery>; + /// --- MAP ( netuid ) --> last_update + #[pallet::storage] + pub type LastUpdate = + StorageMap<_, Identity, NetUidStorageIndex, Vec, ValueQuery, EmptyU64Vec>; + /// --- MAP ( netuid ) --> validator_trust #[pallet::storage] pub type ValidatorTrust = diff --git a/pallets/subtensor/src/macros/genesis.rs b/pallets/subtensor/src/macros/genesis.rs index 649e3ebf78..0014b63540 100644 --- a/pallets/subtensor/src/macros/genesis.rs +++ b/pallets/subtensor/src/macros/genesis.rs @@ -124,6 +124,7 @@ mod genesis { Consensus::::mutate(netuid, |v| v.push(0)); Incentive::::mutate(NetUidStorageIndex::from(netuid), |v| v.push(0)); Dividends::::mutate(netuid, |v| v.push(0)); + LastUpdate::::mutate(NetUidStorageIndex::from(netuid), |v| v.push(block_number)); PruningScores::::mutate(netuid, |v| v.push(0)); ValidatorTrust::::mutate(netuid, |v| v.push(0)); ValidatorPermit::::mutate(netuid, |v| v.push(false)); diff --git a/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs b/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs index 932674808a..260904395d 100644 --- a/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs +++ b/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs @@ -54,7 +54,6 @@ pub fn migrate_delete_subnet_21() -> Weight { info!(target: LOG_TARGET, ">>> Removing subnet 21 {onchain_version:?}"); let netuid = NetUid::from(21); - let subnetwork_n = SubnetworkN::::get(netuid); // We do this all manually as we don't want to call code related to giving subnet owner back their locked token cost. // Remove network count @@ -88,7 +87,7 @@ pub fn migrate_delete_subnet_21() -> Weight { Consensus::::remove(netuid); Dividends::::remove(netuid); PruningScores::::remove(netuid); - Pallet::::set_weights_rl_last_seen_for_uids(netuid, 0.into(), subnetwork_n, None); + LastUpdate::::remove(NetUidStorageIndex::from(netuid)); ValidatorPermit::::remove(netuid); ValidatorTrust::::remove(netuid); diff --git a/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs b/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs index 30eba689af..0cda0f0e06 100644 --- a/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs +++ b/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs @@ -57,7 +57,6 @@ pub fn migrate_delete_subnet_3() -> Weight { ); let netuid = NetUid::from(3); - let subnetwork_n = SubnetworkN::::get(netuid); // Remove network count SubnetworkN::::remove(netuid); @@ -90,7 +89,7 @@ pub fn migrate_delete_subnet_3() -> Weight { Consensus::::remove(netuid); Dividends::::remove(netuid); PruningScores::::remove(netuid); - Pallet::::set_weights_rl_last_seen_for_uids(netuid, 0.into(), subnetwork_n, None); + LastUpdate::::remove(NetUidStorageIndex::from(netuid)); ValidatorPermit::::remove(netuid); ValidatorTrust::::remove(netuid); diff --git a/pallets/subtensor/src/rpc_info/metagraph.rs b/pallets/subtensor/src/rpc_info/metagraph.rs index 7388080b3e..ea24657aeb 100644 --- a/pallets/subtensor/src/rpc_info/metagraph.rs +++ b/pallets/subtensor/src/rpc_info/metagraph.rs @@ -752,14 +752,10 @@ impl Pallet { .into_iter() .map(Compact::from) .collect(), // Pruning per UID - last_update: Self::weights_rl_last_seen_for_uids( - netuid, - MechId::from(0u8), - Self::get_subnetwork_n(netuid), - ) - .into_iter() - .map(Compact::from) - .collect(), // Last update per UID + last_update: LastUpdate::::get(NetUidStorageIndex::from(netuid)) + .into_iter() + .map(Compact::from) + .collect(), // Last update per UID emission: Emission::::get(netuid) .into_iter() .map(Compact::from) @@ -824,11 +820,10 @@ impl Pallet { // Update with mechanism information meta.netuid = NetUid::from(u16::from(netuid_index)).into(); - meta.last_update = - Self::weights_rl_last_seen_for_uids(netuid, mecid, Self::get_subnetwork_n(netuid)) - .into_iter() - .map(Compact::from) - .collect(); + meta.last_update = LastUpdate::::get(netuid_index) + .into_iter() + .map(Compact::from) + .collect(); meta.incentives = Incentive::::get(netuid_index) .into_iter() .map(Compact::from) @@ -1279,14 +1274,10 @@ impl Pallet { Some(SelectiveMetagraphIndex::LastUpdate) => SelectiveMetagraph { netuid: netuid.into(), last_update: Some( - Self::weights_rl_last_seen_for_uids( - netuid, - MechId::from(0u8), - Self::get_subnetwork_n(netuid), - ) - .into_iter() - .map(Compact::from) - .collect(), + LastUpdate::::get(NetUidStorageIndex::from(netuid)) + .into_iter() + .map(Compact::from) + .collect(), ), ..Default::default() }, @@ -1485,14 +1476,10 @@ impl Pallet { Some(SelectiveMetagraphIndex::LastUpdate) => SelectiveMetagraph { netuid: netuid.into(), last_update: Some( - Self::weights_rl_last_seen_for_uids( - netuid, - mecid, - Self::get_subnetwork_n(netuid), - ) - .into_iter() - .map(Compact::from) - .collect(), + LastUpdate::::get(netuid_index) + .into_iter() + .map(Compact::from) + .collect(), ), ..Default::default() }, diff --git a/pallets/subtensor/src/rpc_info/neuron_info.rs b/pallets/subtensor/src/rpc_info/neuron_info.rs index d053b77a54..6e29a51ef5 100644 --- a/pallets/subtensor/src/rpc_info/neuron_info.rs +++ b/pallets/subtensor/src/rpc_info/neuron_info.rs @@ -2,9 +2,7 @@ use super::*; use frame_support::pallet_prelude::{Decode, Encode}; extern crate alloc; use codec::Compact; -use rate_limiting_interface::RateLimitingInterface; -use sp_runtime::SaturatedConversion; -use subtensor_runtime_common::{AlphaCurrency, MechId, NetUid, NetUidStorageIndex, rate_limiting}; +use subtensor_runtime_common::{AlphaCurrency, NetUid, NetUidStorageIndex}; #[freeze_struct("9e5a291e7e71482d")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] @@ -95,11 +93,7 @@ impl Pallet { let validator_trust = Self::get_validator_trust_for_uid(netuid, uid); let dividends = Self::get_dividends_for_uid(netuid, uid); let pruning_score = Self::get_pruning_score_for_uid(netuid, uid); - let usage = Self::weights_rl_usage_key_for_uid(netuid, MechId::from(0u8), uid); - let last_update = - T::RateLimiting::last_seen(rate_limiting::GROUP_WEIGHTS_SUBNET, Some(usage)) - .map(|block| block.saturated_into::()) - .unwrap_or(0); + let last_update = Self::get_last_update_for_uid(NetUidStorageIndex::from(netuid), uid); let validator_permit = Self::get_validator_permit_for_uid(netuid, uid); let weights = Weights::::get(NetUidStorageIndex::from(netuid), uid) @@ -185,11 +179,7 @@ impl Pallet { let validator_trust = Self::get_validator_trust_for_uid(netuid, uid); let dividends = Self::get_dividends_for_uid(netuid, uid); let pruning_score = Self::get_pruning_score_for_uid(netuid, uid); - let usage = Self::weights_rl_usage_key_for_uid(netuid, MechId::from(0u8), uid); - let last_update = - T::RateLimiting::last_seen(rate_limiting::GROUP_WEIGHTS_SUBNET, Some(usage)) - .map(|block| block.saturated_into::()) - .unwrap_or(0); + let last_update = Self::get_last_update_for_uid(NetUidStorageIndex::from(netuid), uid); let validator_permit = Self::get_validator_permit_for_uid(netuid, uid); let stake: Vec<(T::AccountId, Compact)> = vec![( diff --git a/pallets/subtensor/src/rpc_info/show_subnet.rs b/pallets/subtensor/src/rpc_info/show_subnet.rs index a7cef1f147..abd9670bb8 100644 --- a/pallets/subtensor/src/rpc_info/show_subnet.rs +++ b/pallets/subtensor/src/rpc_info/show_subnet.rs @@ -103,14 +103,10 @@ impl Pallet { .into_iter() .map(Compact::from) .collect(); - let last_update: Vec> = Self::weights_rl_last_seen_for_uids( - netuid, - subtensor_runtime_common::MechId::from(0u8), - n, - ) - .into_iter() - .map(Compact::from) - .collect(); + let last_update: Vec> = LastUpdate::::get(NetUidStorageIndex::from(netuid)) + .into_iter() + .map(Compact::from) + .collect(); let emission = Emission::::get(netuid) .into_iter() .map(Compact::from) diff --git a/pallets/subtensor/src/subnets/mechanism.rs b/pallets/subtensor/src/subnets/mechanism.rs index 6de2e18011..481974ef05 100644 --- a/pallets/subtensor/src/subnets/mechanism.rs +++ b/pallets/subtensor/src/subnets/mechanism.rs @@ -132,7 +132,6 @@ impl Pallet { let new_count_u8 = u8::from(new_count); if old_count != new_count_u8 { if old_count > new_count_u8 { - let subnet_n = SubnetworkN::::get(netuid); for mecid in new_count_u8..old_count { let netuid_index = Self::get_mechanism_storage_index(netuid, MechId::from(mecid)); @@ -143,13 +142,8 @@ impl Pallet { // Cleanup Incentive Incentive::::remove(netuid_index); - // Cleanup last-seen - Self::set_weights_rl_last_seen_for_uids( - netuid, - MechId::from(mecid), - subnet_n, - None, - ); + // Cleanup LastUpdate + LastUpdate::::remove(netuid_index); // Cleanup Bonds let _ = Bonds::::clear_prefix(netuid_index, u32::MAX, None); diff --git a/pallets/subtensor/src/subnets/uids.rs b/pallets/subtensor/src/subnets/uids.rs index 24f3d9f96c..0a09017e64 100644 --- a/pallets/subtensor/src/subnets/uids.rs +++ b/pallets/subtensor/src/subnets/uids.rs @@ -1,10 +1,9 @@ use super::*; use frame_support::storage::IterableStorageDoubleMap; -use rate_limiting_interface::RateLimitingInterface; -use sp_runtime::{Percent, SaturatedConversion}; +use sp_runtime::Percent; use sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet}; use sp_std::{cmp, vec}; -use subtensor_runtime_common::{NetUid, rate_limiting}; +use subtensor_runtime_common::NetUid; impl Pallet { /// Returns the number of filled slots on a network. @@ -115,12 +114,7 @@ impl Pallet { for mecid in 0..MechanismCountCurrent::::get(netuid).into() { let netuid_index = Self::get_mechanism_storage_index(netuid, mecid.into()); Incentive::::mutate(netuid_index, |v| v.push(0)); - let usage = Self::weights_rl_usage_key_for_uid(netuid, mecid.into(), next_uid); - T::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage), - Some(block_number.saturated_into()), - ); + Self::set_last_update_for_uid(netuid_index, next_uid, block_number); } Dividends::::mutate(netuid, |v| v.push(0)); ValidatorTrust::::mutate(netuid, |v| v.push(0)); @@ -271,37 +265,19 @@ impl Pallet { // Update incentives/lastupdates for mechanisms for mecid in 0..mechanisms_count { - let mecid = mecid.into(); - let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); + let netuid_index = Self::get_mechanism_storage_index(netuid, mecid.into()); let incentive = Incentive::::get(netuid_index); + let lastupdate = LastUpdate::::get(netuid_index); let mut trimmed_incentive = Vec::with_capacity(trimmed_uids.len()); - let mut trimmed_last_seen = Vec::with_capacity(trimmed_uids.len()); + let mut trimmed_lastupdate = Vec::with_capacity(trimmed_uids.len()); for uid in &trimmed_uids { trimmed_incentive.push(incentive.get(*uid).cloned().unwrap_or_default()); - let usage = Self::weights_rl_usage_key_for_uid(netuid, mecid, *uid as u16); - let last_seen = T::RateLimiting::last_seen( - subtensor_runtime_common::rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage), - ); - trimmed_last_seen.push(last_seen); + trimmed_lastupdate.push(lastupdate.get(*uid).cloned().unwrap_or_default()); } Incentive::::insert(netuid_index, trimmed_incentive); - - Self::set_weights_rl_last_seen_for_uids(netuid, mecid, current_n, None); - - for (uid, last_seen) in trimmed_last_seen.into_iter().enumerate() { - let Some(block) = last_seen else { - continue; - }; - let usage = Self::weights_rl_usage_key_for_uid(netuid, mecid, uid as u16); - T::RateLimiting::set_last_seen( - subtensor_runtime_common::rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage), - Some(block), - ); - } + LastUpdate::::insert(netuid_index, trimmed_lastupdate); } // Create mapping from old uid to new compressed uid diff --git a/pallets/subtensor/src/subnets/weights.rs b/pallets/subtensor/src/subnets/weights.rs index c8389f6639..ac96f68720 100644 --- a/pallets/subtensor/src/subnets/weights.rs +++ b/pallets/subtensor/src/subnets/weights.rs @@ -128,6 +128,9 @@ impl Pallet { commit_hash, )); + // 12. Update the last commit block for the hotkey's UID. + Self::set_last_update_for_uid(netuid_index, neuron_uid, commit_block); + // 13. Return success. Ok(()) }) @@ -235,6 +238,8 @@ impl Pallet { /// 4. Appends `(hotkey, commit_block, commit, reveal_round)` to /// `TimelockedWeightCommits[netuid][epoch]`. /// 5. Emits `TimelockedWeightsCommitted` with the Blake2 hash of `commit`. + /// 6. Updates `LastUpdateForUid` so subsequent rate-limit checks include this + /// commit. /// /// # Raises /// * `CommitRevealDisabled` – Commit-reveal is disabled on `netuid`. @@ -354,6 +359,9 @@ impl Pallet { reveal_round, )); + // 10. Update the last commit block for the hotkey's UID. + Self::set_last_update_for_uid(netuid_index, neuron_uid, commit_block); + // 11. Return success. Ok(()) }, @@ -819,6 +827,11 @@ impl Pallet { // --- 17. Set weights under netuid_index (sub-subnet), uid double map entry. Weights::::insert(netuid_index, neuron_uid, zipped_weights); + // --- 18. Set the activity for the weights on this network. + if !Self::get_commit_reveal_weights_enabled(netuid) { + Self::set_last_update_for_uid(netuid_index, neuron_uid, current_block); + } + // --- 19. Emit the tracking event. log::debug!("WeightsSet( netuid:{netuid_index:?}, neuron_uid:{neuron_uid:?} )"); Self::deposit_event(Event::WeightsSet(netuid_index, neuron_uid)); diff --git a/pallets/subtensor/src/tests/children.rs b/pallets/subtensor/src/tests/children.rs index c3d2a03ec9..146c94516a 100644 --- a/pallets/subtensor/src/tests/children.rs +++ b/pallets/subtensor/src/tests/children.rs @@ -5,9 +5,8 @@ use super::mock; use super::mock::*; use approx::assert_abs_diff_eq; use frame_support::{assert_err, assert_noop, assert_ok}; -use sp_runtime::traits::SaturatedConversion; use substrate_fixed::types::{I64F64, I96F32, U96F32}; -use subtensor_runtime_common::{AlphaCurrency, MechId, NetUidStorageIndex, TaoCurrency}; +use subtensor_runtime_common::{AlphaCurrency, NetUidStorageIndex, TaoCurrency}; use subtensor_swap_interface::SwapHandler; use crate::{utils::rate_limiting::TransactionType, *}; @@ -2907,12 +2906,7 @@ fn test_childkey_take_drain() { BlockAtRegistration::::set(netuid, 0, 1); BlockAtRegistration::::set(netuid, 1, 1); BlockAtRegistration::::set(netuid, 2, 1); - SubtensorModule::set_weights_rl_last_seen_for_uids( - netuid, - MechId::from(0u8), - 3, - Some(2u64.saturated_into()), - ); + LastUpdate::::set(NetUidStorageIndex::from(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]); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 83f3a25552..de42cf6b3b 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -14,12 +14,11 @@ use approx::assert_abs_diff_eq; use frame_support::assert_ok; use pallet_subtensor_swap::position::PositionId; use sp_core::U256; -use sp_runtime::traits::SaturatedConversion; use substrate_fixed::{ transcendental::sqrt, types::{I64F64, I96F32, U64F64, U96F32}, }; -use subtensor_runtime_common::{AlphaCurrency, MechId, NetUidStorageIndex}; +use subtensor_runtime_common::{AlphaCurrency, NetUidStorageIndex}; use subtensor_swap_interface::{SwapEngine, SwapHandler}; #[allow(clippy::arithmetic_side_effects)] @@ -3041,12 +3040,7 @@ fn test_mining_emission_distribution_with_no_root_sell() { BlockAtRegistration::::set(netuid, 0, 1); BlockAtRegistration::::set(netuid, 1, 1); BlockAtRegistration::::set(netuid, 2, 1); - SubtensorModule::set_weights_rl_last_seen_for_uids( - netuid, - MechId::from(0u8), - 3, - Some(2u64.saturated_into()), - ); + LastUpdate::::set(NetUidStorageIndex::from(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]); @@ -3240,12 +3234,7 @@ fn test_mining_emission_distribution_with_root_sell() { BlockAtRegistration::::set(netuid, 0, 1); BlockAtRegistration::::set(netuid, 1, 1); BlockAtRegistration::::set(netuid, 2, 1); - SubtensorModule::set_weights_rl_last_seen_for_uids( - netuid, - MechId::from(0u8), - 3, - Some(2u64.saturated_into()), - ); + LastUpdate::::set(NetUidStorageIndex::from(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]); diff --git a/pallets/subtensor/src/tests/epoch.rs b/pallets/subtensor/src/tests/epoch.rs index e64bbe5c4d..c0be00b0fd 100644 --- a/pallets/subtensor/src/tests/epoch.rs +++ b/pallets/subtensor/src/tests/epoch.rs @@ -10,13 +10,9 @@ use std::time::Instant; use approx::assert_abs_diff_eq; use frame_support::{assert_err, assert_ok}; use rand::{Rng, SeedableRng, distributions::Uniform, rngs::StdRng, seq::SliceRandom, thread_rng}; -use rate_limiting_interface::RateLimitingInterface; use sp_core::{Get, U256}; -use sp_runtime::traits::SaturatedConversion; use substrate_fixed::types::I32F32; -use subtensor_runtime_common::{ - AlphaCurrency, MechId, NetUidStorageIndex, TaoCurrency, rate_limiting, -}; +use subtensor_runtime_common::{AlphaCurrency, NetUidStorageIndex, TaoCurrency}; use subtensor_swap_interface::SwapHandler; use super::mock::*; @@ -2495,15 +2491,7 @@ fn test_can_set_self_weight_as_subnet_owner() { step_block(1); // Set updated so weights are valid - let mecid = MechId::from(0u8); - for (uid, last_seen) in [2u64, 0u64].iter().copied().enumerate() { - let usage = SubtensorModule::weights_rl_usage_key_for_uid(netuid, mecid, uid as u16); - ::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage), - Some(last_seen.saturated_into()), - ); - } + LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![2, 0]); // Run epoch let hotkey_emission = SubtensorModule::epoch(netuid, to_emit.into()); @@ -3821,10 +3809,10 @@ fn test_epoch_does_not_mask_outside_window_but_masks_inside() { }); } -// Test an epoch doesn't panic when weights last-seen entries are missing. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::test_missing_last_seen_does_not_panic --exact --show-output --nocapture +// Test an epoch doesn't panic when LastUpdate size doesn't match to Weights size. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::test_last_update_size_mismatch --exact --show-output --nocapture #[test] -fn test_missing_last_seen_does_not_panic() { +fn test_last_update_size_mismatch() { new_test_ext(1).execute_with(|| { log::info!("test_1_graph:"); let netuid = NetUid::from(1); @@ -3857,6 +3845,9 @@ fn test_missing_last_seen_does_not_panic() { 0 )); + // Set mismatching LastUpdate vector + LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![1, 1, 1]); + SubtensorModule::epoch(netuid, 1_000_000_000.into()); assert_eq!( SubtensorModule::get_total_stake_for_hotkey(&hotkey), diff --git a/pallets/subtensor/src/tests/epoch_logs.rs b/pallets/subtensor/src/tests/epoch_logs.rs index 85bef59a1c..420a9e0eac 100644 --- a/pallets/subtensor/src/tests/epoch_logs.rs +++ b/pallets/subtensor/src/tests/epoch_logs.rs @@ -10,12 +10,10 @@ use super::mock::*; use crate::*; use frame_support::assert_ok; -use rate_limiting_interface::RateLimitingInterface; use sp_core::U256; -use sp_runtime::traits::SaturatedConversion; use std::io::{Result as IoResult, Write}; use std::sync::{Arc, Mutex}; -use subtensor_runtime_common::{AlphaCurrency, MechId, rate_limiting}; +use subtensor_runtime_common::{AlphaCurrency, MechId}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt}; const NETUID: u16 = 1; @@ -89,15 +87,8 @@ fn setup_epoch(neurons: Vec, mechanism_count: u8) { ValidatorPermit::::insert(netuid, permit_vec); for m in 0..mechanism_count { - let mecid = MechId::from(m); - for (uid, last_seen) in last_update_vec.iter().copied().enumerate() { - let usage = SubtensorModule::weights_rl_usage_key_for_uid(netuid, mecid, uid as u16); - ::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage), - Some(last_seen.saturated_into()), - ); - } + let netuid_index = SubtensorModule::get_mechanism_storage_index(netuid, m.into()); + LastUpdate::::insert(netuid_index, last_update_vec.clone()); } } diff --git a/pallets/subtensor/src/tests/mechanism.rs b/pallets/subtensor/src/tests/mechanism.rs index 4a183c99f6..d096cd0515 100644 --- a/pallets/subtensor/src/tests/mechanism.rs +++ b/pallets/subtensor/src/tests/mechanism.rs @@ -27,7 +27,7 @@ // - [x] Incentives are per mechanism // - [x] Per-mechanism incentives are distributed proportionally to miner weights // - [x] Mechanism limit can be set up to 8 (with admin pallet) -// - [x] When reduction of mechanism limit occurs, Weights, Incentive, last-seen, Bonds, and WeightCommits are cleared +// - [x] When reduction of mechanism limit occurs, Weights, Incentive, LastUpdate, Bonds, and WeightCommits are cleared // - [x] Epoch terms of subnet are weighted sum (or logical OR) of all mechanism epoch terms // - [x] Subnet epoch terms persist in state // - [x] Mechanism epoch terms persist in state @@ -48,13 +48,12 @@ use frame_support::{assert_noop, assert_ok}; use frame_system::RawOrigin; use pallet_drand::types::Pulse; use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng}; -use rate_limiting_interface::RateLimitingInterface; use sha2::Digest; use sp_core::{H256, U256}; -use sp_runtime::traits::{BlakeTwo256, Hash, SaturatedConversion}; +use sp_runtime::traits::{BlakeTwo256, Hash}; use sp_std::collections::vec_deque::VecDeque; use substrate_fixed::types::{I32F32, U64F64}; -use subtensor_runtime_common::{MechId, NetUid, NetUidStorageIndex, rate_limiting}; +use subtensor_runtime_common::{MechId, NetUid, NetUidStorageIndex}; use tle::{ curves::drand::TinyBLS381, ibe::fullident::Identity, stream_ciphers::AESGCMStreamCipherProvider, tlock::tle, @@ -303,13 +302,7 @@ fn update_mechanism_counts_decreases_and_cleans() { Weights::::insert(idx_keep, 0u16, vec![(1u16, 1u16)]); Incentive::::insert(idx_keep, vec![1u16]); - let keep_usage = - SubtensorModule::weights_rl_usage_key_for_uid(netuid, MechId::from(1u8), 0); - ::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(keep_usage), - Some(123u64.saturated_into()), - ); + LastUpdate::::insert(idx_keep, vec![123u64]); Bonds::::insert(idx_keep, 0u16, vec![(1u16, 2u16)]); WeightCommits::::insert( idx_keep, @@ -324,13 +317,7 @@ fn update_mechanism_counts_decreases_and_cleans() { Weights::::insert(idx_rm3, 0u16, vec![(9u16, 9u16)]); Incentive::::insert(idx_rm3, vec![9u16]); - let removed_usage = - SubtensorModule::weights_rl_usage_key_for_uid(netuid, MechId::from(2u8), 0); - ::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(removed_usage), - Some(999u64.saturated_into()), - ); + LastUpdate::::insert(idx_rm3, vec![999u64]); Bonds::::insert(idx_rm3, 0u16, vec![(9u16, 9u16)]); WeightCommits::::insert( idx_rm3, @@ -352,14 +339,7 @@ fn update_mechanism_counts_decreases_and_cleans() { // Kept prefix intact assert_eq!(Incentive::::get(idx_keep), vec![1u16]); assert!(Weights::::iter_prefix(idx_keep).next().is_some()); - let keep_usage = - SubtensorModule::weights_rl_usage_key_for_uid(netuid, MechId::from(1u8), 0); - let kept_last_seen = ::RateLimiting::last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(keep_usage), - ) - .map(|block| block.saturated_into::()); - assert_eq!(kept_last_seen, Some(123)); + assert!(LastUpdate::::contains_key(idx_keep)); assert!(Bonds::::iter_prefix(idx_keep).next().is_some()); assert!(WeightCommits::::contains_key(idx_keep, hotkey)); assert!(TimelockedWeightCommits::::contains_key( @@ -369,15 +349,7 @@ fn update_mechanism_counts_decreases_and_cleans() { // Removed prefix (mecid 3) cleared assert!(Weights::::iter_prefix(idx_rm3).next().is_none()); assert_eq!(Incentive::::get(idx_rm3), Vec::::new()); - let removed_usage = - SubtensorModule::weights_rl_usage_key_for_uid(netuid, MechId::from(2u8), 0); - assert!( - ::RateLimiting::last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(removed_usage), - ) - .is_none() - ); + assert!(!LastUpdate::::contains_key(idx_rm3)); assert!(Bonds::::iter_prefix(idx_rm3).next().is_none()); assert!(!WeightCommits::::contains_key(idx_rm3, hotkey)); assert!(!TimelockedWeightCommits::::contains_key( @@ -482,18 +454,8 @@ pub fn mock_epoch_state(netuid: NetUid, ck0: U256, hk0: U256, ck1: U256, hk1: U2 // Make both ACTIVE: recent updates & old registrations. Tempo::::insert(netuid, 1u16); ActivityCutoff::::insert(netuid, u16::MAX); // large cutoff keeps them active - SubtensorModule::set_weights_rl_last_seen_for_uids( - netuid, - MechId::from(0u8), - 2, - Some(2u64.saturated_into()), - ); - SubtensorModule::set_weights_rl_last_seen_for_uids( - netuid, - MechId::from(1u8), - 2, - Some(2u64.saturated_into()), - ); + LastUpdate::::insert(idx0, vec![2, 2]); + LastUpdate::::insert(idx1, vec![2, 2]); BlockAtRegistration::::insert(netuid, 0, 1u64); // registered long ago BlockAtRegistration::::insert(netuid, 1, 1u64); @@ -528,20 +490,13 @@ pub fn mock_epoch_state(netuid: NetUid, ck0: U256, hk0: U256, ck1: U256, hk1: U2 } pub fn mock_3_neurons(netuid: NetUid, hk: U256) { + let idx0 = SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(0)); + let idx1 = SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(1)); + SubnetworkN::::insert(netuid, 3); Keys::::insert(netuid, 2u16, hk); - SubtensorModule::set_weights_rl_last_seen_for_uids( - netuid, - MechId::from(0u8), - 3, - Some(2u64.saturated_into()), - ); - SubtensorModule::set_weights_rl_last_seen_for_uids( - netuid, - MechId::from(1u8), - 3, - Some(2u64.saturated_into()), - ); + LastUpdate::::insert(idx0, vec![2, 2, 2]); + LastUpdate::::insert(idx1, vec![2, 2, 2]); BlockAtRegistration::::insert(netuid, 2, 1u64); } @@ -1434,6 +1389,7 @@ fn epoch_mechanism_emergency_mode_distributes_by_stake() { // setup a single sub-subnet where consensus sum becomes 0 let netuid = NetUid::from(1u16); let mecid = MechId::from(1u8); + let idx = SubtensorModule::get_mechanism_storage_index(netuid, mecid); let tempo: u16 = 5; add_network(netuid, tempo, 0); MechanismCountCurrent::::insert(netuid, MechId::from(2u8)); // allow subids {0,1} @@ -1457,12 +1413,7 @@ fn epoch_mechanism_emergency_mode_distributes_by_stake() { // active + recent updates so they're all active let now = SubtensorModule::get_current_block_as_u64(); ActivityCutoff::::insert(netuid, 1_000u16); - SubtensorModule::set_weights_rl_last_seen_for_uids( - netuid, - mecid, - 4, - Some(now.saturated_into()), - ); + LastUpdate::::insert(idx, vec![now, now, now, now]); // All staking validators permitted => active_stake = stake ValidatorPermit::::insert(netuid, vec![true, true, true, false]); diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 16ba1ee8db..3ef476fa00 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -5,12 +5,10 @@ use crate::migrations::migrate_network_immunity_period; use crate::*; use frame_support::{assert_err, assert_ok}; use frame_system::Config; -use rate_limiting_interface::RateLimitingInterface; use sp_core::U256; -use sp_runtime::traits::SaturatedConversion; use sp_std::collections::{btree_map::BTreeMap, vec_deque::VecDeque}; use substrate_fixed::types::{I96F32, U64F64, U96F32}; -use subtensor_runtime_common::{MechId, NetUidStorageIndex, TaoCurrency, rate_limiting}; +use subtensor_runtime_common::{MechId, NetUidStorageIndex, TaoCurrency}; use subtensor_swap_interface::{Order, SwapHandler}; #[test] @@ -327,12 +325,7 @@ fn dissolve_clears_all_per_subnet_storages() { Consensus::::insert(net, vec![1u16]); Dividends::::insert(net, vec![1u16]); PruningScores::::insert(net, vec![1u16]); - let usage = SubtensorModule::weights_rl_usage_key_for_uid(net, MechId::from(0u8), 0); - ::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage), - Some(1u64.saturated_into()), - ); + LastUpdate::::insert(NetUidStorageIndex::from(net), vec![0u64]); ValidatorPermit::::insert(net, vec![true]); ValidatorTrust::::insert(net, vec![1u16]); @@ -482,14 +475,9 @@ fn dissolve_clears_all_per_subnet_storages() { assert!(!Consensus::::contains_key(net)); assert!(!Dividends::::contains_key(net)); assert!(!PruningScores::::contains_key(net)); - let usage = SubtensorModule::weights_rl_usage_key_for_uid(net, MechId::from(0u8), 0); - assert!( - ::RateLimiting::last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage), - ) - .is_none() - ); + assert!(!LastUpdate::::contains_key(NetUidStorageIndex::from( + net + ))); assert!(!ValidatorPermit::::contains_key(net)); assert!(!ValidatorTrust::::contains_key(net)); @@ -2258,19 +2246,9 @@ fn dissolve_clears_all_mechanism_scoped_maps_for_all_mechanisms() { Incentive::::insert(idx0, vec![1u16, 2u16]); Incentive::::insert(idx1, vec![3u16, 4u16]); - // --- Last-seen (usage-key -> block) - let usage0 = SubtensorModule::weights_rl_usage_key_for_uid(net, m0, 0); - let usage1 = SubtensorModule::weights_rl_usage_key_for_uid(net, m1, 0); - ::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage0), - Some(42u64.saturated_into()), - ); - ::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage1), - Some(84u64.saturated_into()), - ); + // --- LastUpdate (MAP: netuid_index -> Vec) + LastUpdate::::insert(idx0, vec![42u64]); + LastUpdate::::insert(idx1, vec![84u64]); // Sanity: keys are present before dissolve. assert!(Weights::::contains_key(idx0, 0u16)); @@ -2281,20 +2259,8 @@ fn dissolve_clears_all_mechanism_scoped_maps_for_all_mechanisms() { assert!(TimelockedWeightCommits::::contains_key(idx1, 2u64)); assert!(Incentive::::contains_key(idx0)); assert!(Incentive::::contains_key(idx1)); - let usage0 = SubtensorModule::weights_rl_usage_key_for_uid(net, m0, 0); - let usage1 = SubtensorModule::weights_rl_usage_key_for_uid(net, m1, 0); - let last0 = ::RateLimiting::last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage0), - ) - .map(|block| block.saturated_into::()); - let last1 = ::RateLimiting::last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage1), - ) - .map(|block| block.saturated_into::()); - assert_eq!(last0, Some(42)); - assert_eq!(last1, Some(84)); + assert!(LastUpdate::::contains_key(idx0)); + assert!(LastUpdate::::contains_key(idx1)); assert!(MechanismCountCurrent::::contains_key(net)); // --- Dissolve the subnet --- @@ -2331,22 +2297,8 @@ fn dissolve_clears_all_mechanism_scoped_maps_for_all_mechanisms() { // Single-map per-mechanism vectors cleared. assert!(!Incentive::::contains_key(idx0)); assert!(!Incentive::::contains_key(idx1)); - let usage0 = SubtensorModule::weights_rl_usage_key_for_uid(net, m0, 0); - let usage1 = SubtensorModule::weights_rl_usage_key_for_uid(net, m1, 0); - assert!( - ::RateLimiting::last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage0), - ) - .is_none() - ); - assert!( - ::RateLimiting::last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage1), - ) - .is_none() - ); + assert!(!LastUpdate::::contains_key(idx0)); + assert!(!LastUpdate::::contains_key(idx1)); // MechanismCountCurrent cleared assert!(!MechanismCountCurrent::::contains_key(net)); diff --git a/pallets/subtensor/src/tests/registration.rs b/pallets/subtensor/src/tests/registration.rs index a2822c616a..c82e173907 100644 --- a/pallets/subtensor/src/tests/registration.rs +++ b/pallets/subtensor/src/tests/registration.rs @@ -7,14 +7,9 @@ use frame_support::sp_runtime::{DispatchError, transaction_validity::Transaction use frame_support::traits::Currency; use frame_support::{assert_err, assert_noop, assert_ok}; use frame_system::{Config, RawOrigin}; -use rate_limiting_interface::RateLimitingInterface; use sp_core::U256; -use sp_runtime::traits::{ - DispatchInfoOf, SaturatedConversion, TransactionExtension, TxBaseImplication, -}; -use subtensor_runtime_common::{ - AlphaCurrency, Currency as CurrencyT, MechId, NetUid, rate_limiting, -}; +use sp_runtime::traits::{DispatchInfoOf, TransactionExtension, TxBaseImplication}; +use subtensor_runtime_common::{AlphaCurrency, Currency as CurrencyT, NetUid, NetUidStorageIndex}; use super::mock; use super::mock::*; @@ -2147,16 +2142,8 @@ fn test_last_update_correctness() { let existing_neurons = 3; SubnetworkN::::insert(netuid, existing_neurons); - // Simulate no last-seen so far (can happen on mechanisms) - let mecid = MechId::from(0u8); - let existing_usage = SubtensorModule::weights_rl_usage_key_for_uid(netuid, mecid, 0); - assert!( - ::RateLimiting::last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(existing_usage), - ) - .is_none() - ); + // Simulate no LastUpdate so far (can happen on mechanisms) + LastUpdate::::remove(NetUidStorageIndex::from(netuid)); // Give some $$$ to coldkey SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 10000); @@ -2167,15 +2154,11 @@ fn test_last_update_correctness() { hotkey_account_id )); - // Check that last-seen is set for the new uid - let new_uid = existing_neurons; - let usage = SubtensorModule::weights_rl_usage_key_for_uid(netuid, mecid, new_uid); - let last_seen = ::RateLimiting::last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage), - ) - .map(|block| block.saturated_into::()); - assert_eq!(last_seen, Some(SubtensorModule::get_current_block_as_u64())); + // Check that LastUpdate has existing_neurons + 1 elements now + assert_eq!( + LastUpdate::::get(NetUidStorageIndex::from(netuid)).len(), + (existing_neurons + 1) as usize + ); }); } diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index dd63714f2e..a096c29723 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -11,11 +11,10 @@ use pallet_subtensor_swap::tick::TickIndex; use safe_math::FixedExt; use sp_core::{Get, H256, U256}; use sp_runtime::traits::Dispatchable; -use sp_runtime::traits::SaturatedConversion; use substrate_fixed::traits::FromFixed; use substrate_fixed::types::{I96F32, I110F18, U64F64, U96F32}; use subtensor_runtime_common::{ - AlphaCurrency, Currency as CurrencyT, MechId, NetUid, NetUidStorageIndex, TaoCurrency, + AlphaCurrency, Currency as CurrencyT, NetUid, NetUidStorageIndex, TaoCurrency, }; use subtensor_swap_interface::{Order, SwapHandler}; @@ -2330,12 +2329,7 @@ fn test_mining_emission_distribution_validator_valiminer_miner() { BlockAtRegistration::::set(netuid, 0, 1); BlockAtRegistration::::set(netuid, 1, 1); BlockAtRegistration::::set(netuid, 2, 1); - SubtensorModule::set_weights_rl_last_seen_for_uids( - netuid, - MechId::from(0u8), - 3, - Some(2u64.saturated_into()), - ); + LastUpdate::::set(NetUidStorageIndex::from(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]); diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 3fb480508a..89d5a1446a 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -8,7 +8,7 @@ use sp_core::U256; use sp_runtime::{SaturatedConversion, Saturating}; use substrate_fixed::types::{I32F32, I64F64, U64F64, U96F32}; use subtensor_runtime_common::{ - AlphaCurrency, MechId, NetUid, NetUidStorageIndex, TaoCurrency, rate_limiting, + AlphaCurrency, NetUid, NetUidStorageIndex, TaoCurrency, rate_limiting, }; impl Pallet { @@ -173,7 +173,17 @@ impl Pallet { pub fn get_dividends(netuid: NetUid) -> Vec { Dividends::::get(netuid) } - + /// Fetch LastUpdate for `netuid` and ensure its length is at least `get_subnetwork_n(netuid)`, + /// padding with zeros if needed. Returns the (possibly padded) vector. + pub fn get_last_update(netuid_index: NetUidStorageIndex) -> Vec { + let netuid = Self::get_netuid(netuid_index); + let target_len = Self::get_subnetwork_n(netuid) as usize; + let mut v = LastUpdate::::get(netuid_index); + if v.len() < target_len { + v.resize(target_len, 0); + } + v + } pub fn get_pruning_score(netuid: NetUid) -> Vec { PruningScores::::get(netuid) } @@ -187,47 +197,14 @@ impl Pallet { // ================================== // ==== YumaConsensus UID params ==== // ================================== - pub fn weights_rl_usage_key_for_uid( - netuid: NetUid, - mecid: MechId, - uid: u16, - ) -> rate_limiting::RateLimitUsageKey { - if mecid == 0.into() { - rate_limiting::RateLimitUsageKey::::SubnetNeuron { netuid, uid } - } else { - rate_limiting::RateLimitUsageKey::::SubnetMechanismNeuron { - netuid, - mecid, - uid, - } - } - } - - pub fn weights_rl_last_seen_for_uids(netuid: NetUid, mecid: MechId, subnet_n: u16) -> Vec { - let mut last_seen = Vec::with_capacity(subnet_n as usize); - for uid in 0..subnet_n { - let usage = Self::weights_rl_usage_key_for_uid(netuid, mecid, uid); - let block = - T::RateLimiting::last_seen(rate_limiting::GROUP_WEIGHTS_SUBNET, Some(usage)) - .map(|block| block.saturated_into::()) - .unwrap_or(0); - last_seen.push(block); - } - last_seen - } - - pub(crate) fn set_weights_rl_last_seen_for_uids( - netuid: NetUid, - mecid: MechId, - subnet_n: u16, - block: Option>, - ) { - for uid in 0..subnet_n { - let usage = Self::weights_rl_usage_key_for_uid(netuid, mecid, uid); - T::RateLimiting::set_last_seen(rate_limiting::GROUP_WEIGHTS_SUBNET, Some(usage), block); - } + pub fn set_last_update_for_uid(netuid: NetUidStorageIndex, uid: u16, last_update: u64) { + let mut updated_last_update_vec = Self::get_last_update(netuid); + let Some(updated_last_update) = updated_last_update_vec.get_mut(uid as usize) else { + return; + }; + *updated_last_update = last_update; + LastUpdate::::insert(netuid, updated_last_update_vec); } - pub fn set_active_for_uid(netuid: NetUid, uid: u16, active: bool) { let mut updated_active_vec = Self::get_active(netuid); let Some(updated_active) = updated_active_vec.get_mut(uid as usize) else { @@ -277,6 +254,10 @@ impl Pallet { let vec = Dividends::::get(netuid); vec.get(uid as usize).copied().unwrap_or(0) } + pub fn get_last_update_for_uid(netuid: NetUidStorageIndex, uid: u16) -> u64 { + let vec = LastUpdate::::get(netuid); + vec.get(uid as usize).copied().unwrap_or(0) + } pub fn get_pruning_score_for_uid(netuid: NetUid, uid: u16) -> u16 { let vec = PruningScores::::get(netuid); vec.get(uid as usize).copied().unwrap_or(u16::MAX) diff --git a/precompiles/src/metagraph.rs b/precompiles/src/metagraph.rs index 0bf4b3c9a3..cc9652a74b 100644 --- a/precompiles/src/metagraph.rs +++ b/precompiles/src/metagraph.rs @@ -2,12 +2,10 @@ use alloc::string::String; use core::marker::PhantomData; use fp_evm::{ExitError, PrecompileFailure, PrecompileHandle}; -use pallet_rate_limiting::RateLimitingInterface; use pallet_subtensor::AxonInfo as SubtensorModuleAxonInfo; use precompile_utils::{EvmResult, solidity::Codec}; use sp_core::{ByteArray, H256}; -use sp_runtime::SaturatedConversion; -use subtensor_runtime_common::{Currency, NetUid, rate_limiting}; +use subtensor_runtime_common::{Currency, NetUid}; use crate::PrecompileExt; @@ -122,18 +120,10 @@ where #[precompile::public("getLastUpdate(uint16,uint16)")] #[precompile::view] fn get_last_update(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { - let usage = rate_limiting::RateLimitUsageKey::::SubnetNeuron { - netuid: netuid.into(), + Ok(pallet_subtensor::Pallet::::get_last_update_for_uid( + netuid.into(), uid, - }; - let block = ::RateLimiting::last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, - Some(usage), - ) - .map(|block| block.saturated_into::()) - .unwrap_or(0); - - Ok(block) + )) } #[precompile::public("getIsActive(uint16,uint16)")] diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 5cc12da72d..4a5226a809 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -7,8 +7,8 @@ use pallet_rate_limiting::{ GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, RateLimitTarget, TransactionIdentifier, }; use pallet_subtensor::{ - self, AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, Pallet, - Prometheus, + self, AssociatedEvmAddress, Axons, Config as SubtensorConfig, HasMigrationRun, LastUpdate, + Pallet, Prometheus, }; use sp_runtime::traits::SaturatedConversion; use sp_std::{ @@ -451,9 +451,8 @@ fn build_weights(groups: &mut Vec, commits: &mut Vec) -> u6 ); } - let (last_updates, last_update_reads) = legacy_storage::last_updates(); - reads = reads.saturating_add(last_update_reads); - for (index, blocks) in last_updates { + for (index, blocks) in LastUpdate::::iter() { + reads = reads.saturating_add(1); let (netuid, mecid) = Pallet::::get_netuid_and_subid(index).unwrap_or((NetUid::ROOT, 0.into())); for (uid, last_block) in blocks.into_iter().enumerate() { @@ -1022,6 +1021,7 @@ fn block_number(value: u64) -> Option> { #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { + use codec::Encode; use frame_support::traits::OnRuntimeUpgrade; use frame_system::pallet_prelude::BlockNumberFor; use pallet_rate_limiting::{ @@ -1029,12 +1029,13 @@ mod tests { TransactionIdentifier, }; use pallet_subtensor::{ - AxonInfo, Call as SubtensorCall, HasMigrationRun, LastRateLimitedBlock, NetworksAdded, - PrometheusInfo, RateLimitKey, TransactionKeyLastBlock, WeightsSetRateLimit, + AxonInfo, Call as SubtensorCall, HasMigrationRun, LastRateLimitedBlock, LastUpdate, + NetworksAdded, PrometheusInfo, RateLimitKey, TransactionKeyLastBlock, WeightsSetRateLimit, WeightsVersionKeyRateLimit, utils::rate_limiting::TransactionType, }; use sp_core::{H160, ecdsa}; use sp_io::TestExternalities; + use sp_io::{hashing::twox_128, storage}; use sp_runtime::traits::{SaturatedConversion, Zero}; use super::*; @@ -1046,6 +1047,7 @@ mod tests { const ACCOUNT: [u8; 32] = [7u8; 32]; const DELEGATE_TAKE_GROUP_ID: GroupId = GROUP_DELEGATE_TAKE; + const PALLET_PREFIX: &[u8] = b"SubtensorModule"; type UsageKey = RateLimitUsageKey; fn new_test_ext() -> TestExternalities { @@ -1089,6 +1091,19 @@ mod tests { pallet_rate_limiting::NextGroupId::::kill(); } + fn set_legacy_serving_rate_limit(netuid: NetUid, span: u64) { + let mut key = twox_128(b"SubtensorModule").to_vec(); + key.extend(twox_128(b"ServingRateLimit")); + key.extend(netuid.encode()); + storage::set(&key, &span.encode()); + } + + fn set_legacy_network_rate_limit(span: u64) { + let mut key = twox_128(b"SubtensorModule").to_vec(); + key.extend(twox_128(b"NetworkRateLimit")); + storage::set(&key, &span.encode()); + } + fn parity_check( now: u64, call: RuntimeCall, @@ -1190,12 +1205,9 @@ mod tests { let account: AccountId = ACCOUNT.into(); pallet_subtensor::HasMigrationRun::::remove(MIGRATION_NAME); - legacy_storage::set_tx_rate_limit(10); - legacy_storage::set_tx_delegate_take_rate_limit(3); - legacy_storage::set_last_rate_limited_block( - crate::rate_limiting::legacy::RateLimitKey::LastTxBlock(account.clone()), - 5, - ); + put_legacy_value(b"TxRateLimit", 10u64); + put_legacy_value(b"TxDelegateTakeRateLimit", 3u64); + put_last_rate_limited_block(RateLimitKey::LastTxBlock(account.clone()), 5); let weight = migrate_rate_limiting(); assert!(!weight.is_zero()); @@ -1304,7 +1316,7 @@ mod tests { let span = 5u64; System::set_block_number(now.saturated_into()); LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, now - 1); - legacy_storage::set_network_rate_limit(span); + set_legacy_network_rate_limit(span); Migration::::on_runtime_upgrade(); @@ -1355,7 +1367,7 @@ mod tests { RateLimitKey::LastTxBlockDelegateTake(hot.clone()), now - 1, ); - legacy_storage::set_tx_delegate_take_rate_limit(span); + put_legacy_value(b"TxDelegateTakeRateLimit", span); let call = RuntimeCall::SubtensorModule(SubtensorCall::increase_take { hotkey: hot.clone(), @@ -1427,7 +1439,7 @@ mod tests { let hot = account(50); let netuid = NetUid::from(3u16); let span = 5u64; - legacy_storage::set_serving_rate_limit(netuid, span); + set_legacy_serving_rate_limit(netuid, span); pallet_subtensor::Axons::::insert( netuid, hot.clone(), @@ -1494,11 +1506,11 @@ mod tests { let uid: u16 = 0; let weights_span = 4u64; let tempo = 3u16; - // Ensure subnet exists so legacy LastUpdate is imported. + // Ensure subnet exists so LastUpdate is imported. NetworksAdded::::insert(netuid, true); SubtensorModule::set_tempo(netuid, tempo); WeightsSetRateLimit::::insert(netuid, weights_span); - legacy_storage::set_last_update(NetUidStorageIndex::from(netuid), vec![now - 1]); + LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![now - 1]); let weights_call = RuntimeCall::SubtensorModule(SubtensorCall::set_weights { netuid, @@ -1511,7 +1523,7 @@ mod tests { let usage = Some(vec![UsageKey::SubnetNeuron { netuid, uid }]); let legacy_weights = || { - let last = legacy_storage::get_last_update(NetUidStorageIndex::from(netuid)) + let last = LastUpdate::::get(NetUidStorageIndex::from(netuid)) .get(uid as usize) .copied() .unwrap_or_default(); @@ -1619,7 +1631,7 @@ mod tests { fn migration_skips_when_already_run() { new_test_ext().execute_with(|| { pallet_subtensor::HasMigrationRun::::insert(MIGRATION_NAME, true); - legacy_storage::set_tx_rate_limit(99); + put_legacy_value(b"TxRateLimit", 99u64); let base_weight = ::DbWeight::get().reads(1); let weight = migrate_rate_limiting(); @@ -1637,4 +1649,19 @@ mod tests { ); }); } + + fn put_legacy_value(storage_name: &[u8], value: impl Encode) { + let key = storage_key(storage_name); + storage::set(&key, &value.encode()); + } + + fn put_last_rate_limited_block(key: RateLimitKey, block: u64) { + let mut storage_key = storage_key(b"LastRateLimitedBlock"); + storage_key.extend(key.encode()); + storage::set(&storage_key, &block.encode()); + } + + fn storage_key(storage_name: &[u8]) -> Vec { + [twox_128(PALLET_PREFIX), twox_128(storage_name)].concat() + } } diff --git a/runtime/src/rate_limiting/legacy.rs b/runtime/src/rate_limiting/legacy.rs index de67ca2039..cab3d37719 100644 --- a/runtime/src/rate_limiting/legacy.rs +++ b/runtime/src/rate_limiting/legacy.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - use codec::{Decode, Encode}; use frame_support::{Identity, migration::storage_key_iter}; use runtime_common::prod_or_fast; @@ -9,7 +7,7 @@ use sp_io::{ storage::{self as io_storage, next_key}, }; use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; -use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; +use subtensor_runtime_common::NetUid; use super::AccountId; use crate::{ @@ -40,45 +38,10 @@ pub mod storage { (items.into_iter().collect(), reads) } - pub fn last_updates() -> (Vec<(NetUidStorageIndex, Vec)>, u64) { - let items: Vec<_> = storage_key_iter::, Identity>( - PALLET_PREFIX, - b"LastUpdate", - ) - .collect(); - let reads = items.len() as u64; - (items, reads) - } - - pub fn set_last_update(netuid_index: NetUidStorageIndex, blocks: Vec) { - let mut key = storage_prefix(PALLET_PREFIX, b"LastUpdate"); - key.extend(netuid_index.encode()); - io_storage::set(&key, &blocks.encode()); - } - - pub fn get_last_update(netuid_index: NetUidStorageIndex) -> Vec { - let mut key = storage_prefix(PALLET_PREFIX, b"LastUpdate"); - key.extend(netuid_index.encode()); - io_storage::get(&key) - .and_then(|bytes| Decode::decode(&mut &bytes[..]).ok()) - .unwrap_or_default() - } - - pub fn set_serving_rate_limit(netuid: NetUid, span: u64) { - let mut key = storage_prefix(PALLET_PREFIX, b"ServingRateLimit"); - key.extend(netuid.encode()); - io_storage::set(&key, &span.encode()); - } - pub fn tx_rate_limit() -> (u64, u64) { value_with_default(b"TxRateLimit", defaults::tx_rate_limit()) } - pub fn set_tx_rate_limit(span: u64) { - let key = storage_prefix(PALLET_PREFIX, b"TxRateLimit"); - io_storage::set(&key, &span.encode()); - } - pub fn tx_delegate_take_rate_limit() -> (u64, u64) { value_with_default( b"TxDelegateTakeRateLimit", @@ -86,11 +49,6 @@ pub mod storage { ) } - pub fn set_tx_delegate_take_rate_limit(span: u64) { - let key = storage_prefix(PALLET_PREFIX, b"TxDelegateTakeRateLimit"); - io_storage::set(&key, &span.encode()); - } - pub fn tx_childkey_take_rate_limit() -> (u64, u64) { value_with_default( b"TxChildkeyTakeRateLimit", @@ -102,11 +60,6 @@ pub mod storage { value_with_default(b"NetworkRateLimit", defaults::network_rate_limit()) } - pub fn set_network_rate_limit(span: u64) { - let key = storage_prefix(PALLET_PREFIX, b"NetworkRateLimit"); - io_storage::set(&key, &span.encode()); - } - pub fn owner_hyperparam_rate_limit() -> (u64, u64) { let (value, reads) = value_with_default::( b"OwnerHyperparamRateLimit", @@ -132,12 +85,6 @@ pub mod storage { (entries, reads) } - pub fn set_last_rate_limited_block(key: RateLimitKey, block: u64) { - let mut storage_key = storage_prefix(PALLET_PREFIX, b"LastRateLimitedBlock"); - storage_key.extend(key.encode()); - io_storage::set(&storage_key, &block.encode()); - } - pub fn transaction_key_last_block() -> (Vec<((AccountId, NetUid, u16), u64)>, u64) { let prefix = storage_prefix(PALLET_PREFIX, b"TransactionKeyLastBlock"); let mut cursor = prefix.clone(); From 14c937874f24f4393383426f4d4cdc196024768f Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 14 Jan 2026 18:43:31 +0100 Subject: [PATCH 67/95] Add weights-set last-seen manual updates --- pallets/subtensor/src/subnets/uids.rs | 58 +++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/pallets/subtensor/src/subnets/uids.rs b/pallets/subtensor/src/subnets/uids.rs index 0a09017e64..bae22e476e 100644 --- a/pallets/subtensor/src/subnets/uids.rs +++ b/pallets/subtensor/src/subnets/uids.rs @@ -1,9 +1,13 @@ use super::*; use frame_support::storage::IterableStorageDoubleMap; -use sp_runtime::Percent; +use rate_limiting_interface::RateLimitingInterface; +use sp_runtime::{Percent, SaturatedConversion}; use sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet}; use sp_std::{cmp, vec}; -use subtensor_runtime_common::NetUid; +use subtensor_runtime_common::{ + MechId, NetUid, + rate_limiting::{self, RateLimitUsageKey}, +}; impl Pallet { /// Returns the number of filled slots on a network. @@ -112,9 +116,16 @@ impl Pallet { Emission::::mutate(netuid, |v| v.push(0.into())); Consensus::::mutate(netuid, |v| v.push(0)); for mecid in 0..MechanismCountCurrent::::get(netuid).into() { - let netuid_index = Self::get_mechanism_storage_index(netuid, mecid.into()); + let mecid: MechId = mecid.into(); + let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); Incentive::::mutate(netuid_index, |v| v.push(0)); Self::set_last_update_for_uid(netuid_index, next_uid, block_number); + let usage = Self::weights_rl_usage_key(netuid, mecid, next_uid); + T::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + Some(block_number.saturated_into()), + ); } Dividends::::mutate(netuid, |v| v.push(0)); ValidatorTrust::::mutate(netuid, |v| v.push(0)); @@ -265,19 +276,46 @@ impl Pallet { // Update incentives/lastupdates for mechanisms for mecid in 0..mechanisms_count { - let netuid_index = Self::get_mechanism_storage_index(netuid, mecid.into()); + let mecid: MechId = mecid.into(); + let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); let incentive = Incentive::::get(netuid_index); let lastupdate = LastUpdate::::get(netuid_index); let mut trimmed_incentive = Vec::with_capacity(trimmed_uids.len()); let mut trimmed_lastupdate = Vec::with_capacity(trimmed_uids.len()); + let mut trimmed_last_seen = Vec::with_capacity(trimmed_uids.len()); for uid in &trimmed_uids { trimmed_incentive.push(incentive.get(*uid).cloned().unwrap_or_default()); trimmed_lastupdate.push(lastupdate.get(*uid).cloned().unwrap_or_default()); + let usage = Self::weights_rl_usage_key(netuid, mecid, *uid as u16); + trimmed_last_seen.push(T::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + )); } Incentive::::insert(netuid_index, trimmed_incentive); LastUpdate::::insert(netuid_index, trimmed_lastupdate); + + for uid in 0..current_n { + let usage = Self::weights_rl_usage_key(netuid, mecid, uid); + T::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + None, + ); + } + for (new_uid, last_seen) in trimmed_last_seen.into_iter().enumerate() { + let Some(block) = last_seen else { + continue; + }; + let usage = Self::weights_rl_usage_key(netuid, mecid, new_uid as u16); + T::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SUBNET, + Some(usage), + Some(block), + ); + } } // Create mapping from old uid to new compressed uid @@ -439,4 +477,16 @@ impl Pallet { pub fn is_hotkey_registered_on_specific_network(hotkey: &T::AccountId, netuid: NetUid) -> bool { IsNetworkMember::::contains_key(hotkey, netuid) } + + fn weights_rl_usage_key( + netuid: NetUid, + mecid: MechId, + uid: u16, + ) -> RateLimitUsageKey { + if mecid == MechId::MAIN { + RateLimitUsageKey::SubnetNeuron { netuid, uid } + } else { + RateLimitUsageKey::SubnetMechanismNeuron { netuid, mecid, uid } + } + } } From c72a9132960e03f129d59f45f7831fdf3fa233e9 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 14 Jan 2026 19:10:25 +0100 Subject: [PATCH 68/95] Clean up rate-limiting migration tests --- runtime/src/migrations/rate_limiting.rs | 48 +++++----------------- runtime/src/rate_limiting/legacy.rs | 53 ++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 4a5226a809..925de2dc48 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -1021,7 +1021,6 @@ fn block_number(value: u64) -> Option> { #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { - use codec::Encode; use frame_support::traits::OnRuntimeUpgrade; use frame_system::pallet_prelude::BlockNumberFor; use pallet_rate_limiting::{ @@ -1035,7 +1034,6 @@ mod tests { }; use sp_core::{H160, ecdsa}; use sp_io::TestExternalities; - use sp_io::{hashing::twox_128, storage}; use sp_runtime::traits::{SaturatedConversion, Zero}; use super::*; @@ -1047,7 +1045,6 @@ mod tests { const ACCOUNT: [u8; 32] = [7u8; 32]; const DELEGATE_TAKE_GROUP_ID: GroupId = GROUP_DELEGATE_TAKE; - const PALLET_PREFIX: &[u8] = b"SubtensorModule"; type UsageKey = RateLimitUsageKey; fn new_test_ext() -> TestExternalities { @@ -1091,19 +1088,6 @@ mod tests { pallet_rate_limiting::NextGroupId::::kill(); } - fn set_legacy_serving_rate_limit(netuid: NetUid, span: u64) { - let mut key = twox_128(b"SubtensorModule").to_vec(); - key.extend(twox_128(b"ServingRateLimit")); - key.extend(netuid.encode()); - storage::set(&key, &span.encode()); - } - - fn set_legacy_network_rate_limit(span: u64) { - let mut key = twox_128(b"SubtensorModule").to_vec(); - key.extend(twox_128(b"NetworkRateLimit")); - storage::set(&key, &span.encode()); - } - fn parity_check( now: u64, call: RuntimeCall, @@ -1205,9 +1189,12 @@ mod tests { let account: AccountId = ACCOUNT.into(); pallet_subtensor::HasMigrationRun::::remove(MIGRATION_NAME); - put_legacy_value(b"TxRateLimit", 10u64); - put_legacy_value(b"TxDelegateTakeRateLimit", 3u64); - put_last_rate_limited_block(RateLimitKey::LastTxBlock(account.clone()), 5); + legacy_storage::set_tx_rate_limit(10); + legacy_storage::set_tx_delegate_take_rate_limit(3); + legacy_storage::set_last_rate_limited_block( + super::RateLimitKey::LastTxBlock(account.clone()), + 5, + ); let weight = migrate_rate_limiting(); assert!(!weight.is_zero()); @@ -1316,7 +1303,7 @@ mod tests { let span = 5u64; System::set_block_number(now.saturated_into()); LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, now - 1); - set_legacy_network_rate_limit(span); + legacy_storage::set_network_rate_limit(span); Migration::::on_runtime_upgrade(); @@ -1367,7 +1354,7 @@ mod tests { RateLimitKey::LastTxBlockDelegateTake(hot.clone()), now - 1, ); - put_legacy_value(b"TxDelegateTakeRateLimit", span); + legacy_storage::set_tx_delegate_take_rate_limit(span); let call = RuntimeCall::SubtensorModule(SubtensorCall::increase_take { hotkey: hot.clone(), @@ -1439,7 +1426,7 @@ mod tests { let hot = account(50); let netuid = NetUid::from(3u16); let span = 5u64; - set_legacy_serving_rate_limit(netuid, span); + legacy_storage::set_serving_rate_limit(netuid, span); pallet_subtensor::Axons::::insert( netuid, hot.clone(), @@ -1631,7 +1618,7 @@ mod tests { fn migration_skips_when_already_run() { new_test_ext().execute_with(|| { pallet_subtensor::HasMigrationRun::::insert(MIGRATION_NAME, true); - put_legacy_value(b"TxRateLimit", 99u64); + legacy_storage::set_tx_rate_limit(99); let base_weight = ::DbWeight::get().reads(1); let weight = migrate_rate_limiting(); @@ -1649,19 +1636,4 @@ mod tests { ); }); } - - fn put_legacy_value(storage_name: &[u8], value: impl Encode) { - let key = storage_key(storage_name); - storage::set(&key, &value.encode()); - } - - fn put_last_rate_limited_block(key: RateLimitKey, block: u64) { - let mut storage_key = storage_key(b"LastRateLimitedBlock"); - storage_key.extend(key.encode()); - storage::set(&storage_key, &block.encode()); - } - - fn storage_key(storage_name: &[u8]) -> Vec { - [twox_128(PALLET_PREFIX), twox_128(storage_name)].concat() - } } diff --git a/runtime/src/rate_limiting/legacy.rs b/runtime/src/rate_limiting/legacy.rs index cab3d37719..2de1cdf9d6 100644 --- a/runtime/src/rate_limiting/legacy.rs +++ b/runtime/src/rate_limiting/legacy.rs @@ -7,7 +7,7 @@ use sp_io::{ storage::{self as io_storage, next_key}, }; use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; -use subtensor_runtime_common::NetUid; +use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; use super::AccountId; use crate::{ @@ -38,10 +38,45 @@ pub mod storage { (items.into_iter().collect(), reads) } + pub fn last_updates() -> (Vec<(NetUidStorageIndex, Vec)>, u64) { + let items: Vec<_> = storage_key_iter::, Identity>( + PALLET_PREFIX, + b"LastUpdate", + ) + .collect(); + let reads = items.len() as u64; + (items, reads) + } + + pub fn set_last_update(netuid_index: NetUidStorageIndex, blocks: Vec) { + let mut key = storage_prefix(PALLET_PREFIX, b"LastUpdate"); + key.extend(netuid_index.encode()); + io_storage::set(&key, &blocks.encode()); + } + + pub fn get_last_update(netuid_index: NetUidStorageIndex) -> Vec { + let mut key = storage_prefix(PALLET_PREFIX, b"LastUpdate"); + key.extend(netuid_index.encode()); + io_storage::get(&key) + .and_then(|bytes| Decode::decode(&mut &bytes[..]).ok()) + .unwrap_or_default() + } + + pub fn set_serving_rate_limit(netuid: NetUid, span: u64) { + let mut key = storage_prefix(PALLET_PREFIX, b"ServingRateLimit"); + key.extend(netuid.encode()); + io_storage::set(&key, &span.encode()); + } + pub fn tx_rate_limit() -> (u64, u64) { value_with_default(b"TxRateLimit", defaults::tx_rate_limit()) } + pub fn set_tx_rate_limit(span: u64) { + let key = storage_prefix(PALLET_PREFIX, b"TxRateLimit"); + io_storage::set(&key, &span.encode()); + } + pub fn tx_delegate_take_rate_limit() -> (u64, u64) { value_with_default( b"TxDelegateTakeRateLimit", @@ -49,6 +84,11 @@ pub mod storage { ) } + pub fn set_tx_delegate_take_rate_limit(span: u64) { + let key = storage_prefix(PALLET_PREFIX, b"TxDelegateTakeRateLimit"); + io_storage::set(&key, &span.encode()); + } + pub fn tx_childkey_take_rate_limit() -> (u64, u64) { value_with_default( b"TxChildkeyTakeRateLimit", @@ -60,6 +100,11 @@ pub mod storage { value_with_default(b"NetworkRateLimit", defaults::network_rate_limit()) } + pub fn set_network_rate_limit(span: u64) { + let key = storage_prefix(PALLET_PREFIX, b"NetworkRateLimit"); + io_storage::set(&key, &span.encode()); + } + pub fn owner_hyperparam_rate_limit() -> (u64, u64) { let (value, reads) = value_with_default::( b"OwnerHyperparamRateLimit", @@ -85,6 +130,12 @@ pub mod storage { (entries, reads) } + pub fn set_last_rate_limited_block(key: RateLimitKey, block: u64) { + let mut storage_key = storage_prefix(PALLET_PREFIX, b"LastRateLimitedBlock"); + storage_key.extend(key.encode()); + io_storage::set(&storage_key, &block.encode()); + } + pub fn transaction_key_last_block() -> (Vec<((AccountId, NetUid, u16), u64)>, u64) { let prefix = storage_prefix(PALLET_PREFIX, b"TransactionKeyLastBlock"); let mut cursor = prefix.clone(); From ae3cdd30a287945928c1a11cd5c5ac1513eecd55 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 15 Jan 2026 16:07:11 +0100 Subject: [PATCH 69/95] Remove WeightsSetRateLimit --- pallets/subtensor/src/coinbase/root.rs | 1 - pallets/subtensor/src/lib.rs | 14 +--- pallets/subtensor/src/macros/errors.rs | 2 - pallets/subtensor/src/macros/events.rs | 2 - pallets/subtensor/src/subnets/weights.rs | 3 - pallets/subtensor/src/tests/networks.rs | 2 - pallets/subtensor/src/utils/misc.rs | 92 ++++++++++++++++++++++++ precompiles/src/subnet.rs | 28 +++++--- runtime/src/migrations/rate_limiting.rs | 6 +- runtime/src/rate_limiting/legacy.rs | 14 ++++ 10 files changed, 130 insertions(+), 34 deletions(-) diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index c01fc38442..106a5d6f28 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -337,7 +337,6 @@ impl Pallet { BondsMovingAverage::::remove(netuid); BondsPenalty::::remove(netuid); BondsResetOn::::remove(netuid); - WeightsSetRateLimit::::remove(netuid); ValidatorPruneLen::::remove(netuid); ScalingLawPower::::remove(netuid); TargetRegistrationsPerInterval::::remove(netuid); diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 8459d49369..e76a8f6f5e 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -697,12 +697,6 @@ pub mod pallet { T::InitialTempo::get() } - /// Default value for weights set rate limit. - #[pallet::type_value] - pub fn DefaultWeightsSetRateLimit() -> u64 { - 100 - } - /// Default block number at registration. #[pallet::type_value] pub fn DefaultBlockAtRegistration() -> u64 { @@ -1746,11 +1740,6 @@ pub mod pallet { pub type BondsResetOn = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultBondsResetOn>; - /// --- MAP ( netuid ) --> weights_set_rate_limit - #[pallet::storage] - pub type WeightsSetRateLimit = - StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultWeightsSetRateLimit>; - /// --- MAP ( netuid ) --> validator_prune_len #[pallet::storage] pub type ValidatorPruneLen = @@ -1947,7 +1936,8 @@ pub mod pallet { #[pallet::storage] pub type Emission = StorageMap<_, Identity, NetUid, Vec, ValueQuery>; - /// --- MAP ( netuid ) --> last_update + /// Last updated weights per neuron (used for activity/outdated masking in epochs). + /// This is not rate-limiting state; rate-limiting uses `pallet-rate-limiting` last-seen. #[pallet::storage] pub type LastUpdate = StorageMap<_, Identity, NetUidStorageIndex, Vec, ValueQuery, EmptyU64Vec>; diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index b5a1e8f37f..29bad79b51 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -175,8 +175,6 @@ mod errors { RevealTooEarly, /// Attempted to batch reveal weights with mismatched vector input lenghts. InputLengthsUnequal, - /// A transactor exceeded the rate limit for setting weights. - CommittingWeightsTooFast, /// Stake amount is too low. AmountTooLow, /// Not enough liquidity. diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 80852f4c1b..6b2df74010 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -75,8 +75,6 @@ mod events { ValidatorPruneLenSet(NetUid, u64), /// the scaling law power has been set for a subnet. ScalingLawPowerSet(NetUid, u16), - /// weights set rate limit has been set for a subnet. - WeightsSetRateLimitSet(NetUid, u64), /// immunity period is set for a subnet. ImmunityPeriodSet(NetUid, u16), /// bonds moving average is set for a subnet. diff --git a/pallets/subtensor/src/subnets/weights.rs b/pallets/subtensor/src/subnets/weights.rs index ac96f68720..3e8a909da8 100644 --- a/pallets/subtensor/src/subnets/weights.rs +++ b/pallets/subtensor/src/subnets/weights.rs @@ -32,9 +32,6 @@ impl Pallet { /// * `HotKeyNotRegisteredInSubNet`: /// - Raised if the hotkey is not registered on the specified network. /// - /// * `CommittingWeightsTooFast`: - /// - Raised if the hotkey's commit rate exceeds the permitted limit. - /// /// * `TooManyUnrevealedCommits`: /// - Raised if the hotkey has reached the maximum number of unrevealed commits. /// diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 3ef476fa00..e867dee6fa 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -398,7 +398,6 @@ fn dissolve_clears_all_per_subnet_storages() { BondsMovingAverage::::insert(net, 1u64); BondsPenalty::::insert(net, 1u16); BondsResetOn::::insert(net, true); - WeightsSetRateLimit::::insert(net, 1u64); ValidatorPruneLen::::insert(net, 1u64); ScalingLawPower::::insert(net, 1u16); TargetRegistrationsPerInterval::::insert(net, 1u16); @@ -557,7 +556,6 @@ fn dissolve_clears_all_per_subnet_storages() { assert!(!BondsMovingAverage::::contains_key(net)); assert!(!BondsPenalty::::contains_key(net)); assert!(!BondsResetOn::::contains_key(net)); - assert!(!WeightsSetRateLimit::::contains_key(net)); assert!(!ValidatorPruneLen::::contains_key(net)); assert!(!ScalingLawPower::::contains_key(net)); assert!(!TargetRegistrationsPerInterval::::contains_key(net)); diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 89d5a1446a..f103a50516 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -109,27 +109,34 @@ impl Pallet { Tempo::::insert(netuid, tempo); Self::deposit_event(Event::TempoSet(netuid, tempo)); } + pub fn set_last_adjustment_block(netuid: NetUid, last_adjustment_block: u64) { LastAdjustmentBlock::::insert(netuid, last_adjustment_block); } + pub fn set_blocks_since_last_step(netuid: NetUid, blocks_since_last_step: u64) { BlocksSinceLastStep::::insert(netuid, blocks_since_last_step); } + pub fn set_registrations_this_block(netuid: NetUid, registrations_this_block: u16) { RegistrationsThisBlock::::insert(netuid, registrations_this_block); } + pub fn set_last_mechanism_step_block(netuid: NetUid, last_mechanism_step_block: u64) { LastMechansimStepBlock::::insert(netuid, last_mechanism_step_block); } + pub fn set_registrations_this_interval(netuid: NetUid, registrations_this_interval: u16) { RegistrationsThisInterval::::insert(netuid, registrations_this_interval); } + pub fn set_pow_registrations_this_interval( netuid: NetUid, pow_registrations_this_interval: u16, ) { POWRegistrationsThisInterval::::insert(netuid, pow_registrations_this_interval); } + pub fn set_burn_registrations_this_interval( netuid: NetUid, burn_registrations_this_interval: u16, @@ -143,6 +150,7 @@ impl Pallet { pub fn get_total_issuance() -> TaoCurrency { TotalIssuance::::get() } + pub fn get_current_block_as_u64() -> u64 { TryInto::try_into(>::block_number()) .ok() @@ -155,24 +163,31 @@ impl Pallet { pub fn get_rank(netuid: NetUid) -> Vec { Rank::::get(netuid) } + pub fn get_trust(netuid: NetUid) -> Vec { Trust::::get(netuid) } + pub fn get_active(netuid: NetUid) -> Vec { Active::::get(netuid) } + pub fn get_emission(netuid: NetUid) -> Vec { Emission::::get(netuid) } + pub fn get_consensus(netuid: NetUid) -> Vec { Consensus::::get(netuid) } + pub fn get_incentive(netuid: NetUidStorageIndex) -> Vec { Incentive::::get(netuid) } + pub fn get_dividends(netuid: NetUid) -> Vec { Dividends::::get(netuid) } + /// Fetch LastUpdate for `netuid` and ensure its length is at least `get_subnetwork_n(netuid)`, /// padding with zeros if needed. Returns the (possibly padded) vector. pub fn get_last_update(netuid_index: NetUidStorageIndex) -> Vec { @@ -184,12 +199,15 @@ impl Pallet { } v } + pub fn get_pruning_score(netuid: NetUid) -> Vec { PruningScores::::get(netuid) } + pub fn get_validator_trust(netuid: NetUid) -> Vec { ValidatorTrust::::get(netuid) } + pub fn get_validator_permit(netuid: NetUid) -> Vec { ValidatorPermit::::get(netuid) } @@ -205,6 +223,7 @@ impl Pallet { *updated_last_update = last_update; LastUpdate::::insert(netuid, updated_last_update_vec); } + pub fn set_active_for_uid(netuid: NetUid, uid: u16, active: bool) { let mut updated_active_vec = Self::get_active(netuid); let Some(updated_active) = updated_active_vec.get_mut(uid as usize) else { @@ -213,6 +232,7 @@ impl Pallet { *updated_active = active; Active::::insert(netuid, updated_active_vec); } + pub fn set_validator_permit_for_uid(netuid: NetUid, uid: u16, validator_permit: bool) { let mut updated_validator_permits = Self::get_validator_permit(netuid); let Some(updated_validator_permit) = updated_validator_permits.get_mut(uid as usize) else { @@ -221,6 +241,7 @@ impl Pallet { *updated_validator_permit = validator_permit; ValidatorPermit::::insert(netuid, updated_validator_permits); } + pub fn set_stake_threshold(min_stake: u64) { StakeThreshold::::put(min_stake); Self::deposit_event(Event::StakeThresholdSet(min_stake)); @@ -230,46 +251,57 @@ impl Pallet { let vec = Rank::::get(netuid); vec.get(uid as usize).copied().unwrap_or(0) } + pub fn get_trust_for_uid(netuid: NetUid, uid: u16) -> u16 { let vec = Trust::::get(netuid); vec.get(uid as usize).copied().unwrap_or(0) } + pub fn get_emission_for_uid(netuid: NetUid, uid: u16) -> AlphaCurrency { let vec = Emission::::get(netuid); vec.get(uid as usize).copied().unwrap_or_default() } + pub fn get_active_for_uid(netuid: NetUid, uid: u16) -> bool { let vec = Active::::get(netuid); vec.get(uid as usize).copied().unwrap_or(false) } + pub fn get_consensus_for_uid(netuid: NetUid, uid: u16) -> u16 { let vec = Consensus::::get(netuid); vec.get(uid as usize).copied().unwrap_or(0) } + pub fn get_incentive_for_uid(netuid: NetUidStorageIndex, uid: u16) -> u16 { let vec = Incentive::::get(netuid); vec.get(uid as usize).copied().unwrap_or(0) } + pub fn get_dividends_for_uid(netuid: NetUid, uid: u16) -> u16 { let vec = Dividends::::get(netuid); vec.get(uid as usize).copied().unwrap_or(0) } + pub fn get_last_update_for_uid(netuid: NetUidStorageIndex, uid: u16) -> u64 { let vec = LastUpdate::::get(netuid); vec.get(uid as usize).copied().unwrap_or(0) } + pub fn get_pruning_score_for_uid(netuid: NetUid, uid: u16) -> u16 { let vec = PruningScores::::get(netuid); vec.get(uid as usize).copied().unwrap_or(u16::MAX) } + pub fn get_validator_trust_for_uid(netuid: NetUid, uid: u16) -> u16 { let vec = ValidatorTrust::::get(netuid); vec.get(uid as usize).copied().unwrap_or(0) } + pub fn get_validator_permit_for_uid(netuid: NetUid, uid: u16) -> bool { let vec = ValidatorPermit::::get(netuid); vec.get(uid as usize).copied().unwrap_or(false) } + pub fn get_stake_threshold() -> u64 { StakeThreshold::::get() } @@ -280,33 +312,43 @@ impl Pallet { pub fn get_tempo(netuid: NetUid) -> u16 { Tempo::::get(netuid) } + pub fn get_last_adjustment_block(netuid: NetUid) -> u64 { LastAdjustmentBlock::::get(netuid) } + pub fn get_blocks_since_last_step(netuid: NetUid) -> u64 { BlocksSinceLastStep::::get(netuid) } + pub fn get_difficulty(netuid: NetUid) -> U256 { U256::from(Self::get_difficulty_as_u64(netuid)) } + pub fn get_registrations_this_block(netuid: NetUid) -> u16 { RegistrationsThisBlock::::get(netuid) } + pub fn get_last_mechanism_step_block(netuid: NetUid) -> u64 { LastMechansimStepBlock::::get(netuid) } + pub fn get_registrations_this_interval(netuid: NetUid) -> u16 { RegistrationsThisInterval::::get(netuid) } + pub fn get_pow_registrations_this_interval(netuid: NetUid) -> u16 { POWRegistrationsThisInterval::::get(netuid) } + pub fn get_burn_registrations_this_interval(netuid: NetUid) -> u16 { BurnRegistrationsThisInterval::::get(netuid) } + pub fn get_neuron_block_at_registration(netuid: NetUid, neuron_uid: u16) -> u64 { BlockAtRegistration::::get(netuid, neuron_uid) } + /// Returns the minimum number of non-immortal & non-immune UIDs that must remain in a subnet. pub fn get_min_non_immune_uids(netuid: NetUid) -> u16 { MinNonImmuneUids::::get(netuid) @@ -343,6 +385,7 @@ impl Pallet { pub fn recycle_tao(amount: TaoCurrency) { TotalIssuance::::put(TotalIssuance::::get().saturating_sub(amount)); } + pub fn increase_issuance(amount: TaoCurrency) { TotalIssuance::::put(TotalIssuance::::get().saturating_add(amount)); } @@ -350,9 +393,11 @@ impl Pallet { pub fn set_subnet_locked_balance(netuid: NetUid, amount: TaoCurrency) { SubnetLocked::::insert(netuid, amount); } + pub fn get_subnet_locked_balance(netuid: NetUid) -> TaoCurrency { SubnetLocked::::get(netuid) } + pub fn get_total_subnet_locked() -> TaoCurrency { let mut total_subnet_locked: u64 = 0; for (_, locked) in SubnetLocked::::iter() { @@ -373,48 +418,60 @@ impl Pallet { pub fn get_tx_rate_limit() -> u64 { TxRateLimit::::get() } + pub fn set_tx_rate_limit(tx_rate_limit: u64) { TxRateLimit::::put(tx_rate_limit); Self::deposit_event(Event::TxRateLimitSet(tx_rate_limit)); } + pub fn set_min_delegate_take(take: u16) { MinDelegateTake::::put(take); Self::deposit_event(Event::MinDelegateTakeSet(take)); } + pub fn set_max_delegate_take(take: u16) { MaxDelegateTake::::put(take); Self::deposit_event(Event::MaxDelegateTakeSet(take)); } + pub fn get_min_delegate_take() -> u16 { MinDelegateTake::::get() } + pub fn get_max_delegate_take() -> u16 { MaxDelegateTake::::get() } + pub fn get_default_delegate_take() -> u16 { // Default to maximum MaxDelegateTake::::get() } + // get_default_childkey_take pub fn get_default_childkey_take() -> u16 { // Default to maximum MinChildkeyTake::::get() } + pub fn get_tx_childkey_take_rate_limit() -> u64 { TxChildkeyTakeRateLimit::::get() } + pub fn set_tx_childkey_take_rate_limit(tx_rate_limit: u64) { TxChildkeyTakeRateLimit::::put(tx_rate_limit); Self::deposit_event(Event::TxChildKeyTakeRateLimitSet(tx_rate_limit)); } + pub fn set_min_childkey_take(take: u16) { MinChildkeyTake::::put(take); Self::deposit_event(Event::MinChildKeyTakeSet(take)); } + pub fn set_max_childkey_take(take: u16) { MaxChildkeyTake::::put(take); Self::deposit_event(Event::MaxChildKeyTakeSet(take)); } + pub fn get_min_childkey_take() -> u16 { MinChildkeyTake::::get() } @@ -432,6 +489,7 @@ impl Pallet { pub fn get_min_difficulty(netuid: NetUid) -> u64 { MinDifficulty::::get(netuid) } + pub fn set_min_difficulty(netuid: NetUid, min_difficulty: u64) { MinDifficulty::::insert(netuid, min_difficulty); Self::deposit_event(Event::MinDifficultySet(netuid, min_difficulty)); @@ -440,6 +498,7 @@ impl Pallet { pub fn get_max_difficulty(netuid: NetUid) -> u64 { MaxDifficulty::::get(netuid) } + pub fn set_max_difficulty(netuid: NetUid, max_difficulty: u64) { MaxDifficulty::::insert(netuid, max_difficulty); Self::deposit_event(Event::MaxDifficultySet(netuid, max_difficulty)); @@ -448,6 +507,7 @@ impl Pallet { pub fn get_weights_version_key(netuid: NetUid) -> u64 { WeightsVersionKey::::get(netuid) } + pub fn set_weights_version_key(netuid: NetUid, weights_version_key: u64) { WeightsVersionKey::::insert(netuid, weights_version_key); Self::deposit_event(Event::WeightsVersionKeySet(netuid, weights_version_key)); @@ -462,6 +522,7 @@ impl Pallet { pub fn get_adjustment_interval(netuid: NetUid) -> u16 { AdjustmentInterval::::get(netuid) } + pub fn set_adjustment_interval(netuid: NetUid, adjustment_interval: u16) { AdjustmentInterval::::insert(netuid, adjustment_interval); Self::deposit_event(Event::AdjustmentIntervalSet(netuid, adjustment_interval)); @@ -470,6 +531,7 @@ impl Pallet { pub fn get_adjustment_alpha(netuid: NetUid) -> u64 { AdjustmentAlpha::::get(netuid) } + pub fn set_adjustment_alpha(netuid: NetUid, adjustment_alpha: u64) { AdjustmentAlpha::::insert(netuid, adjustment_alpha); Self::deposit_event(Event::AdjustmentAlphaSet(netuid, adjustment_alpha)); @@ -483,6 +545,7 @@ impl Pallet { pub fn get_scaling_law_power(netuid: NetUid) -> u16 { ScalingLawPower::::get(netuid) } + pub fn set_scaling_law_power(netuid: NetUid, scaling_law_power: u16) { ScalingLawPower::::insert(netuid, scaling_law_power); Self::deposit_event(Event::ScalingLawPowerSet(netuid, scaling_law_power)); @@ -496,10 +559,12 @@ impl Pallet { pub fn get_immunity_period(netuid: NetUid) -> u16 { ImmunityPeriod::::get(netuid) } + pub fn set_immunity_period(netuid: NetUid, immunity_period: u16) { ImmunityPeriod::::insert(netuid, immunity_period); Self::deposit_event(Event::ImmunityPeriodSet(netuid, immunity_period)); } + /// Check if a neuron is in immunity based on the current block pub fn get_neuron_is_immune(netuid: NetUid, uid: u16) -> bool { let registered_at = Self::get_neuron_block_at_registration(netuid, uid); @@ -519,6 +584,7 @@ impl Pallet { pub fn get_min_allowed_uids(netuid: NetUid) -> u16 { MinAllowedUids::::get(netuid) } + pub fn set_min_allowed_uids(netuid: NetUid, min_allowed: u16) { MinAllowedUids::::insert(netuid, min_allowed); Self::deposit_event(Event::MinAllowedUidsSet(netuid, min_allowed)); @@ -527,6 +593,7 @@ impl Pallet { pub fn get_max_allowed_uids(netuid: NetUid) -> u16 { MaxAllowedUids::::get(netuid) } + pub fn set_max_allowed_uids(netuid: NetUid, max_allowed: u16) { MaxAllowedUids::::insert(netuid, max_allowed); Self::deposit_event(Event::MaxAllowedUidsSet(netuid, max_allowed)); @@ -535,27 +602,34 @@ impl Pallet { pub fn get_kappa(netuid: NetUid) -> u16 { Kappa::::get(netuid) } + pub fn set_kappa(netuid: NetUid, kappa: u16) { Kappa::::insert(netuid, kappa); Self::deposit_event(Event::KappaSet(netuid, kappa)); } + pub fn get_commit_reveal_weights_enabled(netuid: NetUid) -> bool { CommitRevealWeightsEnabled::::get(netuid) } + pub fn set_commit_reveal_weights_enabled(netuid: NetUid, enabled: bool) { CommitRevealWeightsEnabled::::set(netuid, enabled); Self::deposit_event(Event::CommitRevealEnabled(netuid, enabled)); } + pub fn get_commit_reveal_weights_version() -> u16 { CommitRevealWeightsVersion::::get() } + pub fn set_commit_reveal_weights_version(version: u16) { CommitRevealWeightsVersion::::set(version); Self::deposit_event(Event::CommitRevealVersionSet(version)); } + pub fn get_rho(netuid: NetUid) -> u16 { Rho::::get(netuid) } + pub fn set_rho(netuid: NetUid, rho: u16) { Rho::::insert(netuid, rho); } @@ -563,6 +637,7 @@ impl Pallet { pub fn get_activity_cutoff(netuid: NetUid) -> u16 { ActivityCutoff::::get(netuid) } + pub fn set_activity_cutoff(netuid: NetUid, activity_cutoff: u16) { ActivityCutoff::::insert(netuid, activity_cutoff); Self::deposit_event(Event::ActivityCutoffSet(netuid, activity_cutoff)); @@ -572,6 +647,7 @@ impl Pallet { pub fn get_network_registration_allowed(netuid: NetUid) -> bool { NetworkRegistrationAllowed::::get(netuid) } + pub fn set_network_registration_allowed(netuid: NetUid, registration_allowed: bool) { NetworkRegistrationAllowed::::insert(netuid, registration_allowed); Self::deposit_event(Event::RegistrationAllowed(netuid, registration_allowed)); @@ -580,6 +656,7 @@ impl Pallet { pub fn get_network_pow_registration_allowed(netuid: NetUid) -> bool { NetworkPowRegistrationAllowed::::get(netuid) } + pub fn set_network_pow_registration_allowed(netuid: NetUid, registration_allowed: bool) { NetworkPowRegistrationAllowed::::insert(netuid, registration_allowed); Self::deposit_event(Event::PowRegistrationAllowed(netuid, registration_allowed)); @@ -588,6 +665,7 @@ impl Pallet { pub fn get_target_registrations_per_interval(netuid: NetUid) -> u16 { TargetRegistrationsPerInterval::::get(netuid) } + pub fn set_target_registrations_per_interval( netuid: NetUid, target_registrations_per_interval: u16, @@ -602,6 +680,7 @@ impl Pallet { pub fn get_burn(netuid: NetUid) -> TaoCurrency { Burn::::get(netuid) } + pub fn set_burn(netuid: NetUid, burn: TaoCurrency) { Burn::::insert(netuid, burn); } @@ -609,6 +688,7 @@ impl Pallet { pub fn get_min_burn(netuid: NetUid) -> TaoCurrency { MinBurn::::get(netuid) } + pub fn set_min_burn(netuid: NetUid, min_burn: TaoCurrency) { MinBurn::::insert(netuid, min_burn); Self::deposit_event(Event::MinBurnSet(netuid, min_burn)); @@ -617,6 +697,7 @@ impl Pallet { pub fn get_max_burn(netuid: NetUid) -> TaoCurrency { MaxBurn::::get(netuid) } + pub fn set_max_burn(netuid: NetUid, max_burn: TaoCurrency) { MaxBurn::::insert(netuid, max_burn); Self::deposit_event(Event::MaxBurnSet(netuid, max_burn)); @@ -625,6 +706,7 @@ impl Pallet { pub fn get_difficulty_as_u64(netuid: NetUid) -> u64 { Difficulty::::get(netuid) } + pub fn set_difficulty(netuid: NetUid, difficulty: u64) { Difficulty::::insert(netuid, difficulty); Self::deposit_event(Event::DifficultySet(netuid, difficulty)); @@ -633,6 +715,7 @@ impl Pallet { pub fn get_max_allowed_validators(netuid: NetUid) -> u16 { MaxAllowedValidators::::get(netuid) } + pub fn set_max_allowed_validators(netuid: NetUid, max_allowed_validators: u16) { MaxAllowedValidators::::insert(netuid, max_allowed_validators); Self::deposit_event(Event::MaxAllowedValidatorsSet( @@ -644,6 +727,7 @@ impl Pallet { pub fn get_bonds_moving_average(netuid: NetUid) -> u64 { BondsMovingAverage::::get(netuid) } + pub fn set_bonds_moving_average(netuid: NetUid, bonds_moving_average: u64) { BondsMovingAverage::::insert(netuid, bonds_moving_average); Self::deposit_event(Event::BondsMovingAverageSet(netuid, bonds_moving_average)); @@ -660,6 +744,7 @@ impl Pallet { pub fn get_bonds_reset(netuid: NetUid) -> bool { BondsResetOn::::get(netuid) } + pub fn set_bonds_reset(netuid: NetUid, bonds_reset: bool) { BondsResetOn::::insert(netuid, bonds_reset); Self::deposit_event(Event::BondsResetOnSet(netuid, bonds_reset)); @@ -668,6 +753,7 @@ impl Pallet { pub fn get_max_registrations_per_block(netuid: NetUid) -> u16 { MaxRegistrationsPerBlock::::get(netuid) } + pub fn set_max_registrations_per_block(netuid: NetUid, max_registrations_per_block: u16) { MaxRegistrationsPerBlock::::insert(netuid, max_registrations_per_block); Self::deposit_event(Event::MaxRegistrationsPerBlockSet( @@ -679,13 +765,16 @@ impl Pallet { pub fn get_subnet_owner(netuid: NetUid) -> T::AccountId { SubnetOwner::::get(netuid) } + pub fn get_subnet_owner_cut() -> u16 { SubnetOwnerCut::::get() } + pub fn get_float_subnet_owner_cut() -> U96F32 { U96F32::saturating_from_num(SubnetOwnerCut::::get()) .safe_div(U96F32::saturating_from_num(u16::MAX)) } + pub fn set_subnet_owner_cut(subnet_owner_cut: u16) { SubnetOwnerCut::::set(subnet_owner_cut); Self::deposit_event(Event::SubnetOwnerCutSet(subnet_owner_cut)); @@ -694,6 +783,7 @@ impl Pallet { pub fn get_owned_hotkeys(coldkey: &T::AccountId) -> Vec { OwnedHotkeys::::get(coldkey) } + pub fn get_all_staked_hotkeys(coldkey: &T::AccountId) -> Vec { StakingHotkeys::::get(coldkey) } @@ -705,10 +795,12 @@ impl Pallet { pub fn get_rao_recycled(netuid: NetUid) -> TaoCurrency { RAORecycledForRegistration::::get(netuid) } + pub fn set_rao_recycled(netuid: NetUid, rao_recycled: TaoCurrency) { RAORecycledForRegistration::::insert(netuid, rao_recycled); Self::deposit_event(Event::RAORecycledForRegistrationSet(netuid, rao_recycled)); } + pub fn increase_rao_recycled(netuid: NetUid, inc_rao_recycled: TaoCurrency) { let curr_rao_recycled = Self::get_rao_recycled(netuid); let rao_recycled = curr_rao_recycled.saturating_add(inc_rao_recycled); diff --git a/precompiles/src/subnet.rs b/precompiles/src/subnet.rs index 55524ee1ba..d6273fdd0f 100644 --- a/precompiles/src/subnet.rs +++ b/precompiles/src/subnet.rs @@ -10,7 +10,7 @@ use precompile_utils::{EvmResult, prelude::BoundedString}; use sp_core::H256; use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable, SaturatedConversion}; use sp_std::vec; -use subtensor_runtime_common::{Currency, NetUid}; +use subtensor_runtime_common::{Currency, NetUid, rate_limiting}; use crate::{PrecompileExt, PrecompileHandleExt}; @@ -272,20 +272,30 @@ where #[precompile::public("getWeightsSetRateLimit(uint16)")] #[precompile::view] fn get_weights_set_rate_limit(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { - Ok(pallet_subtensor::WeightsSetRateLimit::::get( - NetUid::from(netuid), - )) + let target = RateLimitTarget::Group(rate_limiting::GROUP_WEIGHTS_SUBNET); + let scope = Some(NetUid::from(netuid)); + let limit = + pallet_rate_limiting::Pallet::::resolved_limit(&target, &scope).unwrap_or_default(); + Ok(limit.saturated_into()) } #[precompile::public("setWeightsSetRateLimit(uint16,uint64)")] #[precompile::payable] fn set_weights_set_rate_limit( - _handle: &mut impl PrecompileHandle, - _netuid: u16, - _weights_set_rate_limit: u64, + handle: &mut impl PrecompileHandle, + netuid: u16, + weights_set_rate_limit: u64, ) -> EvmResult<()> { - // DEPRECATED. Subnet owner cannot set weight setting rate limits - Ok(()) + let call = pallet_rate_limiting::Call::::set_rate_limit { + target: RateLimitTarget::Group(rate_limiting::GROUP_WEIGHTS_SUBNET), + scope: Some(netuid.into()), + limit: RateLimitKind::Exact(weights_set_rate_limit.saturated_into()), + }; + + handle.try_dispatch_runtime_call::( + call, + RawOrigin::Signed(handle.caller_account_id::()), + ) } #[precompile::public("getAdjustmentAlpha(uint16)")] diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 925de2dc48..5257f47179 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -1029,7 +1029,7 @@ mod tests { }; use pallet_subtensor::{ AxonInfo, Call as SubtensorCall, HasMigrationRun, LastRateLimitedBlock, LastUpdate, - NetworksAdded, PrometheusInfo, RateLimitKey, TransactionKeyLastBlock, WeightsSetRateLimit, + NetworksAdded, PrometheusInfo, RateLimitKey, TransactionKeyLastBlock, WeightsVersionKeyRateLimit, utils::rate_limiting::TransactionType, }; use sp_core::{H160, ecdsa}; @@ -1496,7 +1496,7 @@ mod tests { // Ensure subnet exists so LastUpdate is imported. NetworksAdded::::insert(netuid, true); SubtensorModule::set_tempo(netuid, tempo); - WeightsSetRateLimit::::insert(netuid, weights_span); + legacy_storage::set_weights_set_rate_limit(netuid, weights_span); LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![now - 1]); let weights_call = RuntimeCall::SubtensorModule(SubtensorCall::set_weights { @@ -1514,7 +1514,7 @@ mod tests { .get(uid as usize) .copied() .unwrap_or_default(); - let limit = WeightsSetRateLimit::::get(netuid); + let limit = legacy_storage::get_weights_set_rate_limit(netuid); now.saturating_sub(last) >= limit }; parity_check( diff --git a/runtime/src/rate_limiting/legacy.rs b/runtime/src/rate_limiting/legacy.rs index 2de1cdf9d6..f204a4b811 100644 --- a/runtime/src/rate_limiting/legacy.rs +++ b/runtime/src/rate_limiting/legacy.rs @@ -38,6 +38,20 @@ pub mod storage { (items.into_iter().collect(), reads) } + pub fn get_weights_set_rate_limit(netuid: NetUid) -> u64 { + let mut key = storage_prefix(PALLET_PREFIX, b"WeightsSetRateLimit"); + key.extend(netuid.encode()); + io_storage::get(&key) + .and_then(|bytes| Decode::decode(&mut &bytes[..]).ok()) + .unwrap_or_else(defaults::weights_set_rate_limit) + } + + pub fn set_weights_set_rate_limit(netuid: NetUid, span: u64) { + let mut key = storage_prefix(PALLET_PREFIX, b"WeightsSetRateLimit"); + key.extend(netuid.encode()); + io_storage::set(&key, &span.encode()); + } + pub fn last_updates() -> (Vec<(NetUidStorageIndex, Vec)>, u64) { let items: Vec<_> = storage_key_iter::, Identity>( PALLET_PREFIX, From 70224b9bc57f882ee65f220723d97b4e0002089e Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 15 Jan 2026 16:28:47 +0100 Subject: [PATCH 70/95] Rename GROUP_WEIGHTS_SUBNET to GROUP_WEIGHTS_SET --- common/src/rate_limiting.rs | 2 +- pallets/admin-utils/src/lib.rs | 4 ++-- pallets/subtensor/src/subnets/uids.rs | 8 ++++---- pallets/subtensor/src/utils/misc.rs | 2 +- precompiles/src/subnet.rs | 4 ++-- runtime/src/migrations/rate_limiting.rs | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/common/src/rate_limiting.rs b/common/src/rate_limiting.rs index 20c9e2d629..c8c563878d 100644 --- a/common/src/rate_limiting.rs +++ b/common/src/rate_limiting.rs @@ -30,7 +30,7 @@ pub const GROUP_SERVE: GroupId = 0; /// Group id for delegate-take related calls. pub const GROUP_DELEGATE_TAKE: GroupId = 1; /// Group id for subnet weight-setting calls. -pub const GROUP_WEIGHTS_SUBNET: GroupId = 2; +pub const GROUP_WEIGHTS_SET: GroupId = 2; /// Group id for network registration calls. pub const GROUP_REGISTER_NETWORK: GroupId = 3; /// Group id for owner hyperparameter calls. diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 9deb74fe6b..bbd82043ab 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -343,13 +343,13 @@ pub mod pallet { /// The extrinsic will call the Subtensor pallet to set the weights set rate limit. /// /// Deprecated: weights set rate limit is now configured via `pallet-rate-limiting` on the - /// weights set group target (`GROUP_WEIGHTS_SUBNET`) with `scope = Some(netuid)`. + /// weights set group target (`GROUP_WEIGHTS_SET`) with `scope = Some(netuid)`. #[pallet::call_index(7)] #[pallet::weight(Weight::from_parts(15_060_000, 0) .saturating_add(::DbWeight::get().reads(1_u64)) .saturating_add(::DbWeight::get().writes(1_u64)))] #[deprecated( - note = "deprecated: configure via pallet-rate-limiting::set_rate_limit(target=Group(GROUP_WEIGHTS_SUBNET), scope=Some(netuid), ...)" + note = "deprecated: configure via pallet-rate-limiting::set_rate_limit(target=Group(GROUP_WEIGHTS_SET), scope=Some(netuid), ...)" )] pub fn sudo_set_weights_set_rate_limit( _origin: OriginFor, diff --git a/pallets/subtensor/src/subnets/uids.rs b/pallets/subtensor/src/subnets/uids.rs index bae22e476e..dad434a718 100644 --- a/pallets/subtensor/src/subnets/uids.rs +++ b/pallets/subtensor/src/subnets/uids.rs @@ -122,7 +122,7 @@ impl Pallet { Self::set_last_update_for_uid(netuid_index, next_uid, block_number); let usage = Self::weights_rl_usage_key(netuid, mecid, next_uid); T::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, + rate_limiting::GROUP_WEIGHTS_SET, Some(usage), Some(block_number.saturated_into()), ); @@ -289,7 +289,7 @@ impl Pallet { trimmed_lastupdate.push(lastupdate.get(*uid).cloned().unwrap_or_default()); let usage = Self::weights_rl_usage_key(netuid, mecid, *uid as u16); trimmed_last_seen.push(T::RateLimiting::last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, + rate_limiting::GROUP_WEIGHTS_SET, Some(usage), )); } @@ -300,7 +300,7 @@ impl Pallet { for uid in 0..current_n { let usage = Self::weights_rl_usage_key(netuid, mecid, uid); T::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, + rate_limiting::GROUP_WEIGHTS_SET, Some(usage), None, ); @@ -311,7 +311,7 @@ impl Pallet { }; let usage = Self::weights_rl_usage_key(netuid, mecid, new_uid as u16); T::RateLimiting::set_last_seen( - rate_limiting::GROUP_WEIGHTS_SUBNET, + rate_limiting::GROUP_WEIGHTS_SET, Some(usage), Some(block), ); diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index f103a50516..f3d4d96b01 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -514,7 +514,7 @@ impl Pallet { } pub fn get_weights_set_rate_limit(netuid: NetUid) -> u64 { - T::RateLimiting::rate_limit(rate_limiting::GROUP_WEIGHTS_SUBNET, Some(netuid)) + T::RateLimiting::rate_limit(rate_limiting::GROUP_WEIGHTS_SET, Some(netuid)) .unwrap_or_default() .saturated_into() } diff --git a/precompiles/src/subnet.rs b/precompiles/src/subnet.rs index d6273fdd0f..7c35adf333 100644 --- a/precompiles/src/subnet.rs +++ b/precompiles/src/subnet.rs @@ -272,7 +272,7 @@ where #[precompile::public("getWeightsSetRateLimit(uint16)")] #[precompile::view] fn get_weights_set_rate_limit(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { - let target = RateLimitTarget::Group(rate_limiting::GROUP_WEIGHTS_SUBNET); + let target = RateLimitTarget::Group(rate_limiting::GROUP_WEIGHTS_SET); let scope = Some(NetUid::from(netuid)); let limit = pallet_rate_limiting::Pallet::::resolved_limit(&target, &scope).unwrap_or_default(); @@ -287,7 +287,7 @@ where weights_set_rate_limit: u64, ) -> EvmResult<()> { let call = pallet_rate_limiting::Call::::set_rate_limit { - target: RateLimitTarget::Group(rate_limiting::GROUP_WEIGHTS_SUBNET), + target: RateLimitTarget::Group(rate_limiting::GROUP_WEIGHTS_SET), scope: Some(netuid.into()), limit: RateLimitKind::Exact(weights_set_rate_limit.saturated_into()), }; diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 5257f47179..ad1858f235 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -20,7 +20,7 @@ use subtensor_runtime_common::{ NetUid, rate_limiting::{ GROUP_DELEGATE_TAKE, GROUP_OWNER_HPARAMS, GROUP_REGISTER_NETWORK, GROUP_SERVE, - GROUP_STAKING_OPS, GROUP_SWAP_KEYS, GROUP_WEIGHTS_SUBNET, GroupId, RateLimitUsageKey, + GROUP_STAKING_OPS, GROUP_SWAP_KEYS, GROUP_WEIGHTS_SET, GroupId, RateLimitUsageKey, ServingEndpoint, }, }; @@ -416,7 +416,7 @@ fn build_delegate_take(groups: &mut Vec, commits: &mut Vec) fn build_weights(groups: &mut Vec, commits: &mut Vec) -> u64 { let mut reads: u64 = 0; groups.push(GroupConfig { - id: GROUP_WEIGHTS_SUBNET, + id: GROUP_WEIGHTS_SET, name: b"weights".to_vec(), sharing: GroupSharing::ConfigAndUsage, members: vec![ @@ -442,7 +442,7 @@ fn build_weights(groups: &mut Vec, commits: &mut Vec) -> u6 reads = reads.saturating_add(1); push_limit_commit_if_non_zero( commits, - RateLimitTarget::Group(GROUP_WEIGHTS_SUBNET), + RateLimitTarget::Group(GROUP_WEIGHTS_SET), weights_limits .get(&netuid) .copied() @@ -475,7 +475,7 @@ fn build_weights(groups: &mut Vec, commits: &mut Vec) -> u6 } }; commits.push(Commit { - target: RateLimitTarget::Group(GROUP_WEIGHTS_SUBNET), + target: RateLimitTarget::Group(GROUP_WEIGHTS_SET), kind: CommitKind::LastSeen(MigratedLastSeen { block, usage: Some(usage), From e0df6dc9f900ad0cf628d23373d67d0f1594ac7f Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 15 Jan 2026 16:30:30 +0100 Subject: [PATCH 71/95] Fix warnings in runtime tests --- runtime/src/rate_limiting/legacy.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/src/rate_limiting/legacy.rs b/runtime/src/rate_limiting/legacy.rs index f204a4b811..dfdbdc41b7 100644 --- a/runtime/src/rate_limiting/legacy.rs +++ b/runtime/src/rate_limiting/legacy.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use codec::{Decode, Encode}; use frame_support::{Identity, migration::storage_key_iter}; use runtime_common::prod_or_fast; From 7f5ea77215c9ee98422c2a7c0ad180a073a2ddad Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 15 Jan 2026 17:25:49 +0100 Subject: [PATCH 72/95] Add integration tests for weights set rate-limiting --- runtime/src/rate_limiting/mod.rs | 2 +- runtime/tests/rate_limiting.rs | 248 ++++++++++++++++++++++++++++++- 2 files changed, 242 insertions(+), 8 deletions(-) diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 5728895ca1..7f5dedeea2 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -33,7 +33,7 @@ use subtensor_runtime_common::{ use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; -pub(crate) mod legacy; +pub mod legacy; /// Authorization rules for configuring rate limits via `pallet-rate-limiting::set_rate_limit`. /// diff --git a/runtime/tests/rate_limiting.rs b/runtime/tests/rate_limiting.rs index 68b73ead82..908dff3550 100644 --- a/runtime/tests/rate_limiting.rs +++ b/runtime/tests/rate_limiting.rs @@ -1,21 +1,21 @@ -#![cfg(feature = "integration-tests")] #![allow(clippy::unwrap_used)] -use codec::Encode; +use codec::{Compact, Encode}; use frame_support::assert_ok; use node_subtensor_runtime::{ Executive, Runtime, RuntimeCall, SignedPayload, SubtensorInitialTxDelegateTakeRateLimit, - System, TransactionExtensions, UncheckedExtrinsic, check_nonce, sudo_wrapper, - transaction_payment_wrapper, + System, TransactionExtensions, UncheckedExtrinsic, check_nonce, + rate_limiting::legacy::storage as legacy_storage, sudo_wrapper, transaction_payment_wrapper, }; -use sp_core::{Pair, sr25519}; +use pallet_subtensor::MAX_CRV3_COMMIT_SIZE_BYTES; +use sp_core::{ConstU32, H256, Pair, sr25519}; use sp_runtime::{ - MultiSignature, + BoundedVec, MultiSignature, generic::Era, traits::SaturatedConversion, transaction_validity::{InvalidTransaction, TransactionValidityError}, }; -use subtensor_runtime_common::{AccountId, NetUid}; +use subtensor_runtime_common::{AccountId, MechId, NetUid}; use common::ExtBuilder; @@ -66,6 +66,18 @@ fn signed_extrinsic(call: RuntimeCall, pair: &sr25519::Pair, nonce: u32) -> Unch UncheckedExtrinsic::new_signed(call, address, signature, extra) } +fn setup_weights_network(netuid: NetUid, hotkey: &AccountId, block: u64, mechanisms: u8) { + pallet_subtensor::Pallet::::init_new_network(netuid, 1); + if mechanisms > 1 { + pallet_subtensor::MechanismCountCurrent::::insert( + netuid, + MechId::from(mechanisms), + ); + } + System::set_block_number(block.saturated_into()); + pallet_subtensor::Pallet::::append_neuron(netuid, hotkey, block); +} + #[test] fn register_network_is_rate_limited_after_migration() { let coldkey_pair = sr25519::Pair::from_seed(&[1u8; 32]); @@ -356,3 +368,225 @@ fn delegate_take_decrease_blocks_immediate_increase_after_migration() { assert_extrinsic_ok(&coldkey, &coldkey_pair, increase); }); } + +#[test] +fn weights_set_is_rate_limited_after_migration() { + let hotkey_pair = sr25519::Pair::from_seed(&[12u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let netuid = NetUid::from(1u16); + let span = 3u64; + let registration_block = 1u64; + + ExtBuilder::default() + .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) + .build() + .execute_with(|| { + setup_weights_network(netuid, &hotkey, registration_block, 1); + legacy_storage::set_weights_set_rate_limit(netuid, span); + + Executive::execute_on_runtime_upgrade(); + + pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, false); + + let version_key = pallet_subtensor::WeightsVersionKey::::get(netuid); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights { + netuid, + dests: vec![0], + weights: vec![u16::MAX], + version_key, + }); + + System::set_block_number(registration_block.saturated_into()); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, call.clone()); + + System::set_block_number((registration_block + span - 1).saturated_into()); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, call.clone()); + + System::set_block_number((registration_block + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, call.clone()); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, call.clone()); + + System::set_block_number((registration_block + span + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, call); + }); +} + +#[test] +fn commit_weights_shares_rate_limit_with_set_weights() { + let hotkey_pair = sr25519::Pair::from_seed(&[13u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let netuid = NetUid::from(2u16); + let span = 4u64; + let registration_block = 1u64; + let commit_hash = H256::from_low_u64_be(42); + + ExtBuilder::default() + .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) + .build() + .execute_with(|| { + setup_weights_network(netuid, &hotkey, registration_block, 1); + legacy_storage::set_weights_set_rate_limit(netuid, span); + + Executive::execute_on_runtime_upgrade(); + + let commit_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::commit_weights { + netuid, + commit_hash, + }); + + System::set_block_number((registration_block + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_call); + + pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, false); + + let version_key = pallet_subtensor::WeightsVersionKey::::get(netuid); + let set_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights { + netuid, + dests: vec![0], + weights: vec![u16::MAX], + version_key, + }); + + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, set_call.clone()); + + System::set_block_number((registration_block + span + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, set_call); + }); +} + +#[test] +fn commit_timelocked_weights_is_rate_limited_after_migration() { + let hotkey_pair = sr25519::Pair::from_seed(&[14u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let netuid = NetUid::from(3u16); + let span = 4u64; + let registration_block = 1u64; + let commit = BoundedVec::try_from(vec![1u8; 16]).expect("commit payload within limit"); + let reveal_round = 10u64; + + ExtBuilder::default() + .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) + .build() + .execute_with(|| { + setup_weights_network(netuid, &hotkey, registration_block, 1); + legacy_storage::set_weights_set_rate_limit(netuid, span); + + Executive::execute_on_runtime_upgrade(); + + let commit_reveal_version = + pallet_subtensor::Pallet::::get_commit_reveal_weights_version(); + let commit_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::commit_timelocked_weights { + netuid, + commit: commit.clone(), + reveal_round, + commit_reveal_version, + }); + + System::set_block_number((registration_block + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_call.clone()); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, commit_call.clone()); + + System::set_block_number((registration_block + span + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_call); + }); +} + +#[test] +fn commit_crv3_mechanism_weights_are_rate_limited_per_mechanism() { + let hotkey_pair = sr25519::Pair::from_seed(&[15u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let netuid = NetUid::from(4u16); + let span = 4u64; + let registration_block = 1u64; + let commit = BoundedVec::try_from(vec![1u8; 16]).expect("commit payload within limit"); + let reveal_round = 10u64; + let mecid_a = MechId::from(0u8); + let mecid_b = MechId::from(1u8); + + ExtBuilder::default() + .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) + .build() + .execute_with(|| { + setup_weights_network(netuid, &hotkey, registration_block, 2); + legacy_storage::set_weights_set_rate_limit(netuid, span); + + Executive::execute_on_runtime_upgrade(); + + let commit_a = RuntimeCall::SubtensorModule( + pallet_subtensor::Call::commit_crv3_mechanism_weights { + netuid, + mecid: mecid_a, + commit: commit.clone(), + reveal_round, + }, + ); + let commit_b = RuntimeCall::SubtensorModule( + pallet_subtensor::Call::commit_crv3_mechanism_weights { + netuid, + mecid: mecid_b, + commit: commit.clone(), + reveal_round, + }, + ); + + System::set_block_number((registration_block + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_a.clone()); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, commit_a); + assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_b); + }); +} + +#[test] +fn batch_set_weights_is_rate_limited_if_any_scope_is_within_span() { + let hotkey_pair = sr25519::Pair::from_seed(&[16u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let netuid_a = NetUid::from(5u16); + let netuid_b = NetUid::from(6u16); + let span = 3u64; + let registration_block = 1u64; + + ExtBuilder::default() + .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) + .build() + .execute_with(|| { + setup_weights_network(netuid_a, &hotkey, registration_block, 1); + setup_weights_network(netuid_b, &hotkey, registration_block, 1); + legacy_storage::set_weights_set_rate_limit(netuid_a, span); + legacy_storage::set_weights_set_rate_limit(netuid_b, span); + + Executive::execute_on_runtime_upgrade(); + + pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid_a, false); + pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid_b, false); + + let version_key_a = pallet_subtensor::WeightsVersionKey::::get(netuid_a); + let version_key_b = pallet_subtensor::WeightsVersionKey::::get(netuid_b); + + let set_call_a = RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights { + netuid: netuid_a, + dests: vec![0], + weights: vec![u16::MAX], + version_key: version_key_a, + }); + + System::set_block_number((registration_block + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, set_call_a); + + let batch_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::batch_set_weights { + netuids: vec![Compact(netuid_a), Compact(netuid_b)], + weights: vec![ + vec![(Compact(0u16), Compact(1u16))], + vec![(Compact(0u16), Compact(1u16))], + ], + version_keys: vec![Compact(version_key_a), Compact(version_key_b)], + }); + + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, batch_call.clone()); + + System::set_block_number((registration_block + span + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, batch_call); + }); +} From f72113452d2acf16fd6c04d009b90be5e341373e Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 15 Jan 2026 18:07:39 +0100 Subject: [PATCH 73/95] Remove integration-tests feature --- runtime/Cargo.toml | 1 - runtime/tests/rate_limiting.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 70e06f0468..af62850e09 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -282,7 +282,6 @@ std = [ "ethereum/std", "pallet-shield/std", ] -integration-tests = [] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", diff --git a/runtime/tests/rate_limiting.rs b/runtime/tests/rate_limiting.rs index 908dff3550..e0a88fcded 100644 --- a/runtime/tests/rate_limiting.rs +++ b/runtime/tests/rate_limiting.rs @@ -7,8 +7,7 @@ use node_subtensor_runtime::{ System, TransactionExtensions, UncheckedExtrinsic, check_nonce, rate_limiting::legacy::storage as legacy_storage, sudo_wrapper, transaction_payment_wrapper, }; -use pallet_subtensor::MAX_CRV3_COMMIT_SIZE_BYTES; -use sp_core::{ConstU32, H256, Pair, sr25519}; +use sp_core::{H256, Pair, sr25519}; use sp_runtime::{ BoundedVec, MultiSignature, generic::Era, From bf5542097e2f0261bfb8a5cbcfd86d469a94d13d Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 16 Jan 2026 15:01:00 +0100 Subject: [PATCH 74/95] Configure genesis config for new groups in rate-limiting --- node/src/chain_spec/devnet.rs | 18 ++++++++++++++++-- node/src/chain_spec/localnet.rs | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/node/src/chain_spec/devnet.rs b/node/src/chain_spec/devnet.rs index ac3afc7c94..f8b769d0d9 100644 --- a/node/src/chain_spec/devnet.rs +++ b/node/src/chain_spec/devnet.rs @@ -2,7 +2,11 @@ #![allow(clippy::unwrap_used)] use super::*; -use subtensor_runtime_common::rate_limiting::GROUP_SERVE; +use node_subtensor_runtime::rate_limiting::legacy::defaults as rate_limit_defaults; +use subtensor_runtime_common::{ + NetUid, + rate_limiting::{GROUP_DELEGATE_TAKE, GROUP_REGISTER_NETWORK, GROUP_SERVE, GROUP_WEIGHTS_SET}, +}; pub fn devnet_config() -> Result { let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; @@ -92,9 +96,19 @@ fn devnet_genesis( }, "rateLimiting": { "defaultLimit": 0, - "limits": [], + "limits": vec![ + (serde_json::json!({ "Group": GROUP_SERVE }), Some(NetUid::ROOT), serde_json::json!({ "Exact": rate_limit_defaults::serving_rate_limit() })), + (serde_json::json!({ "Group": GROUP_SERVE }), Some(NetUid::from(1u16)), serde_json::json!({ "Exact": rate_limit_defaults::serving_rate_limit() })), + (serde_json::json!({ "Group": GROUP_REGISTER_NETWORK }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::network_rate_limit() })), + (serde_json::json!({ "Group": GROUP_DELEGATE_TAKE }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::tx_delegate_take_rate_limit() })), + (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::ROOT), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), + (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::from(1u16)), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), + ], "groups": vec![ (GROUP_SERVE, b"serving".to_vec(), "ConfigAndUsage"), + (GROUP_REGISTER_NETWORK, b"register-network".to_vec(), "ConfigAndUsage"), + (GROUP_DELEGATE_TAKE, b"delegate-take".to_vec(), "ConfigAndUsage"), + (GROUP_WEIGHTS_SET, b"weights".to_vec(), "ConfigAndUsage"), ], "limitSettingRules": vec![ (serde_json::json!({ "Group": GROUP_SERVE }), "RootOrSubnetOwnerAdminWindow"), diff --git a/node/src/chain_spec/localnet.rs b/node/src/chain_spec/localnet.rs index 269f5f661b..6d7ec87b5d 100644 --- a/node/src/chain_spec/localnet.rs +++ b/node/src/chain_spec/localnet.rs @@ -2,7 +2,11 @@ #![allow(clippy::unwrap_used)] use super::*; -use subtensor_runtime_common::rate_limiting::GROUP_SERVE; +use node_subtensor_runtime::rate_limiting::legacy::defaults as rate_limit_defaults; +use subtensor_runtime_common::{ + NetUid, + rate_limiting::{GROUP_DELEGATE_TAKE, GROUP_REGISTER_NETWORK, GROUP_SERVE, GROUP_WEIGHTS_SET}, +}; pub fn localnet_config(single_authority: bool) -> Result { let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; @@ -127,9 +131,19 @@ fn localnet_genesis( }, "rateLimiting": { "defaultLimit": 0, - "limits": [], + "limits": vec![ + (serde_json::json!({ "Group": GROUP_SERVE }), Some(NetUid::ROOT), serde_json::json!({ "Exact": rate_limit_defaults::serving_rate_limit() })), + (serde_json::json!({ "Group": GROUP_SERVE }), Some(NetUid::from(1u16)), serde_json::json!({ "Exact": rate_limit_defaults::serving_rate_limit() })), + (serde_json::json!({ "Group": GROUP_REGISTER_NETWORK }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::network_rate_limit() })), + (serde_json::json!({ "Group": GROUP_DELEGATE_TAKE }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::tx_delegate_take_rate_limit() })), + (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::ROOT), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), + (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::from(1u16)), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), + ], "groups": vec![ (GROUP_SERVE, b"serving".to_vec(), "ConfigAndUsage"), + (GROUP_REGISTER_NETWORK, b"register-network".to_vec(), "ConfigAndUsage"), + (GROUP_DELEGATE_TAKE, b"delegate-take".to_vec(), "ConfigAndUsage"), + (GROUP_WEIGHTS_SET, b"weights".to_vec(), "ConfigAndUsage"), ], "limitSettingRules": vec![ (serde_json::json!({ "Group": GROUP_SERVE }), "RootOrSubnetOwnerAdminWindow"), From 96f96bc7e5a008a1652be1b0a4a2a6c0cf181338 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 16 Jan 2026 16:03:41 +0100 Subject: [PATCH 75/95] Update contract tests --- contract-tests/src/subtensor.ts | 30 +++++++-- .../subnet.precompile.hyperparameter.test.ts | 67 +++---------------- 2 files changed, 31 insertions(+), 66 deletions(-) diff --git a/contract-tests/src/subtensor.ts b/contract-tests/src/subtensor.ts index 8ab0d45632..47fee61cb5 100644 --- a/contract-tests/src/subtensor.ts +++ b/contract-tests/src/subtensor.ts @@ -15,9 +15,9 @@ export async function addNewSubnetwork(api: TypedApi, hotkey: Key const registerNetworkGroupId = 3; // GROUP_REGISTER_NETWORK constant const target = { Group: registerNetworkGroupId } as const; const limits = await api.query.RateLimiting.Limits.getValue(target as any) as any; - const rateLimit = limits?.tag === "Global" && limits?.value?.tag === "Exact" - ? BigInt(limits.value.value) - : BigInt(0); + assert.ok(limits?.tag === "Global"); + assert.ok(limits.value?.tag === "Exact"); + const rateLimit = BigInt(limits.value.value); if (rateLimit !== BigInt(0)) { const internalCall = api.tx.RateLimiting.set_rate_limit({ target: target as any, @@ -75,17 +75,33 @@ export async function setCommitRevealWeightsEnabled(api: TypedApi } export async function setWeightsSetRateLimit(api: TypedApi, netuid: number, rateLimit: bigint) { - const value = await api.query.SubtensorModule.WeightsSetRateLimit.getValue(netuid) - if (value === rateLimit) { + const weightsSetGroupId = 2; // GROUP_WEIGHTS_SET constant + const target = { Group: weightsSetGroupId } as const; + const limits = await api.query.RateLimiting.Limits.getValue(target as any) as any; + assert.ok(limits?.tag === "Scoped"); + const entries = Array.from(limits.value as any); + const entry = entries.find((item: any) => Number(item[0]) === netuid); + const currentLimit = entry ? BigInt(entry[1].value) : BigInt(0); + if (currentLimit === rateLimit) { return; } const alice = getAliceSigner() - const internalCall = api.tx.AdminUtils.sudo_set_weights_set_rate_limit({ netuid: netuid, weights_set_rate_limit: rateLimit }) + const internalCall = api.tx.RateLimiting.set_rate_limit({ + target: target as any, + scope: netuid, + limit: { Exact: rateLimit }, + }) const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) await waitForTransactionWithRetry(api, tx, alice) - assert.equal(rateLimit, await api.query.SubtensorModule.WeightsSetRateLimit.getValue(netuid)) + const updated = await api.query.RateLimiting.Limits.getValue(target as any) as any; + assert.ok(updated?.tag === "Scoped"); + const updatedEntry = Array.from(updated.value as any).find( + (item: any) => Number(item[0]) === netuid, + ); + assert.ok(updatedEntry); + assert.equal(rateLimit, BigInt(updatedEntry[1].value)) } // tempo is u16 in rust, but we just number in js. so value should be less than u16::Max diff --git a/contract-tests/test/subnet.precompile.hyperparameter.test.ts b/contract-tests/test/subnet.precompile.hyperparameter.test.ts index 73ad6fe93a..e6e6a41b6c 100644 --- a/contract-tests/test/subnet.precompile.hyperparameter.test.ts +++ b/contract-tests/test/subnet.precompile.hyperparameter.test.ts @@ -98,65 +98,14 @@ describe("Test the Subnet precompile contract", () => { const tx = await contract.setServingRateLimit(netuid, newValue); await tx.wait(); - const unwrapEnum = (value: unknown): { tag: string; value: unknown } | null => { - if (!value || typeof value !== "object") { - return null; - } - if ("type" in value && "value" in value) { - return { tag: (value as { type: string }).type, value: (value as { value: unknown }).value }; - } - const keys = Object.keys(value); - if (keys.length === 1) { - const key = keys[0]; - return { tag: key, value: (value as Record)[key] }; - } - return null; - }; - - const toNumber = (value: unknown): number | undefined => { - if (typeof value === "number") { - return value; - } - if (typeof value === "bigint") { - return Number(value); - } - if (typeof value === "string") { - const parsed = Number(value); - return Number.isNaN(parsed) ? undefined : parsed; - } - return undefined; - }; - - const extractRateLimit = (limits: unknown, scope: number): number | undefined => { - const decoded = unwrapEnum(limits); - if (!decoded) { - return undefined; - } - if (decoded.tag === "Global") { - const kind = unwrapEnum(decoded.value); - return kind?.tag === "Exact" ? toNumber(kind.value) : undefined; - } - if (decoded.tag !== "Scoped") { - return undefined; - } - const scopedEntries = decoded.value instanceof Map - ? Array.from(decoded.value.entries()) - : Array.isArray(decoded.value) - ? decoded.value - : []; - const entry = scopedEntries.find( - (item: unknown) => - Array.isArray(item) && item.length === 2 && toNumber(item[0]) === scope, - ) as [unknown, unknown] | undefined; - if (!entry) { - return undefined; - } - const kind = unwrapEnum(entry[1]); - return kind?.tag === "Exact" ? toNumber(kind.value) : undefined; - }; - - const limits = await api.query.RateLimiting.Limits.getValue({ Group: 0 } as any); - const onchainValue = extractRateLimit(limits, netuid); + const limits = await api.query.RateLimiting.Limits.getValue({ Group: 0 } as any) as any; + assert.ok(limits?.tag === "Scoped"); + const entry = Array.from(limits.value as any).find( + (item: any) => Number(item[0]) === netuid, + ); + assert.ok(entry); + assert.ok(entry[1]?.tag === "Exact"); + const onchainValue = Number(entry[1].value); let valueFromContract = Number( From 8e411e9ab6df717a12ae7da43cca3239d555f598 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 19 Jan 2026 15:29:25 +0100 Subject: [PATCH 76/95] Remove get_tx_rate_limit --- pallets/subtensor/src/tests/swap_hotkey.rs | 4 ++-- pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs | 4 ++-- pallets/subtensor/src/utils/misc.rs | 4 ---- pallets/subtensor/src/utils/rate_limiting.rs | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pallets/subtensor/src/tests/swap_hotkey.rs b/pallets/subtensor/src/tests/swap_hotkey.rs index 99a615ec00..05114c4418 100644 --- a/pallets/subtensor/src/tests/swap_hotkey.rs +++ b/pallets/subtensor/src/tests/swap_hotkey.rs @@ -725,13 +725,13 @@ fn test_swap_hotkey_tx_rate_limit_exceeded() { let tx_rate_limit = 1; // Get the current transaction rate limit - let current_tx_rate_limit = SubtensorModule::get_tx_rate_limit(); + let current_tx_rate_limit = TxRateLimit::::get(); log::info!("current_tx_rate_limit: {current_tx_rate_limit:?}"); // Set the transaction rate limit SubtensorModule::set_tx_rate_limit(tx_rate_limit); // assert the rate limit is set to 1000 blocks - assert_eq!(SubtensorModule::get_tx_rate_limit(), tx_rate_limit); + assert_eq!(TxRateLimit::::get(), tx_rate_limit); // Setup initial state add_network(netuid, tempo, 0); diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index dc2af38cb6..80a3aac349 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -766,13 +766,13 @@ fn test_swap_hotkey_tx_rate_limit_exceeded() { let tx_rate_limit = 1; // Get the current transaction rate limit - let current_tx_rate_limit = SubtensorModule::get_tx_rate_limit(); + let current_tx_rate_limit = TxRateLimit::::get(); log::info!("current_tx_rate_limit: {current_tx_rate_limit:?}"); // Set the transaction rate limit SubtensorModule::set_tx_rate_limit(tx_rate_limit); // assert the rate limit is set to 1000 blocks - assert_eq!(SubtensorModule::get_tx_rate_limit(), tx_rate_limit); + assert_eq!(TxRateLimit::::get(), tx_rate_limit); // Setup initial state add_network(netuid, tempo, 0); diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index f3d4d96b01..94e2e09e7e 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -415,10 +415,6 @@ impl Pallet { // ======================== // Configure tx rate limiting - pub fn get_tx_rate_limit() -> u64 { - TxRateLimit::::get() - } - pub fn set_tx_rate_limit(tx_rate_limit: u64) { TxRateLimit::::put(tx_rate_limit); Self::deposit_event(Event::TxRateLimitSet(tx_rate_limit)); diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index 18d0728bf8..fb11bd823a 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -242,7 +242,7 @@ impl Pallet { ); } pub fn exceeds_tx_rate_limit(prev_tx_block: u64, current_block: u64) -> bool { - let rate_limit: u64 = Self::get_tx_rate_limit(); + let rate_limit: u64 = TxRateLimit::::get(); if rate_limit == 0 || prev_tx_block == 0 { return false; } From 1fb07f91558f512e000f5a1c85d361518b3fa718 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 19 Jan 2026 15:54:16 +0100 Subject: [PATCH 77/95] Remove staking-ops related tests (will be moved into the integration tests) --- pallets/subtensor/src/tests/move_stake.rs | 143 ---------------------- runtime/src/migrations/rate_limiting.rs | 2 +- 2 files changed, 1 insertion(+), 144 deletions(-) diff --git a/pallets/subtensor/src/tests/move_stake.rs b/pallets/subtensor/src/tests/move_stake.rs index dfd9927da4..3a7756f809 100644 --- a/pallets/subtensor/src/tests/move_stake.rs +++ b/pallets/subtensor/src/tests/move_stake.rs @@ -1783,146 +1783,3 @@ fn test_move_stake_specific_stake_into_subnet_fail() { ); }); } - -#[test] -fn test_transfer_stake_rate_limited() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let origin_coldkey = U256::from(1); - let destination_coldkey = U256::from(2); - let hotkey = U256::from(3); - let stake_amount = DefaultMinStake::::get().to_u64() * 10; - - SubtensorModule::create_account_if_non_existent(&origin_coldkey, &hotkey); - SubtensorModule::create_account_if_non_existent(&destination_coldkey, &hotkey); - SubtensorModule::stake_into_subnet( - &hotkey, - &origin_coldkey, - netuid, - stake_amount.into(), - ::SwapInterface::max_price(), - true, - false, - ) - .unwrap(); - let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &origin_coldkey, - netuid, - ); - - assert_err!( - SubtensorModule::do_transfer_stake( - RuntimeOrigin::signed(origin_coldkey), - destination_coldkey, - hotkey, - netuid, - netuid, - alpha - ), - Error::::StakingOperationRateLimitExceeded - ); - }); -} - -#[test] -fn test_transfer_stake_doesnt_limit_destination_coldkey() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let netuid2 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let origin_coldkey = U256::from(1); - let destination_coldkey = U256::from(2); - let hotkey = U256::from(3); - let stake_amount = DefaultMinStake::::get().to_u64() * 10; - - SubtensorModule::create_account_if_non_existent(&origin_coldkey, &hotkey); - SubtensorModule::create_account_if_non_existent(&destination_coldkey, &hotkey); - SubtensorModule::stake_into_subnet( - &hotkey, - &origin_coldkey, - netuid, - stake_amount.into(), - ::SwapInterface::max_price(), - false, - false, - ) - .unwrap(); - let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &origin_coldkey, - netuid, - ); - - assert_ok!(SubtensorModule::do_transfer_stake( - RuntimeOrigin::signed(origin_coldkey), - destination_coldkey, - hotkey, - netuid, - netuid2, - alpha - ),); - - assert!(!StakingOperationRateLimiter::::contains_key(( - hotkey, - destination_coldkey, - netuid2 - ))); - }); -} - -#[test] -fn test_swap_stake_limits_destination_netuid() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let netuid2 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let origin_coldkey = U256::from(1); - let hotkey = U256::from(3); - let stake_amount = DefaultMinStake::::get().to_u64() * 10; - - SubtensorModule::create_account_if_non_existent(&origin_coldkey, &hotkey); - SubtensorModule::stake_into_subnet( - &hotkey, - &origin_coldkey, - netuid, - stake_amount.into(), - ::SwapInterface::max_price(), - false, - false, - ) - .unwrap(); - let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &origin_coldkey, - netuid, - ); - - assert_ok!(SubtensorModule::do_swap_stake( - RuntimeOrigin::signed(origin_coldkey), - hotkey, - netuid, - netuid2, - alpha - ),); - - assert!(!StakingOperationRateLimiter::::contains_key(( - hotkey, - origin_coldkey, - netuid - ))); - - assert!(StakingOperationRateLimiter::::contains_key(( - hotkey, - origin_coldkey, - netuid2 - ))); - }); -} diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index ad1858f235..5843904d26 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -562,7 +562,7 @@ fn build_owner_hparams(groups: &mut Vec, commits: &mut Vec) // Staking ops group (config + usage shared, all ops 1 block). // usage: coldkey+hotkey+netuid -// legacy sources: TxRateLimit (reset every block for staking ops), StakingOperationRateLimiter +// legacy sources: StakingOperationRateLimiter (reset every block for staking ops) fn build_staking_ops(groups: &mut Vec, commits: &mut Vec) -> u64 { groups.push(GroupConfig { id: GROUP_STAKING_OPS, From 6e3cdcfa75fe1c86b82f6950cdf096d799276fd6 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 19 Jan 2026 16:57:26 +0100 Subject: [PATCH 78/95] Fix staking-ops rate-limiting inconsistencies --- pallets/subtensor/src/staking/remove_stake.rs | 26 ++++++++++++++++++- runtime/src/rate_limiting/mod.rs | 8 ++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 423cb97493..d7d5c048a0 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -1,6 +1,10 @@ use super::*; +use rate_limiting_interface::RateLimitingInterface; use substrate_fixed::types::U96F32; -use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; +use subtensor_runtime_common::{ + AlphaCurrency, Currency, NetUid, TaoCurrency, + rate_limiting::{self, RateLimitUsageKey}, +}; use subtensor_swap_interface::{Order, SwapHandler}; impl Pallet { @@ -226,6 +230,7 @@ impl Pallet { // 3. Get all netuids. let netuids = Self::get_all_subnet_netuids(); log::debug!("All subnet netuids: {netuids:?}"); + let staking_ops_span = T::RateLimiting::rate_limit(rate_limiting::GROUP_STAKING_OPS, None); // 4. Iterate through all subnets and remove stake. let mut total_tao_unstaked = TaoCurrency::ZERO; @@ -235,6 +240,25 @@ impl Pallet { } // If not Root network. if !netuid.is_root() { + // Manually filter out rate-limited subnets. + if let Some(span) = staking_ops_span { + if !span.is_zero() { + let usage_key = RateLimitUsageKey::ColdkeyHotkeySubnet { + coldkey: coldkey.clone(), + hotkey: hotkey.clone(), + netuid, + }; + if let Some(last_seen) = T::RateLimiting::last_seen( + rate_limiting::GROUP_STAKING_OPS, + Some(usage_key), + ) { + let current = >::block_number(); + if current.saturating_sub(last_seen) < span { + continue; + } + } + } + } // Ensure that the hotkey has enough stake to withdraw. let alpha_unstaked = Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 7f5dedeea2..34c7583f55 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -141,6 +141,14 @@ impl RateLimitScopeResolver for } match inner { + SubtensorCall::move_stake { + origin_netuid, + destination_netuid, + .. + } if origin_netuid == destination_netuid => { + // Legacy: same-netuid moves enforced but did not record usage. + return BypassDecision::new(false, false); + } SubtensorCall::set_childkey_take { hotkey, netuid, From e05c6778a65dcbddcc5f6e128a0e381a5810052b Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 19 Jan 2026 18:07:19 +0100 Subject: [PATCH 79/95] Remove set_stake_operation_limit - remove test_stake_rate_limit test --- chain-extensions/src/mock.rs | 5 -- chain-extensions/src/tests.rs | 16 ---- pallets/subtensor/src/staking/add_stake.rs | 11 +-- pallets/subtensor/src/staking/move_stake.rs | 6 -- pallets/subtensor/src/staking/remove_stake.rs | 7 +- pallets/subtensor/src/staking/stake_utils.rs | 13 --- pallets/subtensor/src/tests/children.rs | 2 +- pallets/subtensor/src/tests/mock.rs | 5 -- pallets/subtensor/src/tests/move_stake.rs | 36 +-------- pallets/subtensor/src/tests/staking.rs | 81 +++---------------- pallets/subtensor/src/tests/subnet.rs | 4 - pallets/subtensor/src/tests/swap_coldkey.rs | 2 - pallets/transaction-fee/src/tests/mock.rs | 7 +- 13 files changed, 20 insertions(+), 175 deletions(-) diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 555934419f..6fb741f1dd 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -721,11 +721,6 @@ pub fn add_dynamic_network(hotkey: &U256, coldkey: &U256) -> NetUid { netuid } -#[allow(dead_code)] -pub(crate) fn remove_stake_rate_limit_for_tests(hotkey: &U256, coldkey: &U256, netuid: NetUid) { - StakingOperationRateLimiter::::remove((hotkey, coldkey, netuid)); -} - #[allow(dead_code)] pub(crate) fn setup_reserves(netuid: NetUid, tao: TaoCurrency, alpha: AlphaCurrency) { SubnetTAO::::set(netuid, tao); diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index bd6f46c8ab..41c81e8664 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -100,8 +100,6 @@ fn remove_stake_full_limit_success_with_limit_price() { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = Weight::from_parts(395_300_000, 0) .saturating_add(::DbWeight::get().reads(28)) .saturating_add(::DbWeight::get().writes(14)); @@ -166,8 +164,6 @@ fn swap_stake_limit_with_tight_price_returns_slippage_error() { stake_alpha, ); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid_a); - let alpha_origin_before = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &coldkey, netuid_a, @@ -239,8 +235,6 @@ fn remove_stake_limit_success_respects_price_limit() { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let alpha_before = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &coldkey, netuid, @@ -390,8 +384,6 @@ fn swap_stake_success_moves_between_subnets() { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid_a); - let alpha_origin_before = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &coldkey, netuid_a, @@ -467,8 +459,6 @@ fn transfer_stake_success_moves_between_coldkeys() { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &origin_coldkey, netuid); - let alpha_before = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, @@ -551,8 +541,6 @@ fn move_stake_success_moves_alpha_between_hotkeys() { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&origin_hotkey, &coldkey, netuid); - let alpha_before = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( &origin_hotkey, @@ -631,8 +619,6 @@ fn unstake_all_alpha_success_moves_stake_to_root() { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = Weight::from_parts(358_500_000, 0) .saturating_add(::DbWeight::get().reads(36)) .saturating_add(::DbWeight::get().writes(21)); @@ -940,8 +926,6 @@ fn unstake_all_success_unstakes_balance() { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = Weight::from_parts(28_830_000, 0) .saturating_add(::DbWeight::get().reads(6)) .saturating_add(::DbWeight::get().writes(0)); diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index ea33912bf1..d3470cc572 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -75,7 +75,6 @@ impl Pallet { netuid, tao_staked.saturating_to_num::().into(), T::SwapInterface::max_price(), - true, false, )?; @@ -165,15 +164,7 @@ impl Pallet { // 6. Swap the stake into alpha on the subnet and increase counters. // Emit the staking event. - Self::stake_into_subnet( - &hotkey, - &coldkey, - netuid, - tao_staked, - limit_price, - true, - false, - )?; + Self::stake_into_subnet(&hotkey, &coldkey, netuid, tao_staked, limit_price, false)?; // Ok and return. Ok(()) diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index a1d9b46d5b..5dd167c5d4 100644 --- a/pallets/subtensor/src/staking/move_stake.rs +++ b/pallets/subtensor/src/staking/move_stake.rs @@ -50,7 +50,6 @@ impl Pallet { None, None, false, - true, )?; // Log the event. @@ -141,7 +140,6 @@ impl Pallet { None, None, true, - false, )?; // 9. Emit an event for logging/monitoring. @@ -206,7 +204,6 @@ impl Pallet { None, None, false, - true, )?; // Emit an event for logging. @@ -274,7 +271,6 @@ impl Pallet { Some(limit_price), Some(allow_partial), false, - true, )?; // Emit an event for logging. @@ -306,7 +302,6 @@ impl Pallet { maybe_limit_price: Option, maybe_allow_partial: Option, check_transfer_toggle: bool, - set_limit: bool, ) -> Result { // Cap the alpha_amount at available Alpha because user might be paying transaxtion fees // in Alpha and their total is already reduced by now. @@ -379,7 +374,6 @@ impl Pallet { destination_netuid, tao_unstaked, T::SwapInterface::max_price(), - set_limit, drop_fee_destination, )?; } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index d7d5c048a0..779fa71958 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -1,5 +1,5 @@ -use super::*; use rate_limiting_interface::RateLimitingInterface; +use sp_runtime::Saturating; use substrate_fixed::types::U96F32; use subtensor_runtime_common::{ AlphaCurrency, Currency, NetUid, TaoCurrency, @@ -7,6 +7,8 @@ use subtensor_runtime_common::{ }; use subtensor_swap_interface::{Order, SwapHandler}; +use super::*; + impl Pallet { /// ---- The implementation for the extrinsic remove_stake: Removes stake from a hotkey account and adds it onto a coldkey. /// @@ -240,7 +242,7 @@ impl Pallet { } // If not Root network. if !netuid.is_root() { - // Manually filter out rate-limited subnets. + // Manually filter out rate-limited subnets. if let Some(span) = staking_ops_span { if !span.is_zero() { let usage_key = RateLimitUsageKey::ColdkeyHotkeySubnet { @@ -304,7 +306,6 @@ impl Pallet { NetUid::ROOT, total_tao_unstaked, T::SwapInterface::max_price(), - false, // no limit for Root subnet false, )?; diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 1aeeacc33c..8f8919ba50 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -763,7 +763,6 @@ impl Pallet { netuid: NetUid, tao: TaoCurrency, price_limit: TaoCurrency, - set_limit: bool, drop_fees: bool, ) -> Result { // Swap the tao to alpha. @@ -808,10 +807,6 @@ impl Pallet { LastColdkeyHotkeyStakeBlock::::insert(coldkey, hotkey, Self::get_current_block_as_u64()); - if set_limit { - Self::set_stake_operation_limit(hotkey, coldkey, netuid.into()); - } - // If this is a root-stake if netuid == NetUid::ROOT { // Adjust root claimed for this hotkey and coldkey. @@ -1258,14 +1253,6 @@ impl Pallet { } } - pub fn set_stake_operation_limit( - hotkey: &T::AccountId, - coldkey: &T::AccountId, - netuid: NetUid, - ) { - StakingOperationRateLimiter::::insert((hotkey, coldkey, netuid), true); - } - pub fn ensure_stake_operation_limit_not_exceeded( hotkey: &T::AccountId, coldkey: &T::AccountId, diff --git a/pallets/subtensor/src/tests/children.rs b/pallets/subtensor/src/tests/children.rs index 146c94516a..78202bd64b 100644 --- a/pallets/subtensor/src/tests/children.rs +++ b/pallets/subtensor/src/tests/children.rs @@ -2271,7 +2271,7 @@ fn test_do_remove_stake_clears_pending_childkeys() { assert!(pending_before.1 > 0); // Remove stake - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); + assert_ok!(SubtensorModule::do_remove_stake( RuntimeOrigin::signed(coldkey), hotkey, diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 0408b52c1e..ea2f355bc1 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -925,7 +925,6 @@ pub fn increase_stake_on_coldkey_hotkey_account( tao_staked, ::SwapInterface::max_price(), false, - false, ) .unwrap(); } @@ -945,10 +944,6 @@ pub fn increase_stake_on_hotkey_account(hotkey: &U256, increment: TaoCurrency, n ); } -pub(crate) fn remove_stake_rate_limit_for_tests(hotkey: &U256, coldkey: &U256, netuid: NetUid) { - StakingOperationRateLimiter::::remove((hotkey, coldkey, netuid)); -} - pub(crate) fn setup_reserves(netuid: NetUid, tao: TaoCurrency, alpha: AlphaCurrency) { SubnetTAO::::set(netuid, tao); SubnetAlphaIn::::set(netuid, alpha); diff --git a/pallets/subtensor/src/tests/move_stake.rs b/pallets/subtensor/src/tests/move_stake.rs index 3a7756f809..523adaab1e 100644 --- a/pallets/subtensor/src/tests/move_stake.rs +++ b/pallets/subtensor/src/tests/move_stake.rs @@ -35,7 +35,6 @@ fn test_do_move_success() { stake_amount, ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -112,7 +111,6 @@ fn test_do_move_different_subnets() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -180,7 +178,6 @@ fn test_do_move_nonexistent_subnet() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -284,7 +281,6 @@ fn test_do_move_nonexistent_destination_hotkey() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -349,7 +345,6 @@ fn test_do_move_partial_stake() { total_stake.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -418,7 +413,6 @@ fn test_do_move_multiple_times() { initial_stake.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = @@ -430,7 +424,7 @@ fn test_do_move_multiple_times() { let alpha1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey1, &coldkey, netuid, ); - remove_stake_rate_limit_for_tests(&hotkey1, &coldkey, netuid); + assert_ok!(SubtensorModule::do_move_stake( RuntimeOrigin::signed(coldkey), hotkey1, @@ -442,7 +436,7 @@ fn test_do_move_multiple_times() { let alpha2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey2, &coldkey, netuid, ); - remove_stake_rate_limit_for_tests(&hotkey2, &coldkey, netuid); + assert_ok!(SubtensorModule::do_move_stake( RuntimeOrigin::signed(coldkey), hotkey2, @@ -490,7 +484,6 @@ fn test_do_move_wrong_origin() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -557,7 +550,6 @@ fn test_do_move_same_hotkey_fails() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = @@ -608,7 +600,6 @@ fn test_do_move_event_emission() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -669,7 +660,6 @@ fn test_do_move_storage_updates() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -736,7 +726,6 @@ fn test_move_full_amount_same_netuid() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -804,7 +793,6 @@ fn test_do_move_max_values() { max_stake.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -871,7 +859,6 @@ fn test_moving_too_little_unstakes() { (amount.to_u64() + fee * 2).into() )); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); assert_err!( SubtensorModule::move_stake( RuntimeOrigin::signed(coldkey_account_id), @@ -910,7 +897,6 @@ fn test_do_transfer_success() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1019,7 +1005,6 @@ fn test_do_transfer_insufficient_stake() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1060,7 +1045,6 @@ fn test_do_transfer_wrong_origin() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1098,7 +1082,6 @@ fn test_do_transfer_minimum_stake_check() { stake_amount, ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1146,7 +1129,6 @@ fn test_do_transfer_different_subnets() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1212,7 +1194,6 @@ fn test_do_swap_success() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1320,7 +1301,6 @@ fn test_do_swap_insufficient_stake() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1355,7 +1335,6 @@ fn test_do_swap_wrong_origin() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1393,7 +1372,6 @@ fn test_do_swap_minimum_stake_check() { total_stake, ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1429,7 +1407,6 @@ fn test_do_swap_same_subnet() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1474,7 +1451,6 @@ fn test_do_swap_partial_stake() { total_stake_tao.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let total_stake_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1526,7 +1502,6 @@ fn test_do_swap_storage_updates() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1586,7 +1561,6 @@ fn test_do_swap_multiple_times() { initial_stake.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1596,7 +1570,6 @@ fn test_do_swap_multiple_times() { &hotkey, &coldkey, netuid1, ); if !alpha1.is_zero() { - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid1); assert_ok!(SubtensorModule::do_swap_stake( RuntimeOrigin::signed(coldkey), hotkey, @@ -1612,7 +1585,7 @@ fn test_do_swap_multiple_times() { let (tao_equivalent, _) = mock::swap_alpha_to_tao_ext(netuid2, alpha2, true); // we do this in the loop, because we need the value before the swap expected_alpha = mock::swap_tao_to_alpha(netuid1, tao_equivalent).0; - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid2); + assert_ok!(SubtensorModule::do_swap_stake( RuntimeOrigin::signed(coldkey), hotkey, @@ -1657,7 +1630,6 @@ fn test_do_swap_allows_non_owned_hotkey() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1752,7 +1724,7 @@ fn test_move_stake_specific_stake_into_subnet_fail() { // Move stake to destination subnet let (tao_equivalent, _) = mock::swap_alpha_to_tao_ext(origin_netuid, alpha_to_move, true); let (expected_value, _) = mock::swap_tao_to_alpha(netuid, tao_equivalent); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, origin_netuid); + assert_ok!(SubtensorModule::move_stake( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index a096c29723..79ea76fa32 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -872,7 +872,6 @@ fn test_remove_stake_insufficient_liquidity() { amount_staked.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -954,8 +953,6 @@ fn test_remove_stake_total_issuance_no_change() { let total_fee = mock::swap_alpha_to_tao(netuid, stake).1 + fee; - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); - assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -1060,7 +1057,6 @@ fn test_remove_prev_epoch_stake() { netuid, ); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); let fee = mock::swap_alpha_to_tao(netuid, stake).1 + fee; assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey_account_id), @@ -1661,7 +1657,7 @@ fn test_clear_small_nominations() { SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold1, netuid); let unstake_amount1 = AlphaCurrency::from(alpha_stake1.to_u64() * 997 / 1000); let small1 = alpha_stake1 - unstake_amount1; - remove_stake_rate_limit_for_tests(&hot1, &cold1, netuid); + assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(cold1), hot1, @@ -1685,7 +1681,7 @@ fn test_clear_small_nominations() { SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold2, netuid); let unstake_amount2 = AlphaCurrency::from(alpha_stake2.to_u64() * 997 / 1000); let small2 = alpha_stake2 - unstake_amount2; - remove_stake_rate_limit_for_tests(&hot1, &cold2, netuid); + assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(cold2), hot1, @@ -2045,10 +2041,10 @@ fn test_get_total_delegated_stake_after_unstaking() { &delegator, netuid, ); - remove_stake_rate_limit_for_tests(&delegator, &delegate_hotkey, netuid); + // Unstake part of the delegation let unstake_amount_alpha = delegated_alpha / 2.into(); - remove_stake_rate_limit_for_tests(&delegate_hotkey, &delegator, netuid); + assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(delegator), delegate_hotkey, @@ -3794,7 +3790,7 @@ fn test_remove_stake_limit_ok() { let fee: u64 = (expected_alpha_reduction as f64 * 0.003) as u64; // Remove stake with slippage safety - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); + assert_ok!(SubtensorModule::remove_stake_limit( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -3986,7 +3982,7 @@ fn test_remove_99_9991_per_cent_stake_removes_all() { &coldkey_account_id, netuid, ); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); + let remove_amount = AlphaCurrency::from( (U64F64::from_num(alpha) * U64F64::from_num(0.999991)).to_num::(), ); @@ -4043,7 +4039,7 @@ fn test_remove_99_9989_per_cent_stake_leaves_a_little() { )); // Remove 99.9989% stake - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); + let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey_account_id, &coldkey_account_id, @@ -4270,8 +4266,6 @@ fn test_unstake_all_alpha_works() { stake_amount.into() )); - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - // Setup the pool so that removing all the TAO will keep liq above min mock::setup_reserves( netuid, @@ -4328,7 +4322,6 @@ fn test_unstake_all_works() { (stake_amount * 10).into(), (stake_amount * 100).into(), ); - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); // Unstake all alpha to free balance assert_ok!(SubtensorModule::unstake_all( @@ -4382,7 +4375,6 @@ fn test_stake_into_subnet_ok() { amount.into(), TaoCurrency::MAX, false, - false, )); let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 / u16::MAX as f64; @@ -4436,7 +4428,6 @@ fn test_stake_into_subnet_low_amount() { amount.into(), TaoCurrency::MAX, false, - false, )); let expected_stake = AlphaCurrency::from(((amount as f64) * 0.997 / current_price) as u64); @@ -4484,7 +4475,6 @@ fn test_unstake_from_subnet_low_amount() { amount.into(), TaoCurrency::MAX, false, - false, )); // Remove stake @@ -4598,7 +4588,6 @@ fn test_unstake_from_subnet_prohibitive_limit() { amount.into(), TaoCurrency::MAX, false, - false, )); // Remove stake @@ -4674,7 +4663,6 @@ fn test_unstake_full_amount() { amount.into(), TaoCurrency::MAX, false, - false, )); // Remove stake @@ -4816,7 +4804,7 @@ fn test_swap_fees_tao_correctness() { &coldkey, netuid, ); - remove_stake_rate_limit_for_tests(&owner_hotkey, &coldkey, netuid); + assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey), owner_hotkey, @@ -5055,7 +5043,7 @@ fn test_default_min_stake_sufficiency() { let fee_stake = (fee_rate * amount as f64) as u64; let current_price_after_stake = ::SwapInterface::current_alpha_price(netuid.into()); - remove_stake_rate_limit_for_tests(&owner_hotkey, &coldkey, netuid); + let user_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &owner_hotkey, &coldkey, @@ -5139,8 +5127,6 @@ fn test_update_position_fees() { amount.into(), )); - remove_stake_rate_limit_for_tests(&owner_hotkey, &coldkey, netuid); - let user_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &owner_hotkey, &coldkey, @@ -5285,54 +5271,6 @@ fn test_large_swap() { }); } -#[test] -fn test_stake_rate_limits() { - new_test_ext(0).execute_with(|| { - // Create subnet and accounts. - let subnet_owner_coldkey = U256::from(10); - let subnet_owner_hotkey = U256::from(20); - let hot1 = U256::from(1); - let cold1 = U256::from(3); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let amount = DefaultMinStake::::get().to_u64() * 10; - let fee = DefaultMinStake::::get().to_u64(); - let init_balance = amount + fee + ExistentialDeposit::get(); - - register_ok_neuron(netuid, hot1, cold1, 0); - Delegates::::insert(hot1, SubtensorModule::get_min_delegate_take()); - assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hot1), cold1); - - SubtensorModule::add_balance_to_coldkey_account(&cold1, init_balance); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(cold1), - hot1, - netuid, - (amount + fee).into() - )); - - assert_err!( - SubtensorModule::remove_stake( - RuntimeOrigin::signed(cold1), - hot1, - netuid, - amount.into() - ), - Error::::StakingOperationRateLimitExceeded - ); - - // Test limit clear each block - assert!(StakingOperationRateLimiter::::contains_key(( - hot1, cold1, netuid - ))); - - next_block(); - - assert!(!StakingOperationRateLimiter::::contains_key(( - hot1, cold1, netuid - ))); - }); -} - // cargo test --package pallet-subtensor --lib -- tests::staking::test_add_root_updates_counters --exact --show-output #[test] fn test_add_root_updates_counters() { @@ -5488,7 +5426,6 @@ fn test_staking_records_flow() { amount.into(), TaoCurrency::MAX, false, - false, )); let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 / u16::MAX as f64; diff --git a/pallets/subtensor/src/tests/subnet.rs b/pallets/subtensor/src/tests/subnet.rs index a547b30a14..8792278d90 100644 --- a/pallets/subtensor/src/tests/subnet.rs +++ b/pallets/subtensor/src/tests/subnet.rs @@ -618,8 +618,6 @@ fn test_subtoken_enable_trading_ok_with_enable() { stake_amount )); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); - assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -650,8 +648,6 @@ fn test_subtoken_enable_trading_ok_with_enable() { unstake_amount, )); - remove_stake_rate_limit_for_tests(&hotkey_account_2_id, &coldkey_account_id, netuid); - assert_ok!(SubtensorModule::transfer_stake( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, diff --git a/pallets/subtensor/src/tests/swap_coldkey.rs b/pallets/subtensor/src/tests/swap_coldkey.rs index 9d3bdbfc62..10637057b1 100644 --- a/pallets/subtensor/src/tests/swap_coldkey.rs +++ b/pallets/subtensor/src/tests/swap_coldkey.rs @@ -2437,8 +2437,6 @@ fn test_coldkey_in_swap_schedule_prevents_funds_usage() { CustomTransactionError::ColdkeyInSwapSchedule.into() ); - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - // Remove stake let call = RuntimeCall::SubtensorModule(SubtensorCall::remove_stake { hotkey, diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 7647c53f1a..7526bb412e 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -737,20 +737,15 @@ pub fn setup_subnets(sncount: u16, neurons: u16) -> TestSetup { } } -pub(crate) fn remove_stake_rate_limit_for_tests(hotkey: &U256, coldkey: &U256, netuid: NetUid) { - StakingOperationRateLimiter::::remove((hotkey, coldkey, netuid)); -} - #[allow(dead_code)] pub fn setup_stake(netuid: NetUid, coldkey: &U256, hotkey: &U256, amount: u64) { // Stake to hotkey account, and check if the result is ok SubtensorModule::add_balance_to_coldkey_account(coldkey, amount + ExistentialDeposit::get()); - remove_stake_rate_limit_for_tests(hotkey, coldkey, netuid); + assert_ok!(SubtensorModule::add_stake( RuntimeOrigin::signed(*coldkey), *hotkey, netuid, amount.into() )); - remove_stake_rate_limit_for_tests(hotkey, coldkey, netuid); } From 0200dabf03653e40d7b8e01282b1eafea1616a25 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 20 Jan 2026 13:28:26 +0100 Subject: [PATCH 80/95] Remove ensure_stake_operation_limit_not_exceeded --- pallets/subtensor/src/staking/stake_utils.rs | 21 -------------------- 1 file changed, 21 deletions(-) diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 8f8919ba50..a8cacca701 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -1028,8 +1028,6 @@ impl Pallet { // Ensure that the subnet exists. ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); - Self::ensure_stake_operation_limit_not_exceeded(hotkey, coldkey, netuid.into())?; - // Ensure that the subnet is enabled. // Self::ensure_subtoken_enabled(netuid)?; @@ -1124,12 +1122,6 @@ impl Pallet { ensure!(origin_netuid != destination_netuid, Error::::SameNetuid); } - Self::ensure_stake_operation_limit_not_exceeded( - origin_hotkey, - origin_coldkey, - origin_netuid.into(), - )?; - // Ensure that both subnets exist. ensure!( Self::if_subnet_exist(origin_netuid), @@ -1252,19 +1244,6 @@ impl Pallet { SubnetAlphaIn::::set(netuid, subnet_alpha.saturating_sub(carry_over)); } } - - pub fn ensure_stake_operation_limit_not_exceeded( - hotkey: &T::AccountId, - coldkey: &T::AccountId, - netuid: NetUid, - ) -> Result<(), Error> { - ensure!( - !StakingOperationRateLimiter::::contains_key((hotkey, coldkey, netuid)), - Error::::StakingOperationRateLimitExceeded - ); - - Ok(()) - } } /////////////////////////////////////////// From a777aba0726dcc3a31fe670acca8c7569e6aa32d Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 20 Jan 2026 13:32:15 +0100 Subject: [PATCH 81/95] Remove StakingOperationRateLimiter --- pallets/subtensor/src/benchmarks.rs | 20 -------------------- pallets/subtensor/src/coinbase/root.rs | 14 -------------- pallets/subtensor/src/lib.rs | 14 -------------- pallets/subtensor/src/macros/errors.rs | 2 -- pallets/subtensor/src/macros/hooks.rs | 11 ----------- 5 files changed, 61 deletions(-) diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index fe701a9187..7eaabff73f 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -736,9 +736,6 @@ mod pallet_benchmarks { Subtensor::::create_account_if_non_existent(&coldkey, &destination); - // Remove stake limit for benchmark - StakingOperationRateLimiter::::remove((origin.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -797,9 +794,6 @@ mod pallet_benchmarks { let amount_unstaked = AlphaCurrency::from(30_000_000_000); - // Remove stake limit for benchmark - StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -860,9 +854,6 @@ mod pallet_benchmarks { allow )); - // Remove stake limit for benchmark - StakingOperationRateLimiter::::remove((hot.clone(), coldkey.clone(), netuid1)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -914,9 +905,6 @@ mod pallet_benchmarks { Subtensor::::create_account_if_non_existent(&dest, &hot); - // Remove stake limit for benchmark - StakingOperationRateLimiter::::remove((hot.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -969,9 +957,6 @@ mod pallet_benchmarks { let alpha_to_swap = Subtensor::::get_stake_for_hotkey_and_coldkey_on_subnet(&hot, &coldkey, netuid1); - // Remove stake limit for benchmark - StakingOperationRateLimiter::::remove((hot.clone(), coldkey.clone(), netuid1)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1281,9 +1266,6 @@ mod pallet_benchmarks { staked_amt.into() )); - // Remove stake limit for benchmark - StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _(RawOrigin::Signed(coldkey), hotkey); } @@ -1333,8 +1315,6 @@ mod pallet_benchmarks { u64_staked_amt.into() )); - StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 106a5d6f28..a891ace15c 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -460,20 +460,6 @@ impl Pallet { TransactionKeyLastBlock::::remove((hot, netuid, name)); } } - // StakingOperationRateLimiter NMAP: (hot, cold, netuid) → bool - { - let to_rm: sp_std::vec::Vec<(T::AccountId, T::AccountId)> = - StakingOperationRateLimiter::::iter() - .filter_map( - |((hot, cold, n), _)| { - if n == netuid { Some((hot, cold)) } else { None } - }, - ) - .collect(); - for (hot, cold) in to_rm { - StakingOperationRateLimiter::::remove((hot, cold, netuid)); - } - } // --- 22. Subnet leasing: remove mapping and any lease-scoped state linked to this netuid. if let Some(lease_id) = SubnetUidToLeaseId::::take(netuid) { diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 4fd7f63571..b7fa01ef38 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -2163,20 +2163,6 @@ pub mod pallet { OptionQuery, >; - /// DMAP ( hot, cold, netuid ) --> rate limits for staking operations - /// Value contains just a marker: we use this map as a set. - #[pallet::storage] - pub type StakingOperationRateLimiter = StorageNMap< - _, - ( - NMapKey, // hot - NMapKey, // cold - NMapKey, // subnet - ), - bool, - ValueQuery, - >; - #[pallet::storage] // --- MAP(netuid ) --> Root claim threshold pub type RootClaimableThreshold = StorageMap<_, Blake2_128Concat, NetUid, I96F32, ValueQuery, DefaultMinRootClaimAmount>; diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 29bad79b51..e2540d02ee 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -209,8 +209,6 @@ mod errors { SameNetuid, /// The caller does not have enough balance for the operation. InsufficientBalance, - /// Too frequent staking operations - StakingOperationRateLimitExceeded, /// Invalid lease beneficiary to register the leased network. InvalidLeaseBeneficiary, /// Lease cannot end in the past. diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 441beba5fd..8ad22b0423 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -38,17 +38,6 @@ mod hooks { } } - // ---- Called on the finalization of this pallet. The code weight must be taken into account prior to the execution of this macro. - // - // # Args: - // * 'n': (BlockNumberFor): - // - The number of the block we are finalizing. - fn on_finalize(_block_number: BlockNumberFor) { - for _ in StakingOperationRateLimiter::::drain() { - // Clear all entries each block - } - } - fn on_runtime_upgrade() -> frame_support::weights::Weight { // --- Migrate storage let mut weight = frame_support::weights::Weight::from_parts(0, 0); From 06760b6cfc3d2a070f162976950da25b922676ae Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 20 Jan 2026 15:47:31 +0100 Subject: [PATCH 82/95] Add integration tests for staking-ops rate-limiting group --- runtime/src/rate_limiting/mod.rs | 33 ++++- runtime/tests/rate_limiting.rs | 231 ++++++++++++++++++++++++++++++- 2 files changed, 256 insertions(+), 8 deletions(-) diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index 34c7583f55..b0683dc824 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -329,23 +329,44 @@ impl RateLimitUsageResolver { + let coldkey = signed_origin(origin)?; + Some(vec![RateLimitUsageKey::::ColdkeyHotkeySubnet { + coldkey, + hotkey: hotkey.clone(), + netuid: *netuid, + }]) } - | SubtensorCall::swap_stake { + SubtensorCall::swap_stake { hotkey, - origin_netuid: netuid, + destination_netuid: netuid, .. } | SubtensorCall::swap_stake_limit { hotkey, - origin_netuid: netuid, + destination_netuid: netuid, .. + } => { + let coldkey = signed_origin(origin)?; + Some(vec![RateLimitUsageKey::::ColdkeyHotkeySubnet { + coldkey, + hotkey: hotkey.clone(), + netuid: *netuid, + }]) } - | SubtensorCall::move_stake { - origin_hotkey: hotkey, - origin_netuid: netuid, + SubtensorCall::move_stake { + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, .. } => { let coldkey = signed_origin(origin)?; + let (hotkey, netuid) = if origin_netuid == destination_netuid { + (origin_hotkey, origin_netuid) + } else { + (destination_hotkey, destination_netuid) + }; Some(vec![RateLimitUsageKey::::ColdkeyHotkeySubnet { coldkey, hotkey: hotkey.clone(), diff --git a/runtime/tests/rate_limiting.rs b/runtime/tests/rate_limiting.rs index e0a88fcded..e2e216bd34 100644 --- a/runtime/tests/rate_limiting.rs +++ b/runtime/tests/rate_limiting.rs @@ -1,7 +1,7 @@ #![allow(clippy::unwrap_used)] use codec::{Compact, Encode}; -use frame_support::assert_ok; +use frame_support::{assert_ok, traits::Get}; use node_subtensor_runtime::{ Executive, Runtime, RuntimeCall, SignedPayload, SubtensorInitialTxDelegateTakeRateLimit, System, TransactionExtensions, UncheckedExtrinsic, check_nonce, @@ -14,7 +14,7 @@ use sp_runtime::{ traits::SaturatedConversion, transaction_validity::{InvalidTransaction, TransactionValidityError}, }; -use subtensor_runtime_common::{AccountId, MechId, NetUid}; +use subtensor_runtime_common::{AccountId, AlphaCurrency, Currency, MechId, NetUid}; use common::ExtBuilder; @@ -77,6 +77,22 @@ fn setup_weights_network(netuid: NetUid, hotkey: &AccountId, block: u64, mechani pallet_subtensor::Pallet::::append_neuron(netuid, hotkey, block); } +fn setup_staking_network(netuid: NetUid) { + pallet_subtensor::Pallet::::init_new_network(netuid, 1); + pallet_subtensor::SubtokenEnabled::::insert(netuid, true); + pallet_subtensor::TransferToggle::::insert(netuid, true); +} + +fn seed_stake(netuid: NetUid, hotkey: &AccountId, coldkey: &AccountId, alpha: u64) { + pallet_subtensor::Pallet::::create_account_if_non_existent(coldkey, hotkey); + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, + coldkey, + netuid, + AlphaCurrency::from(alpha), + ); +} + #[test] fn register_network_is_rate_limited_after_migration() { let coldkey_pair = sr25519::Pair::from_seed(&[1u8; 32]); @@ -589,3 +605,214 @@ fn batch_set_weights_is_rate_limited_if_any_scope_is_within_span() { assert_extrinsic_ok(&hotkey, &hotkey_pair, batch_call); }); } + +#[test] +fn staking_add_then_remove_is_rate_limited_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[20u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from([21u8; 32]); + let netuid = NetUid::from(10u16); + let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; + let balance = stake_amount * 10; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_staking_network(netuid); + pallet_subtensor::Pallet::::create_account_if_non_existent(&coldkey, &hotkey); + + Executive::execute_on_runtime_upgrade(); + + let add_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { + hotkey: hotkey.clone(), + netuid, + amount_staked: stake_amount.into(), + }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, add_call); + + let alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let remove_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey, + netuid, + amount_unstaked: alpha, + }); + + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, remove_call.clone()); + + System::set_block_number(2); + assert_extrinsic_ok(&coldkey, &coldkey_pair, remove_call); + }); +} + +#[test] +fn transfer_stake_is_rate_limited_after_add_stake() { + let coldkey_pair = sr25519::Pair::from_seed(&[22u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let destination_coldkey = AccountId::from([23u8; 32]); + let hotkey = AccountId::from([24u8; 32]); + let netuid = NetUid::from(11u16); + let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; + let balance = stake_amount * 10; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_staking_network(netuid); + pallet_subtensor::Pallet::::create_account_if_non_existent(&coldkey, &hotkey); + + Executive::execute_on_runtime_upgrade(); + + let add_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { + hotkey: hotkey.clone(), + netuid, + amount_staked: stake_amount.into(), + }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, add_call); + + let alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let transfer_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { + destination_coldkey, + hotkey, + origin_netuid: netuid, + destination_netuid: netuid, + alpha_amount: alpha, + }); + + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, transfer_call); + }); +} + +#[test] +fn transfer_stake_does_not_limit_destination_coldkey() { + let coldkey_pair = sr25519::Pair::from_seed(&[25u8; 32]); + let destination_pair = sr25519::Pair::from_seed(&[26u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let destination_coldkey = AccountId::from(destination_pair.public()); + let hotkey = AccountId::from([27u8; 32]); + let origin_netuid = NetUid::from(12u16); + let destination_netuid = NetUid::from(13u16); + let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; + + ExtBuilder::default() + .with_balances(vec![ + (coldkey.clone(), stake_amount * 10), + (destination_coldkey.clone(), stake_amount * 10), + ]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_staking_network(origin_netuid); + setup_staking_network(destination_netuid); + seed_stake(origin_netuid, &hotkey, &coldkey, stake_amount); + + Executive::execute_on_runtime_upgrade(); + + let alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + origin_netuid, + ); + let transfer_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { + destination_coldkey: destination_coldkey.clone(), + hotkey: hotkey.clone(), + origin_netuid, + destination_netuid, + alpha_amount: alpha, + }); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, transfer_call); + + let destination_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &destination_coldkey, + destination_netuid, + ); + let remove_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey, + netuid: destination_netuid, + amount_unstaked: destination_alpha, + }); + + assert_extrinsic_ok(&destination_coldkey, &destination_pair, remove_call); + }); +} + +#[test] +fn swap_stake_limits_destination_netuid() { + let coldkey_pair = sr25519::Pair::from_seed(&[28u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from([29u8; 32]); + let origin_netuid = NetUid::from(14u16); + let destination_netuid = NetUid::from(15u16); + let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), stake_amount * 10)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_staking_network(origin_netuid); + setup_staking_network(destination_netuid); + seed_stake(origin_netuid, &hotkey, &coldkey, stake_amount); + + Executive::execute_on_runtime_upgrade(); + + let alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + origin_netuid, + ); + let swap_alpha = AlphaCurrency::from(alpha.to_u64() / 2); + let swap_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake { + hotkey: hotkey.clone(), + origin_netuid, + destination_netuid, + alpha_amount: swap_alpha, + }); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_call); + + let destination_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + destination_netuid, + ); + let remove_destination = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey: hotkey.clone(), + netuid: destination_netuid, + amount_unstaked: destination_alpha, + }); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, remove_destination); + + let origin_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + origin_netuid, + ); + let remove_origin = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey, + netuid: origin_netuid, + amount_unstaked: origin_alpha, + }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, remove_origin); + }); +} From bf54f31bdb37cba173c538630f160590becc6b1c Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 20 Jan 2026 16:27:27 +0100 Subject: [PATCH 83/95] Update genesis configs for localnet/devnet to include staking-ops rate-limiting --- node/src/chain_spec/devnet.rs | 7 ++++++- node/src/chain_spec/localnet.rs | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/node/src/chain_spec/devnet.rs b/node/src/chain_spec/devnet.rs index f8b769d0d9..50317373fb 100644 --- a/node/src/chain_spec/devnet.rs +++ b/node/src/chain_spec/devnet.rs @@ -5,7 +5,10 @@ use super::*; use node_subtensor_runtime::rate_limiting::legacy::defaults as rate_limit_defaults; use subtensor_runtime_common::{ NetUid, - rate_limiting::{GROUP_DELEGATE_TAKE, GROUP_REGISTER_NETWORK, GROUP_SERVE, GROUP_WEIGHTS_SET}, + rate_limiting::{ + GROUP_DELEGATE_TAKE, GROUP_REGISTER_NETWORK, GROUP_SERVE, GROUP_STAKING_OPS, + GROUP_WEIGHTS_SET, + }, }; pub fn devnet_config() -> Result { @@ -101,6 +104,7 @@ fn devnet_genesis( (serde_json::json!({ "Group": GROUP_SERVE }), Some(NetUid::from(1u16)), serde_json::json!({ "Exact": rate_limit_defaults::serving_rate_limit() })), (serde_json::json!({ "Group": GROUP_REGISTER_NETWORK }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::network_rate_limit() })), (serde_json::json!({ "Group": GROUP_DELEGATE_TAKE }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::tx_delegate_take_rate_limit() })), + (serde_json::json!({ "Group": GROUP_STAKING_OPS }), Option::::None, serde_json::json!({ "Exact": 1 })), (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::ROOT), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::from(1u16)), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), ], @@ -108,6 +112,7 @@ fn devnet_genesis( (GROUP_SERVE, b"serving".to_vec(), "ConfigAndUsage"), (GROUP_REGISTER_NETWORK, b"register-network".to_vec(), "ConfigAndUsage"), (GROUP_DELEGATE_TAKE, b"delegate-take".to_vec(), "ConfigAndUsage"), + (GROUP_STAKING_OPS, b"staking-ops".to_vec(), "ConfigAndUsage"), (GROUP_WEIGHTS_SET, b"weights".to_vec(), "ConfigAndUsage"), ], "limitSettingRules": vec![ diff --git a/node/src/chain_spec/localnet.rs b/node/src/chain_spec/localnet.rs index 6d7ec87b5d..f2aa494e5f 100644 --- a/node/src/chain_spec/localnet.rs +++ b/node/src/chain_spec/localnet.rs @@ -5,7 +5,10 @@ use super::*; use node_subtensor_runtime::rate_limiting::legacy::defaults as rate_limit_defaults; use subtensor_runtime_common::{ NetUid, - rate_limiting::{GROUP_DELEGATE_TAKE, GROUP_REGISTER_NETWORK, GROUP_SERVE, GROUP_WEIGHTS_SET}, + rate_limiting::{ + GROUP_DELEGATE_TAKE, GROUP_REGISTER_NETWORK, GROUP_SERVE, GROUP_STAKING_OPS, + GROUP_WEIGHTS_SET, + }, }; pub fn localnet_config(single_authority: bool) -> Result { @@ -136,6 +139,7 @@ fn localnet_genesis( (serde_json::json!({ "Group": GROUP_SERVE }), Some(NetUid::from(1u16)), serde_json::json!({ "Exact": rate_limit_defaults::serving_rate_limit() })), (serde_json::json!({ "Group": GROUP_REGISTER_NETWORK }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::network_rate_limit() })), (serde_json::json!({ "Group": GROUP_DELEGATE_TAKE }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::tx_delegate_take_rate_limit() })), + (serde_json::json!({ "Group": GROUP_STAKING_OPS }), Option::::None, serde_json::json!({ "Exact": 1 })), (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::ROOT), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::from(1u16)), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), ], @@ -143,6 +147,7 @@ fn localnet_genesis( (GROUP_SERVE, b"serving".to_vec(), "ConfigAndUsage"), (GROUP_REGISTER_NETWORK, b"register-network".to_vec(), "ConfigAndUsage"), (GROUP_DELEGATE_TAKE, b"delegate-take".to_vec(), "ConfigAndUsage"), + (GROUP_STAKING_OPS, b"staking-ops".to_vec(), "ConfigAndUsage"), (GROUP_WEIGHTS_SET, b"weights".to_vec(), "ConfigAndUsage"), ], "limitSettingRules": vec![ From a48397246ff410f2f9d3e79400185d5a636db716 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 20 Jan 2026 20:49:00 +0100 Subject: [PATCH 84/95] Update contract tests --- contract-tests/get-metadata.sh | 2 +- contract-tests/src/subtensor.ts | 23 +++++++++++-------- .../subnet.precompile.hyperparameter.test.ts | 9 ++++---- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/contract-tests/get-metadata.sh b/contract-tests/get-metadata.sh index 90cb97e162..bd24051922 100755 --- a/contract-tests/get-metadata.sh +++ b/contract-tests/get-metadata.sh @@ -5,4 +5,4 @@ rm -rf .papi npx papi add devnet -w ws://localhost:9944 npx papi ink add ./bittensor/target/ink/bittensor.json # Yarn copies file: dependencies into node_modules, so reinstall to pick up new .papi/descriptors. -yarn install +yarn install --check-files diff --git a/contract-tests/src/subtensor.ts b/contract-tests/src/subtensor.ts index 47fee61cb5..77396377ae 100644 --- a/contract-tests/src/subtensor.ts +++ b/contract-tests/src/subtensor.ts @@ -1,28 +1,32 @@ import * as assert from "assert"; import { devnet, MultiAddress } from '@polkadot-api/descriptors'; -import { TypedApi, TxCallData, Binary } from 'polkadot-api'; +import { TypedApi, TxCallData, Binary, Enum } from 'polkadot-api'; import { KeyPair } from "@polkadot-labs/hdkd-helpers" import { getAliceSigner, waitForTransactionCompletion, getSignerFromKeypair, waitForTransactionWithRetry } from './substrate' import { convertH160ToSS58, convertPublicKeyToSs58, ethAddressToH160 } from './address-utils' import { tao } from './balance-math' import internal from "stream"; +const rateLimitTargetGroup = (groupId: number) => Enum("Group", groupId); +const rateLimitKindExact = (limit: bigint | number) => + Enum("Exact", typeof limit === "bigint" ? Number(limit) : limit); + // create a new subnet and return netuid export async function addNewSubnetwork(api: TypedApi, hotkey: KeyPair, coldkey: KeyPair) { const alice = getAliceSigner() const totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue() const registerNetworkGroupId = 3; // GROUP_REGISTER_NETWORK constant - const target = { Group: registerNetworkGroupId } as const; + const target = rateLimitTargetGroup(registerNetworkGroupId); const limits = await api.query.RateLimiting.Limits.getValue(target as any) as any; - assert.ok(limits?.tag === "Global"); - assert.ok(limits.value?.tag === "Exact"); + assert.ok(limits?.type === "Global"); + assert.ok(limits.value?.type === "Exact"); const rateLimit = BigInt(limits.value.value); if (rateLimit !== BigInt(0)) { const internalCall = api.tx.RateLimiting.set_rate_limit({ target: target as any, scope: undefined, - limit: { Exact: BigInt(0) }, + limit: rateLimitKindExact(0), }) const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) await waitForTransactionWithRetry(api, tx, alice) @@ -76,27 +80,26 @@ export async function setCommitRevealWeightsEnabled(api: TypedApi export async function setWeightsSetRateLimit(api: TypedApi, netuid: number, rateLimit: bigint) { const weightsSetGroupId = 2; // GROUP_WEIGHTS_SET constant - const target = { Group: weightsSetGroupId } as const; + const target = rateLimitTargetGroup(weightsSetGroupId); const limits = await api.query.RateLimiting.Limits.getValue(target as any) as any; - assert.ok(limits?.tag === "Scoped"); + assert.ok(limits?.type === "Scoped"); const entries = Array.from(limits.value as any); const entry = entries.find((item: any) => Number(item[0]) === netuid); const currentLimit = entry ? BigInt(entry[1].value) : BigInt(0); if (currentLimit === rateLimit) { return; } - const alice = getAliceSigner() const internalCall = api.tx.RateLimiting.set_rate_limit({ target: target as any, scope: netuid, - limit: { Exact: rateLimit }, + limit: rateLimitKindExact(rateLimit), }) const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) await waitForTransactionWithRetry(api, tx, alice) const updated = await api.query.RateLimiting.Limits.getValue(target as any) as any; - assert.ok(updated?.tag === "Scoped"); + assert.ok(updated?.type === "Scoped"); const updatedEntry = Array.from(updated.value as any).find( (item: any) => Number(item[0]) === netuid, ); diff --git a/contract-tests/test/subnet.precompile.hyperparameter.test.ts b/contract-tests/test/subnet.precompile.hyperparameter.test.ts index e6e6a41b6c..369e9a919b 100644 --- a/contract-tests/test/subnet.precompile.hyperparameter.test.ts +++ b/contract-tests/test/subnet.precompile.hyperparameter.test.ts @@ -2,7 +2,7 @@ import * as assert from "assert"; import { getAliceSigner, getDevnetApi, getRandomSubstrateKeypair, waitForTransactionWithRetry } from "../src/substrate" import { devnet } from "@polkadot-api/descriptors" -import { Binary, TypedApi, getTypedCodecs } from "polkadot-api"; +import { Binary, Enum, TypedApi, getTypedCodecs } from "polkadot-api"; import { convertH160ToSS58, convertPublicKeyToSs58 } from "../src/address-utils" import { generateRandomEthersWallet } from "../src/utils"; import { ISubnetABI, ISUBNET_ADDRESS } from "../src/contracts/subnet" @@ -98,16 +98,15 @@ describe("Test the Subnet precompile contract", () => { const tx = await contract.setServingRateLimit(netuid, newValue); await tx.wait(); - const limits = await api.query.RateLimiting.Limits.getValue({ Group: 0 } as any) as any; - assert.ok(limits?.tag === "Scoped"); + const limits = await api.query.RateLimiting.Limits.getValue(Enum("Group", 0) as any) as any; + assert.ok(limits?.type === "Scoped"); const entry = Array.from(limits.value as any).find( (item: any) => Number(item[0]) === netuid, ); assert.ok(entry); - assert.ok(entry[1]?.tag === "Exact"); + assert.ok(entry[1]?.type === "Exact"); const onchainValue = Number(entry[1].value); - let valueFromContract = Number( await contract.getServingRateLimit(netuid) ); From ea0f4cb9e3eb26368a8da7aa38706b3c8583578e Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 21 Jan 2026 15:59:10 +0100 Subject: [PATCH 85/95] Remove TxRateLimit dependency from rate-limiting migration --- runtime/src/migrations/rate_limiting.rs | 10 ++++++++-- runtime/src/rate_limiting/mod.rs | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 5843904d26..e7bd95242a 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -1331,7 +1331,7 @@ mod tests { RateLimitKey::LastTxBlock(cold.clone()), now - 1, ); - pallet_subtensor::TxRateLimit::::put(span); + legacy_storage::set_tx_rate_limit(span); let call = RuntimeCall::SubtensorModule(SubtensorCall::swap_hotkey { hotkey: old_hot, @@ -1339,7 +1339,13 @@ mod tests { netuid: None, }); let origin = RuntimeOrigin::signed(cold.clone()); - let legacy = || !SubtensorModule::exceeds_tx_rate_limit(now - 1, now); + let legacy = || { + let last = now - 1; + if span == 0 || last == 0 { + return true; + } + now - last > span + }; parity_check(now, call, origin, None, None, legacy); }); } diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index b0683dc824..ba9a4ebebf 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -233,7 +233,7 @@ impl RateLimitUsageResolver::Account(coldkey), - RateLimitUsageKey::::Account(new_hotkey.clone()), + RateLimitUsageKey::::Account(new_hotkey), ]) } SubtensorCall::increase_take { hotkey, .. } From 273dcbb4b60f9158697fa1f785210493a75605f6 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 22 Jan 2026 16:39:15 +0100 Subject: [PATCH 86/95] Fix swap-keys rate-limiting inconsistencies --- pallets/subtensor/src/swap/swap_hotkey.rs | 17 +++++++++++++---- runtime/src/migrations/rate_limiting.rs | 9 ++++++++- runtime/src/rate_limiting/mod.rs | 11 ++++------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 597e8c489d..9fc9c7b7e0 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -1,8 +1,13 @@ use super::*; use frame_support::weights::Weight; +use rate_limiting_interface::RateLimitingInterface; use sp_core::Get; +use sp_runtime::traits::SaturatedConversion; use substrate_fixed::types::U64F64; -use subtensor_runtime_common::{Currency, MechId, NetUid}; +use subtensor_runtime_common::{ + Currency, MechId, NetUid, + rate_limiting::{self, RateLimitUsageKey}, +}; impl Pallet { /// Swaps the hotkey of a coldkey account. @@ -64,8 +69,13 @@ impl Pallet { ); // 8. Swap LastTxBlock - let last_tx_block: u64 = Self::get_last_tx_block(old_hotkey); - Self::set_last_tx_block(new_hotkey, last_tx_block); + let last_tx_block = Self::get_last_tx_block(old_hotkey); + let last_seen = (last_tx_block != 0).then(|| last_tx_block.saturated_into()); + T::RateLimiting::set_last_seen( + rate_limiting::GROUP_SWAP_KEYS, + Some(RateLimitUsageKey::Account(new_hotkey.clone())), + last_seen, + ); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); // 10. Swap LastTxBlockChildKeyTake @@ -297,7 +307,6 @@ impl Pallet { Self::perform_hotkey_swap_on_one_subnet(old_hotkey, new_hotkey, &mut weight, netuid)?; // 10. Update the last transaction block for the coldkey - Self::set_last_tx_block(coldkey, block); LastHotkeySwapOnNetuid::::insert(netuid, coldkey, block); weight.saturating_accrue(T::DbWeight::get().writes(2)); diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index e7bd95242a..2235b2e1d3 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -607,7 +607,14 @@ fn build_swap_keys(groups: &mut Vec, commits: &mut Vec) -> let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); let (tx_rate_limit, tx_reads) = legacy_storage::tx_rate_limit(); reads = reads.saturating_add(tx_reads); - push_limit_commit_if_non_zero(commits, target, tx_rate_limit, None); + // Legacy check blocks at delta == limit; pallet-rate-limiting allows at delta == span. + // Add one block to preserve legacy behavior when legacy rate-limiting is removed. + let effective_limit = if tx_rate_limit == 0 { + 0 + } else { + tx_rate_limit.saturating_add(1) + }; + push_limit_commit_if_non_zero(commits, target, effective_limit, None); reads = reads.saturating_add( last_seen_helpers::collect_last_seen_from_last_rate_limited_block( diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs index ba9a4ebebf..123e3995fe 100644 --- a/runtime/src/rate_limiting/mod.rs +++ b/runtime/src/rate_limiting/mod.rs @@ -227,14 +227,11 @@ impl RateLimitUsageResolver { - // Record against the coldkey (enforcement) and the new hotkey to mirror legacy - // writes. + SubtensorCall::swap_hotkey { .. } => { + // Enforce only by coldkey; new_hotkey last-seen is recorded in pallet-subtensor + // to avoid double enforcement while preserving legacy tracking. let coldkey = signed_origin(origin)?; - Some(vec![ - RateLimitUsageKey::::Account(coldkey), - RateLimitUsageKey::::Account(new_hotkey), - ]) + Some(vec![RateLimitUsageKey::::Account(coldkey)]) } SubtensorCall::increase_take { hotkey, .. } | SubtensorCall::decrease_take { hotkey, .. } => { From 927d72a96c4e938ae4331554f1e595c03e1046e4 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 22 Jan 2026 16:58:03 +0100 Subject: [PATCH 87/95] Remove rate limit checked for swap-keys group - remove rate limit setter for swap-keys group - remove rate limit tests for swap-keys group --- pallets/admin-utils/src/lib.rs | 11 ++-- pallets/subtensor/src/swap/swap_hotkey.rs | 6 -- pallets/subtensor/src/tests/swap_hotkey.rs | 58 ------------------ .../src/tests/swap_hotkey_with_subnet.rs | 60 ------------------- pallets/subtensor/src/utils/misc.rs | 5 -- pallets/subtensor/src/utils/rate_limiting.rs | 8 --- 6 files changed, 7 insertions(+), 141 deletions(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index bbd82043ab..5ffa884e39 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -207,6 +207,9 @@ pub mod pallet { /// The extrinsic sets the transaction rate limit for the network. /// It is only callable by the root account. /// The extrinsic will call the Subtensor pallet to set the transaction rate limit. + /// + /// Deprecated: swap-keys rate limits are now configured via `pallet-rate-limiting` on the + /// swap-keys group target (`GROUP_SWAP_KEYS`). #[pallet::call_index(2)] #[pallet::weight( (Weight::from_parts(5_400_000, 0) @@ -214,11 +217,11 @@ pub mod pallet { DispatchClass::Operational, Pays::Yes) )] + #[deprecated( + note = "deprecated: configure via pallet-rate-limiting::set_rate_limit(target=Group(GROUP_SWAP_KEYS), ...)" + )] pub fn sudo_set_tx_rate_limit(origin: OriginFor, tx_rate_limit: u64) -> DispatchResult { - ensure_root(origin)?; - pallet_subtensor::Pallet::::set_tx_rate_limit(tx_rate_limit); - log::debug!("TxRateLimitSet( tx_rate_limit: {tx_rate_limit:?} ) "); - Ok(()) + Err(Error::::Deprecated.into()) } /// The extrinsic sets the serving rate limit for a subnet. diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 9fc9c7b7e0..3718bffa12 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -54,12 +54,6 @@ impl Pallet { // 5. Get the current block number let block: u64 = Self::get_current_block_as_u64(); - // 6. Ensure the transaction rate limit is not exceeded - ensure!( - !Self::exceeds_tx_rate_limit(Self::get_last_tx_block(&coldkey), block), - Error::::HotKeySetTxRateLimitExceeded - ); - weight.saturating_accrue(T::DbWeight::get().reads(2)); // 7. Ensure the new hotkey is not already registered on any network diff --git a/pallets/subtensor/src/tests/swap_hotkey.rs b/pallets/subtensor/src/tests/swap_hotkey.rs index 05114c4418..c53b232231 100644 --- a/pallets/subtensor/src/tests/swap_hotkey.rs +++ b/pallets/subtensor/src/tests/swap_hotkey.rs @@ -710,64 +710,6 @@ fn test_swap_hotkey_with_multiple_coldkeys_and_subnets() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey -- test_swap_hotkey_tx_rate_limit_exceeded --exact --nocapture -#[test] -fn test_swap_hotkey_tx_rate_limit_exceeded() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let old_hotkey = U256::from(1); - let new_hotkey_1 = U256::from(2); - let new_hotkey_2 = U256::from(4); - let coldkey = U256::from(3); - let swap_cost = 1_000_000_000u64 * 2; - - let tx_rate_limit = 1; - - // Get the current transaction rate limit - let current_tx_rate_limit = TxRateLimit::::get(); - log::info!("current_tx_rate_limit: {current_tx_rate_limit:?}"); - - // Set the transaction rate limit - SubtensorModule::set_tx_rate_limit(tx_rate_limit); - // assert the rate limit is set to 1000 blocks - assert_eq!(TxRateLimit::::get(), tx_rate_limit); - - // Setup initial state - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, old_hotkey, coldkey, 0); - SubtensorModule::add_balance_to_coldkey_account(&coldkey, swap_cost); - - // Perform the first swap - assert_ok!(SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &old_hotkey, - &new_hotkey_1, - None - )); - - // Attempt to perform another swap immediately, which should fail due to rate limit - assert_err!( - SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &new_hotkey_1, - &new_hotkey_2, - None - ), - Error::::HotKeySetTxRateLimitExceeded - ); - - // move in time past the rate limit - step_block(1001); - assert_ok!(SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &new_hotkey_1, - &new_hotkey_2, - None - )); - }); -} - // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey -- test_do_swap_hotkey_err_not_owner --exact --nocapture #[test] fn test_do_swap_hotkey_err_not_owner() { diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index 80a3aac349..9427e4affb 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -751,66 +751,6 @@ fn test_swap_hotkey_with_multiple_coldkeys_and_subnets() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_tx_rate_limit_exceeded --exact --nocapture -#[test] -fn test_swap_hotkey_tx_rate_limit_exceeded() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let old_hotkey = U256::from(1); - let new_hotkey_1 = U256::from(2); - let new_hotkey_2 = U256::from(4); - let coldkey = U256::from(3); - let swap_cost = 1_000_000_000u64 * 2; - - let tx_rate_limit = 1; - - // Get the current transaction rate limit - let current_tx_rate_limit = TxRateLimit::::get(); - log::info!("current_tx_rate_limit: {current_tx_rate_limit:?}"); - - // Set the transaction rate limit - SubtensorModule::set_tx_rate_limit(tx_rate_limit); - // assert the rate limit is set to 1000 blocks - assert_eq!(TxRateLimit::::get(), tx_rate_limit); - - // Setup initial state - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, old_hotkey, coldkey, 0); - SubtensorModule::add_balance_to_coldkey_account(&coldkey, swap_cost); - - // Perform the first swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey_1, - Some(netuid) - ),); - - // Attempt to perform another swap immediately, which should fail due to rate limit - assert_err!( - SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey_1, - Some(netuid) - ), - Error::::HotKeySetTxRateLimitExceeded - ); - - // move in time past the rate limit - step_block(1001); - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &new_hotkey_1, - &new_hotkey_2, - None - )); - }); -} - // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_do_swap_hotkey_err_not_owner --exact --nocapture #[test] fn test_do_swap_hotkey_err_not_owner() { diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 94e2e09e7e..6fd8d22ff6 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -415,11 +415,6 @@ impl Pallet { // ======================== // Configure tx rate limiting - pub fn set_tx_rate_limit(tx_rate_limit: u64) { - TxRateLimit::::put(tx_rate_limit); - Self::deposit_event(Event::TxRateLimitSet(tx_rate_limit)); - } - pub fn set_min_delegate_take(take: u16) { MinDelegateTake::::put(take); Self::deposit_event(Event::MinDelegateTakeSet(take)); diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index fb11bd823a..ad544065d4 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -241,12 +241,4 @@ impl Pallet { block, ); } - pub fn exceeds_tx_rate_limit(prev_tx_block: u64, current_block: u64) -> bool { - let rate_limit: u64 = TxRateLimit::::get(); - if rate_limit == 0 || prev_tx_block == 0 { - return false; - } - - current_block.saturating_sub(prev_tx_block) <= rate_limit - } } From cea3ffbdf8eda6dd9978d6ed08bcfd3bb3601463 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 22 Jan 2026 17:33:01 +0100 Subject: [PATCH 88/95] Remove get_last_tx_block --- pallets/subtensor/src/swap/swap_hotkey.rs | 8 +- pallets/subtensor/src/tests/migration.rs | 105 ------------------ pallets/subtensor/src/tests/swap_hotkey.rs | 38 ------- .../src/tests/swap_hotkey_with_subnet.rs | 40 ------- pallets/subtensor/src/utils/rate_limiting.rs | 3 - 5 files changed, 6 insertions(+), 188 deletions(-) diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 3718bffa12..65ea6dd19f 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -62,8 +62,12 @@ impl Pallet { Error::::HotKeyAlreadyRegisteredInSubNet ); - // 8. Swap LastTxBlock - let last_tx_block = Self::get_last_tx_block(old_hotkey); + // 8. Swap last-seen + let last_tx_block = T::RateLimiting::last_seen( + rate_limiting::GROUP_SWAP_KEYS, + Some(RateLimitUsageKey::Account(old_hotkey)), + ) + .unwrap_or_default(); let last_seen = (last_tx_block != 0).then(|| last_tx_block.saturated_into()); T::RateLimiting::set_last_seen( rate_limiting::GROUP_SWAP_KEYS, diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 2b8cd0d061..03b6c289e1 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -829,111 +829,6 @@ fn test_migrate_remove_commitments_rate_limit() { }); } -// TODO this must be removed after the legacy rate-limiting deprecation -#[test] -fn test_migrate_rate_limit_keys() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &[u8] = b"migrate_rate_limit_keys"; - let prefix = { - let pallet_prefix = twox_128("SubtensorModule".as_bytes()); - let storage_prefix = twox_128("LastRateLimitedBlock".as_bytes()); - [pallet_prefix, storage_prefix].concat() - }; - - // Seed new-format entries that must survive the migration untouched. - let new_last_account = U256::from(10); - SubtensorModule::set_last_tx_block(&new_last_account, 555); - let new_child_account = U256::from(11); - SubtensorModule::set_last_tx_block_childkey(&new_child_account, 777); - - // Legacy NetworkLastRegistered entry (index 1) - let mut legacy_network_key = prefix.clone(); - legacy_network_key.push(1u8); - sp_io::storage::set(&legacy_network_key, &111u64.encode()); - - // Legacy LastTxBlock entry (index 2) for an account that already has a new-format value. - let mut legacy_last_key = prefix.clone(); - legacy_last_key.push(2u8); - legacy_last_key.extend_from_slice(&new_last_account.encode()); - sp_io::storage::set(&legacy_last_key, &666u64.encode()); - - // Legacy LastTxBlockChildKeyTake entry (index 3) - let legacy_child_account = U256::from(3); - ChildKeys::::insert( - legacy_child_account, - NetUid::from(0), - vec![(0u64, U256::from(99))], - ); - let mut legacy_child_key = prefix.clone(); - legacy_child_key.push(3u8); - legacy_child_key.extend_from_slice(&legacy_child_account.encode()); - sp_io::storage::set(&legacy_child_key, &333u64.encode()); - - // Legacy LastTxBlockDelegateTake entry (index 4) - let legacy_delegate_account = U256::from(4); - Delegates::::insert(legacy_delegate_account, 500u16); - let mut legacy_delegate_key = prefix.clone(); - legacy_delegate_key.push(4u8); - legacy_delegate_key.extend_from_slice(&legacy_delegate_account.encode()); - sp_io::storage::set(&legacy_delegate_key, &444u64.encode()); - - let weight = crate::migrations::migrate_rate_limit_keys::migrate_rate_limit_keys::(); - assert!( - HasMigrationRun::::get(MIGRATION_NAME.to_vec()), - "Migration should be marked as executed" - ); - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - - // Legacy entries were migrated and cleared. - let network_last_lock_block: u64 = ::RateLimiting::last_seen( - rate_limiting::GROUP_REGISTER_NETWORK, - None, - ) - .unwrap_or_default() - .saturated_into(); - assert_eq!( - network_last_lock_block, 111, - "Network last lock block should match migrated value" - ); - assert!( - sp_io::storage::get(&legacy_network_key).is_none(), - "Legacy network entry should be cleared" - ); - - assert_eq!( - SubtensorModule::get_last_tx_block(&new_last_account), - 666u64, - "LastTxBlock should reflect the merged legacy value" - ); - assert!( - sp_io::storage::get(&legacy_last_key).is_none(), - "Legacy LastTxBlock entry should be cleared" - ); - - assert_eq!( - SubtensorModule::get_last_tx_block_childkey_take(&legacy_child_account), - 333u64, - "Child key take block should be migrated" - ); - assert!( - sp_io::storage::get(&legacy_child_key).is_none(), - "Legacy child take entry should be cleared" - ); - - assert!( - sp_io::storage::get(&legacy_delegate_key).is_none(), - "Legacy delegate take entry should be cleared" - ); - - // New-format entries remain untouched. - assert_eq!( - SubtensorModule::get_last_tx_block_childkey_take(&new_child_account), - 777u64, - "Existing child take entry should be preserved" - ); - }); -} - #[test] fn test_migrate_fix_staking_hot_keys() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/tests/swap_hotkey.rs b/pallets/subtensor/src/tests/swap_hotkey.rs index c53b232231..e2dd4f00ae 100644 --- a/pallets/subtensor/src/tests/swap_hotkey.rs +++ b/pallets/subtensor/src/tests/swap_hotkey.rs @@ -1322,44 +1322,6 @@ fn test_swap_hotkey_is_sn_owner_hotkey() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey -- test_swap_hotkey_swap_rate_limits --exact --nocapture -#[test] -fn test_swap_hotkey_swap_rate_limits() { - 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 = add_dynamic_network(&old_hotkey, &coldkey); - SubtensorModule::add_balance_to_coldkey_account(&coldkey, u64::MAX); - - let last_tx_block = 123; - let child_key_take_block = 8910; - - // Set the last tx block for the old hotkey - SubtensorModule::set_last_tx_block(&old_hotkey, last_tx_block); - // Set last childkey take block for the old hotkey - SubtensorModule::set_last_tx_block_childkey(&old_hotkey, child_key_take_block); - - // Perform the swap - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - None - )); - - // Check for new hotkey - assert_eq!( - SubtensorModule::get_last_tx_block(&new_hotkey), - last_tx_block - ); - assert_eq!( - SubtensorModule::get_last_tx_block_childkey_take(&new_hotkey), - child_key_take_block - ); - }); -} - #[test] fn test_swap_parent_hotkey_self_loops_in_pending() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index 9427e4affb..5243d23763 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -1370,46 +1370,6 @@ fn test_swap_hotkey_is_sn_owner_hotkey() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_swap_rate_limits --exact --nocapture -#[test] -fn test_swap_hotkey_swap_rate_limits() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let last_tx_block = 123; - let child_key_take_block = 8910; - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - SubtensorModule::add_balance_to_coldkey_account(&coldkey, u64::MAX); - - // Set the last tx block for the old hotkey - SubtensorModule::set_last_tx_block(&old_hotkey, last_tx_block); - // Set last childkey take block for the old hotkey - SubtensorModule::set_last_tx_block_childkey(&old_hotkey, child_key_take_block); - - // Perform the swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid) - ),); - - // Check for new hotkey - assert_eq!( - SubtensorModule::get_last_tx_block(&new_hotkey), - last_tx_block - ); - assert_eq!( - SubtensorModule::get_last_tx_block_childkey_take(&new_hotkey), - child_key_take_block - ); - }); -} - #[test] fn test_swap_owner_failed_interval_not_passed() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index ad544065d4..03d38dc9c3 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -222,9 +222,6 @@ impl Pallet { pub fn set_last_tx_block(key: &T::AccountId, block: u64) { Self::set_rate_limited_last_block(&RateLimitKey::LastTxBlock(key.clone()), block); } - pub fn get_last_tx_block(key: &T::AccountId) -> u64 { - Self::get_rate_limited_last_block(&RateLimitKey::LastTxBlock(key.clone())) - } pub fn remove_last_tx_block_delegate_take(key: &T::AccountId) { Self::remove_rate_limited_last_block(&RateLimitKey::LastTxBlockDelegateTake(key.clone())) From 744bfb93e6d6508709bc5a44daf78b4179f3a32e Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 22 Jan 2026 17:42:40 +0100 Subject: [PATCH 89/95] Remove set_ and remove_last_tx_block --- pallets/subtensor/src/swap/swap_coldkey.rs | 4 --- pallets/subtensor/src/swap/swap_hotkey.rs | 26 +++++++------------ pallets/subtensor/src/tests/swap_hotkey.rs | 1 - .../src/tests/swap_hotkey_with_subnet.rs | 1 - pallets/subtensor/src/utils/rate_limiting.rs | 7 ----- 5 files changed, 9 insertions(+), 30 deletions(-) diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index c81138b58c..06c4b56941 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -73,10 +73,6 @@ impl Pallet { // 9. Perform the actual coldkey swap let _ = Self::perform_swap_coldkey(old_coldkey, new_coldkey, &mut weight); - // 10. Update the last transaction block for the new coldkey - Self::set_last_tx_block(new_coldkey, Self::get_current_block_as_u64()); - weight.saturating_accrue(T::DbWeight::get().writes(1)); - // 11. Remove the coldkey swap scheduled record ColdkeySwapScheduled::::remove(old_coldkey); diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 65ea6dd19f..1a36945a5c 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -2,7 +2,6 @@ use super::*; use frame_support::weights::Weight; use rate_limiting_interface::RateLimitingInterface; use sp_core::Get; -use sp_runtime::traits::SaturatedConversion; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{ Currency, MechId, NetUid, @@ -51,11 +50,6 @@ impl Pallet { // 4. Ensure the new hotkey is different from the old one ensure!(old_hotkey != new_hotkey, Error::::NewHotKeyIsSameWithOld); - // 5. Get the current block number - let block: u64 = Self::get_current_block_as_u64(); - - weight.saturating_accrue(T::DbWeight::get().reads(2)); - // 7. Ensure the new hotkey is not already registered on any network ensure!( !Self::is_hotkey_registered_on_any_network(new_hotkey), @@ -65,14 +59,12 @@ impl Pallet { // 8. Swap last-seen let last_tx_block = T::RateLimiting::last_seen( rate_limiting::GROUP_SWAP_KEYS, - Some(RateLimitUsageKey::Account(old_hotkey)), - ) - .unwrap_or_default(); - let last_seen = (last_tx_block != 0).then(|| last_tx_block.saturated_into()); + Some(RateLimitUsageKey::Account(old_hotkey.clone())), + ); T::RateLimiting::set_last_seen( rate_limiting::GROUP_SWAP_KEYS, Some(RateLimitUsageKey::Account(new_hotkey.clone())), - last_seen, + last_tx_block, ); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); @@ -110,10 +102,6 @@ impl Pallet { // 19. Perform the hotkey swap Self::perform_hotkey_swap_on_all_subnets(old_hotkey, new_hotkey, &coldkey, &mut weight)?; - // 20. Update the last transaction block for the coldkey - Self::set_last_tx_block(&coldkey, block); - weight.saturating_accrue(T::DbWeight::get().writes(1)); - // 21. Emit an event for the hotkey swap Self::deposit_event(Event::HotkeySwapped { coldkey, @@ -196,8 +184,12 @@ impl Pallet { // 6. Swap LastTxBlock // LastTxBlock( hotkey ) --> u64 -- the last transaction block for the hotkey. - Self::remove_last_tx_block(old_hotkey); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); + T::RateLimiting::set_last_seen( + rate_limiting::GROUP_SWAP_KEYS, + Some(RateLimitUsageKey::Account(old_hotkey.clone())), + None, + ); + weight.saturating_accrue(T::DbWeight::get().writes(1)); // 7. Swap LastTxBlockDelegateTake // LastTxBlockDelegateTake( hotkey ) --> u64 -- the last transaction block for the hotkey delegate take. diff --git a/pallets/subtensor/src/tests/swap_hotkey.rs b/pallets/subtensor/src/tests/swap_hotkey.rs index e2dd4f00ae..7500b3e919 100644 --- a/pallets/subtensor/src/tests/swap_hotkey.rs +++ b/pallets/subtensor/src/tests/swap_hotkey.rs @@ -945,7 +945,6 @@ fn test_swap_hotkey_error_cases() { // Set up initial state Owner::::insert(old_hotkey, coldkey); TotalNetworks::::put(1); - SubtensorModule::set_last_tx_block(&coldkey, 0); // Test not enough balance let swap_cost = SubtensorModule::get_key_swap_cost(); diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index 5243d23763..e1f816d199 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -937,7 +937,6 @@ fn test_swap_hotkey_error_cases() { // Set up initial state Owner::::insert(old_hotkey, coldkey); TotalNetworks::::put(1); - SubtensorModule::set_last_tx_block(&coldkey, 0); // Test not enough balance let swap_cost = SubtensorModule::get_key_swap_cost(); diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index 03d38dc9c3..946d980d57 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -216,13 +216,6 @@ impl Pallet { // ==== Rate Limiting ===== // ======================== - pub fn remove_last_tx_block(key: &T::AccountId) { - Self::remove_rate_limited_last_block(&RateLimitKey::LastTxBlock(key.clone())) - } - pub fn set_last_tx_block(key: &T::AccountId, block: u64) { - Self::set_rate_limited_last_block(&RateLimitKey::LastTxBlock(key.clone()), block); - } - pub fn remove_last_tx_block_delegate_take(key: &T::AccountId) { Self::remove_rate_limited_last_block(&RateLimitKey::LastTxBlockDelegateTake(key.clone())) } From 787c5b16af0f47034ada7d47a40ba305ae3c5d3e Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 23 Jan 2026 16:16:15 +0100 Subject: [PATCH 90/95] Remove storage, event, error and config associated with swap-keys rate-limiting --- chain-extensions/src/mock.rs | 2 -- pallets/admin-utils/src/tests/mock.rs | 2 -- pallets/subtensor/src/lib.rs | 13 ------------- pallets/subtensor/src/macros/config.rs | 3 --- pallets/subtensor/src/macros/errors.rs | 2 -- pallets/subtensor/src/macros/events.rs | 2 -- pallets/subtensor/src/swap/swap_hotkey.rs | 1 - pallets/subtensor/src/tests/mock.rs | 2 -- pallets/transaction-fee/src/tests/mock.rs | 2 -- runtime/src/lib.rs | 1 - 10 files changed, 30 deletions(-) diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 6fb741f1dd..7e1eb85b2d 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -294,7 +294,6 @@ parameter_types! { pub const InitialMinChildKeyTake: u16 = 0; // 0 %; pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; - pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 1; // 1 block take rate limit for testing pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -377,7 +376,6 @@ impl pallet_subtensor::Config for Test { type InitialWeightsVersionKey = InitialWeightsVersionKey; type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; - type InitialTxRateLimit = InitialTxRateLimit; type InitialBurn = InitialBurn; type InitialMaxBurn = InitialMaxBurn; type InitialMinBurn = InitialMinBurn; diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 6a80c0e15f..2a10dcbfb8 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -104,7 +104,6 @@ parameter_types! { pub const InitialMinChildKeyTake: u16 = 0; // Allow 0 % pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; - pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -187,7 +186,6 @@ impl pallet_subtensor::Config for Test { type InitialWeightsVersionKey = InitialWeightsVersionKey; type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; - type InitialTxRateLimit = InitialTxRateLimit; type InitialTxChildKeyTakeRateLimit = InitialTxChildKeyTakeRateLimit; type InitialBurn = InitialBurn; type InitialMaxBurn = InitialMaxBurn; diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index b7fa01ef38..504aa3bafd 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -865,15 +865,6 @@ pub mod pallet { T::AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) .expect("trailing zeroes always produce a valid account ID; qed") } - // pub fn DefaultHotkeyEmissionTempo() -> u64 { - // T::InitialHotkeyEmissionTempo::get() - // } (DEPRECATED) - - /// Default value for rate limiting - #[pallet::type_value] - pub fn DefaultTxRateLimit() -> u64 { - T::InitialTxRateLimit::get() - } /// Default value for chidlkey take rate limiting #[pallet::type_value] @@ -1819,10 +1810,6 @@ pub mod pallet { DefaultRAORecycledForRegistration, >; - /// --- ITEM ( tx_rate_limit ) - #[pallet::storage] - pub type TxRateLimit = StorageValue<_, u64, ValueQuery, DefaultTxRateLimit>; - /// --- ITEM ( tx_childkey_take_rate_limit ) #[pallet::storage] pub type TxChildkeyTakeRateLimit = diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index 3cbe846eb6..93be12b361 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -183,9 +183,6 @@ mod config { /// Initial weights version key. #[pallet::constant] type InitialWeightsVersionKey: Get; - /// Initial transaction rate limit. - #[pallet::constant] - type InitialTxRateLimit: Get; /// Initial childkey take transaction rate limit. #[pallet::constant] type InitialTxChildKeyTakeRateLimit: Get; diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index e2540d02ee..db4eb48a5d 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -86,8 +86,6 @@ mod errors { UidsLengthExceedUidsInSubNet, // 32 /// A transactor exceeded the rate limit for add network transaction. NetworkTxRateLimitExceeded, - /// A transactor exceeded the rate limit for setting or swapping hotkey. - HotKeySetTxRateLimitExceeded, /// A transactor exceeded the rate limit for staking. StakingRateLimitExceeded, /// Registration is disabled. diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 6b2df74010..1f184dbc47 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -105,8 +105,6 @@ mod events { MaxBurnSet(NetUid, TaoCurrency), /// setting min burn on a network. MinBurnSet(NetUid, TaoCurrency), - /// setting the transaction rate limit. - TxRateLimitSet(u64), /// setting the childkey take transaction rate limit. TxChildKeyTakeRateLimitSet(u64), /// setting the admin freeze window length (last N blocks of tempo) diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 1a36945a5c..be9c785580 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -25,7 +25,6 @@ impl Pallet { /// # Errors /// /// * `NonAssociatedColdKey` - If the coldkey does not own the old hotkey. - /// * `HotKeySetTxRateLimitExceeded` - If the transaction rate limit is exceeded. /// * `NewHotKeyIsSameWithOld` - If the new hotkey is the same as the old hotkey. /// * `HotKeyAlreadyRegisteredInSubNet` - If the new hotkey is already registered in the subnet. /// * `NotEnoughBalanceToPaySwapHotKey` - If there is not enough balance to pay for the swap. diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index ea2f355bc1..255ce1a93d 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -179,7 +179,6 @@ parameter_types! { pub const InitialMinChildKeyTake: u16 = 0; // 0 %; pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; - pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 1; // 1 block take rate limit for testing pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -262,7 +261,6 @@ impl crate::Config for Test { type InitialWeightsVersionKey = InitialWeightsVersionKey; type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; - type InitialTxRateLimit = InitialTxRateLimit; type InitialBurn = InitialBurn; type InitialMaxBurn = InitialMaxBurn; type InitialMinBurn = InitialMinBurn; diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 7526bb412e..9cc9927815 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -171,7 +171,6 @@ parameter_types! { pub const InitialMinChildKeyTake: u16 = 0; // Allow 0 % pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; pub const InitialWeightsVersionKey: u16 = 0; - pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialTxChildKeyTakeRateLimit: u64 = 0; // Disable rate limit for testing pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -254,7 +253,6 @@ impl pallet_subtensor::Config for Test { type InitialWeightsVersionKey = InitialWeightsVersionKey; type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; - type InitialTxRateLimit = InitialTxRateLimit; type InitialTxChildKeyTakeRateLimit = InitialTxChildKeyTakeRateLimit; type InitialBurn = InitialBurn; type InitialMaxBurn = InitialMaxBurn; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ba17fb557d..bc821c2c29 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1110,7 +1110,6 @@ impl pallet_subtensor::Config for Runtime { type InitialMinBurn = SubtensorInitialMinBurn; type MinBurnUpperBound = MinBurnUpperBound; type MaxBurnLowerBound = MaxBurnLowerBound; - type InitialTxRateLimit = SubtensorInitialTxRateLimit; type InitialTxChildKeyTakeRateLimit = SubtensorInitialTxChildKeyTakeRateLimit; type InitialMaxChildKeyTake = SubtensorInitialMaxChildKeyTake; type InitialRAORecycledForRegistration = SubtensorInitialRAORecycledForRegistration; From 646f59a5b4aaa7e2405ce090999e264c13d7d903 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 23 Jan 2026 17:49:35 +0100 Subject: [PATCH 91/95] Add integration tests for swap-keys rate-limiting group --- pallets/admin-utils/src/lib.rs | 5 +- pallets/subtensor/src/swap/swap_hotkey.rs | 3 + runtime/src/migrations/rate_limiting.rs | 5 + runtime/tests/rate_limiting.rs | 300 +++++++++++++++++++++- 4 files changed, 308 insertions(+), 5 deletions(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 5ffa884e39..893d8ed734 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -220,7 +220,10 @@ pub mod pallet { #[deprecated( note = "deprecated: configure via pallet-rate-limiting::set_rate_limit(target=Group(GROUP_SWAP_KEYS), ...)" )] - pub fn sudo_set_tx_rate_limit(origin: OriginFor, tx_rate_limit: u64) -> DispatchResult { + pub fn sudo_set_tx_rate_limit( + _origin: OriginFor, + _tx_rate_limit: u64, + ) -> DispatchResult { Err(Error::::Deprecated.into()) } diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index be9c785580..a81ff53319 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -243,6 +243,9 @@ impl Pallet { let hotkey_swap_interval = T::HotkeySwapOnSubnetInterval::get(); let last_hotkey_swap_block = LastHotkeySwapOnNetuid::::get(netuid, coldkey); + // NOTE: This subnet interval gate is legacy swap-keys rate-limiting group behavior and + // remains in pallet-subtensor; it is not migrated into pallet-rate-limiting because that + // system supports only a single span per target. ensure!( last_hotkey_swap_block.saturating_add(hotkey_swap_interval) < block, Error::::HotKeySwapOnSubnetIntervalNotPassed diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 2235b2e1d3..7cb6cdc5c8 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -592,6 +592,11 @@ fn build_staking_ops(groups: &mut Vec, commits: &mut Vec) - // usage. // usage: account (coldkey) // legacy sources: TxRateLimit, LastRateLimitedBlock per LastTxBlock +// +// NOTE: HotkeySwapOnSubnetInterval (per coldkey+netuid) remains enforced in pallet-subtensor +// (LastHotkeySwapOnNetuid). It is a separate legacy gate with its own span, and +// pallet-rate-limiting currently supports only one span per target, so we do not migrate it into +// this group. fn build_swap_keys(groups: &mut Vec, commits: &mut Vec) -> u64 { let mut reads: u64 = 0; groups.push(GroupConfig { diff --git a/runtime/tests/rate_limiting.rs b/runtime/tests/rate_limiting.rs index e2e216bd34..be7f4e25b4 100644 --- a/runtime/tests/rate_limiting.rs +++ b/runtime/tests/rate_limiting.rs @@ -3,10 +3,13 @@ use codec::{Compact, Encode}; use frame_support::{assert_ok, traits::Get}; use node_subtensor_runtime::{ - Executive, Runtime, RuntimeCall, SignedPayload, SubtensorInitialTxDelegateTakeRateLimit, - System, TransactionExtensions, UncheckedExtrinsic, check_nonce, - rate_limiting::legacy::storage as legacy_storage, sudo_wrapper, transaction_payment_wrapper, + Executive, HotkeySwapOnSubnetInterval, Runtime, RuntimeCall, SignedPayload, + SubtensorInitialTxDelegateTakeRateLimit, System, TransactionExtensions, UncheckedExtrinsic, + check_nonce, + rate_limiting::legacy::{RateLimitKey, storage as legacy_storage}, + sudo_wrapper, transaction_payment_wrapper, }; +use pallet_rate_limiting::RateLimitTarget; use sp_core::{H256, Pair, sr25519}; use sp_runtime::{ BoundedVec, MultiSignature, @@ -14,7 +17,10 @@ use sp_runtime::{ traits::SaturatedConversion, transaction_validity::{InvalidTransaction, TransactionValidityError}, }; -use subtensor_runtime_common::{AccountId, AlphaCurrency, Currency, MechId, NetUid}; +use subtensor_runtime_common::{ + AccountId, AlphaCurrency, Currency, MechId, NetUid, + rate_limiting::{GROUP_SWAP_KEYS, RateLimitUsageKey}, +}; use common::ExtBuilder; @@ -26,6 +32,17 @@ fn assert_extrinsic_ok(account_id: &AccountId, pair: &sr25519::Pair, call: Runti assert_ok!(Executive::apply_extrinsic(xt)); } +fn assert_sudo_extrinsic_ok( + sudo_account: &AccountId, + sudo_pair: &sr25519::Pair, + call: RuntimeCall, +) { + let sudo_call = RuntimeCall::Sudo(pallet_sudo::Call::sudo { + call: Box::new(call), + }); + assert_extrinsic_ok(sudo_account, sudo_pair, sudo_call); +} + fn assert_extrinsic_rate_limited(account_id: &AccountId, pair: &sr25519::Pair, call: RuntimeCall) { let nonce = System::account(account_id).nonce; let xt = signed_extrinsic(call, pair, nonce); @@ -65,6 +82,11 @@ fn signed_extrinsic(call: RuntimeCall, pair: &sr25519::Pair, nonce: u32) -> Unch UncheckedExtrinsic::new_signed(call, address, signature, extra) } +fn setup_swap_hotkey_state(netuid: NetUid, coldkey: &AccountId, hotkey: &AccountId, block: u64) { + setup_weights_network(netuid, hotkey, block, 1); + pallet_subtensor::Pallet::::create_account_if_non_existent(coldkey, hotkey); +} + fn setup_weights_network(netuid: NetUid, hotkey: &AccountId, block: u64, mechanisms: u8) { pallet_subtensor::Pallet::::init_new_network(netuid, 1); if mechanisms > 1 { @@ -816,3 +838,273 @@ fn swap_stake_limits_destination_netuid() { assert_extrinsic_ok(&coldkey, &coldkey_pair, remove_origin); }); } + +#[test] +fn swap_hotkey_tx_rate_limit_exceeded_all_subnets() { + let coldkey_pair = sr25519::Pair::from_seed(&[30u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let old_hotkey = AccountId::from([31u8; 32]); + let new_hotkey_a = AccountId::from([32u8; 32]); + let new_hotkey_b = AccountId::from([33u8; 32]); + let netuid = NetUid::from(20u16); + let balance = 10_000_000_000_000_u64; + let legacy_span = 3u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); + + legacy_storage::set_tx_rate_limit(legacy_span); + Executive::execute_on_runtime_upgrade(); + + let swap_first = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey_a.clone(), + netuid: None, + }); + let swap_second = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: new_hotkey_a.clone(), + new_hotkey: new_hotkey_b.clone(), + netuid: None, + }); + + let start_block: u64 = System::block_number().saturated_into(); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_first); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, swap_second.clone()); + + let limit_block = start_block.saturating_add(legacy_span); + System::set_block_number(limit_block.saturated_into()); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, swap_second.clone()); + + System::set_block_number((limit_block + 1).saturated_into()); + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_second); + }); +} + +#[test] +fn swap_hotkey_tx_rate_limit_exceeded_on_subnet() { + let coldkey_pair = sr25519::Pair::from_seed(&[34u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let old_hotkey = AccountId::from([35u8; 32]); + let new_hotkey_a = AccountId::from([36u8; 32]); + let new_hotkey_b = AccountId::from([37u8; 32]); + let netuid = NetUid::from(21u16); + let balance = 10_000_000_000_000_u64; + let legacy_span = 3u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); + + legacy_storage::set_tx_rate_limit(legacy_span); + Executive::execute_on_runtime_upgrade(); + + let interval: u64 = HotkeySwapOnSubnetInterval::get().saturated_into(); + let start_block = interval.saturating_add(1); + System::set_block_number(start_block.saturated_into()); + + let swap_subnet = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey_a.clone(), + netuid: Some(netuid), + }); + let swap_subnet_again = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey_a.clone(), + netuid: Some(netuid), + }); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_subnet); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, swap_subnet_again); + + let limit_block = start_block.saturating_add(legacy_span + 1); + System::set_block_number(limit_block.saturated_into()); + + let swap_all = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: new_hotkey_a.clone(), + new_hotkey: new_hotkey_b.clone(), + netuid: None, + }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_all); + }); +} + +#[test] +fn swap_hotkey_transfers_last_seen_all_subnets() { + let coldkey_pair = sr25519::Pair::from_seed(&[38u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let old_hotkey = AccountId::from([39u8; 32]); + let new_hotkey = AccountId::from([40u8; 32]); + let netuid = NetUid::from(22u16); + let balance = 10_000_000_000_000_u64; + let legacy_last_seen = 7u64; + let childkey_last_seen = 91u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); + + legacy_storage::set_last_rate_limited_block( + RateLimitKey::LastTxBlock(old_hotkey.clone()), + legacy_last_seen, + ); + pallet_subtensor::Pallet::::set_last_tx_block_childkey( + &old_hotkey, + childkey_last_seen, + ); + + Executive::execute_on_runtime_upgrade(); + + let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); + let usage_old = RateLimitUsageKey::Account(old_hotkey.clone()); + let usage_new = RateLimitUsageKey::Account(new_hotkey.clone()); + assert_eq!( + pallet_rate_limiting::LastSeen::::get(target, Some(usage_old.clone())), + Some(legacy_last_seen.saturated_into()) + ); + + System::set_block_number(10); + let swap_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey.clone(), + netuid: None, + }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_call); + + assert_eq!( + pallet_rate_limiting::LastSeen::::get(target, Some(usage_new)), + Some(legacy_last_seen.saturated_into()) + ); + assert_eq!( + pallet_rate_limiting::LastSeen::::get(target, Some(usage_old)), + None + ); + assert_eq!( + pallet_subtensor::Pallet::::get_last_tx_block_childkey_take(&new_hotkey), + childkey_last_seen + ); + }); +} + +#[test] +fn swap_hotkey_transfers_last_seen_on_subnet() { + let coldkey_pair = sr25519::Pair::from_seed(&[41u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let old_hotkey = AccountId::from([42u8; 32]); + let new_hotkey = AccountId::from([43u8; 32]); + let netuid = NetUid::from(23u16); + let balance = 10_000_000_000_000_u64; + let legacy_last_seen = 9u64; + let childkey_last_seen = 97u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); + + legacy_storage::set_last_rate_limited_block( + RateLimitKey::LastTxBlock(old_hotkey.clone()), + legacy_last_seen, + ); + pallet_subtensor::Pallet::::set_last_tx_block_childkey( + &old_hotkey, + childkey_last_seen, + ); + + Executive::execute_on_runtime_upgrade(); + + let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); + let usage_new = RateLimitUsageKey::Account(new_hotkey.clone()); + + let interval: u64 = HotkeySwapOnSubnetInterval::get().saturated_into(); + System::set_block_number(interval.saturating_add(1).saturated_into()); + + let swap_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey.clone(), + netuid: Some(netuid), + }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_call); + + assert_eq!( + pallet_rate_limiting::LastSeen::::get(target, Some(usage_new)), + Some(legacy_last_seen.saturated_into()) + ); + assert_eq!( + pallet_subtensor::Pallet::::get_last_tx_block_childkey_take(&new_hotkey), + childkey_last_seen + ); + }); +} + +// NOTE: This currently fails. When `swap_coldkey` is dispatched via `Sudo::sudo`, rate-limiting +// sees the outer sudo call, so `swap_coldkey` does not record usage in the swap-keys group. Keep +// this test to flag the issue until the rate-limiting extension unwraps sudo calls. +#[test] +fn swap_coldkey_records_usage_for_swap_keys_group() { + let sudo_pair = sr25519::Pair::from_seed(&[44u8; 32]); + let new_coldkey_pair = sr25519::Pair::from_seed(&[45u8; 32]); + let sudo_account = AccountId::from(sudo_pair.public()); + let old_coldkey = AccountId::from([46u8; 32]); + let new_coldkey = AccountId::from(new_coldkey_pair.public()); + let old_hotkey = AccountId::from([47u8; 32]); + let new_hotkey = AccountId::from([48u8; 32]); + let balance = 10_000_000_000_000_u64; + let legacy_span = 3u64; + let swap_cost = 1u64; + + ExtBuilder::default() + .with_balances(vec![ + (sudo_account.clone(), balance), + (old_coldkey.clone(), balance), + (new_coldkey.clone(), balance), + ]) + .build() + .execute_with(|| { + System::set_block_number(10); + pallet_sudo::Key::::put(sudo_account.clone()); + legacy_storage::set_tx_rate_limit(legacy_span); + Executive::execute_on_runtime_upgrade(); + + let swap_coldkey_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_coldkey { + old_coldkey: old_coldkey.clone(), + new_coldkey: new_coldkey.clone(), + swap_cost: swap_cost.into(), + }); + assert_sudo_extrinsic_ok(&sudo_account, &sudo_pair, swap_coldkey_call); + + let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); + let usage_new = RateLimitUsageKey::Account(new_coldkey.clone()); + assert_eq!( + pallet_rate_limiting::LastSeen::::get(target, Some(usage_new.clone())), + Some(10u64.into()) + ); + + pallet_subtensor::Pallet::::create_account_if_non_existent( + &new_coldkey, + &old_hotkey, + ); + + let swap_hotkey_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey.clone(), + netuid: None, + }); + assert_extrinsic_rate_limited(&new_coldkey, &new_coldkey_pair, swap_hotkey_call); + }); +} From b1174ed62354faca59764d67ca6d39a2018215fc Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 23 Jan 2026 18:03:53 +0100 Subject: [PATCH 92/95] Restructure rate-limiting integration tests by rate-limiting groups --- runtime/tests/rate_limiting.rs | 1883 ++++++++++++++++---------------- 1 file changed, 970 insertions(+), 913 deletions(-) diff --git a/runtime/tests/rate_limiting.rs b/runtime/tests/rate_limiting.rs index be7f4e25b4..7827624f6f 100644 --- a/runtime/tests/rate_limiting.rs +++ b/runtime/tests/rate_limiting.rs @@ -82,131 +82,95 @@ fn signed_extrinsic(call: RuntimeCall, pair: &sr25519::Pair, nonce: u32) -> Unch UncheckedExtrinsic::new_signed(call, address, signature, extra) } -fn setup_swap_hotkey_state(netuid: NetUid, coldkey: &AccountId, hotkey: &AccountId, block: u64) { - setup_weights_network(netuid, hotkey, block, 1); - pallet_subtensor::Pallet::::create_account_if_non_existent(coldkey, hotkey); -} - -fn setup_weights_network(netuid: NetUid, hotkey: &AccountId, block: u64, mechanisms: u8) { - pallet_subtensor::Pallet::::init_new_network(netuid, 1); - if mechanisms > 1 { - pallet_subtensor::MechanismCountCurrent::::insert( - netuid, - MechId::from(mechanisms), - ); - } - System::set_block_number(block.saturated_into()); - pallet_subtensor::Pallet::::append_neuron(netuid, hotkey, block); -} - -fn setup_staking_network(netuid: NetUid) { - pallet_subtensor::Pallet::::init_new_network(netuid, 1); - pallet_subtensor::SubtokenEnabled::::insert(netuid, true); - pallet_subtensor::TransferToggle::::insert(netuid, true); -} - -fn seed_stake(netuid: NetUid, hotkey: &AccountId, coldkey: &AccountId, alpha: u64) { - pallet_subtensor::Pallet::::create_account_if_non_existent(coldkey, hotkey); - pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( - hotkey, - coldkey, - netuid, - AlphaCurrency::from(alpha), - ); -} - -#[test] -fn register_network_is_rate_limited_after_migration() { - let coldkey_pair = sr25519::Pair::from_seed(&[1u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let hotkey_a = AccountId::from([2u8; 32]); - let hotkey_b = AccountId::from([3u8; 32]); - let balance = 10_000_000_000_000_u64; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), balance)]) - .build() - .execute_with(|| { - System::set_block_number(1); - - // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. - Executive::execute_on_runtime_upgrade(); - - let call_a = RuntimeCall::SubtensorModule(pallet_subtensor::Call::register_network { - hotkey: hotkey_a, - }); - let call_b = RuntimeCall::SubtensorModule( - pallet_subtensor::Call::register_network_with_identity { - hotkey: hotkey_b, - identity: None, - }, - ); - let start_block = - pallet_subtensor::NetworkRegistrationStartBlock::::get().saturated_into(); +mod register_network { + use super::*; + + #[test] + fn register_network_is_rate_limited_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[1u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey_a = AccountId::from([2u8; 32]); + let hotkey_b = AccountId::from([3u8; 32]); + let balance = 10_000_000_000_000_u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + + // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. + Executive::execute_on_runtime_upgrade(); + + let call_a = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::register_network { + hotkey: hotkey_a, + }); + let call_b = RuntimeCall::SubtensorModule( + pallet_subtensor::Call::register_network_with_identity { + hotkey: hotkey_b, + identity: None, + }, + ); + let start_block = pallet_subtensor::NetworkRegistrationStartBlock::::get() + .saturated_into(); - System::set_block_number(start_block); + System::set_block_number(start_block); - assert_extrinsic_ok(&coldkey, &coldkey_pair, call_a.clone()); + assert_extrinsic_ok(&coldkey, &coldkey_pair, call_a.clone()); - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, call_b.clone()); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, call_b.clone()); - // Migration sets register-network limit to 4 days (28_800 blocks). - let limit = start_block + 28_800; + // Migration sets register-network limit to 4 days (28_800 blocks). + let limit = start_block + 28_800; - // Should still be rate-limited. - System::set_block_number(limit - 1); - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, call_a.clone()); + // Should still be rate-limited. + System::set_block_number(limit - 1); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, call_a.clone()); - // Should pass now. - System::set_block_number(limit); - assert_extrinsic_ok(&coldkey, &coldkey_pair, call_b); + // Should pass now. + System::set_block_number(limit); + assert_extrinsic_ok(&coldkey, &coldkey_pair, call_b); - // Both calls share the same usage key and window. - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, call_a.clone()); + // Both calls share the same usage key and window. + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, call_a.clone()); - System::set_block_number(limit + 28_800); - assert_extrinsic_ok(&coldkey, &coldkey_pair, call_a); - }); + System::set_block_number(limit + 28_800); + assert_extrinsic_ok(&coldkey, &coldkey_pair, call_a); + }); + } } -#[test] -fn serving_is_rate_limited_after_migration() { - let coldkey_pair = sr25519::Pair::from_seed(&[4u8; 32]); - let hotkey_pair = sr25519::Pair::from_seed(&[5u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let hotkey = AccountId::from(hotkey_pair.public()); - let balance = 10_000_000_000_000_u64; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), balance)]) - .build() - .execute_with(|| { - System::set_block_number(1); - // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. - Executive::execute_on_runtime_upgrade(); - - assert_extrinsic_ok( - &coldkey, - &coldkey_pair, - RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { - hotkey: hotkey.clone(), - }), - ); +mod serving { + use super::*; + + #[test] + fn serving_is_rate_limited_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[4u8; 32]); + let hotkey_pair = sr25519::Pair::from_seed(&[5u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from(hotkey_pair.public()); + let balance = 10_000_000_000_000_u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. + Executive::execute_on_runtime_upgrade(); + + assert_extrinsic_ok( + &coldkey, + &coldkey_pair, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { + hotkey: hotkey.clone(), + }), + ); - let netuid = NetUid::ROOT; - let start_block = System::block_number(); - let serve_axon = RuntimeCall::SubtensorModule(pallet_subtensor::Call::serve_axon { - netuid, - version: 1, - ip: 0, - port: 3030, - ip_type: 4, - protocol: 0, - placeholder1: 0, - placeholder2: 0, - }); - let serve_axon_tls = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::serve_axon_tls { + let netuid = NetUid::ROOT; + let start_block = System::block_number(); + let serve_axon = RuntimeCall::SubtensorModule(pallet_subtensor::Call::serve_axon { netuid, version: 1, ip: 0, @@ -215,896 +179,989 @@ fn serving_is_rate_limited_after_migration() { protocol: 0, placeholder1: 0, placeholder2: 0, - certificate: b"cert".to_vec(), - }); - let serve_prometheus = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::serve_prometheus { - netuid, - version: 1, - ip: 1_676_056_785, - port: 3031, - ip_type: 4, }); + let serve_axon_tls = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::serve_axon_tls { + netuid, + version: 1, + ip: 0, + port: 3030, + ip_type: 4, + protocol: 0, + placeholder1: 0, + placeholder2: 0, + certificate: b"cert".to_vec(), + }); + let serve_prometheus = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::serve_prometheus { + netuid, + version: 1, + ip: 1_676_056_785, + port: 3031, + ip_type: 4, + }); + + assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_axon.clone()); + + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_axon_tls.clone()); + + assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_prometheus.clone()); + + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_prometheus.clone()); + + // Migration sets serving limit to 50 blocks by default. + let limit = start_block + 50; + + // Should still be rate-limited. + System::set_block_number(limit - 1); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_axon.clone()); + + // Should pass now. + System::set_block_number(limit); + assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_axon_tls); + + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_axon); + + assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_prometheus.clone()); + + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_prometheus); + }); + } +} - assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_axon.clone()); +mod delegate_take { + use super::*; + + #[test] + fn delegate_take_increase_is_rate_limited_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[6u8; 32]); + let hotkey_pair = sr25519::Pair::from_seed(&[7u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from(hotkey_pair.public()); + let balance = 10_000_000_000_000_u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. + Executive::execute_on_runtime_upgrade(); + + assert_extrinsic_ok( + &coldkey, + &coldkey_pair, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { + hotkey: hotkey.clone(), + }), + ); - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_axon_tls.clone()); + // Seed current take so increase_take passes take checks. + pallet_subtensor::Delegates::::insert(&hotkey, 1u16); - assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_prometheus.clone()); + let increase_once = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::increase_take { + hotkey: hotkey.clone(), + take: 2u16, + }); + let increase_twice = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::increase_take { + hotkey: hotkey.clone(), + take: 3u16, + }); - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_prometheus.clone()); + let start_block = System::block_number(); - // Migration sets serving limit to 50 blocks by default. - let limit = start_block + 50; + assert_extrinsic_ok(&coldkey, &coldkey_pair, increase_once); - // Should still be rate-limited. - System::set_block_number(limit - 1); - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_axon.clone()); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase_twice.clone()); - // Should pass now. - System::set_block_number(limit); - assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_axon_tls); + let limit = SubtensorInitialTxDelegateTakeRateLimit::get(); + let limit_block = start_block + limit.saturated_into::(); + let allowed_block = limit_block + 1; - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_axon); + System::set_block_number(limit_block - 1); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase_twice.clone()); - assert_extrinsic_ok(&hotkey, &hotkey_pair, serve_prometheus.clone()); + System::set_block_number(allowed_block); + assert_extrinsic_ok(&coldkey, &coldkey_pair, increase_twice); + }); + } - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, serve_prometheus); - }); -} + #[test] + fn delegate_take_decrease_is_not_rate_limited_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[10u8; 32]); + let hotkey_pair = sr25519::Pair::from_seed(&[11u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from(hotkey_pair.public()); + let balance = 10_000_000_000_000_u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. + Executive::execute_on_runtime_upgrade(); + + assert_extrinsic_ok( + &coldkey, + &coldkey_pair, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { + hotkey: hotkey.clone(), + }), + ); -#[test] -fn delegate_take_increase_is_rate_limited_after_migration() { - let coldkey_pair = sr25519::Pair::from_seed(&[6u8; 32]); - let hotkey_pair = sr25519::Pair::from_seed(&[7u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let hotkey = AccountId::from(hotkey_pair.public()); - let balance = 10_000_000_000_000_u64; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), balance)]) - .build() - .execute_with(|| { - System::set_block_number(1); - // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. - Executive::execute_on_runtime_upgrade(); - - assert_extrinsic_ok( - &coldkey, - &coldkey_pair, - RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { - hotkey: hotkey.clone(), - }), - ); + // Seed current take so decreases are valid and deterministic. + pallet_subtensor::Delegates::::insert(&hotkey, 3u16); + + let decrease_once = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::decrease_take { + hotkey: hotkey.clone(), + take: 2u16, + }); + let decrease_twice = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::decrease_take { + hotkey: hotkey.clone(), + take: 1u16, + }); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, decrease_once); + assert_extrinsic_ok(&coldkey, &coldkey_pair, decrease_twice); + }); + } - // Seed current take so increase_take passes take checks. - pallet_subtensor::Delegates::::insert(&hotkey, 1u16); + #[test] + fn delegate_take_decrease_blocks_immediate_increase_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[8u8; 32]); + let hotkey_pair = sr25519::Pair::from_seed(&[9u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from(hotkey_pair.public()); + let balance = 10_000_000_000_000_u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. + Executive::execute_on_runtime_upgrade(); + + assert_extrinsic_ok( + &coldkey, + &coldkey_pair, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { + hotkey: hotkey.clone(), + }), + ); - let increase_once = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::increase_take { - hotkey: hotkey.clone(), - take: 2u16, - }); - let increase_twice = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::increase_take { - hotkey: hotkey.clone(), - take: 3u16, - }); + // Seed current take so decrease then increase remains valid. + pallet_subtensor::Delegates::::insert(&hotkey, 2u16); - let start_block = System::block_number(); + let decrease = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::decrease_take { + hotkey: hotkey.clone(), + take: 1u16, + }); + let increase = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::increase_take { + hotkey: hotkey.clone(), + take: 2u16, + }); - assert_extrinsic_ok(&coldkey, &coldkey_pair, increase_once); + let start_block = System::block_number(); - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase_twice.clone()); + assert_extrinsic_ok(&coldkey, &coldkey_pair, decrease); - let limit = SubtensorInitialTxDelegateTakeRateLimit::get(); - let limit_block = start_block + limit.saturated_into::(); - let allowed_block = limit_block + 1; + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase.clone()); - System::set_block_number(limit_block - 1); - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase_twice.clone()); + let limit = SubtensorInitialTxDelegateTakeRateLimit::get(); + let limit_block = start_block + limit.saturated_into::(); + let allowed_block = limit_block + 1; - System::set_block_number(allowed_block); - assert_extrinsic_ok(&coldkey, &coldkey_pair, increase_twice); - }); + System::set_block_number(limit_block - 1); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase.clone()); + + System::set_block_number(allowed_block); + assert_extrinsic_ok(&coldkey, &coldkey_pair, increase); + }); + } } -#[test] -fn delegate_take_decrease_is_not_rate_limited_after_migration() { - let coldkey_pair = sr25519::Pair::from_seed(&[10u8; 32]); - let hotkey_pair = sr25519::Pair::from_seed(&[11u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let hotkey = AccountId::from(hotkey_pair.public()); - let balance = 10_000_000_000_000_u64; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), balance)]) - .build() - .execute_with(|| { - System::set_block_number(1); - // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. - Executive::execute_on_runtime_upgrade(); - - assert_extrinsic_ok( - &coldkey, - &coldkey_pair, - RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { - hotkey: hotkey.clone(), - }), +mod weights { + use super::*; + + fn setup_weights_network(netuid: NetUid, hotkey: &AccountId, block: u64, mechanisms: u8) { + pallet_subtensor::Pallet::::init_new_network(netuid, 1); + if mechanisms > 1 { + pallet_subtensor::MechanismCountCurrent::::insert( + netuid, + MechId::from(mechanisms), ); + } + System::set_block_number(block.saturated_into()); + pallet_subtensor::Pallet::::append_neuron(netuid, hotkey, block); + } - // Seed current take so decreases are valid and deterministic. - pallet_subtensor::Delegates::::insert(&hotkey, 3u16); + #[test] + fn weights_set_is_rate_limited_after_migration() { + let hotkey_pair = sr25519::Pair::from_seed(&[12u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let netuid = NetUid::from(1u16); + let span = 3u64; + let registration_block = 1u64; + + ExtBuilder::default() + .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) + .build() + .execute_with(|| { + setup_weights_network(netuid, &hotkey, registration_block, 1); + legacy_storage::set_weights_set_rate_limit(netuid, span); + + Executive::execute_on_runtime_upgrade(); + + pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled( + netuid, false, + ); - let decrease_once = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::decrease_take { - hotkey: hotkey.clone(), - take: 2u16, - }); - let decrease_twice = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::decrease_take { - hotkey: hotkey.clone(), - take: 1u16, + let version_key = pallet_subtensor::WeightsVersionKey::::get(netuid); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights { + netuid, + dests: vec![0], + weights: vec![u16::MAX], + version_key, }); - assert_extrinsic_ok(&coldkey, &coldkey_pair, decrease_once); - assert_extrinsic_ok(&coldkey, &coldkey_pair, decrease_twice); - }); -} + System::set_block_number(registration_block.saturated_into()); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, call.clone()); -#[test] -fn delegate_take_decrease_blocks_immediate_increase_after_migration() { - let coldkey_pair = sr25519::Pair::from_seed(&[8u8; 32]); - let hotkey_pair = sr25519::Pair::from_seed(&[9u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let hotkey = AccountId::from(hotkey_pair.public()); - let balance = 10_000_000_000_000_u64; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), balance)]) - .build() - .execute_with(|| { - System::set_block_number(1); - // Run runtime upgrades explicitly so rate-limiting config is seeded for tests. - Executive::execute_on_runtime_upgrade(); - - assert_extrinsic_ok( - &coldkey, - &coldkey_pair, - RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { - hotkey: hotkey.clone(), - }), - ); + System::set_block_number((registration_block + span - 1).saturated_into()); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, call.clone()); - // Seed current take so decrease then increase remains valid. - pallet_subtensor::Delegates::::insert(&hotkey, 2u16); + System::set_block_number((registration_block + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, call.clone()); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, call.clone()); - let decrease = RuntimeCall::SubtensorModule(pallet_subtensor::Call::decrease_take { - hotkey: hotkey.clone(), - take: 1u16, - }); - let increase = RuntimeCall::SubtensorModule(pallet_subtensor::Call::increase_take { - hotkey: hotkey.clone(), - take: 2u16, + System::set_block_number((registration_block + span + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, call); }); + } - let start_block = System::block_number(); - - assert_extrinsic_ok(&coldkey, &coldkey_pair, decrease); - - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase.clone()); - - let limit = SubtensorInitialTxDelegateTakeRateLimit::get(); - let limit_block = start_block + limit.saturated_into::(); - let allowed_block = limit_block + 1; - - System::set_block_number(limit_block - 1); - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, increase.clone()); + #[test] + fn commit_weights_shares_rate_limit_with_set_weights() { + let hotkey_pair = sr25519::Pair::from_seed(&[13u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let netuid = NetUid::from(2u16); + let span = 4u64; + let registration_block = 1u64; + let commit_hash = H256::from_low_u64_be(42); + + ExtBuilder::default() + .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) + .build() + .execute_with(|| { + setup_weights_network(netuid, &hotkey, registration_block, 1); + legacy_storage::set_weights_set_rate_limit(netuid, span); + + Executive::execute_on_runtime_upgrade(); + + let commit_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::commit_weights { + netuid, + commit_hash, + }); + + System::set_block_number((registration_block + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_call); + + pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled( + netuid, false, + ); - System::set_block_number(allowed_block); - assert_extrinsic_ok(&coldkey, &coldkey_pair, increase); - }); -} + let version_key = pallet_subtensor::WeightsVersionKey::::get(netuid); + let set_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights { + netuid, + dests: vec![0], + weights: vec![u16::MAX], + version_key, + }); -#[test] -fn weights_set_is_rate_limited_after_migration() { - let hotkey_pair = sr25519::Pair::from_seed(&[12u8; 32]); - let hotkey = AccountId::from(hotkey_pair.public()); - let netuid = NetUid::from(1u16); - let span = 3u64; - let registration_block = 1u64; + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, set_call.clone()); - ExtBuilder::default() - .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) - .build() - .execute_with(|| { - setup_weights_network(netuid, &hotkey, registration_block, 1); - legacy_storage::set_weights_set_rate_limit(netuid, span); + System::set_block_number((registration_block + span + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, set_call); + }); + } - Executive::execute_on_runtime_upgrade(); + #[test] + fn commit_timelocked_weights_is_rate_limited_after_migration() { + let hotkey_pair = sr25519::Pair::from_seed(&[14u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let netuid = NetUid::from(3u16); + let span = 4u64; + let registration_block = 1u64; + let commit = BoundedVec::try_from(vec![1u8; 16]).expect("commit payload within limit"); + let reveal_round = 10u64; + + ExtBuilder::default() + .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) + .build() + .execute_with(|| { + setup_weights_network(netuid, &hotkey, registration_block, 1); + legacy_storage::set_weights_set_rate_limit(netuid, span); + + Executive::execute_on_runtime_upgrade(); + + let commit_reveal_version = + pallet_subtensor::Pallet::::get_commit_reveal_weights_version(); + let commit_call = RuntimeCall::SubtensorModule( + pallet_subtensor::Call::commit_timelocked_weights { + netuid, + commit: commit.clone(), + reveal_round, + commit_reveal_version, + }, + ); - pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, false); + System::set_block_number((registration_block + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_call.clone()); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, commit_call.clone()); - let version_key = pallet_subtensor::WeightsVersionKey::::get(netuid); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights { - netuid, - dests: vec![0], - weights: vec![u16::MAX], - version_key, + System::set_block_number((registration_block + span + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_call); }); + } - System::set_block_number(registration_block.saturated_into()); - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, call.clone()); + #[test] + fn commit_crv3_mechanism_weights_are_rate_limited_per_mechanism() { + let hotkey_pair = sr25519::Pair::from_seed(&[15u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let netuid = NetUid::from(4u16); + let span = 4u64; + let registration_block = 1u64; + let commit = BoundedVec::try_from(vec![1u8; 16]).expect("commit payload within limit"); + let reveal_round = 10u64; + let mecid_a = MechId::from(0u8); + let mecid_b = MechId::from(1u8); + + ExtBuilder::default() + .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) + .build() + .execute_with(|| { + setup_weights_network(netuid, &hotkey, registration_block, 2); + legacy_storage::set_weights_set_rate_limit(netuid, span); + + Executive::execute_on_runtime_upgrade(); + + let commit_a = RuntimeCall::SubtensorModule( + pallet_subtensor::Call::commit_crv3_mechanism_weights { + netuid, + mecid: mecid_a, + commit: commit.clone(), + reveal_round, + }, + ); + let commit_b = RuntimeCall::SubtensorModule( + pallet_subtensor::Call::commit_crv3_mechanism_weights { + netuid, + mecid: mecid_b, + commit: commit.clone(), + reveal_round, + }, + ); - System::set_block_number((registration_block + span - 1).saturated_into()); - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, call.clone()); + System::set_block_number((registration_block + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_a.clone()); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, commit_a); + assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_b); + }); + } - System::set_block_number((registration_block + span).saturated_into()); - assert_extrinsic_ok(&hotkey, &hotkey_pair, call.clone()); - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, call.clone()); + #[test] + fn batch_set_weights_is_rate_limited_if_any_scope_is_within_span() { + let hotkey_pair = sr25519::Pair::from_seed(&[16u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let netuid_a = NetUid::from(5u16); + let netuid_b = NetUid::from(6u16); + let span = 3u64; + let registration_block = 1u64; + + ExtBuilder::default() + .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) + .build() + .execute_with(|| { + setup_weights_network(netuid_a, &hotkey, registration_block, 1); + setup_weights_network(netuid_b, &hotkey, registration_block, 1); + legacy_storage::set_weights_set_rate_limit(netuid_a, span); + legacy_storage::set_weights_set_rate_limit(netuid_b, span); + + Executive::execute_on_runtime_upgrade(); + + pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled( + netuid_a, false, + ); + pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled( + netuid_b, false, + ); - System::set_block_number((registration_block + span + span).saturated_into()); - assert_extrinsic_ok(&hotkey, &hotkey_pair, call); - }); + let version_key_a = pallet_subtensor::WeightsVersionKey::::get(netuid_a); + let version_key_b = pallet_subtensor::WeightsVersionKey::::get(netuid_b); + + let set_call_a = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights { + netuid: netuid_a, + dests: vec![0], + weights: vec![u16::MAX], + version_key: version_key_a, + }); + + System::set_block_number((registration_block + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, set_call_a); + + let batch_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::batch_set_weights { + netuids: vec![Compact(netuid_a), Compact(netuid_b)], + weights: vec![ + vec![(Compact(0u16), Compact(1u16))], + vec![(Compact(0u16), Compact(1u16))], + ], + version_keys: vec![Compact(version_key_a), Compact(version_key_b)], + }); + + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, batch_call.clone()); + + System::set_block_number((registration_block + span + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, batch_call); + }); + } } -#[test] -fn commit_weights_shares_rate_limit_with_set_weights() { - let hotkey_pair = sr25519::Pair::from_seed(&[13u8; 32]); - let hotkey = AccountId::from(hotkey_pair.public()); - let netuid = NetUid::from(2u16); - let span = 4u64; - let registration_block = 1u64; - let commit_hash = H256::from_low_u64_be(42); - - ExtBuilder::default() - .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) - .build() - .execute_with(|| { - setup_weights_network(netuid, &hotkey, registration_block, 1); - legacy_storage::set_weights_set_rate_limit(netuid, span); - - Executive::execute_on_runtime_upgrade(); - - let commit_call = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::commit_weights { - netuid, - commit_hash, - }); - - System::set_block_number((registration_block + span).saturated_into()); - assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_call); +mod staking_ops { + use super::*; - pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, false); + fn setup_staking_network(netuid: NetUid) { + pallet_subtensor::Pallet::::init_new_network(netuid, 1); + pallet_subtensor::SubtokenEnabled::::insert(netuid, true); + pallet_subtensor::TransferToggle::::insert(netuid, true); + } - let version_key = pallet_subtensor::WeightsVersionKey::::get(netuid); - let set_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights { - netuid, - dests: vec![0], - weights: vec![u16::MAX], - version_key, - }); + fn seed_stake(netuid: NetUid, hotkey: &AccountId, coldkey: &AccountId, alpha: u64) { + pallet_subtensor::Pallet::::create_account_if_non_existent(coldkey, hotkey); + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, + coldkey, + netuid, + AlphaCurrency::from(alpha), + ); + } - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, set_call.clone()); + #[test] + fn staking_add_then_remove_is_rate_limited_after_migration() { + let coldkey_pair = sr25519::Pair::from_seed(&[20u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from([21u8; 32]); + let netuid = NetUid::from(10u16); + let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; + let balance = stake_amount * 10; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_staking_network(netuid); + pallet_subtensor::Pallet::::create_account_if_non_existent( + &coldkey, &hotkey, + ); - System::set_block_number((registration_block + span + span).saturated_into()); - assert_extrinsic_ok(&hotkey, &hotkey_pair, set_call); - }); -} + Executive::execute_on_runtime_upgrade(); -#[test] -fn commit_timelocked_weights_is_rate_limited_after_migration() { - let hotkey_pair = sr25519::Pair::from_seed(&[14u8; 32]); - let hotkey = AccountId::from(hotkey_pair.public()); - let netuid = NetUid::from(3u16); - let span = 4u64; - let registration_block = 1u64; - let commit = BoundedVec::try_from(vec![1u8; 16]).expect("commit payload within limit"); - let reveal_round = 10u64; - - ExtBuilder::default() - .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) - .build() - .execute_with(|| { - setup_weights_network(netuid, &hotkey, registration_block, 1); - legacy_storage::set_weights_set_rate_limit(netuid, span); - - Executive::execute_on_runtime_upgrade(); - - let commit_reveal_version = - pallet_subtensor::Pallet::::get_commit_reveal_weights_version(); - let commit_call = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::commit_timelocked_weights { + let add_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { + hotkey: hotkey.clone(), netuid, - commit: commit.clone(), - reveal_round, - commit_reveal_version, + amount_staked: stake_amount.into(), }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, add_call); + + let alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let remove_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey, + netuid, + amount_unstaked: alpha, + }); + + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, remove_call.clone()); + + System::set_block_number(2); + assert_extrinsic_ok(&coldkey, &coldkey_pair, remove_call); + }); + } - System::set_block_number((registration_block + span).saturated_into()); - assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_call.clone()); - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, commit_call.clone()); + #[test] + fn transfer_stake_is_rate_limited_after_add_stake() { + let coldkey_pair = sr25519::Pair::from_seed(&[22u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let destination_coldkey = AccountId::from([23u8; 32]); + let hotkey = AccountId::from([24u8; 32]); + let netuid = NetUid::from(11u16); + let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; + let balance = stake_amount * 10; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_staking_network(netuid); + pallet_subtensor::Pallet::::create_account_if_non_existent( + &coldkey, &hotkey, + ); - System::set_block_number((registration_block + span + span).saturated_into()); - assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_call); - }); -} + Executive::execute_on_runtime_upgrade(); -#[test] -fn commit_crv3_mechanism_weights_are_rate_limited_per_mechanism() { - let hotkey_pair = sr25519::Pair::from_seed(&[15u8; 32]); - let hotkey = AccountId::from(hotkey_pair.public()); - let netuid = NetUid::from(4u16); - let span = 4u64; - let registration_block = 1u64; - let commit = BoundedVec::try_from(vec![1u8; 16]).expect("commit payload within limit"); - let reveal_round = 10u64; - let mecid_a = MechId::from(0u8); - let mecid_b = MechId::from(1u8); - - ExtBuilder::default() - .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) - .build() - .execute_with(|| { - setup_weights_network(netuid, &hotkey, registration_block, 2); - legacy_storage::set_weights_set_rate_limit(netuid, span); - - Executive::execute_on_runtime_upgrade(); - - let commit_a = RuntimeCall::SubtensorModule( - pallet_subtensor::Call::commit_crv3_mechanism_weights { - netuid, - mecid: mecid_a, - commit: commit.clone(), - reveal_round, - }, - ); - let commit_b = RuntimeCall::SubtensorModule( - pallet_subtensor::Call::commit_crv3_mechanism_weights { + let add_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { + hotkey: hotkey.clone(), netuid, - mecid: mecid_b, - commit: commit.clone(), - reveal_round, - }, - ); - - System::set_block_number((registration_block + span).saturated_into()); - assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_a.clone()); - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, commit_a); - assert_extrinsic_ok(&hotkey, &hotkey_pair, commit_b); - }); -} + amount_staked: stake_amount.into(), + }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, add_call); + + let alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let transfer_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { + destination_coldkey, + hotkey, + origin_netuid: netuid, + destination_netuid: netuid, + alpha_amount: alpha, + }); + + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, transfer_call); + }); + } -#[test] -fn batch_set_weights_is_rate_limited_if_any_scope_is_within_span() { - let hotkey_pair = sr25519::Pair::from_seed(&[16u8; 32]); - let hotkey = AccountId::from(hotkey_pair.public()); - let netuid_a = NetUid::from(5u16); - let netuid_b = NetUid::from(6u16); - let span = 3u64; - let registration_block = 1u64; - - ExtBuilder::default() - .with_balances(vec![(hotkey.clone(), 10_000_000_000_000_u64)]) - .build() - .execute_with(|| { - setup_weights_network(netuid_a, &hotkey, registration_block, 1); - setup_weights_network(netuid_b, &hotkey, registration_block, 1); - legacy_storage::set_weights_set_rate_limit(netuid_a, span); - legacy_storage::set_weights_set_rate_limit(netuid_b, span); - - Executive::execute_on_runtime_upgrade(); - - pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid_a, false); - pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid_b, false); - - let version_key_a = pallet_subtensor::WeightsVersionKey::::get(netuid_a); - let version_key_b = pallet_subtensor::WeightsVersionKey::::get(netuid_b); - - let set_call_a = RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights { - netuid: netuid_a, - dests: vec![0], - weights: vec![u16::MAX], - version_key: version_key_a, + #[test] + fn transfer_stake_does_not_limit_destination_coldkey() { + let coldkey_pair = sr25519::Pair::from_seed(&[25u8; 32]); + let destination_pair = sr25519::Pair::from_seed(&[26u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let destination_coldkey = AccountId::from(destination_pair.public()); + let hotkey = AccountId::from([27u8; 32]); + let origin_netuid = NetUid::from(12u16); + let destination_netuid = NetUid::from(13u16); + let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; + + ExtBuilder::default() + .with_balances(vec![ + (coldkey.clone(), stake_amount * 10), + (destination_coldkey.clone(), stake_amount * 10), + ]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_staking_network(origin_netuid); + setup_staking_network(destination_netuid); + seed_stake(origin_netuid, &hotkey, &coldkey, stake_amount); + + Executive::execute_on_runtime_upgrade(); + + let alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + origin_netuid, + ); + let transfer_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { + destination_coldkey: destination_coldkey.clone(), + hotkey: hotkey.clone(), + origin_netuid, + destination_netuid, + alpha_amount: alpha, + }); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, transfer_call); + + let destination_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &destination_coldkey, + destination_netuid, + ); + let remove_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey, + netuid: destination_netuid, + amount_unstaked: destination_alpha, + }); + + assert_extrinsic_ok(&destination_coldkey, &destination_pair, remove_call); }); + } - System::set_block_number((registration_block + span).saturated_into()); - assert_extrinsic_ok(&hotkey, &hotkey_pair, set_call_a); - - let batch_call = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::batch_set_weights { - netuids: vec![Compact(netuid_a), Compact(netuid_b)], - weights: vec![ - vec![(Compact(0u16), Compact(1u16))], - vec![(Compact(0u16), Compact(1u16))], - ], - version_keys: vec![Compact(version_key_a), Compact(version_key_b)], + #[test] + fn swap_stake_limits_destination_netuid() { + let coldkey_pair = sr25519::Pair::from_seed(&[28u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let hotkey = AccountId::from([29u8; 32]); + let origin_netuid = NetUid::from(14u16); + let destination_netuid = NetUid::from(15u16); + let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), stake_amount * 10)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_staking_network(origin_netuid); + setup_staking_network(destination_netuid); + seed_stake(origin_netuid, &hotkey, &coldkey, stake_amount); + + Executive::execute_on_runtime_upgrade(); + + let alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + origin_netuid, + ); + let swap_alpha = AlphaCurrency::from(alpha.to_u64() / 2); + let swap_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake { + hotkey: hotkey.clone(), + origin_netuid, + destination_netuid, + alpha_amount: swap_alpha, }); - assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, batch_call.clone()); - - System::set_block_number((registration_block + span + span).saturated_into()); - assert_extrinsic_ok(&hotkey, &hotkey_pair, batch_call); - }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_call); + + let destination_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + destination_netuid, + ); + let remove_destination = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey: hotkey.clone(), + netuid: destination_netuid, + amount_unstaked: destination_alpha, + }); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, remove_destination); + + let origin_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + origin_netuid, + ); + let remove_origin = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey, + netuid: origin_netuid, + amount_unstaked: origin_alpha, + }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, remove_origin); + }); + } } -#[test] -fn staking_add_then_remove_is_rate_limited_after_migration() { - let coldkey_pair = sr25519::Pair::from_seed(&[20u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let hotkey = AccountId::from([21u8; 32]); - let netuid = NetUid::from(10u16); - let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; - let balance = stake_amount * 10; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), balance)]) - .build() - .execute_with(|| { - System::set_block_number(1); - setup_staking_network(netuid); - pallet_subtensor::Pallet::::create_account_if_non_existent(&coldkey, &hotkey); - - Executive::execute_on_runtime_upgrade(); - - let add_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { - hotkey: hotkey.clone(), - netuid, - amount_staked: stake_amount.into(), - }); - assert_extrinsic_ok(&coldkey, &coldkey_pair, add_call); +mod swap_keys { + use super::*; + + fn setup_swap_hotkey_state( + netuid: NetUid, + coldkey: &AccountId, + hotkey: &AccountId, + block: u64, + ) { + pallet_subtensor::Pallet::::init_new_network(netuid, 1); + System::set_block_number(block.saturated_into()); + pallet_subtensor::Pallet::::append_neuron(netuid, hotkey, block); + pallet_subtensor::Pallet::::create_account_if_non_existent(coldkey, hotkey); + } - let alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - let remove_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey, - netuid, - amount_unstaked: alpha, + #[test] + fn swap_hotkey_tx_rate_limit_exceeded_all_subnets() { + let coldkey_pair = sr25519::Pair::from_seed(&[30u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let old_hotkey = AccountId::from([31u8; 32]); + let new_hotkey_a = AccountId::from([32u8; 32]); + let new_hotkey_b = AccountId::from([33u8; 32]); + let netuid = NetUid::from(20u16); + let balance = 10_000_000_000_000_u64; + let legacy_span = 3u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); + + legacy_storage::set_tx_rate_limit(legacy_span); + Executive::execute_on_runtime_upgrade(); + + let swap_first = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey_a.clone(), + netuid: None, + }); + let swap_second = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: new_hotkey_a.clone(), + new_hotkey: new_hotkey_b.clone(), + netuid: None, + }); + + let start_block: u64 = System::block_number().saturated_into(); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_first); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, swap_second.clone()); + + let limit_block = start_block.saturating_add(legacy_span); + System::set_block_number(limit_block.saturated_into()); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, swap_second.clone()); + + System::set_block_number((limit_block + 1).saturated_into()); + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_second); }); + } - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, remove_call.clone()); - - System::set_block_number(2); - assert_extrinsic_ok(&coldkey, &coldkey_pair, remove_call); - }); -} - -#[test] -fn transfer_stake_is_rate_limited_after_add_stake() { - let coldkey_pair = sr25519::Pair::from_seed(&[22u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let destination_coldkey = AccountId::from([23u8; 32]); - let hotkey = AccountId::from([24u8; 32]); - let netuid = NetUid::from(11u16); - let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; - let balance = stake_amount * 10; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), balance)]) - .build() - .execute_with(|| { - System::set_block_number(1); - setup_staking_network(netuid); - pallet_subtensor::Pallet::::create_account_if_non_existent(&coldkey, &hotkey); - - Executive::execute_on_runtime_upgrade(); - - let add_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { - hotkey: hotkey.clone(), - netuid, - amount_staked: stake_amount.into(), + #[test] + fn swap_hotkey_tx_rate_limit_exceeded_on_subnet() { + let coldkey_pair = sr25519::Pair::from_seed(&[34u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let old_hotkey = AccountId::from([35u8; 32]); + let new_hotkey_a = AccountId::from([36u8; 32]); + let new_hotkey_b = AccountId::from([37u8; 32]); + let netuid = NetUid::from(21u16); + let balance = 10_000_000_000_000_u64; + let legacy_span = 3u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); + + legacy_storage::set_tx_rate_limit(legacy_span); + Executive::execute_on_runtime_upgrade(); + + let interval: u64 = HotkeySwapOnSubnetInterval::get().saturated_into(); + let start_block = interval.saturating_add(1); + System::set_block_number(start_block.saturated_into()); + + let swap_subnet = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey_a.clone(), + netuid: Some(netuid), + }); + let swap_subnet_again = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey_a.clone(), + netuid: Some(netuid), + }); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_subnet); + assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, swap_subnet_again); + + let limit_block = start_block.saturating_add(legacy_span + 1); + System::set_block_number(limit_block.saturated_into()); + + let swap_all = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: new_hotkey_a.clone(), + new_hotkey: new_hotkey_b.clone(), + netuid: None, + }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_all); }); - assert_extrinsic_ok(&coldkey, &coldkey_pair, add_call); + } - let alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, + #[test] + fn swap_hotkey_transfers_last_seen_all_subnets() { + let coldkey_pair = sr25519::Pair::from_seed(&[38u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let old_hotkey = AccountId::from([39u8; 32]); + let new_hotkey = AccountId::from([40u8; 32]); + let netuid = NetUid::from(22u16); + let balance = 10_000_000_000_000_u64; + let legacy_last_seen = 7u64; + let childkey_last_seen = 91u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); + + legacy_storage::set_last_rate_limited_block( + RateLimitKey::LastTxBlock(old_hotkey.clone()), + legacy_last_seen, ); - let transfer_call = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { - destination_coldkey, - hotkey, - origin_netuid: netuid, - destination_netuid: netuid, - alpha_amount: alpha, - }); - - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, transfer_call); - }); -} - -#[test] -fn transfer_stake_does_not_limit_destination_coldkey() { - let coldkey_pair = sr25519::Pair::from_seed(&[25u8; 32]); - let destination_pair = sr25519::Pair::from_seed(&[26u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let destination_coldkey = AccountId::from(destination_pair.public()); - let hotkey = AccountId::from([27u8; 32]); - let origin_netuid = NetUid::from(12u16); - let destination_netuid = NetUid::from(13u16); - let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; - - ExtBuilder::default() - .with_balances(vec![ - (coldkey.clone(), stake_amount * 10), - (destination_coldkey.clone(), stake_amount * 10), - ]) - .build() - .execute_with(|| { - System::set_block_number(1); - setup_staking_network(origin_netuid); - setup_staking_network(destination_netuid); - seed_stake(origin_netuid, &hotkey, &coldkey, stake_amount); - - Executive::execute_on_runtime_upgrade(); - - let alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - origin_netuid, + pallet_subtensor::Pallet::::set_last_tx_block_childkey( + &old_hotkey, + childkey_last_seen, ); - let transfer_call = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { - destination_coldkey: destination_coldkey.clone(), - hotkey: hotkey.clone(), - origin_netuid, - destination_netuid, - alpha_amount: alpha, - }); - assert_extrinsic_ok(&coldkey, &coldkey_pair, transfer_call); + Executive::execute_on_runtime_upgrade(); - let destination_alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &destination_coldkey, - destination_netuid, + let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); + let usage_old = RateLimitUsageKey::Account(old_hotkey.clone()); + let usage_new = RateLimitUsageKey::Account(new_hotkey.clone()); + assert_eq!( + pallet_rate_limiting::LastSeen::::get(target, Some(usage_old.clone())), + Some(legacy_last_seen.saturated_into()) ); - let remove_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey, - netuid: destination_netuid, - amount_unstaked: destination_alpha, - }); - assert_extrinsic_ok(&destination_coldkey, &destination_pair, remove_call); - }); -} + System::set_block_number(10); + let swap_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey.clone(), + netuid: None, + }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_call); -#[test] -fn swap_stake_limits_destination_netuid() { - let coldkey_pair = sr25519::Pair::from_seed(&[28u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let hotkey = AccountId::from([29u8; 32]); - let origin_netuid = NetUid::from(14u16); - let destination_netuid = NetUid::from(15u16); - let stake_amount = pallet_subtensor::DefaultMinStake::::get().to_u64() * 10; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), stake_amount * 10)]) - .build() - .execute_with(|| { - System::set_block_number(1); - setup_staking_network(origin_netuid); - setup_staking_network(destination_netuid); - seed_stake(origin_netuid, &hotkey, &coldkey, stake_amount); - - Executive::execute_on_runtime_upgrade(); - - let alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - origin_netuid, + assert_eq!( + pallet_rate_limiting::LastSeen::::get(target, Some(usage_new)), + Some(legacy_last_seen.saturated_into()) ); - let swap_alpha = AlphaCurrency::from(alpha.to_u64() / 2); - let swap_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake { - hotkey: hotkey.clone(), - origin_netuid, - destination_netuid, - alpha_amount: swap_alpha, - }); - - assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_call); - - let destination_alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - destination_netuid, + assert_eq!( + pallet_rate_limiting::LastSeen::::get(target, Some(usage_old)), + None ); - let remove_destination = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey: hotkey.clone(), - netuid: destination_netuid, - amount_unstaked: destination_alpha, - }); - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, remove_destination); - - let origin_alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - origin_netuid, + assert_eq!( + pallet_subtensor::Pallet::::get_last_tx_block_childkey_take( + &new_hotkey + ), + childkey_last_seen ); - let remove_origin = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey, - netuid: origin_netuid, - amount_unstaked: origin_alpha, - }); - assert_extrinsic_ok(&coldkey, &coldkey_pair, remove_origin); - }); -} - -#[test] -fn swap_hotkey_tx_rate_limit_exceeded_all_subnets() { - let coldkey_pair = sr25519::Pair::from_seed(&[30u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let old_hotkey = AccountId::from([31u8; 32]); - let new_hotkey_a = AccountId::from([32u8; 32]); - let new_hotkey_b = AccountId::from([33u8; 32]); - let netuid = NetUid::from(20u16); - let balance = 10_000_000_000_000_u64; - let legacy_span = 3u64; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), balance)]) - .build() - .execute_with(|| { - System::set_block_number(1); - setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); - - legacy_storage::set_tx_rate_limit(legacy_span); - Executive::execute_on_runtime_upgrade(); - - let swap_first = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { - hotkey: old_hotkey.clone(), - new_hotkey: new_hotkey_a.clone(), - netuid: None, - }); - let swap_second = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { - hotkey: new_hotkey_a.clone(), - new_hotkey: new_hotkey_b.clone(), - netuid: None, }); + } - let start_block: u64 = System::block_number().saturated_into(); + #[test] + fn swap_hotkey_transfers_last_seen_on_subnet() { + let coldkey_pair = sr25519::Pair::from_seed(&[41u8; 32]); + let coldkey = AccountId::from(coldkey_pair.public()); + let old_hotkey = AccountId::from([42u8; 32]); + let new_hotkey = AccountId::from([43u8; 32]); + let netuid = NetUid::from(23u16); + let balance = 10_000_000_000_000_u64; + let legacy_last_seen = 9u64; + let childkey_last_seen = 97u64; + + ExtBuilder::default() + .with_balances(vec![(coldkey.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(1); + setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); + + legacy_storage::set_last_rate_limited_block( + RateLimitKey::LastTxBlock(old_hotkey.clone()), + legacy_last_seen, + ); + pallet_subtensor::Pallet::::set_last_tx_block_childkey( + &old_hotkey, + childkey_last_seen, + ); - assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_first); - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, swap_second.clone()); + Executive::execute_on_runtime_upgrade(); - let limit_block = start_block.saturating_add(legacy_span); - System::set_block_number(limit_block.saturated_into()); - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, swap_second.clone()); + let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); + let usage_new = RateLimitUsageKey::Account(new_hotkey.clone()); - System::set_block_number((limit_block + 1).saturated_into()); - assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_second); - }); -} + let interval: u64 = HotkeySwapOnSubnetInterval::get().saturated_into(); + System::set_block_number(interval.saturating_add(1).saturated_into()); -#[test] -fn swap_hotkey_tx_rate_limit_exceeded_on_subnet() { - let coldkey_pair = sr25519::Pair::from_seed(&[34u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let old_hotkey = AccountId::from([35u8; 32]); - let new_hotkey_a = AccountId::from([36u8; 32]); - let new_hotkey_b = AccountId::from([37u8; 32]); - let netuid = NetUid::from(21u16); - let balance = 10_000_000_000_000_u64; - let legacy_span = 3u64; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), balance)]) - .build() - .execute_with(|| { - System::set_block_number(1); - setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); - - legacy_storage::set_tx_rate_limit(legacy_span); - Executive::execute_on_runtime_upgrade(); - - let interval: u64 = HotkeySwapOnSubnetInterval::get().saturated_into(); - let start_block = interval.saturating_add(1); - System::set_block_number(start_block.saturated_into()); - - let swap_subnet = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { - hotkey: old_hotkey.clone(), - new_hotkey: new_hotkey_a.clone(), - netuid: Some(netuid), - }); - let swap_subnet_again = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + let swap_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { hotkey: old_hotkey.clone(), - new_hotkey: new_hotkey_a.clone(), + new_hotkey: new_hotkey.clone(), netuid: Some(netuid), }); + assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_call); - assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_subnet); - assert_extrinsic_rate_limited(&coldkey, &coldkey_pair, swap_subnet_again); - - let limit_block = start_block.saturating_add(legacy_span + 1); - System::set_block_number(limit_block.saturated_into()); - - let swap_all = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { - hotkey: new_hotkey_a.clone(), - new_hotkey: new_hotkey_b.clone(), - netuid: None, - }); - assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_all); - }); -} - -#[test] -fn swap_hotkey_transfers_last_seen_all_subnets() { - let coldkey_pair = sr25519::Pair::from_seed(&[38u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let old_hotkey = AccountId::from([39u8; 32]); - let new_hotkey = AccountId::from([40u8; 32]); - let netuid = NetUid::from(22u16); - let balance = 10_000_000_000_000_u64; - let legacy_last_seen = 7u64; - let childkey_last_seen = 91u64; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), balance)]) - .build() - .execute_with(|| { - System::set_block_number(1); - setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); - - legacy_storage::set_last_rate_limited_block( - RateLimitKey::LastTxBlock(old_hotkey.clone()), - legacy_last_seen, - ); - pallet_subtensor::Pallet::::set_last_tx_block_childkey( - &old_hotkey, - childkey_last_seen, - ); - - Executive::execute_on_runtime_upgrade(); - - let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); - let usage_old = RateLimitUsageKey::Account(old_hotkey.clone()); - let usage_new = RateLimitUsageKey::Account(new_hotkey.clone()); - assert_eq!( - pallet_rate_limiting::LastSeen::::get(target, Some(usage_old.clone())), - Some(legacy_last_seen.saturated_into()) - ); - - System::set_block_number(10); - let swap_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { - hotkey: old_hotkey.clone(), - new_hotkey: new_hotkey.clone(), - netuid: None, + assert_eq!( + pallet_rate_limiting::LastSeen::::get(target, Some(usage_new)), + Some(legacy_last_seen.saturated_into()) + ); + assert_eq!( + pallet_subtensor::Pallet::::get_last_tx_block_childkey_take( + &new_hotkey + ), + childkey_last_seen + ); }); - assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_call); - - assert_eq!( - pallet_rate_limiting::LastSeen::::get(target, Some(usage_new)), - Some(legacy_last_seen.saturated_into()) - ); - assert_eq!( - pallet_rate_limiting::LastSeen::::get(target, Some(usage_old)), - None - ); - assert_eq!( - pallet_subtensor::Pallet::::get_last_tx_block_childkey_take(&new_hotkey), - childkey_last_seen - ); - }); -} - -#[test] -fn swap_hotkey_transfers_last_seen_on_subnet() { - let coldkey_pair = sr25519::Pair::from_seed(&[41u8; 32]); - let coldkey = AccountId::from(coldkey_pair.public()); - let old_hotkey = AccountId::from([42u8; 32]); - let new_hotkey = AccountId::from([43u8; 32]); - let netuid = NetUid::from(23u16); - let balance = 10_000_000_000_000_u64; - let legacy_last_seen = 9u64; - let childkey_last_seen = 97u64; - - ExtBuilder::default() - .with_balances(vec![(coldkey.clone(), balance)]) - .build() - .execute_with(|| { - System::set_block_number(1); - setup_swap_hotkey_state(netuid, &coldkey, &old_hotkey, 1); - - legacy_storage::set_last_rate_limited_block( - RateLimitKey::LastTxBlock(old_hotkey.clone()), - legacy_last_seen, - ); - pallet_subtensor::Pallet::::set_last_tx_block_childkey( - &old_hotkey, - childkey_last_seen, - ); - - Executive::execute_on_runtime_upgrade(); + } - let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); - let usage_new = RateLimitUsageKey::Account(new_hotkey.clone()); + // NOTE: This currently fails. When `swap_coldkey` is dispatched via `Sudo::sudo`, rate-limiting + // sees the outer sudo call, so `swap_coldkey` does not record usage in the swap-keys group. Keep + // this test to flag the issue until the rate-limiting extension unwraps sudo calls. + #[test] + fn swap_coldkey_records_usage_for_swap_keys_group() { + let sudo_pair = sr25519::Pair::from_seed(&[44u8; 32]); + let new_coldkey_pair = sr25519::Pair::from_seed(&[45u8; 32]); + let sudo_account = AccountId::from(sudo_pair.public()); + let old_coldkey = AccountId::from([46u8; 32]); + let new_coldkey = AccountId::from(new_coldkey_pair.public()); + let old_hotkey = AccountId::from([47u8; 32]); + let new_hotkey = AccountId::from([48u8; 32]); + let balance = 10_000_000_000_000_u64; + let legacy_span = 3u64; + let swap_cost = 1u64; + + ExtBuilder::default() + .with_balances(vec![ + (sudo_account.clone(), balance), + (old_coldkey.clone(), balance), + (new_coldkey.clone(), balance), + ]) + .build() + .execute_with(|| { + System::set_block_number(10); + pallet_sudo::Key::::put(sudo_account.clone()); + legacy_storage::set_tx_rate_limit(legacy_span); + Executive::execute_on_runtime_upgrade(); + + let swap_coldkey_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_coldkey { + old_coldkey: old_coldkey.clone(), + new_coldkey: new_coldkey.clone(), + swap_cost: swap_cost.into(), + }); + assert_sudo_extrinsic_ok(&sudo_account, &sudo_pair, swap_coldkey_call); + + let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); + let usage_new = RateLimitUsageKey::Account(new_coldkey.clone()); + assert_eq!( + pallet_rate_limiting::LastSeen::::get(target, Some(usage_new.clone())), + Some(10u64.saturated_into()) + ); - let interval: u64 = HotkeySwapOnSubnetInterval::get().saturated_into(); - System::set_block_number(interval.saturating_add(1).saturated_into()); + pallet_subtensor::Pallet::::create_account_if_non_existent( + &new_coldkey, + &old_hotkey, + ); - let swap_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { - hotkey: old_hotkey.clone(), - new_hotkey: new_hotkey.clone(), - netuid: Some(netuid), + let swap_hotkey_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: old_hotkey.clone(), + new_hotkey: new_hotkey.clone(), + netuid: None, + }); + assert_extrinsic_rate_limited(&new_coldkey, &new_coldkey_pair, swap_hotkey_call); }); - assert_extrinsic_ok(&coldkey, &coldkey_pair, swap_call); - - assert_eq!( - pallet_rate_limiting::LastSeen::::get(target, Some(usage_new)), - Some(legacy_last_seen.saturated_into()) - ); - assert_eq!( - pallet_subtensor::Pallet::::get_last_tx_block_childkey_take(&new_hotkey), - childkey_last_seen - ); - }); -} - -// NOTE: This currently fails. When `swap_coldkey` is dispatched via `Sudo::sudo`, rate-limiting -// sees the outer sudo call, so `swap_coldkey` does not record usage in the swap-keys group. Keep -// this test to flag the issue until the rate-limiting extension unwraps sudo calls. -#[test] -fn swap_coldkey_records_usage_for_swap_keys_group() { - let sudo_pair = sr25519::Pair::from_seed(&[44u8; 32]); - let new_coldkey_pair = sr25519::Pair::from_seed(&[45u8; 32]); - let sudo_account = AccountId::from(sudo_pair.public()); - let old_coldkey = AccountId::from([46u8; 32]); - let new_coldkey = AccountId::from(new_coldkey_pair.public()); - let old_hotkey = AccountId::from([47u8; 32]); - let new_hotkey = AccountId::from([48u8; 32]); - let balance = 10_000_000_000_000_u64; - let legacy_span = 3u64; - let swap_cost = 1u64; - - ExtBuilder::default() - .with_balances(vec![ - (sudo_account.clone(), balance), - (old_coldkey.clone(), balance), - (new_coldkey.clone(), balance), - ]) - .build() - .execute_with(|| { - System::set_block_number(10); - pallet_sudo::Key::::put(sudo_account.clone()); - legacy_storage::set_tx_rate_limit(legacy_span); - Executive::execute_on_runtime_upgrade(); - - let swap_coldkey_call = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_coldkey { - old_coldkey: old_coldkey.clone(), - new_coldkey: new_coldkey.clone(), - swap_cost: swap_cost.into(), - }); - assert_sudo_extrinsic_ok(&sudo_account, &sudo_pair, swap_coldkey_call); - - let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); - let usage_new = RateLimitUsageKey::Account(new_coldkey.clone()); - assert_eq!( - pallet_rate_limiting::LastSeen::::get(target, Some(usage_new.clone())), - Some(10u64.into()) - ); - - pallet_subtensor::Pallet::::create_account_if_non_existent( - &new_coldkey, - &old_hotkey, - ); - - let swap_hotkey_call = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { - hotkey: old_hotkey.clone(), - new_hotkey: new_hotkey.clone(), - netuid: None, - }); - assert_extrinsic_rate_limited(&new_coldkey, &new_coldkey_pair, swap_hotkey_call); - }); + } } From 2cc272d664adf1ba01f232eb7cd2cf76bef4b616 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 23 Jan 2026 18:10:48 +0100 Subject: [PATCH 93/95] Update contract tests --- contract-tests/src/subtensor.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/contract-tests/src/subtensor.ts b/contract-tests/src/subtensor.ts index 77396377ae..fc4c5b5045 100644 --- a/contract-tests/src/subtensor.ts +++ b/contract-tests/src/subtensor.ts @@ -195,16 +195,24 @@ export async function sendProxyCall(api: TypedApi, calldata: TxCa export async function setTxRateLimit(api: TypedApi, txRateLimit: bigint) { - const value = await api.query.SubtensorModule.TxRateLimit.getValue() - if (value === txRateLimit) { + const swapKeysGroupId = 6; // GROUP_SWAP_KEYS constant + const target = rateLimitTargetGroup(swapKeysGroupId); + const limits = await api.query.RateLimiting.Limits.getValue(target as any) as any; + assert.ok(limits?.type === "Global"); + assert.ok(limits.value?.type === "Exact"); + const currentLimit = BigInt(limits.value.value); + if (currentLimit === txRateLimit) { return; } const alice = getAliceSigner() - const internalCall = api.tx.AdminUtils.sudo_set_tx_rate_limit({ tx_rate_limit: txRateLimit }) + const internalCall = api.tx.RateLimiting.set_rate_limit({ + target: target as any, + scope: undefined, + limit: rateLimitKindExact(txRateLimit), + }) const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) - await waitForTransactionWithRetry(api, tx, alice) } From 7344fc34f66320df4ce2e21ad8e1520c17f1c52c Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Fri, 23 Jan 2026 18:17:17 +0100 Subject: [PATCH 94/95] Add swap-keys rate-limiting genesis config --- node/src/chain_spec/devnet.rs | 6 +++++- node/src/chain_spec/localnet.rs | 6 +++++- runtime/src/migrations/rate_limiting.rs | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/node/src/chain_spec/devnet.rs b/node/src/chain_spec/devnet.rs index 50317373fb..7770065abb 100644 --- a/node/src/chain_spec/devnet.rs +++ b/node/src/chain_spec/devnet.rs @@ -7,7 +7,7 @@ use subtensor_runtime_common::{ NetUid, rate_limiting::{ GROUP_DELEGATE_TAKE, GROUP_REGISTER_NETWORK, GROUP_SERVE, GROUP_STAKING_OPS, - GROUP_WEIGHTS_SET, + GROUP_SWAP_KEYS, GROUP_WEIGHTS_SET, }, }; @@ -105,6 +105,9 @@ fn devnet_genesis( (serde_json::json!({ "Group": GROUP_REGISTER_NETWORK }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::network_rate_limit() })), (serde_json::json!({ "Group": GROUP_DELEGATE_TAKE }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::tx_delegate_take_rate_limit() })), (serde_json::json!({ "Group": GROUP_STAKING_OPS }), Option::::None, serde_json::json!({ "Exact": 1 })), + // Legacy TxRateLimit blocks when delta <= limit; rate-limiting blocks at delta < span. + // Add one block to preserve legacy swap-keys behavior when legacy rate-limiting is removed. + (serde_json::json!({ "Group": GROUP_SWAP_KEYS }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::tx_rate_limit().saturating_add(1) })), (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::ROOT), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::from(1u16)), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), ], @@ -113,6 +116,7 @@ fn devnet_genesis( (GROUP_REGISTER_NETWORK, b"register-network".to_vec(), "ConfigAndUsage"), (GROUP_DELEGATE_TAKE, b"delegate-take".to_vec(), "ConfigAndUsage"), (GROUP_STAKING_OPS, b"staking-ops".to_vec(), "ConfigAndUsage"), + (GROUP_SWAP_KEYS, b"swap-keys".to_vec(), "ConfigAndUsage"), (GROUP_WEIGHTS_SET, b"weights".to_vec(), "ConfigAndUsage"), ], "limitSettingRules": vec![ diff --git a/node/src/chain_spec/localnet.rs b/node/src/chain_spec/localnet.rs index f2aa494e5f..efd8c8de5f 100644 --- a/node/src/chain_spec/localnet.rs +++ b/node/src/chain_spec/localnet.rs @@ -7,7 +7,7 @@ use subtensor_runtime_common::{ NetUid, rate_limiting::{ GROUP_DELEGATE_TAKE, GROUP_REGISTER_NETWORK, GROUP_SERVE, GROUP_STAKING_OPS, - GROUP_WEIGHTS_SET, + GROUP_SWAP_KEYS, GROUP_WEIGHTS_SET, }, }; @@ -140,6 +140,9 @@ fn localnet_genesis( (serde_json::json!({ "Group": GROUP_REGISTER_NETWORK }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::network_rate_limit() })), (serde_json::json!({ "Group": GROUP_DELEGATE_TAKE }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::tx_delegate_take_rate_limit() })), (serde_json::json!({ "Group": GROUP_STAKING_OPS }), Option::::None, serde_json::json!({ "Exact": 1 })), + // Legacy TxRateLimit blocks when delta <= limit; rate-limiting blocks at delta < span. + // Add one block to preserve legacy swap-keys behavior when legacy rate-limiting is removed. + (serde_json::json!({ "Group": GROUP_SWAP_KEYS }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::tx_rate_limit().saturating_add(1) })), (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::ROOT), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), (serde_json::json!({ "Group": GROUP_WEIGHTS_SET }), Some(NetUid::from(1u16)), serde_json::json!({ "Exact": rate_limit_defaults::weights_set_rate_limit() })), ], @@ -148,6 +151,7 @@ fn localnet_genesis( (GROUP_REGISTER_NETWORK, b"register-network".to_vec(), "ConfigAndUsage"), (GROUP_DELEGATE_TAKE, b"delegate-take".to_vec(), "ConfigAndUsage"), (GROUP_STAKING_OPS, b"staking-ops".to_vec(), "ConfigAndUsage"), + (GROUP_SWAP_KEYS, b"swap-keys".to_vec(), "ConfigAndUsage"), (GROUP_WEIGHTS_SET, b"weights".to_vec(), "ConfigAndUsage"), ], "limitSettingRules": vec![ diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index 7cb6cdc5c8..abcfac0754 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -612,7 +612,7 @@ fn build_swap_keys(groups: &mut Vec, commits: &mut Vec) -> let target = RateLimitTarget::Group(GROUP_SWAP_KEYS); let (tx_rate_limit, tx_reads) = legacy_storage::tx_rate_limit(); reads = reads.saturating_add(tx_reads); - // Legacy check blocks at delta == limit; pallet-rate-limiting allows at delta == span. + // Legacy check blocks at delta <= limit; pallet-rate-limiting blocks at delta < span. // Add one block to preserve legacy behavior when legacy rate-limiting is removed. let effective_limit = if tx_rate_limit == 0 { 0 From fbc4579dddfb906bbe16c9a91a7ed69e51c21752 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Wed, 28 Jan 2026 19:51:39 +0100 Subject: [PATCH 95/95] Remove/deprecate hyperparams legacy rate-limiting --- pallets/admin-utils/src/lib.rs | 322 +++---------------- pallets/admin-utils/src/tests/mod.rs | 14 - pallets/subtensor/src/lib.rs | 11 - pallets/subtensor/src/macros/events.rs | 2 - pallets/subtensor/src/tests/ensure.rs | 82 +---- pallets/subtensor/src/utils/misc.rs | 37 --- pallets/subtensor/src/utils/rate_limiting.rs | 15 +- runtime/src/migrations/rate_limiting.rs | 5 +- runtime/src/rate_limiting/legacy.rs | 7 + 9 files changed, 57 insertions(+), 438 deletions(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 654d88b438..af71dccc45 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -26,10 +26,7 @@ pub mod pallet { use frame_support::{dispatch::DispatchResult, pallet_prelude::StorageMap}; use frame_system::pallet_prelude::*; use pallet_evm_chain_id::{self, ChainId}; - use pallet_subtensor::{ - DefaultMaxAllowedUids, - utils::rate_limiting::{Hyperparameter, TransactionType}, - }; + use pallet_subtensor::DefaultMaxAllowedUids; use sp_runtime::BoundedVec; use substrate_fixed::types::{I64F64, I96F32, U64F64}; use subtensor_runtime_common::{MechId, NetUid, TaoCurrency}; @@ -286,11 +283,7 @@ pub mod pallet { netuid: NetUid, max_difficulty: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::MaxDifficulty.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -301,11 +294,6 @@ pub mod pallet { log::debug!( "MaxDifficultySet( netuid: {netuid:?} max_difficulty: {max_difficulty:?} ) " ); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::MaxDifficulty.into()], - ); Ok(()) } @@ -321,11 +309,7 @@ pub mod pallet { netuid: NetUid, weights_version_key: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin.clone(), - netuid, - &[TransactionType::SetWeightsVersionKey], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -333,12 +317,6 @@ pub mod pallet { Error::::SubnetDoesNotExist ); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[TransactionType::SetWeightsVersionKey], - ); - pallet_subtensor::Pallet::::set_weights_version_key(netuid, weights_version_key); log::debug!( "WeightsVersionKeySet( netuid: {netuid:?} weights_version_key: {weights_version_key:?} ) " @@ -406,11 +384,7 @@ pub mod pallet { netuid: NetUid, adjustment_alpha: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::AdjustmentAlpha.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -418,11 +392,6 @@ pub mod pallet { Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_adjustment_alpha(netuid, adjustment_alpha); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::AdjustmentAlpha.into()], - ); log::debug!("AdjustmentAlphaSet( adjustment_alpha: {adjustment_alpha:?} ) "); Ok(()) } @@ -439,11 +408,7 @@ pub mod pallet { netuid: NetUid, immunity_period: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::ImmunityPeriod.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -451,11 +416,6 @@ pub mod pallet { ); pallet_subtensor::Pallet::::set_immunity_period(netuid, immunity_period); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::ImmunityPeriod.into()], - ); log::debug!( "ImmunityPeriodSet( netuid: {netuid:?} immunity_period: {immunity_period:?} ) " ); @@ -474,11 +434,7 @@ pub mod pallet { netuid: NetUid, min_allowed_weights: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::MinAllowedWeights.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -489,11 +445,6 @@ pub mod pallet { log::debug!( "MinAllowedWeightSet( netuid: {netuid:?} min_allowed_weights: {min_allowed_weights:?} ) " ); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::MinAllowedWeights.into()], - ); Ok(()) } @@ -509,11 +460,7 @@ pub mod pallet { netuid: NetUid, max_allowed_uids: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::MaxAllowedUids.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -532,11 +479,6 @@ pub mod pallet { Error::::MaxAllowedUidsGreaterThanDefaultMaxAllowedUids ); pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, max_allowed_uids); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::MaxAllowedUids.into()], - ); log::debug!( "MaxAllowedUidsSet( netuid: {netuid:?} max_allowed_uids: {max_allowed_uids:?} ) " ); @@ -569,11 +511,7 @@ pub mod pallet { .saturating_add(::DbWeight::get().reads(3_u64)) .saturating_add(::DbWeight::get().writes(1_u64)))] pub fn sudo_set_rho(origin: OriginFor, netuid: NetUid, rho: u16) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::Rho.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -582,11 +520,6 @@ pub mod pallet { ); pallet_subtensor::Pallet::::set_rho(netuid, rho); log::debug!("RhoSet( netuid: {netuid:?} rho: {rho:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::Rho.into()], - ); Ok(()) } @@ -602,11 +535,7 @@ pub mod pallet { netuid: NetUid, activity_cutoff: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::ActivityCutoff.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -623,11 +552,6 @@ pub mod pallet { log::debug!( "ActivityCutoffSet( netuid: {netuid:?} activity_cutoff: {activity_cutoff:?} ) " ); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::ActivityCutoff.into()], - ); Ok(()) } @@ -671,11 +595,7 @@ pub mod pallet { netuid: NetUid, registration_allowed: bool, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::PowRegistrationAllowed.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::set_network_pow_registration_allowed( @@ -685,11 +605,6 @@ pub mod pallet { log::debug!( "NetworkPowRegistrationAllowed( registration_allowed: {registration_allowed:?} ) " ); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::PowRegistrationAllowed.into()], - ); Ok(()) } @@ -734,11 +649,7 @@ pub mod pallet { netuid: NetUid, min_burn: TaoCurrency, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::MinBurn.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -755,11 +666,6 @@ pub mod pallet { ); pallet_subtensor::Pallet::::set_min_burn(netuid, min_burn); log::debug!("MinBurnSet( netuid: {netuid:?} min_burn: {min_burn:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::MinBurn.into()], - ); Ok(()) } @@ -775,11 +681,7 @@ pub mod pallet { netuid: NetUid, max_burn: TaoCurrency, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::MaxBurn.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -796,11 +698,6 @@ pub mod pallet { ); pallet_subtensor::Pallet::::set_max_burn(netuid, max_burn); log::debug!("MaxBurnSet( netuid: {netuid:?} max_burn: {max_burn:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::MaxBurn.into()], - ); Ok(()) } @@ -873,11 +770,8 @@ pub mod pallet { netuid: NetUid, bonds_moving_average: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::BondsMovingAverage.into()], - )?; + let maybe_owner = + pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; if maybe_owner.is_some() { ensure!( @@ -894,11 +788,6 @@ pub mod pallet { log::debug!( "BondsMovingAverageSet( netuid: {netuid:?} bonds_moving_average: {bonds_moving_average:?} ) " ); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::BondsMovingAverage.into()], - ); Ok(()) } @@ -914,11 +803,7 @@ pub mod pallet { netuid: NetUid, bonds_penalty: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::BondsPenalty.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -927,11 +812,6 @@ pub mod pallet { ); pallet_subtensor::Pallet::::set_bonds_penalty(netuid, bonds_penalty); log::debug!("BondsPenalty( netuid: {netuid:?} bonds_penalty: {bonds_penalty:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::BondsPenalty.into()], - ); Ok(()) } @@ -1257,11 +1137,7 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::CommitRevealEnabled.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -1271,11 +1147,6 @@ pub mod pallet { pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, enabled); log::debug!("ToggleSetWeightsCommitReveal( netuid: {netuid:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::CommitRevealEnabled.into()], - ); Ok(()) } @@ -1301,19 +1172,10 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::LiquidAlphaEnabled.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::set_liquid_alpha_enabled(netuid, enabled); log::debug!("LiquidAlphaEnableToggled( netuid: {netuid:?}, Enabled: {enabled:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::LiquidAlphaEnabled.into()], - ); Ok(()) } @@ -1332,23 +1194,12 @@ pub mod pallet { alpha_low: u16, alpha_high: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin.clone(), - netuid, - &[Hyperparameter::AlphaValues.into()], - )?; + let _ = + pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin.clone(), netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; - let res = pallet_subtensor::Pallet::::do_set_alpha_values( + pallet_subtensor::Pallet::::do_set_alpha_values( origin, netuid, alpha_low, alpha_high, - ); - if res.is_ok() { - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::AlphaValues.into()], - ); - } - res + ) } /// Sets the duration of the coldkey swap schedule. @@ -1452,11 +1303,7 @@ pub mod pallet { netuid: NetUid, interval: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::WeightCommitInterval.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -1467,11 +1314,6 @@ pub mod pallet { log::debug!("SetWeightCommitInterval( netuid: {netuid:?}, interval: {interval:?} ) "); pallet_subtensor::Pallet::::set_reveal_period(netuid, interval)?; - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::WeightCommitInterval.into()], - ); Ok(()) } @@ -1551,21 +1393,9 @@ pub mod pallet { netuid: NetUid, toggle: bool, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::TransferEnabled.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; - let res = pallet_subtensor::Pallet::::toggle_transfer(netuid, toggle); - if res.is_ok() { - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::TransferEnabled.into()], - ); - } - res + pallet_subtensor::Pallet::::toggle_transfer(netuid, toggle) } /// Set the behaviour of the "burn" UID(s) for a given subnet. @@ -1584,19 +1414,10 @@ pub mod pallet { netuid: NetUid, recycle_or_burn: pallet_subtensor::RecycleOrBurnEnum, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::RecycleOrBurn.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::set_recycle_or_burn(netuid, recycle_or_burn); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::RecycleOrBurn.into()], - ); Ok(()) } @@ -1752,11 +1573,8 @@ pub mod pallet { netuid: NetUid, steepness: i16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin.clone(), - netuid, - &[Hyperparameter::AlphaSigmoidSteepness.into()], - )?; + let _ = + pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin.clone(), netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -1773,11 +1591,6 @@ pub mod pallet { pallet_subtensor::Pallet::::set_alpha_sigmoid_steepness(netuid, steepness); log::debug!("AlphaSigmoidSteepnessSet( netuid: {netuid:?}, steepness: {steepness:?} )"); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::AlphaSigmoidSteepness.into()], - ); Ok(()) } @@ -1803,21 +1616,12 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::Yuma3Enabled.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::set_yuma3_enabled(netuid, enabled); Self::deposit_event(Event::Yuma3EnableToggled { netuid, enabled }); log::debug!("Yuma3EnableToggled( netuid: {netuid:?}, Enabled: {enabled:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::Yuma3Enabled.into()], - ); Ok(()) } @@ -1843,21 +1647,12 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::BondsResetEnabled.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::set_bonds_reset(netuid, enabled); Self::deposit_event(Event::BondsResetToggled { netuid, enabled }); log::debug!("BondsResetToggled( netuid: {netuid:?} bonds_reset: {enabled:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::BondsResetEnabled.into()], - ); Ok(()) } @@ -1970,18 +1765,9 @@ pub mod pallet { netuid: NetUid, immune_neurons: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::ImmuneNeuronLimit.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::set_owner_immune_neuron_limit(netuid, immune_neurons)?; - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::ImmuneNeuronLimit.into()], - ); Ok(()) } @@ -2017,6 +1803,9 @@ pub mod pallet { /// Sets the owner hyperparameter rate limit in epochs (global multiplier). /// Only callable by root. + /// + /// Deprecated: hyperparameters rate limits are now configured via `pallet-rate-limiting` on + /// the owner-hparams group target (`GROUP_OWNER_HPARAMS`). #[pallet::call_index(75)] #[pallet::weight(( Weight::from_parts(5_701_000, 0) @@ -2024,14 +1813,14 @@ pub mod pallet { .saturating_add(::DbWeight::get().writes(1_u64)), DispatchClass::Operational ))] + #[deprecated( + note = "deprecated: configure via pallet-rate-limiting::set_rate_limit(target=Group(GROUP_OWNER_HPARAMS), ...)" + )] pub fn sudo_set_owner_hparam_rate_limit( - origin: OriginFor, - epochs: u16, + _origin: OriginFor, + _epochs: u16, ) -> DispatchResult { - ensure_root(origin)?; - pallet_subtensor::Pallet::::set_owner_hyperparam_rate_limit(epochs); - log::debug!("OwnerHyperparamRateLimitSet( epochs: {epochs:?} ) "); - Ok(()) + Err(Error::::Deprecated.into()) } /// Sets the desired number of mechanisms in a subnet @@ -2044,20 +1833,11 @@ pub mod pallet { netuid: NetUid, mechanism_count: MechId, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[TransactionType::MechanismCountUpdate], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::do_set_mechanism_count(netuid, mechanism_count)?; - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[TransactionType::MechanismCountUpdate], - ); Ok(()) } @@ -2071,20 +1851,11 @@ pub mod pallet { netuid: NetUid, maybe_split: Option>, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[TransactionType::MechanismEmission], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::do_set_emission_split(netuid, maybe_split)?; - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[TransactionType::MechanismEmission], - ); Ok(()) } @@ -2102,20 +1873,11 @@ pub mod pallet { netuid: NetUid, max_n: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin.clone(), - netuid, - &[TransactionType::MaxUidsTrimming], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::trim_to_max_allowed_uids(netuid, max_n)?; - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[TransactionType::MaxUidsTrimming], - ); Ok(()) } diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index eaf1dd3dca..763d9dd7b7 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -1916,20 +1916,6 @@ fn test_sudo_set_admin_freeze_window_and_rate() { 7 )); assert_eq!(pallet_subtensor::AdminFreezeWindow::::get(), 7); - - // Owner hyperparam tempos setter - assert_eq!( - AdminUtils::sudo_set_owner_hparam_rate_limit( - <::RuntimeOrigin>::signed(U256::from(1)), - 5 - ), - Err(DispatchError::BadOrigin) - ); - assert_ok!(AdminUtils::sudo_set_owner_hparam_rate_limit( - <::RuntimeOrigin>::root(), - 5 - )); - assert_eq!(pallet_subtensor::OwnerHyperparamRateLimit::::get(), 5); }); } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 504aa3bafd..b622f55501 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1009,12 +1009,6 @@ pub mod pallet { 10 } - /// Default number of tempos for owner hyperparameter update rate limit - #[pallet::type_value] - pub fn DefaultOwnerHyperparamRateLimit() -> u16 { - 2 - } - /// Default value for ck burn, 18%. #[pallet::type_value] pub fn DefaultCKBurn() -> u64 { @@ -1042,11 +1036,6 @@ pub mod pallet { pub type AdminFreezeWindow = StorageValue<_, u16, ValueQuery, DefaultAdminFreezeWindow>; - /// Global number of epochs used to rate limit subnet owner hyperparameter updates - #[pallet::storage] - pub type OwnerHyperparamRateLimit = - StorageValue<_, u16, ValueQuery, DefaultOwnerHyperparamRateLimit>; - /// Duration of coldkey swap schedule before execution #[pallet::storage] pub type ColdkeySwapScheduleDuration = diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 1f184dbc47..6eca06d088 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -109,8 +109,6 @@ mod events { TxChildKeyTakeRateLimitSet(u64), /// setting the admin freeze window length (last N blocks of tempo) AdminFreezeWindowSet(u16), - /// setting the owner hyperparameter rate limit in epochs - OwnerHyperparamRateLimitSet(u16), /// minimum childkey take set MinChildKeyTakeSet(u16), /// maximum childkey take set diff --git a/pallets/subtensor/src/tests/ensure.rs b/pallets/subtensor/src/tests/ensure.rs index 1253285306..05a89f3ce0 100644 --- a/pallets/subtensor/src/tests/ensure.rs +++ b/pallets/subtensor/src/tests/ensure.rs @@ -1,12 +1,10 @@ #![allow(clippy::expect_used)] -use frame_support::{assert_noop, assert_ok}; use frame_system::Config; use sp_core::U256; use subtensor_runtime_common::NetUid; use super::mock::*; -use crate::utils::rate_limiting::{Hyperparameter, TransactionType}; -use crate::{OwnerHyperparamRateLimit, SubnetOwner, SubtokenEnabled}; +use crate::SubnetOwner; #[test] fn ensure_subnet_owner_returns_who_and_checks_ownership() { @@ -79,81 +77,3 @@ fn ensure_admin_window_open_blocks_in_freeze_window() { assert!(crate::Pallet::::ensure_admin_window_open(netuid).is_ok()); }); } - -#[test] -fn ensure_owner_or_root_with_limits_checks_rl_and_freeze() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let tempo = 10; - add_network(netuid, 10, 0); - SubtokenEnabled::::insert(netuid, true); - let owner: U256 = U256::from(5); - SubnetOwner::::insert(netuid, owner); - // Set freeze window to 0 initially to avoid blocking when tempo is small - crate::Pallet::::set_admin_freeze_window(0); - - // Set tempo to 1 so owner hyperparam RL = 2 blocks - crate::Pallet::::set_tempo(netuid, 1); - - assert_eq!(OwnerHyperparamRateLimit::::get(), 2); - - // Outside freeze window initially; should pass and return Some(owner) - let res = crate::Pallet::::ensure_sn_owner_or_root_with_limits( - <::RuntimeOrigin>::signed(owner), - netuid, - &[Hyperparameter::Kappa.into()], - ) - .expect("should pass"); - assert_eq!(res, Some(owner)); - assert_ok!(crate::Pallet::::ensure_admin_window_open(netuid)); - - // Simulate previous update at current block -> next call should fail due to rate limit - let now = crate::Pallet::::get_current_block_as_u64(); - TransactionType::from(Hyperparameter::Kappa) - .set_last_block_on_subnet::(&owner, netuid, now); - assert_noop!( - crate::Pallet::::ensure_sn_owner_or_root_with_limits( - <::RuntimeOrigin>::signed(owner), - netuid, - &[Hyperparameter::Kappa.into()], - ), - crate::Error::::TxRateLimitExceeded - ); - - // Advance beyond RL and ensure passes again - run_to_block(now + 3); - TransactionType::from(Hyperparameter::Kappa) - .set_last_block_on_subnet::(&owner, netuid, 0); - assert_ok!(crate::Pallet::::ensure_sn_owner_or_root_with_limits( - <::RuntimeOrigin>::signed(owner), - netuid, - &[Hyperparameter::Kappa.into()] - )); - assert_ok!(crate::Pallet::::ensure_admin_window_open(netuid)); - - // Now advance into the freeze window; ensure blocks - // (using loop for clarity, because epoch calculation function uses netuid) - // Restore tempo and configure freeze window for this part - let freeze_window = 3; - crate::Pallet::::set_tempo(netuid, tempo); - crate::Pallet::::set_admin_freeze_window(freeze_window); - let freeze_window = freeze_window as u64; - loop { - let cur = crate::Pallet::::get_current_block_as_u64(); - let rem = crate::Pallet::::blocks_until_next_epoch(netuid, tempo, cur); - if rem < freeze_window { - break; - } - run_to_block(cur + 1); - } - assert_ok!(crate::Pallet::::ensure_sn_owner_or_root_with_limits( - <::RuntimeOrigin>::signed(owner), - netuid, - &[Hyperparameter::Kappa.into()] - )); - assert_noop!( - crate::Pallet::::ensure_admin_window_open(netuid), - crate::Error::::AdminActionProhibitedDuringWeightsWindow - ); - }); -} diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 6fd8d22ff6..cae54e1b53 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -37,24 +37,6 @@ impl Pallet { } } - /// Ensure owner-or-root with a set of TransactionType rate checks (owner only). - pub fn ensure_sn_owner_or_root_with_limits( - o: T::RuntimeOrigin, - netuid: NetUid, - limits: &[crate::utils::rate_limiting::TransactionType], - ) -> Result, DispatchError> { - let maybe_who = Self::ensure_subnet_owner_or_root(o, netuid)?; - if let Some(who) = maybe_who.as_ref() { - for tx in limits.iter() { - ensure!( - tx.passes_rate_limit_on_subnet::(who, netuid), - Error::::TxRateLimitExceeded - ); - } - } - Ok(maybe_who) - } - /// Returns true if the current block is within the terminal freeze window of the tempo for the /// given subnet. During this window, admin ops are prohibited to avoid interference with /// validator weight submissions. @@ -83,25 +65,6 @@ impl Pallet { Self::deposit_event(Event::AdminFreezeWindowSet(window)); } - pub fn set_owner_hyperparam_rate_limit(epochs: u16) { - OwnerHyperparamRateLimit::::set(epochs); - Self::deposit_event(Event::OwnerHyperparamRateLimitSet(epochs)); - } - - /// If owner is `Some`, record last-blocks for the provided `TransactionType`s. - pub fn record_owner_rl( - maybe_owner: Option<::AccountId>, - netuid: NetUid, - txs: &[TransactionType], - ) { - if let Some(who) = maybe_owner { - let now = Self::get_current_block_as_u64(); - for tx in txs { - tx.set_last_block_on_subnet::(&who, netuid, now); - } - } - } - // ======================== // ==== Global Setters ==== // ======================== diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index 946d980d57..2b07c5dffd 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -43,10 +43,10 @@ impl TransactionType { match self { Self::SetWeightsVersionKey => (Tempo::::get(netuid) as u64) .saturating_mul(WeightsVersionKeyRateLimit::::get()), - // Owner hyperparameter updates are rate-limited by N tempos on the subnet (sudo configurable) + // Owner hyperparameter updates are rate-limited by N tempos on the subnet (sudo + // configurable) Self::OwnerHyperparamUpdate(_) => { - let epochs = OwnerHyperparamRateLimit::::get() as u64; - (Tempo::::get(netuid) as u64).saturating_mul(epochs) + 0 /*DEPRECATED*/ } Self::SetSNOwnerHotkey => DefaultSetSNOwnerHotkeyRateLimit::::get(), @@ -102,9 +102,7 @@ impl TransactionType { Self::SetSNOwnerHotkey => { Pallet::::get_rate_limited_last_block(&RateLimitKey::SetSNOwnerHotkey(netuid)) } - Self::OwnerHyperparamUpdate(hparam) => Pallet::::get_rate_limited_last_block( - &RateLimitKey::OwnerHyperparamUpdate(netuid, *hparam), - ), + Self::OwnerHyperparamUpdate(_) => 0, // DEPRECATED _ => { let tx_type: u16 = (*self).into(); TransactionKeyLastBlock::::get((hotkey, netuid, tx_type)) @@ -121,15 +119,10 @@ impl TransactionType { block: u64, ) { match self { - Self::RegisterNetwork => { /*DEPRECATED*/ } Self::SetSNOwnerHotkey => Pallet::::set_rate_limited_last_block( &RateLimitKey::SetSNOwnerHotkey(netuid), block, ), - Self::OwnerHyperparamUpdate(hparam) => Pallet::::set_rate_limited_last_block( - &RateLimitKey::OwnerHyperparamUpdate(netuid, *hparam), - block, - ), _ => { let tx_type: u16 = (*self).into(); TransactionKeyLastBlock::::insert((key, netuid, tx_type), block); diff --git a/runtime/src/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs index abcfac0754..b8ff133511 100644 --- a/runtime/src/migrations/rate_limiting.rs +++ b/runtime/src/migrations/rate_limiting.rs @@ -1217,10 +1217,11 @@ mod tests { let tx_target = RateLimitTarget::Group(GROUP_SWAP_KEYS); let delegate_group = RateLimitTarget::Group(DELEGATE_TAKE_GROUP_ID); + // swap-keys migration adds +1 to preserve legacy <= behavior. assert_eq!( pallet_rate_limiting::Limits::::get(tx_target), Some(RateLimit::Global(RateLimitKind::Exact( - 10u64.saturated_into() + 11u64.saturated_into() ))) ); assert_eq!( @@ -1546,7 +1547,7 @@ mod tests { // Hyperparam (activity_cutoff) with tempo scaling. let hparam_span_epochs = 2u16; - pallet_subtensor::OwnerHyperparamRateLimit::::put(hparam_span_epochs); + legacy_storage::set_owner_hyperparam_rate_limit(hparam_span_epochs.into()); LastRateLimitedBlock::::insert( RateLimitKey::OwnerHyperparamUpdate( netuid, diff --git a/runtime/src/rate_limiting/legacy.rs b/runtime/src/rate_limiting/legacy.rs index dfdbdc41b7..deecad707e 100644 --- a/runtime/src/rate_limiting/legacy.rs +++ b/runtime/src/rate_limiting/legacy.rs @@ -8,6 +8,7 @@ use sp_io::{ hashing::twox_128, storage::{self as io_storage, next_key}, }; +use sp_runtime::traits::SaturatedConversion; use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; @@ -129,6 +130,12 @@ pub mod storage { (u64::from(value), reads) } + pub fn set_owner_hyperparam_rate_limit(span_epochs: u64) { + let key = storage_prefix(PALLET_PREFIX, b"OwnerHyperparamRateLimit"); + let value: u16 = span_epochs.saturated_into(); + io_storage::set(&key, &value.encode()); + } + pub fn weights_version_key_rate_limit() -> (u64, u64) { value_with_default( b"WeightsVersionKeyRateLimit",