Open
Conversation
13 tasks
gztensor
previously approved these changes
Feb 11, 2026
sam0x17
previously approved these changes
Feb 11, 2026
shamil-gadelshin
requested changes
Feb 12, 2026
Collaborator
shamil-gadelshin
left a comment
There was a problem hiding this comment.
A couple of comments:
- IMHO, we should break large PRs into meaningful pieces in the future.
- It makes sense to test it on a mainnet clone and verify that all limit classes are migrated successfully.
- I don't see the latest "add_stake_burn" (subnet_buyback) limit as part of the migration here.
- probably, we should use if-let-else instead of match-some-none.
- There are multiple broken comment numerations after the refactoring
Contributor
Author
Thank you for review! Replying to this one, will go next comments one by one afterwards.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR introduces
pallet-rate-limitingas a uniform rate-limiting solution and migrates a part of legacy rate limiting to this pallet.Rate limits are set with
pallet_rate_limiting::set_rate_limit(target, scope, limit).targetis either one call or a group of calls.scopeis optional context used to select the configured span (for example,netuid). If a call/group does not need context, it can be configured directly at runtime withtarget + limitand no resolver data.There are standalone calls and groups of calls. Groups are defined at runtime, and each call can be either standalone or within a group, but not both. Groups allow multiple calls to share rate-limiting behavior. Depending on mode (
ConfigOnly,UsageOnly,ConfigAndUsage), calls share config, usage tracking, or both.For calls that need additional context, resolvers provide it. Limits can be configured either globally per target, or scoped per target+scope. The role of
ScopeResolveris to provide that scope context (for example,netuid) so the extension can select the correct scoped limit entry.ScopeResolvercan also adjust span (for example, tempo scaling) and define bypass behavior.UsageResolverresolves usage key(s) soLastSeenis tracked with additional context (for example, per account/per subnet/per mechanism), not only by target.Enforcement happens via
UnwrappedRateLimitTransactionExtension. It first unwraps nested calls (sudo,proxy,utility,multisig), then delegates each inner call toRateLimitTransactionExtension. The extension checks the resolved target/scope and compares current block vsLastSeen. If within span, validation fails withInvalidTransaction::Custom(1). On successful dispatch, the extension writesLastSeenfor resolved usage key(s). This is what enforces rate limiting for subsequent calls.Other pallets should use
rate-limiting-interface(RateLimitingInterface) to read limits and last-seen state without depending on pallet internals. Writes through this interface should be avoided as much as possible because they introduce side-effects. The expected write path is the transaction extension itself. Manualset_last_seenwrites are only for cases where usage must be updated outside the normal rate-limited call path.pallet-rate-limitingis instanceable and is intended to be used as one instance per pallet, with local scope/usage types and resolvers. This runtime currently uses one centralized instance forpallet-subtensor+pallet-admin-utilsas a transitional setup. Migration and resolvers already exist for grouped and standalone legacy limits, but this PR migrates only grouped legacy limits (GroupedRateLimitingMigration). Standalone migration/cleanup is deferred to a follow-up PR.How to review:
I tried to organize this PR so that each major change is represented by a single commit, while avoiding meaningless commits as much as possible. You can refer to the commit history and review changes related to specific limits.
runtime/src/migrations/rate_limiting::commits_grouped. This is where you'll find everything considered "legacy" rate-limiting. You can then review each migration from the list separately.runtime/src/rate_limiting. There you'll find the resolver implementations and can review how different calls are bypassed, adjusted to tempo, and scoped (for limits: only byNetUid; for last-seen timestamp: various cases).runtime/tests/rate_limiting. These are integration tests at the extrinsic level, where transaction extensions are involved. You can verify whether rate-limiting behavior is consistent with legacy behavior and whether all cases are covered.Type of Change
Deprecated extrinsics with their equivalents in
pallet-rate-limitingpallet-admin-utils)pallet-rate-limiting::set_rate_limitparamssudo_set_tx_rate_limit(tx_rate_limit)target = Group(GROUP_SWAP_KEYS), scope = None, limit = Exact(tx_rate_limit)sudo_set_serving_rate_limit(netuid, serving_rate_limit)target = Group(GROUP_SERVE), scope = Some(netuid), limit = Exact(serving_rate_limit)sudo_set_weights_set_rate_limit(netuid, weights_set_rate_limit)target = Group(GROUP_WEIGHTS_SET), scope = Some(netuid), limit = Exact(weights_set_rate_limit)sudo_set_network_rate_limit(limit)target = Group(GROUP_REGISTER_NETWORK), scope = None, limit = Exact(limit)sudo_set_tx_delegate_take_rate_limit(tx_rate_limit)target = Group(GROUP_DELEGATE_TAKE), scope = None, limit = Exact(tx_rate_limit)sudo_set_owner_hparam_rate_limit(epochs)target = Group(GROUP_OWNER_HPARAMS), scope = Some(netuid), limit = Exact(epochs)You can find the values of
GROUP_*constants incommon/src/rate_limiting.rs.From the client's perspective, you can query
pallet-rate-limiting::Groupsstorage to list all groups with their IDs and configuration, orpallet-rate-limiting::GroupNameIndexto get the ID of a particular group by name.Removed storages from
pallet-subtensorOn the client side, use
pallet-rate-limiting::Limitsstorage to fetch limits, orpallet-rate-limiting::LastSeento fetch last-seen timestamps.NetworkRateLimit->Limits({ Group: GROUP_REGISTER_NETWORK })OwnerHyperparamRateLimit->Limits({ Group: GROUP_OWNER_HPARAMS })ServingRateLimit->Limits({ Group: GROUP_SERVE })then scoped value fornetuidStakingOperationRateLimiter->LastSeen({ Group: GROUP_STAKING_OPS }, { ColdkeyHotkeySubnet: { coldkey, hotkey, netuid } })TxDelegateTakeRateLimit->Limits({ Group: GROUP_DELEGATE_TAKE })TxRateLimit->Limits({ Group: GROUP_SWAP_KEYS })WeightsSetRateLimit->Limits({ Group: GROUP_WEIGHTS_SET })then scoped value fornetuidpallet-subtensor::Configchangestype RateLimiting: RateLimitingInterfaceInitialServingRateLimitInitialTxRateLimitInitialTxDelegateTakeRateLimitInitialNetworkRateLimitRemoved events
They moved to
pallet-rate-limiting::RateLimitSetevent like this:NetworkRateLimitSet->{ target: Group(GROUP_REGISTER_NETWORK), scope: None, limit: Exact(span) }OwnerHyperparamRateLimitSet->{ target: Group(GROUP_OWNER_HPARAMS), scope: None, limit: Exact(epochs) }ServingRateLimitSet->{ target: Group(GROUP_SERVE), scope: Some(netuid), limit: Exact(span) }TxDelegateTakeRateLimitSet->{ target: Group(GROUP_DELEGATE_TAKE), scope: None, limit: Exact(span) }TxRateLimitSet->{ target: Group(GROUP_SWAP_KEYS), scope: None, limit: Exact(span) }WeightsSetRateLimitSet->{ target: Group(GROUP_WEIGHTS_SET), scope: Some(netuid), limit: Exact(span) }Additional changes
get_network_lock_cost()now usesT::RateLimiting::last_seen(GROUP_REGISTER_NETWORK, None)instead of legacyRateLimitKey::NetworkLastRegistered.pallet_admin_utils::sudo_set_tx_rate_limit->pallet_rate_limiting::set_rate_limit(... GROUP_SWAP_KEYS)is now limit + 1:pallet_admin_utils::sudo_set_tx_rate_limit(N)) - delta <= limit: a swap was still blocked when exactlyNblocks had passed, and it became allowed only afterN + 1blocks (delta > limit);pallet_rate_limiting::set_rate_limit(... GROUP_SWAP_KEYS, Exact(S))) - delta < span: a swap is blocked while fewer thanSblocks have passed, and it is allowed at exactlySblocks (delta >= span).So to keep the same real wait time as old
N, you must setS = N + 1(N = 0stays0).The reason for this change is that
pallet-rate-limitinguses one unified comparison rule (delta < span) for all limits. We keep that consistent and compensate for this specific legacy boundary in migration (+1).Checklist
cargo fmtandcargo clippyto ensure my code is formatted and linted correctly