diff --git a/evm-tests/README.md b/evm-tests/README.md index 83dc8f326f..ed3782e0f7 100644 --- a/evm-tests/README.md +++ b/evm-tests/README.md @@ -13,6 +13,12 @@ between runtime and precompile contracts. ## polkadot api +You need `polkadot-api` globally installed: + +```bash +$ npm i -g polkadot-api +``` + To get the metadata, you need start the localnet via run `./scripts/localnet.sh`. then run following command to get metadata, a folder name .papi will be created, which include the metadata and type definitions. diff --git a/evm-tests/test/neuron.precompile.reveal-weights.test.ts b/evm-tests/test/neuron.precompile.reveal-weights.test.ts index 4ac63468db..52ddc91967 100644 --- a/evm-tests/test/neuron.precompile.reveal-weights.test.ts +++ b/evm-tests/test/neuron.precompile.reveal-weights.test.ts @@ -1,5 +1,5 @@ import * as assert from "assert"; -import { getAliceSigner, getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { getAliceSigner, getDevnetApi, getRandomSubstrateKeypair, waitForTransactionWithRetry } from "../src/substrate" import { devnet } from "@polkadot-api/descriptors" import { PolkadotSigner, TypedApi } from "polkadot-api"; import { convertPublicKeyToSs58, convertH160ToSS58 } from "../src/address-utils" @@ -70,6 +70,20 @@ describe("Test neuron precompile reveal weights", () => { await startCall(api, netuid, coldkey) console.log("test the case on subnet ", netuid) + // Disable admin freeze window and owner hyperparam rate limiting for tests + { + const alice = getAliceSigner() + + // Set AdminFreezeWindow to 0 + const setFreezeWindow = api.tx.AdminUtils.sudo_set_admin_freeze_window({ window: 0 }) + const sudoFreezeTx = api.tx.Sudo.sudo({ call: setFreezeWindow.decodedCall }) + await waitForTransactionWithRetry(api, sudoFreezeTx, alice) + + // Set OwnerHyperparamRateLimit to 0 + const setOwnerRateLimit = api.tx.AdminUtils.sudo_set_owner_hparam_rate_limit({ limit: BigInt(0) }) + const sudoOwnerRateTx = api.tx.Sudo.sudo({ call: setOwnerRateLimit.decodedCall }) + await waitForTransactionWithRetry(api, sudoOwnerRateTx, alice) + } await setWeightsSetRateLimit(api, netuid, BigInt(0)) @@ -164,4 +178,4 @@ describe("Test neuron precompile reveal weights", () => { assert.ok(weight[1] !== undefined) } }) -}); \ No newline at end of file +}); diff --git a/evm-tests/test/neuron.precompile.set-weights.test.ts b/evm-tests/test/neuron.precompile.set-weights.test.ts index 1c9f62e773..4ecc0b36db 100644 --- a/evm-tests/test/neuron.precompile.set-weights.test.ts +++ b/evm-tests/test/neuron.precompile.set-weights.test.ts @@ -1,6 +1,6 @@ import * as assert from "assert"; -import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { getAliceSigner, getDevnetApi, getRandomSubstrateKeypair, waitForTransactionWithRetry } from "../src/substrate" import { devnet } from "@polkadot-api/descriptors" import { TypedApi } from "polkadot-api"; import { convertH160ToSS58, convertPublicKeyToSs58, } from "../src/address-utils" @@ -38,6 +38,20 @@ describe("Test neuron precompile contract, set weights function", () => { await burnedRegister(api, netuid, convertH160ToSS58(wallet.address), coldkey) const uid = await api.query.SubtensorModule.Uids.getValue(netuid, convertH160ToSS58(wallet.address)) assert.notEqual(uid, undefined) + // Disable admin freeze window and owner hyperparam rate limiting for tests + { + const alice = getAliceSigner() + + // Set AdminFreezeWindow to 0 + const setFreezeWindow = api.tx.AdminUtils.sudo_set_admin_freeze_window({ window: 0 }) + const sudoFreezeTx = api.tx.Sudo.sudo({ call: setFreezeWindow.decodedCall }) + await waitForTransactionWithRetry(api, sudoFreezeTx, alice) + + // Set OwnerHyperparamRateLimit to 0 + const setOwnerRateLimit = api.tx.AdminUtils.sudo_set_owner_hparam_rate_limit({ limit: BigInt(0) }) + const sudoOwnerRateTx = api.tx.Sudo.sudo({ call: setOwnerRateLimit.decodedCall }) + await waitForTransactionWithRetry(api, sudoOwnerRateTx, alice) + } // disable reveal and enable direct set weights await setCommitRevealWeightsEnabled(api, netuid, false) await setWeightsSetRateLimit(api, netuid, BigInt(0)) @@ -68,4 +82,4 @@ describe("Test neuron precompile contract, set weights function", () => { }); } }) -}); \ No newline at end of file +}); diff --git a/evm-tests/test/staking.precompile.reward.test.ts b/evm-tests/test/staking.precompile.reward.test.ts index 79ad977515..108e0ed88c 100644 --- a/evm-tests/test/staking.precompile.reward.test.ts +++ b/evm-tests/test/staking.precompile.reward.test.ts @@ -1,5 +1,5 @@ import * as assert from "assert"; -import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { getAliceSigner, getDevnetApi, getRandomSubstrateKeypair, waitForTransactionWithRetry } from "../src/substrate" import { devnet } from "@polkadot-api/descriptors" import { TypedApi } from "polkadot-api"; import { convertPublicKeyToSs58 } from "../src/address-utils" @@ -39,6 +39,20 @@ describe("Test neuron precompile reward", () => { await startCall(api, netuid, coldkey) console.log("test the case on subnet ", netuid) + // Disable admin freeze window and owner hyperparam rate limiting for tests + { + const alice = getAliceSigner() + + // Set AdminFreezeWindow to 0 + const setFreezeWindow = api.tx.AdminUtils.sudo_set_admin_freeze_window({ window: 0 }) + const sudoFreezeTx = api.tx.Sudo.sudo({ call: setFreezeWindow.decodedCall }) + await waitForTransactionWithRetry(api, sudoFreezeTx, alice) + + // Set OwnerHyperparamRateLimit to 0 + const setOwnerRateLimit = api.tx.AdminUtils.sudo_set_owner_hparam_rate_limit({ limit: BigInt(0) }) + const sudoOwnerRateTx = api.tx.Sudo.sudo({ call: setOwnerRateLimit.decodedCall }) + await waitForTransactionWithRetry(api, sudoOwnerRateTx, alice) + } await setTxRateLimit(api, BigInt(0)) await setTempo(api, root_netuid, root_tempo) diff --git a/evm-tests/test/staking.precompile.stake-get.test.ts b/evm-tests/test/staking.precompile.stake-get.test.ts index d9cc79aeab..4730e310d9 100644 --- a/evm-tests/test/staking.precompile.stake-get.test.ts +++ b/evm-tests/test/staking.precompile.stake-get.test.ts @@ -45,7 +45,7 @@ describe("Test staking precompile get methods", () => { await contract.getStake(hotkey.publicKey, coldkey.publicKey, netuid) ); - // validator returned as bigint now. + // validator returned as bigint now. const validators = await contract.getAlphaStakedValidators(hotkey.publicKey, netuid) diff --git a/evm-tests/test/subnet.precompile.hyperparameter.test.ts b/evm-tests/test/subnet.precompile.hyperparameter.test.ts index b8a6f19075..5d81049d41 100644 --- a/evm-tests/test/subnet.precompile.hyperparameter.test.ts +++ b/evm-tests/test/subnet.precompile.hyperparameter.test.ts @@ -1,6 +1,6 @@ import * as assert from "assert"; -import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { getDevnetApi, getRandomSubstrateKeypair, getAliceSigner, waitForTransactionWithRetry } from "../src/substrate" import { devnet } from "@polkadot-api/descriptors" import { TypedApi } from "polkadot-api"; import { convertPublicKeyToSs58 } from "../src/address-utils" @@ -25,6 +25,21 @@ describe("Test the Subnet precompile contract", () => { await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey1.publicKey)) await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey2.publicKey)) await forceSetBalanceToEthAddress(api, wallet.address) + + // Disable admin freeze window and owner hyperparam rate limiting for tests + { + const alice = getAliceSigner() + + // Set AdminFreezeWindow to 0 + const setFreezeWindow = api.tx.AdminUtils.sudo_set_admin_freeze_window({ window: 0 }) + const sudoFreezeTx = api.tx.Sudo.sudo({ call: setFreezeWindow.decodedCall }) + await waitForTransactionWithRetry(api, sudoFreezeTx, alice) + + // Set OwnerHyperparamRateLimit to 0 + const setOwnerRateLimit = api.tx.AdminUtils.sudo_set_owner_hparam_rate_limit({ limit: BigInt(0) }) + const sudoOwnerRateTx = api.tx.Sudo.sudo({ call: setOwnerRateLimit.decodedCall }) + await waitForTransactionWithRetry(api, sudoOwnerRateTx, alice) + } }) it("Can register network without identity info", async () => { diff --git a/pallets/admin-utils/src/benchmarking.rs b/pallets/admin-utils/src/benchmarking.rs index c824a879a5..b8dafc0de2 100644 --- a/pallets/admin-utils/src/benchmarking.rs +++ b/pallets/admin-utils/src/benchmarking.rs @@ -346,6 +346,18 @@ mod benchmarks { _(RawOrigin::Root, 5u16/*version*/)/*sudo_set_commit_reveal_version()*/; } + #[benchmark] + fn sudo_set_admin_freeze_window() { + #[extrinsic_call] + _(RawOrigin::Root, 5u16/*window*/)/*sudo_set_admin_freeze_window*/; + } + + #[benchmark] + fn sudo_set_owner_hparam_rate_limit() { + #[extrinsic_call] + _(RawOrigin::Root, 10u64/*limit*/)/*sudo_set_owner_hparam_rate_limit*/; + } + #[benchmark] fn sudo_set_owner_immune_neuron_limit() { pallet_subtensor::Pallet::::init_new_network( diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 2a785d7c88..5808d53a70 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -22,10 +22,7 @@ pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_support::traits::tokens::Balance; - use frame_support::{ - dispatch::{DispatchResult, RawOrigin}, - pallet_prelude::StorageMap, - }; + use frame_support::{dispatch::DispatchResult, pallet_prelude::StorageMap}; use frame_system::pallet_prelude::*; use pallet_evm_chain_id::{self, ChainId}; use pallet_subtensor::utils::rate_limiting::TransactionType; @@ -216,10 +213,18 @@ pub mod pallet { netuid: NetUid, serving_rate_limit: u64, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; - + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; 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, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -235,7 +240,7 @@ pub mod pallet { netuid: NetUid, min_difficulty: u64, ) -> DispatchResult { - ensure_root(origin)?; + pallet_subtensor::Pallet::::ensure_root_with_rate_limit(origin, netuid)?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -260,7 +265,11 @@ pub mod pallet { netuid: NetUid, max_difficulty: u64, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -270,6 +279,11 @@ pub mod pallet { log::debug!( "MaxDifficultySet( netuid: {netuid:?} max_difficulty: {max_difficulty:?} ) " ); + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -285,34 +299,28 @@ pub mod pallet { netuid: NetUid, weights_version_key: u64, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin.clone(), netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin.clone(), + netuid, + &[ + TransactionType::OwnerHyperparamUpdate, + TransactionType::SetWeightsVersionKey, + ], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), Error::::SubnetDoesNotExist ); - if let Ok(RawOrigin::Signed(who)) = origin.into() { - // SN Owner - // Ensure the origin passes the rate limit. - ensure!( - pallet_subtensor::Pallet::::passes_rate_limit_on_subnet( - &TransactionType::SetWeightsVersionKey, - &who, - netuid, - ), - pallet_subtensor::Error::::TxRateLimitExceeded - ); - - // Set last transaction block - let current_block = pallet_subtensor::Pallet::::get_current_block_as_u64(); - pallet_subtensor::Pallet::::set_last_transaction_block_on_subnet( - &who, - netuid, - &TransactionType::SetWeightsVersionKey, - current_block, - ); - } + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[ + TransactionType::OwnerHyperparamUpdate, + TransactionType::SetWeightsVersionKey, + ], + ); pallet_subtensor::Pallet::::set_weights_version_key(netuid, weights_version_key); log::debug!( @@ -388,13 +396,22 @@ pub mod pallet { netuid: NetUid, adjustment_alpha: u64, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_adjustment_alpha(netuid, adjustment_alpha); + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ); log::debug!("AdjustmentAlphaSet( adjustment_alpha: {adjustment_alpha:?} ) "); Ok(()) } @@ -411,13 +428,22 @@ pub mod pallet { netuid: NetUid, max_weight_limit: u16, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_max_weight_limit(netuid, max_weight_limit); + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ); log::debug!( "MaxWeightLimitSet( netuid: {netuid:?} max_weight_limit: {max_weight_limit:?} ) " ); @@ -436,13 +462,22 @@ pub mod pallet { netuid: NetUid, immunity_period: u16, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_immunity_period(netuid, immunity_period); + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ); log::debug!( "ImmunityPeriodSet( netuid: {netuid:?} immunity_period: {immunity_period:?} ) " ); @@ -461,7 +496,11 @@ pub mod pallet { netuid: NetUid, min_allowed_weights: u16, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -471,6 +510,11 @@ pub mod pallet { log::debug!( "MinAllowedWeightSet( netuid: {netuid:?} min_allowed_weights: {min_allowed_weights:?} ) " ); + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -510,7 +554,11 @@ pub mod pallet { .saturating_add(::DbWeight::get().reads(1_u64)) .saturating_add(::DbWeight::get().writes(1_u64)))] pub fn sudo_set_kappa(origin: OriginFor, netuid: NetUid, kappa: u16) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -518,6 +566,11 @@ pub mod pallet { ); pallet_subtensor::Pallet::::set_kappa(netuid, kappa); log::debug!("KappaSet( netuid: {netuid:?} kappa: {kappa:?} ) "); + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -529,7 +582,11 @@ pub mod pallet { .saturating_add(::DbWeight::get().reads(1_u64)) .saturating_add(::DbWeight::get().writes(1_u64)))] pub fn sudo_set_rho(origin: OriginFor, netuid: NetUid, rho: u16) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -537,6 +594,11 @@ 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, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -552,7 +614,11 @@ pub mod pallet { netuid: NetUid, activity_cutoff: u16, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -568,6 +634,11 @@ pub mod pallet { log::debug!( "ActivityCutoffSet( netuid: {netuid:?} activity_cutoff: {activity_cutoff:?} ) " ); + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -611,7 +682,11 @@ pub mod pallet { netuid: NetUid, registration_allowed: bool, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; pallet_subtensor::Pallet::::set_network_pow_registration_allowed( netuid, @@ -620,6 +695,11 @@ pub mod pallet { log::debug!( "NetworkPowRegistrationAllowed( registration_allowed: {registration_allowed:?} ) " ); + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -635,7 +715,7 @@ pub mod pallet { netuid: NetUid, target_registrations_per_interval: u16, ) -> DispatchResult { - ensure_root(origin)?; + pallet_subtensor::Pallet::::ensure_root_with_rate_limit(origin, netuid)?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -663,7 +743,11 @@ pub mod pallet { netuid: NetUid, min_burn: TaoCurrency, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin.clone(), netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), Error::::SubnetDoesNotExist @@ -679,6 +763,11 @@ 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, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -694,7 +783,11 @@ pub mod pallet { netuid: NetUid, max_burn: TaoCurrency, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin.clone(), netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), Error::::SubnetDoesNotExist @@ -710,6 +803,11 @@ 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, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -725,7 +823,7 @@ pub mod pallet { netuid: NetUid, difficulty: u64, ) -> DispatchResult { - ensure_root(origin)?; + pallet_subtensor::Pallet::::ensure_root_with_rate_limit(origin, netuid)?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), Error::::SubnetDoesNotExist @@ -747,7 +845,7 @@ pub mod pallet { netuid: NetUid, max_allowed_validators: u16, ) -> DispatchResult { - ensure_root(origin)?; + pallet_subtensor::Pallet::::ensure_root_with_rate_limit(origin, netuid)?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), Error::::SubnetDoesNotExist @@ -780,9 +878,12 @@ pub mod pallet { netuid: NetUid, bonds_moving_average: u64, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin.clone(), netuid)?; - - if pallet_subtensor::Pallet::::ensure_subnet_owner(origin, netuid).is_ok() { + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; + if maybe_owner.is_some() { ensure!( bonds_moving_average <= 975000, Error::::BondsMovingAverageMaxReached @@ -797,6 +898,11 @@ pub mod pallet { log::debug!( "BondsMovingAverageSet( netuid: {netuid:?} bonds_moving_average: {bonds_moving_average:?} ) " ); + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -812,7 +918,11 @@ pub mod pallet { netuid: NetUid, bonds_penalty: u16, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -820,6 +930,11 @@ 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, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -835,7 +950,7 @@ pub mod pallet { netuid: NetUid, max_registrations_per_block: u16, ) -> DispatchResult { - ensure_root(origin)?; + pallet_subtensor::Pallet::::ensure_root_with_rate_limit(origin, netuid)?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -899,7 +1014,7 @@ pub mod pallet { .saturating_add(::DbWeight::get().reads(1_u64)) .saturating_add(::DbWeight::get().writes(1_u64)))] pub fn sudo_set_tempo(origin: OriginFor, netuid: NetUid, tempo: u16) -> DispatchResult { - ensure_root(origin)?; + pallet_subtensor::Pallet::::ensure_root_with_rate_limit(origin, netuid)?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), Error::::SubnetDoesNotExist @@ -1087,7 +1202,11 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -1096,6 +1215,11 @@ 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, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -1115,9 +1239,18 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; 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, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -1130,10 +1263,22 @@ pub mod pallet { alpha_low: u16, alpha_high: u16, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin.clone(), netuid)?; - pallet_subtensor::Pallet::::do_set_alpha_values( + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin.clone(), + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; + let res = 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, + &[TransactionType::OwnerHyperparamUpdate], + ); + } + res } /// Sets the duration of the coldkey swap schedule. @@ -1225,7 +1370,11 @@ pub mod pallet { netuid: NetUid, interval: u64, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -1235,6 +1384,11 @@ 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, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -1308,8 +1462,20 @@ pub mod pallet { netuid: NetUid, toggle: bool, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; - pallet_subtensor::Pallet::::toggle_transfer(netuid, toggle) + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; + let res = pallet_subtensor::Pallet::::toggle_transfer(netuid, toggle); + if res.is_ok() { + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ); + } + res } /// Toggles the enablement of an EVM precompile. @@ -1438,7 +1604,11 @@ pub mod pallet { netuid: NetUid, steepness: i16, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin.clone(), netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin.clone(), + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; ensure!( pallet_subtensor::Pallet::::if_subnet_exist(netuid), @@ -1454,6 +1624,11 @@ 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, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -1473,11 +1648,20 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; 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, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -1497,11 +1681,20 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; 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, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -1564,7 +1757,7 @@ pub mod pallet { netuid: NetUid, subtoken_enabled: bool, ) -> DispatchResult { - ensure_root(origin)?; + pallet_subtensor::Pallet::::ensure_root_with_rate_limit(origin, netuid)?; pallet_subtensor::SubtokenEnabled::::set(netuid, subtoken_enabled); log::debug!( @@ -1601,8 +1794,17 @@ pub mod pallet { netuid: NetUid, immune_neurons: u16, ) -> DispatchResult { - pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; + let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + origin, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + )?; pallet_subtensor::Pallet::::set_owner_immune_neuron_limit(netuid, immune_neurons)?; + pallet_subtensor::Pallet::::record_owner_rl( + maybe_owner, + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ); Ok(()) } @@ -1611,14 +1813,39 @@ pub mod pallet { /// The extrinsic will call the Subtensor pallet to set the childkey burn. #[pallet::call_index(73)] #[pallet::weight(Weight::from_parts(15_650_000, 0) - .saturating_add(::DbWeight::get().reads(1_u64)) - .saturating_add(::DbWeight::get().writes(1_u64)))] + .saturating_add(::DbWeight::get().reads(1_u64)) + .saturating_add(::DbWeight::get().writes(1_u64)))] pub fn sudo_set_ck_burn(origin: OriginFor, burn: u64) -> DispatchResult { ensure_root(origin)?; pallet_subtensor::Pallet::::set_ck_burn(burn); log::debug!("CKBurnSet( burn: {burn:?} ) "); Ok(()) } + + /// Sets the admin freeze window length (in blocks) at the end of a tempo. + /// Only callable by root. + #[pallet::call_index(74)] + #[pallet::weight((0, DispatchClass::Operational, Pays::No))] + pub fn sudo_set_admin_freeze_window(origin: OriginFor, window: u16) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_admin_freeze_window(window); + log::debug!("AdminFreezeWindowSet( window: {window:?} ) "); + Ok(()) + } + + /// Sets the owner hyperparameter rate limit (in blocks). + /// Only callable by root. + #[pallet::call_index(75)] + #[pallet::weight((0, DispatchClass::Operational, Pays::No))] + pub fn sudo_set_owner_hparam_rate_limit( + origin: OriginFor, + limit: u64, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_owner_hyperparam_rate_limit(limit); + log::debug!("OwnerHyperparamRateLimitSet( limit: {limit:?} ) "); + Ok(()) + } } } diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 7bba7929dd..954ef91440 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -481,7 +481,10 @@ pub fn new_test_ext() -> sp_io::TestExternalities { .build_storage() .unwrap(); let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); + ext.execute_with(|| { + System::set_block_number(1); + SubtensorModule::set_admin_freeze_window(1); + }); ext } diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index ef36bb7856..9b0197860c 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -175,7 +175,7 @@ fn test_sudo_set_weights_version_key_rate_limit() { SubnetOwner::::insert(netuid, sn_owner); let rate_limit = WeightsVersionKeyRateLimit::::get(); - let tempo: u16 = Tempo::::get(netuid); + let tempo = Tempo::::get(netuid); let rate_limit_period = rate_limit * (tempo as u64); @@ -205,7 +205,7 @@ fn test_sudo_set_weights_version_key_rate_limit() { ); // Wait for rate limit to pass - run_to_block(rate_limit_period + 2); + run_to_block(rate_limit_period + 1); assert!(SubtensorModule::passes_rate_limit_on_subnet( &pallet_subtensor::utils::rate_limiting::TransactionType::SetWeightsVersionKey, &sn_owner, @@ -1952,6 +1952,80 @@ fn test_sudo_set_commit_reveal_version() { }); } +#[test] +fn test_sudo_set_admin_freeze_window_and_rate() { + new_test_ext().execute_with(|| { + // Non-root fails + assert_eq!( + AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::signed(U256::from(1)), + 7 + ), + Err(DispatchError::BadOrigin) + ); + // Root succeeds + assert_ok!(AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::root(), + 7 + )); + assert_eq!(pallet_subtensor::AdminFreezeWindow::::get(), 7); + + // Owner hyperparam rate limit 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); + }); +} + +#[test] +fn test_freeze_window_blocks_root_and_owner() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let tempo = 10; + // Create subnet with tempo 10 + add_network(netuid, tempo); + // Set freeze window to 3 blocks + assert_ok!(AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::root(), + 3 + )); + // Advance to a block where remaining < 3 + run_to_block((tempo - 2).into()); + + // Root should be blocked during freeze window + assert_noop!( + AdminUtils::sudo_set_min_burn( + <::RuntimeOrigin>::root(), + netuid, + 123.into() + ), + SubtensorError::::AdminActionProhibitedDuringWeightsWindow + ); + + // Owner should be blocked during freeze window as well + // Set owner + let owner: U256 = U256::from(9); + SubnetOwner::::insert(netuid, owner); + assert_noop!( + AdminUtils::sudo_set_kappa( + <::RuntimeOrigin>::signed(owner), + netuid, + 77 + ), + SubtensorError::::AdminActionProhibitedDuringWeightsWindow + ); + }); +} + #[test] fn test_sudo_set_min_burn() { new_test_ext().execute_with(|| { @@ -2011,6 +2085,90 @@ 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); + + // Configure owner hyperparam RL to 2 blocks + assert_ok!(AdminUtils::sudo_set_owner_hparam_rate_limit( + <::RuntimeOrigin>::root(), + 2 + )); + + // First update succeeds + assert_ok!(AdminUtils::sudo_set_kappa( + <::RuntimeOrigin>::signed(owner), + netuid, + 11 + )); + // Immediate second update fails due to TxRateLimitExceeded + assert_noop!( + AdminUtils::sudo_set_kappa( + <::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_kappa( + <::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_kappa( + <::RuntimeOrigin>::signed(owner), + netuid, + 14 + )); + }); +} + +// Verifies that when the owner hyperparameter rate limit is left at its default (0), hyperparameter +// updates are not blocked until a non-zero value is set. +#[test] +fn test_hyperparam_rate_limit_not_blocking_with_default() { + 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); + + // Read the default (unset) owner hyperparam rate limit + let default_limit = pallet_subtensor::OwnerHyperparamRateLimit::::get(); + + assert_eq!(default_limit, 0); + + // First owner update should always succeed + assert_ok!(AdminUtils::sudo_set_kappa( + <::RuntimeOrigin>::signed(owner), + netuid, + 1 + )); + + // With default == 0, second immediate update should also pass (no rate limiting) + assert_ok!(AdminUtils::sudo_set_kappa( + <::RuntimeOrigin>::signed(owner), + netuid, + 2 + )); + }); +} + #[test] fn test_sudo_set_max_burn() { new_test_ext().execute_with(|| { diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index dff52d6b55..6036411a91 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -863,6 +863,18 @@ pub mod pallet { 50400 } + #[pallet::type_value] + /// Default value for subnet owner hyperparameter update rate limit (in blocks) + pub fn DefaultOwnerHyperparamRateLimit() -> u64 { + 0 + } + + #[pallet::type_value] + /// Default number of terminal blocks in a tempo during which admin operations are prohibited + pub fn DefaultAdminFreezeWindow() -> u16 { + 10 + } + #[pallet::type_value] /// Default value for ck burn, 18%. pub fn DefaultCKBurn() -> u64 { @@ -873,6 +885,16 @@ pub mod pallet { pub type MinActivityCutoff = StorageValue<_, u16, ValueQuery, DefaultMinActivityCutoff>; + #[pallet::storage] + /// Global window (in blocks) at the end of each tempo where admin ops are disallowed + pub type AdminFreezeWindow = + StorageValue<_, u16, ValueQuery, DefaultAdminFreezeWindow>; + + #[pallet::storage] + /// Global rate limit (in blocks) for subnet owner hyperparameter updates + pub type OwnerHyperparamRateLimit = + StorageValue<_, u64, ValueQuery, DefaultOwnerHyperparamRateLimit>; + #[pallet::storage] pub type ColdkeySwapScheduleDuration = StorageValue<_, BlockNumberFor, ValueQuery, DefaultColdkeySwapScheduleDuration>; @@ -2146,6 +2168,8 @@ impl> pub enum RateLimitKey { // The setting sn owner hotkey operation is rate limited per netuid SetSNOwnerHotkey(NetUid), + // Generic rate limit for subnet-owner hyperparameter updates (per netuid) + OwnerHyperparamUpdate(NetUid), // Subnet registration rate limit NetworkLastRegistered, // Last tx block limit per account ID diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index e6d9c231d1..ed6ca3c002 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -238,6 +238,8 @@ mod errors { BeneficiaryDoesNotOwnHotkey, /// Expected beneficiary origin. ExpectedBeneficiaryOrigin, + /// Admin operation is prohibited during the protected weights window + AdminActionProhibitedDuringWeightsWindow, /// Symbol does not exist. SymbolDoesNotExist, /// Symbol already in use. diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 2fab5ecdb4..4fdff241b2 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -114,6 +114,10 @@ mod events { 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 blocks) + OwnerHyperparamRateLimitSet(u64), /// minimum childkey take set MinChildKeyTakeSet(u16), /// maximum childkey take set diff --git a/pallets/subtensor/src/tests/ensure.rs b/pallets/subtensor/src/tests/ensure.rs new file mode 100644 index 0000000000..a8b3843fa3 --- /dev/null +++ b/pallets/subtensor/src/tests/ensure.rs @@ -0,0 +1,156 @@ +use frame_support::{assert_noop, assert_ok}; +use frame_system::Config; +use sp_core::U256; +use subtensor_runtime_common::NetUid; + +use super::mock::*; +use crate::utils::rate_limiting::TransactionType; +use crate::{RateLimitKey, SubnetOwner, SubtokenEnabled}; + +#[test] +fn ensure_subnet_owner_returns_who_and_checks_ownership() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 10, 0); + + let owner: U256 = U256::from(42); + SubnetOwner::::insert(netuid, owner); + + // Non-owner signed should fail + assert!( + crate::Pallet::::ensure_subnet_owner( + <::RuntimeOrigin>::signed(U256::from(7)), + netuid + ) + .is_err() + ); + + // Owner signed returns who + let who = crate::Pallet::::ensure_subnet_owner( + <::RuntimeOrigin>::signed(owner), + netuid, + ) + .expect("owner must pass"); + assert_eq!(who, owner); + }); +} + +#[test] +fn ensure_subnet_owner_or_root_distinguishes_root_and_owner() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(2); + add_network(netuid, 10, 0); + let owner: U256 = U256::from(9); + SubnetOwner::::insert(netuid, owner); + + // Root path returns None + let root = crate::Pallet::::ensure_subnet_owner_or_root( + <::RuntimeOrigin>::root(), + netuid, + ) + .expect("root allowed"); + assert!(root.is_none()); + + // Owner path returns Some(owner) + let maybe_owner = crate::Pallet::::ensure_subnet_owner_or_root( + <::RuntimeOrigin>::signed(owner), + netuid, + ) + .expect("owner allowed"); + assert_eq!(maybe_owner, Some(owner)); + }); +} + +#[test] +fn ensure_root_with_rate_limit_blocks_in_freeze_window() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let tempo = 10; + add_network(netuid, 10, 0); + + // Set freeze window to 3 + let freeze_window = 3; + crate::Pallet::::set_admin_freeze_window(freeze_window); + + run_to_block((tempo - freeze_window + 1).into()); + + // Root is blocked in freeze window + assert!( + crate::Pallet::::ensure_root_with_rate_limit( + <::RuntimeOrigin>::root(), + netuid + ) + .is_err() + ); + }); +} + +#[test] +fn ensure_owner_or_root_with_limits_checks_rl_and_freeze() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let tempo = 10; + add_network(netuid, 10, 0); + SubtokenEnabled::::insert(netuid, true); + let owner: U256 = U256::from(5); + SubnetOwner::::insert(netuid, owner); + // Set freeze window to 3 + let freeze_window = 3; + crate::Pallet::::set_admin_freeze_window(freeze_window); + + // Set owner RL to 2 blocks + crate::Pallet::::set_owner_hyperparam_rate_limit(2); + + // Outside freeze window initially; should pass and return Some(owner) + let res = crate::Pallet::::ensure_sn_owner_or_root_with_limits( + <::RuntimeOrigin>::signed(owner), + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ) + .expect("should pass"); + assert_eq!(res, Some(owner)); + + // Simulate previous update at current block -> next call should fail due to rate limit + let now = crate::Pallet::::get_current_block_as_u64(); + crate::Pallet::::set_rate_limited_last_block( + &RateLimitKey::OwnerHyperparamUpdate(netuid), + now, + ); + assert_noop!( + crate::Pallet::::ensure_sn_owner_or_root_with_limits( + <::RuntimeOrigin>::signed(owner), + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ), + crate::Error::::TxRateLimitExceeded + ); + + // Advance beyond RL and ensure passes again + run_to_block(now + 3); + assert_ok!(crate::Pallet::::ensure_sn_owner_or_root_with_limits( + <::RuntimeOrigin>::signed(owner), + netuid, + &[TransactionType::OwnerHyperparamUpdate] + )); + + // Now advance into the freeze window; ensure blocks + // (using loop for clarity, because epoch calculation function uses netuid) + 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_noop!( + crate::Pallet::::ensure_sn_owner_or_root_with_limits( + <::RuntimeOrigin>::signed(owner), + netuid, + &[TransactionType::OwnerHyperparamUpdate], + ), + crate::Error::::AdminActionProhibitedDuringWeightsWindow + ); + }); +} diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index b743d7c1ff..1f4aa71363 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -5,6 +5,7 @@ mod consensus; mod delegate_info; mod difficulty; mod emission; +mod ensure; mod epoch; mod evm; mod leasing; diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index f64962f094..cc4485bf55 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -14,25 +14,127 @@ impl Pallet { pub fn ensure_subnet_owner_or_root( o: T::RuntimeOrigin, netuid: NetUid, - ) -> Result<(), DispatchError> { + ) -> Result, DispatchError> { let coldkey = ensure_signed_or_root(o); match coldkey { - Ok(Some(who)) if SubnetOwner::::get(netuid) == who => Ok(()), + Ok(Some(who)) if SubnetOwner::::get(netuid) == who => Ok(Some(who)), Ok(Some(_)) => Err(DispatchError::BadOrigin), - Ok(None) => Ok(()), + Ok(None) => Ok(None), Err(x) => Err(x.into()), } } - pub fn ensure_subnet_owner(o: T::RuntimeOrigin, netuid: NetUid) -> Result<(), DispatchError> { + pub fn ensure_subnet_owner( + o: T::RuntimeOrigin, + netuid: NetUid, + ) -> Result { let coldkey = ensure_signed(o); match coldkey { - Ok(who) if SubnetOwner::::get(netuid) == who => Ok(()), + Ok(who) if SubnetOwner::::get(netuid) == who => Ok(who), Ok(_) => Err(DispatchError::BadOrigin), Err(x) => Err(x.into()), } } + /// Like `ensure_root` but also prohibits calls during the last N blocks of the tempo. + pub fn ensure_root_with_rate_limit( + o: T::RuntimeOrigin, + netuid: NetUid, + ) -> Result<(), DispatchError> { + ensure_root(o)?; + let now = Self::get_current_block_as_u64(); + Self::ensure_not_in_admin_freeze_window(netuid, now)?; + Ok(()) + } + + /// Ensure owner-or-root with a set of TransactionType rate checks (owner only). + /// - Root: only freeze window is enforced; no TransactionType checks. + /// - Owner (Signed): freeze window plus all rate checks in `limits` using signer extracted from + /// origin. + pub fn ensure_sn_owner_or_root_with_limits( + o: T::RuntimeOrigin, + netuid: NetUid, + limits: &[crate::utils::rate_limiting::TransactionType], + ) -> Result, DispatchError> { + let maybe_who = Self::ensure_subnet_owner_or_root(o, netuid)?; + let now = Self::get_current_block_as_u64(); + Self::ensure_not_in_admin_freeze_window(netuid, now)?; + if let Some(who) = maybe_who.as_ref() { + for tx in limits.iter() { + ensure!( + Self::passes_rate_limit_on_subnet(tx, who, netuid), + Error::::TxRateLimitExceeded + ); + } + } + Ok(maybe_who) + } + + /// Ensure the caller is the subnet owner and passes all provided rate limits. + /// This does NOT allow root; it is strictly owner-only. + /// Returns the signer (owner) on success so callers may record last-blocks. + pub fn ensure_sn_owner_with_limits( + o: T::RuntimeOrigin, + netuid: NetUid, + limits: &[crate::utils::rate_limiting::TransactionType], + ) -> Result { + let who = Self::ensure_subnet_owner(o, netuid)?; + let now = Self::get_current_block_as_u64(); + Self::ensure_not_in_admin_freeze_window(netuid, now)?; + for tx in limits.iter() { + ensure!( + Self::passes_rate_limit_on_subnet(tx, &who, netuid), + Error::::TxRateLimitExceeded + ); + } + Ok(who) + } + + /// Returns true if the current block is within the terminal freeze window of the tempo for the + /// given subnet. During this window, admin ops are prohibited to avoid interference with + /// validator weight submissions. + pub fn is_in_admin_freeze_window(netuid: NetUid, current_block: u64) -> bool { + let tempo = Self::get_tempo(netuid); + if tempo == 0 { + return false; + } + let remaining = Self::blocks_until_next_epoch(netuid, tempo, current_block); + let window = AdminFreezeWindow::::get() as u64; + remaining < window + } + + fn ensure_not_in_admin_freeze_window(netuid: NetUid, now: u64) -> Result<(), DispatchError> { + ensure!( + !Self::is_in_admin_freeze_window(netuid, now), + Error::::AdminActionProhibitedDuringWeightsWindow + ); + Ok(()) + } + + pub fn set_admin_freeze_window(window: u16) { + AdminFreezeWindow::::set(window); + Self::deposit_event(Event::AdminFreezeWindowSet(window)); + } + + pub fn set_owner_hyperparam_rate_limit(limit: u64) { + OwnerHyperparamRateLimit::::set(limit); + Self::deposit_event(Event::OwnerHyperparamRateLimitSet(limit)); + } + + /// If owner is `Some`, record last-blocks for the provided `TransactionType`s. + pub fn record_owner_rl( + maybe_owner: Option<::AccountId>, + netuid: NetUid, + txs: &[TransactionType], + ) { + if let Some(who) = maybe_owner { + let now = Self::get_current_block_as_u64(); + for tx in txs { + Self::set_last_transaction_block_on_subnet(&who, netuid, tx, now); + } + } + } + // ======================== // ==== Global Setters ==== // ======================== diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index de75086ea1..5d1005333c 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -11,6 +11,7 @@ pub enum TransactionType { RegisterNetwork, SetWeightsVersionKey, SetSNOwnerHotkey, + OwnerHyperparamUpdate, } /// Implement conversion from TransactionType to u16 @@ -23,6 +24,7 @@ impl From for u16 { TransactionType::RegisterNetwork => 3, TransactionType::SetWeightsVersionKey => 4, TransactionType::SetSNOwnerHotkey => 5, + TransactionType::OwnerHyperparamUpdate => 6, } } } @@ -36,6 +38,7 @@ impl From for TransactionType { 3 => TransactionType::RegisterNetwork, 4 => TransactionType::SetWeightsVersionKey, 5 => TransactionType::SetSNOwnerHotkey, + 6 => TransactionType::OwnerHyperparamUpdate, _ => TransactionType::Unknown, } } @@ -50,6 +53,7 @@ impl Pallet { TransactionType::SetChildren => 150, // 30 minutes TransactionType::SetChildkeyTake => TxChildkeyTakeRateLimit::::get(), TransactionType::RegisterNetwork => NetworkRateLimit::::get(), + TransactionType::OwnerHyperparamUpdate => OwnerHyperparamRateLimit::::get(), TransactionType::Unknown => 0, // Default to no limit for unknown types (no limit) _ => 0, @@ -112,6 +116,9 @@ impl Pallet { TransactionType::SetSNOwnerHotkey => { Self::get_rate_limited_last_block(&RateLimitKey::SetSNOwnerHotkey(netuid)) } + TransactionType::OwnerHyperparamUpdate => { + Self::get_rate_limited_last_block(&RateLimitKey::OwnerHyperparamUpdate(netuid)) + } _ => { let tx_as_u16: u16 = (*tx_type).into(); TransactionKeyLastBlock::::get((hotkey, netuid, tx_as_u16)) @@ -131,6 +138,10 @@ impl Pallet { TransactionType::SetSNOwnerHotkey => { Self::set_rate_limited_last_block(&RateLimitKey::SetSNOwnerHotkey(netuid), block) } + TransactionType::OwnerHyperparamUpdate => Self::set_rate_limited_last_block( + &RateLimitKey::OwnerHyperparamUpdate(netuid), + block, + ), _ => { let tx_as_u16: u16 = (*tx_type).into(); TransactionKeyLastBlock::::insert((key, netuid, tx_as_u16), block);