From 085350a5fe6262702f21c71e65b65292784b17cc Mon Sep 17 00:00:00 2001 From: konrad0960 Date: Mon, 1 Dec 2025 23:03:16 +0100 Subject: [PATCH 1/2] commit Cargo.lock --- evm-tests/src/contracts/votingPower.ts | 85 +++ evm-tests/test/votingPower.precompile.test.ts | 206 ++++++ pallets/admin-utils/src/lib.rs | 2 + pallets/subtensor/src/epoch/run_epoch.rs | 3 + pallets/subtensor/src/lib.rs | 49 +- pallets/subtensor/src/macros/dispatches.rs | 91 +++ pallets/subtensor/src/macros/errors.rs | 4 + pallets/subtensor/src/macros/events.rs | 29 + pallets/subtensor/src/swap/swap_hotkey.rs | 5 + pallets/subtensor/src/tests/mod.rs | 1 + pallets/subtensor/src/tests/voting_power.rs | 603 ++++++++++++++++++ pallets/subtensor/src/utils/mod.rs | 1 + pallets/subtensor/src/utils/voting_power.rs | 314 +++++++++ precompiles/src/lib.rs | 8 +- precompiles/src/voting_power.rs | 112 ++++ 15 files changed, 1511 insertions(+), 2 deletions(-) create mode 100644 evm-tests/src/contracts/votingPower.ts create mode 100644 evm-tests/test/votingPower.precompile.test.ts create mode 100644 pallets/subtensor/src/tests/voting_power.rs create mode 100644 pallets/subtensor/src/utils/voting_power.rs create mode 100644 precompiles/src/voting_power.rs diff --git a/evm-tests/src/contracts/votingPower.ts b/evm-tests/src/contracts/votingPower.ts new file mode 100644 index 0000000000..7cbc5b30d0 --- /dev/null +++ b/evm-tests/src/contracts/votingPower.ts @@ -0,0 +1,85 @@ +export const IVOTING_POWER_ADDRESS = "0x0000000000000000000000000000000000000806"; + +export const IVotingPowerABI = [ + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + }, + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + } + ], + "name": "getVotingPower", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "isVotingPowerTrackingEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "getVotingPowerDisableAtBlock", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "getVotingPowerEmaAlpha", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/evm-tests/test/votingPower.precompile.test.ts b/evm-tests/test/votingPower.precompile.test.ts new file mode 100644 index 0000000000..8337437c93 --- /dev/null +++ b/evm-tests/test/votingPower.precompile.test.ts @@ -0,0 +1,206 @@ +import * as assert from "assert"; + +import { getDevnetApi, getRandomSubstrateKeypair, getAliceSigner, getSignerFromKeypair, waitForTransactionWithRetry } from "../src/substrate" +import { getPublicClient } from "../src/utils"; +import { ETH_LOCAL_URL } from "../src/config"; +import { devnet } from "@polkadot-api/descriptors" +import { PublicClient } from "viem"; +import { PolkadotSigner, TypedApi } from "polkadot-api"; +import { toViemAddress, convertPublicKeyToSs58 } from "../src/address-utils" +import { IVotingPowerABI, IVOTING_POWER_ADDRESS } from "../src/contracts/votingPower" +import { forceSetBalanceToSs58Address, addNewSubnetwork, startCall } from "../src/subtensor"; + +describe("Test VotingPower Precompile", () => { + // init substrate part + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + let publicClient: PublicClient; + + let api: TypedApi; + + // sudo account alice as signer + let alice: PolkadotSigner; + + // init other variable + let subnetId = 0; + + before(async () => { + // init variables got from await and async + publicClient = await getPublicClient(ETH_LOCAL_URL) + api = await getDevnetApi() + alice = await getAliceSigner(); + + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + + let netuid = await addNewSubnetwork(api, hotkey, coldkey) + await startCall(api, netuid, coldkey) + subnetId = netuid + }) + + describe("VotingPower Tracking Status Functions", () => { + it("isVotingPowerTrackingEnabled returns false by default", async () => { + const isEnabled = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "isVotingPowerTrackingEnabled", + args: [subnetId] + }) + + assert.ok(isEnabled !== undefined, "isVotingPowerTrackingEnabled should return a value"); + assert.strictEqual(typeof isEnabled, 'boolean', "isVotingPowerTrackingEnabled should return a boolean"); + // By default, voting power tracking is disabled + assert.strictEqual(isEnabled, false, "Voting power tracking should be disabled by default"); + }); + + it("getVotingPowerDisableAtBlock returns 0 when not scheduled", async () => { + const disableAtBlock = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPowerDisableAtBlock", + args: [subnetId] + }) + + assert.ok(disableAtBlock !== undefined, "getVotingPowerDisableAtBlock should return a value"); + assert.strictEqual(typeof disableAtBlock, 'bigint', "getVotingPowerDisableAtBlock should return a bigint"); + assert.strictEqual(disableAtBlock, BigInt(0), "Disable at block should be 0 when not scheduled"); + }); + + it("getVotingPowerEmaAlpha returns default alpha value", async () => { + const alpha = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPowerEmaAlpha", + args: [subnetId] + }) + + assert.ok(alpha !== undefined, "getVotingPowerEmaAlpha should return a value"); + assert.strictEqual(typeof alpha, 'bigint', "getVotingPowerEmaAlpha should return a bigint"); + // Default alpha is 0.1 * 10^18 = 100_000_000_000_000_000 + assert.strictEqual(alpha, BigInt("100000000000000000"), "Default alpha should be 0.1 (100_000_000_000_000_000)"); + }); + }); + + describe("VotingPower Query Functions", () => { + it("getVotingPower returns 0 for hotkey without voting power", async () => { + // Convert hotkey public key to bytes32 format (0x prefixed hex string) + const hotkeyBytes32 = '0x' + Buffer.from(hotkey.publicKey).toString('hex'); + + const votingPower = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPower", + args: [subnetId, hotkeyBytes32 as `0x${string}`] + }) + + assert.ok(votingPower !== undefined, "getVotingPower should return a value"); + assert.strictEqual(typeof votingPower, 'bigint', "getVotingPower should return a bigint"); + // Without voting power tracking enabled, voting power should be 0 + assert.strictEqual(votingPower, BigInt(0), "Voting power should be 0 when tracking is disabled"); + }); + + it("getVotingPower returns 0 for unknown hotkey", async () => { + // Generate a random hotkey that doesn't exist + const randomHotkey = getRandomSubstrateKeypair(); + const randomHotkeyBytes32 = '0x' + Buffer.from(randomHotkey.publicKey).toString('hex'); + + const votingPower = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPower", + args: [subnetId, randomHotkeyBytes32 as `0x${string}`] + }) + + assert.ok(votingPower !== undefined, "getVotingPower should return a value"); + assert.strictEqual(votingPower, BigInt(0), "Voting power should be 0 for unknown hotkey"); + }); + }); + + describe("VotingPower with Tracking Enabled", () => { + let enabledSubnetId: number; + + before(async () => { + // Create a new subnet for this test + const hotkey2 = getRandomSubstrateKeypair(); + const coldkey2 = getRandomSubstrateKeypair(); + + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey2.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey2.publicKey)) + + enabledSubnetId = await addNewSubnetwork(api, hotkey2, coldkey2) + await startCall(api, enabledSubnetId, coldkey2) + + // Enable voting power tracking via sudo + const internalCall = api.tx.SubtensorModule.enable_voting_power_tracking({ netuid: enabledSubnetId }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + await waitForTransactionWithRetry(api, tx, alice) + }); + + it("isVotingPowerTrackingEnabled returns true after enabling", async () => { + const isEnabled = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "isVotingPowerTrackingEnabled", + args: [enabledSubnetId] + }) + + assert.strictEqual(isEnabled, true, "Voting power tracking should be enabled"); + }); + + it("getVotingPowerDisableAtBlock still returns 0 when enabled but not scheduled for disable", async () => { + const disableAtBlock = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPowerDisableAtBlock", + args: [enabledSubnetId] + }) + + assert.strictEqual(disableAtBlock, BigInt(0), "Disable at block should still be 0"); + }); + }); + + describe("All precompile functions are accessible", () => { + it("All VotingPower precompile functions can be called", async () => { + const hotkeyBytes32 = '0x' + Buffer.from(hotkey.publicKey).toString('hex'); + + // Test all four functions + const results = await Promise.all([ + publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPower", + args: [subnetId, hotkeyBytes32 as `0x${string}`] + }), + publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "isVotingPowerTrackingEnabled", + args: [subnetId] + }), + publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPowerDisableAtBlock", + args: [subnetId] + }), + publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPowerEmaAlpha", + args: [subnetId] + }) + ]); + + // All functions should return defined values + results.forEach((result, index) => { + assert.ok(result !== undefined, `Function ${index} should return a value`); + }); + + // Verify types + assert.strictEqual(typeof results[0], 'bigint', "getVotingPower should return bigint"); + assert.strictEqual(typeof results[1], 'boolean', "isVotingPowerTrackingEnabled should return boolean"); + assert.strictEqual(typeof results[2], 'bigint', "getVotingPowerDisableAtBlock should return bigint"); + assert.strictEqual(typeof results[3], 'bigint', "getVotingPowerEmaAlpha should return bigint"); + }); + }); +}); diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index de6ac5825b..7de7d9e48f 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -143,6 +143,8 @@ pub mod pallet { Proxy, /// Leasing precompile Leasing, + /// Voting power precompile + VotingPower, } #[pallet::type_value] diff --git a/pallets/subtensor/src/epoch/run_epoch.rs b/pallets/subtensor/src/epoch/run_epoch.rs index 5e4dd1f43e..37f67235e8 100644 --- a/pallets/subtensor/src/epoch/run_epoch.rs +++ b/pallets/subtensor/src/epoch/run_epoch.rs @@ -147,6 +147,9 @@ impl Pallet { ValidatorTrust::::insert(netuid, validator_trust); ValidatorPermit::::insert(netuid, new_validator_permit); StakeWeight::::insert(netuid, stake_weight); + + // Update voting power EMA for all validators on this subnet + Self::update_voting_power_for_subnet(netuid); } /// Calculates reward consensus and returns the emissions for uids/hotkeys in a given `netuid`. diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 6d61baadf3..df86371d8b 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1874,8 +1874,55 @@ pub mod pallet { pub type SubtokenEnabled = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultFalse>; - /// Default value for burn keys limit + // ======================================= + // ==== VotingPower Storage ==== + // ======================================= + #[pallet::type_value] + /// Default VotingPower EMA alpha value (0.1 represented as u64 with 18 decimals) + /// alpha = 0.1 means slow response, 10% weight to new values per epoch + pub fn DefaultVotingPowerEmaAlpha() -> u64 { + 100_000_000_000_000_000 // 0.1 * 10^18 + } + + #[pallet::storage] + /// --- DMAP ( netuid, hotkey ) --> voting_power | EMA of stake for voting + /// This tracks stake EMA updated every epoch when VotingPowerTrackingEnabled is true. + /// Used by smart contracts to determine validator voting power for subnet governance. + pub type VotingPower = StorageDoubleMap< + _, + Identity, + NetUid, + Blake2_128Concat, + T::AccountId, + u64, + ValueQuery, + >; + + #[pallet::storage] + /// --- MAP ( netuid ) --> bool | Whether voting power tracking is enabled for this subnet. + /// When enabled, VotingPower EMA is updated every epoch. Default is false. + /// When disabled with disable_at_block set, tracking continues until that block. + pub type VotingPowerTrackingEnabled = + StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultFalse>; + + #[pallet::storage] + /// --- MAP ( netuid ) --> block_number | Block at which voting power tracking will be disabled. + /// When set (non-zero), tracking continues until this block, then automatically disables + /// and clears VotingPower entries for the subnet. Provides a 14-day grace period. + pub type VotingPowerDisableAtBlock = + StorageMap<_, Identity, NetUid, u64, ValueQuery>; + + #[pallet::storage] + /// --- MAP ( netuid ) --> u64 | EMA alpha value for voting power calculation. + /// Higher alpha = faster response to stake changes. + /// Stored as u64 with 18 decimal precision (1.0 = 10^18). + /// Only settable by sudo/root. + pub type VotingPowerEmaAlpha = + StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultVotingPowerEmaAlpha>; + + #[pallet::type_value] + /// Default value for burn keys limit pub fn DefaultImmuneOwnerUidsLimit() -> u16 { 1 } diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index ef36b17921..92762f1ec8 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2431,5 +2431,96 @@ mod dispatches { Ok(()) } + + /// Enables voting power tracking for a subnet. + /// + /// This function can be called by the subnet owner or root. + /// When enabled, voting power EMA is updated every epoch for all validators. + /// Voting power starts at 0 and increases over epochs. + /// + /// # Arguments: + /// * `origin` - The origin of the call, must be subnet owner or root. + /// * `netuid` - The subnet to enable voting power tracking for. + /// + /// # Errors: + /// * `SubnetNotExist` - If the subnet does not exist. + /// * `NotSubnetOwner` - If the caller is not the subnet owner or root. + #[pallet::call_index(125)] + #[pallet::weight(( + Weight::from_parts(10_000, 0) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)), + DispatchClass::Operational, + Pays::Yes + ))] + pub fn enable_voting_power_tracking( + origin: OriginFor, + netuid: NetUid, + ) -> DispatchResult { + Self::ensure_subnet_owner_or_root(origin, netuid)?; + Self::do_enable_voting_power_tracking(netuid) + } + + /// Schedules disabling of voting power tracking for a subnet. + /// + /// This function can be called by the subnet owner or root. + /// Voting power tracking will continue for 14 days (grace period) after this call, + /// then automatically disable and clear all VotingPower entries for the subnet. + /// + /// # Arguments: + /// * `origin` - The origin of the call, must be subnet owner or root. + /// * `netuid` - The subnet to schedule disabling voting power tracking for. + /// + /// # Errors: + /// * `SubnetNotExist` - If the subnet does not exist. + /// * `NotSubnetOwner` - If the caller is not the subnet owner or root. + /// * `VotingPowerTrackingNotEnabled` - If voting power tracking is not enabled. + #[pallet::call_index(126)] + #[pallet::weight(( + Weight::from_parts(10_000, 0) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)), + DispatchClass::Operational, + Pays::Yes + ))] + pub fn disable_voting_power_tracking( + origin: OriginFor, + netuid: NetUid, + ) -> DispatchResult { + Self::ensure_subnet_owner_or_root(origin, netuid)?; + Self::do_disable_voting_power_tracking(netuid) + } + + /// Sets the EMA alpha value for voting power calculation on a subnet. + /// + /// This function can only be called by root (sudo). + /// Higher alpha = faster response to stake changes. + /// Alpha is stored as u64 with 18 decimal precision (1.0 = 10^18). + /// + /// # Arguments: + /// * `origin` - The origin of the call, must be root. + /// * `netuid` - The subnet to set the alpha for. + /// * `alpha` - The new alpha value (u64 with 18 decimal precision). + /// + /// # Errors: + /// * `BadOrigin` - If the origin is not root. + /// * `SubnetNotExist` - If the subnet does not exist. + /// * `InvalidVotingPowerEmaAlpha` - If alpha is greater than 10^18 (1.0). + #[pallet::call_index(127)] + #[pallet::weight(( + Weight::from_parts(6_000, 0) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)), + DispatchClass::Operational, + Pays::Yes + ))] + pub fn sudo_set_voting_power_ema_alpha( + origin: OriginFor, + netuid: NetUid, + alpha: u64, + ) -> DispatchResult { + ensure_root(origin)?; + Self::do_set_voting_power_ema_alpha(netuid, alpha) + } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 5a15330075..0ea6dcea5a 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -266,5 +266,9 @@ mod errors { InvalidRootClaimThreshold, /// Exceeded subnet limit number or zero. InvalidSubnetNumber, + /// Voting power tracking is not enabled for this subnet. + VotingPowerTrackingNotEnabled, + /// Invalid voting power EMA alpha value (must be <= 10^18). + InvalidVotingPowerEmaAlpha, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index d015205d4d..d1d6268678 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -468,6 +468,35 @@ mod events { root_claim_type: RootClaimTypeEnum, }, + /// Voting power tracking has been enabled for a subnet. + VotingPowerTrackingEnabled { + /// The subnet ID + netuid: NetUid, + }, + + /// Voting power tracking has been scheduled for disabling. + /// Tracking will continue until disable_at_block, then stop and clear entries. + VotingPowerTrackingDisableScheduled { + /// The subnet ID + netuid: NetUid, + /// Block at which tracking will be disabled + disable_at_block: u64, + }, + + /// Voting power tracking has been fully disabled and entries cleared. + VotingPowerTrackingDisabled { + /// The subnet ID + netuid: NetUid, + }, + + /// Voting power EMA alpha has been set for a subnet. + VotingPowerEmaAlphaSet { + /// The subnet ID + netuid: NetUid, + /// The new alpha value (u64 with 18 decimal precision) + alpha: u64, + }, + /// Subnet lease dividends have been distributed. SubnetLeaseDividendsDistributed { /// The lease ID diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 38f1f85df8..992e336520 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -497,6 +497,11 @@ impl Pallet { // 8.3 Swap TaoDividendsPerSubnet // Tao dividends were removed + // 8.4 Swap VotingPower + // VotingPower( netuid, hotkey ) --> u64 -- the voting power EMA for the hotkey. + Self::swap_voting_power_for_hotkey(old_hotkey, new_hotkey, netuid); + weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); + // 9. Swap Alpha // Alpha( hotkey, coldkey, netuid ) -> alpha let old_alpha_values: Vec<((T::AccountId, NetUid), U64F64)> = diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index bbaf25af58..6ce0639516 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -30,4 +30,5 @@ mod swap_coldkey; mod swap_hotkey; mod swap_hotkey_with_subnet; mod uids; +mod voting_power; mod weights; diff --git a/pallets/subtensor/src/tests/voting_power.rs b/pallets/subtensor/src/tests/voting_power.rs new file mode 100644 index 0000000000..11b67064c0 --- /dev/null +++ b/pallets/subtensor/src/tests/voting_power.rs @@ -0,0 +1,603 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] + +use frame_support::weights::Weight; +use frame_support::{assert_err, assert_noop, assert_ok}; +use frame_system::RawOrigin; +use sp_core::U256; +use subtensor_runtime_common::NetUid; + +use super::mock; +use super::mock::*; +use crate::utils::voting_power::{MAX_VOTING_POWER_EMA_ALPHA, VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS}; +use crate::*; + +// ============================================ +// === Test Helpers === +// ============================================ + +const DEFAULT_STAKE_AMOUNT: u64 = 1_000_000_000_000; // 1 million RAO + +/// Test fixture containing common test setup data +struct VotingPowerTestFixture { + hotkey: U256, + coldkey: U256, + netuid: NetUid, +} + +impl VotingPowerTestFixture { + /// Create a basic fixture with a dynamic network + fn new() -> Self { + let hotkey = U256::from(1); + let coldkey = U256::from(2); + let netuid = add_dynamic_network(&hotkey, &coldkey); + Self { hotkey, coldkey, netuid } + } + + /// Setup reserves and add balance to coldkey for staking + fn setup_for_staking(&self) { + self.setup_for_staking_with_amount(DEFAULT_STAKE_AMOUNT); + } + + /// Setup reserves and add balance with custom amount + fn setup_for_staking_with_amount(&self, amount: u64) { + mock::setup_reserves(self.netuid, (amount * 100).into(), (amount * 100).into()); + SubtensorModule::add_balance_to_coldkey_account(&self.coldkey, amount * 10); + } + + /// Enable voting power tracking for the subnet + fn enable_tracking(&self) { + assert_ok!(SubtensorModule::enable_voting_power_tracking( + RuntimeOrigin::signed(self.coldkey), + self.netuid + )); + } + + /// Add stake from coldkey to hotkey + fn add_stake(&self, amount: u64) { + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(self.coldkey), + self.hotkey, + self.netuid, + amount.into() + )); + } + + /// Set validator permit for the hotkey (uid 0) + fn set_validator_permit(&self, has_permit: bool) { + ValidatorPermit::::insert(self.netuid, vec![has_permit]); + } + + /// Run voting power update for N epochs + fn run_epochs(&self, n: u32) { + for _ in 0..n { + SubtensorModule::update_voting_power_for_subnet(self.netuid); + } + } + + /// Get current voting power for the hotkey + fn get_voting_power(&self) -> u64 { + SubtensorModule::get_voting_power(self.netuid, &self.hotkey) + } + + /// Full setup: reserves, balance, tracking enabled, stake added, validator permit + fn setup_full(&self) { + self.setup_for_staking(); + self.enable_tracking(); + self.add_stake(DEFAULT_STAKE_AMOUNT); + self.set_validator_permit(true); + } +} + +// ============================================ +// === Test Enable/Disable Voting Power === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_enable_voting_power_tracking --exact --nocapture +#[test] +fn test_enable_voting_power_tracking() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Initially disabled + assert!(!SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + + // Enable tracking (subnet owner can do this) + f.enable_tracking(); + + // Now enabled + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + assert_eq!(SubtensorModule::get_voting_power_disable_at_block(f.netuid), 0); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_enable_voting_power_tracking_root_can_enable --exact --nocapture +#[test] +fn test_enable_voting_power_tracking_root_can_enable() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Root can enable + assert_ok!(SubtensorModule::enable_voting_power_tracking( + RuntimeOrigin::root(), + f.netuid + )); + + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_disable_voting_power_tracking_schedules_disable --exact --nocapture +#[test] +fn test_disable_voting_power_tracking_schedules_disable() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.enable_tracking(); + + let current_block = SubtensorModule::get_current_block_as_u64(); + + // Schedule disable + assert_ok!(SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(f.coldkey), + f.netuid + )); + + // Still enabled, but scheduled for disable + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + let disable_at = SubtensorModule::get_voting_power_disable_at_block(f.netuid); + assert_eq!(disable_at, current_block + VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_disable_voting_power_tracking_fails_when_not_enabled --exact --nocapture +#[test] +fn test_disable_voting_power_tracking_fails_when_not_enabled() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Try to disable when not enabled + assert_noop!( + SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(f.coldkey), + f.netuid + ), + Error::::VotingPowerTrackingNotEnabled + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_enable_voting_power_tracking_non_owner_fails --exact --nocapture +#[test] +fn test_enable_voting_power_tracking_non_owner_fails() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + let random_account = U256::from(999); + + // Non-owner cannot enable (returns BadOrigin) + assert_noop!( + SubtensorModule::enable_voting_power_tracking( + RuntimeOrigin::signed(random_account), + f.netuid + ), + sp_runtime::DispatchError::BadOrigin + ); + + // Should still be disabled + assert!(!SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_disable_voting_power_tracking_non_owner_fails --exact --nocapture +#[test] +fn test_disable_voting_power_tracking_non_owner_fails() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + let random_account = U256::from(999); + f.enable_tracking(); + + // Non-owner cannot disable (returns BadOrigin) + assert_noop!( + SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(random_account), + f.netuid + ), + sp_runtime::DispatchError::BadOrigin + ); + + // Should still be enabled with no disable scheduled + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + assert_eq!(SubtensorModule::get_voting_power_disable_at_block(f.netuid), 0); + }); +} + +// ============================================ +// === Test EMA Alpha === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_set_voting_power_ema_alpha --exact --nocapture +#[test] +fn test_set_voting_power_ema_alpha() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Get default alpha + let default_alpha = SubtensorModule::get_voting_power_ema_alpha(f.netuid); + assert_eq!(default_alpha, 100_000_000_000_000_000); // 0.1 * 10^18 + + // Set new alpha (only root can do this) + let new_alpha: u64 = 500_000_000_000_000_000; // 0.5 * 10^18 + assert_ok!(SubtensorModule::sudo_set_voting_power_ema_alpha( + RuntimeOrigin::root(), + f.netuid, + new_alpha + )); + + assert_eq!(SubtensorModule::get_voting_power_ema_alpha(f.netuid), new_alpha); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_set_voting_power_ema_alpha_fails_above_one --exact --nocapture +#[test] +fn test_set_voting_power_ema_alpha_fails_above_one() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Try to set alpha > 1.0 (> 10^18) + let invalid_alpha: u64 = MAX_VOTING_POWER_EMA_ALPHA + 1; + assert_noop!( + SubtensorModule::sudo_set_voting_power_ema_alpha( + RuntimeOrigin::root(), + f.netuid, + invalid_alpha + ), + Error::::InvalidVotingPowerEmaAlpha + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_set_voting_power_ema_alpha_non_root_fails --exact --nocapture +#[test] +fn test_set_voting_power_ema_alpha_non_root_fails() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Non-root cannot set alpha + assert_noop!( + SubtensorModule::sudo_set_voting_power_ema_alpha( + RuntimeOrigin::signed(f.coldkey), + f.netuid, + 500_000_000_000_000_000 + ), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +// ============================================ +// === Test EMA Calculation === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_ema_calculation --exact --nocapture +#[test] +fn test_voting_power_ema_calculation() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Initially voting power is 0 + assert_eq!(f.get_voting_power(), 0); + + // Run epoch to update voting power + f.run_epochs(1); + + // Voting power should now be > 0 (but less than full stake due to EMA starting from 0) + let voting_power_after_first_epoch = f.get_voting_power(); + assert!(voting_power_after_first_epoch > 0); + + // Run more epochs - voting power should increase towards stake + f.run_epochs(10); + + let voting_power_after_many_epochs = f.get_voting_power(); + assert!(voting_power_after_many_epochs > voting_power_after_first_epoch); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_cleared_when_deregistered --exact --nocapture +#[test] +fn test_voting_power_cleared_when_deregistered() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Run epochs to build up voting power + f.run_epochs(10); + + let voting_power_before = f.get_voting_power(); + assert!(voting_power_before > 0, "Voting power should be built up"); + + // Deregister the hotkey (simulate by removing from IsNetworkMember) + IsNetworkMember::::remove(&f.hotkey, f.netuid); + + // Run epoch - voting power should be cleared for deregistered hotkey + f.run_epochs(1); + + // Should be removed from storage immediately when deregistered + assert_eq!(f.get_voting_power(), 0); + assert!(!VotingPower::::contains_key(f.netuid, &f.hotkey), + "Entry should be removed when hotkey is deregistered"); + }); +} + +// ============================================ +// === Test Validators Only === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_only_validators_get_voting_power --exact --nocapture +#[test] +fn test_only_validators_get_voting_power() { + new_test_ext(1).execute_with(|| { + let validator_hotkey = U256::from(1); + let miner_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&validator_hotkey, &coldkey); + + mock::setup_reserves(netuid, (DEFAULT_STAKE_AMOUNT * 100).into(), (DEFAULT_STAKE_AMOUNT * 100).into()); + SubtensorModule::add_balance_to_coldkey_account(&coldkey, DEFAULT_STAKE_AMOUNT * 20); + + // Register miner + register_ok_neuron(netuid, miner_hotkey, coldkey, 0); + + // Enable voting power tracking + assert_ok!(SubtensorModule::enable_voting_power_tracking( + RuntimeOrigin::signed(coldkey), + netuid + )); + + // Add stake to both + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + validator_hotkey, + netuid, + DEFAULT_STAKE_AMOUNT.into() + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + miner_hotkey, + netuid, + DEFAULT_STAKE_AMOUNT.into() + )); + + // Set validator permit: uid 0 (validator) has permit, uid 1 (miner) does not + ValidatorPermit::::insert(netuid, vec![true, false]); + + // Run epoch + SubtensorModule::update_voting_power_for_subnet(netuid); + + // Only validator should have voting power + assert!(SubtensorModule::get_voting_power(netuid, &validator_hotkey) > 0); + assert_eq!(SubtensorModule::get_voting_power(netuid, &miner_hotkey), 0); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_miner_voting_power_removed_when_loses_vpermit --exact --nocapture +#[test] +fn test_miner_voting_power_removed_when_loses_vpermit() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Run epochs to build voting power + f.run_epochs(10); + + let voting_power_before = f.get_voting_power(); + assert!(voting_power_before > 0); + + // Remove validator permit (now they're a miner) + f.set_validator_permit(false); + + // Run epoch - voting power should be removed + f.run_epochs(1); + + assert_eq!(f.get_voting_power(), 0); + }); +} + +// ============================================ +// === Test Hotkey Swap === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_transfers_on_hotkey_swap --exact --nocapture +#[test] +fn test_voting_power_transfers_on_hotkey_swap() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + let new_hotkey = U256::from(99); + let voting_power_value = 5_000_000_000_000_u64; + + // Set some voting power for the old hotkey + VotingPower::::insert(f.netuid, f.hotkey, voting_power_value); + + // Verify old hotkey has voting power + assert_eq!(f.get_voting_power(), voting_power_value); + assert_eq!(SubtensorModule::get_voting_power(f.netuid, &new_hotkey), 0); + + // Perform hotkey swap for this subnet + SubtensorModule::swap_voting_power_for_hotkey(&f.hotkey, &new_hotkey, f.netuid); + + // Old hotkey should have 0, new hotkey should have the voting power + assert_eq!(f.get_voting_power(), 0); + assert_eq!(SubtensorModule::get_voting_power(f.netuid, &new_hotkey), voting_power_value); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_swap_adds_to_existing --exact --nocapture +#[test] +fn test_voting_power_swap_adds_to_existing() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + let new_hotkey = U256::from(99); + let old_voting_power = 5_000_000_000_000_u64; + let new_existing_voting_power = 2_000_000_000_000_u64; + + // Set voting power for both hotkeys + VotingPower::::insert(f.netuid, f.hotkey, old_voting_power); + VotingPower::::insert(f.netuid, new_hotkey, new_existing_voting_power); + + // Perform swap + SubtensorModule::swap_voting_power_for_hotkey(&f.hotkey, &new_hotkey, f.netuid); + + // New hotkey should have combined voting power + assert_eq!(f.get_voting_power(), 0); + assert_eq!( + SubtensorModule::get_voting_power(f.netuid, &new_hotkey), + old_voting_power + new_existing_voting_power + ); + }); +} + +// ============================================ +// === Test Threshold Logic === +// ============================================ +// Tests the rule: Only remove voting power entry if it decayed FROM above threshold TO below. +// New validators building up from 0 should NOT be removed even if below threshold. + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_not_removed_if_never_above_threshold --exact --nocapture +#[test] +fn test_voting_power_not_removed_if_never_above_threshold() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Get the threshold + let min_stake = SubtensorModule::get_stake_threshold(); + + // Set voting power directly to a value below threshold (simulating building up) + // This is below threshold but was never above it + let below_threshold = min_stake.saturating_sub(1); + VotingPower::::insert(f.netuid, f.hotkey, below_threshold); + + // Run epoch + f.run_epochs(1); + + // Key assertion: Entry should NOT be removed because previous_ema was below threshold + // The removal rule only triggers when previous_ema >= threshold and new_ema < threshold + let voting_power = f.get_voting_power(); + assert!(voting_power > 0, "Voting power should still exist - it was never above threshold"); + assert!(VotingPower::::contains_key(f.netuid, &f.hotkey), + "Entry should exist - it was never above threshold so shouldn't be removed"); + }); +} + +// ============================================ +// === Test Tracking Not Active === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_not_updated_when_disabled --exact --nocapture +#[test] +fn test_voting_power_not_updated_when_disabled() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_for_staking(); + // DON'T enable voting power tracking + f.add_stake(DEFAULT_STAKE_AMOUNT); + f.set_validator_permit(true); + + // Run epoch + f.run_epochs(1); + + // Voting power should still be 0 since tracking is disabled + assert_eq!(f.get_voting_power(), 0); + }); +} + +// ============================================ +// === Test Re-enable After Disable === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_reenable_voting_power_clears_disable_schedule --exact --nocapture +#[test] +fn test_reenable_voting_power_clears_disable_schedule() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.enable_tracking(); + + // Schedule disable + assert_ok!(SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(f.coldkey), + f.netuid + )); + + assert!(SubtensorModule::get_voting_power_disable_at_block(f.netuid) > 0); + + // Re-enable should clear the disable schedule + f.enable_tracking(); + + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + assert_eq!(SubtensorModule::get_voting_power_disable_at_block(f.netuid), 0); + }); +} + +// ============================================ +// === Test Grace Period Finalization === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_finalized_after_grace_period --exact --nocapture +#[test] +fn test_voting_power_finalized_after_grace_period() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Build up voting power + f.run_epochs(10); + + let voting_power_before = f.get_voting_power(); + assert!(voting_power_before > 0); + + // Schedule disable + assert_ok!(SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(f.coldkey), + f.netuid + )); + + let disable_at = SubtensorModule::get_voting_power_disable_at_block(f.netuid); + + // Advance block past grace period (time travel!) + System::set_block_number(disable_at + 1); + + // Run epoch - should finalize disable + f.run_epochs(1); + + // Tracking should be disabled and all entries cleared + assert!(!SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + assert_eq!(SubtensorModule::get_voting_power_disable_at_block(f.netuid), 0); + assert_eq!(f.get_voting_power(), 0); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_continues_during_grace_period --exact --nocapture +#[test] +fn test_voting_power_continues_during_grace_period() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Schedule disable + assert_ok!(SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(f.coldkey), + f.netuid + )); + + let disable_at = SubtensorModule::get_voting_power_disable_at_block(f.netuid); + + // Set block to middle of grace period (time travel!) + System::set_block_number(disable_at - 1000); + + // Run epoch - should still update voting power during grace period + f.run_epochs(1); + + // Tracking should still be enabled and voting power should exist + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + assert!(f.get_voting_power() > 0); + }); +} diff --git a/pallets/subtensor/src/utils/mod.rs b/pallets/subtensor/src/utils/mod.rs index 3eb8439959..0c11d52668 100644 --- a/pallets/subtensor/src/utils/mod.rs +++ b/pallets/subtensor/src/utils/mod.rs @@ -3,5 +3,6 @@ pub mod evm; pub mod identity; pub mod misc; pub mod rate_limiting; +pub mod voting_power; #[cfg(feature = "try-runtime")] pub mod try_state; diff --git a/pallets/subtensor/src/utils/voting_power.rs b/pallets/subtensor/src/utils/voting_power.rs new file mode 100644 index 0000000000..ebc5cd431e --- /dev/null +++ b/pallets/subtensor/src/utils/voting_power.rs @@ -0,0 +1,314 @@ +use super::*; +use subtensor_runtime_common::{Currency, 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 { + // 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 { + // 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 {:?} for netuid {:?}", + disable_at_block, + 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 { + // 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 {:?} for netuid {:?}", + alpha, + netuid + ); + + Ok(()) + } + + // ======================== + // === Epoch Processing === + // ======================== + + /// Update voting power for all validators on a subnet during epoch. + /// Called from persist_netuid_epoch_terms or similar epoch processing function. + pub fn update_voting_power_for_subnet(netuid: NetUid) { + // 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(); + + // Get all hotkeys registered on this subnet + let n = Self::get_subnetwork_n(netuid); + + for uid in 0..n { + if let Ok(hotkey) = Self::get_hotkey_for_net_and_uid(netuid, uid) { + // Only validators (with vpermit) get voting power, not miners + if Self::get_validator_permit_for_uid(netuid, uid) { + Self::update_voting_power_for_hotkey(netuid, &hotkey, 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 {:?} on netuid {:?}", + hotkey, + netuid + ); + } + } + + /// Update voting power for a single hotkey. + fn update_voting_power_for_hotkey( + netuid: NetUid, + hotkey: &T::AccountId, + alpha: u64, + min_stake: u64, + ) { + // Get current stake for the hotkey on this subnet + // If deregistered (not in IsNetworkMember), stake is treated as 0 + let current_stake = if IsNetworkMember::::get(hotkey, netuid) { + Self::get_total_stake_for_hotkey(hotkey).to_u64() + } else { + 0 + }; + + // 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, previous_ema, alpha); + + // Only remove if they previously had voting power ABOVE threshold and it 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 {:?} on netuid {:?} (decayed below threshold: {:?} < {:?})", + hotkey, + netuid, + 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 {:?} on netuid {:?}: {:?} -> {:?}", + hotkey, + 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 - 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 {:?} to {:?} on netuid {:?}: {:?}", + old_hotkey, + new_hotkey, + netuid, + voting_power + ); + } + } +} diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index 8069a1eb92..fd58c41cfc 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -40,6 +40,7 @@ use crate::staking::*; use crate::storage_query::*; use crate::subnet::*; use crate::uid_lookup::*; +use crate::voting_power::*; mod alpha; mod balance_transfer; @@ -55,6 +56,7 @@ mod staking; mod storage_query; mod subnet; mod uid_lookup; +mod voting_power; pub struct Precompiles(PhantomData); impl Default for Precompiles @@ -110,7 +112,7 @@ where Self(Default::default()) } - pub fn used_addresses() -> [H160; 25] { + pub fn used_addresses() -> [H160; 26] { [ hash(1), hash(2), @@ -136,6 +138,7 @@ where hash(AlphaPrecompile::::INDEX), hash(CrowdloanPrecompile::::INDEX), hash(LeasingPrecompile::::INDEX), + hash(VotingPowerPrecompile::::INDEX), hash(ProxyPrecompile::::INDEX), ] } @@ -223,6 +226,9 @@ where a if a == hash(LeasingPrecompile::::INDEX) => { LeasingPrecompile::::try_execute::(handle, PrecompileEnum::Leasing) } + a if a == hash(VotingPowerPrecompile::::INDEX) => { + VotingPowerPrecompile::::try_execute::(handle, PrecompileEnum::VotingPower) + } a if a == hash(ProxyPrecompile::::INDEX) => { ProxyPrecompile::::try_execute::(handle, PrecompileEnum::Proxy) } diff --git a/precompiles/src/voting_power.rs b/precompiles/src/voting_power.rs new file mode 100644 index 0000000000..9f59c6d886 --- /dev/null +++ b/precompiles/src/voting_power.rs @@ -0,0 +1,112 @@ +use core::marker::PhantomData; + +use fp_evm::PrecompileHandle; +use precompile_utils::EvmResult; +use sp_core::{ByteArray, H256, U256}; +use subtensor_runtime_common::NetUid; + +use crate::PrecompileExt; + +/// VotingPower precompile for smart contract access to validator voting power. +/// +/// This precompile allows smart contracts to query voting power for validators, +/// enabling on-chain governance decisions like slashing and spending. +pub struct VotingPowerPrecompile(PhantomData); + +impl PrecompileExt for VotingPowerPrecompile +where + R: frame_system::Config + pallet_subtensor::Config, + R::AccountId: From<[u8; 32]> + ByteArray, +{ + const INDEX: u64 = 2054; +} + +#[precompile_utils::precompile] +impl VotingPowerPrecompile +where + R: frame_system::Config + pallet_subtensor::Config, + R::AccountId: From<[u8; 32]> + ByteArray, +{ + /// Get voting power for a hotkey on a subnet. + /// + /// Returns the EMA of stake for the hotkey, which represents its voting power. + /// Returns 0 if: + /// - The hotkey has no voting power entry + /// - Voting power tracking is not enabled for the subnet + /// - The hotkey is not registered on the subnet + /// + /// # Arguments + /// * `netuid` - The subnet identifier (u16) + /// * `hotkey` - The hotkey account ID (bytes32) + /// + /// # Returns + /// * `u256` - The voting power value (in RAO, same precision as stake) + #[precompile::public("getVotingPower(uint16,bytes32)")] + #[precompile::view] + fn get_voting_power( + _: &mut impl PrecompileHandle, + netuid: u16, + hotkey: H256, + ) -> EvmResult { + let hotkey = R::AccountId::from(hotkey.0); + let voting_power = pallet_subtensor::VotingPower::::get(NetUid::from(netuid), &hotkey); + Ok(U256::from(voting_power)) + } + + /// Check if voting power tracking is enabled for a subnet. + /// + /// # Arguments + /// * `netuid` - The subnet identifier (u16) + /// + /// # Returns + /// * `bool` - True if voting power tracking is enabled + #[precompile::public("isVotingPowerTrackingEnabled(uint16)")] + #[precompile::view] + fn is_voting_power_tracking_enabled( + _: &mut impl PrecompileHandle, + netuid: u16, + ) -> EvmResult { + Ok(pallet_subtensor::VotingPowerTrackingEnabled::::get( + NetUid::from(netuid), + )) + } + + /// Get the block at which voting power tracking will be disabled. + /// + /// Returns 0 if not scheduled for disabling. + /// When non-zero, tracking continues until this block, then stops. + /// + /// # Arguments + /// * `netuid` - The subnet identifier (u16) + /// + /// # Returns + /// * `u64` - The block number at which tracking will be disabled (0 if not scheduled) + #[precompile::public("getVotingPowerDisableAtBlock(uint16)")] + #[precompile::view] + fn get_voting_power_disable_at_block( + _: &mut impl PrecompileHandle, + netuid: u16, + ) -> EvmResult { + Ok(pallet_subtensor::VotingPowerDisableAtBlock::::get( + NetUid::from(netuid), + )) + } + + /// Get the EMA alpha value for voting power calculation on a subnet. + /// + /// Alpha is stored with 18 decimal precision (1.0 = 10^18). + /// Higher alpha = faster response to stake changes. + /// + /// # Arguments + /// * `netuid` - The subnet identifier (u16) + /// + /// # Returns + /// * `u64` - The alpha value (with 18 decimal precision) + #[precompile::public("getVotingPowerEmaAlpha(uint16)")] + #[precompile::view] + fn get_voting_power_ema_alpha(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + Ok(pallet_subtensor::VotingPowerEmaAlpha::::get( + NetUid::from(netuid), + )) + } +} From e4ba52f745b45c15b03860363a6ba817b2881e24 Mon Sep 17 00:00:00 2001 From: konrad0960 Date: Tue, 2 Dec 2025 23:56:11 +0100 Subject: [PATCH 2/2] CI fixes --- pallets/subtensor/src/lib.rs | 11 +-- pallets/subtensor/src/tests/voting_power.rs | 80 ++++++++++++++++----- pallets/subtensor/src/utils/mod.rs | 2 +- pallets/subtensor/src/utils/voting_power.rs | 52 ++++---------- precompiles/src/voting_power.rs | 2 +- runtime/src/lib.rs | 2 +- 6 files changed, 79 insertions(+), 70 deletions(-) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index df86371d8b..dd7930f113 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1889,15 +1889,8 @@ pub mod pallet { /// --- DMAP ( netuid, hotkey ) --> voting_power | EMA of stake for voting /// This tracks stake EMA updated every epoch when VotingPowerTrackingEnabled is true. /// Used by smart contracts to determine validator voting power for subnet governance. - pub type VotingPower = StorageDoubleMap< - _, - Identity, - NetUid, - Blake2_128Concat, - T::AccountId, - u64, - ValueQuery, - >; + pub type VotingPower = + StorageDoubleMap<_, Identity, NetUid, Blake2_128Concat, T::AccountId, u64, ValueQuery>; #[pallet::storage] /// --- MAP ( netuid ) --> bool | Whether voting power tracking is enabled for this subnet. diff --git a/pallets/subtensor/src/tests/voting_power.rs b/pallets/subtensor/src/tests/voting_power.rs index 11b67064c0..fa78e0b7a3 100644 --- a/pallets/subtensor/src/tests/voting_power.rs +++ b/pallets/subtensor/src/tests/voting_power.rs @@ -8,7 +8,9 @@ use subtensor_runtime_common::NetUid; use super::mock; use super::mock::*; -use crate::utils::voting_power::{MAX_VOTING_POWER_EMA_ALPHA, VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS}; +use crate::utils::voting_power::{ + MAX_VOTING_POWER_EMA_ALPHA, VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS, +}; use crate::*; // ============================================ @@ -30,7 +32,11 @@ impl VotingPowerTestFixture { let hotkey = U256::from(1); let coldkey = U256::from(2); let netuid = add_dynamic_network(&hotkey, &coldkey); - Self { hotkey, coldkey, netuid } + Self { + hotkey, + coldkey, + netuid, + } } /// Setup reserves and add balance to coldkey for staking @@ -99,14 +105,19 @@ fn test_enable_voting_power_tracking() { let f = VotingPowerTestFixture::new(); // Initially disabled - assert!(!SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + assert!(!SubtensorModule::get_voting_power_tracking_enabled( + f.netuid + )); // Enable tracking (subnet owner can do this) f.enable_tracking(); // Now enabled assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); - assert_eq!(SubtensorModule::get_voting_power_disable_at_block(f.netuid), 0); + assert_eq!( + SubtensorModule::get_voting_power_disable_at_block(f.netuid), + 0 + ); }); } @@ -144,7 +155,10 @@ fn test_disable_voting_power_tracking_schedules_disable() { // Still enabled, but scheduled for disable assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); let disable_at = SubtensorModule::get_voting_power_disable_at_block(f.netuid); - assert_eq!(disable_at, current_block + VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS); + assert_eq!( + disable_at, + current_block + VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS + ); }); } @@ -182,7 +196,9 @@ fn test_enable_voting_power_tracking_non_owner_fails() { ); // Should still be disabled - assert!(!SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + assert!(!SubtensorModule::get_voting_power_tracking_enabled( + f.netuid + )); }); } @@ -205,7 +221,10 @@ fn test_disable_voting_power_tracking_non_owner_fails() { // Should still be enabled with no disable scheduled assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); - assert_eq!(SubtensorModule::get_voting_power_disable_at_block(f.netuid), 0); + assert_eq!( + SubtensorModule::get_voting_power_disable_at_block(f.netuid), + 0 + ); }); } @@ -231,7 +250,10 @@ fn test_set_voting_power_ema_alpha() { new_alpha )); - assert_eq!(SubtensorModule::get_voting_power_ema_alpha(f.netuid), new_alpha); + assert_eq!( + SubtensorModule::get_voting_power_ema_alpha(f.netuid), + new_alpha + ); }); } @@ -322,8 +344,10 @@ fn test_voting_power_cleared_when_deregistered() { // Should be removed from storage immediately when deregistered assert_eq!(f.get_voting_power(), 0); - assert!(!VotingPower::::contains_key(f.netuid, &f.hotkey), - "Entry should be removed when hotkey is deregistered"); + assert!( + !VotingPower::::contains_key(f.netuid, &f.hotkey), + "Entry should be removed when hotkey is deregistered" + ); }); } @@ -341,7 +365,11 @@ fn test_only_validators_get_voting_power() { let netuid = add_dynamic_network(&validator_hotkey, &coldkey); - mock::setup_reserves(netuid, (DEFAULT_STAKE_AMOUNT * 100).into(), (DEFAULT_STAKE_AMOUNT * 100).into()); + mock::setup_reserves( + netuid, + (DEFAULT_STAKE_AMOUNT * 100).into(), + (DEFAULT_STAKE_AMOUNT * 100).into(), + ); SubtensorModule::add_balance_to_coldkey_account(&coldkey, DEFAULT_STAKE_AMOUNT * 20); // Register miner @@ -426,7 +454,10 @@ fn test_voting_power_transfers_on_hotkey_swap() { // Old hotkey should have 0, new hotkey should have the voting power assert_eq!(f.get_voting_power(), 0); - assert_eq!(SubtensorModule::get_voting_power(f.netuid, &new_hotkey), voting_power_value); + assert_eq!( + SubtensorModule::get_voting_power(f.netuid, &new_hotkey), + voting_power_value + ); }); } @@ -482,9 +513,14 @@ fn test_voting_power_not_removed_if_never_above_threshold() { // Key assertion: Entry should NOT be removed because previous_ema was below threshold // The removal rule only triggers when previous_ema >= threshold and new_ema < threshold let voting_power = f.get_voting_power(); - assert!(voting_power > 0, "Voting power should still exist - it was never above threshold"); - assert!(VotingPower::::contains_key(f.netuid, &f.hotkey), - "Entry should exist - it was never above threshold so shouldn't be removed"); + assert!( + voting_power > 0, + "Voting power should still exist - it was never above threshold" + ); + assert!( + VotingPower::::contains_key(f.netuid, &f.hotkey), + "Entry should exist - it was never above threshold so shouldn't be removed" + ); }); } @@ -533,7 +569,10 @@ fn test_reenable_voting_power_clears_disable_schedule() { f.enable_tracking(); assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); - assert_eq!(SubtensorModule::get_voting_power_disable_at_block(f.netuid), 0); + assert_eq!( + SubtensorModule::get_voting_power_disable_at_block(f.netuid), + 0 + ); }); } @@ -569,8 +608,13 @@ fn test_voting_power_finalized_after_grace_period() { f.run_epochs(1); // Tracking should be disabled and all entries cleared - assert!(!SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); - assert_eq!(SubtensorModule::get_voting_power_disable_at_block(f.netuid), 0); + assert!(!SubtensorModule::get_voting_power_tracking_enabled( + f.netuid + )); + assert_eq!( + SubtensorModule::get_voting_power_disable_at_block(f.netuid), + 0 + ); assert_eq!(f.get_voting_power(), 0); }); } diff --git a/pallets/subtensor/src/utils/mod.rs b/pallets/subtensor/src/utils/mod.rs index 0c11d52668..a91875da59 100644 --- a/pallets/subtensor/src/utils/mod.rs +++ b/pallets/subtensor/src/utils/mod.rs @@ -3,6 +3,6 @@ pub mod evm; pub mod identity; pub mod misc; pub mod rate_limiting; -pub mod voting_power; #[cfg(feature = "try-runtime")] pub mod try_state; +pub mod voting_power; diff --git a/pallets/subtensor/src/utils/voting_power.rs b/pallets/subtensor/src/utils/voting_power.rs index ebc5cd431e..61a944bf8a 100644 --- a/pallets/subtensor/src/utils/voting_power.rs +++ b/pallets/subtensor/src/utils/voting_power.rs @@ -50,10 +50,7 @@ impl Pallet { // Emit event Self::deposit_event(Event::VotingPowerTrackingEnabled { netuid }); - log::info!( - "VotingPower tracking enabled for netuid {:?}", - netuid - ); + log::info!("VotingPower tracking enabled for netuid {netuid:?}"); Ok(()) } @@ -69,7 +66,8 @@ impl Pallet { // 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); + let disable_at_block = + current_block.saturating_add(VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS); // Schedule disable VotingPowerDisableAtBlock::::insert(netuid, disable_at_block); @@ -81,9 +79,7 @@ impl Pallet { }); log::info!( - "VotingPower tracking scheduled to disable at block {:?} for netuid {:?}", - disable_at_block, - netuid + "VotingPower tracking scheduled to disable at block {disable_at_block:?} for netuid {netuid:?}" ); Ok(()) @@ -103,11 +99,7 @@ impl Pallet { // Emit event Self::deposit_event(Event::VotingPowerEmaAlphaSet { netuid, alpha }); - log::info!( - "VotingPower EMA alpha set to {:?} for netuid {:?}", - alpha, - netuid - ); + log::info!("VotingPower EMA alpha set to {alpha:?} for netuid {netuid:?}"); Ok(()) } @@ -159,10 +151,7 @@ impl Pallet { // 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 - ); + log::trace!("VotingPower updated for validators on netuid {netuid:?}"); } /// Clear voting power for hotkeys that are no longer registered on the subnet. @@ -183,9 +172,7 @@ impl Pallet { for hotkey in hotkeys_to_remove { VotingPower::::remove(netuid, &hotkey); log::trace!( - "VotingPower removed for deregistered hotkey {:?} on netuid {:?}", - hotkey, - netuid + "VotingPower removed for deregistered hotkey {hotkey:?} on netuid {netuid:?}" ); } } @@ -219,21 +206,13 @@ impl Pallet { // Was above threshold, now decayed below - remove VotingPower::::remove(netuid, hotkey); log::trace!( - "VotingPower removed for hotkey {:?} on netuid {:?} (decayed below threshold: {:?} < {:?})", - hotkey, - netuid, - new_ema, - min_stake + "VotingPower removed for hotkey {hotkey:?} on netuid {netuid:?} (decayed below 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 {:?} on netuid {:?}: {:?} -> {:?}", - hotkey, - netuid, - previous_ema, - new_ema + "VotingPower updated for hotkey {hotkey:?} on netuid {netuid:?}: {previous_ema:?} -> {new_ema:?}" ); } // If new_ema == 0 do nothing @@ -245,7 +224,7 @@ impl Pallet { 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 - alpha_128; + 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; @@ -277,10 +256,7 @@ impl Pallet { // Emit event Self::deposit_event(Event::VotingPowerTrackingDisabled { netuid }); - log::info!( - "VotingPower tracking disabled and entries cleared for netuid {:?}", - netuid - ); + log::info!("VotingPower tracking disabled and entries cleared for netuid {netuid:?}"); } // ======================== @@ -303,11 +279,7 @@ impl Pallet { VotingPower::::insert(netuid, new_hotkey, voting_power.saturating_add(existing)); log::trace!( - "VotingPower transferred from {:?} to {:?} on netuid {:?}: {:?}", - old_hotkey, - new_hotkey, - netuid, - voting_power + "VotingPower transferred from {old_hotkey:?} to {new_hotkey:?} on netuid {netuid:?}: {voting_power:?}" ); } } diff --git a/precompiles/src/voting_power.rs b/precompiles/src/voting_power.rs index 9f59c6d886..23cdfbe69d 100644 --- a/precompiles/src/voting_power.rs +++ b/precompiles/src/voting_power.rs @@ -25,7 +25,7 @@ where impl VotingPowerPrecompile where R: frame_system::Config + pallet_subtensor::Config, - R::AccountId: From<[u8; 32]> + ByteArray, + R::AccountId: From<[u8; 32]>, { /// Get voting power for a hotkey on a subnet. /// diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index e3230ae540..f10d7c067b 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -237,7 +237,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 353, + spec_version: 354, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,