Skip to content

fix(sdk): enforce wrapped intent safety preflights - #3100

Merged
UnArbosFive merged 11 commits into
release-448from
fix/sdk-intent-safety-preflights-ready
Aug 20, 2026
Merged

fix(sdk): enforce wrapped intent safety preflights#3100
UnArbosFive merged 11 commits into
release-448from
fix/sdk-intent-safety-preflights-ready

Conversation

@UnArbosFive

@UnArbosFive UnArbosFive commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Why

This closes six SDK safety gaps found while reviewing the release-448 behavior around shielded submissions, root-claim preflights, wrapped execution, claim-then-unstake, and finalization. It also includes the focused CI and correctness fixes found while validating the release branch.

Original findings addressed

1. Shielded submissions now enforce intent hard stops

Previously submit_shielded() applied Policy checks but bypassed the hard stops returned by an intent.

This change:

  • introduces a context-aware IntentPreflight result containing effects, warnings, hard stops, and claim affordability data
  • routes normal planning and shielded submission through the same preflight path
  • merges preflight hard stops into shielded policy enforcement before any signature is requested
  • propagates child hard stops through Batch, with child indexes preserved in messages
  • preserves semantic intent safety checks through saved-multisig adapters
  • fails closed when a shielded root claim cannot prove that free TAO covers both the inner declared-fee reserve and the outer MEV-shield carrier fee

2. Root claims now preflight RootClaimTooHeavy

The runtime rejects root claims when either:

  • hotkey_count × existing_network_count > 256, or
  • total reachable basket holdings exceed 256

The SDK now mirrors that admission envelope before signing:

  • reads the dispatch-origin coldkey hotkeys for coldkey-wide claims
  • counts only live NetworksAdded=true networks, including the root minimum
  • reads each validator basket and totals its holdings
  • applies the same inclusive 256-unit boundaries used by the runtime
  • recommends per-validator claim_root_with_hotkey when a coldkey-wide claim is too large
  • treats unavailable admission state as a hard stop instead of risking the unreduced declared fee
  • applies the same check to embedded claim-then-unstake flows and shielded submissions

3. --claim --all now removes newly claimed root yield

The old flow resolved all from the pre-claim stake and then batched a claim before that fixed-amount unstake, leaving the newly claimed yield staked.

For root claim=True plus amount_alpha="all", the SDK now:

  • preserves the existing no-position guard before building
  • submits u64::MAX to plain remove_stake
  • relies on the runtime remove_stake cap to resolve the amount from the live post-claim position inside the atomic batch
  • uses the same runtime-capped plain unstake for RemoveStakeLimit on root after validating that its root limit is valid
  • refuses atomic claim-then-unstake while RootStakeUnlockInterval is active, because a successful claim refreshes the hold and the following unstake cannot succeed

This keeps claim and full exit atomic while removing principal and yield together.

4. Wrapped execution now separates dispatch origin from fee payer

Proxy and multisig wrappers can make the account whose state changes different from the member or delegate paying the extrinsic fee.

The executor now derives both accounts explicitly:

  • semantic build, effects, warnings, and hard stops use the actual dispatch origin
  • fee estimation and free-balance checks use the outer fee payer
  • proxy execution reads the proxied account state while pricing the delegate-signed wrapper
  • multisig execution reads multisig-owned state while checking the signing member balance
  • proxy plus multisig composition keeps the proxy target as dispatch origin and the member as fee payer
  • the public async and sync clients expose the same context-aware preflight
  • clear and shielded wrapped register_subnet completion use the semantic operation and actual dispatch-origin owner, rather than the wrapper operation or member address

The final registration-completion item was added after review found that origin separation had not yet been carried through the post-submission path.

5. Shielded APIs now honor and safely bound finalization requests

The shielded raw-call and multisig approval paths now propagate wait_for_finalization instead of hardcoding it to false.

When finalization is requested, decrypted inner-extrinsic resolution now:

  • waits until the finalized head reaches the block containing the inner extrinsic
  • tolerates transient finalized-head RPC failures
  • times out individual finalized-head RPC attempts after at most one detected block time (with a one-second floor)
  • raises a clear ChainError after four consecutive finalized-head RPC failures
  • bounds the total finality-lag polling phase using the same era-derived four-polls-per-block budget as shielded receipt scanning
  • re-reads the canonical hash at the inner extrinsic's height after finality
  • bounds the canonical-hash and reorg receipt lookups to four timed attempts each, tolerating transient failure and raising a clear ChainError when either RPC remains unavailable or never returns
  • re-resolves the inner receipt if a reorg changed the block before finality
  • continues scanning when the previously observed inner extrinsic is no longer canonical

This prevents a finalized shielded API call from returning only an inclusion-level inner receipt, and prevents a permanently unavailable, non-returning, or stalled finality RPC from making the call wait forever.

6. Coldkey-wide thresholds are now evaluated per validator

The runtime applies RootClaimableThreshold separately to each validator basket. The SDK previously compared the threshold with one aggregate coldkey-wide owed value.

The quote now:

  • reads get_root_basket_positions
  • maps payout by validator hotkey
  • classifies eligibility independently for each validator
  • reports eligible and below-threshold validator counts
  • calculates redeemable accrued TAO from eligible validators only
  • prices full redemption work and below-threshold scan work separately
  • warns when only part of a coldkey-wide claim can redeem

A review follow-up also fixed the zero-entitlement case: validators omitted by get_root_basket_positions have no owed shares. They are no longer misclassified as below-threshold positions or charged basket scan work in the quote.

Additional correctness and CI fixes

  • Timelock UserData decoding now rejects trailing bytes instead of accepting a valid SCALE prefix and ignoring leftover ciphertext. This removes a probabilistic decode path that made the randomized ciphertext test flaky and applies strict decoding to reveal, inner-ciphertext, decrypt, and signed-decrypt paths.
  • TotalAlphaStaked now removes its storage key when a live delta reduces the aggregate to zero. This restores the expected invariant after a final unstake or subnet dissolution without changing the storage read/write cost.
  • The trusted docs-preview deployment now extracts its artifact beneath the configured website project root, preserving Vercel's rootDirectory contract while retaining the existing trust boundary.
  • Python runtime metadata bindings are regenerated from the exact CI-built v448 node. The generator now normalizes each artifact to one trailing newline, preventing unchanged descriptor files from failing git diff --check while retaining an exact live-node drift gate.

Additional safety behavior

  • reserved root-claim fees remain a fail-closed affordability check even when optional payout preview APIs are unavailable
  • shielded carrier pricing uses the metadata ciphertext bound to conservatively estimate the outer fee
  • claim fee estimates reuse the fully composed proxy, batch, or multisig call
  • affected CLI pre-confirmation output uses the same context-aware preflight as execution
  • generated transaction reference pages and the public intents catalog reflect the claim-all and root-hold behavior

Design scope

The existing IntentPreflight abstraction is intentionally retained. A broader redesign of how generic wrappers merge balance requirements was considered during review, but it is not needed to fix a demonstrated bug and would materially expand this correctness-focused PR.

Validation

  • uv run --no-sync pytest tests/unit/test_intents_table.py tests/unit/test_root_claim_fee.py tests/unit/test_multisig_safety.py -q566 passed
  • uv run --no-sync ruff check .
  • uv run --no-sync ruff format --check .
  • uv run --no-sync python ../../website/apps/bittensor-website/scripts/generate.py --check
  • uv run --no-sync python -m codegen.check --coverage
  • uv run --no-sync python -m codegen.check --names
  • uv run --no-sync python -m codegen.check --units
  • uv run --no-sync python -m codegen.check --namespaces
  • focused timelock regression test repeated 20 times after strict-decoding fix
  • exact TotalAlphaStaked final-unstake and subnet-dissolution regressions
  • docs-preview workflow YAML parsing and 57 security/policy tests
  • python -m codegen.check --drift ws://127.0.0.1:9944 against the exact CI-built v448 node — no drift
  • cargo fmt --check --all
  • git diff --check

Documentation

  • Added the v448 release page and promoted it in the releases index, covering fixed-envelope root claims, protected stake moves, bulk multi-hotkey exits, bounded staking-index migrations, linked orders, and wrapped/shielded submission behavior.
  • Corrected the Root Reborn and staking guides for the fixed 256-unit claim reserve, per-validator thresholds, the root hold-window sequence, SDK/btcli fill-or-kill moves, and runtime-skipped bulk-exit positions.
  • Updated migration and advanced-submission guidance for move_stake_limit, dispatch-origin versus fee-payer semantics, and bounded canonical-finality waits.
  • Documented the EVM moveStakeLimit ABI, units, and non-payable status, plus WASM chain-extension function IDs 37 and 38.
  • Corrected the RootClaimTooHeavy, root-claim fee, staking-index, and limit-order references; regenerated transaction/query/error pages, catalogs, and namespace stubs.
  • Fixed generated move/swap examples so destination hotkeys and netuids differ from their origins.

@vercel

vercel Bot commented Aug 20, 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 20, 2026 9:04pm

Request Review

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🛡️ AI Review — Skeptic (security review)

VERDICT: SAFE

HIGH account-age scrutiny (49-day-old account), moderated by admin permission, substantial contributions, matching author/committer, and no known Gittensor association; fix/sdk-intent-safety-preflights-ready → release-448.

Findings

No findings.

Conclusion

Static analysis found no security vulnerability, malicious behavior, or AI-review trust-boundary modification. The previously reviewed bounded canonical-receipt handling remains addressed.


🔍 AI Review — Auditor (domain review)

VERDICT: 👍

Established repository admin with substantial recent contributions; no trusted gittensor allowlist match (UNKNOWN).

The implementation matches the substantive PR description and includes focused coverage for the safety-sensitive paths.

PR #3097 overlaps the single-hotkey root-claim quote fix, but this PR provides the broader integrated solution and stronger coverage. This PR is the better candidate. Recommend closing #3097.

Checks:

  • git diff --check — passed
  • git status --short — clean
  • cargo fmt --check --all — skipped because rustup could not write to its locked environment
  • Python Ruff/docs checks — skipped because uv is unavailable

Findings

No findings.

Conclusion

The wrapped-origin, claim-admission, shielded-finality, and storage-invariant changes are coherent and adequately tested. No substantive merge-blocking issue was found.

@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 thread sdk/python/bittensor/executor.py Outdated
Comment on lines +1089 to +1092
finalized = await self.substrate.finalized_block_number()
except Exception:
finalized = block - 1
if finalized >= block:

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.

[MEDIUM] Finalization RPC failure causes an infinite wait

Every finalized_block_number() failure is converted to block - 1, so a permanently unavailable or unsupported RPC leaves this while True loop sleeping forever. Propagate the RPC failure instead, or apply an explicit bounded retry policy, so wait_for_finalization=True cannot hang indefinitely.

Suggested change
finalized = await self.substrate.finalized_block_number()
except Exception:
finalized = block - 1
if finalized >= block:
finalized = await self.substrate.finalized_block_number()

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: SAFE Auditor: 👍

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: SAFE Auditor: 👍

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: SAFE Auditor: 👍

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: SAFE Auditor: 👍

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: SAFE Auditor: 👍

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: SAFE Auditor: 👍

@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 thread sdk/python/bittensor/executor.py Outdated
# Finality applies to the canonical block at this height.
# Re-resolve after a reorg instead of returning a receipt
# observed on a fork before the finalized head caught up.
canonical_hash = await self.substrate.block_hash(block)

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.

[MEDIUM] Canonical-hash RPC can defeat the finalization bound

After finality is detected, this newly added RPC is awaited without a timeout. A stalled or adversarial endpoint can therefore hang wait_for_finalization=True indefinitely, bypassing the bounded polling immediately above. Apply the same per-attempt timeout and convert exhaustion into ChainError; the subsequent receipt lookup also needs a bound if it can block independently.

@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 thread sdk/python/bittensor/executor.py Outdated
)
raise ChainError(message) from canonical_hash_error
if canonical_hash != block_hash:
inner = await self.substrate.find_extrinsic(inner_hash, canonical_hash)

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.

[MEDIUM] Canonical receipt lookup remains unbounded

After a pre-finality reorg, this newly introduced RPC await has no timeout. A node that returns the canonical hash but never completes find_extrinsic can therefore hang a shielded submission indefinitely despite the surrounding finalization bounds. Apply a bounded timeout/retry policy here as well, and fail clearly or resume scanning when it is exhausted.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: SAFE Auditor: 👍

@UnArbosFive
UnArbosFive merged commit 0d39882 into release-448 Aug 20, 2026
45 of 47 checks passed
@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