diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 078f85615c..1a2b4ef8c5 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -1477,6 +1477,46 @@ pub mod pallet { Ok(()) } + /// Sets or updates the hotkey account associated with the owner of a specific subnet. + /// + /// This function allows either the root origin or the current subnet owner to set or update + /// the hotkey for a given subnet. The subnet must already exist. To prevent abuse, the call is + /// rate-limited to once per configured interval (default: one week) per subnet. + /// + /// # Parameters + /// - `origin`: The dispatch origin of the call. Must be either root or the current owner of the subnet. + /// - `netuid`: The unique identifier of the subnet whose owner hotkey is being set. + /// - `hotkey`: The new hotkey account to associate with the subnet owner. + /// + /// # Returns + /// - `DispatchResult`: Returns `Ok(())` if the hotkey was successfully set, or an appropriate error otherwise. + /// + /// # Errors + /// - `Error::SubnetNotExists`: If the specified subnet does not exist. + /// - `Error::TxRateLimitExceeded`: If the function is called more frequently than the allowed rate limit. + /// + /// # Access Control + /// Only callable by: + /// - Root origin, or + /// - The coldkey account that owns the subnet. + /// + /// # Storage + /// - Updates [`SubnetOwnerHotkey`] for the given `netuid`. + /// - Reads and updates [`LastRateLimitedBlock`] for rate-limiting. + /// - Reads [`DefaultSetSNOwnerHotkeyRateLimit`] to determine the interval between allowed updates. + /// + /// # Rate Limiting + /// This function is rate-limited to one call per subnet per interval (e.g., one week). + #[pallet::call_index(67)] + #[pallet::weight((0, DispatchClass::Operational, Pays::No))] + pub fn sudo_set_sn_owner_hotkey( + origin: OriginFor, + netuid: u16, + hotkey: T::AccountId, + ) -> DispatchResult { + pallet_subtensor::Pallet::::do_set_sn_owner_hotkey(origin, netuid, &hotkey) + } + /// Enables or disables subtoken trading for a given subnet. /// /// # Arguments diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index 2f4c3f2b51..bb813ce117 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -1711,3 +1711,72 @@ fn test_sudo_set_ema_halving() { assert_eq!(value_after_2, to_be_set); }); } + +// cargo test --package pallet-admin-utils --lib -- tests::test_set_sn_owner_hotkey --exact --show-output +#[test] +fn test_set_sn_owner_hotkey_owner() { + new_test_ext().execute_with(|| { + let netuid: u16 = 1; + let hotkey: U256 = U256::from(3); + let bad_origin_coldkey: U256 = U256::from(4); + add_network(netuid, 10); + + let owner = U256::from(10); + pallet_subtensor::SubnetOwner::::insert(netuid, owner); + + // Non-owner and non-root cannot set the sn owner hotkey + assert_eq!( + AdminUtils::sudo_set_sn_owner_hotkey( + <::RuntimeOrigin>::signed(bad_origin_coldkey), + netuid, + hotkey + ), + Err(DispatchError::BadOrigin) + ); + + // SN owner can set the hotkey + assert_ok!(AdminUtils::sudo_set_sn_owner_hotkey( + <::RuntimeOrigin>::signed(owner), + netuid, + hotkey + )); + + // Check the value + let actual_hotkey = pallet_subtensor::SubnetOwnerHotkey::::get(netuid); + assert_eq!(actual_hotkey, hotkey); + + // Cannot set again (rate limited) + assert_err!( + AdminUtils::sudo_set_sn_owner_hotkey( + <::RuntimeOrigin>::signed(owner), + netuid, + hotkey + ), + pallet_subtensor::Error::::TxRateLimitExceeded + ); + }); +} + +// cargo test --package pallet-admin-utils --lib -- tests::test_set_sn_owner_hotkey_root --exact --show-output +#[test] +fn test_set_sn_owner_hotkey_root() { + new_test_ext().execute_with(|| { + let netuid: u16 = 1; + let hotkey: U256 = U256::from(3); + add_network(netuid, 10); + + let owner = U256::from(10); + pallet_subtensor::SubnetOwner::::insert(netuid, owner); + + // Root can set the hotkey + assert_ok!(AdminUtils::sudo_set_sn_owner_hotkey( + <::RuntimeOrigin>::root(), + netuid, + hotkey + )); + + // Check the value + let actual_hotkey = pallet_subtensor::SubnetOwnerHotkey::::get(netuid); + assert_eq!(actual_hotkey, hotkey); + }); +} diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index b2633fa381..1f3a91b339 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -665,4 +665,10 @@ impl Pallet { let halved_interval: I64F64 = interval.saturating_mul(halving); halved_interval.saturating_to_num::() } + pub fn get_rate_limited_last_block(rate_limit_key: &RateLimitKey) -> u64 { + LastRateLimitedBlock::::get(rate_limit_key) + } + pub fn set_rate_limited_last_block(rate_limit_key: &RateLimitKey, block: u64) { + LastRateLimitedBlock::::set(rate_limit_key, block); + } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 1aa9a56b8f..2e0b479c0f 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -66,6 +66,7 @@ pub const MAX_CRV3_COMMIT_SIZE_BYTES: u32 = 5000; #[import_section(config::config)] #[frame_support::pallet] pub mod pallet { + use crate::RateLimitKey; use crate::migrations; use frame_support::{ BoundedVec, @@ -874,6 +875,12 @@ pub mod pallet { 360 } + #[pallet::type_value] + /// Default value for setting subnet owner hotkey rate limit + pub fn DefaultSetSNOwnerHotkeyRateLimit() -> u64 { + 50400 + } + #[pallet::storage] pub type MinActivityCutoff = StorageValue<_, u16, ValueQuery, DefaultMinActivityCutoff>; @@ -1176,6 +1183,15 @@ pub mod pallet { pub type WeightsVersionKeyRateLimit = StorageValue<_, u64, ValueQuery, DefaultWeightsVersionKeyRateLimit>; + /// ============================ + /// ==== Rate Limiting ===== + /// ============================ + + #[pallet::storage] + /// --- MAP ( RateLimitKey ) --> Block number in which the last rate limited operation occured + pub type LastRateLimitedBlock = + StorageMap<_, Identity, RateLimitKey, u64, ValueQuery, DefaultZeroU64>; + /// ============================ /// ==== Subnet Locks ===== /// ============================ @@ -2598,3 +2614,11 @@ impl CollectiveInterface for () { Ok(true) } } + +/// Enum that defines types of rate limited operations for +/// storing last block when this operation occured +#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo)] +pub enum RateLimitKey { + // The setting sn owner hotkey operation is rate limited per netuid + SetSNOwnerHotkey(u16), +} diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index de22d64c88..98b83791e8 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -1195,7 +1195,7 @@ mod dispatches { #[pallet::call_index(59)] #[pallet::weight((Weight::from_parts(260_500_000, 0) .saturating_add(T::DbWeight::get().reads(33)) - .saturating_add(T::DbWeight::get().writes(52)), DispatchClass::Operational, Pays::No))] + .saturating_add(T::DbWeight::get().writes(51)), DispatchClass::Operational, Pays::No))] pub fn register_network(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_register_network(origin, &hotkey, 1, None) } @@ -1533,7 +1533,7 @@ mod dispatches { #[pallet::call_index(79)] #[pallet::weight((Weight::from_parts(239_700_000, 0) .saturating_add(T::DbWeight::get().reads(32)) - .saturating_add(T::DbWeight::get().writes(51)), DispatchClass::Operational, Pays::No))] + .saturating_add(T::DbWeight::get().writes(50)), DispatchClass::Operational, Pays::No))] pub fn register_network_with_identity( origin: OriginFor, hotkey: T::AccountId, diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 11e040db1d..9eb44cfbd5 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -59,6 +59,7 @@ impl Pallet { U96F32::saturating_from_num(SubnetMovingPrice::::get(netuid)) } } + pub fn update_moving_price(netuid: u16) { let blocks_since_start_call = U96F32::saturating_from_num({ // We expect FirstEmissionBlockNumber to be set earlier, and we take the block when @@ -70,6 +71,10 @@ impl Pallet { Self::get_current_block_as_u64().saturating_sub(start_call_block) }); + // Use halving time hyperparameter. The meaning of this parameter can be best explained under + // the assumption of a constant price and SubnetMovingAlpha == 0.5: It is how many blocks it + // will take in order for the distance between current EMA of price and current price to shorten + // by half. let halving_time = EMAPriceHalvingBlocks::::get(netuid); let current_ma_unsigned = U96F32::saturating_from_num(SubnetMovingAlpha::::get()); let alpha: U96F32 = current_ma_unsigned.saturating_mul(blocks_since_start_call.safe_div( diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index 0b351fd8d2..b122bfa049 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -370,6 +370,73 @@ impl Pallet { Ok(()) } + /// Sets or updates the hotkey account associated with the owner of a specific subnet. + /// + /// This function allows either the root origin or the current subnet owner to set or update + /// the hotkey for a given subnet. The subnet must already exist. To prevent abuse, the call is + /// rate-limited to once per configured interval (default: one week) per subnet. + /// + /// # Parameters + /// - `origin`: The dispatch origin of the call. Must be either root or the current owner of the subnet. + /// - `netuid`: The unique identifier of the subnet whose owner hotkey is being set. + /// - `hotkey`: The new hotkey account to associate with the subnet owner. + /// + /// # Returns + /// - `DispatchResult`: Returns `Ok(())` if the hotkey was successfully set, or an appropriate error otherwise. + /// + /// # Errors + /// - `Error::SubnetNotExists`: If the specified subnet does not exist. + /// - `Error::TxRateLimitExceeded`: If the function is called more frequently than the allowed rate limit. + /// + /// # Access Control + /// Only callable by: + /// - Root origin, or + /// - The coldkey account that owns the subnet. + /// + /// # Storage + /// - Updates [`SubnetOwnerHotkey`] for the given `netuid`. + /// - Reads and updates [`LastRateLimitedBlock`] for rate-limiting. + /// - Reads [`DefaultSetSNOwnerHotkeyRateLimit`] to determine the interval between allowed updates. + /// + /// # Rate Limiting + /// This function is rate-limited to one call per subnet per interval (e.g., one week). + pub fn do_set_sn_owner_hotkey( + origin: T::RuntimeOrigin, + netuid: u16, + hotkey: &T::AccountId, + ) -> DispatchResult { + // Ensure the caller is either root or subnet owner. + Self::ensure_subnet_owner_or_root(origin, netuid)?; + + // Ensure that the subnet exists. + ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + + // Rate limit: 1 call per week + ensure!( + Self::passes_rate_limit_on_subnet( + &TransactionType::SetSNOwnerHotkey, + hotkey, // ignored + netuid, // Specific to a subnet. + ), + Error::::TxRateLimitExceeded + ); + + // Set last transaction block + let current_block = Self::get_current_block_as_u64(); + Self::set_last_transaction_block_on_subnet( + hotkey, + netuid, + &TransactionType::SetSNOwnerHotkey, + current_block, + ); + + // Insert/update the hotkey + SubnetOwnerHotkey::::insert(netuid, hotkey); + + // Return success. + Ok(()) + } + pub fn is_valid_subnet_for_emission(netuid: u16) -> bool { FirstEmissionBlockNumber::::get(netuid).is_some() } diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index c37a78d2e4..7edaebc98a 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -8,6 +8,7 @@ pub enum TransactionType { Unknown, RegisterNetwork, SetWeightsVersionKey, + SetSNOwnerHotkey, } /// Implement conversion from TransactionType to u16 @@ -19,6 +20,7 @@ impl From for u16 { TransactionType::Unknown => 2, TransactionType::RegisterNetwork => 3, TransactionType::SetWeightsVersionKey => 4, + TransactionType::SetSNOwnerHotkey => 5, } } } @@ -31,6 +33,7 @@ impl From for TransactionType { 1 => TransactionType::SetChildkeyTake, 3 => TransactionType::RegisterNetwork, 4 => TransactionType::SetWeightsVersionKey, + 5 => TransactionType::SetSNOwnerHotkey, _ => TransactionType::Unknown, } } @@ -56,6 +59,8 @@ impl Pallet { match tx_type { TransactionType::SetWeightsVersionKey => (Tempo::::get(netuid) as u64) .saturating_mul(WeightsVersionKeyRateLimit::::get()), + TransactionType::SetSNOwnerHotkey => DefaultSetSNOwnerHotkeyRateLimit::::get(), + _ => Self::get_rate_limit(tx_type), } } @@ -102,6 +107,9 @@ impl Pallet { ) -> u64 { match tx_type { TransactionType::RegisterNetwork => Self::get_network_last_lock_block(), + TransactionType::SetSNOwnerHotkey => { + Self::get_rate_limited_last_block(&RateLimitKey::SetSNOwnerHotkey(netuid)) + } _ => { let tx_as_u16: u16 = (*tx_type).into(); TransactionKeyLastBlock::::get((hotkey, netuid, tx_as_u16)) @@ -126,6 +134,9 @@ impl Pallet { ) { match tx_type { TransactionType::RegisterNetwork => Self::set_network_last_lock_block(block), + TransactionType::SetSNOwnerHotkey => { + Self::set_rate_limited_last_block(&RateLimitKey::SetSNOwnerHotkey(netuid), block) + } _ => { let tx_as_u16: u16 = (*tx_type).into(); TransactionKeyLastBlock::::insert((key, netuid, tx_as_u16), block); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 55c2d957f8..7e94c66bc8 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -722,7 +722,15 @@ impl InstanceFilter for ProxyType { }) => *alpha_amount < SMALL_TRANSFER_LIMIT, _ => false, }, - ProxyType::Owner => matches!(c, RuntimeCall::AdminUtils(..)), + ProxyType::Owner => { + matches!(c, RuntimeCall::AdminUtils(..)) + && !matches!( + c, + RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_sn_owner_hotkey { .. } + ) + ) + } ProxyType::NonCritical => !matches!( c, RuntimeCall::SubtensorModule(pallet_subtensor::Call::dissolve_network { .. }) diff --git a/runtime/tests/pallet_proxy.rs b/runtime/tests/pallet_proxy.rs index 563c274bb9..1fcb36dec5 100644 --- a/runtime/tests/pallet_proxy.rs +++ b/runtime/tests/pallet_proxy.rs @@ -68,6 +68,15 @@ fn call_owner_util() -> RuntimeCall { }) } +// sn owner hotkey call +fn call_sn_owner_hotkey() -> RuntimeCall { + let netuid = 1; + RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_sn_owner_hotkey { + netuid, + hotkey: AccountId::from(ACCOUNT).into(), + }) +} + // critical call for Subtensor fn call_propose() -> RuntimeCall { let proposal = call_remark(); @@ -230,3 +239,30 @@ fn test_non_transfer_cannot_transfer() { ); }); } + +#[test] +fn test_owner_type_cannot_set_sn_owner_hotkey() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(AccountId::from(ACCOUNT)), + AccountId::from(DELEGATE).into(), + ProxyType::Owner, + 0 + )); + + let call = call_sn_owner_hotkey(); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(AccountId::from(DELEGATE)), + AccountId::from(ACCOUNT).into(), + None, + Box::new(call.clone()), + )); + + System::assert_last_event( + pallet_proxy::Event::ProxyExecuted { + result: Err(SystemError::CallFiltered.into()), + } + .into(), + ); + }); +}