use super::*; use crate::epoch::run_epoch::EpochTerms; use alloc::collections::BTreeMap; use subtensor_runtime_common::{AlphaBalance, NetUid}; /// 14 days in blocks (assuming ~12 second blocks) /// 14 * 24 * 60 * 60 / 12 = 100800 blocks pub const VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS: u64 = 100800; /// Maximum alpha value (1.0 represented as u64 with 18 decimals) pub const MAX_VOTING_POWER_EMA_ALPHA: u64 = 1_000_000_000_000_000_000; impl Pallet { // ======================== // === Getters === // ======================== /// Get voting power for a hotkey on a subnet. /// Returns 0 if not found or tracking disabled. pub fn get_voting_power(netuid: NetUid, hotkey: &T::AccountId) -> u64 { VotingPower::::get(netuid, hotkey) } /// Check if voting power tracking is enabled for a subnet. pub fn get_voting_power_tracking_enabled(netuid: NetUid) -> bool { VotingPowerTrackingEnabled::::get(netuid) } /// Get the block at which voting power tracking will be disabled. /// Returns 0 if not scheduled for disabling. pub fn get_voting_power_disable_at_block(netuid: NetUid) -> u64 { VotingPowerDisableAtBlock::::get(netuid) } /// Get the EMA alpha value for voting power calculation on a subnet. pub fn get_voting_power_ema_alpha(netuid: NetUid) -> u64 { VotingPowerEmaAlpha::::get(netuid) } // ======================== // === Extrinsic Handlers === // ======================== /// Enable voting power tracking for a subnet. pub fn do_enable_voting_power_tracking(netuid: NetUid) -> DispatchResult { ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); // Enable tracking VotingPowerTrackingEnabled::::insert(netuid, true); // Clear any scheduled disable VotingPowerDisableAtBlock::::remove(netuid); // Emit event Self::deposit_event(Event::VotingPowerTrackingEnabled { netuid }); log::info!("VotingPower tracking enabled for netuid {netuid:?}"); Ok(()) } /// Schedule disabling of voting power tracking for a subnet. /// Tracking will continue for 14 days, then automatically disable. pub fn do_disable_voting_power_tracking(netuid: NetUid) -> DispatchResult { ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); // Check if tracking is enabled ensure!( Self::get_voting_power_tracking_enabled(netuid), Error::::VotingPowerTrackingNotEnabled ); // Calculate the block at which tracking will be disabled let current_block = Self::get_current_block_as_u64(); let disable_at_block = current_block.saturating_add(VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS); // Schedule disable VotingPowerDisableAtBlock::::insert(netuid, disable_at_block); // Emit event Self::deposit_event(Event::VotingPowerTrackingDisableScheduled { netuid, disable_at_block, }); log::info!( "VotingPower tracking scheduled to disable at block {disable_at_block:?} for netuid {netuid:?}" ); Ok(()) } /// Set the EMA alpha value for voting power calculation on a subnet. pub fn do_set_voting_power_ema_alpha(netuid: NetUid, alpha: u64) -> DispatchResult { ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); // Validate alpha (must be <= 1.0, represented as 10^18) ensure!( alpha <= MAX_VOTING_POWER_EMA_ALPHA, Error::::InvalidVotingPowerEmaAlpha ); // Set the alpha VotingPowerEmaAlpha::::insert(netuid, alpha); // Emit event Self::deposit_event(Event::VotingPowerEmaAlphaSet { netuid, alpha }); log::info!("VotingPower EMA alpha set to {alpha:?} for netuid {netuid:?}"); Ok(()) } // ======================== // === Epoch Processing === // ======================== /// Update voting power for all validators on a subnet using pre-calculated epoch terms. pub fn update_voting_power_for_subnet( netuid: NetUid, epoch_output: &BTreeMap, ) { // Early exit if tracking not enabled if !Self::get_voting_power_tracking_enabled(netuid) { return; } // Check if past grace period and should finalize disable let disable_at = Self::get_voting_power_disable_at_block(netuid); if disable_at > 0 { let current_block = Self::get_current_block_as_u64(); if current_block >= disable_at { Self::finalize_voting_power_disable(netuid); return; } // Still in grace period - continue updating } // Get the EMA alpha value for this subnet let alpha = Self::get_voting_power_ema_alpha(netuid); // Get minimum stake threshold for validator permit let min_stake = Self::get_stake_threshold(); // Iterate over epoch output using pre-calculated values for (hotkey, terms) in epoch_output.iter() { // Only validators (with vpermit) get voting power, not miners if terms.new_validator_permit { // Use the subnet-specific stake from epoch calculation Self::update_voting_power_for_hotkey(netuid, hotkey, terms.stake, alpha, min_stake); } else { // Miner without vpermit - remove any existing voting power VotingPower::::remove(netuid, hotkey); } } // Remove voting power for any hotkeys that are no longer registered on this subnet Self::clear_voting_power_for_deregistered_hotkeys(netuid); log::trace!("VotingPower updated for validators on netuid {netuid:?}"); } /// Clear voting power for hotkeys that are no longer registered on the subnet. fn clear_voting_power_for_deregistered_hotkeys(netuid: NetUid) { // Collect hotkeys to remove (can't mutate while iterating) let hotkeys_to_remove: Vec = VotingPower::::iter_prefix(netuid) .filter_map(|(hotkey, _)| { // If the hotkey is not a network member, it's deregistered if !IsNetworkMember::::get(&hotkey, netuid) { Some(hotkey) } else { None } }) .collect(); // Remove voting power for deregistered hotkeys for hotkey in hotkeys_to_remove { VotingPower::::remove(netuid, &hotkey); log::trace!( "VotingPower removed for deregistered hotkey {hotkey:?} on netuid {netuid:?}" ); } } /// Update voting power EMA for a single hotkey using subnet-specific stake. fn update_voting_power_for_hotkey( netuid: NetUid, hotkey: &T::AccountId, current_stake: AlphaBalance, alpha: u64, min_stake: u64, ) { // Get previous EMA value let previous_ema = VotingPower::::get(netuid, hotkey); // Calculate new EMA value // new_ema = alpha * current_stake + (1 - alpha) * previous_ema // All values use 18 decimal precision for alpha (alpha is in range [0, 10^18]) let new_ema = Self::calculate_voting_power_ema(current_stake.to_u64(), previous_ema, alpha); // Only remove if they previously had voting power ABOVE threshold and decayed below. // This allows new validators to build up voting power from 0 without being removed. if new_ema < min_stake && previous_ema >= min_stake { // Was above threshold, now decayed below - remove VotingPower::::remove(netuid, hotkey); log::trace!( "VotingPower removed for hotkey {hotkey:?} on netuid {netuid:?} (decayed below removal threshold: {new_ema:?} < {min_stake:?})" ); } else if new_ema > 0 { // Update voting power (building up or maintaining) VotingPower::::insert(netuid, hotkey, new_ema); log::trace!( "VotingPower updated for hotkey {hotkey:?} on netuid {netuid:?}: {previous_ema:?} -> {new_ema:?}" ); } // If new_ema == 0 do nothing } /// Calculate EMA for voting power. /// new_ema = alpha * current_stake + (1 - alpha) * previous_ema /// Alpha is in 18 decimal precision (10^18 = 1.0) fn calculate_voting_power_ema(current_stake: u64, previous_ema: u64, alpha: u64) -> u64 { // Use u128 for intermediate calculations to avoid overflow let alpha_128 = alpha as u128; let one_minus_alpha = (MAX_VOTING_POWER_EMA_ALPHA as u128).saturating_sub(alpha_128); let current_128 = current_stake as u128; let previous_128 = previous_ema as u128; // new_ema = (alpha * current_stake + (1 - alpha) * previous_ema) / 10^18 let numerator = alpha_128 .saturating_mul(current_128) .saturating_add(one_minus_alpha.saturating_mul(previous_128)); let result = numerator .checked_div(MAX_VOTING_POWER_EMA_ALPHA as u128) .unwrap_or(0); // Safely convert back to u64, saturating at u64::MAX result.min(u64::MAX as u128) as u64 } /// Finalize the disabling of voting power tracking. /// Clears all VotingPower entries for the subnet. fn finalize_voting_power_disable(netuid: NetUid) { // Clear all VotingPower entries for this subnet let _ = VotingPower::::clear_prefix(netuid, u32::MAX, None); // Disable tracking VotingPowerTrackingEnabled::::insert(netuid, false); // Clear disable schedule VotingPowerDisableAtBlock::::remove(netuid); // Emit event Self::deposit_event(Event::VotingPowerTrackingDisabled { netuid }); log::info!("VotingPower tracking disabled and entries cleared for netuid {netuid:?}"); } // ======================== // === Hotkey Swap === // ======================== /// Transfer voting power from old hotkey to new hotkey during swap. pub fn swap_voting_power_for_hotkey( old_hotkey: &T::AccountId, new_hotkey: &T::AccountId, netuid: NetUid, ) { // Get voting power from old hotkey let voting_power = VotingPower::::take(netuid, old_hotkey); // Transfer to new hotkey if non-zero if voting_power > 0 { // Add to any existing voting power on new hotkey (in case new hotkey already has some) let existing = VotingPower::::get(netuid, new_hotkey); VotingPower::::insert(netuid, new_hotkey, voting_power.saturating_add(existing)); log::trace!( "VotingPower transferred from {old_hotkey:?} to {new_hotkey:?} on netuid {netuid:?}: {voting_power:?}" ); } } }