Skip to content

Extrinsic for setting sn owner hotkey #1513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions pallets/admin-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>,
netuid: u16,
hotkey: T::AccountId,
) -> DispatchResult {
pallet_subtensor::Pallet::<T>::do_set_sn_owner_hotkey(origin, netuid, &hotkey)
}

/// Enables or disables subtoken trading for a given subnet.
///
/// # Arguments
Expand Down
69 changes: 69 additions & 0 deletions pallets/admin-utils/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Test>::insert(netuid, owner);

// Non-owner and non-root cannot set the sn owner hotkey
assert_eq!(
AdminUtils::sudo_set_sn_owner_hotkey(
<<Test as Config>::RuntimeOrigin>::signed(bad_origin_coldkey),
netuid,
hotkey
),
Err(DispatchError::BadOrigin)
);

// SN owner can set the hotkey
assert_ok!(AdminUtils::sudo_set_sn_owner_hotkey(
<<Test as Config>::RuntimeOrigin>::signed(owner),
netuid,
hotkey
));

// Check the value
let actual_hotkey = pallet_subtensor::SubnetOwnerHotkey::<Test>::get(netuid);
assert_eq!(actual_hotkey, hotkey);

// Cannot set again (rate limited)
assert_err!(
AdminUtils::sudo_set_sn_owner_hotkey(
<<Test as Config>::RuntimeOrigin>::signed(owner),
netuid,
hotkey
),
pallet_subtensor::Error::<Test>::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::<Test>::insert(netuid, owner);

// Root can set the hotkey
assert_ok!(AdminUtils::sudo_set_sn_owner_hotkey(
<<Test as Config>::RuntimeOrigin>::root(),
netuid,
hotkey
));

// Check the value
let actual_hotkey = pallet_subtensor::SubnetOwnerHotkey::<Test>::get(netuid);
assert_eq!(actual_hotkey, hotkey);
});
}
6 changes: 6 additions & 0 deletions pallets/subtensor/src/coinbase/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,4 +665,10 @@ impl<T: Config> Pallet<T> {
let halved_interval: I64F64 = interval.saturating_mul(halving);
halved_interval.saturating_to_num::<u64>()
}
pub fn get_rate_limited_last_block(rate_limit_key: &RateLimitKey) -> u64 {
LastRateLimitedBlock::<T>::get(rate_limit_key)
}
pub fn set_rate_limited_last_block(rate_limit_key: &RateLimitKey, block: u64) {
LastRateLimitedBlock::<T>::set(rate_limit_key, block);
}
}
24 changes: 24 additions & 0 deletions pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -874,6 +875,12 @@ pub mod pallet {
360
}

#[pallet::type_value]
/// Default value for setting subnet owner hotkey rate limit
pub fn DefaultSetSNOwnerHotkeyRateLimit<T: Config>() -> u64 {
50400
}

#[pallet::storage]
pub type MinActivityCutoff<T: Config> =
StorageValue<_, u16, ValueQuery, DefaultMinActivityCutoff<T>>;
Expand Down Expand Up @@ -1176,6 +1183,15 @@ pub mod pallet {
pub type WeightsVersionKeyRateLimit<T> =
StorageValue<_, u64, ValueQuery, DefaultWeightsVersionKeyRateLimit<T>>;

/// ============================
/// ==== Rate Limiting =====
/// ============================

#[pallet::storage]
/// --- MAP ( RateLimitKey ) --> Block number in which the last rate limited operation occured
pub type LastRateLimitedBlock<T: Config> =
StorageMap<_, Identity, RateLimitKey, u64, ValueQuery, DefaultZeroU64<T>>;

/// ============================
/// ==== Subnet Locks =====
/// ============================
Expand Down Expand Up @@ -2598,3 +2614,11 @@ impl<T, H, P> CollectiveInterface<T, H, P> 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),
}
4 changes: 2 additions & 2 deletions pallets/subtensor/src/macros/dispatches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>, hotkey: T::AccountId) -> DispatchResult {
Self::do_register_network(origin, &hotkey, 1, None)
}
Expand Down Expand Up @@ -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<T>,
hotkey: T::AccountId,
Expand Down
5 changes: 5 additions & 0 deletions pallets/subtensor/src/staking/stake_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ impl<T: Config> Pallet<T> {
U96F32::saturating_from_num(SubnetMovingPrice::<T>::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
Expand All @@ -70,6 +71,10 @@ impl<T: Config> Pallet<T> {
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::<T>::get(netuid);
let current_ma_unsigned = U96F32::saturating_from_num(SubnetMovingAlpha::<T>::get());
let alpha: U96F32 = current_ma_unsigned.saturating_mul(blocks_since_start_call.safe_div(
Expand Down
67 changes: 67 additions & 0 deletions pallets/subtensor/src/subnets/subnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,73 @@ impl<T: Config> Pallet<T> {
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::<T>::SubnetNotExists);

// Rate limit: 1 call per week
ensure!(
Self::passes_rate_limit_on_subnet(
&TransactionType::SetSNOwnerHotkey,
hotkey, // ignored
netuid, // Specific to a subnet.
),
Error::<T>::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::<T>::insert(netuid, hotkey);

// Return success.
Ok(())
}

pub fn is_valid_subnet_for_emission(netuid: u16) -> bool {
FirstEmissionBlockNumber::<T>::get(netuid).is_some()
}
Expand Down
11 changes: 11 additions & 0 deletions pallets/subtensor/src/utils/rate_limiting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub enum TransactionType {
Unknown,
RegisterNetwork,
SetWeightsVersionKey,
SetSNOwnerHotkey,
}

/// Implement conversion from TransactionType to u16
Expand All @@ -19,6 +20,7 @@ impl From<TransactionType> for u16 {
TransactionType::Unknown => 2,
TransactionType::RegisterNetwork => 3,
TransactionType::SetWeightsVersionKey => 4,
TransactionType::SetSNOwnerHotkey => 5,
}
}
}
Expand All @@ -31,6 +33,7 @@ impl From<u16> for TransactionType {
1 => TransactionType::SetChildkeyTake,
3 => TransactionType::RegisterNetwork,
4 => TransactionType::SetWeightsVersionKey,
5 => TransactionType::SetSNOwnerHotkey,
_ => TransactionType::Unknown,
}
}
Expand All @@ -56,6 +59,8 @@ impl<T: Config> Pallet<T> {
match tx_type {
TransactionType::SetWeightsVersionKey => (Tempo::<T>::get(netuid) as u64)
.saturating_mul(WeightsVersionKeyRateLimit::<T>::get()),
TransactionType::SetSNOwnerHotkey => DefaultSetSNOwnerHotkeyRateLimit::<T>::get(),

_ => Self::get_rate_limit(tx_type),
}
}
Expand Down Expand Up @@ -102,6 +107,9 @@ impl<T: Config> Pallet<T> {
) -> 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::<T>::get((hotkey, netuid, tx_as_u16))
Expand All @@ -126,6 +134,9 @@ impl<T: Config> Pallet<T> {
) {
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::<T>::insert((key, netuid, tx_as_u16), block);
Expand Down
10 changes: 9 additions & 1 deletion runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,15 @@ impl InstanceFilter<RuntimeCall> 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 { .. })
Expand Down
Loading
Loading