Skip to content
30 changes: 30 additions & 0 deletions pallets/admin-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2219,6 +2219,36 @@ pub mod pallet {
Ok(())
}

/// Set whether subnet owner cut is auto-locked for a subnet.
/// It is only callable by root and subnet owner.
#[pallet::call_index(95)]
#[pallet::weight((
Weight::from_parts(25_000_000, 0)
.saturating_add(T::DbWeight::get().reads(4))
.saturating_add(T::DbWeight::get().writes(1)),
DispatchClass::Operational,
Pays::Yes,
))]
pub fn sudo_set_owner_cut_auto_lock_enabled(
origin: OriginFor<T>,
netuid: NetUid,
enabled: bool,
) -> DispatchResult {
pallet_subtensor::Pallet::<T>::ensure_subnet_owner_or_root(origin, netuid)?;
pallet_subtensor::Pallet::<T>::ensure_admin_window_open(netuid)?;

ensure!(
pallet_subtensor::Pallet::<T>::if_subnet_exist(netuid),
Error::<T>::SubnetDoesNotExist
);
ensure!(!netuid.is_root(), Error::<T>::NotPermittedOnRootSubnet);

pallet_subtensor::Pallet::<T>::set_owner_cut_auto_lock_enabled(netuid, enabled);
log::debug!("OwnerCutAutoLockEnabledSet( netuid: {netuid:?}, enabled: {enabled:?} ) ");

Ok(())
}

/// Enables or disables subnet pool-side emission for a subnet.
///
/// This does not remove the subnet from emission share calculation and does not
Expand Down
48 changes: 48 additions & 0 deletions pallets/admin-utils/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2457,6 +2457,54 @@ fn test_sudo_set_owner_cut_enabled() {
});
}

#[test]
fn test_sudo_set_owner_cut_auto_lock_enabled() {
new_test_ext().execute_with(|| {
let netuid = NetUid::from(11);
let owner = U256::from(1234);
let non_owner = U256::from(4321);
let call = RuntimeCall::AdminUtils(crate::Call::sudo_set_owner_cut_auto_lock_enabled {
netuid,
enabled: true,
});

add_network(netuid, 10);
SubnetOwner::<Test>::insert(netuid, owner);

assert_ok!(AdminUtils::sudo_set_admin_freeze_window(
<<Test as Config>::RuntimeOrigin>::root(),
0
));

let dispatch_info = call.get_dispatch_info();
assert_eq!(dispatch_info.pays_fee, Pays::Yes);

assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid));
assert_noop!(
AdminUtils::sudo_set_owner_cut_auto_lock_enabled(
<<Test as Config>::RuntimeOrigin>::signed(non_owner),
netuid,
true
),
DispatchError::BadOrigin
);

assert_ok!(AdminUtils::sudo_set_owner_cut_auto_lock_enabled(
<<Test as Config>::RuntimeOrigin>::signed(owner),
netuid,
false
));
assert!(!SubtensorModule::get_owner_cut_auto_lock_enabled(netuid));

assert_ok!(AdminUtils::sudo_set_owner_cut_auto_lock_enabled(
<<Test as Config>::RuntimeOrigin>::root(),
netuid,
true
));
assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid));
});
}

// cargo test --package pallet-admin-utils --lib -- tests::test_sudo_set_mechanism_count_and_emissions --exact --show-output
#[test]
fn test_sudo_set_mechanism_count_and_emissions() {
Expand Down
25 changes: 24 additions & 1 deletion pallets/subtensor/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use subtensor_runtime_common::{MechId, NetUid, TaoBalance};
use sp_api::ProvideRuntimeApi;

pub use subtensor_custom_rpc_runtime_api::{
DelegateInfoRuntimeApi, NeuronInfoRuntimeApi, SubnetInfoRuntimeApi,
DelegateInfoRuntimeApi, NeuronInfoRuntimeApi, StakeInfoRuntimeApi, SubnetInfoRuntimeApi,
SubnetRegistrationRuntimeApi,
};

Expand Down Expand Up @@ -111,6 +111,13 @@ pub trait SubtensorCustomApi<BlockHash> {
fn get_subnet_to_prune(&self, at: Option<BlockHash>) -> RpcResult<Option<NetUid>>;
#[method(name = "subnetInfo_getSubnetAccountId")]
fn get_subnet_account_id(&self, netuid: NetUid, at: Option<BlockHash>) -> RpcResult<Vec<u8>>;
#[method(name = "stakeInfo_getColdkeyLock")]
fn get_coldkey_lock(
&self,
coldkey: AccountId32,
netuid: NetUid,
at: Option<BlockHash>,
) -> RpcResult<Vec<u8>>;
}

pub struct SubtensorCustom<C, P> {
Expand Down Expand Up @@ -158,6 +165,7 @@ where
C::Api: DelegateInfoRuntimeApi<Block>,
C::Api: NeuronInfoRuntimeApi<Block>,
C::Api: SubnetInfoRuntimeApi<Block>,
C::Api: StakeInfoRuntimeApi<Block>,
C::Api: SubnetRegistrationRuntimeApi<Block>,
{
fn get_delegates(&self, at: Option<<Block as BlockT>::Hash>) -> RpcResult<Vec<u8>> {
Expand Down Expand Up @@ -547,4 +555,19 @@ where
Err(_) => Err(Error::RuntimeError("Subnet does not exist".to_string()).into()),
}
}

fn get_coldkey_lock(
&self,
coldkey: AccountId32,
netuid: NetUid,
at: Option<<Block as BlockT>::Hash>,
) -> RpcResult<Vec<u8>> {
let api = self.client.runtime_api();
let at = at.unwrap_or_else(|| self.client.info().best_hash);

match api.get_coldkey_lock(at, coldkey, netuid) {
Ok(result) => Ok(result.encode()),
Err(e) => Err(Error::RuntimeError(format!("Unable to get coldkey lock: {e:?}")).into()),
}
}
}
2 changes: 2 additions & 0 deletions pallets/subtensor/runtime-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use pallet_subtensor::rpc_info::{
stake_info::StakeInfo,
subnet_info::{SubnetHyperparams, SubnetHyperparamsV2, SubnetInfo, SubnetInfov2},
};
use pallet_subtensor::staking::lock::LockState;
use sp_runtime::AccountId32;
use substrate_fixed::types::U64F64;
use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, TaoBalance};
Expand Down Expand Up @@ -57,6 +58,7 @@ sp_api::decl_runtime_apis! {
fn get_stake_info_for_coldkeys( coldkey_accounts: Vec<AccountId32> ) -> Vec<(AccountId32, Vec<StakeInfo<AccountId32>>)>;
fn get_stake_info_for_hotkey_coldkey_netuid( hotkey_account: AccountId32, coldkey_account: AccountId32, netuid: NetUid ) -> Option<StakeInfo<AccountId32>>;
fn get_stake_fee( origin: Option<(AccountId32, NetUid)>, origin_coldkey_account: AccountId32, destination: Option<(AccountId32, NetUid)>, destination_coldkey_account: AccountId32, amount: u64 ) -> u64;
fn get_coldkey_lock(coldkey: AccountId32, netuid: NetUid) -> Option<LockState>;
fn get_hotkey_conviction(hotkey: AccountId32, netuid: NetUid) -> U64F64;
fn get_most_convicted_hotkey_on_subnet(netuid: NetUid) -> Option<AccountId32>;
}
Expand Down
1 change: 1 addition & 0 deletions pallets/subtensor/src/coinbase/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ impl<T: Config> Pallet<T> {
Yuma3On::<T>::remove(netuid);
AlphaValues::<T>::remove(netuid);
SubtokenEnabled::<T>::remove(netuid);
OwnerCutAutoLockEnabled::<T>::remove(netuid);
ImmuneOwnerUidsLimit::<T>::remove(netuid);

// --- 18. Consensus aux vectors.
Expand Down
30 changes: 23 additions & 7 deletions pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1556,26 +1556,42 @@ pub mod pallet {
OptionQuery,
>;

/// --- MAP ( netuid ) --> LockState | Aggregate owner-coldkey lock for a subnet.
/// --- MAP ( netuid ) --> LockState | Total perpetual lock to the owner hotkey for a subnet.
#[pallet::storage]
pub type OwnerLock<T: Config> = StorageMap<_, Identity, NetUid, LockState, OptionQuery>;

/// --- DMAP ( coldkey, netuid ) --> false | When present, this coldkey's lock decays.
/// Missing entries mean the lock is perpetual.
/// --- MAP ( netuid ) --> LockState | Total decaying lock to the owner hotkey for a subnet.
#[pallet::storage]
pub type DecayingOwnerLock<T: Config> = StorageMap<_, Identity, NetUid, LockState, OptionQuery>;

/// --- DMAP ( coldkey, netuid ) --> false | When present and false, this coldkey's lock is perpetual.
/// Missing entries mean the lock decays by default.
#[pallet::storage]
pub type DecayingLock<T: Config> =
StorageDoubleMap<_, Blake2_128Concat, T::AccountId, Identity, NetUid, bool, OptionQuery>;

/// Default unlock timescale: 90% decay over ~365.25 days at 12s blocks.
/// Default value for owner cut auto-locking.
#[pallet::type_value]
pub fn DefaultOwnerCutAutoLockEnabled<T: Config>() -> bool {
true
}

/// --- MAP ( netuid ) --> bool | Whether subnet owner cut should be auto-locked.
/// Missing entries default to true, so auto-locking is enabled unless explicitly disabled.
#[pallet::storage]
pub type OwnerCutAutoLockEnabled<T: Config> =
StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultOwnerCutAutoLockEnabled<T>>;

/// Default unlock timescale: 50% lock back in 1 month.
#[pallet::type_value]
pub fn DefaultUnlockRate<T: Config>() -> u64 {
1_142_108
311_622
}

/// Default maturity timescale: Conviction is ~5.2x faster than the default unlock rate.
/// Default maturity timescale: 50% conviction in 1 month
#[pallet::type_value]
pub fn DefaultMaturityRate<T: Config>() -> u64 {
216_000
311_622
}

/// --- ITEM( maturity_rate ) | Decay timescale in blocks for lock conviction.
Expand Down
4 changes: 3 additions & 1 deletion pallets/subtensor/src/macros/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ mod hooks {
// Fix testnet Subtensor TotalIssuance after the EVM fees issue.
.saturating_add(migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::<T>())
// Remove deprecated conviction lock storage.
.saturating_add(migrations::migrate_remove_deprecated_conviction_maps::migrate_remove_deprecated_conviction_maps::<T>());
.saturating_add(migrations::migrate_remove_deprecated_conviction_maps::migrate_remove_deprecated_conviction_maps::<T>())
// Reset testnet conviction lock storage before deploying the current design.
.saturating_add(migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::<T>());
weight
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use super::*;
use frame_support::weights::Weight;
use scale_info::prelude::string::String;

/// Clears conviction v2 lock state that only exists on testnet before this
/// conviction design is deployed more broadly.
///
/// `devnet-ready` had `Lock`, `HotkeyLock`, `DecayingHotkeyLock`, `OwnerLock`,
/// and `DecayingLock`, but did not have `DecayingOwnerLock`. `OwnerLock` also
/// used the old owner-coldkey aggregate semantics. Clear these prefixes without
/// decoding values so old or incompatible aggregate bytes are removed safely.
pub fn migrate_reset_tnet_conviction_locks<T: Config>() -> Weight {
let migration_name = b"migrate_reset_tnet_conviction_locks".to_vec();
let mut weight = T::DbWeight::get().reads(1);

if HasMigrationRun::<T>::get(&migration_name) {
log::info!(
"Migration '{:?}' has already run. Skipping.",
String::from_utf8_lossy(&migration_name)
);
return weight;
}

log::info!(
"Running migration '{}'",
String::from_utf8_lossy(&migration_name)
);

// This only affects testnet: mainnet has not had this conviction lock state
// deployed with live values yet.
let lock_removal = Lock::<T>::clear(u32::MAX, None);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Migration clears user-sized lock storage in one upgrade block

Lock, HotkeyLock, DecayingHotkeyLock, OwnerLock, DecayingOwnerLock, and DecayingLock are cleared with u32::MAX in a single on_runtime_upgrade migration. These maps can scale with user lock activity, and computing the weight after clear() does not bound the work already performed. If testnet/devnet has more entries than expected, the upgrade can exceed block limits or stall. Make this bounded with finite clear limits plus a cursor/progress flag, or otherwise gate/prove the exact live state size before running it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Migration clears user-sized lock storage in one upgrade block

This migration calls clear(u32::MAX, None) over Lock and then repeats the same pattern for every aggregate lock map. These maps are user-sized, so a populated testnet/devnet can force the runtime upgrade to delete an unbounded number of keys in one on_runtime_upgrade. Returning the measured weight after the work does not make the upgrade bounded; the block still has to execute all deletions before it can finish. Please convert this to a bounded migration with a cursor and per-block limit, or gate it behind a provable small upper bound for the target network state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Migration clears user-sized lock storage in one upgrade block

This migration calls clear(u32::MAX, None) on Lock and then repeats the same unbounded clear for five more conviction maps. These maps are user-sized, so a runtime upgrade can spend an unbounded amount of work in one on_runtime_upgrade pass before the chain can produce the next block. The returned weight accounts for work after it has already happened; it does not bound execution. Make this migration bounded, e.g. with stored cursors and limited per-block cleanup, or prove from live state that these prefixes are bounded to a safe maximum before shipping the upgrade.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Migration clears user-sized lock storage in one upgrade block

This runtime-upgrade migration clears every entry in Lock, HotkeyLock, DecayingHotkeyLock, OwnerLock, DecayingOwnerLock, and DecayingLock with clear(u32::MAX, None) in a single on_runtime_upgrade pass. Returning the measured weight after deletion does not bound execution; if testnet has user-sized lock state, the upgrade block still has to perform all deletions before the weight is known and can exceed block limits. Make this bounded, e.g. with cursor-based cleanup across blocks or a hard pre-upgrade guarantee that these maps are empty before this migration runs.

weight = weight.saturating_add(
T::DbWeight::get().reads_writes(lock_removal.loops as u64, lock_removal.backend as u64),
);

let hotkey_lock_removal = HotkeyLock::<T>::clear(u32::MAX, None);
weight = weight.saturating_add(T::DbWeight::get().reads_writes(
hotkey_lock_removal.loops as u64,
hotkey_lock_removal.backend as u64,
));

let decaying_hotkey_lock_removal = DecayingHotkeyLock::<T>::clear(u32::MAX, None);
weight = weight.saturating_add(T::DbWeight::get().reads_writes(
decaying_hotkey_lock_removal.loops as u64,
decaying_hotkey_lock_removal.backend as u64,
));

let owner_lock_removal = OwnerLock::<T>::clear(u32::MAX, None);
weight = weight.saturating_add(T::DbWeight::get().reads_writes(
owner_lock_removal.loops as u64,
owner_lock_removal.backend as u64,
));

let decaying_owner_lock_removal = DecayingOwnerLock::<T>::clear(u32::MAX, None);
weight = weight.saturating_add(T::DbWeight::get().reads_writes(
decaying_owner_lock_removal.loops as u64,
decaying_owner_lock_removal.backend as u64,
));

let decaying_lock_removal = DecayingLock::<T>::clear(u32::MAX, None);
weight = weight.saturating_add(T::DbWeight::get().reads_writes(
decaying_lock_removal.loops as u64,
decaying_lock_removal.backend as u64,
));

HasMigrationRun::<T>::insert(&migration_name, true);
weight = weight.saturating_add(T::DbWeight::get().writes(1));

log::info!(
"Migration '{:?}' completed successfully. Removed Lock: {:?}, HotkeyLock: {:?}, DecayingHotkeyLock: {:?}, OwnerLock: {:?}, DecayingOwnerLock: {:?}, DecayingLock: {:?}.",
String::from_utf8_lossy(&migration_name),
lock_removal.backend,
hotkey_lock_removal.backend,
decaying_hotkey_lock_removal.backend,
owner_lock_removal.backend,
decaying_owner_lock_removal.backend,
decaying_lock_removal.backend,
);

weight
}
1 change: 1 addition & 0 deletions pallets/subtensor/src/migrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub mod migrate_remove_unused_maps_and_values;
pub mod migrate_remove_zero_total_hotkey_alpha;
pub mod migrate_reset_bonds_moving_average;
pub mod migrate_reset_max_burn;
pub mod migrate_reset_tnet_conviction_locks;
pub mod migrate_reset_unactive_sn;
pub mod migrate_set_first_emission_block_number;
pub mod migrate_set_min_burn;
Expand Down
Loading
Loading