diff --git a/pallets/subtensor/src/epoch/run_epoch.rs b/pallets/subtensor/src/epoch/run_epoch.rs index 793f6fbe4a..962c5bbbb4 100644 --- a/pallets/subtensor/src/epoch/run_epoch.rs +++ b/pallets/subtensor/src/epoch/run_epoch.rs @@ -627,11 +627,15 @@ impl Pallet { // Get the minimum stake required. let min_stake = Self::get_stake_threshold(); + // Get owner uid. + let owner_uid: Option = Self::get_owner_uid(netuid); + // Set stake of validators that doesn't meet the staking threshold to 0 as filter. let mut filtered_stake: Vec = total_stake .iter() - .map(|&s| { - if fixed64_to_u64(s) < min_stake { + .enumerate() + .map(|(uid, &s)| { + if owner_uid != Some(uid as u16) && fixed64_to_u64(s) < min_stake { return I64F64::from(0); } s @@ -648,7 +652,12 @@ impl Pallet { // ======================= // Get current validator permits. - let validator_permits: Vec = Self::get_validator_permit(netuid); + let mut validator_permits: Vec = Self::get_validator_permit(netuid); + if let Some(owner_uid) = owner_uid + && let Some(owner_permit) = validator_permits.get_mut(owner_uid as usize) + { + *owner_permit = true; + } log::trace!("validator_permits: {validator_permits:?}"); // Logical negation of validator_permits. @@ -659,8 +668,13 @@ impl Pallet { log::trace!("max_allowed_validators: {max_allowed_validators:?}"); // Get new validator permits. - let new_validator_permits: Vec = + let mut new_validator_permits: Vec = is_topk_nonzero(&stake, max_allowed_validators as usize); + if let Some(owner_uid) = owner_uid + && let Some(owner_permit) = new_validator_permits.get_mut(owner_uid as usize) + { + *owner_permit = true; + } log::trace!("new_validator_permits: {new_validator_permits:?}"); // ================== @@ -683,8 +697,6 @@ impl Pallet { // == Weights == // ============= - let owner_uid: Option = Self::get_owner_uid(netuid); - // Access network weights row unnormalized. let mut weights: Vec> = Self::get_weights_sparse(netuid_index); log::trace!("Weights: {:?}", &weights); diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 54ccadc1a1..7730df0cab 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -2477,11 +2477,17 @@ pub mod pallet { impl Pallet { /// Is the caller allowed to set weights pub fn check_weights_min_stake(hotkey: &T::AccountId, netuid: NetUid) -> bool { + // Allow the subnet owner hotkey to set weights regardless of stake. + if let Some(owner_uid) = Self::get_owner_uid(netuid) + && Uids::::get(netuid, hotkey) == Some(owner_uid) + { + return true; + } + // Blacklist weights transactions for low stake peers. let (total_stake, _, _) = Self::get_stake_weights_for_hotkey_on_subnet(hotkey, netuid); total_stake >= Self::get_stake_threshold() } - /// Helper function to check if register is allowed pub fn checked_allowed_register(netuid: NetUid) -> bool { if netuid.is_root() { diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index f70b83f52d..9074b4d4b3 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -83,7 +83,7 @@ mod dispatches { /// - Attempting to set weights with max value exceeding limit. #[pallet::call_index(0)] #[pallet::weight((Weight::from_parts(15_540_000_000, 0) - .saturating_add(T::DbWeight::get().reads(4111_u64)) + .saturating_add(T::DbWeight::get().reads(4112_u64)) .saturating_add(T::DbWeight::get().writes(2)), DispatchClass::Normal, Pays::No))] pub fn set_weights( origin: OriginFor, @@ -206,7 +206,7 @@ mod dispatches { /// #[pallet::call_index(80)] #[pallet::weight((Weight::from_parts(95_460_000, 0) - .saturating_add(T::DbWeight::get().reads(15_u64)) + .saturating_add(T::DbWeight::get().reads(16_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)), DispatchClass::Normal, Pays::No))] pub fn batch_set_weights( origin: OriginFor, @@ -356,7 +356,7 @@ mod dispatches { /// #[pallet::call_index(97)] #[pallet::weight((Weight::from_parts(122_000_000, 0) - .saturating_add(T::DbWeight::get().reads(17_u64)) + .saturating_add(T::DbWeight::get().reads(18_u64)) .saturating_add(T::DbWeight::get().writes(2)), DispatchClass::Normal, Pays::No))] pub fn reveal_weights( origin: OriginFor, @@ -569,7 +569,7 @@ mod dispatches { /// - The input vectors are of mismatched lengths. #[pallet::call_index(98)] #[pallet::weight((Weight::from_parts(412_000_000, 0) - .saturating_add(T::DbWeight::get().reads(17_u64)) + .saturating_add(T::DbWeight::get().reads(18_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)), DispatchClass::Normal, Pays::No))] pub fn batch_reveal_weights( origin: OriginFor, diff --git a/pallets/subtensor/src/subnets/weights.rs b/pallets/subtensor/src/subnets/weights.rs index 652815dfa4..43e3c7e4ba 100644 --- a/pallets/subtensor/src/subnets/weights.rs +++ b/pallets/subtensor/src/subnets/weights.rs @@ -1161,10 +1161,17 @@ impl Pallet { if Self::is_self_weight(uid, uids, weights) { return true; } + + // Allow the subnet owner hotkey to act as a validator regardless of permit state. + if let Some(owner_uid) = Self::get_owner_uid(netuid) + && owner_uid == uid + { + return true; + } + // Check if uid has validator permit. Self::get_validator_permit_for_uid(netuid, uid) } - /// Returns True if the uids and weights are have a valid length for uid on network. pub fn check_length(netuid: NetUid, uid: u16, uids: &[u16], weights: &[u16]) -> bool { let subnet_n: usize = Self::get_subnetwork_n(netuid) as usize; diff --git a/pallets/subtensor/src/tests/weights.rs b/pallets/subtensor/src/tests/weights.rs index 13baf63a8b..4539b13b9d 100644 --- a/pallets/subtensor/src/tests/weights.rs +++ b/pallets/subtensor/src/tests/weights.rs @@ -6858,3 +6858,109 @@ fn test_reveal_crv3_commits_legacy_payload_success() { ); }); } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_subnet_owner_can_validate_without_stake_or_manual_permit --exact --show-output --nocapture +#[test] +fn test_subnet_owner_can_validate_without_stake_or_manual_permit() { + new_test_ext(0).execute_with(|| { + let owner_hotkey = U256::from(10); + let owner_coldkey = U256::from(11); + let other_hotkey = U256::from(20); + let other_coldkey = U256::from(21); + + // Create a real dynamic subnet whose owner hotkey is `owner_hotkey`. + let netuid = add_dynamic_network_disable_commit_reveal(&owner_hotkey, &owner_coldkey); + + // Add one non-owner neuron with deterministic subnet stake. + register_ok_neuron(netuid, other_hotkey, other_coldkey, 0); + SubtensorModule::add_balance_to_coldkey_account(&other_coldkey, 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &other_hotkey, + &other_coldkey, + netuid, + 1.into(), + ); + + let owner_uid = + SubtensorModule::get_owner_uid(netuid).expect("subnet owner should resolve to a uid"); + let registered_owner_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &owner_hotkey) + .expect("owner hotkey should be registered on the subnet"); + assert_eq!(registered_owner_uid, owner_uid); + + let other_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &other_hotkey) + .expect("other hotkey should be registered on the subnet"); + + let (owner_weight_stake, _, _) = + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&owner_hotkey, netuid); + let (other_weight_stake, _, _) = + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&other_hotkey, netuid); + assert!(owner_weight_stake < other_weight_stake); + + // Make the non-owner stake-qualified while the owner remains below threshold. + SubtensorModule::set_stake_threshold(1_u64); + assert!(SubtensorModule::check_weights_min_stake( + &other_hotkey, + netuid + )); + + // Clear all explicit permits. The owner should not rely on manual permit state. + SubtensorModule::set_validator_permit_for_uid(netuid, owner_uid, false); + SubtensorModule::set_validator_permit_for_uid(netuid, other_uid, false); + assert!(!SubtensorModule::get_validator_permit_for_uid( + netuid, owner_uid + )); + assert!(!SubtensorModule::get_validator_permit_for_uid( + netuid, other_uid + )); + + // Sanity check: a non-owner without a permit still cannot set non-self weights. + assert!(!SubtensorModule::check_validator_permit( + netuid, + other_uid, + &[owner_uid], + &[1u16], + )); + assert_eq!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(other_hotkey), + netuid, + vec![owner_uid], + vec![1u16], + 0, + ), + Err(Error::::NeuronNoValidatorPermit.into()) + ); + + // The subnet owner bypasses both the stake gate and the validator-permit gate. + assert!(SubtensorModule::check_weights_min_stake( + &owner_hotkey, + netuid + )); + assert!(SubtensorModule::check_validator_permit( + netuid, + owner_uid, + &[other_uid], + &[1u16], + )); + + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(owner_hotkey), + netuid, + vec![other_uid], + vec![1u16], + 0, + )); + + // After an epoch, the owner is still validator-eligible even though only the + step_epochs(1, netuid); + assert!(SubtensorModule::get_validator_permit_for_uid( + netuid, owner_uid + )); + + // The original top-k result is preserved; the owner is added on top. + assert!(SubtensorModule::get_validator_permit_for_uid( + netuid, other_uid + )); + }); +}