Skip to content

Fix conviction aggregate roll-forward- #3060 - #3073

Open
UnArbosFour wants to merge 13 commits into
mainfrom
fix/zero-lock-aggregates
Open

Fix conviction aggregate roll-forward- #3060#3073
UnArbosFour wants to merge 13 commits into
mainfrom
fix/zero-lock-aggregates

Conversation

@UnArbosFour

@UnArbosFour UnArbosFour commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix conviction-lock aggregate accounting and rebuild aggregates corrupted by partial roll-forward semantics.

This change keeps the current main takeover policy unchanged: the winning hotkey must independently hold strictly more than 18% of eligible alpha (SubnetAlphaOut - SubnetProtocolAlpha - AlphaBurned), using exact U256 arithmetic.

Motivation

An individual lock could be rolled forward and have only its delta applied to its aggregate before advancing the aggregate timestamp. This broke the aggregate invariant:

  • Untouched sibling locks did not mature before the aggregate timestamp advanced.
  • A sibling updated later could contribute maturation already represented by that timestamp.
  • Existing corrupted aggregates could not be repaired safely from aggregate state alone.

Ownership changes also updated aggregate buckets without consistently reclassifying canonical individual locks, allowing stale owner conviction to reappear after later member updates.

Changes

Aggregate accounting

  • Scope each ConvictionModel to one individual lock and its corresponding aggregate class.
  • Roll the individual and complete selected aggregate to the same block before mutation.
  • Remove delta-only aggregate timestamp advancement.
  • Centralize individual dust collection and remove complete dust contributions from aggregates.
  • Preserve consistency during lock additions, reductions, class changes, moves, and transfers.
  • Merge pre-existing destination locks instead of replacing them.
  • Re-read destination state after saving the source when aggregate buckets may be shared.

Ownership transitions

  • Reclassify every indexed individual lock belonging to outgoing and incoming owner hotkeys.
  • Roll outgoing locks under their previous owner role before demotion.
  • Promote incoming locks to owner conviction at the transition block.
  • Apply the transition consistently to:
    • Automatic conviction-based takeover
    • Lease termination
    • Administrative owner-hotkey changes
  • Add member-scaled transition weights and propagate hook weight.

Takeover policy

Preserve the current main behavior:

  • Select the highest-conviction hotkey.
  • Count only that hotkey’s conviction toward admission.
  • Require strictly more than 18% of eligible alpha.
  • Calculate the threshold using exact U256 cross-multiplication.
  • Exclude protocol-owned and burned alpha from the denominator.

Runtime migration

Add migrate_rebuild_conviction_aggregates, guarded by HasMigrationRun.

The migration:

  • Runs after the subnet-hotkey-swap repair.
  • Reads canonical individual Lock rows.
  • Rolls each row to the upgrade block using its current lock class.
  • Removes dust locks and orphaned reverse-index entries.
  • Clears all four aggregate maps.
  • Rebuilds aggregates exclusively from retained individual rows.
  • Reconstructs LockingColdkeys.

Test coverage

Added regression coverage for:

  • Sibling maturity during lock cleanup
  • Double-count prevention after aggregate roll-forward
  • Decaying sibling dust cleanup
  • Move and transfer dust removal
  • Pre-existing destination-lock merging
  • Pure read behavior
  • Owner demotion without ghost conviction
  • Decaying-owner demotion under the previous role
  • Lease and administrative ownership transitions
  • Member-scaled ownership-transition weights
  • Aggregate reconstruction, dust cleanup, and migration ordering

Validation

  • cargo fmt --check --all
  • git diff --check

Full builds and tests are left to CI per repository guidance.

Runtime version

The rebase preserves main’s current spec_version of 451. A subsequent version bump or no-spec-version-bump label may be required before merge.

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
subtensor Ready Ready Preview Aug 28, 2026 8:49pm

Request Review

@github-actions github-actions Bot left a comment

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.

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

weight = weight.saturating_add(T::DbWeight::get().reads(3));

// Collect before rewriting Lock so mutation cannot disturb the iterator.
let locks: Vec<_> = Lock::<T>::iter().collect();

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] Runtime upgrade materializes every lock without a hard bound

on_runtime_upgrade collects the entire permissionlessly growable Lock map into WASM memory, then clears several maps and rewrites every retained row in the same upgrade block. The archive snapshot is not a protocol bound and state can grow before deployment; returning the consumed weight only after execution cannot prevent an overweight or memory-exhausting upgrade. Stage this migration with a cursor and per-block limit, or enforce and validate a hard storage bound before performing the rebuild.

Comment thread pallets/subtensor/src/staking/lock.rs Outdated
/// complete member-scaled work instead of adding permanent storage bookkeeping.
pub fn owner_transition_member_count(netuid: NetUid, new_owner_hotkey: &T::AccountId) -> u32 {
let old_owner_hotkey = SubnetOwnerHotkey::<T>::get(netuid);
let old_owner_members = LockingColdkeys::<T>::iter_prefix((netuid, &old_owner_hotkey))

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] Ownership transitions scan an unbounded member index

This unbounded prefix scan is evaluated while determining dispatch weight, and the transition subsequently collects and rewrites every indexed member. LockingColdkeys has no protocol-level per-hotkey bound, so historical observations do not constrain adversarial state growth. Automatic ownership changes also reach the same work from the block hook. Dynamic weight accounting does not stop execution once the block limit is exceeded; introduce a maintained bound or a staged transition with bounded work per block.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🛡️ AI Review — Skeptic (security review)

VERDICT: VULNERABLE

VERY HIGH scrutiny: account under 90 days with one public repository; author has write access, no Gittensor association found, and multiple commits have other authors.

The refreshed head removes the migration from the runtime-upgrade hook and retains an unbounded ownership-transition scan.

Findings

Sev File Finding
HIGH pallets/subtensor/src/macros/hooks.rs Aggregate-rebuild migration is never scheduled (off-diff)
HIGH pallets/subtensor/src/staking/lock.rs:1211 Ownership transitions scan an unbounded member index inline

Other findings

  • [HIGH] Aggregate-rebuild migration is never scheduled (pallets/subtensor/src/macros/hooks.rs) — The new migration is exported and tested directly, but on_runtime_upgrade in pallets/subtensor/src/macros/hooks.rs never invokes it. Consequently, the corrupted aggregate state this PR is intended to repair survives the upgrade, while takeover decisions continue consuming those aggregates. Add the migration to the upgrade chain after migrate_fix_subnet_hotkey_lock_swaps and ensure its execution is safely bounded or staged.

Prior-comment reconciliation

  • 67b6ef94: no longer applies — The unbounded migration is no longer reachable from on_runtime_upgrade; however, its removal creates the separate current finding that the required repair never runs.
  • 26ccb1de: not addressed — Ownership transitions still collect and process the entire unbounded LockingColdkeys prefix.

Conclusion

The PR appears legitimate, but corrupted conviction aggregates would remain unrepaired and ownership changes can still perform unbounded runtime work. Both issues must be resolved before merge.


📜 Previous run (superseded)
Sev File Finding Status
HIGH pallets/subtensor/src/migrations/migrate_rebuild_conviction_aggregates.rs:75 Runtime upgrade materializes every lock without a hard bound ⏭️ No longer applies
The unbounded migration is no longer reachable from on_runtime_upgrade; however, its removal creates the separate current finding that the required repair never runs.
HIGH pallets/subtensor/src/staking/lock.rs:624 Ownership transitions scan an unbounded member index ➡️ Carried forward to current findings
Ownership transitions still collect and process the entire unbounded LockingColdkeys prefix.

# 🔍 AI Review — Auditor (domain review) has not yet run on this PR.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

This was referenced Aug 11, 2026
# Conflicts:
#	docs/migration.mdx
#	pallets/admin-utils/src/weights.rs
#	pallets/subtensor/src/macros/hooks.rs
#	pallets/subtensor/src/migrations/mod.rs
#	pallets/subtensor/src/staking/lock.rs
#	pallets/subtensor/src/tests/locks.rs
#	runtime/src/lib.rs
#	sdk/python/bittensor/_generated/calls.py
#	sdk/python/bittensor/_generated/constants.py
#	sdk/python/bittensor/_generated/errors.py
#	sdk/python/bittensor/_generated/runtime_apis.py
#	sdk/python/bittensor/_generated/storage.py
#	website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx
@github-actions

Copy link
Copy Markdown
Contributor

eco-tests changed — indexer review required

This PR modifies files under eco-tests/. and may affect downstream indexing.
cc @evgeny-s — please review manually

Changed files
  • eco-tests/src/tests_mentat_indexer.rs

@github-actions
github-actions Bot requested a review from evgeny-s August 28, 2026 20:33
@UnArbosFour
UnArbosFour changed the base branch from fix/restore-miner-burn-scaling to main August 28, 2026 20:34

@github-actions github-actions Bot left a comment

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.

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

weight = weight.saturating_add(T::DbWeight::get().reads(3));

// Collect before rewriting Lock so mutation cannot disturb the iterator.
let locks: Vec<_> = Lock::<T>::iter().collect();

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] Runtime upgrade materializes every lock without a hard bound

This runtime upgrade collects the entire Lock map and then clears and rebuilds several maps in one block. The observed mainnet row count is not an enforced bound; unexpected state growth can make the upgrade exceed block resources and jeopardize chain liveness. Stage the migration across blocks or enforce a proven hard bound before upgrading.

Comment thread pallets/subtensor/src/staking/lock.rs Outdated
/// complete member-scaled work instead of adding permanent storage bookkeeping.
pub fn owner_transition_member_count(netuid: NetUid, new_owner_hotkey: &T::AccountId) -> u32 {
let old_owner_hotkey = SubnetOwnerHotkey::<T>::get(netuid);
let old_owner_members = LockingColdkeys::<T>::iter_prefix((netuid, &old_owner_hotkey))

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] Ownership transitions scan an unbounded member index

This prefix scan has no enforced cardinality limit and runs while determining dispatch weight; the transition later scans and materializes the same unbounded index. Returned member-scaled weight accounts for work after it occurs but does not prevent oversized validation or execution. Bound membership per subnet or implement a staged transition.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@github-actions github-actions Bot left a comment

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.

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

Comment on lines +1209 to +1211
let coldkeys: Vec<T::AccountId> = LockingColdkeys::<T>::iter_prefix((netuid, hotkey))
.map(|(coldkey, ())| coldkey)
.collect();

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] Ownership transitions scan an unbounded member index

Every ownership change collects and processes the entire (netuid, hotkey) reverse-index prefix, with no enforced member bound. These transitions are reachable from recurring runtime paths, including automatic takeover, lease termination, and administrative owner changes; an oversized index can therefore exhaust block resources. Reported weight after completion does not bound execution. Use a bounded collection with an invariant-enforced maximum, or stage the transition across blocks.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

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.

3 participants