Skip to content

Fix coldkey-wide root claims and stale staking relationships - #3141

Merged
UnArbosFive merged 8 commits into
release-454from
fix/claim-budget-cap
Sep 4, 2026
Merged

Fix coldkey-wide root claims and stale staking relationships#3141
UnArbosFive merged 8 commits into
release-454from
fix/claim-budget-cap

Conversation

@UnArbosFour

@UnArbosFour UnArbosFour commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes root claims that could be blocked by unrelated subnet stakes, stale StakingHotkeys relationships, subnet count, or zero-rounded basket holdings.

The runtime spec version is bumped to 454.

Behavior changes

Scenario Before After
Coldkey stakes to several root validators Claim could exceed the topology-based budget All root-relevant validators are claimed within the fixed work envelope
Coldkey also stakes on ordinary subnets Every StakingHotkeys entry increased claim work Non-root-only hotkeys are excluded
Network has many subnets Work was estimated as hotkeys × networks Subnet count does not affect admission
Unstaked claimant has outstanding basket entitlement Could be filtered out Retained when BasketClaimed < 0
Valuable basket holding rounds to a zero take Claim could succeed, charge a fee, and pay nothing Claim remains unsettled without changing its watermark
Full unstake leaves shares worth less than 1 RAO Residual shares could later regain value Positive residual shares worth 0 RAO are cleared with the corresponding denominator adjustment

Admission now uses:

root-relevant hotkeys + actual Alpha/AlphaV2 basket rows <= 256

The raw StakingHotkeys classification scan is independently capped at 256 and charged through actual post-dispatch weight.

Mainnet and testnet

Network Declared work envelope Effect of subnet count
Mainnet 256 None
Testnet 256 None

The previous genesis-hash-specific testnet envelope has been removed. Both networks now use the same fixed budget and refund unused work through post-dispatch weight.

Stale relationship cleanup

The bounded storage cleanup and StakingHotkeys cleanup are rerun under fresh migration names:

  • migrate_storage_bloat_v3
  • migrate_cleanup_staking_hotkeys_v2

The storage cleanup now also removes exact-zero AlphaV2 rows. Once zero share rows are gone, the relationship cleanup removes entries that have neither share rows nor a nonzero basket watermark.

Existing positive sub-RAO shares are retained because deleting only their index would leave live shares undiscoverable if their value later increased. They no longer block root claims when they are not root-relevant, and the share-pool change prevents new zero-valued residue from being created during full withdrawals.

Validation

  • cargo fmt --check --all
  • Ruff lint and formatting checks
  • git diff --check
  • Added coverage for root-hotkey filtering, negative watermarks, fixed admission accounting, zero AlphaV2 cleanup, migration reruns, and share-residue canonicalization

The generated documentation check detected broad unrelated pre-existing drift, so generated reference files were not rewritten.

@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
subtensor Ready Ready Preview Sep 4, 2026 4:01pm UTC

Request Review

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🛡️ AI Review — Skeptic (security review)

VERDICT: VULNERABLE

VERY HIGH scrutiny (one public repo), mitigated by repository write access and substantive merged history; no Gittensor association or author/committer mismatch. fix/claim-budget-cap -> release-454.

The pre-dispatch declaration is now additive, but post-dispatch accounting still collapses two sequential workloads with max. No malicious behavior or AI-review trust-boundary changes were found.

Findings

Sev File Finding
HIGH pallets/subtensor/src/staking/claim_root.rs:973 Post-dispatch weight still drops additive selection work inline

Prior-comment reconciliation

  • 332699e8: not addressed — The declaration is now additive, but actual post-dispatch accounting still combines classification and claim workloads with .max(selection_scanned).

Conclusion

Coldkey-wide claims can still underreport consumed weight because relationship classification and claim processing are accounted non-additively. This remains merge-blocking.


📜 Previous run (superseded)
Sev File Finding Status
HIGH pallets/subtensor/src/staking/claim_root.rs:973 Post-dispatch weight still drops additive selection work ➡️ Carried forward to current findings
The declaration is now additive, but actual post-dispatch accounting still combines classification and claim workloads with .max(selection_scanned).

🔍 AI Review — Auditor (domain review)

VERDICT: 👍

LIKELY Gittensor-associated contributor with repository write access and substantial recent Subtensor activity; no substantive duplicate PR identified.

The chain-specific admission envelope, declared/actual weight accounting, rounding behavior, SDK preview, documentation, and focused regressions are consistent with the PR description. The spec_version bump to 454 is present.

Checks: git diff --check passed. cargo fmt --check --all was skipped because rustup could not write to its locked toolchain temp directory; Python Ruff checks were skipped because the existing sdk/python environment was unavailable. The working tree remains clean.

Findings

No findings.

Conclusion

The change is narrowly scoped, preserves the mainnet admission cap, correctly accounts for both redemption and scan work, and adds appropriate boundary and value-preservation coverage. Ready to merge subject to CI.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: SAFE Auditor: 👍

@UnArbosFour UnArbosFour changed the title Fix root claim admission budgets and rounding-blocked payouts Fix coldkey-wide root claims and stale staking relationships Sep 4, 2026
@UnArbosFour
UnArbosFour changed the base branch from main to release-454 September 4, 2026 14:53

@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 969 to +973
let active = hotkey_count
.max(outcome.realized.saturating_add(outcome.swept))
// Classifying a StakingHotkeys relationship reads the position's share-pool state
// and basket watermark. Price it conservatively as a full hotkey unit.
.max(selection_scanned)

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] Post-dispatch weight drops an additive selection workload

selection_scanned is work performed before redemption, but folding it into active with max charges for either relationship classification or active basket processing—not both. For example, a coldkey can have 256 staking relationships while its selected root hotkey also processes many basket rows; the call then reports only claim_root(256) (plus non-realized row scans), despite performing both workloads. Because this value is returned as actual_weight, the declared claim_root(256) + claim_root_scan(256) reservation is refunded and repeated calls can make a block execute materially more work than its recorded weight. Account for selection scans additively using a benchmarked component while retaining the declared envelope.

@github-actions

github-actions Bot commented Sep 4, 2026

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 969 to +973
let active = hotkey_count
.max(outcome.realized.saturating_add(outcome.swept))
// Classifying a StakingHotkeys relationship reads the position's share-pool state
// and basket watermark. Price it conservatively as a full hotkey unit.
.max(selection_scanned)

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] Post-dispatch weight still drops additive selection work

Every StakingHotkeys relationship is classified before the selected hotkeys and basket rows are processed. Taking the maximum of selection_scanned and claim work charges only the larger workload, although both execute. An attacker can therefore pack blocks using understated post-dispatch weight. Account for classification additively, consistently with the declared claim_root + claim_root_scan envelope.

Suggested change
let active = hotkey_count
.max(outcome.realized.saturating_add(outcome.swept))
// Classifying a StakingHotkeys relationship reads the position's share-pool state
// and basket watermark. Price it conservatively as a full hotkey unit.
.max(selection_scanned)
let active = hotkey_count
.max(outcome.realized.saturating_add(outcome.swept))
.max(1);
let scanned = outcome
.rows
.saturating_sub(outcome.realized)
.saturating_add(selection_scanned);
<T as crate::pallet::Config>::WeightInfo::claim_root(active).saturating_add(
<T as crate::pallet::Config>::WeightInfo::claim_root_scan(scanned),
)

@github-actions

github-actions Bot commented Sep 4, 2026

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.

.max(outcome.realized.saturating_add(outcome.swept))
// Classifying a StakingHotkeys relationship reads the position's share-pool state
// and basket watermark. Price it conservatively as a full hotkey unit.
.max(selection_scanned)

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] Post-dispatch weight still drops additive selection work

selection_scanned is combined with claim work using max, although classification happens before and in addition to basket processing. A call can classify up to 256 relationships and then perform up to 256 admitted claim-work units, yet its post-dispatch weight reports only the larger workload. Charge classification additively (using its benchmarked cost) before adding basket scan/redeem weight.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@UnArbosFour UnArbosFour added the skip-cargo-audit This PR fails cargo audit but needs to be merged anyway label Sep 4, 2026

@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.

.max(outcome.realized.saturating_add(outcome.swept))
// Classifying a StakingHotkeys relationship reads the position's share-pool state
// and basket watermark. Price it conservatively as a full hotkey unit.
.max(selection_scanned)

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] Post-dispatch weight still drops additive selection work

selection_scanned represents candidate-classification work performed before the selected hotkeys are claimed. Taking the maximum charges for either classification or claim processing, not both, so a call with many candidates and substantial claim work receives an excessive refund. Account for classification additively—consistent with the declared claim_root(limit) + claim_root_scan(limit) envelope—rather than folding it into active.

@github-actions

github-actions Bot commented Sep 4, 2026

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 969 to +973
let active = hotkey_count
.max(outcome.realized.saturating_add(outcome.swept))
// Classifying a StakingHotkeys relationship reads the position's share-pool state
// and basket watermark. Price it conservatively as a full hotkey unit.
.max(selection_scanned)

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] Post-dispatch weight still drops additive selection work

The classification scan and subsequent claim processing execute sequentially, but max(selection_scanned) charges only the larger workload. A claim with many raw StakingHotkeys entries and substantial selected-hotkey/basket work therefore refunds weight that was actually consumed, permitting blocks to execute beyond their accounted limit. Charge selection work additively and ensure the declared envelope covers the resulting worst case.

@github-actions

github-actions Bot commented Sep 4, 2026

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 969 to +973
let active = hotkey_count
.max(outcome.realized.saturating_add(outcome.swept))
// Classifying a StakingHotkeys relationship reads the position's share-pool state
// and basket watermark. Price it conservatively as a full hotkey unit.
.max(selection_scanned)

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] Post-dispatch weight still drops additive selection work

selection_scanned measures the classification pass performed before do_root_claim, so its cost is additive to selected-hotkey and redemption work. Combining it with max refunds one of two sequential workloads, allowing an extrinsic to report less weight than it consumed. Mirror the additive declaration by adding the measured classification weight separately.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@UnArbosFive
UnArbosFive merged commit 1e2aec0 into release-454 Sep 4, 2026
96 of 102 checks passed
@UnArbosFive UnArbosFive mentioned this pull request Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-cargo-audit This PR fails cargo audit but needs to be merged anyway

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants