Skip to content
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 = "0x000000000000000000000000000000000000080d";

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"
}
]
25 changes: 23 additions & 2 deletions contract-tests/src/subtensor.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import * as assert from "assert";
import { devnet, MultiAddress } from '@polkadot-api/descriptors';
import { TypedApi, TxCallData, Binary, Enum } from 'polkadot-api';
import { TypedApi, TxCallData, Binary, Enum, getTypedCodecs } from 'polkadot-api';
import { KeyPair } from "@polkadot-labs/hdkd-helpers"
import { getAliceSigner, waitForTransactionCompletion, getSignerFromKeypair, waitForTransactionWithRetry } from './substrate'
import { convertH160ToSS58, convertPublicKeyToSs58, ethAddressToH160 } from './address-utils'
import { tao } from './balance-math'
import internal from "stream";
import { createCodec } from "scale-ts";

// create a new subnet and return netuid
export async function addNewSubnetwork(api: TypedApi<typeof devnet>, hotkey: KeyPair, coldkey: KeyPair) {
const alice = getAliceSigner()
const totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue()

const defaultNetworkLastLockCost = await api.query.SubtensorModule.NetworkLastLockCost.getValue()

const rateLimit = await api.query.SubtensorModule.NetworkRateLimit.getValue()
if (rateLimit !== BigInt(0)) {
const internalCall = api.tx.AdminUtils.sudo_set_network_rate_limit({ rate_limit: BigInt(0) })
Expand All @@ -26,6 +29,9 @@ export async function addNewSubnetwork(api: TypedApi<typeof devnet>, hotkey: Key
const newTotalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue()
// could create multiple subnetworks during retry, just return the first created one
assert.ok(newTotalNetworks > totalNetworks)

// reset network last lock cost to 0, to avoid the lock cost calculation error
await setNetworkLastLockCost(api, defaultNetworkLastLockCost)
return totalNetworks
}

Expand Down Expand Up @@ -398,4 +404,19 @@ export async function sendWasmContractExtrinsic(api: TypedApi<typeof devnet>, co
storage_deposit_limit: BigInt(1000000000)
})
await waitForTransactionWithRetry(api, tx, signer)
}
}

export async function setNetworkLastLockCost(api: TypedApi<typeof devnet>, defaultNetworkLastLockCost: bigint) {
const alice = getAliceSigner()
const key = await api.query.SubtensorModule.NetworkLastLockCost.getKey()
const codec = await getTypedCodecs(devnet);
const value = codec.query.SubtensorModule.NetworkLastLockCost.value.enc(defaultNetworkLastLockCost)
const internalCall = api.tx.System.set_storage({
items: [[Binary.fromHex(key), Binary.fromBytes(value)]]
})
const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall })
await waitForTransactionWithRetry(api, tx, alice)

const valueOnChain = await api.query.SubtensorModule.NetworkLastLockCost.getValue()
assert.equal(defaultNetworkLastLockCost, valueOnChain)
}
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_003_570_000_000_000_000 // 0.00357 * 10^18 = 2 weeks e-folding (time-constant) @ 361
assert.strictEqual(alpha, BigInt("3570000000000000"), "Default alpha should be 0.00357 * 10^18 (3570000000000000)");
});
});

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 @@ -145,6 +145,8 @@ pub mod pallet {
Leasing,
/// Address mapping precompile
AddressMapping,
/// Voting power precompile
VotingPower,
}

#[pallet::type_value]
Expand Down
Loading
Loading