Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions evm-tests/src/contracts/votingPower.ts
Original file line number Diff line number Diff line change
@@ -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"
}
]
206 changes: 206 additions & 0 deletions evm-tests/test/votingPower.precompile.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof devnet>;

// 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");
});
});
});
2 changes: 2 additions & 0 deletions pallets/admin-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ pub mod pallet {
Proxy,
/// Leasing precompile
Leasing,
/// Voting power precompile
VotingPower,
}

#[pallet::type_value]
Expand Down
3 changes: 3 additions & 0 deletions pallets/subtensor/src/epoch/run_epoch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ impl<T: Config> Pallet<T> {
ValidatorTrust::<T>::insert(netuid, validator_trust);
ValidatorPermit::<T>::insert(netuid, new_validator_permit);
StakeWeight::<T>::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`.
Expand Down
42 changes: 41 additions & 1 deletion pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1874,8 +1874,48 @@ pub mod pallet {
pub type SubtokenEnabled<T> =
StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultFalse<T>>;

/// 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<T: Config>() -> 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<T: Config> =
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<T: Config> =
StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultFalse<T>>;

#[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<T: Config> =
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<T: Config> =
StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultVotingPowerEmaAlpha<T>>;

#[pallet::type_value]
/// Default value for burn keys limit
pub fn DefaultImmuneOwnerUidsLimit<T: Config>() -> u16 {
1
}
Expand Down
Loading
Loading