Fix conviction aggregate roll-forward- #3060 - #3073
Conversation
… challengers to independently meet the 10% conviction threshold.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| 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(); |
There was a problem hiding this comment.
[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.
| /// 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)) |
There was a problem hiding this comment.
[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.
🛡️ 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
Other findings
Prior-comment reconciliation
ConclusionThe 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)
# 🔍 AI Review — Auditor (domain review) has not yet run on this PR. |
|
🔄 AI review updated — Skeptic: VULNERABLE |
# 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
eco-tests changed — indexer review requiredThis PR modifies files under Changed files
|
| 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(); |
There was a problem hiding this comment.
[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.
| /// 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)) |
There was a problem hiding this comment.
[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.
|
🔄 AI review updated — Skeptic: VULNERABLE |
| let coldkeys: Vec<T::AccountId> = LockingColdkeys::<T>::iter_prefix((netuid, hotkey)) | ||
| .map(|(coldkey, ())| coldkey) | ||
| .collect(); |
There was a problem hiding this comment.
[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.
|
🔄 AI review updated — Skeptic: VULNERABLE |
Summary
Fix conviction-lock aggregate accounting and rebuild aggregates corrupted by partial roll-forward semantics.
This change keeps the current
maintakeover 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:
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
ConvictionModelto one individual lock and its corresponding aggregate class.Ownership transitions
Takeover policy
Preserve the current
mainbehavior:Runtime migration
Add
migrate_rebuild_conviction_aggregates, guarded byHasMigrationRun.The migration:
Lockrows.LockingColdkeys.Test coverage
Added regression coverage for:
Validation
cargo fmt --check --allgit diff --checkFull builds and tests are left to CI per repository guidance.
Runtime version
The rebase preserves
main’s currentspec_versionof 451. A subsequent version bump orno-spec-version-bumplabel may be required before merge.