-
Notifications
You must be signed in to change notification settings - Fork 287
Voting Power EMA #2240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sam0x17
merged 18 commits into
opentensor:devnet-ready
from
konrad0960:voting-power-feature
Jan 19, 2026
+1,699
−3
Merged
Voting Power EMA #2240
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
3f985eb
commit Cargo.lock
konrad0960 7c56e26
CI fixes
konrad0960 aa08aa6
refactor: pass epoch terms to voting power instead of re-reading state
konrad0960 e541d53
add removal hysteresis
konrad0960 4af751d
move update_voting_power_for_subnet and fix formatting
konrad0960 6144ef6
get total voting power precompile
konrad0960 9b43431
Simplify the formula for removal of VotingPower entry
ppolewicz e4ce9b9
Merge branch 'devnet-ready' into voting-power-feature
konrad0960 6196849
Adjust DefaultVotingPowerEmaAlpha to 2 weeks
ppolewicz bd3969a
cargo fmt
ppolewicz c4b8ff9
cargo clippy
ppolewicz 3d65741
Thank you clippy but I know what I'm doing
ppolewicz 4ce1f5d
Fix macro conflict
ppolewicz eeb01f1
fix macro usage
ppolewicz c888f20
Merge branch 'devnet-ready' into voting-power-feature
ppolewicz 0e1bd21
Fix precompile to have unique id
ppolewicz 1526f89
Change the precompile address again as after something else merged th…
ppolewicz c66f782
update test
ppolewicz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| } | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use type-safe method of expressing stake (alpha): AlphaCurrency.