Skip to content

Everyone's convinction to subnet owners is immediate#2687

Open
gztensor wants to merge 4 commits into
devnet-readyfrom
feat/conviction-v2
Open

Everyone's convinction to subnet owners is immediate#2687
gztensor wants to merge 4 commits into
devnet-readyfrom
feat/conviction-v2

Conversation

@gztensor
Copy link
Copy Markdown
Contributor

@gztensor gztensor commented May 21, 2026

Motivation

The conviction lock model should award immediate owner conviction based on the subnet owner hotkey, not only based on the subnet owner coldkey. This allows any coldkey locking stake to the subnet owner hotkey to contribute immediately to owner conviction.

What Changed

  • Added DecayingOwnerLock storage for decaying locks that target the subnet owner hotkey.
  • Reworked lock aggregation so owner-lock classification is based on SubnetOwnerHotkey while perpetual/decaying classification remains based on the coldkey's DecayingLock flag.
  • Updated lock movement, coldkey swap, hotkey swap, total conviction, hotkey conviction, and subnet-king logic to account for perpetual and decaying owner-hotkey aggregate buckets separately.
  • Added a migration to reset existing testnet conviction lock storage before the revised conviction design is deployed more broadly.
  • Added tests for non-owner coldkeys locking to owner hotkeys, decaying owner-hotkey locks, owner-coldkey locks to non-owner hotkeys, subnet-owner reassignment, hotkey swap owner-lock movement, lock movement to owner hotkeys, and the reset migration.

Behavioral Impact

Locks targeting the subnet owner hotkey now receive immediate owner conviction regardless of which coldkey created the lock. Locks by the subnet owner coldkey to non-owner hotkeys mature normally instead of being treated as owner locks.

Migration / Spec Version

This PR changes runtime behavior and storage layout. The Auditor auto-fix bumped spec_version from 408 to 409.

The included migration clears existing conviction lock storage intended to be testnet-only legacy state before the updated design takes effect.

Testing

The PR adds focused unit tests in pallets/subtensor/src/tests/locks.rs and pallets/subtensor/src/tests/migration.rs. The author checklist states ./scripts/fix_rust.sh and relevant tests were run locally.

@gztensor gztensor changed the title Everyone's comvinction to subnet owners is immediate Everyone's convinction to subnet owners is immediate May 21, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

.map(|lock| Self::roll_forward_owner_lock(netuid, lock, now).conviction)
.unwrap_or_else(|| U64F64::saturating_from_num(0)),
)
let owner_conviction = OwnerLock::<T>::get(netuid)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] OwnerLock storage is reinterpreted without migrating existing aggregates

OwnerLock previously represented aggregate locks made by the subnet owner coldkey, regardless of which hotkey was locked to and whether that coldkey was in decaying mode. This PR now credits any existing OwnerLock directly to SubnetOwnerHotkey and rolls it as (owner_lock=true, perpetual_lock=true), while the new DecayingOwnerLock starts empty. Without an on-chain migration that rebuilds OwnerLock, DecayingOwnerLock, HotkeyLock, and DecayingHotkeyLock from individual Lock entries under the new (hotkey == SubnetOwnerHotkey, is_perpetual) rules, legacy owner-coldkey locks to non-owner hotkeys or decaying owner locks are misattributed to the current owner hotkey. That corrupts hotkey_conviction, get_total_conviction, and subnet_king, and can incorrectly preserve or change subnet ownership. Add a storage-versioned migration or keep backward-compatible interpretation until migration completes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

🛡️ AI Review — Skeptic (security review)

VERDICT: SAFE

BASELINE scrutiny: established write-permission opentensor contributor; no Gittensor allowlist hit; branch feat/conviction-v2 -> devnet-ready is valid.

No .github, dependency, lockfile, or build-script changes are present. The new one-shot migration clears the prior conviction lock storage before the revised owner-hotkey aggregate semantics can read it, addressing the previously flagged storage reinterpretation issue.

Findings

No findings.

Prior-comment reconciliation

  • 473b9654: addressed — The current diff adds migrate_reset_testnet_conviction_locks, which clears Lock, HotkeyLock, DecayingHotkeyLock, OwnerLock, DecayingOwnerLock, and DecayingLock without decoding existing values, so old OwnerLock bytes are no longer reinterpreted under the new owner-hotkey aggregate semantics.

Conclusion

No malicious code or security vulnerability found in the current diff. The prior OwnerLock transition concern is addressed by clearing the old conviction lock maps before the new aggregate layout is used.


📜 Previous run (superseded)
Sev File Finding Status
HIGH pallets/subtensor/src/staking/lock.rs:613 OwnerLock storage is reinterpreted without migrating existing aggregates ✅ Addressed
The current diff adds migrate_reset_testnet_conviction_locks, which clears Lock, HotkeyLock, DecayingHotkeyLock, OwnerLock, DecayingOwnerLock, and DecayingLock without decoding existing values, so old OwnerLock bytes are no longer reinterpreted under the new owner-hotkey aggregate semantics.

🔍 AI Review — Auditor (domain review)

VERDICT: 👎

Gittensor: LIKELY; established subtensor contributor with write permission and substantial recent merged PR history.

PR body was empty/trivial; the Auditor has auto-filled it. Please review.

Auto-fix applied: bumped runtime/src/lib.rs spec_version from 408 to 409 because this PR changes pallet runtime behavior on devnet-ready.

Duplicate-work check: overlapping open PRs appear to share common hot files or deployment/runtime surface area; I did not find a better duplicate candidate for this specific conviction owner-hotkey change.

Findings

Sev File Finding
HIGH pallets/subtensor/src/migrations/migrate_reset_testnet_conviction_locks.rs:31 Migration clears user-sized lock storage in one upgrade block inline
HIGH pallets/subtensor/src/staking/lock.rs:955 Owner hotkey swaps can scan the entire lock table inline

Conclusion

Blocking on bounded-execution issues in runtime upgrade / hotkey-swap paths. The conviction logic is covered with focused tests, but the unbounded storage work must be made safe before merge.

@github-actions
Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

.map(|lock| Self::roll_forward_owner_lock(netuid, lock, now).conviction)
.unwrap_or_else(|| U64F64::saturating_from_num(0)),
)
let owner_conviction = OwnerLock::<T>::get(netuid)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] OwnerLock storage is reinterpreted without migrating existing aggregates

OwnerLock already exists on-chain with the old meaning: aggregate locks made by the subnet owner coldkey. This PR changes the runtime to treat the same storage item as aggregate locks to the subnet owner hotkey, and this line credits that legacy value to SubnetOwnerHotkey. Existing OwnerLock entries can therefore be assigned to the wrong hotkey, and prior decaying owner-coldkey aggregates can also be rolled as perpetual owner-hotkey conviction. That can corrupt hotkey_conviction, subnet_king, and subsequent owner reassignment after the runtime upgrade.

This needs a storage migration that rebuilds owner-hotkey aggregate buckets from the individual Lock records, splitting perpetual vs decaying entries into OwnerLock and DecayingOwnerLock, or otherwise clears/recomputes the legacy OwnerLock before these new reads are live.

@github-actions
Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.


// This only affects testnet: mainnet has not had this conviction lock state
// deployed with live values yet.
let lock_removal = Lock::<T>::clear(u32::MAX, None);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Migration clears user-sized lock storage in one upgrade block

Lock, HotkeyLock, DecayingHotkeyLock, OwnerLock, DecayingOwnerLock, and DecayingLock are cleared with u32::MAX in a single on_runtime_upgrade migration. These maps can scale with user lock activity, and computing the weight after clear() does not bound the work already performed. If testnet/devnet has more entries than expected, the upgrade can exceed block limits or stall. Make this bounded with finite clear limits plus a cursor/progress flag, or otherwise gate/prove the exact live state size before running it.


// Gather locks for old hotkey (only if hotkey locks exist, otherwise skip to save reads)
if !hotkey_locks_to_transfer.is_empty() || !decaying_hotkey_locks_to_transfer.is_empty() {
if !netuids_to_transfer.is_empty() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Owner hotkey swaps can scan the entire lock table

With old_is_owner_hotkey || new_is_owner_hotkey feeding netuids_to_transfer, any swap involving a subnet owner hotkey enters Lock::<T>::iter() and scans every lock across all coldkeys/subnets. This is reachable from signed swap_hotkey / swap_hotkey_v2, while the call has a fixed pre-dispatch weight. Returning dynamic post-dispatch weight after the scan does not bound block execution. Use a bounded cursor, maintain a reverse index by hotkey, or disallow this path from user extrinsics and handle owner-hotkey aggregate repair through a bounded migration.

@github-actions
Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: SAFE Auditor: 👎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant