use super::*; use sp_runtime::PerU16; use sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet}; use subtensor_runtime_common::NetUid; pub struct PCRelations { /// The distinguished `hotkey` this structure is built around. pivot: T::AccountId, children: BTreeMap, parents: BTreeMap, } impl PCRelations { /// Create empty relations for a given pivot. pub fn new(hotkey: T::AccountId) -> Self { Self { pivot: hotkey, children: BTreeMap::new(), parents: BTreeMap::new(), } } //////////////////////////////////////////////////////////// // Constraint checkers /// Ensures sum(proportions) <= u64::MAX pub fn ensure_total_proportions(children: &BTreeMap) -> DispatchResult { let total: u128 = children .values() .fold(0u128, |acc, &w| acc.saturating_add(w as u128)); ensure!(total <= u64::MAX as u128, Error::::ProportionOverflow); Ok(()) } /// Ensure that the number of children does not exceed 5 pub fn ensure_childkey_count(children: &BTreeMap) -> DispatchResult { ensure!(children.len() <= 5, Error::::TooManyChildren); Ok(()) } /// Ensures the given children or parent set doesn't contain pivot pub fn ensure_no_self_loop( pivot: &T::AccountId, hotkey_set: &BTreeMap, ) -> DispatchResult { ensure!(!hotkey_set.contains_key(pivot), Error::::InvalidChild); Ok(()) } /// Ensures that children and parents sets do not have any overlap pub fn ensure_bipartite_separation( children: &BTreeMap, parents: &BTreeMap, ) -> DispatchResult { let has_overlap = children.keys().any(|c| parents.contains_key(c)); ensure!(!has_overlap, Error::::ChildParentInconsistency); Ok(()) } /// Validate that applying `pending_children_vec` to `relations` (as the new /// pivot->children mapping) preserves all invariants. /// /// Checks: /// 1) No self-loop: pivot must not appear among children. /// 2) Sum of child proportions fits in `u64`. /// 3) Bipartite role separation: no child may also be a parent. pub fn ensure_pending_consistency( &self, pending_children_vec: &Vec<(u64, T::AccountId)>, ) -> DispatchResult { // Build a deduped children map (last proportion wins if duplicates present). let mut new_children: BTreeMap = BTreeMap::new(); for (prop, child) in pending_children_vec { new_children.insert(child.clone(), *prop); } // Check constraints Self::ensure_no_self_loop(&self.pivot, &new_children)?; Self::ensure_childkey_count(&new_children)?; Self::ensure_total_proportions(&new_children)?; Self::ensure_bipartite_separation(&new_children, &self.parents)?; Ok(()) } //////////////////////////////////////////////////////////// // Getters #[inline] pub fn pivot(&self) -> &T::AccountId { &self.pivot } #[inline] pub fn children(&self) -> &BTreeMap { &self.children } #[inline] pub fn parents(&self) -> &BTreeMap { &self.parents } //////////////////////////////////////////////////////////// // Safe updaters /// Replace the pivot->children mapping after validating invariants. /// /// Invariants: /// * No self-loop: child != pivot /// * sum(proportions) fits in u64 (checked as u128 to avoid overflow mid-sum) pub fn link_children(&mut self, new_children: BTreeMap) -> DispatchResult { // Check constraints Self::ensure_no_self_loop(&self.pivot, &new_children)?; Self::ensure_total_proportions(&new_children)?; Self::ensure_bipartite_separation(&new_children, &self.parents)?; self.children = new_children; Ok(()) } pub fn link_parents(&mut self, new_parents: BTreeMap) -> DispatchResult { // Check constraints Self::ensure_no_self_loop(&self.pivot, &new_parents)?; Self::ensure_bipartite_separation(&self.children, &new_parents)?; self.parents = new_parents; Ok(()) } #[inline] fn upsert_edge(list: &mut Vec<(u64, T::AccountId)>, proportion: u64, id: &T::AccountId) { for (p, who) in list.iter_mut() { if who == id { *p = proportion; return; } } list.push((proportion, id.clone())); } #[inline] fn remove_edge(list: &mut Vec<(u64, T::AccountId)>, id: &T::AccountId) { list.retain(|(_, who)| who != id); } /// Change the pivot hotkey for these relations. /// Ensures there are no self-loops with the new pivot. pub fn rebind_pivot(&mut self, new_pivot: T::AccountId) -> DispatchResult { // No self-loop via children or parents for the new pivot. Self::ensure_no_self_loop(&new_pivot, &self.children)?; Self::ensure_no_self_loop(&new_pivot, &self.parents)?; self.pivot = new_pivot; Ok(()) } } impl Pallet { /// Set childkeys vector making sure there are no empty vectors in the state fn set_childkeys(parent: T::AccountId, netuid: NetUid, childkey_vec: Vec<(u64, T::AccountId)>) { if childkey_vec.is_empty() { ChildKeys::::remove(parent, netuid); } else { ChildKeys::::insert(parent, netuid, childkey_vec); } } /// Set parentkeys vector making sure there are no empty vectors in the state fn set_parentkeys( child: T::AccountId, netuid: NetUid, parentkey_vec: Vec<(u64, T::AccountId)>, ) { if parentkey_vec.is_empty() { ParentKeys::::remove(child, netuid); } else { ParentKeys::::insert(child, netuid, parentkey_vec); } } /// Loads all records from ChildKeys and ParentKeys where (hotkey, netuid) is the key. /// Produces a parent->(child->prop) adjacency map that **cannot violate** /// the required consistency because all inserts go through `link`. fn load_child_parent_relations( hotkey: &T::AccountId, netuid: NetUid, ) -> Result, DispatchError> { let mut rel = PCRelations::::new(hotkey.clone()); // Load children: (prop, child) from ChildKeys(hotkey, netuid) let child_links = ChildKeys::::get(hotkey, netuid); let mut children = BTreeMap::::new(); for (prop, child) in child_links { // Ignore any accidental self-loop in storage if child != *hotkey { children.insert(child, prop); } } // Validate & set (enforce no self-loop and sum limit) rel.link_children(children)?; // Load parents: (prop, parent) from ParentKeys(hotkey, netuid) let parent_links = ParentKeys::::get(hotkey, netuid); let mut parents = BTreeMap::::new(); for (prop, parent) in parent_links { if parent != *hotkey { parents.insert(parent, prop); } } // Keep the same validation rules for parents (no self-loop, bounded sum). rel.link_parents(parents)?; Ok(rel) } /// Build a `PCRelations` for `pivot` (parent) from the `PendingChildKeys` queue, /// preserving the current `ParentKeys(pivot, netuid)` so `persist_child_parent_relations` /// won’t accidentally clear existing parents. /// /// PendingChildKeys layout: /// (netuid, pivot) -> (Vec<(proportion, child)>) pub fn load_relations_from_pending( pivot: T::AccountId, pending_children_vec: &Vec<(u64, T::AccountId)>, netuid: NetUid, ) -> Result, DispatchError> { let mut rel = PCRelations::::new(pivot.clone()); // Deduplicate into a BTreeMap (last wins if duplicates). let mut children: BTreeMap = BTreeMap::new(); for (prop, child) in pending_children_vec { if *child != pivot { children.insert(child.clone(), *prop); } } // Enforce invariants (no self-loop, total weight <= u64::MAX) rel.link_children(children)?; // Preserve the current parents of the pivot so `persist_child_parent_relations` // won’t clear them when we only intend to update children. let existing_parents_vec = ParentKeys::::get(pivot.clone(), netuid); let mut parents: BTreeMap = BTreeMap::new(); for (w, parent) in existing_parents_vec { if parent != pivot { parents.insert(parent, w); } } // This uses the same basic checks (no self-loop, bounded sum). // If you didn't expose link_parents, inline the simple validations here. rel.link_parents(parents)?; Ok(rel) } /// Persist the `relations` around `hotkey` to storage, updating both directions: /// * Writes ChildKeys(hotkey, netuid) = children /// and synchronizes ParentKeys(child, netuid) entries accordingly. /// * Writes ParentKeys(hotkey, netuid) = parents /// and synchronizes ChildKeys(parent, netuid) entries accordingly. /// /// This is a **diff-based** update that only touches affected neighbors. pub fn persist_child_parent_relations( relations: PCRelations, netuid: NetUid, weight: &mut Weight, ) -> DispatchResult { let pivot = relations.pivot().clone(); // --------------------------- // 1) Pivot -> Children side // --------------------------- let new_children_map = relations.children(); let new_children_vec: Vec<(u64, T::AccountId)> = new_children_map .iter() .map(|(c, p)| (*p, c.clone())) .collect(); let prev_children_vec = ChildKeys::::get(&pivot, netuid); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 0)); // Overwrite pivot's children vector Self::set_childkeys(pivot.clone(), netuid, new_children_vec.clone()); weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 1)); // Build quick-lookup sets for diffing let prev_children_set: BTreeSet = prev_children_vec.iter().map(|(_, c)| c.clone()).collect(); let new_children_set: BTreeSet = new_children_map.keys().cloned().collect(); // Added children = new / prev for added in new_children_set .iter() .filter(|c| !prev_children_set.contains(*c)) { let p = match new_children_map.get(added) { Some(p) => *p, None => return Err(Error::::ChildParentInconsistency.into()), }; let mut pk = ParentKeys::::get(added.clone(), netuid); PCRelations::::upsert_edge(&mut pk, p, &pivot); Self::set_parentkeys(added.clone(), netuid, pk); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); } // Updated children = intersection where proportion changed for common in new_children_set.intersection(&prev_children_set) { let new_p = match new_children_map.get(common) { Some(p) => *p, None => return Err(Error::::ChildParentInconsistency.into()), }; let mut pk = ParentKeys::::get(common.clone(), netuid); PCRelations::::upsert_edge(&mut pk, new_p, &pivot); Self::set_parentkeys(common.clone(), netuid, pk); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); } // Removed children = prev \ new => remove (pivot) from ParentKeys(child) for removed in prev_children_set .iter() .filter(|c| !new_children_set.contains(*c)) { let mut pk = ParentKeys::::get(removed.clone(), netuid); PCRelations::::remove_edge(&mut pk, &pivot); Self::set_parentkeys(removed.clone(), netuid, pk); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); } // --------------------------- // 2) Parents -> Pivot side // --------------------------- let new_parents_map = relations.parents(); let new_parents_vec: Vec<(u64, T::AccountId)> = new_parents_map .iter() .map(|(p, pr)| (*pr, p.clone())) .collect(); let prev_parents_vec = ParentKeys::::get(&pivot, netuid); // Overwrite pivot's parents vector Self::set_parentkeys(pivot.clone(), netuid, new_parents_vec.clone()); let prev_parents_set: BTreeSet = prev_parents_vec.into_iter().map(|(_, p)| p).collect(); let new_parents_set: BTreeSet = new_parents_map.keys().cloned().collect(); // Added parents = new / prev => ensure ChildKeys(parent) has (p, pivot) for added in new_parents_set .iter() .filter(|p| !prev_parents_set.contains(*p)) { let p_val = match new_parents_map.get(added) { Some(p) => *p, None => return Err(Error::::ChildParentInconsistency.into()), }; let mut ck = ChildKeys::::get(added.clone(), netuid); PCRelations::::upsert_edge(&mut ck, p_val, &pivot); Self::set_childkeys(added.clone(), netuid, ck); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); } // Updated parents = intersection where proportion changed for common in new_parents_set.intersection(&prev_parents_set) { let new_p = new_parents_map .get(common) .ok_or(Error::::ChildParentInconsistency)?; let mut ck = ChildKeys::::get(common.clone(), netuid); PCRelations::::upsert_edge(&mut ck, *new_p, &pivot); Self::set_childkeys(common.clone(), netuid, ck); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); } // Removed parents = prev \ new => remove (pivot) from ChildKeys(parent) for removed in prev_parents_set .iter() .filter(|p| !new_parents_set.contains(*p)) { let mut ck = ChildKeys::::get(removed.clone(), netuid); PCRelations::::remove_edge(&mut ck, &pivot); Self::set_childkeys(removed.clone(), netuid, ck); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); } Ok(()) } /// Swap all parent/child relations from `old_hotkey` to `new_hotkey` on `netuid`. /// Steps: /// 1) Load relations around `old_hotkey` /// 2) Clean up storage references to `old_hotkey` (both directions) /// 3) Rebind pivot to `new_hotkey` /// 4) Persist relations around `new_hotkey` pub fn parent_child_swap_hotkey( old_hotkey: &T::AccountId, new_hotkey: &T::AccountId, netuid: NetUid, weight: &mut Weight, ) -> DispatchResult { // 1) Load the current relations around old_hotkey let mut relations = Self::load_child_parent_relations(old_hotkey, netuid)?; weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 0)); // 2) Clean up all storage entries that reference old_hotkey // 2a) For each child of old_hotkey: remove old_hotkey from ParentKeys(child, netuid) for (child, _) in relations.children().iter() { let mut pk = ParentKeys::::get(child.clone(), netuid); PCRelations::::remove_edge(&mut pk, old_hotkey); Self::set_parentkeys(child.clone(), netuid, pk.clone()); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); } // 2b) For each parent of old_hotkey: remove old_hotkey from ChildKeys(parent, netuid) for (parent, _) in relations.parents().iter() { let mut ck = ChildKeys::::get(parent.clone(), netuid); PCRelations::::remove_edge(&mut ck, old_hotkey); ChildKeys::::insert(parent.clone(), netuid, ck); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); } // 2c) Clear direct maps of old_hotkey ChildKeys::::insert( old_hotkey.clone(), netuid, Vec::<(u64, T::AccountId)>::new(), ); Self::set_parentkeys( old_hotkey.clone(), netuid, Vec::<(u64, T::AccountId)>::new(), ); weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 2)); // 3) Rebind pivot to new_hotkey (validate no self-loop with existing maps) relations.rebind_pivot(new_hotkey.clone())?; // 4) Swap PendingChildKeys( netuid, parent ) --> Vec<(proportion,child), cool_down_block> // Fail if consistency breaks if PendingChildKeys::::contains_key(netuid, old_hotkey) { let (children, cool_down_block) = PendingChildKeys::::get(netuid, old_hotkey); relations.ensure_pending_consistency(&children)?; PendingChildKeys::::remove(netuid, old_hotkey); PendingChildKeys::::insert(netuid, new_hotkey, (children, cool_down_block)); weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); } // 5) Persist relations under the new pivot (diffs vs existing state at new_hotkey) Self::persist_child_parent_relations(relations, netuid, weight) } /// The implementation for the extrinsic do_set_child_singular: Sets a single child. /// This function allows a coldkey to set children keys. /// /// Adds a childkey vector to the PendingChildKeys map and performs a few checks: /// **Signature Verification**: Ensures that the caller has signed the transaction, verifying the coldkey. /// **Root Network Check**: Ensures that the delegation is not on the root network, as child hotkeys are not valid on the root. /// **Network Existence Check**: Ensures that the specified network exists. /// **Ownership Verification**: Ensures that the coldkey owns the hotkey. /// **Hotkey Account Existence Check**: Ensures that the hotkey account already exists. /// **Child count**: Only allow to add up to 5 children per parent /// **Child-Hotkey Distinction**: Ensures that the child is not the same as the hotkey. /// **Minimum stake**: Ensures that the parent key has at least the minimum stake. /// **Proportion check**: Ensure that the sum of the proportions does not exceed u64::MAX. /// **Duplicate check**: Ensure there are no duplicates in the list of children. /// /// # Events /// * `SetChildrenScheduled`: If all checks pass and setting the childkeys is scheduled. /// /// # Errors /// * `MechanismDoesNotExist`: Attempting to register to a non-existent network. /// * `RegistrationNotPermittedOnRootSubnet`: Attempting to register a child on the root network. /// * `NonAssociatedColdKey`: The coldkey does not own the hotkey or the child is the same as the hotkey. /// * `HotKeyAccountNotExists`: The hotkey account does not exist. /// * `TooManyChildren`: Too many children in request. /// pub fn do_schedule_children( origin: OriginFor, hotkey: T::AccountId, netuid: NetUid, children: Vec<(u64, T::AccountId)>, ) -> DispatchResult { // Check that the caller has signed the transaction. (the coldkey of the pairing) let coldkey = ensure_signed(origin)?; log::trace!( "do_set_children( coldkey:{coldkey:?} hotkey:{netuid:?} netuid:{hotkey:?} children:{children:?} )" ); // Ensure the hotkey passes the rate limit. ensure!( TransactionType::SetChildren.passes_rate_limit_on_subnet::( &hotkey, // Specific to a hotkey. netuid, // Specific to a subnet. ), Error::::TxRateLimitExceeded ); // Check that this delegation is not on the root network. Child hotkeys are not valid on root. ensure!( !netuid.is_root(), Error::::RegistrationNotPermittedOnRootSubnet ); // Check that the network we are trying to create the child on exists. ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); // Check that the coldkey owns the hotkey. ensure!( Self::coldkey_owns_hotkey(&coldkey, &hotkey), Error::::NonAssociatedColdKey ); // Ensure there are no duplicates in the list of children. let mut unique_children = Vec::new(); for (_, child_i) in &children { ensure!( !unique_children.contains(child_i), Error::::DuplicateChild ); unique_children.push(child_i.clone()); } // Ensure we don't break consistency when these new childkeys are set: // - Ensure that the number of children does not exceed 5 // - Each child is not the hotkey. // - The sum of the proportions does not exceed u64::MAX. // - Bipartite separation (no A <-> B relations) let relations = Self::load_child_parent_relations(&hotkey, netuid)?; relations.ensure_pending_consistency(&children)?; // Check that the parent key has at least the minimum own stake // if children vector is not empty // (checking with check_weights_min_stake wouldn't work because it considers // grandparent stake in this case) ensure!( children.is_empty() || Self::get_total_stake_for_hotkey(&hotkey) >= StakeThreshold::::get().into() || SubnetOwnerHotkey::::try_get(netuid) .is_ok_and(|owner_hotkey| owner_hotkey.eq(&hotkey)), Error::::NotEnoughStakeToSetChildkeys ); // Set last transaction block let current_block = Self::get_current_block_as_u64(); TransactionType::SetChildren.set_last_block_on_subnet::(&hotkey, netuid, current_block); // Schedule or immediately apply CK Self::schedule_or_apply_ck(netuid, hotkey, children) } /// If the start call occured, schedule children, otherwise, /// apply immediately fn schedule_or_apply_ck( netuid: NetUid, hotkey: T::AccountId, children: Vec<(u64, T::AccountId)>, ) -> DispatchResult { if !SubtokenEnabled::::get(netuid) { Self::persist_pending_chidren_ok(netuid, &hotkey, &children); return Ok(()); } // Calculate cool-down block let cooldown_block = Self::get_current_block_as_u64().saturating_add(PendingChildKeyCooldown::::get()); // Insert or update PendingChildKeys PendingChildKeys::::insert(netuid, hotkey.clone(), (children.clone(), cooldown_block)); // Log and return. log::trace!( "SetChildrenScheduled( netuid:{:?}, cooldown_block:{:?}, hotkey:{:?}, children:{:?} )", cooldown_block, hotkey, netuid, children.clone() ); Self::deposit_event(Event::SetChildrenScheduled( hotkey, netuid, cooldown_block, children, )); // Ok and return. Ok(()) } /// This function executes setting children keys when called during hotkey draining. /// /// * `netuid`: The u16 network identifier where the child keys will exist. /// /// # Events /// * `SetChildren`: On successfully registering children to a hotkey. /// /// # Errors /// * `MechanismDoesNotExist`: Attempting to register to a non-existent network. /// * `RegistrationNotPermittedOnRootSubnet`: Attempting to register a child on the root network. /// * `NonAssociatedColdKey`: The coldkey does not own the hotkey or the child is the same as the hotkey. /// * `HotKeyAccountNotExists`: The hotkey account does not exist. /// /// # Note /// 1. **Old Children Cleanup**: Removes the hotkey from the parent list of its old children. /// 2. **New Children Assignment**: Assigns the new child to the hotkey and updates the parent list for the new child. /// pub fn do_set_pending_children(netuid: NetUid) { let current_block = Self::get_current_block_as_u64(); // If the childkey cools down before the subnet start call + PendingChildKeyCooldown: // - If Start call happened: Normal track // - If Start call didn't happen: Apply immediately // TODO: This check may be removed after all ck are applied after the runtime upgrade let start_call_occured = SubtokenEnabled::::get(netuid); // Iterate over all pending children of this subnet and set as needed let mut to_remove: Vec = Vec::new(); PendingChildKeys::::iter_prefix(netuid).for_each( |(hotkey, (children, cool_down_block))| { if (cool_down_block < current_block) || !start_call_occured { Self::persist_pending_chidren_ok(netuid, &hotkey, &children); to_remove.push(hotkey); } }, ); for hotkey in to_remove { PendingChildKeys::::remove(netuid, hotkey); } } // If child-parent consistency is broken, fail setting new children silently fn persist_pending_chidren_ok( netuid: NetUid, hotkey: &T::AccountId, children: &Vec<(u64, T::AccountId)>, ) { let maybe_relations = Self::load_relations_from_pending(hotkey.clone(), children, netuid); if let Ok(relations) = maybe_relations { let mut _weight: Weight = T::DbWeight::get().reads(0); if let Ok(()) = Self::persist_child_parent_relations(relations, netuid, &mut _weight) { // Log and emit event. log::trace!( "SetChildren( netuid:{:?}, hotkey:{:?}, children:{:?} )", hotkey, netuid, children.clone() ); Self::deposit_event(Event::SetChildren(hotkey.clone(), netuid, children.clone())); } } } /* Retrieves the list of children for a given hotkey and network. /// /// # Arguments /// * `hotkey`: The hotkey whose children are to be retrieved. /// * `netuid`: The network identifier. /// /// # Returns /// * `Vec<(u64, T::AccountId)>`: A vector of tuples containing the proportion and child account ID. /// /// # Example /// ``` /// let children = SubtensorModule::get_children(&hotkey, netuid); */ pub fn get_children(hotkey: &T::AccountId, netuid: NetUid) -> Vec<(u64, T::AccountId)> { ChildKeys::::get(hotkey, netuid) } /* Retrieves the list of parents for a given child and network. /// /// # Arguments /// * `child`: The child whose parents are to be retrieved. /// * `netuid`: The network identifier. /// /// # Returns /// * `Vec<(u64, T::AccountId)>`: A vector of tuples containing the proportion and parent account ID. /// /// # Example /// ``` /// let parents = SubtensorModule::get_parents(&child, netuid); */ pub fn get_parents(child: &T::AccountId, netuid: NetUid) -> Vec<(u64, T::AccountId)> { ParentKeys::::get(child, netuid) } /// Sets the childkey take for a given hotkey. /// /// This function allows a coldkey to set the childkey take for a given hotkey. /// The childkey take determines the proportion of stake that the hotkey keeps for itself /// when distributing stake to its children. /// /// # Arguments /// * `coldkey`: The coldkey that owns the hotkey. /// /// * `hotkey`: The hotkey for which the childkey take will be set. /// /// * `take`: The new childkey take value. This is a ratio represented in parts per 65535, /// where 65535 represents 100%. /// /// # Returns /// * `DispatchResult`: The result of the operation. /// /// # Errors /// * `NonAssociatedColdKey`: The coldkey does not own the hotkey. /// * `InvalidChildkeyTake`: The provided take value is invalid (greater than the maximum allowed take). /// * `TxChildkeyTakeRateLimitExceeded`: The rate limit for changing childkey take has been exceeded. pub fn do_set_childkey_take( coldkey: T::AccountId, hotkey: T::AccountId, netuid: NetUid, take: PerU16, ) -> DispatchResult { // Ensure the coldkey owns the hotkey ensure!( Self::coldkey_owns_hotkey(&coldkey, &hotkey), Error::::NonAssociatedColdKey ); ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); // Ensure the take value is valid ensure!( take.deconstruct() >= Self::get_effective_min_childkey_take(netuid) && take.deconstruct() <= Self::get_max_childkey_take(), Error::::InvalidChildkeyTake ); let current_take = Self::get_childkey_take(&hotkey, netuid); // Check the rate limit for increasing childkey take case if take.deconstruct() > current_take { // Ensure the hotkey passes the rate limit. ensure!( TransactionType::SetChildkeyTake.passes_rate_limit_on_subnet::( &hotkey, // Specific to a hotkey. netuid, // Specific to a subnet. ), Error::::TxChildkeyTakeRateLimitExceeded ); } // Set last transaction block let current_block = Self::get_current_block_as_u64(); TransactionType::SetChildkeyTake.set_last_block_on_subnet::( &hotkey, netuid, current_block, ); // Set the new childkey take value for the given hotkey and network ChildkeyTake::::insert(hotkey.clone(), netuid, take); // Update the last transaction block TransactionType::SetChildkeyTake.set_last_block_on_subnet::( &hotkey, netuid, current_block, ); // Emit the event Self::deposit_event(Event::ChildKeyTakeSet(hotkey.clone(), take)); log::debug!("Childkey take set for hotkey: {hotkey:?} and take: {take:?}"); Ok(()) } /// Gets the childkey take for a given hotkey. /// /// This function retrieves the current childkey take value for a specified hotkey. /// If no specific take value has been set, it returns the default childkey take. /// /// # Arguments /// * `hotkey` (&T::AccountId): The hotkey for which to retrieve the childkey take. /// /// # Returns /// * `u16`: The childkey take value, scaled so `u16::MAX` represents 100%. pub fn get_childkey_take(hotkey: &T::AccountId, netuid: NetUid) -> u16 { ChildkeyTake::::get(hotkey, netuid) .deconstruct() .max(Self::get_effective_min_childkey_take(netuid)) } pub fn get_auto_parent_delegation_enabled(root_validator_hotkey: &T::AccountId) -> bool { AutoParentDelegationEnabled::::get(root_validator_hotkey) } //////////////////////////////////////////////////////////// // State cleaners (for use in migration) // TODO: Deprecate when the state is clean for a while /// Establishes parent-child relationships between all root validators and /// a subnet owner's hotkey on the specified subnet. /// /// For each validator on the root network (netuid 0), this function calls /// `do_schedule_children` to schedule the subnet owner hotkey as a child /// of that root validator on the given subnet, with full proportion (u64::MAX). /// /// # Arguments /// * `netuid`: The subnet on which to establish relationships. /// /// # Returns /// * `DispatchResult`: Ok if at least the setup completes; individual /// scheduling failures per validator are logged but do not abort the loop. pub fn do_set_root_validators_for_subnet(netuid: NetUid) -> DispatchResult { // Cannot set children on root network itself. ensure!( !netuid.is_root(), Error::::RegistrationNotPermittedOnRootSubnet ); // Subnet must exist. ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); // Get the subnet owner hotkey. let subnet_owner_hotkey = SubnetOwnerHotkey::::try_get(netuid).map_err(|_| Error::::SubnetNotExists)?; // Iterate over all root validators and schedule each one as a parent // of the subnet owner hotkey. for (_uid, root_validator_hotkey) in Keys::::iter_prefix(NetUid::ROOT) { // Skip if the root validator is the subnet owner hotkey itself // (cannot be both parent and child). if root_validator_hotkey == subnet_owner_hotkey { continue; } // Skip if root validator disabled auto parent delegation via AutoParentDelegationEnabled flag if !Self::get_auto_parent_delegation_enabled(&root_validator_hotkey) { continue; } // Look up the coldkey that owns this root validator hotkey. let coldkey = Self::get_owning_coldkey_for_hotkey(&root_validator_hotkey); // Build a signed origin from the coldkey. let origin: ::RuntimeOrigin = frame_system::RawOrigin::Signed(coldkey).into(); // Schedule the subnet owner hotkey as a child with full proportion. let children = vec![(u64::MAX, subnet_owner_hotkey.clone())]; if let Err(e) = Self::do_schedule_children(origin, root_validator_hotkey.clone(), netuid, children) { log::warn!( "Failed to schedule children for root validator {:?} on netuid {:?}: {:?}", root_validator_hotkey, netuid, e ); } } Ok(()) } pub fn clean_zero_childkey_vectors(weight: &mut Weight) { // Collect keys to delete first to avoid mutating while iterating. let mut to_remove: Vec<(T::AccountId, NetUid)> = Vec::new(); for (parent, netuid, children) in ChildKeys::::iter() { // Account for the read *weight = weight.saturating_add(T::DbWeight::get().reads(1)); if children.is_empty() { to_remove.push((parent, netuid)); } } // Remove all empty entries for (parent, netuid) in &to_remove { ChildKeys::::remove(parent, netuid); // Account for the write *weight = weight.saturating_add(T::DbWeight::get().writes(1)); } log::info!( target: "runtime", "Removed {} empty childkey vectors.", to_remove.len() ); } /// Remove self-loops in `ChildKeys` and `ParentKeys`. /// If, after removal, a value-vector becomes empty, the storage key is removed. pub fn clean_self_loops(weight: &mut Weight) { // ------------------------------- // 1) ChildKeys: (parent, netuid) -> Vec<(w, child)> // Remove any entries where child == parent. // ------------------------------- let mut to_update_ck: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); let mut to_remove_ck: Vec<(T::AccountId, NetUid)> = Vec::new(); for (parent, netuid, children) in ChildKeys::::iter() { *weight = weight.saturating_add(T::DbWeight::get().reads(1)); // Filter out self-loops let filtered: Vec<(u64, T::AccountId)> = children .clone() .into_iter() .filter(|(_, c)| *c != parent) .collect(); // If nothing changed, skip // (we can detect by comparing lengths; safer is to re-check if any removed existed) // For simplicity, just compare lengths: // If len unchanged and the previous vector had no self-loop, skip. // If there *was* a self-loop and filtered is empty, we'll remove the key. if filtered.len() == children.len() { // No change -> continue continue; } if filtered.is_empty() { to_remove_ck.push((parent, netuid)); } else { to_update_ck.push(((parent, netuid), filtered)); } } // Apply ChildKeys updates/removals for ((parent, netuid), new_vec) in &to_update_ck { Self::set_childkeys(parent.clone(), *netuid, new_vec.clone()); *weight = weight.saturating_add(T::DbWeight::get().writes(1)); } for (parent, netuid) in &to_remove_ck { ChildKeys::::remove(parent, netuid); *weight = weight.saturating_add(T::DbWeight::get().writes(1)); } log::info!( target: "runtime", "Removed {} self-looping childkeys.", to_update_ck.len().saturating_add(to_remove_ck.len()) ); // ------------------------------- // 2) ParentKeys: (child, netuid) -> Vec<(w, parent)> // Remove any entries where parent == child. // ------------------------------- let mut to_update_pk: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); let mut to_remove_pk: Vec<(T::AccountId, NetUid)> = Vec::new(); for (child, netuid, parents) in ParentKeys::::iter() { *weight = weight.saturating_add(T::DbWeight::get().reads(1)); // Filter out self-loops let filtered: Vec<(u64, T::AccountId)> = parents .clone() .into_iter() .filter(|(_, p)| *p != child) .collect(); // If unchanged, skip if filtered.len() == parents.len() { continue; } if filtered.is_empty() { to_remove_pk.push((child, netuid)); } else { to_update_pk.push(((child, netuid), filtered)); } } // Apply ParentKeys updates/removals for ((child, netuid), new_vec) in &to_update_pk { Self::set_parentkeys(child.clone(), *netuid, new_vec.clone()); *weight = weight.saturating_add(T::DbWeight::get().writes(1)); } for (child, netuid) in &to_remove_pk { ParentKeys::::remove(child, netuid); *weight = weight.saturating_add(T::DbWeight::get().writes(1)); } log::info!( target: "runtime", "Removed {} self-looping parentkeys.", to_update_pk.len().saturating_add(to_remove_pk.len()) ); } pub fn clean_zero_parentkey_vectors(weight: &mut Weight) { // Collect keys to delete first to avoid mutating while iterating. let mut to_remove: Vec<(T::AccountId, NetUid)> = Vec::new(); for (parent, netuid, children) in ParentKeys::::iter() { // Account for the read *weight = weight.saturating_add(T::DbWeight::get().reads(1)); if children.is_empty() { to_remove.push((parent, netuid)); } } // Remove all empty entries for (parent, netuid) in &to_remove { ParentKeys::::remove(parent, netuid); // Account for the write *weight = weight.saturating_add(T::DbWeight::get().writes(1)); } log::info!( target: "runtime", "Removed {} empty parentkey vectors.", to_remove.len() ); } /// Make ChildKeys and ParentKeys bidirectionally consistent by /// **removing** entries that don't have a matching counterpart. /// A match means the exact tuple `(p, other_id)` is present on the opposite map. /// /// Rules: /// * For each (parent, netuid) -> [(p, child)...] in ChildKeys: /// keep only those (p, child) that appear in ParentKeys(child, netuid) as (p, parent). /// If resulting list is empty, remove the key. /// * For each (child, netuid) -> [(p, parent)...] in ParentKeys: /// keep only those (p, parent) that appear in ChildKeys(parent, netuid) as (p, child). /// If resulting list is empty, remove the key. pub fn repair_child_parent_consistency(weight: &mut Weight) { // ------------------------------- // 1) Prune ChildKeys by checking ParentKeys // ------------------------------- let mut ck_updates: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); let mut ck_removes: Vec<(T::AccountId, NetUid)> = Vec::new(); for (parent, netuid, children) in ChildKeys::::iter() { *weight = weight.saturating_add(T::DbWeight::get().reads(1)); // Keep (p, child) only if ParentKeys(child, netuid) contains (p, parent) let mut filtered: Vec<(u64, T::AccountId)> = Vec::with_capacity(children.len()); for (p, child) in children.clone().into_iter() { let rev = ParentKeys::::get(&child, netuid); *weight = weight.saturating_add(T::DbWeight::get().reads(1)); let has_match = rev.iter().any(|(pr, pa)| *pr == p && *pa == parent); if has_match { filtered.push((p, child)); } } if filtered.is_empty() { ck_removes.push((parent, netuid)); } else { // Only write if changed if children != filtered { ck_updates.push(((parent, netuid), filtered)); } } } for ((parent, netuid), new_vec) in &ck_updates { Self::set_childkeys(parent.clone(), *netuid, new_vec.clone()); *weight = weight.saturating_add(T::DbWeight::get().writes(1)); } for (parent, netuid) in &ck_removes { ChildKeys::::remove(parent, netuid); *weight = weight.saturating_add(T::DbWeight::get().writes(1)); } log::info!( target: "runtime", "Updated {} childkey inconsistent records.", ck_updates.len() ); log::info!( target: "runtime", "Removed {} childkey inconsistent records.", ck_removes.len() ); // ------------------------------- // 2) Prune ParentKeys by checking ChildKeys // ------------------------------- let mut pk_updates: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); let mut pk_removes: Vec<(T::AccountId, NetUid)> = Vec::new(); for (child, netuid, parents) in ParentKeys::::iter() { *weight = weight.saturating_add(T::DbWeight::get().reads(1)); // Keep (p, parent) only if ChildKeys(parent, netuid) contains (p, child) let mut filtered: Vec<(u64, T::AccountId)> = Vec::with_capacity(parents.len()); for (p, parent) in parents.clone().into_iter() { let fwd = ChildKeys::::get(&parent, netuid); *weight = weight.saturating_add(T::DbWeight::get().reads(1)); let has_match = fwd.iter().any(|(pr, ch)| *pr == p && *ch == child); if has_match { filtered.push((p, parent)); } } if filtered.is_empty() { pk_removes.push((child, netuid)); } else { // Only write if changed if parents != filtered { pk_updates.push(((child, netuid), filtered)); } } } for ((child, netuid), new_vec) in &pk_updates { Self::set_parentkeys(child.clone(), *netuid, new_vec.clone()); *weight = weight.saturating_add(T::DbWeight::get().writes(1)); } for (child, netuid) in &pk_removes { ParentKeys::::remove(child, netuid); *weight = weight.saturating_add(T::DbWeight::get().writes(1)); } log::info!( target: "runtime", "Updated {} parentkey inconsistent records.", pk_updates.len() ); log::info!( target: "runtime", "Removed {} parentkey inconsistent records.", pk_removes.len() ); } }