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
104 changes: 104 additions & 0 deletions contract-tests/src/contracts/votingPower.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "netuid",
"type": "uint16"
}
],
"name": "getTotalVotingPower",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]
226 changes: 226 additions & 0 deletions contract-tests/test/votingPower.precompile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
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");
});

it("getTotalVotingPower returns 0 when no voting power exists", async () => {
const totalVotingPower = await publicClient.readContract({
abi: IVotingPowerABI,
address: toViemAddress(IVOTING_POWER_ADDRESS),
functionName: "getTotalVotingPower",
args: [subnetId]
})

assert.ok(totalVotingPower !== undefined, "getTotalVotingPower should return a value");
assert.strictEqual(typeof totalVotingPower, 'bigint', "getTotalVotingPower should return a bigint");
assert.strictEqual(totalVotingPower, BigInt(0), "Total voting power should be 0 when tracking is disabled");
});
});

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 five 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]
}),
publicClient.readContract({
abi: IVotingPowerABI,
address: toViemAddress(IVOTING_POWER_ADDRESS),
functionName: "getTotalVotingPower",
args: [subnetId]
})
]);

// All functions should return defined values
results.forEach((result: unknown, index: number) => {
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");
assert.strictEqual(typeof results[4], 'bigint', "getTotalVotingPower 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
6 changes: 6 additions & 0 deletions pallets/subtensor/src/epoch/run_epoch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct EpochTerms {
pub validator_trust: u16,
pub new_validator_permit: bool,
pub bond: Vec<(u16, u16)>,
pub stake: u64,
}

pub struct EpochOutput<T: frame_system::Config>(pub BTreeMap<T::AccountId, EpochTerms>);
Expand Down Expand Up @@ -988,6 +989,10 @@ impl<T: Config> Pallet<T> {
.iter()
.map(|xi| fixed_proportion_to_u16(*xi))
.collect::<Vec<u16>>();
let raw_stake: Vec<u64> = total_stake
.iter()
.map(|s| s.saturating_to_num::<u64>())
.collect::<Vec<u64>>();

for (_hotkey, terms) in terms_map.iter_mut() {
terms.dividend = cloned_dividends.get(terms.uid).copied().unwrap_or_default();
Expand All @@ -1012,6 +1017,7 @@ impl<T: Config> Pallet<T> {
.get(terms.uid)
.copied()
.unwrap_or_default();
terms.stake = raw_stake.get(terms.uid).copied().unwrap_or_default();
let old_validator_permit = validator_permits
.get(terms.uid)
.copied()
Expand Down
Loading
Loading