diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index 82c63e1356..0dcb7d4316 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -103,6 +103,8 @@ jobs: binary: release - test: zombienet_staking binary: fast + - test: zombienet_rate_limiting + binary: fast - test: zombienet_coldkey_swap binary: fast - test: zombienet_subnets diff --git a/.gitignore b/.gitignore index 91993ddf2d..b80f202034 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ scripts/specs/local.json # Node modules node_modules +e2e/.papi/ # Claude Code configuration -.claude \ No newline at end of file +.claude diff --git a/Cargo.lock b/Cargo.lock index 32d4c7655d..ffd226becd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8304,6 +8304,8 @@ dependencies = [ "pallet-balances", "pallet-commitments", "pallet-drand", + "pallet-rate-limiting", + "pallet-rate-limiting-rpc", "pallet-shield", "pallet-subtensor", "pallet-subtensor-swap-rpc", @@ -8421,6 +8423,8 @@ dependencies = [ "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-preimage", + "pallet-rate-limiting", + "pallet-rate-limiting-runtime-api", "pallet-registry", "pallet-safe-mode", "pallet-scheduler", @@ -8445,6 +8449,7 @@ dependencies = [ "rand_chacha 0.3.1", "safe-math", "scale-info", + "serde", "serde_json", "sha2 0.10.9", "smallvec", @@ -8854,6 +8859,7 @@ dependencies = [ "pallet-subtensor", "pallet-subtensor-swap", "parity-scale-codec", + "rate-limiting-interface", "scale-info", "sp-consensus-aura", "sp-consensus-grandpa", @@ -10351,6 +10357,51 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-rate-limiting" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "parity-scale-codec", + "rate-limiting-interface", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "subtensor-macros", + "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-macros", + "subtensor-runtime-common", +] + [[package]] name = "pallet-recovery" version = "41.0.0" @@ -10852,6 +10903,7 @@ dependencies = [ "polkadot-runtime-common", "rand 0.10.0", "rand_chacha 0.3.1", + "rate-limiting-interface", "safe-math", "scale-info", "serde", @@ -13772,6 +13824,18 @@ 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", + "subtensor-macros", +] + [[package]] name = "raw-cpuid" version = "11.6.0" @@ -18188,6 +18252,7 @@ dependencies = [ "pallet-subtensor-utility", "pallet-timestamp", "parity-scale-codec", + "rate-limiting-interface", "scale-info", "sp-core", "sp-io", @@ -18265,6 +18330,7 @@ dependencies = [ "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", "pallet-preimage", + "pallet-rate-limiting", "pallet-scheduler", "pallet-shield", "pallet-subtensor", @@ -18348,6 +18414,7 @@ dependencies = [ "pallet-subtensor-swap", "pallet-transaction-payment", "parity-scale-codec", + "rate-limiting-interface", "scale-info", "smallvec", "sp-consensus-aura", diff --git a/Cargo.toml b/Cargo.toml index 14ded6a4f9..1be7802e88 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", @@ -64,6 +66,9 @@ 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 } +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 } @@ -75,6 +80,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" } stp-shield = { git = "https://github.com/opentensor/polkadot-sdk.git", rev = "7cc54bf2d50ae3921d718736dfeb0de9468539c7", default-features = false } stc-shield = { git = "https://github.com/opentensor/polkadot-sdk.git", rev = "7cc54bf2d50ae3921d718736dfeb0de9468539c7", default-features = false } diff --git a/chain-extensions/Cargo.toml b/chain-extensions/Cargo.toml index ecc30878b5..a8d33f6b69 100644 --- a/chain-extensions/Cargo.toml +++ b/chain-extensions/Cargo.toml @@ -37,6 +37,9 @@ subtensor-swap-interface.workspace = true num_enum.workspace = true substrate-fixed.workspace = true +[dev-dependencies] +rate-limiting-interface.workspace = true + [lints] workspace = true @@ -68,6 +71,7 @@ std = [ "subtensor-swap-interface/std", "num_enum/std", "substrate-fixed/std", + "rate-limiting-interface/std", ] runtime-benchmarks = [ "frame-support/runtime-benchmarks", diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 9c4b3bd4a6..ce15daa8d4 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -19,6 +19,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::{RateLimitingInterface, TryIntoRateLimitTarget}; use sp_core::{ConstU64, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; use sp_runtime::{ @@ -28,6 +29,7 @@ use sp_runtime::{ use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; use subtensor_runtime_common::{ AlphaBalance, AuthorshipInfo, NetUid, Saturating, TaoBalance, Token, + rate_limiting::RateLimitUsageKey, }; type Block = frame_system::mocking::MockBlock; @@ -305,9 +307,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 pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -333,7 +332,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 @@ -392,9 +390,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; type InitialMaxBurn = InitialMaxBurn; type InitialMinBurn = InitialMinBurn; @@ -405,7 +400,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; @@ -427,6 +421,7 @@ impl pallet_subtensor::Config for Test { type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; + type RateLimiting = NoRateLimiting; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; @@ -471,6 +466,42 @@ impl CommitmentsInterface for CommitmentsI { fn purge_netuid(_netuid: NetUid) {} } +pub struct NoRateLimiting; + +impl RateLimitingInterface 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 + } + + fn set_last_seen( + _target: TargetArg, + _usage_key: Option, + _block: Option, + ) where + TargetArg: TryIntoRateLimitTarget, + { + } +} + parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; @@ -763,11 +794,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: TaoBalance, alpha: AlphaBalance) { SubnetTAO::::set(netuid, tao); diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index 08a9e17f77..eeac0ce451 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -106,8 +106,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 = <::WeightInfo as SubtensorWeightInfo>::remove_stake_full_limit(); let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); @@ -170,8 +168,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, @@ -241,8 +237,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, @@ -382,8 +376,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, @@ -454,8 +446,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, @@ -533,8 +523,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, @@ -608,8 +596,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 = <::WeightInfo as SubtensorWeightInfo>::unstake_all_alpha(); let mut env = MockEnv::new(FunctionId::UnstakeAllAlphaV1, coldkey, hotkey.encode()) @@ -1579,8 +1565,6 @@ fn unstake_all_success_unstakes_balance() { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all(); let pre_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); @@ -1752,8 +1736,6 @@ mod caller_dispatch_tests { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all(); let pre_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); @@ -1806,8 +1788,6 @@ mod caller_dispatch_tests { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all_alpha(); let mut env = MockEnv::new( @@ -1870,8 +1850,6 @@ mod caller_dispatch_tests { 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, @@ -1950,8 +1928,6 @@ mod caller_dispatch_tests { 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, @@ -2039,8 +2015,6 @@ mod caller_dispatch_tests { 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, @@ -2171,8 +2145,6 @@ mod caller_dispatch_tests { 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, @@ -2251,8 +2223,6 @@ mod caller_dispatch_tests { 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, @@ -2321,8 +2291,6 @@ mod caller_dispatch_tests { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake_full_limit(); let balance_before = diff --git a/common/src/lib.rs b/common/src/lib.rs index a606dca71d..3b16ed567f 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -20,6 +20,7 @@ pub use transaction_error::*; mod currency; mod evm_context; +pub mod rate_limiting; mod transaction_error; /// Balance of an account. diff --git a/common/src/rate_limiting.rs b/common/src/rate_limiting.rs new file mode 100644 index 0000000000..16f818d5c9 --- /dev/null +++ b/common/src/rate_limiting.rs @@ -0,0 +1,109 @@ +//! 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 scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; + +use crate::{MechId, NetUid}; + +/// 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_SET: 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; + +/// 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/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/get-metadata.sh b/contract-tests/get-metadata.sh index 64d76bff29..bd24051922 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 --check-files diff --git a/contract-tests/src/subtensor.ts b/contract-tests/src/subtensor.ts index 478148909d..a941d34ace 100644 --- a/contract-tests/src/subtensor.ts +++ b/contract-tests/src/subtensor.ts @@ -8,16 +8,28 @@ import { tao } from './balance-math' import internal from "stream"; import { createCodec } from "scale-ts"; +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 = rateLimitTargetGroup(registerNetworkGroupId); + 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 rateLimit = BigInt(limits.value.value); const defaultNetworkLastLockCost = await api.query.SubtensorModule.NetworkLastLockCost.getValue() - - const rateLimit = await api.query.SubtensorModule.NetworkRateLimit.getValue() 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: rateLimitKindExact(0), + }) const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) await waitForTransactionWithRetry(api, tx, alice) } @@ -72,17 +84,32 @@ 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 = rateLimitTargetGroup(weightsSetGroupId); + const limits = await api.query.RateLimiting.Limits.getValue(target as any) as any; + 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.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: rateLimitKindExact(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?.type === "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 @@ -173,16 +200,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) } @@ -382,16 +417,28 @@ export async function disableAdminFreezeWindowAndOwnerHyperparamRateLimit(api: T await waitForTransactionWithRetry(api, sudoFreezeTx, alice) } - const currentOwnerHyperparamRateLimit = await api.query.SubtensorModule.OwnerHyperparamRateLimit.getValue() - if (currentOwnerHyperparamRateLimit !== 0) { - // Set OwnerHyperparamRateLimit to 0 - const setOwnerRateLimit = api.tx.AdminUtils.sudo_set_owner_hparam_rate_limit({ epochs: 0 }) + const ownerHparamsGroupId = 4; // GROUP_OWNER_HPARAMS constant + const target = rateLimitTargetGroup(ownerHparamsGroupId); + const limits = await api.query.RateLimiting.Limits.getValue(target as any) as any; + const currentLimit = + limits?.type === "Global" && limits.value?.type === "Exact" + ? BigInt(limits.value.value) + : BigInt(0); + if (currentLimit !== BigInt(0)) { + const setOwnerRateLimit = api.tx.RateLimiting.set_rate_limit({ + target: target as any, + scope: undefined, + limit: rateLimitKindExact(0), + }) const sudoOwnerRateTx = api.tx.Sudo.sudo({ call: setOwnerRateLimit.decodedCall }) await waitForTransactionWithRetry(api, sudoOwnerRateTx, alice) } assert.equal(0, await api.query.SubtensorModule.AdminFreezeWindow.getValue()) - assert.equal(BigInt(0), await api.query.SubtensorModule.OwnerHyperparamRateLimit.getValue()) + const updated = await api.query.RateLimiting.Limits.getValue(target as any) as any; + assert.ok(updated?.type === "Global"); + assert.ok(updated.value?.type === "Exact"); + assert.equal(BigInt(0), BigInt(updated.value.value)); } export async function sendWasmContractExtrinsic(api: TypedApi, coldkey: KeyPair, contractAddress: string, data: Binary) { diff --git a/contract-tests/test/subnet.precompile.hyperparameter.test.ts b/contract-tests/test/subnet.precompile.hyperparameter.test.ts new file mode 100644 index 0000000000..616e3f9041 --- /dev/null +++ b/contract-tests/test/subnet.precompile.hyperparameter.test.ts @@ -0,0 +1,596 @@ +import * as assert from "assert"; + +import { getAliceSigner, getDevnetApi, getRandomSubstrateKeypair, waitForTransactionWithRetry } from "../src/substrate" +import { devnet } from "@polkadot-api/descriptors" +import { Binary, Enum, FixedSizeBinary, 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" +import { ethers } from "ethers" +import { disableAdminFreezeWindowAndOwnerHyperparamRateLimit, forceSetBalanceToEthAddress, forceSetBalanceToSs58Address } from "../src/subtensor" +import { blake2AsU8a } from "@polkadot/util-crypto" + +describe("Test the Subnet precompile contract", () => { + // init eth part + const wallet = generateRandomEthersWallet(); + // init substrate part + + const hotkey1 = getRandomSubstrateKeypair(); + const hotkey2 = getRandomSubstrateKeypair(); + const hotkey3 = getRandomSubstrateKeypair(); + let api: TypedApi + + before(async () => { + // init variables got from await and async + api = await getDevnetApi() + + 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 () => { + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const tx = await contract.registerNetwork(hotkey1.publicKey); + await tx.wait(); + + const totalNetworkAfterAdd = await api.query.SubtensorModule.TotalNetworks.getValue() + assert.ok(totalNetwork + 1 === totalNetworkAfterAdd) + }); + + it("Can register network with identity info", async () => { + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const tx = await contract.registerNetwork(hotkey2.publicKey, + "name", + "repo", + "contact", + "subnetUrl", + "discord", + "description", + "additional" + ); + await tx.wait(); + + const totalNetworkAfterAdd = await api.query.SubtensorModule.TotalNetworks.getValue() + assert.ok(totalNetwork + 1 === totalNetworkAfterAdd) + }); + + // it.only("Can register network with identity info and logo url", async () => { + // const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + + // const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + + // const tx = await contract["registerNetwork(bytes32,string,string,string,string,string,string,string,string)"]( + // hotkey2.publicKey, + // "name", + // "repo", + // "contact", + // "subnetUrl", + // "discord", + // "description", + // "logoUrl", + // "additional" + // ); + // await tx.wait(); + + // const totalNetworkAfterAdd = await api.query.SubtensorModule.TotalNetworks.getValue() + // assert.ok(totalNetwork + 1 === totalNetworkAfterAdd) + // }); + + it("Can set servingRateLimit parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = 100; + const tx = await contract.setServingRateLimit(netuid, newValue); + await tx.wait(); + + 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]?.type === "Exact"); + const onchainValue = Number(entry[1].value); + + let valueFromContract = Number( + await contract.getServingRateLimit(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + + // minDifficulty hyperparameter + // + // disabled: only by sudo + // + // newValue = 101; + // tx = await contract.setMinDifficulty(netuid, newValue); + // await tx.wait(); + + // await usingApi(async (api) => { + // onchainValue = Number( + // await api.query.subtensorModule.minDifficulty(netuid) + // ); + // }); + + // valueFromContract = Number(await contract.getMinDifficulty(netuid)); + + // expect(valueFromContract).to.eq(newValue); + // expect(valueFromContract).to.eq(onchainValue); + + it("Can set maxDifficulty parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = 102; + const tx = await contract.setMaxDifficulty(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.MaxDifficulty.getValue(netuid) + + + let valueFromContract = Number( + await contract.getMaxDifficulty(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + + it("Can set weightsVersionKey parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = 103; + const tx = await contract.setWeightsVersionKey(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.WeightsVersionKey.getValue(netuid) + + + let valueFromContract = Number( + await contract.getWeightsVersionKey(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + // need sudo as origin now + // it("Can set weightsSetRateLimit parameter", async () => { + + // const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + // const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + // const netuid = totalNetwork - 1; + + // const newValue = 104; + // const tx = await contract.setWeightsSetRateLimit(netuid, newValue); + // await tx.wait(); + + // let onchainValue = await api.query.SubtensorModule.WeightsSetRateLimit.getValue(netuid) + + + // let valueFromContract = Number( + // await contract.getWeightsSetRateLimit(netuid) + // ); + + // assert.equal(valueFromContract, newValue) + // assert.equal(valueFromContract, onchainValue); + // }) + + it("Can set adjustmentAlpha parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = 105; + const tx = await contract.setAdjustmentAlpha(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.AdjustmentAlpha.getValue(netuid) + + + let valueFromContract = Number( + await contract.getAdjustmentAlpha(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + it("Returns constant maxWeightLimit", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const valueFromContract = Number( + await contract.getMaxWeightLimit(netuid) + ); + + assert.equal(valueFromContract, 0xFFFF) + }) + + it("Can set immunityPeriod parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = 107; + const tx = await contract.setImmunityPeriod(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.ImmunityPeriod.getValue(netuid) + + + let valueFromContract = Number( + await contract.getImmunityPeriod(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + it("Can set minAllowedWeights parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = 108; + const tx = await contract.setMinAllowedWeights(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.MinAllowedWeights.getValue(netuid) + + + let valueFromContract = Number( + await contract.getMinAllowedWeights(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + // disable the set kappa parameter test, because it is only callable by sudo now + // it("Can set kappa parameter", async () => { + + // const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + // const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + // const netuid = totalNetwork - 1; + + // const newValue = 109; + // const tx = await contract.setKappa(netuid, newValue); + // await tx.wait(); + + // let onchainValue = await api.query.SubtensorModule.Kappa.getValue(netuid) + + + // let valueFromContract = Number( + // await contract.getKappa(netuid) + // ); + + // assert.equal(valueFromContract, newValue) + // assert.equal(valueFromContract, onchainValue); + // }) + + it("Can set rho parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = 110; + const tx = await contract.setRho(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.Rho.getValue(netuid) + + + let valueFromContract = Number( + await contract.getRho(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + it("Can set activityCutoff parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + const newValue = await api.query.SubtensorModule.MinActivityCutoff.getValue() + 1; + const tx = await contract.setActivityCutoff(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.ActivityCutoff.getValue(netuid) + + + let valueFromContract = Number( + await contract.getActivityCutoff(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + // it("Can set networkRegistrationAllowed parameter", async () => { + + // const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + // const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + // const netuid = totalNetwork - 1; + + // const newValue = true; + // const tx = await contract.setNetworkRegistrationAllowed(netuid, newValue); + // await tx.wait(); + + // let onchainValue = await api.query.SubtensorModule.NetworkRegistrationAllowed.getValue(netuid) + + + // let valueFromContract = Boolean( + // await contract.getNetworkRegistrationAllowed(netuid) + // ); + + // assert.equal(valueFromContract, newValue) + // assert.equal(valueFromContract, onchainValue); + // }) + + // it("Can set networkPowRegistrationAllowed parameter", async () => { + + // const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + // const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + // const netuid = totalNetwork - 1; + + // const newValue = true; + // const tx = await contract.setNetworkPowRegistrationAllowed(netuid, newValue); + // await tx.wait(); + + // let onchainValue = await api.query.SubtensorModule.NetworkPowRegistrationAllowed.getValue(netuid) + + + // let valueFromContract = Boolean( + // await contract.getNetworkPowRegistrationAllowed(netuid) + // ); + + // assert.equal(valueFromContract, newValue) + // assert.equal(valueFromContract, onchainValue); + // }) + + // minBurn hyperparameter. only sudo can set it now + // newValue = 112; + + // tx = await contract.setMinBurn(netuid, newValue); + // await tx.wait(); + + // await usingApi(async (api) => { + // onchainValue = Number( + // await api.query.subtensorModule.minBurn(netuid) + // ); + // }); + + // valueFromContract = Number(await contract.getMinBurn(netuid)); + + // expect(valueFromContract).to.eq(newValue); + // expect(valueFromContract).to.eq(onchainValue); + + // maxBurn hyperparameter. only sudo can set it now + // it("Can set maxBurn parameter", async () => { + + // const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + // const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + // const netuid = totalNetwork - 1; + + // const newValue = 113; + // const tx = await contract.setMaxBurn(netuid, newValue); + // await tx.wait(); + + // let onchainValue = await api.query.SubtensorModule.MaxBurn.getValue(netuid) + + + // let valueFromContract = Number( + // await contract.getMaxBurn(netuid) + // ); + + // assert.equal(valueFromContract, newValue) + // assert.equal(valueFromContract, onchainValue); + // }) + + + // difficulty hyperparameter (disabled: sudo only) + // newValue = 114; + + // tx = await contract.setDifficulty(netuid, newValue); + // await tx.wait(); + + // await usingApi(async (api) => { + // onchainValue = Number( + // await api.query.subtensorModule.difficulty(netuid) + // ); + // }); + + // valueFromContract = Number(await contract.getDifficulty(netuid)); + + // expect(valueFromContract).to.eq(newValue); + // expect(valueFromContract).to.eq(onchainValue); + + it("Can set bondsMovingAverage parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = 115; + const tx = await contract.setBondsMovingAverage(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.BondsMovingAverage.getValue(netuid) + + + let valueFromContract = Number( + await contract.getBondsMovingAverage(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + it("Can set commitRevealWeightsEnabled parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = true; + const tx = await contract.setCommitRevealWeightsEnabled(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.CommitRevealWeightsEnabled.getValue(netuid) + + + let valueFromContract = Boolean( + await contract.getCommitRevealWeightsEnabled(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + it("Can set liquidAlphaEnabled parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = true; + const tx = await contract.setLiquidAlphaEnabled(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.LiquidAlphaOn.getValue(netuid) + + + let valueFromContract = Boolean( + await contract.getLiquidAlphaEnabled(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + it("Can set yuma3Enabled hyperparameter", async () => { + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = true; + const tx = await contract.setYuma3Enabled(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.Yuma3On.getValue(netuid) + + let valueFromContract = Boolean( + await contract.getYuma3Enabled(netuid) + ); + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + + // it("Can set alphaValues parameter", async () => { + // const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + // const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + // const netuid = totalNetwork - 1; + + // const newValue = [118, 52429]; + // const tx = await contract.setAlphaValues(netuid, newValue[0], newValue[1]); + // await tx.wait(); + + // let onchainValue = await api.query.SubtensorModule.AlphaV2Values.getValue(netuid) + + // let value = await contract.getAlphaValues(netuid) + // let valueFromContract = [Number(value[0]), Number(value[1])] + + // assert.equal(valueFromContract[0], newValue[0]) + // assert.equal(valueFromContract[1], newValue[1]) + // assert.equal(valueFromContract[0], onchainValue[0]); + // assert.equal(valueFromContract[1], onchainValue[1]); + // }) + + it("Can set commitRevealWeightsInterval parameter", async () => { + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const newValue = 99; + const tx = await contract.setCommitRevealWeightsInterval(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.RevealPeriodEpochs.getValue(netuid) + + let valueFromContract = Number( + await contract.getCommitRevealWeightsInterval(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + }) + + it("Rejects subnet precompile calls when coldkey swap is scheduled (tx extension)", async () => { + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + const coldkeySs58 = convertH160ToSS58(wallet.address) + const newColdkeyHash = FixedSizeBinary.fromBytes(blake2AsU8a(hotkey1.publicKey)) + const currentBlock = await api.query.System.Number.getValue() + const executionBlock = currentBlock + 10 + + const codec = await getTypedCodecs(devnet); + const valueBytes = codec.query.SubtensorModule.ColdkeySwapAnnouncements.value.enc([ + executionBlock, + newColdkeyHash + ]) + const key = await api.query.SubtensorModule.ColdkeySwapAnnouncements.getKey(coldkeySs58); + + // Use sudo + set_storage since the swap-scheduled check only exists in the tx extension. + const setStorageCall = api.tx.System.set_storage({ + items: [[Binary.fromHex(key), Binary.fromBytes(valueBytes)]], + }) + const sudoTx = api.tx.Sudo.sudo({ call: setStorageCall.decodedCall }) + await waitForTransactionWithRetry(api, sudoTx, getAliceSigner()) + + const storedValue = await api.query.SubtensorModule.ColdkeySwapAnnouncements.getValue(coldkeySs58) + assert.equal(storedValue?.[0], executionBlock) + assert.equal(storedValue?.[1].asHex(), newColdkeyHash.asHex()) + + await assert.rejects(async () => { + const tx = await contract.setServingRateLimit(netuid, 100); + await tx.wait(); + }) + }) +}) diff --git a/contract-tests/test/wasm.contract.test.ts b/contract-tests/test/wasm.contract.test.ts index cd78c8d942..8e11a55a44 100644 --- a/contract-tests/test/wasm.contract.test.ts +++ b/contract-tests/test/wasm.contract.test.ts @@ -5,13 +5,27 @@ import * as assert from "assert"; import fs from "fs"; import { Binary, TypedApi } from "polkadot-api"; 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 { tao } from "../src/balance-math"; import { getDevnetApi, getRandomSubstrateKeypair, getSignerFromKeypair, waitForTransactionWithRetry } from "../src/substrate"; import { addNewSubnetwork, burnedRegister, forceSetBalanceToSs58Address, sendWasmContractExtrinsic, setAdminFreezeWindow, setTargetRegistrationsPerInterval, startCall } from "../src/subtensor"; -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", () => { @@ -80,7 +94,8 @@ describe("Test wasm contract", () => { } before(async () => { - // init variables got from await and async + bittensorBytecode = loadBittensorBytecode() + // init variables got from await and async api = await getDevnetApi() await setAdminFreezeWindow(api); @@ -920,4 +935,4 @@ describe("Test wasm contract", () => { proxies = await api.query.Proxy.Proxies.getValue(convertPublicKeyToSs58(coldkey.publicKey)) assert.ok(proxies !== undefined && proxies[0].length === 0) }) -}); \ No newline at end of file +}); diff --git a/eco-tests/Cargo.toml b/eco-tests/Cargo.toml index f93c81386a..f02755cd21 100644 --- a/eco-tests/Cargo.toml +++ b/eco-tests/Cargo.toml @@ -18,6 +18,7 @@ unwrap-used = "deny" useless_conversion = "allow" [dependencies] +rate-limiting-interface = { default-features = false, path = "../pallets/rate-limiting-interface", features = ["std"] } pallet-subtensor = { path = "../pallets/subtensor", default-features = false, features = ["std"] } pallet-alpha-assets = { path = "../pallets/alpha-assets", default-features = false, features = ["std"] } frame-support = { git = "https://github.com/opentensor/polkadot-sdk.git", rev = "7cc54bf2d50ae3921d718736dfeb0de9468539c7", default-features = false, features = ["std"] } diff --git a/eco-tests/src/helpers.rs b/eco-tests/src/helpers.rs index c6fa0ec72d..e2a72fca12 100644 --- a/eco-tests/src/helpers.rs +++ b/eco-tests/src/helpers.rs @@ -322,10 +322,6 @@ pub fn increase_stake_on_hotkey_account(hotkey: &U256, increment: TaoBalance, ne ); } -pub fn remove_stake_rate_limit_for_tests(hotkey: &U256, coldkey: &U256, netuid: NetUid) { - StakingOperationRateLimiter::::remove((hotkey, coldkey, netuid)); -} - pub fn setup_reserves(netuid: NetUid, tao: TaoBalance, alpha: AlphaBalance) { SubnetTAO::::set(netuid, tao); SubnetAlphaIn::::set(netuid, alpha); @@ -403,8 +399,6 @@ pub fn assert_last_event( } 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 = H256::from_low_u64_be(0xDEAD_BEEF); assert_ok!(SubtensorModule::do_commit_weights( diff --git a/eco-tests/src/mock.rs b/eco-tests/src/mock.rs index 9ab48c12a7..fbe0a1231e 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -26,8 +26,9 @@ use sp_runtime::{ }; use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; use sp_tracing::tracing_subscriber; -use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoBalance}; +use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoBalance, rate_limiting::RateLimitUsageKey}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; +use rate_limiting_interface::{RateLimitingInterface, TryIntoRateLimitTarget}; type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. @@ -188,9 +189,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 pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -216,7 +214,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 @@ -238,11 +235,48 @@ parameter_types! { pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); } +pub struct NoRateLimiting; + +impl RateLimitingInterface 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 + } + + fn set_last_seen( + _target: TargetArg, + _usage_key: Option, + _block: Option, + ) where + TargetArg: TryIntoRateLimitTarget, + { + } +} + impl pallet_subtensor::Config for Test { type RuntimeCall = RuntimeCall; type Currency = Balances; type InitialIssuance = InitialIssuance; type SudoRuntimeCall = TestRuntimeCall; + type RateLimiting = NoRateLimiting; type Scheduler = Scheduler; type InitialMinAllowedWeights = InitialMinAllowedWeights; type InitialEmissionValue = InitialEmissionValue; @@ -275,9 +309,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; type InitialMaxBurn = InitialMaxBurn; type InitialMinBurn = InitialMinBurn; @@ -288,7 +319,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/node/Cargo.toml b/node/Cargo.toml index d067eb19c8..d740137dd3 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -73,6 +73,7 @@ pallet-balances.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", ] } @@ -128,6 +129,7 @@ node-subtensor-runtime = { workspace = true, features = ["std"] } subtensor-runtime-common = { workspace = true, features = ["std"] } subtensor-custom-rpc = { workspace = true, features = ["std"] } subtensor-custom-rpc-runtime-api = { workspace = true, features = ["std"] } +pallet-rate-limiting-rpc = { workspace = true, features = ["std"] } pallet-subtensor-swap-rpc = { workspace = true, features = ["std"] } pallet-subtensor-swap-runtime-api = { workspace = true, features = ["std"] } subtensor-macros.workspace = true @@ -168,6 +170,7 @@ runtime-benchmarks = [ "polkadot-sdk/runtime-benchmarks", "pallet-subtensor/runtime-benchmarks", "pallet-shield/runtime-benchmarks", + "pallet-rate-limiting/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "subtensor-runtime-common/runtime-benchmarks", ] @@ -182,6 +185,7 @@ try-runtime = [ "frame-system/try-runtime", "pallet-transaction-payment/try-runtime", "sp-runtime/try-runtime", + "pallet-rate-limiting/try-runtime", "pallet-commitments/try-runtime", "pallet-drand/try-runtime", "polkadot-sdk/try-runtime", diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index d0c0ac9a40..88fa64007d 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -141,6 +141,7 @@ pub fn create_benchmark_extrinsic( pallet_shield::CheckShieldedTxValidity::::new(), pallet_subtensor::SubtensorTransactionExtension::::new(), pallet_drand::drand_priority::DrandPriority::::new(), + runtime::rate_limiting::UnwrappedRateLimitTransactionExtension::new(), ), frame_metadata_hash_extension::CheckMetadataHash::::new(true), ); @@ -158,7 +159,7 @@ pub fn create_benchmark_extrinsic( (), (), ), - ((), (), (), (), ()), + ((), (), (), (), (), ()), None, ), ); diff --git a/node/src/chain_spec/devnet.rs b/node/src/chain_spec/devnet.rs index cb3bc66924..6771f71c08 100644 --- a/node/src/chain_spec/devnet.rs +++ b/node/src/chain_spec/devnet.rs @@ -2,6 +2,14 @@ #![allow(clippy::unwrap_used)] 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_OWNER_HPARAMS, GROUP_REGISTER_NETWORK, GROUP_SERVE, + GROUP_STAKING_OPS, GROUP_SWAP_KEYS, GROUP_WEIGHTS_SET, + }, +}; pub fn devnet_config() -> Result { let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; @@ -89,5 +97,33 @@ fn devnet_genesis( "sudo": { "key": Some(root_key), }, + "rateLimiting": { + "defaultLimit": 0, + "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_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_OWNER_HPARAMS }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::owner_hyperparam_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_STAKING_OPS, b"staking-ops".to_vec(), "ConfigAndUsage"), + (GROUP_SWAP_KEYS, b"swap-keys".to_vec(), "ConfigAndUsage"), + (GROUP_OWNER_HPARAMS, b"owner-hparams".to_vec(), "ConfigOnly"), + (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 57a60bbd1b..53b40c2c2a 100644 --- a/node/src/chain_spec/localnet.rs +++ b/node/src/chain_spec/localnet.rs @@ -2,6 +2,14 @@ #![allow(clippy::unwrap_used)] 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_OWNER_HPARAMS, GROUP_REGISTER_NETWORK, GROUP_SERVE, + GROUP_STAKING_OPS, GROUP_SWAP_KEYS, 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())?; @@ -133,5 +141,33 @@ fn localnet_genesis( "evmChainId": { "chainId": 42, }, + "rateLimiting": { + "defaultLimit": 0, + "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_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_OWNER_HPARAMS }), Option::::None, serde_json::json!({ "Exact": rate_limit_defaults::owner_hyperparam_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_STAKING_OPS, b"staking-ops".to_vec(), "ConfigAndUsage"), + (GROUP_SWAP_KEYS, b"swap-keys".to_vec(), "ConfigAndUsage"), + (GROUP_OWNER_HPARAMS, b"owner-hparams".to_vec(), "ConfigOnly"), + (GROUP_WEIGHTS_SET, b"weights".to_vec(), "ConfigAndUsage"), + ], + "limitSettingRules": vec![ + (serde_json::json!({ "Group": GROUP_SERVE }), "RootOrSubnetOwnerAdminWindow"), + ], + }, }) } diff --git a/node/src/rpc.rs b/node/src/rpc.rs index e34826462f..ce73b4f623 100644 --- a/node/src/rpc.rs +++ b/node/src/rpc.rs @@ -120,6 +120,7 @@ where CIDP: CreateInherentDataProviders + Send + Clone + 'static, CT: fp_rpc::ConvertTransaction<::Extrinsic> + Send + Sync + Clone + 'static, { + use pallet_rate_limiting_rpc::{RateLimiting, RateLimitingRpcApiServer}; use pallet_subtensor_swap_rpc::{Swap, SwapRpcApiServer}; use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer}; use sc_consensus_manual_seal::rpc::{ManualSeal, ManualSealApiServer}; @@ -137,6 +138,9 @@ where // Custom RPC methods for Paratensor module.merge(SubtensorCustom::new(client.clone()).into_rpc())?; + // Rate limiting RPC + module.merge(RateLimiting::new(client.clone()).into_rpc())?; + // Swap RPC module.merge(Swap::new(client.clone()).into_rpc())?; diff --git a/pallets/admin-utils/Cargo.toml b/pallets/admin-utils/Cargo.toml index 85236a425a..513e30a0ab 100644 --- a/pallets/admin-utils/Cargo.toml +++ b/pallets/admin-utils/Cargo.toml @@ -39,6 +39,7 @@ sp-io.workspace = true sp-tracing.workspace = true sp-consensus-aura.workspace = true pallet-alpha-assets.workspace = true +rate-limiting-interface.workspace = true pallet-balances = { workspace = true, features = ["std"] } pallet-scheduler.workspace = true pallet-grandpa.workspace = true @@ -77,6 +78,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/benchmarking.rs b/pallets/admin-utils/src/benchmarking.rs index 646e480e07..bd64f9a292 100644 --- a/pallets/admin-utils/src/benchmarking.rs +++ b/pallets/admin-utils/src/benchmarking.rs @@ -66,14 +66,6 @@ mod benchmarks { _(RawOrigin::Root, 100u16/*default_take*/)/*sudo_set_default_take*/; } - #[benchmark] - 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*/; - } - #[benchmark] fn sudo_set_max_difficulty() { // disable admin freeze window @@ -100,19 +92,6 @@ mod benchmarks { _(RawOrigin::Root, 1u16.into()/*netuid*/, 1000u64/*min_difficulty*/)/*sudo_set_min_difficulty*/; } - #[benchmark] - fn sudo_set_weights_set_rate_limit() { - // disable admin freeze window - pallet_subtensor::Pallet::::set_admin_freeze_window(0); - pallet_subtensor::Pallet::::init_new_network( - 1u16.into(), /*netuid*/ - 1u16, /*tempo*/ - ); - - #[extrinsic_call] - _(RawOrigin::Root, 1u16.into()/*netuid*/, 3u64/*rate_limit*/)/*sudo_set_weights_set_rate_limit*/; - } - #[benchmark] fn sudo_set_weights_version_key() { // disable admin freeze window @@ -410,12 +389,6 @@ mod benchmarks { _(RawOrigin::Root, 5u16/*version*/)/*sudo_set_commit_reveal_version()*/; } - #[benchmark] - fn sudo_set_tx_rate_limit() { - #[extrinsic_call] - _(RawOrigin::Root, 100u64); - } - #[benchmark] fn sudo_set_total_issuance() { let call = Call::::sudo_set_total_issuance { @@ -454,12 +427,6 @@ mod benchmarks { _(RawOrigin::Root, 100u64); } - #[benchmark] - fn sudo_set_tx_delegate_take_rate_limit() { - #[extrinsic_call] - _(RawOrigin::Root, 100u64); - } - #[benchmark] fn sudo_set_min_delegate_take() { #[extrinsic_call] @@ -625,14 +592,6 @@ mod benchmarks { _(RawOrigin::Root, 5u16/*window*/)/*sudo_set_admin_freeze_window*/; } - #[benchmark] - fn sudo_set_owner_hparam_rate_limit() { - // disable admin freeze window - pallet_subtensor::Pallet::::set_admin_freeze_window(0); - #[extrinsic_call] - _(RawOrigin::Root, 2u16/*epochs*/)/*sudo_set_owner_hparam_rate_limit*/; - } - #[benchmark] fn sudo_set_owner_immune_neuron_limit() { // disable admin freeze window diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index ccf047b2b3..1c85f09c02 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -149,7 +149,7 @@ pub mod pallet { NotPermittedOnRootSubnet, /// POW Registration has been deprecated POWRegistrationDisabled, - /// Call is deprecated + /// The called extrinsic has been deprecated. Deprecated, } /// Enum for specifying the type of precompile operation. @@ -239,39 +239,43 @@ 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(::WeightInfo::sudo_set_tx_rate_limit())] - 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(()) + #[pallet::weight( + (Weight::from_parts(5_400_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)), + 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 { + Err(Error::::Deprecated.into()) } /// 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(::WeightInfo::sudo_set_serving_rate_limit())] + #[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. @@ -308,11 +312,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!( @@ -323,11 +323,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(()) } @@ -342,7 +337,7 @@ pub mod pallet { weights_version_key: u64, ) -> DispatchResult { let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin.clone(), + origin, netuid, &[TransactionType::SetWeightsVersionKey], )?; @@ -369,27 +364,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_SET`) with `scope = Some(netuid)`. #[pallet::call_index(7)] - #[pallet::weight(::WeightInfo::sudo_set_weights_set_rate_limit())] + #[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_SET), 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. @@ -428,11 +418,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!( @@ -440,11 +426,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(()) } @@ -459,11 +440,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), @@ -471,11 +448,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:?} ) " ); @@ -492,11 +464,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!( @@ -507,11 +475,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(()) } @@ -525,11 +488,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), @@ -554,11 +513,6 @@ pub mod pallet { mechanism_count.into(), )?; 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:?} ) " ); @@ -587,11 +541,7 @@ pub mod pallet { #[pallet::call_index(17)] #[pallet::weight(::WeightInfo::sudo_set_rho())] 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!( @@ -600,11 +550,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(()) } @@ -618,11 +563,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!( @@ -639,11 +580,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(()) } @@ -721,11 +657,7 @@ pub mod pallet { netuid: NetUid, min_burn: TaoBalance, ) -> 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), @@ -742,11 +674,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(()) } @@ -760,11 +687,7 @@ pub mod pallet { netuid: NetUid, max_burn: TaoBalance, ) -> 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), @@ -781,11 +704,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(()) } @@ -852,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!( @@ -873,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(()) } @@ -891,11 +801,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!( @@ -904,11 +810,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(()) } @@ -956,19 +857,24 @@ 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) - .saturating_add(::DbWeight::get().writes(1)))] + #[pallet::weight(( + Weight::from_parts(14_000_000, 0) + .saturating_add(::DbWeight::get().writes(1)), + 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. @@ -1122,19 +1028,27 @@ 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(::WeightInfo::sudo_set_tx_delegate_take_rate_limit())] + #[pallet::weight(( + Weight::from_parts(5_019_000, 0) + .saturating_add(T::DbWeight::get().reads(0_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)), + 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. @@ -1198,11 +1112,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!( @@ -1212,11 +1122,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(()) } @@ -1236,19 +1141,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(()) } @@ -1261,23 +1157,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 dissolve network schedule. @@ -1335,11 +1220,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!( @@ -1350,11 +1231,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(()) } @@ -1426,21 +1302,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. @@ -1461,19 +1325,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(()) } @@ -1601,11 +1456,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!( @@ -1622,11 +1474,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(()) } @@ -1646,21 +1493,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(()) } @@ -1680,21 +1518,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(()) } @@ -1787,18 +1616,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(()) } @@ -1829,16 +1649,24 @@ 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(::WeightInfo::sudo_set_owner_hparam_rate_limit())] + #[pallet::weight(( + Weight::from_parts(5_701_000, 0) + .saturating_add(::DbWeight::get().reads(0_u64)) + .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 @@ -1865,6 +1693,7 @@ pub mod pallet { netuid, &[TransactionType::MechanismCountUpdate], ); + Ok(()) } @@ -1892,6 +1721,7 @@ pub mod pallet { netuid, &[TransactionType::MechanismEmission], ); + Ok(()) } @@ -1908,7 +1738,7 @@ pub mod pallet { max_n: u16, ) -> DispatchResult { let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin.clone(), + origin, netuid, &[TransactionType::MaxUidsTrimming], )?; @@ -1921,6 +1751,7 @@ pub mod pallet { netuid, &[TransactionType::MaxUidsTrimming], ); + Ok(()) } @@ -2106,11 +1937,7 @@ pub mod pallet { netuid: NetUid, burn_half_life: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::BurnHalfLife.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -2130,12 +1957,6 @@ pub mod pallet { burn_half_life, }); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::BurnHalfLife.into()], - ); - Ok(()) } @@ -2154,11 +1975,7 @@ pub mod pallet { netuid: NetUid, burn_increase_mult: U64F64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( - origin, - netuid, - &[Hyperparameter::BurnIncreaseMult.into()], - )?; + let _ = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( @@ -2180,12 +1997,6 @@ pub mod pallet { burn_increase_mult, }); - pallet_subtensor::Pallet::::record_owner_rl( - maybe_owner, - netuid, - &[Hyperparameter::BurnIncreaseMult.into()], - ); - Ok(()) } diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 9faf870cbe..f5572a8c80 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::{RateLimitingInterface, TryIntoRateLimitTarget}; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_grandpa::AuthorityList as GrandpaAuthorityList; use sp_core::U256; @@ -20,7 +21,9 @@ use sp_runtime::{ use sp_std::cmp::Ordering; use sp_weights::Weight; use substrate_fixed::types::U64F64; -use subtensor_runtime_common::{AuthorshipInfo, ConstTao, NetUid, TaoBalance}; +use subtensor_runtime_common::{ + AuthorshipInfo, ConstTao, NetUid, TaoBalance, rate_limiting::RateLimitUsageKey, +}; type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. @@ -113,9 +116,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 pub const InitialBurn: TaoBalance = TaoBalance::new(0); pub const InitialMinBurn: TaoBalance = TaoBalance::new(500_000); @@ -141,7 +141,6 @@ parameter_types! { pub const InitialNetworkMinLockCost: TaoBalance = TaoBalance::new(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: TaoBalance = TaoBalance::new(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 @@ -198,9 +197,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; type InitialBurn = InitialBurn; type InitialMaxBurn = InitialMaxBurn; @@ -212,7 +208,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; @@ -234,6 +229,7 @@ impl pallet_subtensor::Config for Test { type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; + type RateLimiting = NoRateLimiting; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; @@ -374,6 +370,42 @@ impl pallet_subtensor::CommitmentsInterface for CommitmentsI { fn purge_netuid(_netuid: NetUid) {} } +pub struct NoRateLimiting; + +impl RateLimitingInterface 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 + } + + fn set_last_seen( + _target: TargetArg, + _usage_key: Option, + _block: Option, + ) where + TargetArg: TryIntoRateLimitTarget, + { + } +} + pub struct GrandpaInterfaceImpl; impl crate::GrandpaInterface for GrandpaInterfaceImpl { fn schedule_change( diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index 61b9662492..f77a2d53f8 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -39,30 +39,6 @@ fn test_sudo_set_default_take() { }); } -#[test] -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!( - AdminUtils::sudo_set_serving_rate_limit( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - 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); - }); -} - #[test] fn test_sudo_set_min_difficulty() { new_test_ext().execute_with(|| { @@ -261,45 +237,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(|| { @@ -1059,33 +996,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(|| { @@ -2021,20 +1931,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); }); } @@ -2137,183 +2033,6 @@ fn test_sudo_set_min_burn() { }); } -#[test] -fn test_owner_hyperparam_update_rate_limit_enforced() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 10); - // Set owner - let owner: U256 = U256::from(5); - SubnetOwner::::insert(netuid, owner); - - // Set tempo to 1 so owner hyperparam RL = 2 tempos = 2 blocks - SubtensorModule::set_tempo(netuid, 1); - // Disable admin freeze window to avoid blocking on small tempo - assert_ok!(AdminUtils::sudo_set_admin_freeze_window( - <::RuntimeOrigin>::root(), - 0 - )); - - // First update succeeds - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 11 - )); - // Immediate second update fails due to TxRateLimitExceeded - assert_noop!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 12 - ), - SubtensorError::::TxRateLimitExceeded - ); - - // Advance less than limit still fails - run_to_block(SubtensorModule::get_current_block_as_u64() + 1); - assert_noop!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 13 - ), - SubtensorError::::TxRateLimitExceeded - ); - - // Advance one more block to pass the limit; should succeed - run_to_block(SubtensorModule::get_current_block_as_u64() + 1); - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 14 - )); - }); -} - -// Verifies that owner hyperparameter rate limit is enforced based on tempo (2 tempos). -#[test] -fn test_hyperparam_rate_limit_enforced_by_tempo() { - new_test_ext().execute_with(|| { - // Setup subnet and owner - let netuid = NetUid::from(42); - add_network(netuid, 10); - let owner: U256 = U256::from(77); - SubnetOwner::::insert(netuid, owner); - - // Set tempo to 1 so RL = 2 blocks - SubtensorModule::set_tempo(netuid, 1); - // Disable admin freeze window to avoid blocking on small tempo - assert_ok!(AdminUtils::sudo_set_admin_freeze_window( - <::RuntimeOrigin>::root(), - 0 - )); - - // First owner update should succeed - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 1 - )); - - // Immediate second update should fail due to tempo-based RL - assert_noop!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 2 - ), - SubtensorError::::TxRateLimitExceeded - ); - - // Advance 2 blocks (2 tempos with tempo=1) then succeed - run_to_block(SubtensorModule::get_current_block_as_u64() + 2); - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 3 - )); - }); -} - -// Verifies owner hyperparameters are rate-limited independently per parameter. -// Setting one hyperparameter should not block setting a different hyperparameter -// during the same rate-limit window, but it should still block itself. -#[test] -fn test_owner_hyperparam_rate_limit_independent_per_param() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(7); - add_network(netuid, 10); - - // Set subnet owner - let owner: U256 = U256::from(123); - SubnetOwner::::insert(netuid, owner); - - // Use small tempo to make RL short and deterministic (2 blocks when tempo=1) - SubtensorModule::set_tempo(netuid, 1); - // Disable admin freeze window so it doesn't interfere with small tempo - assert_ok!(AdminUtils::sudo_set_admin_freeze_window( - <::RuntimeOrigin>::root(), - 0 - )); - - // First update to kappa should succeed - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 10 - )); - - // Immediate second update to the SAME param (kappa) should be blocked by RL - assert_noop!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 11 - ), - SubtensorError::::TxRateLimitExceeded - ); - - // Updating a DIFFERENT param (rho) should pass immediately — independent RL key - assert_ok!(AdminUtils::sudo_set_rho( - <::RuntimeOrigin>::signed(owner), - netuid, - 5 - )); - - // kappa should still be blocked until its own RL window passes - assert_noop!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 12 - ), - SubtensorError::::TxRateLimitExceeded - ); - - // rho should also be blocked for itself immediately after being set - assert_noop!( - AdminUtils::sudo_set_rho(<::RuntimeOrigin>::signed(owner), netuid, 6), - SubtensorError::::TxRateLimitExceeded - ); - - // Advance enough blocks to pass the RL window (2 blocks when tempo=1 and default epochs=2) - run_to_block(SubtensorModule::get_current_block_as_u64() + 2); - - // Now both hyperparameters can be updated again - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 13 - )); - assert_ok!(AdminUtils::sudo_set_rho( - <::RuntimeOrigin>::signed(owner), - netuid, - 7 - )); - }); -} - #[test] fn test_sudo_set_max_burn() { new_test_ext().execute_with(|| { diff --git a/pallets/rate-limiting-interface/Cargo.toml b/pallets/rate-limiting-interface/Cargo.toml new file mode 100644 index 0000000000..d8c2ccdd27 --- /dev/null +++ b/pallets/rate-limiting-interface/Cargo.toml @@ -0,0 +1,25 @@ +[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 } +subtensor-macros.workspace = true + +[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..73011099c1 --- /dev/null +++ b/pallets/rate-limiting-interface/README.md @@ -0,0 +1,3 @@ +# `rate-limiting-interface` + +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 new file mode 100644 index 0000000000..0c7a9ad5fd --- /dev/null +++ b/pallets/rate-limiting-interface/src/lib.rs @@ -0,0 +1,402 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +//! Interface for querying rate limits and last-seen usage, with optional write access. + +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::traits::GetCallMetadata; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_std::vec::Vec; +use subtensor_macros::freeze_struct; + +/// 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. + 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; + + /// 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. +#[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, +)] +#[freeze_struct("c865c7a9be1442a")] +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.first()?; + 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 module_indices = Call::get_module_indices(); + let pallet_position = module_indices + .iter() + .position(|index| *index == self.pallet_index)?; + let pallet_name = *modules.get(pallet_position)?; + let call_names = Call::get_call_names(pallet_name); + let call_indices = Call::get_call_indices(pallet_name); + let extrinsic_position = call_indices + .iter() + .position(|index| *index == self.extrinsic_index)?; + let extrinsic_name = *call_names.get(extrinsic_position)?; + 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 module_indices = Call::get_module_indices(); + let pallet_pos = modules.iter().position(|name| *name == pallet_name)?; + let call_names = Call::get_call_names(pallet_name); + let call_indices = Call::get_call_indices(pallet_name); + let extrinsic_pos = call_names.iter().position(|name| *name == extrinsic_name)?; + let pallet_index = *module_indices.get(pallet_pos)?; + let extrinsic_index = *call_indices.get(extrinsic_pos)?; + 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)] +#[allow(clippy::expect_used)] +mod tests { + use super::*; + use codec::Encode; + use frame_support::traits::CallMetadata; + + #[derive(Clone, Copy, Debug, Encode)] + #[freeze_struct("43380fb4d208f4cf")] + struct DummyCall(u8, u8); + + #[derive(Clone, Copy, Debug, Encode)] + #[freeze_struct("1c68d24b937528db")] + struct SparseDummyCall(u8, u8); + + #[derive(Clone, Copy, Debug, Encode)] + #[freeze_struct("5ee38b16a82ba56")] + struct SparseCallDummyCall(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_indices(module: &str) -> &'static [u8] { + match module { + "P0" => &[0], + "P1" => &[0, 1, 2, 3, 4], + _ => &[], + } + } + + fn get_call_metadata(&self) -> CallMetadata { + CallMetadata { + function_name: "unused", + pallet_name: "unused", + } + } + + fn get_module_indices() -> &'static [u8] { + &[0, 1] + } + } + + impl GetCallMetadata for SparseDummyCall { + 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_indices(module: &str) -> &'static [u8] { + match module { + "P0" => &[0], + "P1" => &[0, 1, 2, 3, 4], + _ => &[], + } + } + + fn get_call_metadata(&self) -> CallMetadata { + CallMetadata { + function_name: "unused", + pallet_name: "unused", + } + } + + fn get_module_indices() -> &'static [u8] { + &[0, 7] + } + } + + impl GetCallMetadata for SparseCallDummyCall { + 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_indices(module: &str) -> &'static [u8] { + match module { + "P0" => &[0], + "P1" => &[0, 2, 4, 9, 11], + _ => &[], + } + } + + fn get_call_metadata(&self) -> CallMetadata { + CallMetadata { + function_name: "unused", + pallet_name: "unused", + } + } + + fn get_module_indices() -> &'static [u8] { + &[0, 1] + } + } + + #[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 transaction_identifier_names_resolves_sparse_module_indices() { + let id = TransactionIdentifier::new(7, 4); + assert_eq!(id.names::(), Some(("P1", "C4"))); + } + + #[test] + fn transaction_identifier_for_call_names_resolves_sparse_module_indices() { + let id = TransactionIdentifier::for_call_names::("P1", "C4").expect("id"); + assert_eq!(id, TransactionIdentifier::new(7, 4)); + } + + #[test] + fn transaction_identifier_names_resolves_sparse_call_indices() { + let id = TransactionIdentifier::new(1, 11); + assert_eq!(id.names::(), Some(("P1", "C4"))); + } + + #[test] + fn transaction_identifier_for_call_names_resolves_sparse_call_indices() { + let id = + TransactionIdentifier::for_call_names::("P1", "C4").expect("id"); + assert_eq!(id, TransactionIdentifier::new(1, 11)); + } + + #[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 new file mode 100644 index 0000000000..3bee379058 --- /dev/null +++ b/pallets/rate-limiting/Cargo.toml @@ -0,0 +1,57 @@ +[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-benchmarking = { workspace = true, optional = true } +frame-support.workspace = true +frame-system.workspace = true +rate-limiting-interface.workspace = true +scale-info = { workspace = true, features = ["derive"] } +serde = { workspace = true, features = ["derive"] } +sp-runtime.workspace = true +sp-std.workspace = true +subtensor-macros.workspace = true +subtensor-runtime-common.workspace = true + +[dev-dependencies] +sp-core.workspace = true +sp-io.workspace = true +sp-runtime.workspace = true + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "rate-limiting-interface/std", + "scale-info/std", + "serde/std", + "sp-core/std", + "sp-io/std", + "sp-std/std", + "sp-runtime/std", + "subtensor-runtime-common/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "subtensor-runtime-common/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] 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..a29e42e3d7 --- /dev/null +++ b/pallets/rate-limiting/runtime-api/Cargo.toml @@ -0,0 +1,27 @@ +[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 } +subtensor-macros.workspace = 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/std", +] 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..480604c40c --- /dev/null +++ b/pallets/rate-limiting/runtime-api/src/lib.rs @@ -0,0 +1,44 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; +use pallet_rate_limiting::RateLimitKind; +use scale_info::TypeInfo; +use sp_std::vec::Vec; +use subtensor_runtime_common::{BlockNumber, rate_limiting::GroupId}; + +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] +/// Rate-limit configuration for a target. +pub enum RateLimitConfigRpcResponse { + /// Global (scope-independent) limit configuration. + Global(RateLimitKind), + /// Per-scope limit configuration. + /// + /// Keys are SCALE-encoded scope bytes as stored by the pallet. + Scoped(Vec<(Vec, RateLimitKind)>), +} + +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] +/// RPC response for `get_rate_limit`. +/// +/// Returns the configured limit for the call and includes group id when the call is assigned to a +/// group. +pub enum RateLimitRpcResponse { + /// Call has no group assignment. + Standalone { limit: RateLimitConfigRpcResponse }, + /// Call is assigned to a group. + Grouped { + group_id: GroupId, + limit: RateLimitConfigRpcResponse, + }, +} + +sp_api::decl_runtime_apis! { + pub trait RateLimitingRuntimeApi { + fn get_rate_limit(pallet: Vec, extrinsic: Vec) -> Option; + } +} diff --git a/pallets/rate-limiting/src/benchmarking.rs b/pallets/rate-limiting/src/benchmarking.rs new file mode 100644 index 0000000000..224b6c09af --- /dev/null +++ b/pallets/rate-limiting/src/benchmarking.rs @@ -0,0 +1,186 @@ +//! Benchmarking setup for pallet-rate-limiting +#![allow(clippy::arithmetic_side_effects)] +#![allow(clippy::expect_used)] + +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; + +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()) +} + +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 register_call() { + let call = sample_call::(); + let identifier = TransactionIdentifier::from_call(call.as_ref()).expect("id"); + let target = RateLimitTarget::Transaction(identifier); + + #[extrinsic_call] + _(RawOrigin::Root, call, None); + + assert!(Limits::::contains_key(target)); + } + + #[benchmark] + 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)); + + #[extrinsic_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, false); + + assert_eq!(CallGroups::::get(identifier), Some(group)); + assert_eq!(CallReadOnly::::get(identifier), Some(false)); + 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(target).is_none()); + assert!(LastSeen::::get(usage_target, None::).is_none()); + assert!(CallGroups::::get(identifier).is_none()); + assert!(!GroupMembers::::get(group).contains(&identifier)); + } + + #[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 new file mode 100644 index 0000000000..1fda846a3c --- /dev/null +++ b/pallets/rate-limiting/src/lib.rs @@ -0,0 +1,1390 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +//! 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 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: +//! +//! - [`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. +//! - [`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 +//! block span used by `RateLimitKind::Default` entries. +//! +//! 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 (in the global sense). +//! +//! # 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: +//! +//! ```ignore +//! pub type TransactionExtensions = ( +//! // ... other extensions ... +//! pallet_rate_limiting::RateLimitTransactionExtension, +//! ); +//! ``` +//! +//! # Context resolvers +//! +//! The pallet relies on two resolvers: +//! +//! - [`Config::LimitScopeResolver`], which determines how limits are stored (for example by +//! 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)`). +//! +//! 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. 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; +//! +//! // Limits are scoped per netuid. +//! pub struct ScopeResolver; +//! 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, .. }) => { +//! let mut scopes = BTreeSet::new(); +//! scopes.insert(*netuid); +//! Some(scopes) +//! } +//! _ => None, +//! } +//! } +//! +//! 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 { +//! span +//! } +//! } +//! +//! // Usage tracking distinguishes hyperparameter + netuid. +//! pub struct UsageResolver; +//! impl pallet_rate_limiting::RateLimitUsageResolver< +//! RuntimeOrigin, +//! RuntimeCall, +//! (NetUid, HyperParam), +//! > for UsageResolver { +//! fn context( +//! _origin: &RuntimeOrigin, +//! call: &RuntimeCall, +//! ) -> Option> { +//! match call { +//! RuntimeCall::Subtensor(pallet_subtensor::Call::set_hyperparam { +//! netuid, +//! hyper, +//! .. +//! }) => { +//! let mut usage = BTreeSet::new(); +//! usage.insert((*netuid, *hyper)); +//! Some(usage) +//! } +//! _ => None, +//! } +//! } +//! } +//! +//! impl pallet_rate_limiting::Config for Runtime { +//! type RuntimeCall = RuntimeCall; +//! type LimitScope = NetUid; +//! type LimitScopeResolver = ScopeResolver; +//! type UsageKey = (NetUid, HyperParam); +//! type UsageResolver = UsageResolver; +//! type AdminOrigin = frame_system::EnsureRoot; +//! } +//! ``` + +#[cfg(feature = "runtime-benchmarks")] +pub use benchmarking::BenchmarkHelper; +pub use pallet::*; +pub use rate_limiting_interface::{RateLimitTarget, TransactionIdentifier}; +pub use rate_limiting_interface::{RateLimitingInterface, TryIntoRateLimitTarget}; +pub use tx_extension::RateLimitTransactionExtension; +pub use types::{ + BypassDecision, EnsureLimitSettingRule, GroupSharing, RateLimit, RateLimitGroup, RateLimitKind, + RateLimitScopeResolver, RateLimitUsageResolver, +}; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +mod tx_extension; +mod types; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[frame_support::pallet] +// FRAME pallet macro expansion uses internal `expect` and clippy reports it at this callsite +// (`#[pallet::storage]`, `#[pallet::error]`, `#[pallet::pallet]`). +#[allow(clippy::expect_used)] +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::{ + AtLeast32BitUnsigned, DispatchOriginOf, Dispatchable, Member, One, Saturating, Zero, + }; + use sp_std::{ + boxed::Box, + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + convert::TryFrom, + marker::PhantomData, + vec, + vec::Vec, + }; + + #[cfg(feature = "runtime-benchmarks")] + use crate::benchmarking::BenchmarkHelper as BenchmarkHelperTrait; + use crate::types::{ + EnsureLimitSettingRule, 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 + 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. + 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; + + /// Resolves the scope for the given runtime call when configuring limits. + type LimitScopeResolver: RateLimitScopeResolver< + DispatchOriginOf<>::RuntimeCall>, + >::RuntimeCall, + Self::LimitScope, + BlockNumberFor, + >; + + /// 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: RateLimitUsageResolver< + DispatchOriginOf<>::RuntimeCall>, + >::RuntimeCall, + 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 rate limit target to its configured rate limit. + #[pallet::storage] + #[pallet::getter(fn limits)] + pub type Limits, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + RateLimitTarget<>::GroupId>, + RateLimit<>::LimitScope, BlockNumberFor>, + OptionQuery, + >; + + /// 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, + T::DefaultLimitSettingRule, + >; + + /// Tracks when a rate-limited target was last observed per usage key. + #[pallet::storage] + pub type LastSeen, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + RateLimitTarget<>::GroupId>, + Blake2_128Concat, + Option<>::UsageKey>, + BlockNumberFor, + OptionQuery, + >; + + /// Default block span applied when an extrinsic uses the default rate limit. + #[pallet::storage] + #[pallet::getter(fn default_limit)] + 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, + >; + + /// 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)] + 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 call was registered for rate limiting. + CallRegistered { + /// Identifier of the registered transaction. + transaction: TransactionIdentifier, + /// Scope seeded during registration (if any). + scope: Option>::LimitScope>>, + /// 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 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>, + /// 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>, + }, + /// 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. + 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>, + }, + /// 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. + #[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, + /// 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] + pub struct GenesisConfig, I: 'static = ()> { + pub default_limit: BlockNumberFor, + pub limits: Vec<( + RateLimitTarget<>::GroupId>, + Option<>::LimitScope>, + RateLimitKind>, + )>, + pub groups: Vec<(>::GroupId, Vec, GroupSharing)>, + pub limit_setting_rules: Vec<( + RateLimitTarget<>::GroupId>, + >::LimitSettingRule, + )>, + } + + impl, I: 'static> Default for GenesisConfig { + fn default() -> Self { + Self { + default_limit: Zero::zero(), + limits: Vec::new(), + groups: Vec::new(), + limit_setting_rules: Vec::new(), + } + } + } + + #[pallet::genesis_build] + #[allow(clippy::expect_used)] + impl, I: 'static> BuildGenesisConfig for GenesisConfig { + fn build(&self) { + DefaultLimit::::put(self.default_limit); + + // Seed groups first so limit targets can reference them. + let mut max_group: >::GroupId = Zero::zero(); + for (group_id, name, sharing) in &self.groups { + let bounded = GroupNameOf::::try_from(name.clone()) + .expect("Genesis group name exceeds MaxGroupNameLength"); + + assert!( + !Groups::::contains_key(group_id), + "Duplicate group id in genesis config" + ); + assert!( + !GroupNameIndex::::contains_key(&bounded), + "Duplicate group name in genesis config" + ); + + Groups::::insert( + group_id, + RateLimitGroup { + id: *group_id, + name: bounded.clone(), + sharing: *sharing, + }, + ); + GroupNameIndex::::insert(&bounded, *group_id); + GroupMembers::::insert(*group_id, GroupMembersOf::::new()); + if *group_id > max_group { + max_group = *group_id; + } + } + let next = max_group.saturating_add(One::one()); + NextGroupId::::put(next); + + for (identifier, scope, kind) in &self.limits { + if let RateLimitTarget::Group(group) = identifier { + assert!( + Groups::::contains_key(group), + "Genesis limit references unknown group" + ); + } + let target = *identifier; + Limits::::mutate(target, |entry| match scope { + None => { + *entry = Some(RateLimit::Global(*kind)); + } + Some(sc) => { + if let Some(config) = entry { + config.upsert_scope(sc.clone(), *kind); + } else { + *entry = Some(RateLimit::scoped_single(sc.clone(), *kind)); + } + } + }); + } + + for (target, rule) in &self.limit_setting_rules { + LimitSettingRules::::insert(target, rule.clone()); + } + } + } + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData<(T, I)>); + + #[deny(clippy::expect_used)] + impl, I: 'static> Pallet { + /// Returns whether a call currently passes rate limiting. + /// + /// `scopes`: `None`/empty means global scope, otherwise all provided scopes are checked. + /// `usage_key`: `None` checks global usage, `Some(key)` checks keyed usage. + /// + /// Returns [`Error::MissingScope`] when scoped config exists but no scope is provided. + /// Returns `Ok(true)` on bypass; otherwise every enforced scope must pass. + pub fn is_within_limit( + origin: &DispatchOriginOf<>::RuntimeCall>, + call: &>::RuntimeCall, + identifier: &TransactionIdentifier, + scopes: &Option>::LimitScope>>, + usage_key: &Option<>::UsageKey>, + ) -> Result { + 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, scopes)?; + + 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(), + }; + + for scope in scope_list { + let Some(block_span) = Self::effective_span(origin, call, &target, &scope) else { + continue; + }; + let last_seen = LastSeen::::get(usage_target, usage_key); + if !Self::within_span(&usage_target, usage_key, block_span, last_seen) { + return Ok(false); + } + } + + Ok(true) + } + + /// 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> { + let config = Limits::::get(target)?; + let kind = config.kind_for(scope.as_ref())?; + Some(match *kind { + RateLimitKind::Default => DefaultLimit::::get(), + RateLimitKind::Exact(block_span) => block_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>, + scope: &Option<>::LimitScope>, + ) -> Option> { + let span = Self::resolved_limit(target, scope)?; + Some(>::LimitScopeResolver::adjust_span( + origin, call, span, + )) + } + + pub(crate) fn within_span( + target: &RateLimitTarget<>::GroupId>, + usage_key: &Option<>::UsageKey>, + block_span: BlockNumberFor, + last_seen_override: Option>, + ) -> bool { + if block_span.is_zero() { + return true; + } + + let last_seen = match last_seen_override { + Some(block) => Some(block), + None => LastSeen::::get(target, usage_key), + }; + + if let Some(last) = last_seen { + let current = frame_system::Pallet::::block_number(); + let delta = current.saturating_sub(last); + if delta < block_span { + return false; + } + } + + 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, + } + } + + fn target_for( + identifier: &TransactionIdentifier, + predicate: impl Fn(GroupSharing) -> bool, + ) -> Result>::GroupId>, DispatchError> { + let details = Self::group_assignment(identifier)?; + Ok(Self::target_from_details( + identifier, + details.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 + && 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>, + scopes: &Option>::LimitScope>>, + ) -> Result<(), DispatchError> { + let has_scope = scopes.as_ref().is_some_and(|scopes| !scopes.is_empty()); + if has_scope { + return Ok(()); + } + + if let Some(RateLimit::Scoped(map)) = Limits::::get(target) + && !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 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(()) + } + + /// 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) + } + + /// Returns the configured limit for the specified pallet/extrinsic names, if any. + pub fn limit_for_call_names( + pallet_name: &str, + extrinsic_name: &str, + scope: Option<>::LimitScope>, + ) -> Option>> { + let identifier = TransactionIdentifier::for_call_names::<>::RuntimeCall>( + pallet_name, + extrinsic_name, + )?; + 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. + pub fn resolved_limit_for_call_names( + pallet_name: &str, + extrinsic_name: &str, + scope: Option<>::LimitScope>, + ) -> Option> { + let identifier = TransactionIdentifier::for_call_names::<>::RuntimeCall>( + pallet_name, + extrinsic_name, + )?; + let target = Self::config_target(&identifier).ok()?; + Self::resolved_limit(&target, &scope) + } + + fn call_metadata( + identifier: &TransactionIdentifier, + ) -> Result<(Vec, Vec), DispatchError> { + 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()), + )) + } + + /// 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) + } + + pub(crate) fn usage_target( + identifier: &TransactionIdentifier, + ) -> Result>::GroupId>, DispatchError> { + Self::target_for(identifier, GroupSharing::usage_uses_group) + } + + fn insert_call_into_group( + identifier: &TransactionIdentifier, + group: >::GroupId, + ) -> Result<(), DispatchError> { + GroupMembers::::try_mutate(group, |members| { + members + .try_insert(*identifier) + .map_err(|_| Error::::GroupMemberLimitExceeded)?; + Ok::<(), DispatchError>(()) + }) + } + + fn detach_call_from_group( + identifier: &TransactionIdentifier, + group: >::GroupId, + ) -> bool { + GroupMembers::::mutate(group, |members| members.remove(identifier)) + } + } + + #[pallet::call] + #[deny(clippy::expect_used)] + impl, I: 'static> Pallet { + /// Registers a call for rate limiting and seeds its initial configuration. + #[pallet::call_index(0)] + #[pallet::weight(T::DbWeight::get().reads_writes(3, 3))] + pub fn register_call( + origin: OriginFor, + call: Box<>::RuntimeCall>, + group: Option<>::GroupId>, + ) -> DispatchResult { + let resolver_origin: DispatchOriginOf<>::RuntimeCall> = + Into::>::RuntimeCall>>::into(origin.clone()); + let scopes = + >::LimitScopeResolver::context(&resolver_origin, call.as_ref()); + + T::AdminOrigin::ensure_origin(origin)?; + + let identifier = TransactionIdentifier::from_call(call.as_ref()) + .ok_or(Error::::InvalidRuntimeCall)?; + Self::ensure_call_unregistered(&identifier)?; + + let target = RateLimitTarget::Transaction(identifier); + + 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)); + } + + 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); + CallReadOnly::::insert(identifier, false); + assigned_group = Some(group_id); + } + + let (pallet, extrinsic) = Self::call_metadata(&identifier)?; + Self::deposit_event(Event::CallRegistered { + transaction: identifier, + scope: scopes, + group: assigned_group, + pallet, + extrinsic, + }); + + if let Some(group_id) = assigned_group { + Self::deposit_event(Event::CallGroupUpdated { + transaction: identifier, + group: Some(group_id), + }); + Self::deposit_event(Event::CallReadOnlyUpdated { + transaction: identifier, + group: group_id, + read_only: false, + }); + } + + Ok(()) + } + + /// Configures a rate limit for either a transaction or group target. + #[pallet::call_index(1)] + #[pallet::weight(T::DbWeight::get().reads_writes(2, 2))] + pub fn set_rate_limit( + origin: OriginFor, + target: RateLimitTarget<>::GroupId>, + scope: Option<>::LimitScope>, + limit: RateLimitKind>, + ) -> DispatchResult { + let rule = LimitSettingRules::::get(&target); + T::LimitSettingOrigin::ensure_origin(origin, &rule, &scope)?; + + 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)) + } + 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::RateLimitSet { + target, + transaction, + scope, + limit, + pallet, + extrinsic, + }); + 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)] + #[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)?; + + Self::ensure_call_registered(&transaction)?; + Self::ensure_group_details(group)?; + + let current = CallGroups::::get(transaction); + ensure!(current.is_none(), Error::::CallAlreadyInGroup); + Self::insert_call_into_group(&transaction, group)?; + 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(()) + } + + /// 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()); + }; + CallReadOnly::::remove(transaction); + 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, + block_span: BlockNumberFor, + ) -> DispatchResult { + 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 + && 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) { + CallReadOnly::::remove(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) { + CallReadOnly::::remove(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(()) + } + + /// 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(()) + } + } +} + +impl, I: 'static> RateLimitingInterface 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) + } + + 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/rate-limiting/src/mock.rs b/pallets/rate-limiting/src/mock.rs new file mode 100644 index 0000000000..f79ae7f32d --- /dev/null +++ b/pallets/rate-limiting/src/mock.rs @@ -0,0 +1,256 @@ +#![allow(dead_code)] +#![allow(clippy::expect_used)] + +use core::convert::TryInto; + +use frame_support::{ + derive_impl, + dispatch::DispatchResult, + sp_runtime::{ + BuildStorage, + traits::{BlakeTwo256, IdentityLookup}, + }, + traits::{ConstU16, ConstU32, ConstU64, EnsureOrigin, Everything}, +}; +use frame_system::{EnsureRoot, ensure_signed}; +use serde::{Deserialize, Serialize}; +use sp_core::H256; +use sp_io::TestExternalities; +use sp_std::{collections::btree_set::BTreeSet, vec::Vec}; + +use crate as pallet_rate_limiting; +use crate::{RateLimitKind, 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 = 0, + RateLimiting: pallet_rate_limiting = 1, + } +); + +#[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 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; + +impl pallet_rate_limiting::RateLimitScopeResolver + for TestScopeResolver +{ + fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option> { + match call { + RuntimeCall::RateLimiting(RateLimitingCall::set_rate_limit { limit, .. }) => { + let RateLimitKind::Exact(span) = limit else { + let mut scopes = BTreeSet::new(); + scopes.insert(1); + return Some(scopes); + }; + let scope: LimitScope = (*span).try_into().ok()?; + // Multi-scope path used by tests: Exact(42/43) returns two scopes. + let mut scopes = BTreeSet::new(); + scopes.insert(scope); + if *span == 42 || *span == 43 { + scopes.insert(scope.saturating_add(1)); + } + Some(scopes) + } + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { + let scope: LimitScope = (*block_span).try_into().ok()?; + let mut scopes = BTreeSet::new(); + scopes.insert(scope); + Some(scopes) + } + RuntimeCall::RateLimiting(_) => { + let mut scopes = BTreeSet::new(); + scopes.insert(1); + Some(scopes) + } + _ => None, + } + } + + 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 { + if matches!( + call, + RuntimeCall::RateLimiting(RateLimitingCall::deregister_call { .. }) + ) { + span.saturating_mul(2) + } else { + span + } + } +} + +impl pallet_rate_limiting::RateLimitUsageResolver + for TestUsageResolver +{ + fn context(_origin: &RuntimeOrigin, call: &RuntimeCall) -> Option> { + match call { + RuntimeCall::RateLimiting(RateLimitingCall::set_rate_limit { limit, .. }) => { + let RateLimitKind::Exact(span) = limit else { + let mut usage = BTreeSet::new(); + usage.insert(1); + return Some(usage); + }; + let key: UsageKey = (*span).try_into().ok()?; + // Multi-usage path used by tests: Exact(42) returns two usage keys. + let mut usage = BTreeSet::new(); + usage.insert(key); + if *span == 42 { + usage.insert(key.saturating_add(1)); + } + Some(usage) + } + RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => { + let key: UsageKey = (*block_span).try_into().ok()?; + let mut usage = BTreeSet::new(); + usage.insert(key); + Some(usage) + } + RuntimeCall::RateLimiting(_) => { + let mut usage = BTreeSet::new(); + usage.insert(1); + Some(usage) + } + _ => None, + } + } +} + +impl pallet_rate_limiting::Config for Test { + type RuntimeCall = RuntimeCall; + type LimitScope = LimitScope; + type LimitScopeResolver = TestScopeResolver; + 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>; + #[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; + +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..91e7ca3d51 --- /dev/null +++ b/pallets/rate-limiting/src/tests.rs @@ -0,0 +1,743 @@ +#![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] + +use frame_support::{assert_noop, assert_ok}; +use sp_std::{collections::btree_set::BTreeSet, vec::Vec}; + +use crate::{ + CallGroups, CallReadOnly, Config, GroupMembers, GroupSharing, LastSeen, LimitSettingRules, + Limits, RateLimit, RateLimitKind, RateLimitTarget, TransactionIdentifier, mock::*, + pallet::Error, +}; +use frame_support::traits::Get; + +fn target(identifier: TransactionIdentifier) -> RateLimitTarget { + RateLimitTarget::Transaction(identifier) +} + +fn remark_call() -> RuntimeCall { + RuntimeCall::System(frame_system::Call::::remark { remark: Vec::new() }) +} + +fn scoped_call() -> RuntimeCall { + 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 { + let identifier = identifier_for(&call); + assert_ok!(RateLimiting::register_call( + RuntimeOrigin::root(), + Box::new(call), + group + )); + identifier +} + +fn create_group(name: &[u8], sharing: GroupSharing) -> GroupId { + assert_ok!(RateLimiting::create_group( + RuntimeOrigin::root(), + name.to_vec(), + sharing, + )); + RateLimiting::next_group_id() - 1 +} + +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(|| { + 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 register_call_seeds_scoped_limit() { + new_test_ext().execute_with(|| { + 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"), + } + + let event = last_event(); + let mut expected = BTreeSet::new(); + expected.insert(1u16); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::CallRegistered { transaction, scope, .. }) + if transaction == identifier && scope == Some(expected) + )); + }); +} + +#[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(); + let mut expected = BTreeSet::new(); + expected.insert(42u16); + expected.insert(43u16); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::CallRegistered { transaction, scope, .. }) + if transaction == identifier && scope == Some(expected) + )); + }); +} + +#[test] +fn set_rate_limit_updates_transaction_target() { + new_test_ext().execute_with(|| { + 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 set_rate_limit_requires_registration_and_group_targeting() { + new_test_ext().execute_with(|| { + 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 + ); + + // Group requires targeting the group. + let group = create_group(b"cfg", GroupSharing::ConfigAndUsage); + assert_ok!(RateLimiting::assign_call_to_group( + RuntimeOrigin::root(), + identifier, + group, + false, + )); + assert_noop!( + RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + target, + None, + RateLimitKind::Exact(2), + ), + Error::::MustTargetGroup + ); + }); +} + +#[test] +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(), + RateLimitTarget::Transaction(identifier), + None, + RateLimitKind::Exact(5), + ), + Error::::MustTargetGroup + ); + }); +} + +#[test] +fn assign_and_remove_group_membership() { + new_test_ext().execute_with(|| { + let identifier = register(remark_call(), None); + let group = create_group(b"team", GroupSharing::UsageOnly); + assert_ok!(RateLimiting::assign_call_to_group( + 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(), + 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 set_rate_limit_on_group_updates_storage() { + new_test_ext().execute_with(|| { + let group = create_group(b"grp", GroupSharing::ConfigOnly); + let target = RateLimitTarget::Group(group); + assert_ok!(RateLimiting::set_rate_limit( + RuntimeOrigin::root(), + target, + None, + RateLimitKind::Exact(3), + )); + assert!(matches!( + Limits::::get(target), + Some(RateLimit::Global(RateLimitKind::Exact(3))) + )); + + 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 create_and_delete_group_emit_events() { + new_test_ext().execute_with(|| { + 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 + )); + + 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 deregister_call_scope_removes_entry() { + new_test_ext().execute_with(|| { + 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), + )); + 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 register_call_rejects_duplicates_and_unknown_group() { + new_test_ext().execute_with(|| { + let identifier = register(remark_call(), None); + // Duplicate should fail. + assert_noop!( + RateLimiting::register_call(RuntimeOrigin::root(), Box::new(remark_call()), None), + Error::::CallAlreadyRegistered + ); + + // 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 group_name_limits_and_uniqueness_enforced() { + new_test_ext().execute_with(|| { + // 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 + ); + + // Duplicate names rejected on create and update. + let first = create_group(b"alpha", GroupSharing::UsageOnly); + let second = create_group(b"beta", GroupSharing::UsageOnly); + + 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 + ); + + assert_eq!( + RateLimiting::groups(first).unwrap().name.into_inner(), + b"alpha".to_vec() + ); + + // 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 group_member_limit_and_removal_errors() { + new_test_ext().execute_with(|| { + let group = create_group(b"cap", GroupSharing::UsageOnly); + + 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)); + } + }); + + // Next insert should fail. + let extra = register(remark_call(), None); + assert_noop!( + RateLimiting::assign_call_to_group(RuntimeOrigin::root(), extra, group, false), + Error::::GroupMemberLimitExceeded + ); + + // Removing a call not in a group errors. + assert_noop!( + RateLimiting::remove_call_from_group(RuntimeOrigin::root(), extra), + Error::::CallNotInGroup + ); + }); +} + +#[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(|| { + 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::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 deregister_call_clears_registration() { + new_test_ext().execute_with(|| { + let identifier = register(remark_call(), None); + let tx_target = target(identifier); + LastSeen::::insert(tx_target, None::, 5); + assert_ok!(RateLimiting::deregister_call( + RuntimeOrigin::root(), + 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 + )); + }); +} + +#[test] +fn deregister_errors_for_unknown_or_missing_scope() { + new_test_ext().execute_with(|| { + let unknown = TransactionIdentifier::new(10, 1); + assert_noop!( + RateLimiting::deregister_call(RuntimeOrigin::root(), unknown, None, true), + Error::::CallNotRegistered + ); + + 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 + ); + + // Removing the last scoped entry clears Limits and LastSeen. + LastSeen::::insert(tx_target, Some(1u16), 5); + assert_ok!(RateLimiting::deregister_call( + RuntimeOrigin::root(), + identifier, + Some(1u16), + true, + )); + assert!(Limits::::get(tx_target).is_none()); + assert!(LastSeen::::get(tx_target, Some(1u16)).is_none()); + }); +} + +#[test] +fn is_within_limit_detects_rate_limited_scope() { + new_test_ext().execute_with(|| { + let call = scoped_call(); + let identifier = identifier_for(&call); + let tx_target = target(identifier); + Limits::::insert( + tx_target, + RateLimit::scoped_single(1u16, 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, + &{ + let mut scopes = BTreeSet::new(); + scopes.insert(1u16); + Some(scopes) + }, + &Some(1u16), + ) + .expect("ok"); + assert!(!result); + }); +} + +#[test] +fn set_default_limit_updates_span_and_resolves_in_enforcement() { + new_test_ext().execute_with(|| { + assert_eq!(RateLimiting::default_limit(), 0); + assert_ok!(RateLimiting::set_default_rate_limit( + RuntimeOrigin::root(), + 5 + )); + let event = last_event(); + assert!(matches!( + event, + RuntimeEvent::RateLimiting(crate::Event::DefaultRateLimitSet { block_span: 5 }) + )); + assert_eq!(RateLimiting::default_limit(), 5); + + let call = remark_call(); + let identifier = register(call.clone(), None); + let tx_target = target(identifier); + + 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 limit_for_call_names_prefers_scoped_value() { + new_test_ext().execute_with(|| { + 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_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 new file mode 100644 index 0000000000..a717b17339 --- /dev/null +++ b/pallets/rate-limiting/src/tx_extension.rs @@ -0,0 +1,889 @@ +use codec::{Decode, DecodeWithMemTracking, Encode}; +use frame_support::{ + dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo}, + pallet_prelude::Weight, + sp_runtime::{ + traits::{ + DispatchInfoOf, DispatchOriginOf, Dispatchable, Implication, TransactionExtension, + ValidateResult, Zero, + }, + transaction_validity::{ + InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction, + }, + }, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use scale_info::TypeInfo; +use sp_std::{ + collections::btree_set::BTreeSet, marker::PhantomData, result::Result, vec, vec::Vec, +}; +use subtensor_macros::freeze_struct; + +use crate::{ + Config, LastSeen, Pallet, + types::{ + RateLimitScopeResolver, RateLimitTarget, RateLimitUsageResolver, 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, TypeInfo)] +#[freeze_struct("ccd11950c3c64123")] +pub struct RateLimitTransactionExtension(PhantomData<(T, I)>) +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) + } + + pub fn validate_calls_same_block<'a>( + &self, + origin: DispatchOriginOf<>::RuntimeCall>, + calls: impl IntoIterator>::RuntimeCall>, + ) -> ValidateResult< + Vec< + Option<( + RateLimitTarget<>::GroupId>, + Option>::UsageKey>>, + bool, + )>, + >, + >::RuntimeCall, + > { + let mut usage_seen_in_block = BTreeSet::<( + RateLimitTarget<>::GroupId>, + Option<>::UsageKey>, + )>::new(); + let mut vals = Vec::new(); + + for call in calls { + let val = self.validate_single_call(&origin, call, &mut usage_seen_in_block)?; + vals.push(val); + } + + Ok((ValidTransaction::default(), vals, origin)) + } + + fn validate_single_call( + &self, + origin: &DispatchOriginOf<>::RuntimeCall>, + call: &>::RuntimeCall, + usage_seen_in_block: &mut BTreeSet<( + RateLimitTarget<>::GroupId>, + Option<>::UsageKey>, + )>, + ) -> Result< + Option<( + RateLimitTarget<>::GroupId>, + Option>::UsageKey>>, + bool, + )>, + TransactionValidityError, + > { + let Some(identifier) = TransactionIdentifier::from_call(call) else { + return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)); + }; + + if !Pallet::::is_registered(&identifier) { + return Ok(None); + } + + let scopes = >::LimitScopeResolver::context(origin, call); + let usage = >::UsageResolver::context(origin, call); + + 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 bypass = >::LimitScopeResolver::should_bypass(origin, call); + let should_record = + bypass.record_usage && Pallet::::should_record_usage(&identifier, &usage_target); + + if bypass.bypass_enforcement { + return Ok(should_record.then_some((usage_target, usage, true))); + } + + let usage_keys: BTreeSet>::UsageKey>> = match usage.clone() { + None => { + let mut keys = BTreeSet::new(); + keys.insert(None); + keys + } + Some(keys) => keys.into_iter().map(Some).collect(), + }; + + let mut last_seen_per_key: Vec<( + Option<>::UsageKey>, + Option>, + )> = Vec::with_capacity(usage_keys.len()); + let fallback_block = frame_system::Pallet::::block_number(); + + for key in usage_keys { + let last_seen = Self::resolve_last_seen_for_key( + usage_target, + key.clone(), + should_record, + fallback_block, + usage_seen_in_block, + ); + + last_seen_per_key.push((key, last_seen)); + } + + 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(), + }; + + 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 = last_seen_per_key.iter().all(|(key, last_seen)| { + Pallet::::within_span(&usage_target, key, block_span, *last_seen) + }); + if !within_limit { + return Err(TransactionValidityError::Invalid( + InvalidTransaction::Custom(RATE_LIMIT_DENIED), + )); + } + } + + if !enforced { + return Ok(None); + } + + Ok(Some((usage_target, usage, should_record))) + } + + fn resolve_last_seen_for_key( + usage_target: RateLimitTarget<>::GroupId>, + key: Option<>::UsageKey>, + should_record: bool, + fallback_block: BlockNumberFor, + usage_seen_in_block: &mut BTreeSet<( + RateLimitTarget<>::GroupId>, + Option<>::UsageKey>, + )>, + ) -> Option> { + if should_record { + let entry = (usage_target, key.clone()); + if !usage_seen_in_block.insert(entry) { + Some(fallback_block) + } else { + LastSeen::::get(usage_target, &key) + } + } else { + LastSeen::::get(usage_target, &key) + } + } +} + +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 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 +where + T: Config + Send + Sync + TypeInfo, + I: 'static + TypeInfo + Send + Sync, + >::RuntimeCall: Dispatchable, +{ + const IDENTIFIER: &'static str = IDENTIFIER; + + type Implicit = (); + type Val = Option<( + RateLimitTarget<>::GroupId>, + Option>::UsageKey>>, + bool, + )>; + type Pre = Option<( + RateLimitTarget<>::GroupId>, + Option>::UsageKey>>, + bool, + )>; + + 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 (valid, vals, origin) = + self.validate_calls_same_block(origin, sp_std::iter::once(call))?; + Ok((valid, vals.into_iter().next().unwrap_or(None), 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() + && let Some((target, usage, should_record)) = pre + { + if !should_record { + return Ok(()); + } + let block_number = frame_system::Pallet::::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(()) + } +} + +#[cfg(test)] +#[allow(clippy::expect_used)] +mod tests { + use codec::Encode; + use frame_support::{ + assert_ok, + dispatch::{GetDispatchInfo, PostDispatchInfo}, + }; + use sp_runtime::{ + traits::{TransactionExtension, TxBaseImplication}, + transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError}, + }; + + use crate::{ + GroupSharing, LastSeen, Limits, + types::{RateLimit, RateLimitKind}, + }; + + 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() }) + } + + fn bypass_call() -> RuntimeCall { + RuntimeCall::RateLimiting(RateLimitingCall::remove_call_from_group { + transaction: TransactionIdentifier::new(0, 0), + }) + } + + fn adjustable_call() -> RuntimeCall { + RuntimeCall::RateLimiting(RateLimitingCall::deregister_call { + transaction: TransactionIdentifier::new(0, 0), + scope: None, + clear_usage: false, + }) + } + + 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()) + } + + 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<(RateLimitTarget, Option>, bool)>, + RuntimeOrigin, + ), + TransactionValidityError, + > { + let info = call.get_dispatch_info(); + let len = call.encode().len(); + extension.validate( + RuntimeOrigin::signed(42), + call, + &info, + len, + (), + &TxBaseImplication(()), + TransactionSource::External, + ) + } + + fn validate_same_block_calls( + extension: &RateLimitTransactionExtension, + calls: &[RuntimeCall], + ) -> Result< + ( + sp_runtime::transaction_validity::ValidTransaction, + Vec, Option>, bool)>>, + RuntimeOrigin, + ), + TransactionValidityError, + > { + extension.validate_calls_same_block(RuntimeOrigin::signed(42), calls.iter()) + } + + #[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 target = target_for_call(&call); + assert_eq!(LastSeen::::get(target, None::), None); + }); + } + + #[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); + 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"); + assert!(post_val.is_none()); + }); + } + + #[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); + let target = RateLimitTarget::Transaction(identifier); + Limits::::insert(target, RateLimit::Global(RateLimitKind::Exact(4))); + LastSeen::::insert(target, 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_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(|| { + 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(|| { + let extension = new_tx_extension(); + let call = remark_call(); + let identifier = identifier_for(&call); + let target = RateLimitTarget::Transaction(identifier); + Limits::::insert(target, RateLimit::Global(RateLimitKind::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(target, 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); + let target = RateLimitTarget::Transaction(identifier); + Limits::::insert(target, RateLimit::Global(RateLimitKind::Exact(5))); + LastSeen::::insert(target, 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_same_block_rejects_duplicate_usage() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + let call = remark_call(); + let identifier = identifier_for(&call); + let target = RateLimitTarget::Transaction(identifier); + + assert_ok!(RateLimiting::register_call( + RuntimeOrigin::root(), + Box::new(call.clone()), + None, + )); + Limits::::insert(target, RateLimit::Global(RateLimitKind::Exact(5))); + + System::set_block_number(10); + + let err = validate_same_block_calls(&extension, &[call.clone(), call.clone()]) + .expect_err("duplicate should block"); + match err { + TransactionValidityError::Invalid(InvalidTransaction::Custom(code)) => { + assert_eq!(code, RATE_LIMIT_DENIED); + } + other => panic!("unexpected error: {:?}", other), + } + }); + } + + #[test] + fn tx_extension_same_block_allows_distinct_usage_keys() { + new_test_ext().execute_with(|| { + let extension = new_tx_extension(); + let call_a = multi_scope_call(5); + let call_b = multi_scope_call(6); + let identifier = identifier_for(&call_a); + let target = RateLimitTarget::Transaction(identifier); + + assert_ok!(RateLimiting::register_call( + RuntimeOrigin::root(), + Box::new(call_a.clone()), + None, + )); + + let mut scopes = BTreeMap::new(); + scopes.insert(5u16, RateLimitKind::Exact(5)); + scopes.insert(6u16, RateLimitKind::Exact(5)); + Limits::::insert(target, RateLimit::Scoped(scopes)); + + System::set_block_number(10); + + let (_valid, vals, _) = + validate_same_block_calls(&extension, &[call_a, call_b]).expect("valid"); + assert_eq!(vals.len(), 2); + }); + } + + #[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); + let target = RateLimitTarget::Transaction(identifier); + Limits::::insert(target, RateLimit::Global(RateLimitKind::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(target, None::), None); + }); + } + + #[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(|| { + 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 new file mode 100644 index 0000000000..71a1879fd9 --- /dev/null +++ b/pallets/rate-limiting/src/types.rs @@ -0,0 +1,233 @@ +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}; +use sp_std::{collections::btree_map::BTreeMap, collections::btree_set::BTreeSet}; +use subtensor_macros::freeze_struct; + +/// Resolves the optional identifier within which a rate limit applies and can optionally adjust +/// enforcement behaviour. +pub trait RateLimitScopeResolver { + /// Returns `Some(scopes)` when the limit should be applied per-scope, or `None` for global + /// limits. + 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 { + BypassDecision::enforce_and_record() + } + + /// Optionally adjusts the effective span used during enforcement. Defaults to the original + /// `span`. + fn adjust_span(_origin: &Origin, _call: &Call, span: Span) -> Span { + span + } +} + +/// 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(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>; +} + +/// 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, + 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, +)] +#[freeze_struct("52b1e178333ab182")] +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, +} + +/// Policy describing the block span enforced by a rate limit. +#[derive( + Serialize, + Deserialize, + Clone, + Copy, + PartialEq, + Eq, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + Debug, +)] +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-scope spans. +#[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" +)] +pub enum RateLimit { + /// Global span applied to every invocation. + Global(RateLimitKind), + /// Per-scope spans keyed by `Scope`. + Scoped(BTreeMap>), +} + +impl RateLimit +where + Scope: Ord, +{ + /// 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(scope, kind); + Self::Scoped(map) + } + + /// 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::Scoped(map) => scope.and_then(|key| map.get(key)), + } + } + + /// 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(scope, kind); + *self = RateLimit::Scoped(map); + } + RateLimit::Scoped(map) => { + map.insert(scope, kind); + } + } + } + + /// Removes a scoped entry, returning whether one existed. + pub fn remove_scope(&mut self, scope: &Scope) -> bool { + match self { + RateLimit::Global(_) => false, + RateLimit::Scoped(map) => map.remove(scope).is_some(), + } + } + + /// 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()) + } +} diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index 99ba71629f..d5b7d2bffb 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -56,6 +56,7 @@ sha2.workspace = true rand_chacha.workspace = true pallet-crowdloan.workspace = true pallet-subtensor-proxy.workspace = true +rate-limiting-interface.workspace = true pallet-shield.workspace = true pallet-scheduler.workspace = true @@ -121,6 +122,7 @@ std = [ "pallet-crowdloan/std", "pallet-drand/std", "pallet-subtensor-proxy/std", + "rate-limiting-interface/std", "pallet-subtensor-swap/std", "pallet-shield/std", "pallet-aura/std", diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index 1dd62bab0b..2fbb7515a6 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -136,7 +136,6 @@ mod pallet_benchmarks { Subtensor::::set_commit_reveal_weights_enabled(netuid, false); SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000_000_000_u64)); SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000_000_000_000_u64)); - Subtensor::::set_weights_set_rate_limit(netuid, 0); let mut seed: u32 = 1; let mut dests = Vec::new(); @@ -244,8 +243,6 @@ mod pallet_benchmarks { caller.clone() )); - Subtensor::::set_serving_rate_limit(netuid, 0); - #[extrinsic_call] _( RawOrigin::Signed(caller.clone()), @@ -284,8 +281,6 @@ mod pallet_benchmarks { caller.clone() )); - Subtensor::::set_serving_rate_limit(netuid, 0); - #[extrinsic_call] _( RawOrigin::Signed(caller.clone()), @@ -353,7 +348,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); add_balance_to_coldkey_account::(&coldkey, amount.into()); @@ -381,7 +375,6 @@ mod pallet_benchmarks { Subtensor::::init_new_network(netuid, tempo); Subtensor::::set_network_registration_allowed(netuid, true); - Subtensor::::set_weights_set_rate_limit(netuid, 0); Subtensor::::set_difficulty(netuid, 1); SubtokenEnabled::::insert(netuid, true); @@ -416,7 +409,6 @@ mod pallet_benchmarks { Subtensor::::init_new_network(netuid, tempo); Subtensor::::set_network_registration_allowed(netuid, true); SubtokenEnabled::::insert(netuid, true); - Subtensor::::set_weights_set_rate_limit(netuid, 0); Subtensor::::set_difficulty(netuid, 1); Subtensor::::set_burn(netuid, benchmark_registration_burn()); @@ -655,7 +647,6 @@ mod pallet_benchmarks { Subtensor::::init_new_network(netuid, tempo); Subtensor::::set_network_registration_allowed(netuid, true); Subtensor::::set_commit_reveal_weights_enabled(netuid, true); - Subtensor::::set_weights_set_rate_limit(netuid, 0); Subtensor::::set_difficulty(netuid, 1); SubtokenEnabled::::insert(netuid, true); @@ -934,8 +925,6 @@ mod pallet_benchmarks { let _ = Subtensor::::create_account_if_non_existent(&coldkey, &destination); - StakingOperationRateLimiter::::remove((origin.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -991,8 +980,6 @@ mod pallet_benchmarks { let amount_unstaked = AlphaBalance::from(30_000_000_000_u64); - StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1053,8 +1040,6 @@ mod pallet_benchmarks { .saturating_to_num::() .into(); - StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1118,8 +1103,6 @@ mod pallet_benchmarks { allow )); - StakingOperationRateLimiter::::remove((hot.clone(), coldkey.clone(), netuid1)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1172,8 +1155,6 @@ mod pallet_benchmarks { let _ = Subtensor::::create_account_if_non_existent(&dest, &hot); - StakingOperationRateLimiter::::remove((hot.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1229,8 +1210,6 @@ mod pallet_benchmarks { let alpha_to_swap = Subtensor::::get_stake_for_hotkey_and_coldkey_on_subnet(&hot, &coldkey, netuid1); - StakingOperationRateLimiter::::remove((hot.clone(), coldkey.clone(), netuid1)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1252,7 +1231,6 @@ mod pallet_benchmarks { Subtensor::::init_new_network(netuid, 1); Subtensor::::set_network_registration_allowed(netuid, true); SubtokenEnabled::::insert(netuid, true); - Subtensor::::set_weights_set_rate_limit(netuid, 0); Subtensor::::set_burn(netuid, benchmark_registration_burn()); seed_swap_reserves::(netuid); @@ -1295,9 +1273,6 @@ mod pallet_benchmarks { SubtokenEnabled::::insert(netuid, true); Subtensor::::set_commit_reveal_weights_enabled(netuid, false); - // Avoid any weights set rate-limit edge cases during benchmark setup. - Subtensor::::set_weights_set_rate_limit(netuid, 0); - Subtensor::::set_burn(netuid, benchmark_registration_burn()); seed_swap_reserves::(netuid); fund_for_registration::(netuid, &hotkey); @@ -1355,7 +1330,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; add_balance_to_coldkey_account::(&coldkey, amount.into()); @@ -1584,8 +1558,6 @@ mod pallet_benchmarks { staked_amt )); - StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _(RawOrigin::Signed(coldkey), hotkey); } @@ -1640,8 +1612,6 @@ mod pallet_benchmarks { staked_amt )); - StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1825,7 +1795,6 @@ mod pallet_benchmarks { Subtensor::::set_validator_permit_for_uid(netuid, 0, true); Subtensor::::set_commit_reveal_weights_enabled(netuid, true); - WeightsSetRateLimit::::set(netuid, 0); #[extrinsic_call] _( diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index b44d76175a..03f220d140 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::RateLimitingInterface; use safe_math::*; +use sp_runtime::SaturatedConversion; use substrate_fixed::types::{I64F64, U96F32}; -use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, Token}; +use subtensor_runtime_common::{ + AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, Token, rate_limiting, +}; use subtensor_swap_interface::SwapHandler; impl Pallet { @@ -327,7 +331,6 @@ impl Pallet { LastAdjustmentBlock::::remove(netuid); // --- 16. Serving / rho / curves, and other per-net controls. - ServingRateLimit::::remove(netuid); Rho::::remove(netuid); AlphaSigmoidSteepness::::remove(netuid); @@ -335,7 +338,6 @@ impl Pallet { BondsMovingAverage::::remove(netuid); BondsPenalty::::remove(netuid); BondsResetOn::::remove(netuid); - WeightsSetRateLimit::::remove(netuid); ValidatorPruneLen::::remove(netuid); ScalingLawPower::::remove(netuid); TargetRegistrationsPerInterval::::remove(netuid); @@ -461,20 +463,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) { @@ -514,7 +502,10 @@ impl Pallet { pub fn get_network_lock_cost() -> TaoBalance { 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: TaoBalance = if last_lock_block == 0 { 1 } else { 2 }.into(); @@ -568,12 +559,6 @@ impl Pallet { pub fn get_network_last_lock() -> TaoBalance { 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/extensions/subtensor.rs b/pallets/subtensor/src/extensions/subtensor.rs index 797ab68216..c0e9677729 100644 --- a/pallets/subtensor/src/extensions/subtensor.rs +++ b/pallets/subtensor/src/extensions/subtensor.rs @@ -70,9 +70,6 @@ where CustomTransactionError::HotKeyNotRegisteredInNetwork } Error::::InvalidIpAddress => CustomTransactionError::InvalidIpAddress, - Error::::ServingRateLimitExceeded => { - CustomTransactionError::ServingRateLimitExceeded - } Error::::InvalidPort => CustomTransactionError::InvalidPort, Error::::NonAssociatedColdKey => CustomTransactionError::NonAssociatedColdKey, _ => CustomTransactionError::BadRequest, diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 7c664fc1c1..d76eb3603c 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -679,15 +679,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 { @@ -739,12 +730,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 { @@ -913,21 +898,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 delegate take rate limiting - #[pallet::type_value] - pub fn DefaultTxDelegateTakeRateLimit() -> u64 { - T::InitialTxDelegateTakeRateLimit::get() - } /// Default value for chidlkey take rate limiting #[pallet::type_value] @@ -941,12 +911,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 { @@ -1069,12 +1033,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 { @@ -1108,11 +1066,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 dissolve network schedule before execution #[pallet::storage] pub type DissolveNetworkScheduleDuration = @@ -1721,10 +1674,6 @@ pub mod pallet { pub type OwnerCutEnabled = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultOwnerCutEnabled>; - /// 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>; @@ -1874,11 +1823,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>; @@ -1972,11 +1916,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 = @@ -2056,15 +1995,6 @@ pub mod pallet { DefaultRAORecycledForRegistration, >; - /// --- ITEM ( tx_rate_limit ) - #[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 = @@ -2212,7 +2142,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>; @@ -2433,20 +2364,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/config.rs b/pallets/subtensor/src/macros/config.rs index 8eec97a5be..83def67a94 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -10,6 +10,7 @@ mod config { use frame_support::PalletId; use pallet_alpha_assets::AlphaAssetsInterface; use pallet_commitments::GetCommitments; + use rate_limiting_interface::RateLimitingInterface; use subtensor_runtime_common::AuthorshipInfo; use subtensor_swap_interface::{SwapEngine, SwapHandler}; @@ -62,6 +63,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: RateLimitingInterface< + 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, + >, + >; + /// Interface to mint, burn, and recycle subnet alpha. type AlphaAssets: AlphaAssetsInterface; @@ -186,15 +198,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; - /// Initial delegate take transaction rate limit. - #[pallet::constant] - type InitialTxDelegateTakeRateLimit: Get; /// Initial childkey take transaction rate limit. #[pallet::constant] type InitialTxChildKeyTakeRateLimit: Get; @@ -213,9 +216,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/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 9ad1225d49..a6c3e8434b 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -793,10 +793,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((::WeightInfo::serve_axon(), DispatchClass::Normal, Pays::No))] pub fn serve_axon( @@ -875,10 +871,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((::WeightInfo::serve_axon_tls(), DispatchClass::Normal, Pays::No))] pub fn serve_axon_tls( diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index cb120b56b5..ea9f25d76f 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -82,16 +82,13 @@ 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. 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. StakingRateLimitExceeded, /// Registration is disabled. @@ -183,8 +180,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. @@ -219,8 +214,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/events.rs b/pallets/subtensor/src/macros/events.rs index 918baf1107..70f93c33df 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. @@ -101,24 +99,16 @@ mod events { MinDifficultySet(NetUid, u64), /// setting max difficulty on a network. MaxDifficultySet(NetUid, u64), - /// setting the prometheus serving rate limit. - ServingRateLimitSet(NetUid, u64), /// setting burn on a network. BurnSet(NetUid, TaoBalance), /// setting max burn on a network. MaxBurnSet(NetUid, TaoBalance), /// setting min burn on a network. MinBurnSet(NetUid, TaoBalance), - /// 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) AdminFreezeWindowSet(u16), - /// setting the owner hyperparameter rate limit in epochs - OwnerHyperparamRateLimitSet(u16), /// minimum childkey take set MinChildKeyTakeSet(u16), /// subnet-specific minimum childkey take set @@ -145,8 +135,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 start call delay is set. diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 55f6bd84a9..cb99b011d8 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); @@ -127,8 +116,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::()) // Remove AddStakeBurn entries from LastRateLimitedBlock @@ -139,12 +126,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_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/migrations/migrate_network_lock_cost_2500.rs b/pallets/subtensor/src/migrations/migrate_network_lock_cost_2500.rs deleted file mode 100644 index 3206a27fbb..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(NEW_LAST_LOCK_RAO.into()); - 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 9b67dfd583..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(TaoBalance::from(1_000_000_000_000_u64)); - 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 f582a631fc..1854172a4e 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -28,14 +28,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_add_stake_burn_rate_limit; pub mod migrate_remove_commitments_rate_limit; pub mod migrate_remove_deprecated_conviction_maps; diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index b88e75cd31..08edde6f59 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -66,7 +66,6 @@ impl Pallet { netuid, stake_to_be_added, T::SwapInterface::max_price(), - true, false, ) } @@ -155,7 +154,6 @@ impl Pallet { netuid, possible_stake, limit_price, - true, false, ) } diff --git a/pallets/subtensor/src/staking/decrease_take.rs b/pallets/subtensor/src/staking/decrease_take.rs index b099d0a7e7..19aaf7feff 100644 --- a/pallets/subtensor/src/staking/decrease_take.rs +++ b/pallets/subtensor/src/staking/decrease_take.rs @@ -52,11 +52,7 @@ 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. + // --- 5. 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 65ea70a025..07137aef38 100644 --- a/pallets/subtensor/src/staking/increase_take.rs +++ b/pallets/subtensor/src/staking/increase_take.rs @@ -52,27 +52,14 @@ 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. + // --- 5. Set the new take value. Delegates::::insert(hotkey.clone(), take); - // --- 7. Emit the take value. + // --- 6. Emit the take value. log::debug!("TakeIncreased( coldkey:{coldkey:?}, hotkey:{hotkey:?}, take:{take:?} )"); Self::deposit_event(Event::TakeIncreased(coldkey, hotkey, take)); - // --- 8. Ok and return. + // --- 7. Ok and return. Ok(()) } } diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index aafefa28ed..7669be93dc 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. @@ -385,7 +380,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 f2d07189a4..700895ffea 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -1,8 +1,14 @@ -use super::*; +use rate_limiting_interface::RateLimitingInterface; +use sp_runtime::Saturating; use substrate_fixed::types::U96F32; -use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_runtime_common::{ + AlphaBalance, NetUid, TaoBalance, Token, + rate_limiting::{self, RateLimitUsageKey}, +}; 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. /// @@ -219,6 +225,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 = TaoBalance::ZERO; @@ -228,6 +235,25 @@ impl Pallet { } // If not Root network. if !netuid.is_root() { + // Manually filter out rate-limited subnets. + if let Some(span) = staking_ops_span + && !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); @@ -274,7 +300,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 0f6a553c91..cd0fb5f2d5 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -847,7 +847,6 @@ impl Pallet { netuid: NetUid, tao: TaoBalance, price_limit: TaoBalance, - set_limit: bool, drop_fees: bool, ) -> Result { // Transfer TAO from coldkey to the subnet account. @@ -913,10 +912,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. @@ -1141,8 +1136,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)?; @@ -1244,12 +1237,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), @@ -1370,27 +1357,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, - netuid: NetUid, - ) -> Result<(), Error> { - ensure!( - !StakingOperationRateLimiter::::contains_key((hotkey, coldkey, netuid)), - Error::::StakingOperationRateLimitExceeded - ); - - Ok(()) - } } /////////////////////////////////////////// diff --git a/pallets/subtensor/src/subnets/serving.rs b/pallets/subtensor/src/subnets/serving.rs index 5416e3df5d..e956a4a76f 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: OriginFor, 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: OriginFor, netuid: NetUid, @@ -188,26 +180,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 @@ -313,11 +285,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(); @@ -359,11 +326,6 @@ impl Pallet { ); 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 - ); prev_prometheus.block = Self::get_current_block_as_u64(); prev_prometheus.version = version; diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index 46a834946b..55dd154ab9 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -64,13 +64,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 @@ -143,12 +136,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(); @@ -199,7 +186,6 @@ impl Pallet { // --- 11. 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); // --- 12. Add the caller to the neuron set. Self::create_account_if_non_existent(&coldkey, hotkey)?; diff --git a/pallets/subtensor/src/subnets/uids.rs b/pallets/subtensor/src/subnets/uids.rs index 3665b139ff..24c47a70e7 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. @@ -100,8 +104,19 @@ impl Pallet { // 6. Replacement creates a new logical neuron at the reused UID, so the weights timing // state should start from this registration block. 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); Self::set_last_update_for_uid(netuid_index, uid_to_replace, block_number); + let usage = RateLimitUsageKey::SubnetMechanismNeuron { + netuid, + mecid, + uid: uid_to_replace, + }; + T::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SET, + Some(usage), + Some(block_number.saturated_into()), + ); } } @@ -121,9 +136,20 @@ 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 = RateLimitUsageKey::SubnetMechanismNeuron { + netuid, + mecid, + uid: next_uid, + }; + T::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SET, + Some(usage), + Some(block_number.saturated_into()), + ); } Dividends::::mutate(netuid, |v| v.push(0)); ValidatorTrust::::mutate(netuid, |v| v.push(0)); @@ -274,19 +300,54 @@ 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 = RateLimitUsageKey::SubnetMechanismNeuron { + netuid, + mecid, + uid: *uid as u16, + }; + trimmed_last_seen.push(T::RateLimiting::last_seen( + rate_limiting::GROUP_WEIGHTS_SET, + Some(usage), + )); } Incentive::::insert(netuid_index, trimmed_incentive); LastUpdate::::insert(netuid_index, trimmed_lastupdate); + + for uid in 0..current_n { + let usage = RateLimitUsageKey::SubnetMechanismNeuron { netuid, mecid, uid }; + T::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SET, + Some(usage), + None, + ); + } + for (new_uid, last_seen) in trimmed_last_seen.into_iter().enumerate() { + let Some(block) = last_seen else { + continue; + }; + let usage = RateLimitUsageKey::SubnetMechanismNeuron { + netuid, + mecid, + uid: new_uid as u16, + }; + T::RateLimiting::set_last_seen( + rate_limiting::GROUP_WEIGHTS_SET, + 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 39bcfb80b5..79fdf76eb3 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. /// @@ -87,14 +84,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); @@ -219,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 @@ -332,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(); @@ -801,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), @@ -1102,30 +1079,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/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index 2358fcecf1..e3a9c8a27c 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -37,8 +37,6 @@ impl Pallet { // Transfer any remaining balance from old_coldkey to new_coldkey Self::transfer_all_tao_and_kill(old_coldkey, new_coldkey)?; - Self::set_last_tx_block(new_coldkey, Self::get_current_block_as_u64()); - Self::deposit_event(Event::ColdkeySwapped { old_coldkey: old_coldkey.clone(), new_coldkey: new_coldkey.clone(), diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 944ea5877f..42c92660b0 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -1,10 +1,14 @@ use super::*; use frame_support::weights::Weight; +use rate_limiting_interface::RateLimitingInterface; use share_pool::SafeFloat; use sp_core::Get; use sp_std::collections::btree_set::BTreeSet; use substrate_fixed::types::U64F64; -use subtensor_runtime_common::{MechId, NetUid, Token}; +use subtensor_runtime_common::{ + MechId, NetUid, Token, + rate_limiting::{self, RateLimitUsageKey}, +}; impl Pallet { /// Swaps the hotkey of a coldkey account. @@ -24,7 +28,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. /// * `NewHotKeyNotCleanForRootSwap` - If the swap touches root and the new hotkey @@ -52,26 +55,16 @@ 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(); - - // 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)); - match netuid { - // 7. Ensure the hotkey is not registered on the network before, if netuid is provided + // 5. Ensure the hotkey is not registered on the network before, if netuid is provided Some(netuid) => { ensure!( !Self::is_hotkey_registered_on_specific_network(new_hotkey, netuid), Error::::HotKeyAlreadyRegisteredInSubNet ); } - // 7.1 Ensure the new hotkey is not already registered on any network, only if netuid is none + // 5.1 Ensure the new hotkey is not already registered on any network, only if netuid is + // None None => { ensure!( !Self::is_hotkey_registered_on_any_network(new_hotkey), @@ -96,22 +89,24 @@ 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); - 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); + // 8. Swap last-seen + let last_tx_block = T::RateLimiting::last_seen( + rate_limiting::GROUP_SWAP_KEYS, + Some(RateLimitUsageKey::Account(old_hotkey.clone())), + ); + T::RateLimiting::set_last_seen( + rate_limiting::GROUP_SWAP_KEYS, + Some(RateLimitUsageKey::Account(new_hotkey.clone())), + last_tx_block, + ); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - // 10. Swap LastTxBlockChildKeyTake + // 7. 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); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - // 11. fork for swap hotkey on a specific subnet case after do the common check + // 8. fork for swap hotkey on a specific subnet case after do the common check if let Some(netuid) = netuid { return Self::swap_hotkey_on_subnet( &coldkey, old_hotkey, new_hotkey, netuid, weight, keep_stake, @@ -119,11 +114,11 @@ impl Pallet { }; // Start to do everything for swap hotkey on all subnets case - // 12. Get the cost for swapping the key + // 9. Get the cost for swapping the key let swap_cost = Self::get_key_swap_cost(); log::debug!("Swap cost: {swap_cost:?}"); - // 13. Ensure the coldkey has enough balance to pay for the swap + // 10. Ensure the coldkey has enough balance to pay for the swap ensure!( Self::can_remove_balance_from_coldkey_account(&coldkey, swap_cost.into()), Error::::NotEnoughBalanceToPaySwapHotKey @@ -135,7 +130,7 @@ impl Pallet { Self::recycle_tao(&coldkey, swap_cost.into())?; weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 2)); - // 19. Perform the hotkey swap + // 13. Perform the hotkey swap Self::perform_hotkey_swap_on_all_subnets( old_hotkey, new_hotkey, @@ -144,18 +139,14 @@ impl Pallet { keep_stake, )?; - // 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 + // 14. Emit an event for the hotkey swap Self::deposit_event(Event::HotkeySwapped { coldkey, old_hotkey: old_hotkey.clone(), new_hotkey: new_hotkey.clone(), }); - // 22. Return the weight of the operation + // 15. Return the weight of the operation Ok(Some(weight).into()) } @@ -237,8 +228,12 @@ impl Pallet { // 7. 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)); // 8. Swap LastTxBlockDelegateTake // LastTxBlockDelegateTake( hotkey ) --> u64 -- the last transaction block for the hotkey delegate take. @@ -299,6 +294,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 @@ -360,7 +358,6 @@ impl Pallet { )?; // 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/pallets/subtensor/src/tests/children.rs b/pallets/subtensor/src/tests/children.rs index ec191ba0e7..d31e629b2a 100644 --- a/pallets/subtensor/src/tests/children.rs +++ b/pallets/subtensor/src/tests/children.rs @@ -2318,7 +2318,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, @@ -2712,8 +2712,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)]); @@ -2809,8 +2807,6 @@ fn test_set_weights_no_parent() { stake_to_give_child, ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - // Has stake and no parent step_block(7200 + 1); @@ -2912,7 +2908,6 @@ fn test_childkey_take_drain() { &nominator, TaoBalance::from(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); @@ -3710,9 +3705,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, @@ -4363,8 +4355,6 @@ fn test_root_children_enable_subnet_owner_set_weights() { root_stake, ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - let version_key = SubtensorModule::get_weights_version_key(netuid); let uids: Vec = vec![0]; let values: Vec = vec![u16::MAX]; @@ -4534,7 +4524,6 @@ fn test_register_network_schedules_root_validators() { ); // --- Verify subnet owner can now set weights --- - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); let version_key = SubtensorModule::get_weights_version_key(netuid); @@ -4664,7 +4653,6 @@ fn test_register_network_schedules_root_validators_auto_parent_delegation_flag() ); // --- Verify subnet owner can now set weights --- - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); let version_key = SubtensorModule::get_weights_version_key(netuid); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index b89041c98c..22c9028161 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -2657,8 +2657,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); @@ -2745,8 +2743,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()); @@ -2840,8 +2836,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); @@ -3212,7 +3206,6 @@ fn test_mining_emission_distribution_with_no_root_sell() { &miner_coldkey, TaoBalance::from(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 @@ -3407,7 +3400,6 @@ fn test_mining_emission_distribution_with_root_sell() { &miner_coldkey, TaoBalance::from(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 @@ -4066,7 +4058,6 @@ fn test_disabling_owner_cut_sends_subnet_emission_to_miners_and_validators() { stake.into() )); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_max_allowed_validators(netuid, 1); step_block(subnet_tempo); @@ -4179,7 +4170,6 @@ fn test_pending_emission_start_call_not_done() { &validator_coldkey, TaoBalance::from(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/ensure.rs b/pallets/subtensor/src/tests/ensure.rs index 1253285306..e8edd41381 100644 --- a/pallets/subtensor/src/tests/ensure.rs +++ b/pallets/subtensor/src/tests/ensure.rs @@ -5,8 +5,7 @@ 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, utils::rate_limiting::TransactionType}; #[test] fn ensure_subnet_owner_returns_who_and_checks_ownership() { @@ -81,79 +80,49 @@ fn ensure_admin_window_open_blocks_in_freeze_window() { } #[test] -fn ensure_owner_or_root_with_limits_checks_rl_and_freeze() { +fn ensure_owner_or_root_with_limits_checks_rate_limit_for_owner_only() { new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let tempo = 10; + let netuid = NetUid::from(3); add_network(netuid, 10, 0); - SubtokenEnabled::::insert(netuid, true); - let owner: U256 = U256::from(5); + let owner: U256 = U256::from(17); 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); + let limits = [TransactionType::SetChildren]; - // Outside freeze window initially; should pass and return Some(owner) - let res = crate::Pallet::::ensure_sn_owner_or_root_with_limits( + let owner_result = crate::Pallet::::ensure_sn_owner_or_root_with_limits( <::RuntimeOrigin>::signed(owner), netuid, - &[Hyperparameter::Kappa.into()], + &limits, ) - .expect("should pass"); - assert_eq!(res, Some(owner)); - assert_ok!(crate::Pallet::::ensure_admin_window_open(netuid)); + .expect("owner should pass before any prior usage"); + assert_eq!(owner_result, Some(owner)); + + let root_result = crate::Pallet::::ensure_sn_owner_or_root_with_limits( + <::RuntimeOrigin>::root(), + netuid, + &limits, + ) + .expect("root should bypass owner-only rate checks"); + assert_eq!(root_result, None); - // 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); + TransactionType::SetChildren.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()], + &limits ), 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); + let limit = TransactionType::SetChildren.rate_limit_on_subnet::(netuid); + run_to_block(now + limit + 1); assert_ok!(crate::Pallet::::ensure_sn_owner_or_root_with_limits( <::RuntimeOrigin>::signed(owner), netuid, - &[Hyperparameter::Kappa.into()] + &limits )); - 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/tests/epoch.rs b/pallets/subtensor/src/tests/epoch.rs index 02236d892d..87a6b0a410 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() + SubtensorModule::get_network_min_lock(), ); register_ok_neuron(netuid, hotkey, coldkey, 1); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); assert_ok!(SubtensorModule::add_stake( RuntimeOrigin::signed(coldkey), @@ -1016,7 +1015,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); @@ -1581,7 +1579,6 @@ fn test_outdated_weights() { let stake: TaoBalance = 1.into(); 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); @@ -1781,7 +1778,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); @@ -1991,7 +1987,6 @@ fn test_deregistered_miner_bonds() { let stake: TaoBalance = 1.into(); 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); @@ -2727,7 +2722,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); @@ -3668,7 +3662,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, @@ -3740,7 +3733,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, @@ -3833,7 +3825,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, @@ -3882,7 +3873,6 @@ fn test_last_update_size_mismatch() { + (SubtensorModule::get_network_min_lock() * 2.into()), ); 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 265add2802..43d7a13451 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/locks.rs b/pallets/subtensor/src/tests/locks.rs index f4eede30e4..faa5da41f2 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -49,7 +49,6 @@ fn setup_subnet_with_stake( amount, ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -273,7 +272,6 @@ fn test_mixed_perpetual_and_decaying_non_owner_locks_same_hotkey_update_aggregat 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1494,7 +1492,6 @@ fn test_lock_on_multiple_subnets() { 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1562,7 +1559,6 @@ fn test_unstake_one_subnet_does_not_affect_other() { 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1636,7 +1632,6 @@ fn test_hotkey_conviction_multiple_lockers() { 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1690,7 +1685,6 @@ fn test_mixed_perpetual_owner_and_decaying_non_owner_locks_roll_forward() { 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1760,7 +1754,6 @@ fn test_total_conviction_equals_sum_of_participating_aggregate_convictions() { 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1820,7 +1813,6 @@ fn test_total_conviction_equals_sum_of_individual_lock_convictions_for_many_lock 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); lockers.push((coldkey, hotkey)); @@ -1900,7 +1892,6 @@ fn test_subnet_king_highest_conviction_wins() { 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -2175,7 +2166,6 @@ fn test_reduce_lock_two_coldkeys() { 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -2801,7 +2791,6 @@ fn test_clear_small_nomination_checks_lock() { 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -2871,7 +2860,6 @@ fn test_clear_small_nomination_reduces_only_tiny_amount_from_lock_state() { large_tao, ::SwapInterface::max_price(), false, - false, ) .unwrap(); SubtensorModule::stake_into_subnet( @@ -2881,7 +2869,6 @@ fn test_clear_small_nomination_reduces_only_tiny_amount_from_lock_state() { tiny_tao, ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -3028,7 +3015,6 @@ fn test_epoch_distribution_auto_locks_owner_cut() { stake.into() )); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_max_allowed_validators(netuid, 1); step_block(subnet_tempo); SubnetOwnerCut::::set(u16::MAX / 10); @@ -3253,7 +3239,6 @@ fn test_moving_partial_lock() { 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -3337,7 +3322,6 @@ fn test_moving_partial_lock_same_owners() { 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); diff --git a/pallets/subtensor/src/tests/mechanism.rs b/pallets/subtensor/src/tests/mechanism.rs index ef51c7e8d5..a554ed5c05 100644 --- a/pallets/subtensor/src/tests/mechanism.rs +++ b/pallets/subtensor/src/tests/mechanism.rs @@ -1063,7 +1063,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); add_balance_to_coldkey_account(&ck1, 1.into()); @@ -1147,7 +1146,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); add_balance_to_coldkey_account(&ck1, 1.into()); @@ -1228,7 +1226,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)); @@ -1334,7 +1331,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)); @@ -1402,7 +1398,6 @@ fn test_do_commit_crv3_mechanism_weights_committing_too_fast() { 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"); @@ -1431,17 +1426,7 @@ fn test_do_commit_crv3_mechanism_weights_committing_too_fast() { )); // 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 - ); + // TODO: Check rate limits from the rate-limit-pallet // BUT committing too soon on a DIFFERENT mecid is allowed let other_subid = MechId::from(0u8); @@ -1458,17 +1443,7 @@ fn test_do_commit_crv3_mechanism_weights_committing_too_fast() { // 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 - ); + // TODO: Check rate limits from the rate-limit-pallet // after enough blocks, OK again on original mecid step_block(3); diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index a4c68e9d1b..942524cf37 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -26,17 +26,14 @@ use frame_support::traits::Bounded; use frame_system::Config; use pallet_drand::types::RoundNumber; use pallet_scheduler::ScheduledOf; +use rate_limiting_interface::RateLimitingInterface; use scale_info::prelude::collections::VecDeque; use sp_core::{H256, U256, crypto::Ss58Codec}; use sp_io::hashing::twox_128; -use sp_runtime::{ - AccountId32, - traits::{Hash, Zero}, -}; +use sp_runtime::{AccountId32, SaturatedConversion, traits::Hash, traits::Zero}; use sp_std::marker::PhantomData; -use substrate_fixed::types::{I96F32, U64F64}; -use substrate_fixed::{traits::ToFixed, types::extra::U2}; -use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance}; +use substrate_fixed::types::{I96F32, U64F64, extra::U2}; +use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance, rate_limiting}; #[allow(clippy::arithmetic_side_effects)] fn close(value: u64, target: u64, eps: u64) { @@ -812,206 +809,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(|| { @@ -1024,11 +821,9 @@ fn test_migrate_rate_limit_keys() { // 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); 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(); @@ -1068,22 +863,11 @@ fn test_migrate_rate_limit_keys() { ); assert!(!weight.is_zero(), "Migration weight should be non-zero"); - // Legacy entries were migrated and cleared. - assert_eq!( - SubtensorModule::get_network_last_lock_block(), - 111u64, - "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" @@ -1099,11 +883,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" @@ -1115,11 +894,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" - ); }); } @@ -2036,70 +1810,8 @@ 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; new_test_ext(0).execute_with(|| { let name = b"migrate_restore_subnet_locked".to_vec(); assert!( @@ -2223,116 +1935,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(|| { @@ -2449,15 +2051,25 @@ fn test_migrate_remove_tao_dividends() { } fn do_setup_unactive_sn() -> (Vec, Vec) { + let mut register_network = |hotkey: U256, coldkey: U256| { + let netuid = add_dynamic_network_without_emission_block(&hotkey, &coldkey); + ::RateLimiting::set_last_seen( + rate_limiting::GROUP_REGISTER_NETWORK, + None, + Some(SubtensorModule::get_current_block_as_u64()), + ); + netuid + }; + // Register some subnets - let netuid0 = add_dynamic_network_without_emission_block(&U256::from(0), &U256::from(0)); - let netuid1 = add_dynamic_network_without_emission_block(&U256::from(1), &U256::from(1)); - let netuid2 = add_dynamic_network_without_emission_block(&U256::from(2), &U256::from(2)); + let netuid0 = register_network(U256::from(0), U256::from(0)); + let netuid1 = register_network(U256::from(1), U256::from(1)); + let netuid2 = register_network(U256::from(2), U256::from(2)); let inactive_netuids = vec![netuid0, netuid1, netuid2]; // Add active subnets - let netuid3 = add_dynamic_network_without_emission_block(&U256::from(3), &U256::from(3)); - let netuid4 = add_dynamic_network_without_emission_block(&U256::from(4), &U256::from(4)); - let netuid5 = add_dynamic_network_without_emission_block(&U256::from(5), &U256::from(5)); + let netuid3 = register_network(U256::from(3), U256::from(3)); + let netuid4 = register_network(U256::from(4), U256::from(4)); + let netuid5 = register_network(U256::from(5), U256::from(5)); let active_netuids = vec![netuid3, netuid4, netuid5]; let netuids: Vec = inactive_netuids .iter() diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 8c553e3ee8..e8fd501542 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -15,14 +15,18 @@ use frame_support::weights::constants::RocksDbWeight; use frame_support::{PalletId, derive_impl}; use frame_support::{ assert_ok, parameter_types, + storage::unhashed, traits::{Hooks, PrivilegeCmp}, }; 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 share_pool::SafeFloat; use sp_core::{ConstU64, Get, H256, U256, offchain::KeyTypeId}; +use sp_io::hashing::twox_128; +use sp_io::storage; use sp_runtime::Perbill; use sp_runtime::{ BuildStorage, Percent, @@ -31,7 +35,10 @@ use sp_runtime::{ use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; use sp_tracing::tracing_subscriber; use substrate_fixed::types::U64F64; -use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoBalance}; +use subtensor_runtime_common::{ + AuthorshipInfo, NetUid, TaoBalance, + rate_limiting::{GROUP_REGISTER_NETWORK, RateLimitUsageKey}, +}; use subtensor_swap_interface::{Order, SwapHandler}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; type Block = frame_system::mocking::MockBlock; @@ -204,9 +211,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 pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -232,7 +236,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 @@ -291,9 +294,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; type InitialMaxBurn = InitialMaxBurn; type InitialMinBurn = InitialMinBurn; @@ -304,7 +304,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; @@ -325,6 +324,7 @@ impl crate::Config for Test { type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; + type RateLimiting = NoRateLimiting; type AlphaAssets = AlphaAssets; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; @@ -370,6 +370,77 @@ impl CommitmentsInterface for CommitmentsI { fn purge_netuid(_netuid: NetUid) {} } +pub struct NoRateLimiting; + +impl RateLimitingInterface 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, + { + let Ok(target) = target.try_into_rate_limit_target::() else { + return None; + }; + + if !matches!(target, RateLimitTarget::Group(group) if group == GROUP_REGISTER_NETWORK) { + return None; + } + + if usage_key.is_some() { + return None; + } + let key = mock_register_network_last_seen_key(); + unhashed::get::(&key) + } + + fn set_last_seen( + target: TargetArg, + usage_key: Option, + block: Option, + ) where + TargetArg: TryIntoRateLimitTarget, + { + let Ok(target) = target.try_into_rate_limit_target::() else { + return; + }; + + if !matches!(target, RateLimitTarget::Group(group) if group == GROUP_REGISTER_NETWORK) { + return; + } + + if usage_key.is_some() { + return; + } + let key = mock_register_network_last_seen_key(); + match block { + Some(block) => unhashed::put(&key, &block), + None => storage::clear(&key), + }; + } +} + +fn mock_register_network_last_seen_key() -> Vec { + let mut key = Vec::with_capacity(32); + key.extend_from_slice(&twox_128(b"MockRateLimiting")); + key.extend_from_slice(&twox_128(b"RegisterNetworkLastSeen")); + key +} + parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; @@ -994,7 +1065,6 @@ pub fn increase_stake_on_coldkey_hotkey_account( tao_staked, ::SwapInterface::max_price(), false, - false, ) .unwrap(); } @@ -1014,10 +1084,6 @@ pub fn increase_stake_on_hotkey_account(hotkey: &U256, increment: TaoBalance, ne ); } -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: TaoBalance, alpha: AlphaBalance) { SubnetTAO::::set(netuid, tao); SubnetAlphaIn::::set(netuid, alpha); @@ -1097,8 +1163,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/mock_high_ed.rs b/pallets/subtensor/src/tests/mock_high_ed.rs index 0f0d818c38..899a82ee95 100644 --- a/pallets/subtensor/src/tests/mock_high_ed.rs +++ b/pallets/subtensor/src/tests/mock_high_ed.rs @@ -15,6 +15,7 @@ use frame_support::{parameter_types, traits::PrivilegeCmp}; use frame_system as system; use frame_system::{EnsureRoot, limits, offchain::CreateTransactionBase}; use pallet_subtensor_proxy as pallet_proxy; +use rate_limiting_interface::{RateLimitingInterface, TryIntoRateLimitTarget}; use sp_core::{ConstU64, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; use sp_runtime::{ @@ -24,6 +25,7 @@ use sp_runtime::{ use sp_std::{cmp::Ordering, sync::OnceLock}; use sp_tracing::tracing_subscriber; use substrate_fixed::types::U64F64; +use subtensor_runtime_common::rate_limiting::RateLimitUsageKey; use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoBalance}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; type Block = frame_system::mocking::MockBlock; @@ -166,7 +168,6 @@ parameter_types! { 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 pub const InitialBurn: u64 = 0; pub const InitialMinBurn: u64 = 500_000; @@ -251,9 +252,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; type InitialMaxBurn = InitialMaxBurn; type InitialMinBurn = InitialMinBurn; @@ -264,7 +262,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; @@ -285,6 +282,7 @@ impl crate::Config for Test { type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; + type RateLimiting = NoRateLimiting; type AlphaAssets = AlphaAssets; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; @@ -330,6 +328,42 @@ impl CommitmentsInterface for CommitmentsI { fn purge_netuid(_netuid: NetUid) {} } +pub struct NoRateLimiting; + +impl RateLimitingInterface for NoRateLimiting { + type GroupId = subtensor_runtime_common::rate_limiting::GroupId; + type CallMetadata = RuntimeCall; + type Limit = BlockNumber; + type Scope = 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 + } + + fn set_last_seen( + _target: TargetArg, + _usage_key: Option, + _block: Option, + ) where + TargetArg: TryIntoRateLimitTarget, + { + } +} + parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; diff --git a/pallets/subtensor/src/tests/move_stake.rs b/pallets/subtensor/src/tests/move_stake.rs index a991df20a5..c95f5fd923 100644 --- a/pallets/subtensor/src/tests/move_stake.rs +++ b/pallets/subtensor/src/tests/move_stake.rs @@ -36,7 +36,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( @@ -114,7 +113,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( @@ -183,7 +181,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( @@ -290,7 +287,6 @@ fn test_do_move_nonexistent_destination_hotkey() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -356,7 +352,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( @@ -427,7 +422,6 @@ fn test_do_move_multiple_times() { initial_stake.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = @@ -439,7 +433,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, @@ -451,7 +445,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, @@ -502,7 +496,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( @@ -570,7 +563,6 @@ fn test_do_move_same_hotkey_fails() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = @@ -622,7 +614,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( @@ -684,7 +675,6 @@ fn test_do_move_storage_updates() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -752,7 +742,6 @@ fn test_move_full_amount_same_netuid() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -821,7 +810,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( @@ -885,7 +873,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), @@ -925,7 +912,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( @@ -1035,7 +1021,6 @@ fn test_do_transfer_insufficient_stake() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1076,7 +1061,6 @@ fn test_do_transfer_wrong_origin() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1115,7 +1099,6 @@ fn test_do_transfer_minimum_stake_check() { stake_amount, ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1163,7 +1146,6 @@ fn test_do_transfer_different_subnets() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1230,7 +1212,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( @@ -1339,7 +1320,6 @@ fn test_do_swap_insufficient_stake() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1375,7 +1355,6 @@ fn test_do_swap_wrong_origin() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1414,7 +1393,6 @@ fn test_do_swap_minimum_stake_check() { total_stake, ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1451,7 +1429,6 @@ fn test_do_swap_same_subnet() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1497,7 +1474,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( @@ -1550,7 +1526,6 @@ fn test_do_swap_storage_updates() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1611,7 +1586,6 @@ fn test_do_swap_multiple_times() { initial_stake.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1621,7 +1595,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, @@ -1637,7 +1610,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, @@ -1683,7 +1656,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( @@ -1775,7 +1747,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, @@ -1828,27 +1800,16 @@ fn test_transfer_stake_rate_limited() { netuid, stake_amount.into(), ::SwapInterface::max_price(), - true, false, ) .unwrap(); - let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + let _ = 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 - ); + // TODO: Check rate limits from the rate-limit-pallet }); } @@ -1875,7 +1836,6 @@ fn test_transfer_stake_doesnt_limit_destination_coldkey() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1893,11 +1853,7 @@ fn test_transfer_stake_doesnt_limit_destination_coldkey() { alpha ),); - assert!(!StakingOperationRateLimiter::::contains_key(( - hotkey, - destination_coldkey, - netuid2 - ))); + // TODO: Check rate limits from the rate-limit-pallet }); } @@ -1922,7 +1878,6 @@ fn test_swap_stake_limits_destination_netuid() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1939,16 +1894,6 @@ fn test_swap_stake_limits_destination_netuid() { alpha ),); - assert!(!StakingOperationRateLimiter::::contains_key(( - hotkey, - origin_coldkey, - netuid - ))); - - assert!(StakingOperationRateLimiter::::contains_key(( - hotkey, - origin_coldkey, - netuid2 - ))); + // TODO: Check rate limits from the rate-limit-pallet }); } diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index d55a8e8411..07b6344151 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -402,7 +402,6 @@ fn dissolve_clears_all_per_subnet_storages() { PendingOwnerCut::::insert(net, AlphaBalance::from(1)); BlocksSinceLastStep::::insert(net, 1u64); LastMechansimStepBlock::::insert(net, 1u64); - ServingRateLimit::::insert(net, 1u64); Rho::::insert(net, 1u16); AlphaSigmoidSteepness::::insert(net, 1i16); @@ -413,7 +412,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); @@ -559,7 +557,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)); @@ -569,7 +566,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/tests/serving.rs b/pallets/subtensor/src/tests/serving.rs index 2979d4438c..7e4a6fcc87 100644 --- a/pallets/subtensor/src/tests/serving.rs +++ b/pallets/subtensor/src/tests/serving.rs @@ -282,23 +282,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 + )); }); } @@ -480,19 +476,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/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 0fe951a29b..8ccc20a273 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -863,7 +863,6 @@ fn test_remove_stake_insufficient_liquidity() { amount_staked.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -934,8 +933,6 @@ fn test_remove_stake_total_issuance_no_change() { netuid, ); - 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, @@ -1038,7 +1035,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), @@ -1694,7 +1690,7 @@ fn test_clear_small_nominations() { SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold1, netuid); let unstake_amount1 = AlphaBalance::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, @@ -1718,7 +1714,7 @@ fn test_clear_small_nominations() { SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold2, netuid); let unstake_amount2 = AlphaBalance::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, @@ -1904,7 +1900,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( @@ -1978,7 +1974,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( @@ -2035,129 +2031,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 - add_balance_to_coldkey_account(&coldkey0, 100000.into()); - - // 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 - add_balance_to_coldkey_account(&coldkey0, 100000.into()); - - // 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() { @@ -2201,10 +2074,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, @@ -2444,7 +2317,6 @@ fn test_mining_emission_distribution_validator_valiminer_miner() { add_balance_to_coldkey_account(&validator_coldkey, stake + ExistentialDeposit::get()); add_balance_to_coldkey_account(&validator_miner_coldkey, stake + ExistentialDeposit::get()); add_balance_to_coldkey_account(&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 @@ -3956,7 +3828,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, @@ -4151,8 +4023,6 @@ fn test_remove_99_9991_per_cent_stake_works_precisely() { let coldkey_balance_before_remove = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); - let remove_amount = AlphaBalance::from( (U64F64::from_num(alpha) * U64F64::from_num(0.999991)).to_num::(), ); @@ -4223,7 +4093,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, @@ -4464,8 +4334,6 @@ fn test_unstake_all_alpha_works() { stake_amount )); - 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, @@ -4519,7 +4387,6 @@ fn test_unstake_all_works() { stake_amount * 10.into(), u64::from(stake_amount * 100.into()).into(), ); - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); // Unstake all alpha to free balance assert_ok!(SubtensorModule::unstake_all( @@ -4574,7 +4441,6 @@ fn test_stake_into_subnet_ok() { amount.into(), TaoBalance::MAX, false, - false, )); let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 / u16::MAX as f64; @@ -4629,7 +4495,6 @@ fn test_stake_into_subnet_low_amount() { amount.into(), TaoBalance::MAX, false, - false, )); let expected_stake = AlphaBalance::from(((amount as f64) * 0.997 / current_price) as u64); @@ -4678,7 +4543,6 @@ fn test_unstake_from_subnet_low_amount() { amount.into(), TaoBalance::MAX, false, - false, )); // Remove stake @@ -4796,7 +4660,6 @@ fn test_unstake_from_subnet_prohibitive_limit() { amount.into(), TaoBalance::MAX, false, - false, )); // Remove stake @@ -4872,7 +4735,6 @@ fn test_unstake_full_amount() { amount.into(), TaoBalance::MAX, false, - false, )); // Remove stake @@ -5014,7 +4876,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, @@ -5275,7 +5137,7 @@ fn test_default_min_stake_sufficiency() { let fee_stake = (fee_rate * u64::from(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, @@ -5298,51 +5160,77 @@ fn test_default_min_stake_sufficiency() { }); } +// TODO: Revise when user liquidity is available +// fn setup_positions(netuid: NetUid) { +// for (coldkey, hotkey, low_price, high_price, liquidity) in [ +// (2, 12, 0.1, 0.20, 1_000_000_000_000_u64), +// (3, 13, 0.15, 0.25, 200_000_000_000_u64), +// (4, 14, 0.25, 0.5, 3_000_000_000_000_u64), +// (5, 15, 0.3, 0.6, 300_000_000_000_u64), +// (6, 16, 0.4, 0.7, 8_000_000_000_000_u64), +// (7, 17, 0.5, 0.8, 600_000_000_000_u64), +// (8, 18, 0.6, 0.9, 700_000_000_000_u64), +// (9, 19, 0.7, 1.0, 100_000_000_000_u64), +// (10, 20, 0.8, 1.1, 300_000_000_000_u64), +// ] { +// SubtensorModule::create_account_if_non_existent(&U256::from(coldkey), &U256::from(hotkey)); +// add_balance_to_coldkey_account( +// &U256::from(coldkey), +// 1_000_000_000_000_000, +// ); +// SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( +// &U256::from(hotkey), +// &U256::from(coldkey), +// netuid.into(), +// 1_000_000_000_000_000.into(), +// ); + +// let tick_low = price_to_tick(low_price); +// let tick_high = price_to_tick(high_price); +// let add_lq_call = SwapCall::::add_liquidity { +// hotkey: U256::from(hotkey), +// netuid: netuid.into(), +// tick_low, +// tick_high, +// liquidity, +// }; +// assert_ok!( +// RuntimeCall::Swap(add_lq_call).dispatch(RuntimeOrigin::signed(U256::from(coldkey))) +// ); +// } +// } + #[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() * 10.into(); - let fee = DefaultMinStake::::get(); - let init_balance = amount + fee + ExistentialDeposit::get(); +fn test_large_swap() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(100); - 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); + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); + pallet_subtensor_swap::EnabledUserLiquidity::::insert(NetUid::from(netuid), true); - add_balance_to_coldkey_account(&cold1, init_balance); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(cold1), - hot1, + // Force the swap to initialize + SubtensorModule::swap_tao_for_alpha( netuid, - (amount + fee).into() - )); - - assert_err!( - SubtensorModule::remove_stake( - RuntimeOrigin::signed(cold1), - hot1, - netuid, - AlphaBalance::from(amount.to_u64()) - ), - Error::::StakingOperationRateLimitExceeded - ); - - // Test limit clear each block - assert!(StakingOperationRateLimiter::::contains_key(( - hot1, cold1, netuid - ))); + TaoBalance::ZERO, + 1_000_000_000_000_u64.into(), + false, + ) + .unwrap(); - next_block(); + // TODO: Revise when user liquidity is available + // setup_positions(netuid.into()); - assert!(!StakingOperationRateLimiter::::contains_key(( - hot1, cold1, netuid - ))); + let swap_amount = TaoBalance::from(100_000_000_000_000_u64); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + swap_amount, + )); }); } @@ -5502,7 +5390,6 @@ fn test_staking_records_flow() { amount.into(), TaoBalance::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 12b23c74e4..e44119540c 100644 --- a/pallets/subtensor/src/tests/subnet.rs +++ b/pallets/subtensor/src/tests/subnet.rs @@ -633,8 +633,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, @@ -665,8 +663,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_hotkey.rs b/pallets/subtensor/src/tests/swap_hotkey.rs index 3fdacf23be..7979396b8f 100644 --- a/pallets/subtensor/src/tests/swap_hotkey.rs +++ b/pallets/subtensor/src/tests/swap_hotkey.rs @@ -719,67 +719,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 = SubtensorModule::get_key_swap_cost() * 2.into(); - - let tx_rate_limit = 1; - - // Get the current transaction rate limit - let current_tx_rate_limit = SubtensorModule::get_tx_rate_limit(); - 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); - - // Setup initial state - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, old_hotkey, coldkey, 0); - add_balance_to_coldkey_account(&coldkey, swap_cost + ExistentialDeposit::get()); - - // Perform the first swap - assert_ok!(SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &old_hotkey, - &new_hotkey_1, - None, - false, - )); - - // 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, - false, - ), - 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, - false, - )); - }); -} - // 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() { @@ -1112,7 +1051,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(); @@ -1193,7 +1131,6 @@ fn test_do_swap_hotkey_err_new_hotkey_not_clean_for_root() { Owner::::insert(old_hotkey, coldkey); TotalNetworks::::put(1); - SubtensorModule::set_last_tx_block(&coldkey, 0); let initial_balance = SubtensorModule::get_key_swap_cost() + 1000.into(); add_balance_to_coldkey_account(&coldkey, initial_balance); @@ -1557,52 +1494,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); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - 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); - - // Perform the swap - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - None, - false, - )); - - // Check for new hotkey - assert_eq!( - 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 - ); - }); -} - #[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 48a4442acd..82a515effa 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -774,69 +774,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 = TaoBalance::from(1_000_000_000u64) * 2.into(); - - let tx_rate_limit = 1; - - // Get the current transaction rate limit - let current_tx_rate_limit = SubtensorModule::get_tx_rate_limit(); - 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); - - // Setup initial state - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, old_hotkey, coldkey, 0); - 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), - false - ),); - - // 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), - false - ), - 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, - false - )); - }); -} - // 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() { @@ -1119,7 +1056,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(); @@ -1566,54 +1502,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 delegate_take_block = 4567; - let child_key_take_block = 8910; - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - // 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); - - // 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), - false - ),); - - // Check for new hotkey - assert_eq!( - 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 - ); - }); -} - #[test] fn test_swap_owner_failed_interval_not_passed() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/tests/transaction_extension_pays_no.rs b/pallets/subtensor/src/tests/transaction_extension_pays_no.rs index a7ac62e5e2..91bd83f0ee 100644 --- a/pallets/subtensor/src/tests/transaction_extension_pays_no.rs +++ b/pallets/subtensor/src/tests/transaction_extension_pays_no.rs @@ -623,37 +623,6 @@ fn extension_associate_evm_key_rejects_uid_not_found() { }); } -#[test] -fn extension_register_network_rejects_global_rate_limit() { - new_test_ext(0).execute_with(|| { - let limit = 50u64; - NetworkRateLimit::::put(limit); - System::set_block_number(200u64.into()); - SubtensorModule::set_network_last_lock_block(170); - - let coldkey = U256::from(70); - let hotkey = U256::from(71); - let call = RuntimeCall::SubtensorModule(SubtensorCall::register_network { hotkey }); - let err = validate_signed(coldkey, &call).unwrap_err(); - assert_eq!(err, CustomTransactionError::RateLimitExceeded.into()); - }); -} - -#[test] -fn extension_register_network_accepts_after_global_cooldown() { - new_test_ext(0).execute_with(|| { - let limit = 50u64; - NetworkRateLimit::::put(limit); - System::set_block_number(200u64.into()); - SubtensorModule::set_network_last_lock_block(150); - - let coldkey = U256::from(72); - let hotkey = U256::from(73); - let call = RuntimeCall::SubtensorModule(SubtensorCall::register_network { hotkey }); - assert!(validate_signed(coldkey, &call).is_ok()); - }); -} - #[test] fn extension_associate_evm_key_rejects_associate_rate_limit() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/tests/weights.rs b/pallets/subtensor/src/tests/weights.rs index f9afd96033..63244de80f 100644 --- a/pallets/subtensor/src/tests/weights.rs +++ b/pallets/subtensor/src/tests/weights.rs @@ -910,68 +910,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); - add_balance_to_coldkey_account(&U256::from(66), 1.into()); - 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] @@ -1667,7 +1605,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); @@ -1728,7 +1665,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); @@ -1796,7 +1732,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); @@ -1932,7 +1867,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); add_balance_to_coldkey_account(&U256::from(0), 1.into()); @@ -2032,7 +1966,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); add_balance_to_coldkey_account(&U256::from(0), 1.into()); @@ -2111,7 +2044,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); add_balance_to_coldkey_account(&U256::from(0), 1.into()); add_balance_to_coldkey_account(&U256::from(1), 1.into()); SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( @@ -2194,7 +2126,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); @@ -2343,7 +2274,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); @@ -2745,7 +2675,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); @@ -2945,7 +2874,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); @@ -3109,8 +3037,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); @@ -3296,7 +3222,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); @@ -3399,7 +3324,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); @@ -3555,7 +3479,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); @@ -3633,7 +3556,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); @@ -3885,7 +3807,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); @@ -3943,7 +3864,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); @@ -4050,7 +3970,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); @@ -4161,7 +4080,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); @@ -4454,7 +4372,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); @@ -4561,151 +4478,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); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - 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() { @@ -4769,7 +4541,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)); @@ -4921,7 +4692,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)); @@ -5047,7 +4817,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( @@ -5083,7 +4852,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!( @@ -5113,7 +4881,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!( @@ -5131,79 +4898,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() { @@ -5217,7 +4911,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 { @@ -5325,7 +5018,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]; @@ -5381,7 +5073,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) @@ -5504,7 +5195,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); @@ -5590,7 +5280,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 @@ -5668,7 +5357,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 { @@ -5749,7 +5437,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 @@ -5790,7 +5477,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); @@ -5814,7 +5500,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) @@ -5902,7 +5587,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]; @@ -5941,7 +5625,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. @@ -6006,7 +5689,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); @@ -6122,7 +5804,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); @@ -6348,7 +6029,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)); @@ -6465,7 +6145,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)); @@ -6617,7 +6296,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(); @@ -6732,7 +6410,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 a1c0309b24..b050007376 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::RateLimitingInterface; 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::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance}; +use subtensor_runtime_common::{ + AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, rate_limiting, +}; impl Pallet { pub fn ensure_subnet_owner_or_root( @@ -80,16 +83,11 @@ 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], + txs: &[crate::utils::rate_limiting::TransactionType], ) { if let Some(who) = maybe_owner { let now = Self::get_current_block_as_u64(); @@ -106,27 +104,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, @@ -154,21 +159,26 @@ impl Pallet { pub fn get_trust(_netuid: NetUid) -> Vec { Vec::new() } + 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,9 +194,11 @@ impl Pallet { pub fn get_pruning_score(_netuid: NetUid) -> Vec { Vec::new() } + pub fn get_validator_trust(netuid: NetUid) -> Vec { ValidatorTrust::::get(netuid) } + pub fn get_validator_permit(netuid: NetUid) -> Vec { ValidatorPermit::::get(netuid) } @@ -202,6 +214,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 { @@ -210,6 +223,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 { @@ -218,6 +232,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)); @@ -235,22 +250,27 @@ impl Pallet { 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) @@ -259,14 +279,17 @@ impl Pallet { pub fn get_pruning_score_for_uid(_netuid: NetUid, _uid: u16) -> u16 { 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() } @@ -277,33 +300,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) @@ -360,50 +393,44 @@ 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)); - } - 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)); } + 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)); @@ -416,6 +443,7 @@ impl Pallet { MaxChildkeyTake::::put(take); Self::deposit_event(Event::MaxChildKeyTakeSet(take)); } + pub fn get_min_childkey_take() -> u16 { MinChildkeyTake::::get() } @@ -431,16 +459,15 @@ impl Pallet { } pub fn get_serving_rate_limit(netuid: NetUid) -> u64 { - ServingRateLimit::::get(netuid) - } - 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)); + T::RateLimiting::rate_limit(rate_limiting::GROUP_SERVE, Some(netuid)) + .unwrap_or_default() + .saturated_into() } 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)); @@ -449,6 +476,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)); @@ -457,25 +485,22 @@ 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)); } 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_SET, Some(netuid)) + .unwrap_or_default() + .saturated_into() } 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)); @@ -484,6 +509,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)); @@ -497,6 +523,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)); @@ -510,10 +537,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); @@ -533,6 +562,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)); @@ -541,6 +571,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)); @@ -549,27 +580,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); } @@ -577,6 +615,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)); @@ -586,6 +625,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)); @@ -594,6 +634,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)); @@ -602,6 +643,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, @@ -639,6 +681,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)); @@ -647,6 +690,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( @@ -658,6 +702,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)); @@ -674,6 +719,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)); @@ -682,6 +728,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( @@ -693,13 +740,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)); @@ -708,6 +758,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) } diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index e9559f2c6d..72b7933ab5 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::*; @@ -25,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(), @@ -39,10 +44,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(), Self::AddStakeBurn => Tempo::::get(netuid) as u64, @@ -80,7 +85,10 @@ 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 => { + /*DEPRECATED*/ + 0 + } _ => self.last_block_on_subnet::(key, NetUid::ROOT), } } @@ -89,13 +97,14 @@ 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 => { + /*DEPRECATED*/ + 0 + } 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)) @@ -112,15 +121,10 @@ impl TransactionType { block: u64, ) { match self { - Self::RegisterNetwork => Pallet::::set_network_last_lock_block(block), 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); @@ -213,28 +217,9 @@ 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 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())) } - 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())) } @@ -247,20 +232,4 @@ impl Pallet { block, ); } - pub fn exceeds_tx_rate_limit(prev_tx_block: u64, current_block: u64) -> bool { - let rate_limit: u64 = Self::get_tx_rate_limit(); - if rate_limit == 0 || prev_tx_block == 0 { - 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/Cargo.toml b/pallets/transaction-fee/Cargo.toml index 9e19bf2758..9cc3d13b37 100644 --- a/pallets/transaction-fee/Cargo.toml +++ b/pallets/transaction-fee/Cargo.toml @@ -31,6 +31,7 @@ pallet-scheduler = { workspace = true, default-features = false, optional = true approx.workspace = true 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 @@ -62,6 +63,7 @@ std = [ "pallet-subtensor/std", "pallet-subtensor-swap/std", "pallet-transaction-payment/std", + "rate-limiting-interface/std", "scale-info/std", "sp-core/std", "sp-runtime/std", diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 3607fd3dfa..4faa4d38f7 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -13,6 +13,7 @@ use frame_system::{ self as system, EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase, }; pub use pallet_subtensor::*; +use rate_limiting_interface::{RateLimitingInterface, TryIntoRateLimitTarget}; pub use sp_core::U256; use sp_core::{ConstU64, H256}; use sp_runtime::{ @@ -24,6 +25,7 @@ use sp_std::cmp::Ordering; use sp_weights::Weight; pub use subtensor_runtime_common::{ AlphaBalance, AuthorshipInfo, ConstTao, NetUid, TaoBalance, Token, + rate_limiting::RateLimitUsageKey, }; use subtensor_swap_interface::{Order, SwapHandler}; @@ -185,9 +187,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 pub const InitialBurn: TaoBalance = TaoBalance::new(0); pub const InitialMinBurn: TaoBalance = TaoBalance::new(500_000); @@ -213,7 +212,6 @@ parameter_types! { pub const InitialNetworkMinLockCost: TaoBalance = TaoBalance::new(100_000_000_000_u64); 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: TaoBalance = TaoBalance::new(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 @@ -270,9 +268,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; type InitialBurn = InitialBurn; type InitialMaxBurn = InitialMaxBurn; @@ -284,7 +279,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; @@ -306,6 +300,7 @@ impl pallet_subtensor::Config for Test { type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; + type RateLimiting = NoRateLimiting; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; @@ -446,6 +441,42 @@ impl pallet_subtensor::CommitmentsInterface for CommitmentsI { fn purge_netuid(_netuid: NetUid) {} } +pub struct NoRateLimiting; + +impl RateLimitingInterface 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 + } + + fn set_last_seen( + _target: TargetArg, + _usage_key: Option, + _block: Option, + ) where + TargetArg: TryIntoRateLimitTarget, + { + } +} + parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; @@ -820,10 +851,6 @@ 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: subtensor_runtime_common::NetUid, @@ -843,7 +870,6 @@ pub fn setup_stake( netuid, stake_amount.into(), )); - remove_stake_rate_limit_for_tests(hotkey, coldkey, netuid); } pub(crate) fn quote_remove_stake_after_alpha_fee( diff --git a/pallets/transaction-fee/src/tests/mod.rs b/pallets/transaction-fee/src/tests/mod.rs index 8203070d76..e40c042f9a 100644 --- a/pallets/transaction-fee/src/tests/mod.rs +++ b/pallets/transaction-fee/src/tests/mod.rs @@ -1540,7 +1540,6 @@ fn test_add_stake_fees_go_to_block_builder() { let (_, swap_fee) = mock::swap_tao_to_alpha(sn.subnets[0].netuid, stake_amount.into()); add_balance_to_coldkey_account(&sn.coldkey, (stake_amount * 10).into()); - remove_stake_rate_limit_for_tests(&sn.hotkeys[0], &sn.coldkey, sn.subnets[0].netuid); // Stake let balance_before = Balances::free_balance(sn.coldkey); diff --git a/precompiles/Cargo.toml b/precompiles/Cargo.toml index c896ecb731..77697c400e 100644 --- a/precompiles/Cargo.toml +++ b/precompiles/Cargo.toml @@ -37,6 +37,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 pallet-shield.workspace = true @@ -67,6 +68,7 @@ std = [ "pallet-evm/std", "pallet-preimage/std", "pallet-scheduler/std", + "pallet-rate-limiting/std", "pallet-subtensor-proxy/std", "pallet-subtensor-swap/std", "pallet-subtensor/std", @@ -99,6 +101,7 @@ runtime-benchmarks = [ "pallet-timestamp/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "subtensor-runtime-common/runtime-benchmarks", + "pallet-rate-limiting/runtime-benchmarks", ] [dev-dependencies] diff --git a/precompiles/src/address_mapping.rs b/precompiles/src/address_mapping.rs index c8f3815c49..add910ed15 100644 --- a/precompiles/src/address_mapping.rs +++ b/precompiles/src/address_mapping.rs @@ -23,9 +23,7 @@ where + pallet_evm::Config + pallet_proxy::Config + pallet_subtensor::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + ByteArray + Into<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> @@ -50,9 +48,7 @@ where + pallet_evm::Config + pallet_proxy::Config + pallet_subtensor::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + ByteArray + Into<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> diff --git a/precompiles/src/balance_transfer.rs b/precompiles/src/balance_transfer.rs index d8d10970a3..982c3a7df9 100644 --- a/precompiles/src/balance_transfer.rs +++ b/precompiles/src/balance_transfer.rs @@ -17,12 +17,11 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: GetDispatchInfo @@ -46,12 +45,11 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: GetDispatchInfo diff --git a/precompiles/src/crowdloan.rs b/precompiles/src/crowdloan.rs index d32dd4ab35..5d65db6072 100644 --- a/precompiles/src/crowdloan.rs +++ b/precompiles/src/crowdloan.rs @@ -24,12 +24,11 @@ where + pallet_crowdloan::Config + pallet_evm::Config + pallet_proxy::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + ByteArray, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> @@ -52,12 +51,11 @@ where + pallet_crowdloan::Config + pallet_evm::Config + pallet_proxy::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + ByteArray, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> diff --git a/precompiles/src/extensions.rs b/precompiles/src/extensions.rs index 4a7c418c86..a2a334013a 100644 --- a/precompiles/src/extensions.rs +++ b/precompiles/src/extensions.rs @@ -10,7 +10,6 @@ use pallet_evm::{ AddressMapping, BalanceConverter, EvmBalance, ExitError, GasWeightMapping, Precompile, PrecompileFailure, PrecompileHandle, PrecompileResult, }; -use pallet_subtensor::SubtensorTransactionExtension; use precompile_utils::EvmResult; use scale_info::TypeInfo; use sp_core::{H160, U256, blake2_256}; @@ -25,6 +24,18 @@ use sp_runtime::{ use sp_std::vec::Vec; use subtensor_runtime_common::with_evm_context; +type RuntimeCallOf = ::RuntimeCall; + +pub trait PrecompileTxExtensionProvider: frame_system::Config +where + RuntimeCallOf: Dispatchable, +{ + /// Runtime-provided transaction extensions used for precompile-dispatched runtime calls + type Extensions: TransactionExtension>; + + fn tx_extensions() -> Self::Extensions; +} + pub(crate) trait PrecompileHandleExt: PrecompileHandle { fn caller_account_id(&self) -> R::AccountId where @@ -58,14 +69,16 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config> + pallet_subtensor::Config + pallet_shield::Config + pallet_subtensor_proxy::Config + + PrecompileTxExtensionProvider + Send + Sync + TypeInfo, - ::RuntimeCall: From, - ::RuntimeCall: GetDispatchInfo + RuntimeCallOf: From, + RuntimeCallOf: GetDispatchInfo + Dispatchable + IsSubType> + IsSubType> @@ -74,12 +87,13 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { ::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 = ::tx_extensions(); 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 { @@ -99,18 +113,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)?; @@ -118,9 +133,9 @@ 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) + <::Extensions as TransactionExtension< + RuntimeCallOf, + >>::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)?; @@ -132,9 +147,9 @@ 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) + <::Extensions as TransactionExtension< + RuntimeCallOf, + >>::post_dispatch(pre, &info, &mut post_info, 0, &result) .map_err(extension_error)?; log::info!("Precompile dispatch failed. message as: {e:?}"); self.charge_and_refund_after_dispatch::(&info, &post_info)?; diff --git a/precompiles/src/leasing.rs b/precompiles/src/leasing.rs index 005782c776..a850d9c9c0 100644 --- a/precompiles/src/leasing.rs +++ b/precompiles/src/leasing.rs @@ -24,13 +24,12 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_crowdloan::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + ByteArray, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> @@ -52,13 +51,12 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_crowdloan::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + ByteArray, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index 39815a6946..083d63a983 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -61,6 +61,24 @@ mod subnet; mod uid_lookup; mod voting_power; +pub use crate::extensions::PrecompileTxExtensionProvider; + +pub trait PrecompileRuntime: + Send + Sync + scale_info::TypeInfo + PrecompileTxExtensionProvider +where + ::RuntimeCall: + Dispatchable, +{ +} + +impl PrecompileRuntime for T +where + T: Send + Sync + scale_info::TypeInfo + PrecompileTxExtensionProvider + frame_system::Config, + ::RuntimeCall: + Dispatchable, +{ +} + #[cfg(test)] mod mock; @@ -76,17 +94,20 @@ where + pallet_subtensor_swap::Config + pallet_proxy::Config + pallet_crowdloan::Config - + pallet_shield::Config + + pallet_rate_limiting::Config< + LimitScope = subtensor_runtime_common::NetUid, + GroupId = subtensor_runtime_common::rate_limiting::GroupId, + RuntimeCall = ::RuntimeCall, + > + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + ByteArray + Into<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> + From> + From> + From> + + From> + From> + GetDispatchInfo + Dispatchable @@ -113,17 +134,20 @@ where + pallet_subtensor_swap::Config + pallet_proxy::Config + pallet_crowdloan::Config - + pallet_shield::Config + + pallet_rate_limiting::Config< + LimitScope = subtensor_runtime_common::NetUid, + GroupId = subtensor_runtime_common::rate_limiting::GroupId, + RuntimeCall = ::RuntimeCall, + > + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + ByteArray + Into<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> + From> + From> + From> + + From> + From> + GetDispatchInfo + Dispatchable @@ -181,17 +205,20 @@ where + pallet_subtensor_swap::Config + pallet_proxy::Config + pallet_crowdloan::Config - + pallet_shield::Config + + pallet_rate_limiting::Config< + LimitScope = subtensor_runtime_common::NetUid, + GroupId = subtensor_runtime_common::rate_limiting::GroupId, + RuntimeCall = ::RuntimeCall, + > + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + ByteArray + Into<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> + From> + From> + From> + + From> + From> + GetDispatchInfo + Dispatchable diff --git a/precompiles/src/mock.rs b/precompiles/src/mock.rs index d82422bf51..2f502a566e 100644 --- a/precompiles/src/mock.rs +++ b/precompiles/src/mock.rs @@ -6,8 +6,10 @@ use core::{marker::PhantomData, num::NonZeroU64}; use fp_evm::{Context, PrecompileResult}; use frame_support::{ - PalletId, derive_impl, parameter_types, - traits::{Everything, InherentBuilder, PrivilegeCmp}, + PalletId, derive_impl, + dispatch::DispatchResult, + parameter_types, + traits::{EnsureOrigin, Everything, InherentBuilder, PrivilegeCmp}, weights::Weight, }; use frame_system::{EnsureRoot, limits, offchain::CreateTransactionBase}; @@ -22,10 +24,15 @@ use sp_runtime::{ testing::TestXt, traits::{BlakeTwo256, ConstU32, IdentityLookup}, }; +use sp_std::collections::btree_set::BTreeSet; use substrate_fixed::types::U96F32; -use subtensor_runtime_common::{AuthorshipInfo, NetUid, ProxyType, TaoBalance}; +use subtensor_runtime_common::{ + AuthorshipInfo, NetUid, ProxyType, TaoBalance, + rate_limiting::{GroupId as RateLimitGroupId, RateLimitUsageKey}, +}; use crate::PrecompileExt; +use crate::extensions::PrecompileTxExtensionProvider; pub(crate) type AccountId = AccountId32; pub(crate) type Block = frame_system::mocking::MockBlock; @@ -48,6 +55,7 @@ frame_support::construct_runtime!( Evm: pallet_evm = 12, AdminUtils: pallet_admin_utils = 13, EVMChainId: pallet_evm_chain_id = 14, + RateLimiting: pallet_rate_limiting = 16, } ); @@ -107,9 +115,6 @@ parameter_types! { pub const InitialMinChildKeyTake: u16 = 0; pub const InitialMaxChildKeyTake: u16 = 11_796; pub const InitialWeightsVersionKey: u64 = 0; - pub const InitialServingRateLimit: u64 = 0; - pub const InitialTxRateLimit: u64 = 0; - pub const InitialTxDelegateTakeRateLimit: u64 = 0; pub const InitialTxChildKeyTakeRateLimit: u64 = 0; pub const InitialBurn: TaoBalance = TaoBalance::new(0); pub const InitialMinBurn: TaoBalance = TaoBalance::new(500_000); @@ -134,7 +139,6 @@ parameter_types! { pub const InitialNetworkMinLockCost: TaoBalance = TaoBalance::new(100_000_000_000); pub const InitialSubnetOwnerCut: u16 = 0; pub const InitialNetworkLockReductionInterval: u64 = 2; - pub const InitialNetworkRateLimit: u64 = 0; pub const InitialKeySwapCost: TaoBalance = TaoBalance::new(1_000_000_000); pub const InitialAlphaHigh: u16 = 58_982; pub const InitialAlphaLow: u16 = 45_875; @@ -450,9 +454,6 @@ impl pallet_subtensor::Config for Runtime { type InitialWeightsVersionKey = InitialWeightsVersionKey; type InitialMaxDifficulty = InitialMaxDifficulty; type InitialMinDifficulty = InitialMinDifficulty; - type InitialServingRateLimit = InitialServingRateLimit; - type InitialTxRateLimit = InitialTxRateLimit; - type InitialTxDelegateTakeRateLimit = InitialTxDelegateTakeRateLimit; type InitialTxChildKeyTakeRateLimit = InitialTxChildKeyTakeRateLimit; type InitialBurn = InitialBurn; type InitialMaxBurn = InitialMaxBurn; @@ -464,7 +465,7 @@ impl pallet_subtensor::Config for Runtime { type InitialNetworkMinLockCost = InitialNetworkMinLockCost; type InitialSubnetOwnerCut = InitialSubnetOwnerCut; type InitialNetworkLockReductionInterval = InitialNetworkLockReductionInterval; - type InitialNetworkRateLimit = InitialNetworkRateLimit; + type RateLimiting = RateLimiting; type KeySwapCost = InitialKeySwapCost; type AlphaHigh = InitialAlphaHigh; type AlphaLow = InitialAlphaLow; @@ -522,6 +523,73 @@ impl pallet_subtensor_proxy::Config for Runtime { type BlockNumberProvider = System; } +parameter_types! { + pub const DefaultLimitSettingRule: () = (); + pub const RateLimitingMaxGroupMembers: u32 = 64; + pub const RateLimitingMaxGroupNameLength: u32 = 64; +} + +pub struct LimitSettingOrigin; +impl pallet_rate_limiting::EnsureLimitSettingRule + for LimitSettingOrigin +{ + fn ensure_origin(origin: RuntimeOrigin, _rule: &(), _scope: &Option) -> DispatchResult { + EnsureRoot::::ensure_origin(origin) + .map(|_| ()) + .map_err(|_| sp_runtime::DispatchError::BadOrigin) + } +} + +pub struct MockScopeResolver; +impl pallet_rate_limiting::RateLimitScopeResolver + for MockScopeResolver +{ + fn context(_origin: &RuntimeOrigin, _call: &RuntimeCall) -> Option> { + None + } +} + +pub struct MockUsageResolver; +impl + pallet_rate_limiting::RateLimitUsageResolver< + RuntimeOrigin, + RuntimeCall, + RateLimitUsageKey, + > for MockUsageResolver +{ + fn context( + _origin: &RuntimeOrigin, + _call: &RuntimeCall, + ) -> Option>> { + None + } +} + +impl pallet_rate_limiting::Config for Runtime { + type RuntimeCall = RuntimeCall; + type AdminOrigin = EnsureRoot; + type LimitSettingRule = (); + type DefaultLimitSettingRule = DefaultLimitSettingRule; + type LimitSettingOrigin = LimitSettingOrigin; + type LimitScope = NetUid; + type LimitScopeResolver = MockScopeResolver; + type UsageKey = RateLimitUsageKey; + type UsageResolver = MockUsageResolver; + type GroupId = RateLimitGroupId; + type MaxGroupMembers = RateLimitingMaxGroupMembers; + type MaxGroupNameLength = RateLimitingMaxGroupNameLength; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +impl PrecompileTxExtensionProvider for Runtime { + type Extensions = pallet_rate_limiting::RateLimitTransactionExtension; + + fn tx_extensions() -> Self::Extensions { + pallet_rate_limiting::RateLimitTransactionExtension::::new() + } +} + pub(crate) struct SinglePrecompileSet

(PhantomData

); impl

Default for SinglePrecompileSet

{ diff --git a/precompiles/src/neuron.rs b/precompiles/src/neuron.rs index 1397baf272..b5a647c421 100644 --- a/precompiles/src/neuron.rs +++ b/precompiles/src/neuron.rs @@ -18,12 +18,11 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> @@ -44,12 +43,11 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> @@ -302,7 +300,6 @@ mod tests { pallet_subtensor::Pallet::::set_network_registration_allowed(netuid, true); pallet_subtensor::Pallet::::set_burn(netuid, REGISTRATION_BURN.into()); pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, 4096); - pallet_subtensor::Pallet::::set_weights_set_rate_limit(netuid, 0); pallet_subtensor::Pallet::::set_tempo(netuid, TEMPO); pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, true); pallet_subtensor::Pallet::::set_reveal_period(netuid, REVEAL_PERIOD) diff --git a/precompiles/src/proxy.rs b/precompiles/src/proxy.rs index 3312b67194..f69f8abf65 100644 --- a/precompiles/src/proxy.rs +++ b/precompiles/src/proxy.rs @@ -28,13 +28,12 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_proxy::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + Into<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::AddressMapping: AddressMapping, @@ -58,13 +57,12 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_proxy::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + Into<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::AddressMapping: AddressMapping, diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index 28e043f07b..31bee44d33 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -87,13 +87,12 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_proxy::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + Into<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> @@ -116,13 +115,12 @@ where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_proxy::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]> + Into<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> @@ -675,14 +673,13 @@ 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 + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> @@ -706,14 +703,13 @@ impl StakingPrecompile where R: frame_system::Config + pallet_evm::Config + + pallet_rate_limiting::Config::RuntimeCall> + pallet_subtensor::Config + pallet_proxy::Config + pallet_balances::Config + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> @@ -1070,11 +1066,6 @@ mod tests { fund_account(&source_account, COLDKEY_BALANCE); add_stake_v2(source, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - source_account.clone(), - netuid, - )); ( netuid, @@ -1269,11 +1260,6 @@ mod tests { fund_account(&caller_account, COLDKEY_BALANCE); add_stake_v1(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - caller_account.clone(), - netuid, - )); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompile::::INDEX); @@ -1309,11 +1295,6 @@ mod tests { fund_account(&caller_account, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - caller_account.clone(), - netuid, - )); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); @@ -1402,11 +1383,6 @@ mod tests { ), ) .execute_returns(()); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - caller_account.clone(), - netuid, - )); let stake_before = stake_for(&hotkey, &caller_account, netuid); precompiles @@ -1458,11 +1434,6 @@ mod tests { ), ) .execute_returns(()); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - caller_account.clone(), - netuid, - )); assert!(stake_for(&hotkey, &caller_account, netuid) > 0); precompiles @@ -1512,11 +1483,6 @@ mod tests { ), ) .execute_returns(()); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - caller_account.clone(), - netuid, - )); assert!(stake_for(&hotkey, &caller_account, netuid) > 0); precompiles diff --git a/precompiles/src/subnet.rs b/precompiles/src/subnet.rs index b89d972eea..edcd6f4db7 100644 --- a/precompiles/src/subnet.rs +++ b/precompiles/src/subnet.rs @@ -5,11 +5,12 @@ use frame_support::traits::ConstU32; use frame_support::traits::IsSubType; 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::{AsSystemOriginSigner, Dispatchable}; +use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable, SaturatedConversion}; use sp_std::vec; -use subtensor_runtime_common::{NetUid, Token}; +use subtensor_runtime_common::{NetUid, Token, rate_limiting}; use crate::{PrecompileExt, PrecompileHandleExt}; @@ -22,15 +23,18 @@ where + pallet_evm::Config + pallet_subtensor::Config + pallet_admin_utils::Config - + pallet_shield::Config + + pallet_rate_limiting::Config< + LimitScope = NetUid, + GroupId = subtensor_runtime_common::rate_limiting::GroupId, + RuntimeCall = ::RuntimeCall, + > + pallet_shield::Config + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> + From> + + From> + GetDispatchInfo + Dispatchable + IsSubType> @@ -51,14 +55,17 @@ where + pallet_subtensor::Config + pallet_shield::Config + pallet_admin_utils::Config - + pallet_subtensor_proxy::Config - + Send - + Sync - + scale_info::TypeInfo, + + pallet_rate_limiting::Config< + LimitScope = NetUid, + GroupId = subtensor_runtime_common::rate_limiting::GroupId, + RuntimeCall = ::RuntimeCall, + > + pallet_subtensor_proxy::Config + + crate::PrecompileRuntime, R::AccountId: From<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> + From> + + From> + GetDispatchInfo + Dispatchable + IsSubType> @@ -164,9 +171,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)")] @@ -176,9 +183,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::( @@ -268,9 +276,11 @@ 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_SET); + 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)")] @@ -814,7 +824,6 @@ mod tests { pallet_subtensor::SubnetOwner::::insert(netuid, owner); pallet_subtensor::SubnetOwnerHotkey::::insert(netuid, owner_hotkey); pallet_subtensor::AdminFreezeWindow::::set(0); - pallet_subtensor::OwnerHyperparamRateLimit::::set(0); netuid } @@ -911,31 +920,6 @@ mod tests { let precompiles = precompiles::>(); let precompile_addr = addr_from_index(SubnetPrecompile::::INDEX); - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("setServingRateLimit(uint16,uint64)"), - (TEST_NETUID_U16, 100_u64), - ), - ) - .execute_returns(()); - assert_eq!( - pallet_subtensor::ServingRateLimit::::get(netuid), - 100 - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("getServingRateLimit(uint16)"), - (TEST_NETUID_U16,), - ), - U256::from(100_u64), - ); - precompiles .prepare_test( caller, diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 48269f5eb5..d180262441 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -40,9 +40,12 @@ 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-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 @@ -54,6 +57,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 @@ -159,7 +163,6 @@ ethereum.workspace = true [dev-dependencies] frame-metadata.workspace = true -sp-io.workspace = true sp-tracing.workspace = true precompile-utils = { workspace = true, features = ["testing"] } @@ -195,6 +198,8 @@ std = [ "pallet-timestamp/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", @@ -218,6 +223,7 @@ std = [ "pallet-admin-utils/std", "subtensor-custom-rpc-runtime-api/std", "subtensor-transaction-fee/std", + "serde/std", "serde_json/std", "sp-io/std", "sp-tracing/std", @@ -322,6 +328,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", "subtensor-precompiles/runtime-benchmarks", @@ -344,6 +351,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 bd0c55f9d4..84aacd1483 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -8,11 +8,13 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +use core::marker::PhantomData; use core::num::NonZeroU64; pub mod check_mortality; pub mod check_nonce; mod migrations; +pub mod rate_limiting; pub mod sudo_wrapper; pub mod transaction_payment_wrapper; @@ -47,6 +49,7 @@ use pallet_subtensor_swap_runtime_api::{SimSwapResult, SubnetPrice}; use pallet_subtensor_utility as pallet_utility; use runtime_common::prod_or_fast; use safe_math::FixedExt; +use scale_info::TypeInfo; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_babe::BabeConfiguration; @@ -74,9 +77,18 @@ use sp_version::NativeVersion; use sp_version::RuntimeVersion; use stp_shield::ShieldedTransaction; use substrate_fixed::types::{U64F64, U96F32}; -use subtensor_precompiles::Precompiles; +use subtensor_precompiles::{PrecompileTxExtensionProvider, Precompiles}; use subtensor_runtime_common::{AlphaBalance, AuthorshipInfo, TaoBalance, time::*, *}; use subtensor_swap_interface::{Order, SwapHandler}; +use subtensor_transaction_fee::{ + SubtensorEvmFeeHandler, 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, +}; // A few exports that help ease life for downstream crates. pub use frame_support::{ @@ -98,23 +110,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::{ + 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::{ - SubtensorEvmFeeHandler, 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, -}; +pub use subtensor_runtime_common::rate_limiting::RateLimitUsageKey; // Drand impl pallet_drand::Config for Runtime { @@ -210,6 +212,7 @@ impl frame_system::offchain::CreateSignedTransaction pallet_shield::CheckShieldedTxValidity::::new(), pallet_subtensor::SubtensorTransactionExtension::::new(), pallet_drand::drand_priority::DrandPriority::::new(), + rate_limiting::UnwrappedRateLimitTransactionExtension::new(), ), frame_metadata_hash_extension::CheckMetadataHash::::new(true), ); @@ -1102,7 +1105,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: TaoBalance = TaoBalance::new(100_000_000); // 0.1 tao pub const SubtensorInitialMinBurn: TaoBalance = TaoBalance::new(500_000); // 500k RAO pub const SubtensorInitialMaxBurn: TaoBalance = TaoBalance::new(100_000_000_000); // 100 tao @@ -1175,14 +1177,11 @@ 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; type MinBurnUpperBound = MinBurnUpperBound; type MaxBurnLowerBound = MaxBurnLowerBound; - type InitialTxRateLimit = SubtensorInitialTxRateLimit; - type InitialTxDelegateTakeRateLimit = SubtensorInitialTxDelegateTakeRateLimit; type InitialTxChildKeyTakeRateLimit = SubtensorInitialTxChildKeyTakeRateLimit; type InitialMaxChildKeyTake = SubtensorInitialMaxChildKeyTake; type InitialRAORecycledForRegistration = SubtensorInitialRAORecycledForRegistration; @@ -1190,7 +1189,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; @@ -1211,6 +1209,7 @@ impl pallet_subtensor::Config for Runtime { type GetCommitments = GetCommitmentsStruct; type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; + type RateLimiting = RateLimiting; type AlphaAssets = AlphaAssets; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = BlockAuthorFromAura; @@ -1219,6 +1218,28 @@ impl pallet_subtensor::Config for Runtime { type WeightInfo = pallet_subtensor::weights::SubstrateWeight; } +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 LimitSettingRule = rate_limiting::LimitSettingRule; + type DefaultLimitSettingRule = rate_limiting::DefaultLimitSettingRule; + type LimitSettingOrigin = rate_limiting::LimitSettingOrigin; + type LimitScope = NetUid; + type LimitScopeResolver = RuntimeScopeResolver; + type UsageKey = RateLimitUsageKey; + type UsageResolver = RuntimeUsageResolver; + type GroupId = subtensor_runtime_common::rate_limiting::GroupId; + 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% @@ -1695,6 +1716,7 @@ construct_runtime!( Contracts: pallet_contracts = 29, MevShield: pallet_shield = 30, AlphaAssets: pallet_alpha_assets = 31, + RateLimiting: pallet_rate_limiting = 32, } ); @@ -1720,6 +1742,7 @@ pub type CustomTxExtension = ( pallet_shield::CheckShieldedTxValidity, pallet_subtensor::SubtensorTransactionExtension, pallet_drand::drand_priority::DrandPriority, + rate_limiting::UnwrappedRateLimitTransactionExtension, ); pub type TxExtension = ( SystemTxExtension, @@ -1727,12 +1750,31 @@ pub type TxExtension = ( frame_metadata_hash_extension::CheckMetadataHash, ); +impl PrecompileTxExtensionProvider for Runtime { + type Extensions = ( + pallet_subtensor::SubtensorTransactionExtension, + rate_limiting::UnwrappedRateLimitTransactionExtension, + ); + + fn tx_extensions() -> Self::Extensions { + ( + pallet_subtensor::SubtensorTransactionExtension::::new(), + rate_limiting::UnwrappedRateLimitTransactionExtension::new(), + ) + } +} + 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, >, + migrations::rate_limiting::GroupedRateLimitingMigration, + // TODO(rate-limiting): enable standalone migration once legacy standalone limits are removed. + // migrations::rate_limiting::StandaloneRateLimitingMigration, + migrations::subtensor_module::Migration, ); // Unchecked extrinsic type as expected by this runtime. @@ -2287,6 +2329,44 @@ 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::{ + RateLimitConfigRpcResponse, RateLimitRpcResponse, + }; + + let pallet_name = sp_std::str::from_utf8(&pallet).ok()?; + let extrinsic_name = sp_std::str::from_utf8(&extrinsic).ok()?; + + let identifier = pallet_rate_limiting::TransactionIdentifier::for_call_names::< + ::RuntimeCall, + >( + pallet_name, + extrinsic_name, + )?; + let group_id = pallet_rate_limiting::CallGroups::::get(identifier); + let target = RateLimiting::::config_target(&identifier).ok()?; + let limit = match pallet_rate_limiting::Limits::::get(target)? { + RateLimit::Global(kind) => RateLimitConfigRpcResponse::Global(kind), + RateLimit::Scoped(entries) => RateLimitConfigRpcResponse::Scoped( + entries + .into_iter() + .map(|(scope, kind)| (scope.encode(), kind)) + .collect(), + ), + }; + + Some(match group_id { + Some(group_id) => RateLimitRpcResponse::Grouped { group_id, limit }, + None => RateLimitRpcResponse::Standalone { limit }, + }) + } + } + impl pallet_contracts::ContractsApi for 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/migrations/rate_limiting.rs b/runtime/src/migrations/rate_limiting.rs new file mode 100644 index 0000000000..7f12e7de5f --- /dev/null +++ b/runtime/src/migrations/rate_limiting.rs @@ -0,0 +1,1858 @@ +// we have the standalone calls migration that will be enforced in the following PR +#![allow(dead_code)] + +use core::{convert::TryFrom, marker::PhantomData}; + +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, LastUpdate, + Pallet, Prometheus, +}; +use sp_runtime::traits::SaturatedConversion; +use sp_std::{ + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + vec, + vec::Vec, +}; +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_SET, GroupId, RateLimitUsageKey, + ServingEndpoint, + }, +}; + +use crate::{ + AccountId, Runtime, + rate_limiting::{ + LimitSettingRule, + legacy::{ + Hyperparameter, RateLimitKey, TransactionType, defaults as legacy_defaults, + storage as legacy_storage, + }, + }, +}; + +type GroupNameOf = BoundedVec::MaxGroupNameLength>; +type GroupMembersOf = + BoundedBTreeSet::MaxGroupMembers>; + +// 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 `pallet_subtensor::HasMigrationRun` once the grouped migration finishes. +pub const MIGRATION_NAME_GROUPED: &[u8] = b"migrate_grouped_rate_limiting"; +/// Marker stored in `pallet_subtensor::HasMigrationRun` for the standalone migration. +pub const MIGRATION_NAME_STANDALONE: &[u8] = b"migrate_standalone_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; + +// 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::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, + Hyperparameter::BurnHalfLife, + Hyperparameter::BurnIncreaseMult, +]; + +/// Runtime hook that executes the grouped rate-limiting migration. +pub struct GroupedRateLimitingMigration(PhantomData); + +impl frame_support::traits::OnRuntimeUpgrade for GroupedRateLimitingMigration +where + T: SubtensorConfig + pallet_rate_limiting::Config, + RateLimitUsageKey: Into<::UsageKey>, +{ + fn on_runtime_upgrade() -> Weight { + let mut weight = ::DbWeight::get().reads(1); + if HasMigrationRun::::get(MIGRATION_NAME_GROUPED) { + info!("Grouped rate-limiting migration already executed. Skipping."); + return weight; + } + + let (groups, commits, reads) = commits_grouped(); + weight = + weight.saturating_add(::DbWeight::get().reads(reads)); + + 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) + }, + ); + + 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); + + let mut writes = group_writes + .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_GROUPED, true); + writes += 1; + + weight = weight + .saturating_add(::DbWeight::get().writes(writes)); + + info!( + "New migration wrote {} limits, {} last-seen entries, and {} groups into pallet-rate-limiting", + limits_len, last_seen_len, group_count + ); + + weight + } +} + +/// Runtime hook that executes the standalone rate-limiting migration. +/// +/// This is intentionally not wired into the runtime yet; it will be enabled in a follow-up PR. +pub struct StandaloneRateLimitingMigration(PhantomData); + +impl frame_support::traits::OnRuntimeUpgrade for StandaloneRateLimitingMigration +where + T: SubtensorConfig + pallet_rate_limiting::Config, + RateLimitUsageKey: Into<::UsageKey>, +{ + fn on_runtime_upgrade() -> Weight { + let mut weight = ::DbWeight::get().reads(1); + if HasMigrationRun::::get(MIGRATION_NAME_STANDALONE) { + info!("Standalone rate-limiting migration already executed. Skipping."); + return weight; + } + + let (commits, reads) = commits_standalone(); + weight = + weight.saturating_add(::DbWeight::get().reads(reads)); + + 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) + }, + ); + + let (limit_writes, limits_len) = migrate_limits(limit_commits); + let (last_seen_writes, last_seen_len) = migrate_last_seen(last_seen_commits); + + let mut writes = limit_writes.saturating_add(last_seen_writes); + HasMigrationRun::::insert(MIGRATION_NAME_STANDALONE, true); + writes += 1; + + weight = weight + .saturating_add(::DbWeight::get().writes(writes)); + + info!( + "Standalone migration wrote {} limits and {} last-seen entries into pallet-rate-limiting", + limits_len, last_seen_len + ); + + weight + } +} + +// The commits functions are main entrypoints. + +// build all groups and commits, along with storage reads. +fn commits_grouped() -> (Vec, Vec, u64) { + let mut groups = Vec::new(); + let mut commits = Vec::new(); + + // grouped + 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)); + + (groups, commits, reads) +} + +// build commits and storage reads for standalone calls. +fn commits_standalone() -> (Vec, u64) { + let mut commits = Vec::new(); + let mut reads: u64 = 0; + + 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)); + + (commits, reads) +} + +fn migrate_grouping(groups: &[GroupConfig]) -> (u64, usize) { + let mut writes: u64 = 0; + let mut max_group_id: Option = None; + + 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; + }; + + 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; + + 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; + + max_group_id = Some(max_group_id.map_or(group.id, |current| current.max(group.id))); + } + + 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; + + (writes, groups.len()) +} + +fn migrate_limits(limit_commits: Vec<(RateLimitTarget, MigratedLimit)>) -> (u64, usize) { + let mut writes: u64 = 0; + let mut limits: BTreeMap, RateLimit>> = + BTreeMap::new(); + + 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(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)); + } + } + + let len = limits.len(); + for (target, limit) in limits { + pallet_rate_limiting::Limits::::insert(target, limit); + writes += 1; + } + + (writes, len) +} + +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); + } + + let len = last_seen.len(); + for ((target, usage), block) in last_seen { + pallet_rate_limiting::LastSeen::::insert(target, usage, block); + writes += 1; + } + + (writes, len) +} + +// 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; + // 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 + ], + }); + + let (serving_limits, serving_reads) = legacy_storage::serving_rate_limits(); + reads = reads.saturating_add(serving_reads); + // Limits per netuid (written to the group target). + // 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 serving_limits.keys() { + if !netuids.contains(&netuid) { + netuids.push(netuid); + } + } + let default_limit = legacy_defaults::serving_rate_limit(); + for netuid in netuids { + reads = reads.saturating_add(1); + push_limit_commit_if_non_zero( + commits, + RateLimitTarget::Group(GROUP_SERVE), + 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 = reads.saturating_add(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: ServingEndpoint::Axon, + }), + }), + }); + } + } + + // Prometheus last-seen (group-shared usage). + for (netuid, hotkey, prom) in Prometheus::::iter() { + reads = reads.saturating_add(1); + 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: ServingEndpoint::Prometheus, + }), + }), + }); + } + } + + reads +} + +// 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); + 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.saturating_add( + last_seen_helpers::collect_last_seen_from_last_rate_limited_block( + commits, + |key| match key { + RateLimitKey::LastTxBlockDelegateTake(account) => { + Some((target, Some(RateLimitUsageKey::Account(account)))) + } + _ => None, + }, + ), + ); + + reads +} + +// Weights group (config + usage shared). +// scope: netuid +// usage: netuid+mechanism+neuron +// legacy source: WeightsSetRateLimit, LastUpdate (subnet/mechanism) +fn build_weights(groups: &mut Vec, commits: &mut Vec) -> u64 { + let mut reads: u64 = 0; + groups.push(GroupConfig { + id: GROUP_WEIGHTS_SET, + name: b"weights".to_vec(), + 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 + 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 + ], + }); + + 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 = reads.saturating_add(1); + push_limit_commit_if_non_zero( + commits, + RateLimitTarget::Group(GROUP_WEIGHTS_SET), + weights_limits + .get(&netuid) + .copied() + .unwrap_or(default_limit), + Some(netuid), + ); + } + + 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() { + let Some(block) = block_number::(last_block) else { + continue; + }; + let Ok(uid_u16) = u16::try_from(uid) else { + continue; + }; + let usage = RateLimitUsageKey::SubnetMechanismNeuron { + netuid, + mecid, + uid: uid_u16, + }; + commits.push(Commit { + target: RateLimitTarget::Group(GROUP_WEIGHTS_SET), + kind: CommitKind::LastSeen(MigratedLastSeen { + block, + usage: Some(usage), + }), + }); + } + } + + reads +} + +// 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; + 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); + 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.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 +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); + 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.saturating_add( + last_seen_helpers::collect_last_seen_from_last_rate_limited_block( + commits, + |key| match key { + RateLimitKey::OwnerHyperparamUpdate(netuid, hyper) => { + let identifier = identifier_for_hyperparameter(hyper)?; + Some(( + RateLimitTarget::Transaction(identifier.identifier()), + Some(RateLimitUsageKey::Subnet(netuid)), + )) + } + _ => None, + }, + ), + ); + + reads +} + +// Staking ops group (config + usage shared, all ops 1 block). +// usage: coldkey+hotkey+netuid +// 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, + 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 +} + +// Swap hotkey/coldkey share the lock and usage; swap_coldkey bypasses enforcement but records +// 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 { + 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); + 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 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 + } 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( + commits, + |key| match key { + RateLimitKey::LastTxBlock(account) => { + Some((target, Some(RateLimitUsageKey::Account(account)))) + } + _ => None, + }, + ), + ); + + reads +} + +// 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)); + 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 = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::SetChildkeyTake, + ), + ); + + reads +} + +// 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 = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::SetChildren, + ), + ); + + reads +} + +// 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)); + 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 = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::SetWeightsVersionKey, + ), + ); + + reads +} + +// 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)); + let sn_owner_limit = legacy_defaults::sn_owner_hotkey_rate_limit(); + reads += 1; + push_limit_commit_if_non_zero(commits, target, sn_owner_limit, None); + + reads = reads.saturating_add( + 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, + }, + ), + ); + + reads +} + +// 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 = reads.saturating_add(1); + let Some(block) = block_number::(block) else { + continue; + }; + commits.push(Commit { + target, + kind: CommitKind::LastSeen(MigratedLastSeen { + block, + usage: Some(RateLimitUsageKey::SubnetNeuron { netuid, uid }), + }), + }); + } + + reads +} + +// 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)); + let mechanism_limit = legacy_defaults::mechanism_count_rate_limit(); + push_limit_commit_if_non_zero(commits, target, mechanism_limit, None); + + reads = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::MechanismCountUpdate, + ), + ); + + reads +} + +// 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)); + let emission_limit = legacy_defaults::mechanism_emission_rate_limit(); + push_limit_commit_if_non_zero(commits, target, emission_limit, None); + + reads = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::MechanismEmission, + ), + ); + + reads +} + +// 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)); + let trim_limit = legacy_defaults::max_uids_trimming_rate_limit(); + push_limit_commit_if_non_zero(commits, target, trim_limit, None); + + reads = reads.saturating_add( + last_seen_helpers::collect_last_seen_from_transaction_key_last_block( + commits, + target, + TransactionType::MaxUidsTrimming, + ), + ); + + reads +} + +struct Commit { + target: RateLimitTarget, + kind: CommitKind, +} + +enum CommitKind { + Limit(MigratedLimit), + LastSeen(MigratedLastSeen), +} + +struct MigratedLimit { + span: BlockNumberFor, + scope: Option, +} + +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, + } + } + + const fn subtensor(call_index: u8, read_only: bool) -> Self { + Self::new(SUBTENSOR_PALLET_INDEX, call_index, read_only) + } + + const fn admin(call_index: u8, read_only: bool) -> Self { + Self::new(ADMIN_UTILS_PALLET_INDEX, call_index, read_only) + } + + pub fn identifier(&self) -> TransactionIdentifier { + self.identifier + } +} + +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 }), + }); + } +} + +mod last_seen_helpers { + use core::mem::discriminant; + + use super::*; + + 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; + + 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; + }; + let Some(block) = block_number::(block) else { + continue; + }; + commits.push(Commit { + target, + kind: CommitKind::LastSeen(MigratedLastSeen { block, usage }), + }); + } + + reads + } + + 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; + + 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; + } + 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`. +fn usage_key_from_transaction_type( + tx: TransactionType, + account: &AccountId, + netuid: NetUid, +) -> Option> { + match tx { + TransactionType::MechanismCountUpdate + | TransactionType::MaxUidsTrimming + | TransactionType::MechanismEmission + | TransactionType::SetChildkeyTake + | TransactionType::SetChildren + | TransactionType::SetWeightsVersionKey => Some(RateLimitUsageKey::AccountSubnet { + account: account.clone(), + netuid, + }), + TransactionType::SetSNOwnerHotkey | TransactionType::OwnerHyperparamUpdate(_) => { + Some(RateLimitUsageKey::Subnet(netuid)) + } + _ => None, + } +} + +// 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), + 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), + BurnHalfLife => MigratedCall::admin(89, false), + BurnIncreaseMult => MigratedCall::admin(90, false), + _ => return None, + }; + + Some(identifier) +} + +fn block_number(value: u64) -> Option> { + if value == 0 { + return None; + } + Some(value.saturated_into::>()) +} + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used)] +mod tests { + 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, + WeightsVersionKeyRateLimit, utils::rate_limiting::TransactionType, + }; + use sp_core::{H160, ecdsa}; + use sp_io::TestExternalities; + use sp_runtime::traits::{SaturatedConversion, Zero}; + + use super::*; + 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; + type UsageKey = RateLimitUsageKey; + + 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 + } + + 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 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_GROUPED); + clear_rate_limiting_storage(); + + // Run migration to hydrate pallet-rate-limiting state. + GroupedRateLimitingMigration::::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 = 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: BTreeSet::UsageKey>> = + match usage { + None => { + let mut keys = BTreeSet::new(); + keys.insert(None); + keys + } + 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 + ); + } + + fn parity_check_standalone( + 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_STANDALONE); + clear_rate_limiting_storage(); + + // Run standalone migration to hydrate pallet-rate-limiting state. + StandaloneRateLimitingMigration::::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 = 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: BTreeSet::UsageKey>> = + match usage { + None => { + let mut keys = BTreeSet::new(); + keys.insert(None); + keys + } + 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!( + identifier_for_hyperparameter(Hyperparameter::ServingRateLimit), + Some(MigratedCall::admin(3, false)) + ); + assert!(identifier_for_hyperparameter(Hyperparameter::MaxWeightLimit).is_none()); + } + + #[test] + fn migration_populates_limits_last_seen_and_groups() { + new_test_ext().execute_with(|| { + let account: AccountId = ACCOUNT.into(); + pallet_subtensor::HasMigrationRun::::remove(MIGRATION_NAME_GROUPED); + + 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 = GroupedRateLimitingMigration::::on_runtime_upgrade(); + assert!(!weight.is_zero()); + assert!(pallet_subtensor::HasMigrationRun::::get( + MIGRATION_NAME_GROUPED + )); + + 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( + 11u64.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( + MigratedCall::subtensor(66, false).identifier() + ), + 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 + ); + }); + } + + #[test] + fn migrates_global_register_network_last_seen() { + new_test_ext().execute_with(|| { + HasMigrationRun::::remove(MIGRATION_NAME_GROUPED); + + // Seed legacy global register rate-limit state. + LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, 10u64); + System::set_block_number(12); + + // Run migration. + GroupedRateLimitingMigration::::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_STANDALONE); + + 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); + + StandaloneRateLimitingMigration::::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(|| { + HasMigrationRun::::remove(MIGRATION_NAME_GROUPED); + let now = 100u64; + let span = 5u64; + System::set_block_number(now.saturated_into()); + LastRateLimitedBlock::::insert(RateLimitKey::NetworkLastRegistered, now - 1); + legacy_storage::set_network_rate_limit(span); + + GroupedRateLimitingMigration::::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::>()); + }); + } + + #[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, + ); + legacy_storage::set_tx_rate_limit(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 = || { + let last = now - 1; + if span == 0 || last == 0 { + return true; + } + now - last > span + }; + 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, + ); + legacy_storage::set_tx_delegate_take_rate_limit(span); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::increase_take { + hotkey: hot.clone(), + take: 5, + }); + let origin = RuntimeOrigin::signed(account(21)); + let legacy = || { + let last = now - 1; + if span == 0 || last == 0 { + return true; + } + now - last > span + }; + 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_standalone(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_standalone(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; + legacy_storage::set_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); + 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 { + netuid, + dests: Vec::new(), + weights: Vec::new(), + version_key: 0, + }); + let origin = RuntimeOrigin::signed(hot.clone()); + let scope = { + let mut scopes = BTreeSet::new(); + scopes.insert(netuid); + Some(scopes) + }; + let usage = { + let mut keys = BTreeSet::new(); + keys.insert(UsageKey::SubnetMechanismNeuron { + netuid, + mecid: subtensor_runtime_common::MechId::MAIN, + uid, + }); + Some(keys) + }; + + let legacy_weights = || { + let last = LastUpdate::::get(NetUidStorageIndex::from(netuid)) + .get(uid as usize) + .copied() + .unwrap_or_default(); + let limit = legacy_storage::get_weights_set_rate_limit(netuid); + now.saturating_sub(last) >= limit + }; + parity_check( + now, + weights_call, + origin.clone(), + usage, + scope, + legacy_weights, + ); + + let mechanism_weights_call = + RuntimeCall::SubtensorModule(SubtensorCall::set_mechanism_weights { + netuid, + mecid: subtensor_runtime_common::MechId::MAIN, + dests: Vec::new(), + weights: Vec::new(), + version_key: 0, + }); + let mechanism_scope = { + let mut scopes = BTreeSet::new(); + scopes.insert(netuid); + Some(scopes) + }; + let mechanism_usage = { + let mut keys = BTreeSet::new(); + keys.insert(UsageKey::SubnetMechanismNeuron { + netuid, + mecid: subtensor_runtime_common::MechId::MAIN, + uid, + }); + Some(keys) + }; + parity_check( + now, + mechanism_weights_call, + origin.clone(), + mechanism_usage, + mechanism_scope, + legacy_weights, + ); + + // Hyperparam (activity_cutoff) with tempo scaling. + let hparam_span_epochs = 2u16; + legacy_storage::set_owner_hyperparam_rate_limit(hparam_span_epochs.into()); + 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_standalone(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 = { + let mut keys = BTreeSet::new(); + keys.insert(UsageKey::SubnetNeuron { netuid, uid }); + Some(keys) + }; + let scope = { + let mut scopes = BTreeSet::new(); + scopes.insert(netuid); + Some(scopes) + }; + let limit = ::EvmKeyAssociateRateLimit::get(); + let legacy = || { + let last = now - 1; + let delta = now.saturating_sub(last); + delta >= limit + }; + parity_check_standalone(now, call, origin, usage, scope, legacy); + }); + } + + #[test] + fn migration_skips_when_already_run() { + new_test_ext().execute_with(|| { + pallet_subtensor::HasMigrationRun::::insert(MIGRATION_NAME_GROUPED, true); + legacy_storage::set_tx_rate_limit(99); + + let base_weight = ::DbWeight::get().reads(1); + let weight = GroupedRateLimitingMigration::::on_runtime_upgrade(); + + assert_eq!(weight, base_weight); + assert!( + pallet_rate_limiting::Limits::::iter() + .next() + .is_none() + ); + assert!( + pallet_rate_limiting::LastSeen::::iter() + .next() + .is_none() + ); + }); + } +} diff --git a/runtime/src/migrations/subtensor_module.rs b/runtime/src/migrations/subtensor_module.rs new file mode 100644 index 0000000000..1ce87bea17 --- /dev/null +++ b/runtime/src/migrations/subtensor_module.rs @@ -0,0 +1,372 @@ +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::TaoBalance; +use subtensor_runtime_common::rate_limiting::{GROUP_REGISTER_NETWORK, GroupId}; + +use pallet_subtensor::{ + Config as SubtensorConfig, HasMigrationRun, NetworkLockReductionInterval, + 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 +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 +where + T: SubtensorConfig + + pallet_rate_limiting::Config< + LimitScope = subtensor_runtime_common::NetUid, + GroupId = GroupId, + >, +{ + 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)); + + 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(TaoBalance::from(1_000_000_000_000_u64)); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + // Hold price at 2000 TAO until day 7, then begin linear decay + let last_lock_block = current_block.saturating_add(ONE_WEEK_BLOCKS); + + // Allow registrations starting at day 7 + 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 -------------------------------------------- + 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 +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 + + 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(TaoBalance::from(NEW_LAST_LOCK_RAO)); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + // 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 + 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 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::Token; + use subtensor_runtime_common::rate_limiting::GROUP_REGISTER_NETWORK; + + 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); + } + + 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(|| { + // -- 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!( + 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 + "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 = register_network_last_seen().expect("last seen entry"); + let expected_block = current_block_before + ONE_WEEK_BLOCKS; + assert_eq!( + last_lock_block, + expected_block.saturated_into::>(), + "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!( + register_network_last_seen().expect("last seen entry"), + current_block_before.saturated_into::>(), + "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 = + register_network_last_seen().expect("last seen entry"); + 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!( + register_network_last_seen().expect("last seen entry"), + 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/legacy.rs b/runtime/src/rate_limiting/legacy.rs new file mode 100644 index 0000000000..2f29857713 --- /dev/null +++ b/runtime/src/rate_limiting/legacy.rs @@ -0,0 +1,384 @@ +#![allow(dead_code)] + +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_runtime::traits::SaturatedConversion; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; +use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; + +use super::AccountId; +use crate::{ + SubtensorInitialNetworkRateLimit, 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 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, + 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", + defaults::tx_delegate_take_rate_limit(), + ) + } + + 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", + defaults::tx_childkey_take_rate_limit(), + ) + } + + pub fn network_rate_limit() -> (u64, u64) { + 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", + defaults::owner_hyperparam_rate_limit(), + ); + (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", + 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 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(); + 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 Some(key_bytes) = next.get(prefix.len()..) else { + cursor = next; + continue; + }; + 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.get(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() + 50 + } + + 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, + BurnHalfLife = 26, + BurnIncreaseMult = 27, + } + + impl From for TransactionType { + fn from(param: Hyperparameter) -> Self { + Self::OwnerHyperparamUpdate(param) + } + } +} diff --git a/runtime/src/rate_limiting/mod.rs b/runtime/src/rate_limiting/mod.rs new file mode 100644 index 0000000000..a6c67a4cd7 --- /dev/null +++ b/runtime/src/rate_limiting/mod.rs @@ -0,0 +1,606 @@ +//! 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 codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::{ + dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo}, + pallet_prelude::Weight, + traits::Get, +}; +use frame_system::RawOrigin; +use pallet_admin_utils::Call as AdminUtilsCall; +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, + traits::{ + DispatchInfoOf, DispatchOriginOf, Dispatchable, Implication, TransactionExtension, + ValidateResult, + }, + transaction_validity::{TransactionSource, TransactionValidityError}, +}; +use sp_std::{collections::btree_set::BTreeSet, vec::Vec}; +use subtensor_macros::freeze_struct; +use subtensor_runtime_common::{ + BlockNumber, MechId, NetUid, + rate_limiting::{RateLimitUsageKey, ServingEndpoint}, +}; + +use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin, pallet_proxy, pallet_utility}; +use pallet_multisig; +use pallet_sudo; + +pub mod legacy; + +/// 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; + +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, .. } => { + let mut scopes = BTreeSet::new(); + scopes.insert(*netuid); + Some(scopes) + } + 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) + } + } + _ => None, + }, + _ => None, + } + } + + fn should_bypass(origin: &RuntimeOrigin, call: &RuntimeCall) -> BypassDecision { + 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::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, + 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 { .. } + | SubtensorCall::swap_coldkey { .. } => { + 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(); + } + } + _ => {} + } + } + + BypassDecision::enforce_and_record() + } + + 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 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 + } + } + _ => span, + } + } +} + +#[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_coldkey { new_coldkey, .. } => { + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::Account(new_coldkey.clone())); + Some(usage) + } + 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)?; + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::Account(coldkey)); + Some(usage) + } + SubtensorCall::increase_take { hotkey, .. } + | SubtensorCall::decrease_take { hotkey, .. } => { + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::Account(hotkey.clone())); + Some(usage) + } + SubtensorCall::set_childkey_take { hotkey, netuid, .. } + | SubtensorCall::set_children { hotkey, netuid, .. } => { + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::AccountSubnet { + account: hotkey.clone(), + netuid: *netuid, + }); + Some(usage) + } + SubtensorCall::batch_set_weights { netuids, .. } + | SubtensorCall::batch_commit_weights { netuids, .. } => { + let mut usage = BTreeSet::new(); + for netuid in netuids { + let netuid: NetUid = (*netuid).into(); + let uid = neuron_identity(origin, netuid)?; + usage.insert(RateLimitUsageKey::::SubnetMechanismNeuron { + netuid, + mecid: MechId::MAIN, + uid, + }); + } + if usage.is_empty() { None } else { Some(usage) } + } + 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 mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::SubnetMechanismNeuron { + netuid: *netuid, + mecid: MechId::MAIN, + uid, + }); + Some(usage) + } + 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)?; + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::SubnetMechanismNeuron { + netuid: *netuid, + mecid: *mecid, + uid, + }); + Some(usage) + } + SubtensorCall::serve_axon { netuid, .. } + | SubtensorCall::serve_axon_tls { netuid, .. } => { + let hotkey = signed_origin(origin)?; + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::AccountSubnetServing { + account: hotkey, + netuid: *netuid, + endpoint: ServingEndpoint::Axon, + }); + Some(usage) + } + SubtensorCall::serve_prometheus { netuid, .. } => { + let hotkey = signed_origin(origin)?; + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::AccountSubnetServing { + account: hotkey, + netuid: *netuid, + endpoint: ServingEndpoint::Prometheus, + }); + Some(usage) + } + SubtensorCall::associate_evm_key { netuid, .. } => { + let hotkey = signed_origin(origin)?; + let uid = pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey( + *netuid, &hotkey, + ) + .ok()?; + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::SubnetNeuron { + netuid: *netuid, + uid, + }); + Some(usage) + } + // 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). + 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, + .. + } => { + let coldkey = signed_origin(origin)?; + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::ColdkeyHotkeySubnet { + coldkey, + hotkey: hotkey.clone(), + netuid: *netuid, + }); + Some(usage) + } + SubtensorCall::swap_stake { + hotkey, + destination_netuid: netuid, + .. + } + | SubtensorCall::swap_stake_limit { + hotkey, + destination_netuid: netuid, + .. + } => { + let coldkey = signed_origin(origin)?; + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::ColdkeyHotkeySubnet { + coldkey, + hotkey: hotkey.clone(), + netuid: *netuid, + }); + Some(usage) + } + 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) + }; + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::ColdkeyHotkeySubnet { + coldkey, + hotkey: hotkey.clone(), + netuid: *netuid, + }); + Some(usage) + } + _ => None, + }, + RuntimeCall::AdminUtils(inner) => { + if let Some(netuid) = owner_hparam_netuid(inner) { + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::Subnet(netuid)); + Some(usage) + } else { + match inner { + AdminUtilsCall::sudo_set_sn_owner_hotkey { netuid, .. } => { + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::Subnet(*netuid)); + Some(usage) + } + 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)?; + let mut usage = BTreeSet::new(); + usage.insert(RateLimitUsageKey::::AccountSubnet { + account: who, + netuid: *netuid, + }); + Some(usage) + } + _ => None, + } + } + } + _ => None, + } + } +} + +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(uid) +} + +fn signed_origin(origin: &RuntimeOrigin) -> Option { + match origin.clone().into() { + Ok(RawOrigin::Signed(who)) => Some(who), + _ => None, + } +} + +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_burn_half_life { netuid, .. } + | AdminUtilsCall::sudo_set_burn_increase_mult { 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_yuma3_enabled { netuid, .. } => Some(*netuid), + _ => None, + } +} + +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo)] +#[freeze_struct("6f3f3ed087b897ba")] +pub struct UnwrappedRateLimitTransactionExtension( + pallet_rate_limiting::RateLimitTransactionExtension, +); + +impl Default for UnwrappedRateLimitTransactionExtension { + fn default() -> Self { + Self(pallet_rate_limiting::RateLimitTransactionExtension::::new()) + } +} + +impl UnwrappedRateLimitTransactionExtension { + pub fn new() -> Self { + Self::default() + } + + fn unwrap_nested_calls(call: &RuntimeCall) -> Vec<&RuntimeCall> { + let mut calls = Vec::new(); + let mut stack = Vec::new(); + stack.push(call); + + while let Some(current) = stack.pop() { + match current { + RuntimeCall::Sudo( + pallet_sudo::Call::sudo { call } + | pallet_sudo::Call::sudo_unchecked_weight { call, .. } + | pallet_sudo::Call::sudo_as { call, .. }, + ) => stack.push(call), + RuntimeCall::Proxy( + pallet_proxy::Call::proxy { call, .. } + | pallet_proxy::Call::proxy_announced { call, .. }, + ) => stack.push(call), + RuntimeCall::Utility(inner) => match inner { + pallet_utility::Call::batch { calls: inner_calls } + | pallet_utility::Call::batch_all { calls: inner_calls } + | pallet_utility::Call::force_batch { calls: inner_calls } => { + for call in inner_calls.iter().rev() { + stack.push(call); + } + } + pallet_utility::Call::dispatch_as { call, .. } + | pallet_utility::Call::as_derivative { call, .. } => stack.push(call), + _ => calls.push(current), + }, + RuntimeCall::Multisig( + pallet_multisig::Call::as_multi { call, .. } + | pallet_multisig::Call::as_multi_threshold_1 { call, .. }, + ) => stack.push(call), + _ => calls.push(current), + } + } + + calls + } +} + +impl TransactionExtension for UnwrappedRateLimitTransactionExtension +where + RuntimeCall: Dispatchable, + DispatchOriginOf: Clone, +{ + const IDENTIFIER: &'static str = "RateLimitTransactionExtension"; + + type Implicit = (); + type Val = Vec< + as TransactionExtension< + RuntimeCall, + >>::Val, + >; + type Pre = Vec< + as TransactionExtension< + RuntimeCall, + >>::Pre, + >; + + fn weight(&self, _call: &RuntimeCall) -> Weight { + Weight::zero() + } + + fn validate( + &self, + origin: DispatchOriginOf, + call: &RuntimeCall, + _info: &DispatchInfoOf, + _len: usize, + _self_implicit: Self::Implicit, + _inherited_implication: &impl Implication, + _source: TransactionSource, + ) -> ValidateResult { + let inner_calls = Self::unwrap_nested_calls(call); + let (valid, vals, origin) = self.0.validate_calls_same_block(origin, inner_calls)?; + Ok((valid, vals, origin)) + } + + fn prepare( + self, + val: Self::Val, + _origin: &DispatchOriginOf, + _call: &RuntimeCall, + _info: &DispatchInfoOf, + _len: usize, + ) -> Result { + Ok(val) + } + + fn post_dispatch( + pre: Self::Pre, + info: &DispatchInfoOf, + post_info: &mut PostDispatchInfo, + len: usize, + result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + for entry in pre { + pallet_rate_limiting::RateLimitTransactionExtension::::post_dispatch( + entry, info, post_info, len, result, + )?; + } + Ok(()) + } +} diff --git a/runtime/tests/common/mock.rs b/runtime/tests/common/mock.rs new file mode 100644 index 0000000000..9885ffd889 --- /dev/null +++ b/runtime/tests/common/mock.rs @@ -0,0 +1,54 @@ +#![allow(clippy::expect_used)] + +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}; + +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, impl Into)>) -> Self { + self.balances = balances + .into_iter() + .map(|(acc, balance)| (acc, Into::into(balance))) + .collect(); + self + } + + #[allow(dead_code)] + 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.saturated_into())); + ext + } +} diff --git a/runtime/tests/common/mod.rs b/runtime/tests/common/mod.rs new file mode 100644 index 0000000000..ff8862d9cb --- /dev/null +++ b/runtime/tests/common/mod.rs @@ -0,0 +1,3 @@ +pub mod mock; + +pub use mock::*; diff --git a/runtime/tests/rate_limiting.rs b/runtime/tests/rate_limiting.rs new file mode 100644 index 0000000000..f66b32d641 --- /dev/null +++ b/runtime/tests/rate_limiting.rs @@ -0,0 +1,1376 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use codec::{Compact, Encode}; +use frame_support::traits::GetCallMetadata; +use frame_support::{assert_ok, traits::Get}; +use node_subtensor_runtime::{ + Executive, HotkeySwapOnSubnetInterval, Runtime, RuntimeCall, SignedPayload, + SubtensorInitialTxDelegateTakeRateLimit, System, TxExtension, UncheckedExtrinsic, + check_mortality::CheckMortality, + check_nonce, + rate_limiting::legacy::{Hyperparameter, RateLimitKey, storage as legacy_storage}, + sudo_wrapper, transaction_payment_wrapper, +}; +use pallet_rate_limiting::{RateLimitTarget, RateLimitingInterface, TransactionIdentifier}; +use sp_core::{H256, Pair, sr25519}; +use sp_runtime::{ + BoundedVec, MultiSignature, + generic::Era, + traits::SaturatedConversion, + transaction_validity::{InvalidTransaction, TransactionValidityError}, +}; +use subtensor_runtime_common::{ + AccountId, AlphaBalance, MechId, NetUid, TaoBalance, Token, + rate_limiting::{GROUP_REGISTER_NETWORK, GROUP_SWAP_KEYS, RateLimitUsageKey}, +}; + +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_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); + 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 extra: TxExtension = ( + ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + CheckMortality::::from(Era::Immortal), + check_nonce::CheckNonce::::from(nonce).into(), + frame_system::CheckWeight::::new(), + ), + ( + transaction_payment_wrapper::ChargeTransactionPaymentWrapper::new(TaoBalance::ZERO), + sudo_wrapper::SudoTransactionExtension::::new(), + pallet_shield::CheckShieldedTxValidity::::new(), + pallet_subtensor::SubtensorTransactionExtension::::new(), + pallet_drand::drand_priority::DrandPriority::::new(), + node_subtensor_runtime::rate_limiting::UnwrappedRateLimitTransactionExtension::new(), + ), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), + ); + + 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) +} + +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(|| { + // 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); + + assert_extrinsic_ok(&coldkey, &coldkey_pair, call_a.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; + + // 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); + + // 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); + }); + } + + #[test] + fn register_network_lock_cost_formula_is_preserved_after_migration() { + ExtBuilder::default().build().execute_with(|| { + Executive::execute_on_runtime_upgrade(); + + let last_seen = ::RateLimiting::last_seen( + GROUP_REGISTER_NETWORK, + None, + ) + .expect("register-network last-seen should be populated by migration") + .saturated_into::(); + + let last_lock = pallet_subtensor::Pallet::::get_network_last_lock(); + let min_lock = pallet_subtensor::Pallet::::get_network_min_lock(); + let interval = pallet_subtensor::Pallet::::get_lock_reduction_interval(); + let expected_at_last_seen: TaoBalance = { + let mut cost: TaoBalance = (u64::from(last_lock) * 2).into(); + if cost < min_lock { + cost = min_lock; + } + cost + }; + let per_block_decay: TaoBalance = (u64::from(last_lock) / interval).into(); + + System::set_block_number(last_seen.saturated_into()); + let actual_at_last_seen = pallet_subtensor::Pallet::::get_network_lock_cost(); + assert_eq!(actual_at_last_seen, expected_at_last_seen); + + let next_block = last_seen + 1; + System::set_block_number(next_block.saturated_into()); + let actual_after_one = pallet_subtensor::Pallet::::get_network_lock_cost(); + let expected_after_one = + core::cmp::max(min_lock, expected_at_last_seen - per_block_decay); + assert_eq!(actual_after_one, expected_after_one); + }); + } +} + +#[test] +fn subtensor_root_register_call_metadata_resolves() { + let hotkey_pair = sr25519::Pair::from_seed(&[99u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { hotkey }); + + let identifier = TransactionIdentifier::from_call(&call).expect("identifier for call"); + let modules = RuntimeCall::get_module_names(); + let subtensor_module = modules.get(identifier.pallet_index as usize).copied(); + let subtensor_calls = RuntimeCall::get_call_names("SubtensorModule"); + println!("identifier={identifier:?}"); + println!("subtensor_module={subtensor_module:?}"); + println!("subtensor_calls_len={}", subtensor_calls.len()); + println!( + "subtensor_call_at_index={:?}", + subtensor_calls.get(identifier.extrinsic_index as usize) + ); + assert_eq!( + identifier.names::(), + Some(("SubtensorModule", "root_register")) + ); +} + +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(|| { + // 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 + 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); + }); + } +} + +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(|| { + // 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(|| { + // 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(|| { + // 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); + }); + } +} + +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); + } + + #[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 set_weights_shares_rate_limit_with_set_mechanism_weights_main() { + let hotkey_pair = sr25519::Pair::from_seed(&[30u8; 32]); + let hotkey = AccountId::from(hotkey_pair.public()); + let netuid = NetUid::from(20u16); + let span = 4u64; + 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 set_weights_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights { + netuid, + dests: vec![0], + weights: vec![u16::MAX], + version_key, + }); + let set_mechanism_main_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_mechanism_weights { + netuid, + mecid: MechId::MAIN, + dests: vec![0], + weights: vec![u16::MAX], + version_key, + }); + + System::set_block_number((registration_block + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, set_weights_call.clone()); + assert_extrinsic_rate_limited( + &hotkey, + &hotkey_pair, + set_mechanism_main_call.clone(), + ); + + System::set_block_number((registration_block + span + span).saturated_into()); + assert_extrinsic_ok(&hotkey, &hotkey_pair, set_mechanism_main_call.clone()); + assert_extrinsic_rate_limited(&hotkey, &hotkey_pair, set_weights_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); + }); + } +} + +mod staking_ops { + use super::*; + + 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) { + let _ = + pallet_subtensor::Pallet::::create_account_if_non_existent(coldkey, hotkey); + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, + coldkey, + netuid, + AlphaBalance::from(alpha), + ); + } + + #[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(|| { + setup_staking_network(netuid); + let _ = 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(|| { + setup_staking_network(netuid); + let _ = 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(|| { + 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(|| { + 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 = AlphaBalance::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); + }); + } +} + +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); + let _ = + pallet_subtensor::Pallet::::create_account_if_non_existent(coldkey, hotkey); + } + + #[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(|| { + 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(|| { + 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(|| { + 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(|| { + 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.saturated_into()) + ); + + let _ = 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); + }); + } +} + +mod owner_hparams { + use super::*; + + fn setup_owner_network(netuid: NetUid, owner: &AccountId, tempo: u16) { + pallet_subtensor::Pallet::::init_new_network(netuid, tempo); + pallet_subtensor::SubnetOwner::::insert(netuid, owner.clone()); + } + + #[test] + fn owner_hparams_respect_migrated_last_seen_and_tempo_scaling() { + let owner_pair = sr25519::Pair::from_seed(&[50u8; 32]); + let owner = AccountId::from(owner_pair.public()); + let balance = 10_000_000_000_000_u64; + let netuid = NetUid::from(30u16); + let tempo = 2u16; + let epochs: u64 = 2; + let legacy_last_seen = 9u64; + + ExtBuilder::default() + .with_balances(vec![(owner.clone(), balance)]) + .build() + .execute_with(|| { + System::set_block_number(10); + setup_owner_network(netuid, &owner, tempo); + pallet_subtensor::AdminFreezeWindow::::put(0); + + legacy_storage::set_owner_hyperparam_rate_limit(epochs); + legacy_storage::set_last_rate_limited_block( + RateLimitKey::OwnerHyperparamUpdate(netuid, Hyperparameter::ActivityCutoff), + legacy_last_seen, + ); + + Executive::execute_on_runtime_upgrade(); + + let activity_cutoff = pallet_subtensor::MinActivityCutoff::::get(); + let set_cutoff = + RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_activity_cutoff { + netuid, + activity_cutoff, + }); + + // Migrated last-seen should enforce immediately. + assert_extrinsic_rate_limited(&owner, &owner_pair, set_cutoff.clone()); + + let span = (tempo as u64) * epochs; + System::set_block_number((legacy_last_seen + span).saturated_into()); + assert_extrinsic_ok(&owner, &owner_pair, set_cutoff.clone()); + + // Different hyperparameter should not be blocked by the cutoff call. + let set_rho = RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_rho { + netuid, + rho: 1, + }); + assert_extrinsic_ok(&owner, &owner_pair, set_rho.clone()); + + // Same hyperparameter should still be rate-limited. + assert_extrinsic_rate_limited(&owner, &owner_pair, set_cutoff.clone()); + // Second hyperparameter should also be self-rate-limited immediately. + assert_extrinsic_rate_limited(&owner, &owner_pair, set_rho.clone()); + + // After the full window passes, both hyperparameters are callable again. + System::set_block_number((legacy_last_seen + (span * 2)).saturated_into()); + assert_extrinsic_ok(&owner, &owner_pair, set_cutoff); + assert_extrinsic_ok(&owner, &owner_pair, set_rho); + }); + } + + #[test] + fn owner_hparams_are_rate_limited_per_netuid() { + let owner_pair = sr25519::Pair::from_seed(&[51u8; 32]); + let owner = AccountId::from(owner_pair.public()); + let balance = 10_000_000_000_000_u64; + let netuid_a = NetUid::from(31u16); + let netuid_b = NetUid::from(32u16); + let tempo = 1u16; + let epochs: u64 = 2; + + ExtBuilder::default() + .with_balances(vec![(owner.clone(), balance)]) + .build() + .execute_with(|| { + setup_owner_network(netuid_a, &owner, tempo); + setup_owner_network(netuid_b, &owner, tempo); + pallet_subtensor::AdminFreezeWindow::::put(0); + + legacy_storage::set_owner_hyperparam_rate_limit(epochs); + Executive::execute_on_runtime_upgrade(); + + let activity_cutoff = pallet_subtensor::MinActivityCutoff::::get(); + let set_cutoff_a = + RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_activity_cutoff { + netuid: netuid_a, + activity_cutoff, + }); + let set_cutoff_b = + RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_activity_cutoff { + netuid: netuid_b, + activity_cutoff, + }); + + assert_extrinsic_ok(&owner, &owner_pair, set_cutoff_a.clone()); + assert_extrinsic_ok(&owner, &owner_pair, set_cutoff_b); + + assert_extrinsic_rate_limited(&owner, &owner_pair, set_cutoff_a); + }); + } +} diff --git a/scripts/rate-limiting-migration/README.md b/scripts/rate-limiting-migration/README.md new file mode 100644 index 0000000000..30883a67ae --- /dev/null +++ b/scripts/rate-limiting-migration/README.md @@ -0,0 +1,132 @@ +# Rate-Limiting Migration Scripts + +These scripts validate the migration from legacy sparse rate-limiting to `pallet-rate-limiting` on a cloned Finney state. + +They cover three things: +- preparing a patched mainnet clone spec and clone state +- applying a real `sudo(set_code)` runtime upgrade on the clone +- validating migrated grouped-call config, post-upgrade behavior, and migrated storage + +Current scope: +- grouped calls only +- standalone-call migration is not covered here and is planned for a separate PR + +## Preparation + +Before using these scripts, make sure: +- the node binary is built: + - `target/release/node-subtensor` +- the runtime wasm is built: + - `target/release/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm` +- `ts-tests` dependencies are installed + +Typical build commands: + +```bash +cargo build -p node-subtensor --release --features=metadata-hash +cd ts-tests && pnpm install +``` + +## Artifacts + +By default everything is stored under: +- `target/rate-limits-test` + +Main paths: +- patched spec: `target/rate-limits-test/patched-finney.json` +- cloned source state: `target/rate-limits-test/source` +- node db: `target/rate-limits-test/run/alice` + +The default artifact root is: +- `/Users/alestsurko/Desktop/subtensor/target/rate-limits-test` + +## Flow + +1. Prepare the clone artifacts once: + +```bash +scripts/rate-limiting-migration/prepare-rate-limits-clone.sh +``` + +2. Start the clone node in terminal 1: + +```bash +scripts/rate-limiting-migration/start-rate-limits-clone-node.sh +``` + +3. Run the runtime upgrade in terminal 2: + +```bash +scripts/rate-limiting-migration/upgrade-rate-limits-clone.sh +``` + +4. Stop the old node after the upgrade script finishes. + +5. Start the upgraded node again in terminal 1: + +```bash +scripts/rate-limiting-migration/start-rate-limits-clone-node.sh +``` + +6. Run validations in terminal 2: + +```bash +scripts/rate-limiting-migration/validate-rate-limits-clone-config.sh +scripts/rate-limiting-migration/validate-rate-limits-clone-behavior.sh +scripts/rate-limiting-migration/validate-rate-limits-clone-storage.sh +``` + +## What each script does + +- `prepare-rate-limits-clone.sh` + - builds the patched clone spec + - prepares synced clone state used for local replay +- `start-rate-limits-clone-node.sh` + - runs the local clone node from the prepared spec and db path +- `upgrade-rate-limits-clone.sh` + - submits the runtime upgrade to the running clone node +- `validate-rate-limits-clone-config.sh` + - checks migrated grouped-call runtime API/config responses +- `validate-rate-limits-clone-behavior.sh` + - runs real post-upgrade extrinsic probes +- `validate-rate-limits-clone-storage.sh` + - audits migrated grouped-call rate-limiting storage directly + +## Behavior phase filter + +You can rerun only one behavior phase: + +```bash +scripts/rate-limiting-migration/validate-rate-limits-clone-behavior.sh weights +``` + +Supported filters: +- `serving` +- `staking` +- `delegate-take` +- `weights` +- `swap-keys` +- `owner-hparams` + +## Useful environment variables + +- `BINARY_PATH` +- `RUNTIME_WASM` +- `CLONE_BASE_DIR` +- `PATCHED_CHAIN_SPEC` +- `CLONE_CHAIN_SPEC` +- `CLONE_SOURCE_BASE_PATH` +- `CLONE_RUN_BASE_PATH` +- `CLONE_NODE_PORT` +- `CLONE_NODE_RPC_PORT` +- `CLONE_PREP_RPC_PORT` +- `CLONE_PREP_P2P_PORT` +- `CLONE_SYNC_MODE` +- `CLONE_SYNC_TIMEOUT_SEC` +- `SKIP_CLONE_PREPARE=1` +- `SKIP_NODE_RESET=1` + +## Notes + +- `validate-rate-limits-clone-config.sh` should finish quickly. It only reads runtime API/config. +- `validate-rate-limits-clone-behavior.sh` is slower. It submits real extrinsics and waits across rate-limit windows. diff --git a/scripts/rate-limiting-migration/prepare-rate-limits-clone.sh b/scripts/rate-limiting-migration/prepare-rate-limits-clone.sh new file mode 100755 index 0000000000..e962c33a42 --- /dev/null +++ b/scripts/rate-limiting-migration/prepare-rate-limits-clone.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$REPO_ROOT/ts-tests" +export NODE_PATH="$PWD/node_modules" +export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=8192" +pnpm exec tsx ../scripts/rate-limiting-migration/prepare-rate-limits-clone.ts "$@" diff --git a/scripts/rate-limiting-migration/prepare-rate-limits-clone.ts b/scripts/rate-limiting-migration/prepare-rate-limits-clone.ts new file mode 100644 index 0000000000..09ca4b762a --- /dev/null +++ b/scripts/rate-limiting-migration/prepare-rate-limits-clone.ts @@ -0,0 +1,11 @@ +import { installConnectionLogFilter, prepareCloneSpec } from "./rate-limits-clone-lib.ts"; + +async function main() { + installConnectionLogFilter(); + await prepareCloneSpec(); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/rate-limiting-migration/rate-limits-clone-lib.ts b/scripts/rate-limiting-migration/rate-limits-clone-lib.ts new file mode 100644 index 0000000000..8aad43ae92 --- /dev/null +++ b/scripts/rate-limiting-migration/rate-limits-clone-lib.ts @@ -0,0 +1,1290 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { constants as fsConstants } from "node:fs"; +import { access, mkdir, readFile } from "node:fs/promises"; +import { basename, dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Binary, Enum, createClient, type PolkadotClient, type TypedApi } from "polkadot-api"; +import { getWsProvider } from "polkadot-api/ws-provider/node"; +import { getPolkadotSigner } from "polkadot-api/signer"; +import { subtensor } from "@polkadot-api/descriptors"; +import { Keyring } from "@polkadot/keyring"; +import { tao } from "../../ts-tests/utils/balance.ts"; +import { generateKeyringPair } from "../../ts-tests/utils/account.ts"; +import { addStake, sudoSetAdminFreezeWindow, sudoSetTempo } from "../../ts-tests/utils/staking.ts"; +import { waitForBlocks } from "../../ts-tests/utils/staking.ts"; +import { waitForFinalizedBlocks, waitForTransactionWithRetry } from "../../ts-tests/utils/transactions.ts"; +import { + expectTransactionFailure, + forceSetBalancesForRateLimit, + getCallRateLimit, + getGroupedResponseGroupId, + getRateLimitConfig, + getStakeValueForRateLimit, + isGlobalConfig, + isScopedConfig, + rateLimitKindExact, + rateLimitTargetGroup, + submitTransactionBestEffort, + waitForRateLimitTransactionWithRetry, +} from "../../ts-tests/utils/rate_limiting.ts"; +import { burnedRegister } from "../../ts-tests/utils/subnet.ts"; + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); +const LOCAL_ROOT = resolve(SCRIPT_DIR, ".."); +export const REPO_ROOT = resolve(LOCAL_ROOT, ".."); +export const BASE_DIR = process.env.CLONE_BASE_DIR || resolve(REPO_ROOT, "target/rate-limits-test"); +export const RUN_DIR = `${BASE_DIR}/run`; +export const RUN_BASE_PATH = process.env.CLONE_RUN_BASE_PATH || `${RUN_DIR}/alice`; +export const SOURCE_CHAIN_SPEC = process.env.CLONE_CHAIN_SPEC || resolve(REPO_ROOT, "chainspecs/raw_spec_finney.json"); +const SOURCE_CHAIN_SPEC_NAME = basename(SOURCE_CHAIN_SPEC, ".json").replace(/^raw_spec_/, ""); +export const PATCHED_CHAIN_SPEC = + process.env.PATCHED_CHAIN_SPEC || `${BASE_DIR}/patched-${SOURCE_CHAIN_SPEC_NAME}.json`; +export const SOURCE_BASE_PATH = process.env.CLONE_SOURCE_BASE_PATH || `${BASE_DIR}/source`; +export const BINARY_PATH = process.env.BINARY_PATH || resolve(REPO_ROOT, "target/release/node-subtensor"); +export const RUNTIME_WASM = + process.env.RUNTIME_WASM || + resolve(REPO_ROOT, "target/release/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm"); +export const SYNC_MODE = process.env.CLONE_SYNC_MODE || "warp"; +export const SYNC_TIMEOUT_SEC = process.env.CLONE_SYNC_TIMEOUT_SEC || "7200"; +export const PREP_RPC_PORT = Number(process.env.CLONE_PREP_RPC_PORT || 9976); +export const PREP_P2P_PORT = Number(process.env.CLONE_PREP_P2P_PORT || 30476); +export const LOCAL_RPC_PORT = Number(process.env.CLONE_NODE_RPC_PORT || 9964); + +const GROUP_SERVE = 0; +const GROUP_DELEGATE_TAKE = 1; +const GROUP_WEIGHTS_SET = 2; +const GROUP_REGISTER_NETWORK = 3; +const GROUP_OWNER_HPARAMS = 4; +const GROUP_STAKING_OPS = 5; +const GROUP_SWAP_KEYS = 6; +const SET_CODE_WEIGHT = { + ref_time: 164_247_810_000n, + proof_size: 67_035n, +} as const; + +export function log(message: string) { + console.log(message); +} + +type BehaviorPhase = "serving" | "staking" | "delegate-take" | "weights" | "swap-keys" | "owner-hparams"; + +function shouldRunPhase(filter: BehaviorPhase | undefined, phase: BehaviorPhase): boolean { + return filter === undefined || filter === phase; +} + +export function installConnectionLogFilter() { + const shouldSuppress = (args: unknown[]) => { + const first = args[0]; + return typeof first === "string" && first.includes("Unable to connect to ws://127.0.0.1:"); + }; + + const originalConsoleError = console.error; + const originalConsoleWarn = console.warn; + const originalConsoleLog = console.log; + + console.error = (...args: unknown[]) => { + if (!shouldSuppress(args)) originalConsoleError(...args); + }; + console.warn = (...args: unknown[]) => { + if (!shouldSuppress(args)) originalConsoleWarn(...args); + }; + console.log = (...args: unknown[]) => { + if (!shouldSuppress(args)) originalConsoleLog(...args); + }; +} + +async function runCommand(command: string, args: string[]) { + await new Promise((resolvePromise, reject) => { + const child = spawn(command, args, { + cwd: REPO_ROOT, + stdio: "inherit", + env: process.env, + }); + + child.on("error", reject); + child.on("exit", (code) => { + if (code === 0) resolvePromise(); + else reject(new Error(`${command} exited with code ${code}`)); + }); + }); +} + +export async function prepareCloneSpec() { + await mkdir(BASE_DIR, { recursive: true }); + + log("Preparing patched mainnet clone spec"); + await runCommand(BINARY_PATH, [ + "build-patched-spec", + "--chain", + SOURCE_CHAIN_SPEC, + "--base-path", + SOURCE_BASE_PATH, + "--output", + PATCHED_CHAIN_SPEC, + "--sync", + SYNC_MODE, + "--sync-timeout-sec", + SYNC_TIMEOUT_SEC, + "--rpc-port", + String(PREP_RPC_PORT), + "--port", + String(PREP_P2P_PORT), + ]); +} + +export async function waitForLocalRpc(timeoutMs = 1_200_000): Promise { + const deadline = Date.now() + timeoutMs; + const url = `ws://127.0.0.1:${LOCAL_RPC_PORT}`; + let lastProgressLog = 0; + + while (Date.now() < deadline) { + let client: PolkadotClient | undefined; + try { + client = createClient(getWsProvider(url)); + const api = client.getTypedApi(subtensor); + await api.query.System.Number.getValue(); + try { + client.destroy(); + } catch {} + return; + } catch { + try { + client?.destroy(); + } catch {} + const now = Date.now(); + if (now - lastProgressLog >= 30_000) { + lastProgressLog = now; + log(`Waiting for alice RPC at ${url} (genesis import may take several minutes)`); + } + await new Promise((resolve) => setTimeout(resolve, 1_000)); + } + } + + throw new Error(`alice RPC at ${url} did not become ready in time`); +} + +export async function waitForLocalFinalization(blocks = 3) { + const client = await connectLocalClient(); + try { + const api = client.getTypedApi(subtensor); + await waitForFinalizedBlocks(api, blocks); + } finally { + try { + client.destroy(); + } catch {} + } +} + +export async function connectLocalClient(): Promise { + return createClient(getWsProvider(`ws://127.0.0.1:${LOCAL_RPC_PORT}`)); +} + +export async function waitForRateLimitingRuntimeApi(client: PolkadotClient, timeoutMs = 120_000) { + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + try { + await getCallRateLimit(client as any, "SubtensorModule", "serve_axon"); + return; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (!message.includes("RateLimitingRuntimeApi_get_rate_limit is not found")) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, 1_000)); + } + } + + throw new Error("RateLimiting runtime API did not become available in time"); +} + +export async function upgradeCloneRuntime() { + const client = await connectLocalClient(); + try { + const api = client.getTypedApi(subtensor); + log(`Upgrading clone runtime from ${RUNTIME_WASM}`); + + const runtimeWasm = new Uint8Array(await readFile(RUNTIME_WASM)); + const keyring = new Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + + const setCode = api.tx.System.set_code({ + code: Binary.fromBytes(runtimeWasm), + }); + const tx = api.tx.Sudo.sudo_unchecked_weight({ + call: setCode.decodedCall, + weight: SET_CODE_WEIGHT, + }); + + const signer = getPolkadotSigner(alice.publicKey, "Sr25519", alice.sign); + const account = await api.query.System.Account.getValue(alice.address, { at: "best" }); + const signedHex = await tx.sign(signer, { + at: "best", + nonce: account.nonce, + }); + const txHash = await (client as any)._request("author_submitExtrinsic", [signedHex]); + log(`Submitted clone runtime upgrade tx ${String(txHash)}`); + const startHeader = (await (client as any)._request("chain_getHeader", [])) as { number?: string }; + const startNumber = Number.parseInt(startHeader?.number ?? "0x0", 16); + const deadline = Date.now() + 60_000; + + while (Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 1_000)); + try { + const header = (await (client as any)._request("chain_getHeader", [])) as { number?: string }; + const currentNumber = Number.parseInt(header?.number ?? "0x0", 16); + if (currentNumber >= startNumber + 2) { + log("Upgrade transaction submitted; restart the node on the same db now"); + return; + } + } catch { + log("Upgrade submitted and RPC disconnected; restart the node on the same db now"); + return; + } + } + + throw new Error("upgrade tx was submitted, but chain did not advance within 60s"); + } finally { + try { + client.destroy(); + } catch {} + } +} + +async function expectGroupedCall(client: PolkadotClient, pallet: string, extrinsic: string, groupId: number) { + const response = await getCallRateLimit(client as any, pallet, extrinsic); + assert.ok(response, `${pallet}.${extrinsic} returned no rate-limit response`); + assert.equal( + getGroupedResponseGroupId(response), + groupId, + `${pallet}.${extrinsic} group mismatch: ${JSON.stringify(response)}` + ); + return response; +} + +function assertScopeKind(response: unknown, scopeKind: "global" | "scoped", label: string) { + const config = getRateLimitConfig(response as any); + if (scopeKind === "global") { + assert.equal(isGlobalConfig(config), true, `${label} expected global config`); + } else { + assert.equal(isScopedConfig(config), true, `${label} expected scoped config`); + } +} + +function enumVariantName(value: any): string | undefined { + if (value && typeof value === "object") { + if (typeof value.type === "string") return value.type; + const [key] = Object.entries(value)[0] ?? []; + if (typeof key === "string") return key; + } + return undefined; +} + +function enumVariantValue(value: any): any { + if (value && typeof value === "object") { + if ("value" in value) return value.value; + const [, inner] = Object.entries(value)[0] ?? []; + return inner; + } + return undefined; +} + +function migrationFlagKey(name: string) { + return Binary.fromBytes(new TextEncoder().encode(name)); +} + +function txTarget(pallet_index: number, extrinsic_index: number) { + return Enum("Transaction", { pallet_index, extrinsic_index }); +} + +const OWNER_HPARAM_IDENTIFIERS = new Map([ + ["ServingRateLimit", { pallet_index: 19, extrinsic_index: 3 }], + ["MaxDifficulty", { pallet_index: 19, extrinsic_index: 5 }], + ["AdjustmentAlpha", { pallet_index: 19, extrinsic_index: 9 }], + ["ImmunityPeriod", { pallet_index: 19, extrinsic_index: 13 }], + ["MinAllowedWeights", { pallet_index: 19, extrinsic_index: 14 }], + ["MaxAllowedUids", { pallet_index: 19, extrinsic_index: 15 }], + ["Rho", { pallet_index: 19, extrinsic_index: 17 }], + ["ActivityCutoff", { pallet_index: 19, extrinsic_index: 18 }], + ["PowRegistrationAllowed", { pallet_index: 19, extrinsic_index: 20 }], + ["MinBurn", { pallet_index: 19, extrinsic_index: 22 }], + ["MaxBurn", { pallet_index: 19, extrinsic_index: 23 }], + ["BondsMovingAverage", { pallet_index: 19, extrinsic_index: 26 }], + ["BondsPenalty", { pallet_index: 19, extrinsic_index: 60 }], + ["CommitRevealEnabled", { pallet_index: 19, extrinsic_index: 49 }], + ["LiquidAlphaEnabled", { pallet_index: 19, extrinsic_index: 50 }], + ["AlphaValues", { pallet_index: 19, extrinsic_index: 51 }], + ["WeightCommitInterval", { pallet_index: 19, extrinsic_index: 57 }], + ["TransferEnabled", { pallet_index: 19, extrinsic_index: 61 }], + ["AlphaSigmoidSteepness", { pallet_index: 19, extrinsic_index: 68 }], + ["Yuma3Enabled", { pallet_index: 19, extrinsic_index: 69 }], + ["BondsResetEnabled", { pallet_index: 19, extrinsic_index: 70 }], + ["ImmuneNeuronLimit", { pallet_index: 19, extrinsic_index: 72 }], + ["RecycleOrBurn", { pallet_index: 19, extrinsic_index: 80 }], + ["BurnHalfLife", { pallet_index: 19, extrinsic_index: 89 }], + ["BurnIncreaseMult", { pallet_index: 19, extrinsic_index: 90 }], +]); + +function getResponseKind(response: any): "grouped" | "standalone" | undefined { + if (response && typeof response === "object") { + if ("group_id" in response) return "grouped"; + if (response.type === "grouped" || response.type === "Grouped" || "Grouped" in response) return "grouped"; + if (response.type === "standalone" || response.type === "Standalone" || "Standalone" in response) { + return "standalone"; + } + const [key] = Object.entries(response)[0] ?? []; + if (typeof key === "string") { + if (key.toLowerCase() === "grouped") return "grouped"; + if (key.toLowerCase() === "standalone") return "standalone"; + } + } + return undefined; +} + +function assertResponseKind(response: unknown, kind: "grouped" | "standalone", label: string) { + assert.equal(getResponseKind(response as any), kind, `${label} expected ${kind} rate-limit response`); +} + +export async function verifyCloneConfig() { + const client = await connectLocalClient(); + try { + log("Validating migrated clone configuration"); + await waitForRateLimitingRuntimeApi(client); + + const expectedGroupedCalls: Array<{ + pallet: string; + extrinsic: string; + groupId: number; + scopeKind: "global" | "scoped"; + }> = [ + { pallet: "SubtensorModule", extrinsic: "serve_axon", groupId: GROUP_SERVE, scopeKind: "scoped" }, + { pallet: "SubtensorModule", extrinsic: "serve_axon_tls", groupId: GROUP_SERVE, scopeKind: "scoped" }, + { pallet: "SubtensorModule", extrinsic: "serve_prometheus", groupId: GROUP_SERVE, scopeKind: "scoped" }, + { + pallet: "SubtensorModule", + extrinsic: "increase_take", + groupId: GROUP_DELEGATE_TAKE, + scopeKind: "global", + }, + { + pallet: "SubtensorModule", + extrinsic: "decrease_take", + groupId: GROUP_DELEGATE_TAKE, + scopeKind: "global", + }, + { pallet: "SubtensorModule", extrinsic: "set_weights", groupId: GROUP_WEIGHTS_SET, scopeKind: "scoped" }, + { + pallet: "SubtensorModule", + extrinsic: "batch_set_weights", + groupId: GROUP_WEIGHTS_SET, + scopeKind: "scoped", + }, + { pallet: "SubtensorModule", extrinsic: "commit_weights", groupId: GROUP_WEIGHTS_SET, scopeKind: "scoped" }, + { + pallet: "SubtensorModule", + extrinsic: "batch_commit_weights", + groupId: GROUP_WEIGHTS_SET, + scopeKind: "scoped", + }, + { + pallet: "SubtensorModule", + extrinsic: "commit_timelocked_weights", + groupId: GROUP_WEIGHTS_SET, + scopeKind: "scoped", + }, + { pallet: "SubtensorModule", extrinsic: "reveal_weights", groupId: GROUP_WEIGHTS_SET, scopeKind: "scoped" }, + { + pallet: "SubtensorModule", + extrinsic: "batch_reveal_weights", + groupId: GROUP_WEIGHTS_SET, + scopeKind: "scoped", + }, + { + pallet: "SubtensorModule", + extrinsic: "set_mechanism_weights", + groupId: GROUP_WEIGHTS_SET, + scopeKind: "scoped", + }, + { + pallet: "SubtensorModule", + extrinsic: "commit_mechanism_weights", + groupId: GROUP_WEIGHTS_SET, + scopeKind: "scoped", + }, + { + pallet: "SubtensorModule", + extrinsic: "commit_crv3_mechanism_weights", + groupId: GROUP_WEIGHTS_SET, + scopeKind: "scoped", + }, + { + pallet: "SubtensorModule", + extrinsic: "commit_timelocked_mechanism_weights", + groupId: GROUP_WEIGHTS_SET, + scopeKind: "scoped", + }, + { + pallet: "SubtensorModule", + extrinsic: "reveal_mechanism_weights", + groupId: GROUP_WEIGHTS_SET, + scopeKind: "scoped", + }, + { + pallet: "SubtensorModule", + extrinsic: "register_network", + groupId: GROUP_REGISTER_NETWORK, + scopeKind: "global", + }, + { + pallet: "SubtensorModule", + extrinsic: "register_network_with_identity", + groupId: GROUP_REGISTER_NETWORK, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_serving_rate_limit", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_max_difficulty", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_adjustment_alpha", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_immunity_period", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_min_allowed_weights", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_max_allowed_uids", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_activity_cutoff", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { pallet: "AdminUtils", extrinsic: "sudo_set_rho", groupId: GROUP_OWNER_HPARAMS, scopeKind: "global" }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_network_pow_registration_allowed", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { pallet: "AdminUtils", extrinsic: "sudo_set_min_burn", groupId: GROUP_OWNER_HPARAMS, scopeKind: "global" }, + { pallet: "AdminUtils", extrinsic: "sudo_set_max_burn", groupId: GROUP_OWNER_HPARAMS, scopeKind: "global" }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_bonds_moving_average", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_bonds_penalty", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_commit_reveal_weights_enabled", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_liquid_alpha_enabled", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_alpha_values", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_commit_reveal_weights_interval", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_toggle_transfer", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_alpha_sigmoid_steepness", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_yuma3_enabled", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_bonds_reset_enabled", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_owner_immune_neuron_limit", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_recycle_or_burn", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_burn_half_life", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { + pallet: "AdminUtils", + extrinsic: "sudo_set_burn_increase_mult", + groupId: GROUP_OWNER_HPARAMS, + scopeKind: "global", + }, + { pallet: "SubtensorModule", extrinsic: "add_stake", groupId: GROUP_STAKING_OPS, scopeKind: "global" }, + { + pallet: "SubtensorModule", + extrinsic: "add_stake_limit", + groupId: GROUP_STAKING_OPS, + scopeKind: "global", + }, + { pallet: "SubtensorModule", extrinsic: "remove_stake", groupId: GROUP_STAKING_OPS, scopeKind: "global" }, + { + pallet: "SubtensorModule", + extrinsic: "remove_stake_limit", + groupId: GROUP_STAKING_OPS, + scopeKind: "global", + }, + { + pallet: "SubtensorModule", + extrinsic: "remove_stake_full_limit", + groupId: GROUP_STAKING_OPS, + scopeKind: "global", + }, + { pallet: "SubtensorModule", extrinsic: "move_stake", groupId: GROUP_STAKING_OPS, scopeKind: "global" }, + { pallet: "SubtensorModule", extrinsic: "transfer_stake", groupId: GROUP_STAKING_OPS, scopeKind: "global" }, + { pallet: "SubtensorModule", extrinsic: "swap_stake", groupId: GROUP_STAKING_OPS, scopeKind: "global" }, + { + pallet: "SubtensorModule", + extrinsic: "swap_stake_limit", + groupId: GROUP_STAKING_OPS, + scopeKind: "global", + }, + { pallet: "SubtensorModule", extrinsic: "swap_hotkey", groupId: GROUP_SWAP_KEYS, scopeKind: "global" }, + { pallet: "SubtensorModule", extrinsic: "swap_coldkey", groupId: GROUP_SWAP_KEYS, scopeKind: "global" }, + ]; + + for (const { pallet, extrinsic, groupId, scopeKind } of expectedGroupedCalls) { + const response = await expectGroupedCall(client, pallet, extrinsic, groupId); + assertResponseKind(response, "grouped", `${pallet}.${extrinsic}`); + assertScopeKind(response, scopeKind, `${pallet}.${extrinsic}`); + } + } finally { + try { + client.destroy(); + } catch {} + } +} + +export async function verifyCloneStorageAudit() { + const client = await connectLocalClient(); + try { + const api = client.getTypedApi(subtensor); + log("Auditing migrated clone storage"); + + const groupedMarker = await api.query.SubtensorModule.HasMigrationRun.getValue( + migrationFlagKey("migrate_grouped_rate_limiting") + ); + const standaloneMarker = await api.query.SubtensorModule.HasMigrationRun.getValue( + migrationFlagKey("migrate_standalone_rate_limiting") + ); + log(`Migration markers: grouped=${groupedMarker} standalone=${standaloneMarker}`); + + try { + await api.query.RateLimiting.LastSeen.getValue(rateLimitTargetGroup(GROUP_SERVE) as never, undefined, { + at: "best", + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (message.includes("Storage(RateLimiting.LastSeen) not found")) { + throw new Error( + "upgraded rate-limiting runtime is not active on this node; run upgrade-rate-limits-clone.sh, stop the old node, and start it again before running storage audit" + ); + } + throw error; + } + + let checkedServing = 0; + for (const { keyArgs, value } of await api.query.SubtensorModule.Axons.getEntries({ at: "best" })) { + if (!value) continue; + const block = Number((value as any).block ?? 0); + if (block === 0) continue; + const [netuid, account] = keyArgs as [number, string]; + const migrated = await api.query.RateLimiting.LastSeen.getValue( + rateLimitTargetGroup(GROUP_SERVE) as never, + Enum("AccountSubnetServing", { account, netuid, endpoint: Enum("Axon") }) as never, + { at: "best" } + ); + assert.equal(Number(migrated), block, `serving axon last_seen mismatch for ${account} on netuid ${netuid}`); + checkedServing += 1; + } + for (const { keyArgs, value } of await api.query.SubtensorModule.Prometheus.getEntries({ at: "best" })) { + if (!value) continue; + const block = Number((value as any).block ?? 0); + if (block === 0) continue; + const [netuid, account] = keyArgs as [number, string]; + const migrated = await api.query.RateLimiting.LastSeen.getValue( + rateLimitTargetGroup(GROUP_SERVE) as never, + Enum("AccountSubnetServing", { account, netuid, endpoint: Enum("Prometheus") }) as never, + { at: "best" } + ); + assert.equal( + Number(migrated), + block, + `serving prometheus last_seen mismatch for ${account} on netuid ${netuid}` + ); + checkedServing += 1; + } + + let checkedWeights = 0; + for (const { keyArgs, value } of await api.query.SubtensorModule.LastUpdate.getEntries({ at: "best" })) { + const [netuidIndex] = keyArgs as [number]; + const netuid = netuidIndex % 4096; + const mecid = Math.floor(netuidIndex / 4096); + const blocks = value as Array; + for (let uid = 0; uid < blocks.length; uid += 1) { + const block = Number(blocks[uid] ?? 0); + if (block === 0) continue; + const migrated = await api.query.RateLimiting.LastSeen.getValue( + rateLimitTargetGroup(GROUP_WEIGHTS_SET) as never, + Enum("SubnetMechanismNeuron", { netuid, mecid, uid }) as never, + { at: "best" } + ); + assert.equal( + Number(migrated), + block, + `weights last_seen mismatch for netuid ${netuid}, mecid ${mecid}, uid ${uid}` + ); + checkedWeights += 1; + } + } + + let checkedOwner = 0; + let checkedRegister = 0; + let checkedDelegateTake = 0; + let checkedSwapKeys = 0; + for (const { keyArgs, value } of await api.query.SubtensorModule.LastRateLimitedBlock.getEntries({ + at: "best", + })) { + const [rateLimitKey] = keyArgs as [any]; + const block = Number(value); + if (block === 0) continue; + const variant = enumVariantName(rateLimitKey); + const payload = enumVariantValue(rateLimitKey); + + if (variant === "OwnerHyperparamUpdate") { + const [netuid, hyper] = payload as [number, any]; + const hyperName = enumVariantName(hyper); + const identifier = hyperName ? OWNER_HPARAM_IDENTIFIERS.get(hyperName) : undefined; + if (!identifier) continue; + const migrated = await api.query.RateLimiting.LastSeen.getValue( + txTarget(identifier.pallet_index, identifier.extrinsic_index) as never, + Enum("Subnet", netuid) as never, + { at: "best" } + ); + assert.equal( + Number(migrated), + block, + `owner-hparam last_seen mismatch for ${hyperName} on netuid ${netuid}` + ); + checkedOwner += 1; + continue; + } + + if (variant === "NetworkLastRegistered") { + const migrated = await api.query.RateLimiting.LastSeen.getValue( + rateLimitTargetGroup(GROUP_REGISTER_NETWORK) as never, + undefined, + { at: "best" } + ); + assert.equal(Number(migrated), block, "register_network last_seen mismatch"); + checkedRegister += 1; + continue; + } + + if (variant === "LastTxBlockDelegateTake") { + const account = payload as string; + const migrated = await api.query.RateLimiting.LastSeen.getValue( + rateLimitTargetGroup(GROUP_DELEGATE_TAKE) as never, + Enum("Account", account) as never, + { at: "best" } + ); + assert.equal(Number(migrated), block, `delegate_take last_seen mismatch for ${account}`); + checkedDelegateTake += 1; + continue; + } + + if (variant === "LastTxBlock") { + const account = payload as string; + const migrated = await api.query.RateLimiting.LastSeen.getValue( + rateLimitTargetGroup(GROUP_SWAP_KEYS) as never, + Enum("Account", account) as never, + { at: "best" } + ); + assert.equal(Number(migrated), block, `swap_keys last_seen mismatch for ${account}`); + checkedSwapKeys += 1; + } + } + + log( + `Storage audit checked serving=${checkedServing}, weights=${checkedWeights}, owner=${checkedOwner}, register=${checkedRegister}, delegate_take=${checkedDelegateTake}, swap_keys=${checkedSwapKeys}` + ); + } finally { + try { + client.destroy(); + } catch {} + } +} + +function getAlice() { + const keyring = new Keyring({ type: "sr25519" }); + return keyring.addFromUri("//Alice"); +} + +function asSudoTx(api: TypedApi, inner: { decodedCall: unknown }) { + return api.tx.Sudo.sudo({ call: inner.decodedCall as any }); +} + +async function sudoSetNetworkRegistrationAllowed( + api: TypedApi, + netuid: number, + registrationAllowed: boolean +) { + const alice = getAlice(); + const inner = api.tx.AdminUtils.sudo_set_network_registration_allowed({ + netuid, + registration_allowed: registrationAllowed, + }); + await waitForTransactionWithRetry( + api, + asSudoTx(api, inner) as any, + alice as any, + `sudo_set_network_registration_allowed_${netuid}` + ); +} + +async function sudoSetCommitRevealWeightsEnabled(api: TypedApi, netuid: number, enabled: boolean) { + const alice = getAlice(); + const inner = api.tx.AdminUtils.sudo_set_commit_reveal_weights_enabled({ + netuid, + enabled, + }); + await waitForTransactionWithRetry( + api, + asSudoTx(api, inner) as any, + alice as any, + `sudo_set_commit_reveal_weights_enabled_${netuid}` + ); +} + +async function sudoSetStakeThreshold(api: TypedApi, minStake: number | bigint) { + const alice = getAlice(); + const inner = api.tx.AdminUtils.sudo_set_stake_threshold({ + min_stake: typeof minStake === "bigint" ? minStake : BigInt(minStake), + }); + await waitForTransactionWithRetry(api, asSudoTx(api, inner) as any, alice as any, "sudo_set_stake_threshold"); +} + +async function sudoSetTargetRegistrationsPerInterval( + api: TypedApi, + netuid: number, + targetRegistrations: number +) { + const alice = getAlice(); + const inner = api.tx.AdminUtils.sudo_set_target_registrations_per_interval({ + netuid, + target_registrations_per_interval: targetRegistrations, + }); + await waitForTransactionWithRetry( + api, + asSudoTx(api, inner) as any, + alice as any, + `sudo_set_target_registrations_per_interval_${netuid}` + ); +} + +async function waitForSudoOk( + api: TypedApi, + tx: any, + signerPair: ReturnType, + label: string +) { + const signer = getPolkadotSigner(signerPair.publicKey, "Sr25519", signerPair.sign); + + await new Promise((resolve, reject) => { + let settled = false; + let timeoutId: ReturnType; + + const finish = (cb: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timeoutId); + subscription.unsubscribe(); + cb(); + }; + + const subscription = tx.signSubmitAndWatch(signer, { at: "best" }).subscribe({ + next: async (event: any) => { + if (event.type !== "txBestBlocksState" || !event.found) return; + + if (!event.ok || event.dispatchError) { + finish(() => + reject(new Error(`[${label}] dispatch error: ${JSON.stringify(event.dispatchError)}`)) + ); + return; + } + + try { + const events = await api.query.System.Events.getValue({ at: event.block.hash }); + const sudoEvent = events.find( + (record: any) => + record.phase?.type === "ApplyExtrinsic" && + record.phase.value === event.block.index && + record.event?.type === "Sudo" && + record.event?.value?.type === "Sudid" + ) as any; + + const sudoResult = sudoEvent?.event?.value?.value?.sudo_result; + if (sudoResult?.success === false) { + finish(() => reject(new Error(`[${label}] sudo error: ${JSON.stringify(sudoResult.value)}`))); + return; + } + + finish(resolve); + } catch (error) { + finish(() => reject(error instanceof Error ? error : new Error(String(error)))); + } + }, + error: (error: unknown) => { + finish(() => reject(error instanceof Error ? error : new Error(String(error)))); + }, + }); + + timeoutId = setTimeout(() => { + finish(() => reject(new Error(`[${label}] timed out waiting for sudo inclusion`))); + }, 30_000); + }); +} + +async function sudoSetScopedGroupRateLimit( + api: TypedApi, + groupId: number, + scope: number, + limit: number +) { + const alice = getAlice(); + const inner = api.tx.RateLimiting.set_rate_limit({ + target: rateLimitTargetGroup(groupId) as never, + scope, + limit: rateLimitKindExact(limit) as never, + }); + await waitForSudoOk(api, asSudoTx(api, inner), alice, `set_scoped_group_rate_limit_${groupId}`); + await waitForFinalizedBlocks(api, 1); +} + +async function sudoSetGlobalGroupRateLimit(api: TypedApi, groupId: number, limit: number) { + const alice = getAlice(); + const inner = api.tx.RateLimiting.set_rate_limit({ + target: rateLimitTargetGroup(groupId) as never, + scope: undefined, + limit: rateLimitKindExact(limit) as never, + }); + await waitForSudoOk(api, asSudoTx(api, inner), alice, `set_group_rate_limit_${groupId}`); + await waitForFinalizedBlocks(api, 1); +} + +async function getExistingSubnetNetuids( + api: TypedApi, + count: number, + requireFreeSlot = false +): Promise { + const totalNetworks = Number(await api.query.SubtensorModule.TotalNetworks.getValue()); + const netuids: number[] = []; + + for (let netuid = 1; netuid < totalNetworks && netuids.length < count; netuid += 1) { + const added = await api.query.SubtensorModule.NetworksAdded.getValue(netuid); + if (!added) continue; + if (requireFreeSlot) { + const current = Number(await api.query.SubtensorModule.SubnetworkN.getValue(netuid)); + const max = Number(await api.query.SubtensorModule.MaxAllowedUids.getValue(netuid)); + if (current >= max) continue; + } + netuids.push(netuid); + } + + if (netuids.length < count) { + throw new Error( + `Expected at least ${count} existing non-root subnets${requireFreeSlot ? " with free UID slots" : ""}, found ${netuids.length}` + ); + } + + return netuids; +} + +export async function verifyCloneBehavior(filter?: BehaviorPhase) { + const client = await connectLocalClient(); + try { + const api = client.getTypedApi(subtensor); + log("Validating clone behavior on fresh state"); + const [netuidServing, netuidStaking, netuidDelegate, netuidWeights, netuidSwap] = + await getExistingSubnetNetuids(api, 5, true); + const netuidOwnerA = netuidServing; + const netuidOwnerB = netuidStaking; + + await sudoSetAdminFreezeWindow(api, 0); + await sudoSetGlobalGroupRateLimit(api, GROUP_OWNER_HPARAMS, 0); + await sudoSetStakeThreshold(api, 0); + for (const netuid of [netuidServing, netuidStaking, netuidDelegate, netuidWeights, netuidSwap]) { + await sudoSetNetworkRegistrationAllowed(api, netuid, true); + await sudoSetTargetRegistrationsPerInterval(api, netuid, 256); + } + + if (shouldRunPhase(filter, "serving")) { + log("Behavior: serving"); + const coldkey = generateKeyringPair("sr25519"); + const hotkey = generateKeyringPair("sr25519"); + + await forceSetBalancesForRateLimit(api, [coldkey.address, hotkey.address]); + await burnedRegister(api, netuidServing, hotkey.address, coldkey); + + await sudoSetScopedGroupRateLimit(api, GROUP_SERVE, netuidServing, 2); + + const serveAxon = api.tx.SubtensorModule.serve_axon({ + netuid: netuidServing, + version: 1, + ip: 0n, + port: 3030, + ip_type: 4, + protocol: 0, + placeholder1: 0, + placeholder2: 0, + }); + const serveAxonTls = api.tx.SubtensorModule.serve_axon_tls({ + netuid: netuidServing, + version: 1, + ip: 0n, + port: 3030, + ip_type: 4, + protocol: 0, + placeholder1: 0, + placeholder2: 0, + certificate: Binary.fromBytes(new Uint8Array([7, 7, 7])), + }); + const servePrometheus = api.tx.SubtensorModule.serve_prometheus({ + netuid: netuidServing, + version: 1, + ip: 1_676_056_785n, + port: 3031, + ip_type: 4, + }); + + await waitForRateLimitTransactionWithRetry(api, serveAxon, hotkey, "clone_serve_axon_initial"); + await expectTransactionFailure(api, serveAxonTls, hotkey, "clone_serve_axon_tls_rate_limited"); + await waitForRateLimitTransactionWithRetry(api, servePrometheus, hotkey, "clone_serve_prometheus_initial"); + await expectTransactionFailure(api, servePrometheus, hotkey, "clone_serve_prometheus_rate_limited"); + await waitForFinalizedBlocks(api, 2); + await waitForRateLimitTransactionWithRetry(api, serveAxonTls, hotkey, "clone_serve_axon_tls_after_window"); + await waitForRateLimitTransactionWithRetry( + api, + servePrometheus, + hotkey, + "clone_serve_prometheus_after_window" + ); + } + + if (shouldRunPhase(filter, "staking")) { + log("Behavior: staking"); + const coldkey = generateKeyringPair("sr25519"); + const hotkey = generateKeyringPair("sr25519"); + + await forceSetBalancesForRateLimit(api, [coldkey.address, hotkey.address]); + await burnedRegister(api, netuidStaking, hotkey.address, coldkey); + + const addStake = api.tx.SubtensorModule.add_stake({ + hotkey: hotkey.address, + netuid: netuidStaking, + amount_staked: tao(100), + }); + await waitForRateLimitTransactionWithRetry(api, addStake, coldkey, "clone_add_stake_initial"); + + const alpha = await getStakeValueForRateLimit(api, hotkey.address, coldkey.address, netuidStaking); + const removeStake = api.tx.SubtensorModule.remove_stake({ + hotkey: hotkey.address, + netuid: netuidStaking, + amount_unstaked: alpha, + }); + await expectTransactionFailure(api, removeStake, coldkey, "clone_remove_stake_rate_limited"); + } + + if (shouldRunPhase(filter, "delegate-take")) { + log("Behavior: delegate-take"); + const coldkey = generateKeyringPair("sr25519"); + const hotkey = generateKeyringPair("sr25519"); + + await forceSetBalancesForRateLimit(api, [coldkey.address, hotkey.address]); + await burnedRegister(api, netuidDelegate, hotkey.address, coldkey); + await sudoSetGlobalGroupRateLimit(api, GROUP_DELEGATE_TAKE, 2); + + const currentTake = await api.query.SubtensorModule.Delegates.getValue(hotkey.address); + assert.ok(currentTake > 0, "expected fresh delegate take to be above zero"); + const loweredTake = currentTake - 1; + + const decreaseTake = api.tx.SubtensorModule.decrease_take({ + hotkey: hotkey.address, + take: loweredTake, + }); + const increaseTake = api.tx.SubtensorModule.increase_take({ + hotkey: hotkey.address, + take: currentTake, + }); + + await waitForRateLimitTransactionWithRetry(api, decreaseTake, coldkey, "clone_decrease_take_initial"); + await expectTransactionFailure(api, increaseTake, coldkey, "clone_increase_take_rate_limited"); + await waitForFinalizedBlocks(api, 2); + await waitForRateLimitTransactionWithRetry(api, increaseTake, coldkey, "clone_increase_take_after_window"); + } + + if (shouldRunPhase(filter, "weights")) { + log("Behavior: weights"); + const coldkey = generateKeyringPair("sr25519"); + const hotkey = generateKeyringPair("sr25519"); + + await forceSetBalancesForRateLimit(api, [coldkey.address, hotkey.address]); + await burnedRegister(api, netuidWeights, hotkey.address, coldkey); + await addStake(api, coldkey, hotkey.address, netuidWeights, tao(100)); + await sudoSetCommitRevealWeightsEnabled(api, netuidWeights, false); + await sudoSetScopedGroupRateLimit(api, GROUP_WEIGHTS_SET, netuidWeights, 2); + + const uid = await api.query.SubtensorModule.Uids.getValue(netuidWeights, hotkey.address); + assert.notEqual(uid, undefined, "expected registered uid for weights probe"); + const versionKey = await api.query.SubtensorModule.WeightsVersionKey.getValue(netuidWeights); + + const setWeights = api.tx.SubtensorModule.set_weights({ + netuid: netuidWeights, + dests: [uid!], + weights: [65_535], + version_key: versionKey, + }); + const setMechanismWeights = api.tx.SubtensorModule.set_mechanism_weights({ + netuid: netuidWeights, + mecid: 0, + dests: [uid!], + weights: [65_535], + version_key: versionKey, + }); + + await waitForRateLimitTransactionWithRetry(api, setWeights, hotkey, "clone_set_weights_initial"); + await expectTransactionFailure( + api, + setMechanismWeights, + hotkey, + "clone_set_mechanism_weights_rate_limited" + ); + await waitForBlocks(api, 2); + await waitForRateLimitTransactionWithRetry( + api, + setMechanismWeights, + hotkey, + "clone_set_mechanism_weights_after_window" + ); + } + + if (shouldRunPhase(filter, "swap-keys")) { + log("Behavior: swap-keys"); + const coldkey = generateKeyringPair("sr25519"); + const oldHotkey = generateKeyringPair("sr25519"); + const newHotkeyA = generateKeyringPair("sr25519"); + const newHotkeyB = generateKeyringPair("sr25519"); + + await forceSetBalancesForRateLimit(api, [ + coldkey.address, + oldHotkey.address, + newHotkeyA.address, + newHotkeyB.address, + ]); + await burnedRegister(api, netuidSwap, oldHotkey.address, coldkey); + await sudoSetGlobalGroupRateLimit(api, GROUP_SWAP_KEYS, 2); + + const swapHotkeyFirst = api.tx.SubtensorModule.swap_hotkey({ + hotkey: oldHotkey.address, + new_hotkey: newHotkeyA.address, + netuid: undefined, + }); + const swapHotkeySecond = api.tx.SubtensorModule.swap_hotkey({ + hotkey: newHotkeyA.address, + new_hotkey: newHotkeyB.address, + netuid: undefined, + }); + + await waitForRateLimitTransactionWithRetry(api, swapHotkeyFirst, coldkey, "clone_swap_hotkey_initial"); + await expectTransactionFailure(api, swapHotkeySecond, coldkey, "clone_swap_hotkey_rate_limited"); + await waitForFinalizedBlocks(api, 2); + await waitForRateLimitTransactionWithRetry( + api, + swapHotkeySecond, + coldkey, + "clone_swap_hotkey_after_window" + ); + } + + if (shouldRunPhase(filter, "owner-hparams")) { + log("Behavior: owner-hparams"); + await sudoSetTempo(api, netuidOwnerA, 1); + await sudoSetTempo(api, netuidOwnerB, 1); + await sudoSetGlobalGroupRateLimit(api, GROUP_OWNER_HPARAMS, 2); + const alice = getAlice(); + + const activityCutoffA = await api.query.SubtensorModule.ActivityCutoff.getValue(netuidOwnerA); + const activityCutoffB = await api.query.SubtensorModule.ActivityCutoff.getValue(netuidOwnerB); + const rhoA = await api.query.SubtensorModule.Rho.getValue(netuidOwnerA); + + const cutoffAFirst = asSudoTx( + api, + api.tx.AdminUtils.sudo_set_activity_cutoff({ + netuid: netuidOwnerA, + activity_cutoff: activityCutoffA + 1, + }) + ); + const cutoffASecond = asSudoTx( + api, + api.tx.AdminUtils.sudo_set_activity_cutoff({ + netuid: netuidOwnerA, + activity_cutoff: activityCutoffA + 2, + }) + ); + const rhoACall = asSudoTx( + api, + api.tx.AdminUtils.sudo_set_rho({ + netuid: netuidOwnerA, + rho: rhoA + 1, + }) + ); + const cutoffB = asSudoTx( + api, + api.tx.AdminUtils.sudo_set_activity_cutoff({ + netuid: netuidOwnerB, + activity_cutoff: activityCutoffB + 1, + }) + ); + const burnHalfLifeFirst = asSudoTx( + api, + api.tx.AdminUtils.sudo_set_burn_half_life({ + netuid: netuidOwnerA, + burn_half_life: 361, + }) + ); + const burnHalfLifeSecond = asSudoTx( + api, + api.tx.AdminUtils.sudo_set_burn_half_life({ + netuid: netuidOwnerA, + burn_half_life: 362, + }) + ); + const burnIncreaseMult = asSudoTx( + api, + api.tx.AdminUtils.sudo_set_burn_increase_mult({ + netuid: netuidOwnerA, + burn_increase_mult: 2n, + }) + ); + + const expectedCutoffAAfterFirst = activityCutoffA + 1; + + await waitForRateLimitTransactionWithRetry(api, cutoffAFirst, alice as any, "clone_owner_cutoff_a"); + await waitForFinalizedBlocks(api, 1); + await waitForRateLimitTransactionWithRetry(api, rhoACall, alice as any, "clone_owner_rho_a"); + await waitForFinalizedBlocks(api, 1); + await waitForRateLimitTransactionWithRetry(api, cutoffB, alice as any, "clone_owner_cutoff_b"); + await submitTransactionBestEffort(api, cutoffASecond, alice as any); + await waitForFinalizedBlocks(api, 2); + assert.equal( + await api.query.SubtensorModule.ActivityCutoff.getValue(netuidOwnerA), + expectedCutoffAAfterFirst + ); + await waitForFinalizedBlocks(api, 1); + await waitForRateLimitTransactionWithRetry( + api, + cutoffASecond, + alice as any, + "clone_owner_cutoff_a_after_window" + ); + + await waitForRateLimitTransactionWithRetry( + api, + burnHalfLifeFirst, + alice as any, + "clone_owner_burn_half_life_a" + ); + await expectTransactionFailure( + api, + burnHalfLifeSecond, + alice as any, + "clone_owner_burn_half_life_rate_limited" + ); + await waitForFinalizedBlocks(api, 1); + await waitForRateLimitTransactionWithRetry( + api, + burnIncreaseMult, + alice as any, + "clone_owner_burn_increase_mult_a" + ); + await waitForFinalizedBlocks(api, 2); + await waitForRateLimitTransactionWithRetry( + api, + burnHalfLifeSecond, + alice as any, + "clone_owner_burn_half_life_a_after_window" + ); + } + } finally { + try { + client.destroy(); + } catch {} + } +} diff --git a/scripts/rate-limiting-migration/start-rate-limits-clone-node.sh b/scripts/rate-limiting-migration/start-rate-limits-clone-node.sh new file mode 100755 index 0000000000..34d3a0b9e7 --- /dev/null +++ b/scripts/rate-limiting-migration/start-rate-limits-clone-node.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +BASE_DIR="${CLONE_BASE_DIR:-$REPO_ROOT/target/rate-limits-test}" +CHAIN_SPEC="${PATCHED_CHAIN_SPEC:-$BASE_DIR/patched-finney.json}" +RUN_BASE_PATH="${CLONE_RUN_BASE_PATH:-$BASE_DIR/run/alice}" +BINARY_PATH="${BINARY_PATH:-$REPO_ROOT/target/release/node-subtensor}" +NODE_PORT="${CLONE_NODE_PORT:-30633}" +RPC_PORT="${CLONE_NODE_RPC_PORT:-9964}" + +exec "$BINARY_PATH" \ + --alice \ + --chain "$CHAIN_SPEC" \ + --base-path "$RUN_BASE_PATH" \ + --database paritydb \ + --force-authoring \ + --port "$NODE_PORT" \ + --rpc-port "$RPC_PORT" \ + --rpc-cors=all \ + --rpc-methods=unsafe \ + --unsafe-rpc-external \ + --unsafe-force-node-key-generation \ + --no-telemetry \ + --no-prometheus \ + --validator diff --git a/scripts/rate-limiting-migration/upgrade-rate-limits-clone.sh b/scripts/rate-limiting-migration/upgrade-rate-limits-clone.sh new file mode 100755 index 0000000000..0d8e59cd15 --- /dev/null +++ b/scripts/rate-limiting-migration/upgrade-rate-limits-clone.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$REPO_ROOT/ts-tests" +export NODE_PATH="$PWD/node_modules" +export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=8192" +pnpm exec tsx ../scripts/rate-limiting-migration/upgrade-rate-limits-clone.ts "$@" diff --git a/scripts/rate-limiting-migration/upgrade-rate-limits-clone.ts b/scripts/rate-limiting-migration/upgrade-rate-limits-clone.ts new file mode 100644 index 0000000000..aa102d723e --- /dev/null +++ b/scripts/rate-limiting-migration/upgrade-rate-limits-clone.ts @@ -0,0 +1,16 @@ +import { + installConnectionLogFilter, + upgradeCloneRuntime, + waitForLocalRpc, +} from "./rate-limits-clone-lib.ts"; + +async function main() { + installConnectionLogFilter(); + await waitForLocalRpc(); + await upgradeCloneRuntime(); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/rate-limiting-migration/validate-rate-limits-clone-behavior.sh b/scripts/rate-limiting-migration/validate-rate-limits-clone-behavior.sh new file mode 100755 index 0000000000..e4a1ee9d35 --- /dev/null +++ b/scripts/rate-limiting-migration/validate-rate-limits-clone-behavior.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$REPO_ROOT/ts-tests" +export NODE_PATH="$PWD/node_modules" +export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=8192" +pnpm exec tsx ../scripts/rate-limiting-migration/validate-rate-limits-clone-behavior.ts "$@" diff --git a/scripts/rate-limiting-migration/validate-rate-limits-clone-behavior.ts b/scripts/rate-limiting-migration/validate-rate-limits-clone-behavior.ts new file mode 100644 index 0000000000..e4fc87b4ef --- /dev/null +++ b/scripts/rate-limiting-migration/validate-rate-limits-clone-behavior.ts @@ -0,0 +1,24 @@ +import { + installConnectionLogFilter, + verifyCloneBehavior, + waitForLocalRpc, +} from "./rate-limits-clone-lib.ts"; + +async function main() { + installConnectionLogFilter(); + await waitForLocalRpc(); + const filter = process.argv[2] as + | "serving" + | "staking" + | "delegate-take" + | "weights" + | "swap-keys" + | "owner-hparams" + | undefined; + await verifyCloneBehavior(filter); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/rate-limiting-migration/validate-rate-limits-clone-config.sh b/scripts/rate-limiting-migration/validate-rate-limits-clone-config.sh new file mode 100755 index 0000000000..83d017f077 --- /dev/null +++ b/scripts/rate-limiting-migration/validate-rate-limits-clone-config.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$REPO_ROOT/ts-tests" +export NODE_PATH="$PWD/node_modules" +export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=8192" +pnpm exec tsx ../scripts/rate-limiting-migration/validate-rate-limits-clone-config.ts "$@" diff --git a/scripts/rate-limiting-migration/validate-rate-limits-clone-config.ts b/scripts/rate-limiting-migration/validate-rate-limits-clone-config.ts new file mode 100644 index 0000000000..9d7512d8a1 --- /dev/null +++ b/scripts/rate-limiting-migration/validate-rate-limits-clone-config.ts @@ -0,0 +1,16 @@ +import { + installConnectionLogFilter, + verifyCloneConfig, + waitForLocalRpc, +} from "./rate-limits-clone-lib.ts"; + +async function main() { + installConnectionLogFilter(); + await waitForLocalRpc(); + await verifyCloneConfig(); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/rate-limiting-migration/validate-rate-limits-clone-storage.sh b/scripts/rate-limiting-migration/validate-rate-limits-clone-storage.sh new file mode 100755 index 0000000000..fdf0e94670 --- /dev/null +++ b/scripts/rate-limiting-migration/validate-rate-limits-clone-storage.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$REPO_ROOT/ts-tests" +export NODE_PATH="$PWD/node_modules" +export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=8192" +pnpm exec tsx ../scripts/rate-limiting-migration/validate-rate-limits-clone-storage.ts "$@" diff --git a/scripts/rate-limiting-migration/validate-rate-limits-clone-storage.ts b/scripts/rate-limiting-migration/validate-rate-limits-clone-storage.ts new file mode 100644 index 0000000000..3210f8077f --- /dev/null +++ b/scripts/rate-limiting-migration/validate-rate-limits-clone-storage.ts @@ -0,0 +1,16 @@ +import { + installConnectionLogFilter, + verifyCloneStorageAudit, + waitForLocalRpc, +} from "./rate-limits-clone-lib.ts"; + +async function main() { + installConnectionLogFilter(); + await waitForLocalRpc(); + await verifyCloneStorageAudit(); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/ts-tests/biome.jsonc b/ts-tests/biome.jsonc index 426aca155a..c343732924 100644 --- a/ts-tests/biome.jsonc +++ b/ts-tests/biome.jsonc @@ -17,6 +17,7 @@ "test/tsconfig.json", "tmp", "**/tmp/", + "**/.papi" ] }, "formatter": { diff --git a/ts-tests/moonwall.config.json b/ts-tests/moonwall.config.json index cc5667af9f..5056f163b5 100644 --- a/ts-tests/moonwall.config.json +++ b/ts-tests/moonwall.config.json @@ -62,6 +62,33 @@ "endpoints": ["ws://127.0.0.1:9947"] } ] + }, + { + "name": "zombienet_rate_limiting", + "timeout": 600000, + "testFileDir": ["suites/zombienet_rate_limiting"], + "runScripts": [ + "generate-types.sh", + "build-spec.sh" + ], + "foundation": { + "type": "zombie", + "zombieSpec": { + "configPath": "./configs/zombie_node.json", + "skipBlockCheck": [] + } + }, + "vitestArgs": { + "bail": 1 + }, + "connections": [ + { + "name": "Node", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9947"], + "descriptor": "subtensor" + } + ] }, { "name": "zombienet_shield", "timeout": 600000, diff --git a/ts-tests/scripts/generate-types.sh b/ts-tests/scripts/generate-types.sh index 8c65e561e5..94ff522f4d 100755 --- a/ts-tests/scripts/generate-types.sh +++ b/ts-tests/scripts/generate-types.sh @@ -2,7 +2,7 @@ # # (Re)generate polkadot-api type descriptors using a running node. # Checks that the node binary exists before running. -# Generates types only if they are missing or empty. +# Generates types if they are missing or older than the node binary. # # Usage: # ./generate-types.sh @@ -27,12 +27,19 @@ if [ ! -d "$DESCRIPTORS_DIR" ] || [ -z "$(ls -A "$DESCRIPTORS_DIR" 2>/dev/null)" echo "==> Type descriptors not found or empty, will generate..." GENERATE_TYPES=true else - echo "==> Type descriptors already exist, skipping generation." + BINARY_MTIME=$(stat -f "%m" "$BINARY") + DESCRIPTORS_MTIME=$(find "$DESCRIPTORS_DIR" -type f -exec stat -f "%m" {} \; | sort -nr | head -1) + if [ -z "$DESCRIPTORS_MTIME" ] || [ "$BINARY_MTIME" -gt "$DESCRIPTORS_MTIME" ]; then + echo "==> Node binary is newer than descriptors, will regenerate..." + GENERATE_TYPES=true + else + echo "==> Types are up-to-date, nothing to do." + fi fi if [ "$GENERATE_TYPES" = true ]; then echo "==> Starting dev node (logs at $NODE_LOG)..." - "$BINARY" --one --dev &>"$NODE_LOG" & + "$BINARY" --one --dev --offchain-worker never --no-prometheus &>"$NODE_LOG" & NODE_PID=$! trap "kill $NODE_PID 2>/dev/null; wait $NODE_PID 2>/dev/null || true; exit 0" EXIT @@ -56,6 +63,4 @@ if [ "$GENERATE_TYPES" = true ]; then echo "==> Done generating types." exit 0 -else - echo "==> Types are up-to-date, nothing to do." fi diff --git a/ts-tests/suites/dev/subtensor/rate_limiting/_utils.ts b/ts-tests/suites/dev/subtensor/rate_limiting/_utils.ts new file mode 100644 index 0000000000..ab6eb9b2a3 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/rate_limiting/_utils.ts @@ -0,0 +1,150 @@ +import type { DevModeContext } from "@moonwall/cli"; +import type { KeyringPair } from "@moonwall/util"; +import type { ApiPromise } from "@polkadot/api"; + +export const GROUP_SERVE = 0; +export const GROUP_DELEGATE_TAKE = 1; +export const GROUP_WEIGHTS_SET = 2; +export const GROUP_REGISTER_NETWORK = 3; +export const GROUP_OWNER_HPARAMS = 4; +export const GROUP_STAKING_OPS = 5; +export const GROUP_SWAP_KEYS = 6; + +export type SubmittableLike = { + method: { callIndex: Uint8Array | number[] }; + decodedCall?: unknown; + signAsync: (signer: KeyringPair) => Promise; +}; + +/** Returns the current finalized block number as a plain number. */ +export async function currentBlock(api: ApiPromise): Promise { + const n = await api.query.system.number(); + return (n as unknown as { toNumber: () => number }).toNumber(); +} + +export async function disableAdminFreezeWindow( + api: ApiPromise, + context: DevModeContext, + sudoSigner: KeyringPair +): Promise { + const inner = api.tx.adminUtils.sudoSetAdminFreezeWindow(0); + await context.createBlock([await api.tx.sudo.sudo(inner).signAsync(sudoSigner)]); +} + +/** Reads `(pallet_idx, call_idx)` from a SubmittableExtrinsic. */ +export function callIndex(tx: SubmittableLike): { pallet: number; call: number } { + const idx = tx.method.callIndex as Uint8Array; + return { pallet: Number(idx[0]), call: Number(idx[1]) }; +} + +/** Returns the GroupId assigned to a call (None ⇒ not in any group). */ +export async function callGroupOf(api: ApiPromise, tx: SubmittableLike): Promise { + const { pallet, call } = callIndex(tx); + const value = await api.query.rateLimiting.callGroups({ + palletIndex: pallet, + extrinsicIndex: call, + }); + if (value.isEmpty) return null; + return Number((value as any).toString()); +} + +export async function ensureCallInGroup( + api: ApiPromise, + context: DevModeContext, + sudoSigner: KeyringPair, + tx: SubmittableLike, + groupId: number, + readOnly: boolean = false +): Promise { + const existing = await callGroupOf(api, tx); + if (existing === groupId) return; + if (existing !== null && existing !== groupId) { + throw new Error( + `call already registered in group ${existing}, expected ${groupId} (cannot reassign without remove_call_from_group)` + ); + } + if (!readOnly) { + // Fast path: register_call also assigns the group and forces read_only=false. + const register = api.tx.rateLimiting.registerCall(tx, groupId); + await context.createBlock([await api.tx.sudo.sudo(register).signAsync(sudoSigner)]); + } else { + // Two-step: register without group (so we control the read_only flag below), then assign. + const register = api.tx.rateLimiting.registerCall(tx, null); + await context.createBlock([await api.tx.sudo.sudo(register).signAsync(sudoSigner)]); + const { pallet, call } = callIndex(tx); + const assign = api.tx.rateLimiting.assignCallToGroup( + { palletIndex: pallet, extrinsicIndex: call }, + groupId, + true + ); + await context.createBlock([await api.tx.sudo.sudo(assign).signAsync(sudoSigner)]); + } + const after = await callGroupOf(api, tx); + if (after !== groupId) { + throw new Error(`failed to register call into group ${groupId} (still ${after})`); + } +} + +export async function setGroupSpan( + api: ApiPromise, + context: DevModeContext, + sudoSigner: KeyringPair, + groupId: number, + scope: number | null, + span: number +): Promise { + const inner = api.tx.rateLimiting.setRateLimit({ Group: groupId }, scope === null ? null : scope, { Exact: span }); + const wrapped = api.tx.sudo.sudo(inner); + await context.createBlock([await wrapped.signAsync(sudoSigner)]); +} + +export async function expectExtrinsicOk( + api: ApiPromise, + context: DevModeContext, + signedTx: any, + label: string +): Promise { + const before = await currentBlock(api); + await context.createBlock([signedTx]); + const events = (await api.query.system.events()) as any; + const failed = events.find((e: any) => e.event.method === "ExtrinsicFailed"); + if (failed) { + const err = JSON.stringify(failed.event.data?.toHuman?.() ?? failed.event.toHuman()); + throw new Error(`[${label}] expected success but ExtrinsicFailed: ${err}`); + } + const after = await currentBlock(api); + if (after === before) { + throw new Error(`[${label}] expected success but no block was produced`); + } +} + +export async function expectRateLimited( + api: ApiPromise, + context: DevModeContext, + signedTx: any, + label: string +): Promise { + const before = await currentBlock(api); + try { + await context.createBlock([signedTx]); + } catch (e: any) { + const msg = String(e?.message ?? e); + if (/Custom.*1|RATE_LIMIT|rate.*limit/i.test(msg)) return; + throw new Error(`[${label}] expected rate-limit rejection but got: ${msg}`); + } + const after = await currentBlock(api); + if (after > before) { + // Block was produced; check if the tx made it in (success) or was simply excluded. + const events = (await api.query.system.events()) as any; + const success = events.find((e: any) => e.event.method === "ExtrinsicSuccess"); + const failed = events.find((e: any) => e.event.method === "ExtrinsicFailed"); + if (success) { + throw new Error(`[${label}] expected rate-limit rejection but tx succeeded`); + } + if (failed) { + const err = JSON.stringify(failed.event.data?.toHuman?.() ?? failed.event.toHuman()); + // Dispatch-level failure isn't a rate-limit rejection (which lives at validate). + throw new Error(`[${label}] expected rate-limit rejection but got dispatch failure: ${err}`); + } + } +} diff --git a/ts-tests/suites/dev/subtensor/rate_limiting/test-group-delegate-take.ts b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-delegate-take.ts new file mode 100644 index 0000000000..921ee77bdb --- /dev/null +++ b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-delegate-take.ts @@ -0,0 +1,91 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { KeyringPair } from "@moonwall/util"; +import type { ApiPromise } from "@polkadot/api"; +import { + GROUP_DELEGATE_TAKE, + ensureCallInGroup, + expectExtrinsicOk, + expectRateLimited, + setGroupSpan, +} from "./_utils.ts"; +import { seal } from "../../../../utils/dev_utils.ts"; + +describeSuite({ + id: "DEV_SUB_RL_DELEGATE_TAKE_01", + title: "Rate-limiting: GROUP_DELEGATE_TAKE", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let bob: KeyringPair; + + beforeAll(() => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + bob = context.keyring.bob; + }); + + it({ + id: "T01", + title: "increase_take rejected inside window; allowed after; decrease_take bypasses enforcement", + test: async () => { + const SPAN = 5; + + // Establish bob as alice's hotkey via register_network + addStake. + const registerTx = polkadotJs.tx.subtensorModule.registerNetwork(bob.address); + await context.createBlock([await registerTx.signAsync(alice)]); + const added = ((await polkadotJs.query.system.events()) as any).find( + (e: any) => e.event.method === "NetworkAdded" + ); + expect(added).to.not.be.undefined; + + // `increase_take` itself creates the Delegates entry on first call — no separate + // `become_delegate` extrinsic exists anymore. + + // Wire calls into the group and reduce span. + const sampleInc = polkadotJs.tx.subtensorModule.increaseTake(bob.address, 100); + const sampleDec = polkadotJs.tx.subtensorModule.decreaseTake(bob.address, 1); + await ensureCallInGroup(polkadotJs, context, alice, sampleInc, GROUP_DELEGATE_TAKE); + await ensureCallInGroup(polkadotJs, context, alice, sampleDec, GROUP_DELEGATE_TAKE); + await setGroupSpan(polkadotJs, context, alice, GROUP_DELEGATE_TAKE, null, SPAN); + + const current = (await polkadotJs.query.subtensorModule.delegates(bob.address)) as any; + const baseTake = Number(current?.toString?.() ?? 0); + if (baseTake === 0) { + throw new Error("expected Delegates[bob] to have a positive default take"); + } + + const dec = polkadotJs.tx.subtensorModule.decreaseTake(bob.address, baseTake - 1); + await expectExtrinsicOk(polkadotJs, context, await dec.signAsync(alice), "decrease_take_arms_window"); + + // 1. Immediate increase back to baseTake → blocked (LastSeen just written). + const earlyInc = polkadotJs.tx.subtensorModule.increaseTake(bob.address, baseTake); + await expectRateLimited( + polkadotJs, + context, + await earlyInc.signAsync(alice), + "increase_take_in_window" + ); + + // 2. Another decrease — `bypass_and_record` for `new <= current` → success despite window. + const decAgain = polkadotJs.tx.subtensorModule.decreaseTake(bob.address, baseTake - 2); + await expectExtrinsicOk( + polkadotJs, + context, + await decAgain.signAsync(alice), + "decrease_take_bypasses_window" + ); + + // 3. Age out and increase → allowed. + await seal(context, SPAN); + const afterInc = polkadotJs.tx.subtensorModule.increaseTake(bob.address, baseTake - 1); + await expectExtrinsicOk( + polkadotJs, + context, + await afterInc.signAsync(alice), + "increase_take_after_window" + ); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/rate_limiting/test-group-owner-hparams.ts b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-owner-hparams.ts new file mode 100644 index 0000000000..6cb971bce3 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-owner-hparams.ts @@ -0,0 +1,95 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { KeyringPair } from "@moonwall/util"; +import type { ApiPromise } from "@polkadot/api"; +import { + GROUP_OWNER_HPARAMS, + ensureCallInGroup, + expectExtrinsicOk, + expectRateLimited, + setGroupSpan, +} from "./_utils.ts"; +import { seal } from "../../../../utils/dev_utils.ts"; + +describeSuite({ + id: "DEV_SUB_RL_OWNER_HPARAMS_01", + title: "Rate-limiting: GROUP_OWNER_HPARAMS", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let bob: KeyringPair; + let netuid: number; + + beforeAll(() => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + bob = context.keyring.bob; + }); + + it({ + id: "T01", + title: "ConfigOnly: shared limit, per-transaction usage; tempo-scaled span via adjust_span", + test: async () => { + // Setup: alice as subnet owner; tempo=1 so adjust_span keeps the effective span small. + const registerTx = polkadotJs.tx.subtensorModule.registerNetwork(bob.address); + await context.createBlock([await registerTx.signAsync(alice)]); + const added = ((await polkadotJs.query.system.events()) as any).find( + (e: any) => e.event.method === "NetworkAdded" + ); + expect(added).to.not.be.undefined; + netuid = Number((added as any).event.data[0].toString()); + + const setTempo = polkadotJs.tx.adminUtils.sudoSetTempo(netuid, 1); + await context.createBlock([await polkadotJs.tx.sudo.sudo(setTempo).signAsync(alice)]); + + // adjust_span multiplies the configured span by Tempo(netuid). With tempo=1 the + // effective span equals the stored value. + const SPAN_RAW = 5; + const sampleKappa = polkadotJs.tx.adminUtils.sudoSetKappa(netuid, 32_768); + const sampleRho = polkadotJs.tx.adminUtils.sudoSetRho(netuid, 30); + await ensureCallInGroup(polkadotJs, context, alice, sampleKappa, GROUP_OWNER_HPARAMS); + await ensureCallInGroup(polkadotJs, context, alice, sampleRho, GROUP_OWNER_HPARAMS); + await setGroupSpan(polkadotJs, context, alice, GROUP_OWNER_HPARAMS, null, SPAN_RAW); + + // 1. sudo wrap: root bypasses, so we go through alice as subnet owner (regular + // signed origin) which IS rate-limited via the admin-window rule. + const kappa1 = polkadotJs.tx.adminUtils.sudoSetKappa(netuid, 32_768); + await expectExtrinsicOk( + polkadotJs, + context, + await polkadotJs.tx.sudo.sudo(kappa1).signAsync(alice), + "set_kappa_initial" + ); + + // 2. Repeating kappa in the same window → blocked (usage is per transaction). + const kappa2 = polkadotJs.tx.adminUtils.sudoSetKappa(netuid, 30_000); + await expectRateLimited( + polkadotJs, + context, + await polkadotJs.tx.sudo.sudo(kappa2).signAsync(alice), + "set_kappa_in_window" + ); + + // 3. ConfigOnly: a DIFFERENT hparam call has its own LastSeen → allowed even though + // the group's config is shared. + const rho = polkadotJs.tx.adminUtils.sudoSetRho(netuid, 31); + await expectExtrinsicOk( + polkadotJs, + context, + await polkadotJs.tx.sudo.sudo(rho).signAsync(alice), + "set_rho_separate_transaction" + ); + + // 4. After window expires → kappa allowed again. + await seal(context, SPAN_RAW); + const kappa3 = polkadotJs.tx.adminUtils.sudoSetKappa(netuid, 28_000); + await expectExtrinsicOk( + polkadotJs, + context, + await polkadotJs.tx.sudo.sudo(kappa3).signAsync(alice), + "set_kappa_after_window" + ); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/rate_limiting/test-group-register-network.ts b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-register-network.ts new file mode 100644 index 0000000000..730fc79371 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-register-network.ts @@ -0,0 +1,62 @@ +import { beforeAll, describeSuite } from "@moonwall/cli"; +import type { KeyringPair } from "@moonwall/util"; +import type { ApiPromise } from "@polkadot/api"; +import { + GROUP_REGISTER_NETWORK, + ensureCallInGroup, + expectExtrinsicOk, + expectRateLimited, + setGroupSpan, +} from "./_utils.ts"; +import { seal } from "../../../../utils/dev_utils.ts"; + +describeSuite({ + id: "DEV_SUB_RL_REGISTER_NETWORK_01", + title: "Rate-limiting: GROUP_REGISTER_NETWORK", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + + beforeAll(() => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + }); + + it({ + id: "T01", + title: "register_network and register_network_with_identity share a global window", + test: async () => { + const SPAN = 5; + const sample = polkadotJs.tx.subtensorModule.registerNetwork(alice.address); + const sampleWithIdentity = polkadotJs.tx.subtensorModule.registerNetworkWithIdentity( + alice.address, + null + ); + + // Bring chain into post-migration state for these two calls. + await ensureCallInGroup(polkadotJs, context, alice, sample, GROUP_REGISTER_NETWORK); + await ensureCallInGroup(polkadotJs, context, alice, sampleWithIdentity, GROUP_REGISTER_NETWORK); + await setGroupSpan(polkadotJs, context, alice, GROUP_REGISTER_NETWORK, null, SPAN); + + // 1. First registration succeeds → LastSeen written. + const first = polkadotJs.tx.subtensorModule.registerNetwork(alice.address); + await expectExtrinsicOk(polkadotJs, context, await first.signAsync(alice), "first_register"); + + // 2. Sibling call in the same group is rejected immediately (delta < span). + const sibling = polkadotJs.tx.subtensorModule.registerNetworkWithIdentity(alice.address, null); + await expectRateLimited(polkadotJs, context, await sibling.signAsync(alice), "sibling_in_window"); + + // 3. Age out window: seal `span - 1` empty blocks, still inside → rejected. + await seal(context, SPAN - 2); + const stillBlocked = polkadotJs.tx.subtensorModule.registerNetwork(alice.address); + await expectRateLimited(polkadotJs, context, await stillBlocked.signAsync(alice), "still_in_window"); + + // 4. One more empty block → delta == span → allowed. + await seal(context, 1); + const afterWindow = polkadotJs.tx.subtensorModule.registerNetwork(alice.address); + await expectExtrinsicOk(polkadotJs, context, await afterWindow.signAsync(alice), "after_window"); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/rate_limiting/test-group-serve.ts b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-serve.ts new file mode 100644 index 0000000000..e2faecdb3a --- /dev/null +++ b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-serve.ts @@ -0,0 +1,121 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { KeyringPair } from "@moonwall/util"; +import type { ApiPromise } from "@polkadot/api"; +import { + GROUP_SERVE, + disableAdminFreezeWindow, + ensureCallInGroup, + expectExtrinsicOk, + expectRateLimited, + setGroupSpan, +} from "./_utils.ts"; +import { seal } from "../../../../utils/dev_utils.ts"; + +describeSuite({ + id: "DEV_SUB_RL_SERVE_01", + title: "Rate-limiting: GROUP_SERVE", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let bob: KeyringPair; + let netuid: number; + + beforeAll(() => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + bob = context.keyring.bob; + }); + + it({ + id: "T01", + title: "serve_axon and serve_axon_tls share the Axon usage key; serve_prometheus separate", + test: async () => { + const SPAN = 5; + // GROUP_SERVE uses `RootOrSubnetOwnerAdminWindow` rule for limit-setting — needs an + // open admin window even under sudo. + await disableAdminFreezeWindow(polkadotJs, context, alice); + + // Subnet with bob registered as a neuron (required by `serve_axon` etc.). + const registerTx = polkadotJs.tx.subtensorModule.registerNetwork(bob.address); + await context.createBlock([await registerTx.signAsync(alice)]); + const added = ((await polkadotJs.query.system.events()) as any).find( + (e: any) => e.event.method === "NetworkAdded" + ); + expect(added).to.not.be.undefined; + netuid = Number((added as any).event.data[0].toString()); + + // Register bob as a neuron on this subnet. + const burnedReg = polkadotJs.tx.subtensorModule.burnedRegister(netuid, bob.address); + await context.createBlock([await burnedReg.signAsync(alice)]); + + // Wire serve calls into the group and reshape span. + const sampleAxon = polkadotJs.tx.subtensorModule.serveAxon(netuid, 1, 0, 3030, 4, 0, 0, 0); + const sampleAxonTls = polkadotJs.tx.subtensorModule.serveAxonTls( + netuid, + 1, + 0, + 3030, + 4, + 0, + 0, + 0, + "0x" + "07".repeat(130) + ); + const samplePrometheus = polkadotJs.tx.subtensorModule.servePrometheus(netuid, 1, 1, 3031, 4); + await ensureCallInGroup(polkadotJs, context, alice, sampleAxon, GROUP_SERVE); + await ensureCallInGroup(polkadotJs, context, alice, sampleAxonTls, GROUP_SERVE); + await ensureCallInGroup(polkadotJs, context, alice, samplePrometheus, GROUP_SERVE); + await setGroupSpan(polkadotJs, context, alice, GROUP_SERVE, netuid, SPAN); + + // 1. serve_axon succeeds → records usage at AccountSubnetServing{Axon}. + const axon1 = polkadotJs.tx.subtensorModule.serveAxon(netuid, 1, 0, 3030, 4, 0, 0, 0); + await expectExtrinsicOk(polkadotJs, context, await axon1.signAsync(bob), "serve_axon_initial"); + + // 2. serve_axon_tls shares the Axon usage key → blocked inside window. + const tlsEarly = polkadotJs.tx.subtensorModule.serveAxonTls( + netuid, + 1, + 0, + 3030, + 4, + 0, + 0, + 0, + "0x" + "07".repeat(130) + ); + await expectRateLimited(polkadotJs, context, await tlsEarly.signAsync(bob), "serve_axon_tls_in_window"); + + // 3. serve_prometheus uses a SEPARATE usage key (endpoint=Prometheus) → allowed + // even though the group's config is shared. + const prom = polkadotJs.tx.subtensorModule.servePrometheus(netuid, 1, 1, 3031, 4); + await expectExtrinsicOk( + polkadotJs, + context, + await prom.signAsync(bob), + "serve_prometheus_separate_key" + ); + + // 4. After window expires → axon_tls allowed. + await seal(context, SPAN); + const tlsAfter = polkadotJs.tx.subtensorModule.serveAxonTls( + netuid, + 1, + 0, + 3030, + 4, + 0, + 0, + 0, + "0x" + "07".repeat(130) + ); + await expectExtrinsicOk( + polkadotJs, + context, + await tlsAfter.signAsync(bob), + "serve_axon_tls_after_window" + ); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/rate_limiting/test-group-staking-ops.ts b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-staking-ops.ts new file mode 100644 index 0000000000..5c8148bc49 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-staking-ops.ts @@ -0,0 +1,115 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { KeyringPair } from "@moonwall/util"; +import type { ApiPromise } from "@polkadot/api"; +import { + GROUP_STAKING_OPS, + disableAdminFreezeWindow, + ensureCallInGroup, + expectExtrinsicOk, + expectRateLimited, + setGroupSpan, +} from "./_utils.ts"; +import { seal } from "../../../../utils/dev_utils.ts"; + +describeSuite({ + id: "DEV_SUB_RL_STAKING_OPS_01", + title: "Rate-limiting: GROUP_STAKING_OPS", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let bob: KeyringPair; + let netuid: number; + + beforeAll(() => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + bob = context.keyring.bob; + }); + + it({ + id: "T01", + title: "add_stake records usage; remove_stake blocked while inside window", + test: async () => { + const SPAN = 5; + + // Subnet hparam toggles require an open admin window. + await disableAdminFreezeWindow(polkadotJs, context, alice); + + // 1. Create a fresh subnet owned by alice, with bob as hotkey. + const registerTx = polkadotJs.tx.subtensorModule.registerNetwork(bob.address); + await context.createBlock([await registerTx.signAsync(alice)]); + const evs = (await polkadotJs.query.system.events()) as any; + const added = evs.find((e: any) => e.event.method === "NetworkAdded"); + expect(added).to.not.be.undefined; + netuid = Number((added as any).event.data[0].toString()); + + // 2. Enable subtoken for staking. + const enableTx = polkadotJs.tx.adminUtils.sudoSetSubtokenEnabled(netuid, true); + await context.createBlock([await polkadotJs.tx.sudo.sudo(enableTx).signAsync(alice)]); + + // 3. Wire staking calls into the group, then override span for fast tests. + const sampleAdd = polkadotJs.tx.subtensorModule.addStake(bob.address, netuid, 1_000_000_000); + const sampleRemove = polkadotJs.tx.subtensorModule.removeStake(bob.address, netuid, 100_000_000); + await ensureCallInGroup(polkadotJs, context, alice, sampleAdd, GROUP_STAKING_OPS); + // remove_stake is read-only in the production migration (`migrations/rate_limiting.rs`). + await ensureCallInGroup(polkadotJs, context, alice, sampleRemove, GROUP_STAKING_OPS, true); + await setGroupSpan(polkadotJs, context, alice, GROUP_STAKING_OPS, null, SPAN); + + // 4. Successful add_stake → writes LastSeen (add_stake is NOT read-only). + const addTx = polkadotJs.tx.subtensorModule.addStake(bob.address, netuid, 1_000_000_000_000); + await expectExtrinsicOk(polkadotJs, context, await addTx.signAsync(alice), "add_stake_initial"); + + // 5. Immediate remove_stake on the same (coldkey, hotkey, netuid) → rate-limited. + const earlyRemove = polkadotJs.tx.subtensorModule.removeStake(bob.address, netuid, 100_000_000); + await expectRateLimited( + polkadotJs, + context, + await earlyRemove.signAsync(alice), + "remove_stake_in_window" + ); + + // 6. Seal `span - 2` more empty blocks → still inside the window. + await seal(context, SPAN - 2); + const stillBlocked = polkadotJs.tx.subtensorModule.removeStake(bob.address, netuid, 100_000_000); + await expectRateLimited( + polkadotJs, + context, + await stillBlocked.signAsync(alice), + "remove_stake_just_before_window" + ); + + // 7. One more empty block → delta == span → allowed. + await seal(context, 1); + const afterWindow = polkadotJs.tx.subtensorModule.removeStake(bob.address, netuid, 100_000_000); + await expectExtrinsicOk( + polkadotJs, + context, + await afterWindow.signAsync(alice), + "remove_stake_after_window" + ); + }, + }); + + it({ + id: "T02", + title: "remove_stake is read-only: success does NOT reset the group window", + test: async () => { + const SPAN = 5; + await setGroupSpan(polkadotJs, context, alice, GROUP_STAKING_OPS, null, SPAN); + + // The previous test left the chain past the window with a successful remove_stake. + // remove_stake is marked `read_only` → it does NOT write LastSeen, so a subsequent + // add_stake/remove_stake should NOT be locked out by it. Seal one block then try. + await seal(context, 1); + const removeAgain = polkadotJs.tx.subtensorModule.removeStake(bob.address, netuid, 100_000_000); + await expectExtrinsicOk( + polkadotJs, + context, + await removeAgain.signAsync(alice), + "remove_stake_read_only_does_not_extend" + ); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/rate_limiting/test-group-swap-keys.ts b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-swap-keys.ts new file mode 100644 index 0000000000..36672331fb --- /dev/null +++ b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-swap-keys.ts @@ -0,0 +1,75 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { KeyringPair } from "@moonwall/util"; +import type { ApiPromise } from "@polkadot/api"; +import { + GROUP_SWAP_KEYS, + disableAdminFreezeWindow, + ensureCallInGroup, + expectExtrinsicOk, + expectRateLimited, + setGroupSpan, +} from "./_utils.ts"; +import { seal } from "../../../../utils/dev_utils.ts"; + +describeSuite({ + id: "DEV_SUB_RL_SWAP_KEYS_01", + title: "Rate-limiting: GROUP_SWAP_KEYS", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let bob: KeyringPair; + let charlie: KeyringPair; + let dave: KeyringPair; + + beforeAll(() => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + bob = context.keyring.bob; + charlie = context.keyring.charlie; + dave = (context.keyring as any).dave ?? context.keyring.charlie; + }); + + it({ + id: "T01", + title: "swap_hotkey within window is rejected; allowed after the window expires", + test: async () => { + const SPAN = 5; + await disableAdminFreezeWindow(polkadotJs, context, alice); + + // Establish OwnedHotkeys[(alice, bob)] by registering a subnet with bob as the + // owner hotkey, then opt bob in via add_stake. + const registerTx = polkadotJs.tx.subtensorModule.registerNetwork(bob.address); + await context.createBlock([await registerTx.signAsync(alice)]); + const evs = (await polkadotJs.query.system.events()) as any; + const added = evs.find((e: any) => e.event.method === "NetworkAdded"); + expect(added).to.not.be.undefined; + const netuid = Number((added as any).event.data[0].toString()); + + const enableTx = polkadotJs.tx.adminUtils.sudoSetSubtokenEnabled(netuid, true); + await context.createBlock([await polkadotJs.tx.sudo.sudo(enableTx).signAsync(alice)]); + + const stakeTx = polkadotJs.tx.subtensorModule.addStake(bob.address, netuid, 1_000_000_000_000); + await context.createBlock([await stakeTx.signAsync(alice)]); + + // Wire swap calls into the group and reduce span for fast tests. + const sample = polkadotJs.tx.subtensorModule.swapHotkey(bob.address, charlie.address, null); + await ensureCallInGroup(polkadotJs, context, alice, sample, GROUP_SWAP_KEYS); + await setGroupSpan(polkadotJs, context, alice, GROUP_SWAP_KEYS, null, SPAN); + + // 1. Successful swap → records LastSeen for usage key Account(alice). + const first = polkadotJs.tx.subtensorModule.swapHotkey(bob.address, charlie.address, null); + await expectExtrinsicOk(polkadotJs, context, await first.signAsync(alice), "swap_hotkey_initial"); + + // 2. Same coldkey, immediate swap → blocked. + const early = polkadotJs.tx.subtensorModule.swapHotkey(charlie.address, dave.address, null); + await expectRateLimited(polkadotJs, context, await early.signAsync(alice), "swap_hotkey_in_window"); + + // 3. Age out: seal `span - 1` blocks → allowed. + await seal(context, SPAN - 1); + const after = polkadotJs.tx.subtensorModule.swapHotkey(charlie.address, dave.address, null); + await expectExtrinsicOk(polkadotJs, context, await after.signAsync(alice), "swap_hotkey_after_window"); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/rate_limiting/test-group-weights-set.ts b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-weights-set.ts new file mode 100644 index 0000000000..40eb3f470a --- /dev/null +++ b/ts-tests/suites/dev/subtensor/rate_limiting/test-group-weights-set.ts @@ -0,0 +1,86 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { KeyringPair } from "@moonwall/util"; +import type { ApiPromise } from "@polkadot/api"; +import { + GROUP_WEIGHTS_SET, + disableAdminFreezeWindow, + ensureCallInGroup, + expectExtrinsicOk, + expectRateLimited, + setGroupSpan, +} from "./_utils.ts"; +import { seal } from "../../../../utils/dev_utils.ts"; + +describeSuite({ + id: "DEV_SUB_RL_WEIGHTS_SET_01", + title: "Rate-limiting: GROUP_WEIGHTS_SET", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let bob: KeyringPair; + let netuid: number; + + beforeAll(() => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + bob = context.keyring.bob; + }); + + it({ + id: "T01", + title: "set_weights and commit_weights share the per-netuid weights window", + test: async () => { + const SPAN = 5; + await disableAdminFreezeWindow(polkadotJs, context, alice); + + // Subnet setup: alice owns the subnet, bob is the validator hotkey. + const registerNet = polkadotJs.tx.subtensorModule.registerNetwork(bob.address); + await context.createBlock([await registerNet.signAsync(alice)]); + const added = ((await polkadotJs.query.system.events()) as any).find( + (e: any) => e.event.method === "NetworkAdded" + ); + expect(added).to.not.be.undefined; + netuid = Number((added as any).event.data[0].toString()); + + // Make weight-setting permissive on this dev subnet. + const tweaks = [ + polkadotJs.tx.adminUtils.sudoSetStakeThreshold(0), + polkadotJs.tx.adminUtils.sudoSetMinAllowedWeights(netuid, 0), + polkadotJs.tx.adminUtils.sudoSetTempo(netuid, 1), + polkadotJs.tx.adminUtils.sudoSetCommitRevealWeightsEnabled(netuid, false), + ]; + for (const t of tweaks) { + await context.createBlock([await polkadotJs.tx.sudo.sudo(t).signAsync(alice)]); + } + + // Register bob as a neuron on this subnet so set_weights is dispatchable. + const burnedReg = polkadotJs.tx.subtensorModule.burnedRegister(netuid, bob.address); + await context.createBlock([await burnedReg.signAsync(alice)]); + + // Wire weight calls into the group and reshape span for fast tests. + const sampleSet = polkadotJs.tx.subtensorModule.setWeights(netuid, [0], [1], 0); + const sampleCommit = polkadotJs.tx.subtensorModule.commitWeights( + netuid, + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + await ensureCallInGroup(polkadotJs, context, alice, sampleSet, GROUP_WEIGHTS_SET); + await ensureCallInGroup(polkadotJs, context, alice, sampleCommit, GROUP_WEIGHTS_SET); + await setGroupSpan(polkadotJs, context, alice, GROUP_WEIGHTS_SET, netuid, SPAN); + + // 1. Initial set_weights → succeeds, records usage by neuron. + const first = polkadotJs.tx.subtensorModule.setWeights(netuid, [0], [1], 0); + await expectExtrinsicOk(polkadotJs, context, await first.signAsync(bob), "set_weights_initial"); + + // 2. Immediate set_weights again → blocked. + const early = polkadotJs.tx.subtensorModule.setWeights(netuid, [0], [1], 0); + await expectRateLimited(polkadotJs, context, await early.signAsync(bob), "set_weights_in_window"); + + // 3. Age out → allowed. + await seal(context, SPAN); + const after = polkadotJs.tx.subtensorModule.setWeights(netuid, [0], [1], 0); + await expectExtrinsicOk(polkadotJs, context, await after.signAsync(bob), "set_weights_after_window"); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/staking/test-add-staking.ts b/ts-tests/suites/dev/subtensor/staking/test-add-staking.ts index 8d8e13cfb6..ce8610c91f 100644 --- a/ts-tests/suites/dev/subtensor/staking/test-add-staking.ts +++ b/ts-tests/suites/dev/subtensor/staking/test-add-staking.ts @@ -2,6 +2,9 @@ import { beforeAll, describeSuite, expect } from "@moonwall/cli"; import type { ApiPromise } from "@polkadot/api"; import type { KeyringPair } from "@moonwall/util"; import { BN } from "@polkadot/util"; +import { disableAdminFreezeWindow } from "../rate_limiting/_utils.ts"; +import { generateKeyringPair } from "../../../../utils"; +import { seal } from "../../../../utils/dev_utils.ts"; describeSuite({ id: "DEV_SUB_STAKING_ADD_STAKING_01", @@ -12,25 +15,29 @@ describeSuite({ let netuid1: number; let alice: KeyringPair; - let bob: KeyringPair; + let snOwner: KeyringPair; beforeAll(() => { polkadotJs = context.polkadotJs(); alice = context.keyring.alice; - bob = context.keyring.bob; + snOwner = generateKeyringPair("sr25519"); }); it({ id: "T01", title: "Add stake payable", test: async () => { - const alice = context.keyring.alice; - const bob = context.keyring.bob; - const appFees = new BN(100_000); + await context.createBlock([ + await polkadotJs.tx.sudo + .sudo(polkadotJs.tx.balances.forceSetBalance(snOwner.address, 10_000_000_000)) + .signAsync(alice), + ]); + + await disableAdminFreezeWindow(polkadotJs, context, alice); // Register network - let tx = polkadotJs.tx.subtensorModule.registerNetwork(bob.address); + let tx = polkadotJs.tx.subtensorModule.registerNetwork(snOwner.address); await context.createBlock([await tx.signAsync(alice)]); let events = await polkadotJs.query.system.events(); @@ -45,7 +52,7 @@ describeSuite({ await context.createBlock([await polkadotJs.tx.sudo.sudo(tx1).signAsync(alice)]); // Adding stake - tx = polkadotJs.tx.subtensorModule.addStake(bob.address, netuid1, 1000_000_000); + tx = polkadotJs.tx.subtensorModule.addStake(snOwner.address, netuid1, 1000_000_000); await context.createBlock([await tx.signAsync(alice)]); events = await polkadotJs.query.system.events(); @@ -61,8 +68,11 @@ describeSuite({ id: "T02", title: "Remove stake payable", test: async () => { + // We need enough blocks to bypass rate limit + await seal(context, 10); + // Removing stake - const tx = polkadotJs.tx.subtensorModule.removeStake(bob.address, netuid1, 500_000_000); + const tx = polkadotJs.tx.subtensorModule.removeStake(snOwner.address, netuid1, 500_000_000); await context.createBlock([await tx.signAsync(alice)]); const events = await polkadotJs.query.system.events(); diff --git a/ts-tests/suites/zombienet_rate_limiting/00-config.test.ts b/ts-tests/suites/zombienet_rate_limiting/00-config.test.ts new file mode 100644 index 0000000000..6cbdba78c7 --- /dev/null +++ b/ts-tests/suites/zombienet_rate_limiting/00-config.test.ts @@ -0,0 +1,53 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import { subtensor } from "@polkadot-api/descriptors"; +import type { TypedApi } from "polkadot-api"; +import { generateKeyringPair } from "../../utils"; +import { + createRateLimitGroup, + getCallRateLimit, + getGroupedResponseGroupId, + getRateLimitConfig, + groupSharingConfigAndUsage, + isGlobalConfig, + registerCallsInGroup, + setGlobalGroupRateLimit, +} from "../../utils/rate_limiting.ts"; + +describeSuite({ + id: "00_config", + title: "Rate-limits RPC smoke", + foundationMethods: "zombie", + testCases: ({ it, context }) => { + let api: TypedApi; + let client: any; + + beforeAll(async () => { + client = context.papi("Node"); + api = client.getTypedApi(subtensor); + }); + + it({ + id: "T01", + title: "Reports explicit grouped setup created by admin extrinsics", + test: async () => { + const hotkey = generateKeyringPair("sr25519").address; + const newHotkey = generateKeyringPair("sr25519").address; + + const groupId = await createRateLimitGroup(api, "rl-smoke-config", groupSharingConfigAndUsage()); + const swapHotkey = api.tx.SubtensorModule.swap_hotkey({ + hotkey, + new_hotkey: newHotkey, + netuid: undefined, + }); + + await registerCallsInGroup(api, groupId, [swapHotkey], "register_smoke_config_calls"); + await setGlobalGroupRateLimit(api, groupId, 3); + + const response = await getCallRateLimit(client, "SubtensorModule", "swap_hotkey"); + expect(response).toBeDefined(); + expect(getGroupedResponseGroupId(response)).toBe(groupId); + expect(isGlobalConfig(getRateLimitConfig(response))).toBe(true); + }, + }); + }, +}); diff --git a/ts-tests/suites/zombienet_rate_limiting/01-serving.test.ts b/ts-tests/suites/zombienet_rate_limiting/01-serving.test.ts new file mode 100644 index 0000000000..a41cb12f0a --- /dev/null +++ b/ts-tests/suites/zombienet_rate_limiting/01-serving.test.ts @@ -0,0 +1,93 @@ +import { beforeAll, describeSuite } from "@moonwall/cli"; +import { Binary, type TypedApi } from "polkadot-api"; +import { subtensor } from "@polkadot-api/descriptors"; +import { waitForFinalizedBlocks } from "../../utils"; +import { + createRateLimitGroup, + createRootHotkeyContext, + expectTransactionFailure, + groupSharingConfigAndUsage, + registerCallsInGroup, + setScopedGroupRateLimit, + waitForRateLimitTransactionWithRetry, +} from "../../utils/rate_limiting.ts"; + +describeSuite({ + id: "01_serving", + title: "Serving rate-limits", + foundationMethods: "zombie", + testCases: ({ it, context }) => { + let api: TypedApi; + + beforeAll(async () => { + api = context.papi("Node").getTypedApi(subtensor); + }); + + it({ + id: "T01", + title: "Shares usage between axon variants and keeps prometheus separate", + test: async () => { + const ctx = await createRootHotkeyContext(api); + + const serveAxon = api.tx.SubtensorModule.serve_axon({ + netuid: 0, + version: 1, + ip: 0n, + port: 3030, + ip_type: 4, + protocol: 0, + placeholder1: 0, + placeholder2: 0, + }); + const serveAxonTls = api.tx.SubtensorModule.serve_axon_tls({ + netuid: 0, + version: 1, + ip: 0n, + port: 3030, + ip_type: 4, + protocol: 0, + placeholder1: 0, + placeholder2: 0, + certificate: Binary.fromBytes(new Uint8Array([1, 2, 3, 4])), + }); + const servePrometheus = api.tx.SubtensorModule.serve_prometheus({ + netuid: 0, + version: 1, + ip: 1_676_056_785n, + port: 3031, + ip_type: 4, + }); + + const groupId = await createRateLimitGroup(api, "rl-smoke-serving", groupSharingConfigAndUsage()); + await registerCallsInGroup( + api, + groupId, + [serveAxon, serveAxonTls, servePrometheus], + "register_smoke_serving_calls" + ); + await setScopedGroupRateLimit(api, groupId, 0, 10); + + await waitForRateLimitTransactionWithRetry(api, serveAxon, ctx.hotkey, "serve_axon_initial"); + await waitForFinalizedBlocks(api, 1); + await expectTransactionFailure(api, serveAxonTls, ctx.hotkey, "serve_axon_tls_rate_limited"); + + await waitForRateLimitTransactionWithRetry( + api, + servePrometheus, + ctx.hotkey, + "serve_prometheus_initial" + ); + await waitForFinalizedBlocks(api, 1); + await expectTransactionFailure(api, servePrometheus, ctx.hotkey, "serve_prometheus_rate_limited"); + + await waitForFinalizedBlocks(api, 1); + await waitForRateLimitTransactionWithRetry( + api, + serveAxonTls, + ctx.hotkey, + "serve_axon_tls_after_window" + ); + }, + }); + }, +}); diff --git a/ts-tests/suites/zombienet_rate_limiting/02-staking-and-delegate.test.ts b/ts-tests/suites/zombienet_rate_limiting/02-staking-and-delegate.test.ts new file mode 100644 index 0000000000..4a38fd6ba9 --- /dev/null +++ b/ts-tests/suites/zombienet_rate_limiting/02-staking-and-delegate.test.ts @@ -0,0 +1,86 @@ +import { beforeAll, describeSuite } from "@moonwall/cli"; +import { subtensor } from "@polkadot-api/descriptors"; +import type { TypedApi } from "polkadot-api"; +import { addStake, sudoSetLockReductionInterval, tao, waitForBlocks } from "../../utils"; +import { + createRateLimitGroup, + createOwnedSubnetContext, + expectTransactionFailure, + getStakeValueForRateLimit, + groupSharingConfigAndUsage, + registerCallsInGroup, + setGlobalGroupRateLimit, + waitForRateLimitTransactionWithRetry, +} from "../../utils/rate_limiting.ts"; + +describeSuite({ + id: "02_staking_and_delegate", + title: "Staking rate-limits", + foundationMethods: "zombie", + testCases: ({ it, context }) => { + let api: TypedApi; + + beforeAll(async () => { + api = context.papi("Node").getTypedApi(subtensor); + }); + + it({ + id: "T01", + title: "Blocks remove_stake immediately after add_stake via shared staking bucket", + test: async () => { + const rateLimitWindow = 10; + await sudoSetLockReductionInterval(api, 1); + const { coldkey, coldkeyAddress, hotkeyAddress, netuid } = await createOwnedSubnetContext(api); + + const addStakeTx = api.tx.SubtensorModule.add_stake({ + hotkey: hotkeyAddress, + netuid, + amount_staked: tao(100), + }); + const removeStakeTemplate = api.tx.SubtensorModule.remove_stake({ + hotkey: hotkeyAddress, + netuid, + amount_unstaked: 1n, + }); + + const groupId = await createRateLimitGroup(api, "rl-smoke-staking", groupSharingConfigAndUsage()); + await registerCallsInGroup( + api, + groupId, + [addStakeTx, removeStakeTemplate], + "register_smoke_staking_calls" + ); + await setGlobalGroupRateLimit(api, groupId, rateLimitWindow); + + await addStake(api, coldkey, hotkeyAddress, netuid, tao(200)); + + const stakeBeforeRemove = await getStakeValueForRateLimit(api, hotkeyAddress, coldkeyAddress, netuid); + const removeStake = api.tx.SubtensorModule.remove_stake({ + hotkey: hotkeyAddress, + netuid, + amount_unstaked: stakeBeforeRemove, + }); + + await expectTransactionFailure(api, removeStake, coldkey, "remove_stake_rate_limited"); + await waitForBlocks(api, rateLimitWindow); + const stakeAfterFailedAttempt = await getStakeValueForRateLimit( + api, + hotkeyAddress, + coldkeyAddress, + netuid + ); + const removeStakeAfterWindow = api.tx.SubtensorModule.remove_stake({ + hotkey: hotkeyAddress, + netuid, + amount_unstaked: tao(100), + }); + await waitForRateLimitTransactionWithRetry( + api, + removeStakeAfterWindow, + coldkey, + "remove_stake_after_window" + ); + }, + }); + }, +}); diff --git a/ts-tests/suites/zombienet_rate_limiting/03-owner-hparams.test.ts b/ts-tests/suites/zombienet_rate_limiting/03-owner-hparams.test.ts new file mode 100644 index 0000000000..6f77470186 --- /dev/null +++ b/ts-tests/suites/zombienet_rate_limiting/03-owner-hparams.test.ts @@ -0,0 +1,116 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import { subtensor } from "@polkadot-api/descriptors"; +import type { TypedApi } from "polkadot-api"; +import { generateKeyringPair, sudoSetAdminFreezeWindow, sudoSetTempo, waitForFinalizedBlocks } from "../../utils"; +import { + addNewSubnetworkForRateLimit, + createRateLimitGroup, + forceSetBalancesForRateLimit, + groupSharingConfigOnly, + registerCallsInGroup, + setGlobalGroupRateLimit, + submitTransactionBestEffort, + startCallForRateLimit, + waitForRateLimitTransactionWithRetry, +} from "../../utils/rate_limiting.ts"; + +describeSuite({ + id: "03_owner_hparams", + title: "Owner hparams rate-limits", + foundationMethods: "zombie", + testCases: ({ it, context }) => { + let api: TypedApi; + + beforeAll(async () => { + api = context.papi("Node").getTypedApi(subtensor); + }); + + it({ + id: "T01", + title: "Shares config, keeps usage per hyperparameter, and scopes by netuid", + test: async () => { + const coldkey = generateKeyringPair("sr25519"); + const hotkeyA = generateKeyringPair("sr25519"); + const hotkeyB = generateKeyringPair("sr25519"); + + await forceSetBalancesForRateLimit(api, [coldkey.address, hotkeyA.address, hotkeyB.address]); + + const netuidA = await addNewSubnetworkForRateLimit(api, hotkeyA, coldkey); + await startCallForRateLimit(api, netuidA, coldkey); + const netuidB = await addNewSubnetworkForRateLimit(api, hotkeyB, coldkey); + await startCallForRateLimit(api, netuidB, coldkey); + + await sudoSetAdminFreezeWindow(api, 0); + await sudoSetTempo(api, netuidA, 1); + await sudoSetTempo(api, netuidB, 1); + + const groupId = await createRateLimitGroup(api, "rl-smoke-owner-hparams", groupSharingConfigOnly()); + const cutoffTemplate = api.tx.AdminUtils.sudo_set_activity_cutoff({ + netuid: netuidA, + activity_cutoff: 1, + }); + const rhoTemplate = api.tx.AdminUtils.sudo_set_rho({ + netuid: netuidA, + rho: 1, + }); + const burnHalfLifeTemplate = api.tx.AdminUtils.sudo_set_burn_half_life({ + netuid: netuidA, + burn_half_life: 1, + }); + await registerCallsInGroup( + api, + groupId, + [cutoffTemplate, rhoTemplate, burnHalfLifeTemplate], + "register_smoke_owner_hparams_calls" + ); + await setGlobalGroupRateLimit(api, groupId, 2); + + const currentCutoffA = await api.query.SubtensorModule.ActivityCutoff.getValue(netuidA); + const currentCutoffB = await api.query.SubtensorModule.ActivityCutoff.getValue(netuidB); + const currentRhoA = await api.query.SubtensorModule.Rho.getValue(netuidA); + const cutoffAFirst = api.tx.AdminUtils.sudo_set_activity_cutoff({ + netuid: netuidA, + activity_cutoff: currentCutoffA + 1, + }); + const cutoffASecond = api.tx.AdminUtils.sudo_set_activity_cutoff({ + netuid: netuidA, + activity_cutoff: currentCutoffA + 2, + }); + const rhoA = api.tx.AdminUtils.sudo_set_rho({ + netuid: netuidA, + rho: currentRhoA + 1, + }); + const burnHalfLifeA = api.tx.AdminUtils.sudo_set_burn_half_life({ + netuid: netuidA, + burn_half_life: 361, + }); + const cutoffB = api.tx.AdminUtils.sudo_set_activity_cutoff({ + netuid: netuidB, + activity_cutoff: currentCutoffB + 1, + }); + const expectedCutoffAAfterFirst = currentCutoffA + 1; + + await waitForRateLimitTransactionWithRetry(api, cutoffAFirst, coldkey, "owner_cutoff_a_initial"); + await waitForFinalizedBlocks(api, 1); + await waitForRateLimitTransactionWithRetry(api, rhoA, coldkey, "owner_rho_a_initial"); + await waitForFinalizedBlocks(api, 1); + await waitForRateLimitTransactionWithRetry( + api, + burnHalfLifeA, + coldkey, + "owner_burn_half_life_a_initial" + ); + await waitForFinalizedBlocks(api, 1); + await waitForRateLimitTransactionWithRetry(api, cutoffB, coldkey, "owner_cutoff_b_allowed"); + await submitTransactionBestEffort(api, cutoffASecond, coldkey); + await waitForFinalizedBlocks(api, 2); + expect(await api.query.SubtensorModule.ActivityCutoff.getValue(netuidA)).toBe( + expectedCutoffAAfterFirst + ); + + await waitForFinalizedBlocks(api, 1); + await waitForRateLimitTransactionWithRetry(api, cutoffASecond, coldkey, "owner_cutoff_a_after"); + }, + }); + }, +}); diff --git a/ts-tests/utils/dev_utils.ts b/ts-tests/utils/dev_utils.ts new file mode 100644 index 0000000000..d690c6c2a9 --- /dev/null +++ b/ts-tests/utils/dev_utils.ts @@ -0,0 +1,6 @@ +/** Seals `count` empty blocks via manual seal — used to age out a rate-limit window. */ +export async function seal(context: DevModeContext, count: number): Promise { + for (let i = 0; i < count; i++) { + await context.createBlock(); + } +} diff --git a/ts-tests/utils/rate_limiting.ts b/ts-tests/utils/rate_limiting.ts new file mode 100644 index 0000000000..069f6c3d6b --- /dev/null +++ b/ts-tests/utils/rate_limiting.ts @@ -0,0 +1,674 @@ +import { Binary, Enum, type TypedApi } from "polkadot-api"; +import { getPolkadotSigner } from "polkadot-api/signer"; +import { MultiAddress, type subtensor } from "@polkadot-api/descriptors"; +import type { KeyringPair } from "@moonwall/util"; +import { Keyring } from "@polkadot/keyring"; +import { waitForBlocks } from "./staking.js"; +import { waitForFinalizedBlocks } from "./transactions.js"; +import { generateKeyringPair } from "./account.ts"; +import { startCall } from "./subnet.js"; +import { waitForTransactionWithRetry } from "./transactions.js"; + +export const groupSharingConfigAndUsage = () => Enum("ConfigAndUsage"); +export const groupSharingConfigOnly = () => Enum("ConfigOnly"); + +type RpcCapableClient = { + _request(method: string, params: unknown[]): Promise; +}; + +export const rateLimitTargetGroup = (groupId: number) => Enum("Group", groupId); + +export const rateLimitKindExact = (limit: bigint | number) => + Enum("Exact", typeof limit === "bigint" ? Number(limit) : limit); + +const TX_TIMEOUT = 30_000; + +type SafeFloatLike = { + mantissa: bigint; + exponent: bigint; +}; + +function toRational(value: SafeFloatLike): { numerator: bigint; denominator: bigint } { + if (value.exponent >= 0n) { + return { + numerator: value.mantissa * 10n ** value.exponent, + denominator: 1n, + }; + } + + return { + numerator: value.mantissa, + denominator: 10n ** -value.exponent, + }; +} + +export async function getStakeValueForRateLimit( + api: TypedApi, + hotkey: string, + coldkey: string, + netuid: number +): Promise { + const totalHotkeyAlpha = await api.query.SubtensorModule.TotalHotkeyAlpha.getValue(hotkey, netuid); + if (totalHotkeyAlpha === 0n) { + return 0n; + } + + const currentShare = (await api.query.SubtensorModule.AlphaV2.getValue(hotkey, coldkey, netuid)) as SafeFloatLike; + const denominator = (await api.query.SubtensorModule.TotalHotkeySharesV2.getValue(hotkey, netuid)) as SafeFloatLike; + + const share = toRational(currentShare); + const total = toRational(denominator); + + if (share.numerator === 0n || total.numerator === 0n) { + return 0n; + } + + return (totalHotkeyAlpha * share.numerator * total.denominator) / (share.denominator * total.numerator); +} + +async function waitForFinalizedBlockAdvance(api: TypedApi, count = 1): Promise { + await waitForFinalizedBlocks(api, count); +} + +async function waitForSudoTransactionWithRetry( + api: TypedApi, + tx: any, + signer: KeyringPair, + label: string, + maxRetries = 1 +): Promise { + let retries = 0; + + while (retries < maxRetries) { + try { + await waitForSudoTransactionCompletion(api, tx, signer, label); + return; + } catch (error) { + retries += 1; + if (retries >= maxRetries) { + throw new Error(`[${label}] failed after ${maxRetries} retries`); + } + await waitForBlocks(api, 1); + } + } +} + +async function waitForSudoTransactionCompletion( + api: TypedApi, + tx: any, + keypair: KeyringPair, + label: string +): Promise { + const signer = getPolkadotSigner(keypair.publicKey, "Sr25519", keypair.sign); + const account = await api.query.System.Account.getValue(keypair.address, { at: "best" }); + + return new Promise((resolve, reject) => { + let timeoutId: ReturnType; + const subscription = tx + .signSubmitAndWatch(signer, { + at: "best", + nonce: account.nonce, + }) + .subscribe({ + next: async (event: any) => { + if (event.type === "txBestBlocksState" && event.found) { + subscription.unsubscribe(); + + if (!event.ok) { + reject(new Error(`[${label}] dispatch error: ${JSON.stringify(event.dispatchError)}`)); + return; + } + + try { + const events = await api.query.System.Events.getValue({ at: event.block.hash }); + const sudoEvent = events.find( + (record: any) => + record.phase?.type === "ApplyExtrinsic" && + record.phase.value === event.block.index && + record.event?.type === "Sudo" && + record.event?.value?.type === "Sudid" + ) as any; + + const sudoResult = sudoEvent?.event?.value?.value?.sudo_result; + if (sudoResult?.success === false) { + reject(new Error(`[${label}] sudo error: ${JSON.stringify(sudoResult.value)}`)); + return; + } + + clearTimeout(timeoutId); + resolve(); + } catch (error) { + clearTimeout(timeoutId); + reject(error instanceof Error ? error : new Error(String(error))); + } + + return; + } + + if (event.type === "txBestBlocksState" && event.isValid === false) { + subscription.unsubscribe(); + clearTimeout(timeoutId); + reject(new Error(`[${label}] transaction rejected before inclusion`)); + } + }, + error: (error: unknown) => { + subscription.unsubscribe(); + clearTimeout(timeoutId); + reject(error instanceof Error ? error : new Error(String(error))); + }, + }); + + timeoutId = setTimeout(() => { + subscription.unsubscribe(); + reject(new Error(`[${label}] timeout`)); + }, TX_TIMEOUT); + }); +} + +export async function waitForRateLimitTransactionWithRetry( + api: TypedApi, + tx: any, + signer: KeyringPair, + label: string, + maxRetries = 1 +): Promise { + let retries = 0; + let lastError: unknown; + + while (retries < maxRetries) { + try { + await waitForRateLimitTransactionCompletion(api, tx, signer, TX_TIMEOUT, label); + return; + } catch (error) { + lastError = error; + retries += 1; + if (retries >= maxRetries) { + const suffix = error instanceof Error ? `: ${error.message}` : `: ${String(error)}`; + throw new Error(`[${label}] failed after ${maxRetries} retries${suffix}`); + } + await waitForBlocks(api, 1); + } + } + + if (lastError instanceof Error) { + throw lastError; + } +} + +async function waitForRateLimitTransactionCompletion( + api: TypedApi, + tx: any, + keypair: KeyringPair, + timeout: number | null = TX_TIMEOUT, + label?: string +): Promise<{ txHash: string; blockHash: string }> { + const signer = getPolkadotSigner(keypair.publicKey, "Sr25519", keypair.sign); + const account = await api.query.System.Account.getValue(keypair.address, { at: "best" }); + const seenEvents: string[] = []; + + const signSubmitAndWatchInner = (): Promise<{ txHash: string; blockHash: string }> => + new Promise((resolve, reject) => { + const subscription = tx + .signSubmitAndWatch(signer, { + at: "best", + nonce: account.nonce, + }) + .subscribe({ + next(event: any) { + const eventSummary = + event.type === "txBestBlocksState" + ? `${event.type}:${event.found ? "found" : "nofound"}:${event.isValid === false ? "invalid" : "valid"}` + : event.type; + seenEvents.push(eventSummary); + + if (event.type === "txBestBlocksState" && event.found) { + subscription.unsubscribe(); + if (event.dispatchError) { + reject(new Error(`ExtrinsicFailed: ${JSON.stringify(event.dispatchError)}`)); + } else { + resolve({ + txHash: event.txHash, + blockHash: event.block.hash, + }); + } + } else if (event.type === "txBestBlocksState" && event.isValid === false) { + subscription.unsubscribe(); + reject(new Error("Transaction rejected before inclusion")); + } + }, + error(err: unknown) { + reject(err instanceof Error ? err : new Error(String(err))); + }, + }); + }); + + if (timeout === null) { + return signSubmitAndWatchInner(); + } + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + const prefix = label ? `[${label}] ` : ""; + reject(new Error(`${prefix}Transaction timed out; seen events: ${seenEvents.join(", ") || "none"}`)); + }, timeout); + signSubmitAndWatchInner() + .then((result) => { + clearTimeout(timer); + resolve(result); + }) + .catch((error) => { + clearTimeout(timer); + reject(error instanceof Error ? error : new Error(String(error))); + }); + }); +} + +export async function forceSetBalancesForRateLimit( + api: TypedApi, + ss58Addresses: string[], + amount: bigint = 10000000000000000000n +): Promise { + const keyring = new Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + const calls = ss58Addresses.map( + (ss58Address) => + api.tx.Balances.force_set_balance({ + who: MultiAddress.Id(ss58Address), + new_free: amount, + }).decodedCall + ); + const batch = api.tx.Utility.force_batch({ calls }); + const tx = api.tx.Sudo.sudo({ call: batch.decodedCall }); + await waitForSudoTransactionWithRetry(api, tx, alice, "force_set_balance"); +} + +export async function addNewSubnetworkForRateLimit( + api: TypedApi, + hotkey: KeyringPair, + coldkey: KeyringPair +): Promise { + const keyring = new Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + const totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue(); + + const target = Enum("Group", 3); + const limits = (await api.query.RateLimiting.Limits.getValue(target as never)) as any; + const rateLimit = + limits?.type === "Global" && limits.value?.type === "Exact" ? BigInt(limits.value.value) : BigInt(0); + + if (rateLimit !== BigInt(0)) { + const internalCall = api.tx.RateLimiting.set_rate_limit({ + target: target as never, + scope: undefined, + limit: Enum("Exact", 0) as never, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForSudoTransactionWithRetry(api, tx, alice, "set_register_network_rate_limit"); + await waitForFinalizedBlockAdvance(api); + } + + const registerNetworkTx = api.tx.SubtensorModule.register_network({ + hotkey: hotkey.address, + }); + await waitForTransactionWithRetry(api, registerNetworkTx, coldkey, "register_network"); + + return totalNetworks; +} + +export async function startCallForRateLimit( + api: TypedApi, + netuid: number, + coldkey: KeyringPair +): Promise { + const existingFirstEmission = await api.query.SubtensorModule.FirstEmissionBlockNumber.getValue(netuid); + if (existingFirstEmission !== undefined) { + return; + } + try { + await startCall(api, netuid, coldkey); + } catch (error) { + if (error instanceof Error && error.message.includes("FirstEmissionBlockNumberAlreadySet")) { + return; + } + throw error; + } +} + +async function waitForGroupAtFinalized( + api: TypedApi, + groupId: number, + timeoutMs = 30_000 +): Promise { + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + const group = await api.query.RateLimiting.Groups.getValue(groupId, { at: "finalized" }); + if (group !== undefined) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 1_000)); + } + + throw new Error(`Timed out waiting for group ${groupId} at finalized`); +} + +export async function createRateLimitGroup( + api: TypedApi, + name: string, + sharing: any +): Promise { + const keyring = new Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + const groupId = await api.query.RateLimiting.NextGroupId.getValue(); + const internalCall = api.tx.RateLimiting.create_group({ + name: Binary.fromText(name), + sharing: sharing as never, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForSudoTransactionWithRetry(api, tx, alice, `create_group_${name}`); + await waitForGroupAtFinalized(api, groupId); + return groupId; +} + +export async function registerCallsInGroup( + api: TypedApi, + groupId: number, + calls: { decodedCall: unknown }[], + label: string +): Promise { + const keyring = new Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + const internalCalls = calls.map( + (call) => + api.tx.RateLimiting.register_call({ + call: call.decodedCall as never, + group: groupId, + }).decodedCall + ); + const batch = api.tx.Utility.batch_all({ calls: internalCalls }); + const tx = api.tx.Sudo.sudo({ call: batch.decodedCall }); + await waitForSudoTransactionWithRetry(api, tx, alice, label); + await waitForFinalizedBlockAdvance(api); +} + +export async function setGlobalGroupRateLimit( + api: TypedApi, + groupId: number, + limit: bigint | number +): Promise { + const target = rateLimitTargetGroup(groupId); + const current = await api.query.RateLimiting.Limits.getValue(target as never); + const currentValue = + current && (current as any).type === "Global" && (current as any).value?.type === "Exact" + ? BigInt((current as any).value.value) + : undefined; + const nextValue = BigInt(limit); + if (currentValue === nextValue) { + return; + } + + const keyring = new Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + const internalCall = api.tx.RateLimiting.set_rate_limit({ + target: target as never, + scope: undefined, + limit: rateLimitKindExact(limit) as never, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForSudoTransactionWithRetry(api, tx, alice, `set_group_rate_limit_${groupId}`); + await waitForFinalizedBlockAdvance(api); +} + +export async function setScopedGroupRateLimit( + api: TypedApi, + groupId: number, + scope: number, + limit: bigint | number +): Promise { + const target = rateLimitTargetGroup(groupId); + const current = await api.query.RateLimiting.Limits.getValue(target as never); + const entries = current && (current as any).type === "Scoped" ? Array.from((current as any).value as any[]) : []; + const existing = entries.find((entry: any) => Number(entry[0]) === scope); + const currentValue = existing ? BigInt(existing[1].value) : undefined; + const nextValue = BigInt(limit); + if (currentValue === nextValue) { + return; + } + + const keyring = new Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + const internalCall = api.tx.RateLimiting.set_rate_limit({ + target: target as never, + scope, + limit: rateLimitKindExact(limit) as never, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForSudoTransactionWithRetry(api, tx, alice, `set_scoped_group_rate_limit_${groupId}`); + await waitForFinalizedBlockAdvance(api); +} + +export async function getCallRateLimit(client: RpcCapableClient, pallet: string, extrinsic: string): Promise { + const encoder = new TextEncoder(); + return client._request("rateLimiting_getRateLimit", [ + Array.from(encoder.encode(pallet)), + Array.from(encoder.encode(extrinsic)), + null, + ]); +} + +export function getGroupedResponseGroupId(response: any): number | undefined { + if (response && typeof response === "object" && "group_id" in response) { + return Number((response as any).group_id); + } + if (response?.type === "grouped" || response?.type === "Grouped") { + return Number(response.value?.group_id); + } + if (response && typeof response === "object" && "Grouped" in response) { + return Number((response as any).Grouped.group_id); + } + if (response && typeof response === "object") { + const [key, value] = Object.entries(response)[0] ?? []; + if (typeof key === "string" && key.toLowerCase() === "grouped") { + return Number((value as any)?.group_id); + } + } + return undefined; +} + +export function getRateLimitConfig(response: any): any { + if (response && typeof response === "object") { + if ("group_id" in response && "limit" in response) { + return (response as any).limit; + } + if ( + response.type === "grouped" || + response.type === "standalone" || + response.type === "Grouped" || + response.type === "Standalone" + ) { + return response.value?.limit; + } + if ("Grouped" in response) { + return (response as any).Grouped.limit; + } + if ("Standalone" in response) { + return (response as any).Standalone.limit; + } + const [key, value] = Object.entries(response)[0] ?? []; + if (typeof key === "string" && (key.toLowerCase() === "grouped" || key.toLowerCase() === "standalone")) { + return (value as any)?.limit; + } + } + return undefined; +} + +export function isScopedConfig(config: any): boolean { + return Boolean(config && ((typeof config === "object" && "Scoped" in config) || config.type === "Scoped")); +} + +export function isGlobalConfig(config: any): boolean { + return Boolean(config && ((typeof config === "object" && "Global" in config) || config.type === "Global")); +} + +export async function rootRegister( + api: TypedApi, + coldkey: KeyringPair, + hotkeyAddress: string +): Promise { + const tx = api.tx.SubtensorModule.root_register({ hotkey: hotkeyAddress }); + await waitForRateLimitTransactionWithRetry(api, tx, coldkey, "root_register"); +} + +export type RootHotkeyContext = { + coldkey: KeyringPair; + hotkey: KeyringPair; + coldkeyAddress: string; + hotkeyAddress: string; +}; + +export async function createRootHotkeyContext(api: TypedApi): Promise { + const coldkey = generateKeyringPair("sr25519"); + const hotkey = generateKeyringPair("sr25519"); + const coldkeyAddress = coldkey.address; + const hotkeyAddress = hotkey.address; + + await forceSetBalancesForRateLimit(api, [coldkeyAddress, hotkeyAddress]); + await rootRegister(api, coldkey, hotkeyAddress); + + return { coldkey, hotkey, coldkeyAddress, hotkeyAddress }; +} + +export type OwnedSubnetContext = { + coldkey: KeyringPair; + hotkey: KeyringPair; + coldkeyAddress: string; + hotkeyAddress: string; + netuid: number; +}; + +export async function createOwnedSubnetContext(api: TypedApi): Promise { + const coldkey = generateKeyringPair("sr25519"); + const hotkey = generateKeyringPair("sr25519"); + const coldkeyAddress = coldkey.address; + const hotkeyAddress = hotkey.address; + + await forceSetBalancesForRateLimit(api, [coldkeyAddress, hotkeyAddress]); + + const netuid = await addNewSubnetworkForRateLimit(api, hotkey, coldkey); + await startCallForRateLimit(api, netuid, coldkey); + + return { coldkey, hotkey, coldkeyAddress, hotkeyAddress, netuid }; +} + +export async function expectTransactionFailure( + api: TypedApi, + tx: any, + keypair: KeyringPair, + label: string, + timeoutMs = 20_000 +): Promise { + const signer = getPolkadotSigner(keypair.publicKey, "Sr25519", keypair.sign); + return new Promise((resolve, reject) => { + let settled = false; + let timeoutId: ReturnType; + const seenEvents: string[] = []; + + const finish = (cb: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timeoutId); + cb(); + }; + + let subscription: { unsubscribe(): void } | undefined; + + void api.query.System.Account.getValue(keypair.address, { at: "best" }) + .then((account) => { + subscription = tx + .signSubmitAndWatch(signer, { + at: "best", + nonce: account.nonce, + }) + .subscribe({ + next(value: any) { + const eventSummary = + value.type === "txBestBlocksState" + ? `${value.type}:${value.found ? "found" : "nofound"}:${value.isValid === false ? "invalid" : "valid"}` + : value.type; + seenEvents.push(eventSummary); + + if (value.type === "txBestBlocksState" && value.found) { + subscription?.unsubscribe(); + if (value.ok) { + finish(() => + reject(new Error(`[${label}] succeeded unexpectedly with tx ${value.txHash}`)) + ); + } else { + finish(() => resolve(value.dispatchError)); + } + } else if (value.type === "txBestBlocksState" && value.isValid === false) { + subscription?.unsubscribe(); + finish(() => resolve(new Error(`[${label}] transaction rejected before inclusion`))); + } + }, + error(error: unknown) { + subscription?.unsubscribe(); + finish(() => resolve(error)); + }, + }); + }) + .catch((error: unknown) => { + finish(() => resolve(error)); + }); + + timeoutId = setTimeout(() => { + subscription?.unsubscribe(); + finish(() => + reject( + new Error( + `[${label}] timed out waiting for failure; seen events: ${seenEvents.join(", ") || "none"}` + ) + ) + ); + }, timeoutMs); + }); +} + +export async function submitTransactionBestEffort( + api: TypedApi, + tx: any, + keypair: KeyringPair +): Promise { + const signer = getPolkadotSigner(keypair.publicKey, "Sr25519", keypair.sign); + const account = await api.query.System.Account.getValue(keypair.address, { at: "best" }); + + await new Promise((resolve, reject) => { + let settled = false; + let subscription: { unsubscribe(): void } | undefined; + + const finish = (cb: () => void) => { + if (settled) return; + settled = true; + subscription?.unsubscribe(); + cb(); + }; + + subscription = tx + .signSubmitAndWatch(signer, { + at: "best", + nonce: account.nonce, + }) + .subscribe({ + next(value: any) { + if (value.type === "broadcasted" || value.type === "txBestBlocksState") { + finish(resolve); + } else if (value.type === "error") { + finish(() => reject(value.error)); + } + }, + error(error: unknown) { + finish(() => reject(error instanceof Error ? error : new Error(String(error)))); + }, + }); + }); +} diff --git a/ts-tests/utils/subnet.ts b/ts-tests/utils/subnet.ts index b45f4a7934..f551a6373d 100644 --- a/ts-tests/utils/subnet.ts +++ b/ts-tests/utils/subnet.ts @@ -10,18 +10,8 @@ export async function addNewSubnetwork( hotkey: KeyringPair, coldkey: KeyringPair ): Promise { - const keyring = new Keyring({ type: "sr25519" }); - const alice = keyring.addFromUri("//Alice"); const totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue(); - // Disable network rate limit for testing - const rateLimit = await api.query.SubtensorModule.NetworkRateLimit.getValue(); - if (rateLimit !== BigInt(0)) { - const internalCall = api.tx.AdminUtils.sudo_set_network_rate_limit({ rate_limit: BigInt(0) }); - const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); - await waitForTransactionWithRetry(api, tx, alice, "set_network_rate_limit"); - } - const registerNetworkTx = api.tx.SubtensorModule.register_network({ hotkey: hotkey.address, });