v431 release#2898
Conversation
Add bittensor/error_descriptions.py: a researched description for each of the 353 classified chain error names, grounded in the pallet raise sites. Surface it in the docs errors table and /catalog/errors.json, in the btcli failure diagnostic and `btcli explain <Name>`, and on ChainError for the SDK. The --names gate now enforces that classification and descriptions cover exactly the same names. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🛡️ AI Review — Skeptic (security review)VERDICT: VULNERABLE MEDIUM scrutiny: established contributor with repository write access; no Gittensor association found; error-descriptions -> main. Static analysis found no evidence of malicious intent, and the trusted AI-review instruction files were not modified. Both previously reported runtime resource-exhaustion vulnerabilities remain present. Findings
Prior-comment reconciliation
ConclusionThe PR remains vulnerable because runtime-upgrade and subnet-dissolution paths can perform attacker-influenced work beyond the block-weight budget. Both paths must be bounded and resumable before merge. 📜 Previous run (superseded)
🔍 AI Review — Auditor (domain review)VERDICT: 👎 Gittensor association UNKNOWN; established contributor with repository write access and substantial prior history, with release-sized changes receiving full scrutiny. Description discrepanciesThe PR body still omits substantial v431 runtime changes, including dissolution settlement, stake refunds and transfer semantics, alpha fee handling, migrations, proxy filtering, nonce weights, allocator support, and deployment tooling. The listed overlaps are incidental rather than duplicate implementations. Findings
Prior-comment reconciliation
ConclusionThe identity migration remains an unbounded upgrade-time scan without try-runtime validation. It must be bounded or batched and fully weight-accounted before merge. 📜 Previous run (superseded)
|
| "chain_errors": { | ||
| n: {"code": c.value, "description": error_descriptions.DESCRIPTIONS[n]} | ||
| for n, c in sorted(error_map.NAME_TO_CODE.items()) | ||
| }, |
There was a problem hiding this comment.
[MEDIUM] Preserve the published catalog's value type
/catalog/errors.json is documented as a machine-readable agent API, but this changes every chain_errors[name] value from a string to an object. Existing consumers comparing or decoding that value as an error-code string will break without any endpoint/version change. Keep the established mapping and publish descriptions in a parallel field.
| "chain_errors": { | |
| n: {"code": c.value, "description": error_descriptions.DESCRIPTIONS[n]} | |
| for n, c in sorted(error_map.NAME_TO_CODE.items()) | |
| }, | |
| "chain_errors": {n: c.value for n, c in sorted(error_map.NAME_TO_CODE.items())}, | |
| "chain_error_descriptions": { | |
| n: error_descriptions.DESCRIPTIONS[n] | |
| for n in sorted(error_map.NAME_TO_CODE) | |
| }, |
|
🔄 AI review updated — Skeptic: SAFE Auditor: 👍 |
The move from gh-action-pypi-publish to uv publish silently dropped attestation generation: uv only uploads attestations that already exist in dist/. Generate them with astral-sh/attest-action in each publish job (stable, rc, and dev channels) so releases are provenance-signed again. Co-authored-by: Cursor <cursoragent@cursor.com>
| "chain_errors": { | ||
| n: {"code": c.value, "description": error_descriptions.DESCRIPTIONS[n]} | ||
| for n, c in sorted(error_map.NAME_TO_CODE.items()) | ||
| }, |
There was a problem hiding this comment.
[MEDIUM] Preserve the published catalog's value type
This still changes every chain_errors[name] value from a string to an object, breaking existing consumers of this public machine-readable catalog. Preserve the established map and publish descriptions under a new sibling key.
| "chain_errors": { | |
| n: {"code": c.value, "description": error_descriptions.DESCRIPTIONS[n]} | |
| for n, c in sorted(error_map.NAME_TO_CODE.items()) | |
| }, | |
| "chain_errors": {n: c.value for n, c in sorted(error_map.NAME_TO_CODE.items())}, | |
| "chain_error_descriptions": { | |
| n: error_descriptions.DESCRIPTIONS[n] | |
| for n in sorted(error_map.NAME_TO_CODE) | |
| }, |
|
🔄 AI review updated — Skeptic: SAFE Auditor: 👍 |
…isting Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
| "chain_errors": { | ||
| n: {"code": c.value, "description": error_descriptions.DESCRIPTIONS[n]} | ||
| for n, c in sorted(error_map.NAME_TO_CODE.items()) | ||
| }, |
There was a problem hiding this comment.
[MEDIUM] Preserve the published catalog's value type
chain_errors is an already-published catalog API whose values were strings. Replacing every value with an object silently breaks consumers expecting chain_errors[name] to be an error-code string. Preserve that field and publish descriptions under a new sibling key.
| "chain_errors": { | |
| n: {"code": c.value, "description": error_descriptions.DESCRIPTIONS[n]} | |
| for n, c in sorted(error_map.NAME_TO_CODE.items()) | |
| }, | |
| "chain_errors": {n: c.value for n, c in sorted(error_map.NAME_TO_CODE.items())}, | |
| "chain_error_descriptions": { | |
| n: error_descriptions.DESCRIPTIONS[n] | |
| for n in sorted(error_map.NAME_TO_CODE) | |
| }, |
|
🔄 AI review updated — Skeptic: SAFE Auditor: 👍 |
Every page that previously had no visual gets a dedicated interactive chart (16 new components: epoch timeline, activity bars, permit line, rate-limit timelines, version/registration gates, per-block caps, bonds clipping/reset, Yuma3 comparison, serve-rate, childkey range, transfer flow, legacy adjustment, PoW dead-end). Family demos now adapt scenarios, sliders, and captions to their focus parameter. Co-authored-by: Cursor <cursoragent@cursor.com>
| "chain_errors": { | ||
| n: {"code": c.value, "description": error_descriptions.DESCRIPTIONS[n]} | ||
| for n, c in sorted(error_map.NAME_TO_CODE.items()) | ||
| }, |
There was a problem hiding this comment.
[MEDIUM] Preserve the published catalog's value type
chain_errors is already a published catalog whose values are strings. Replacing every value with an object breaks consumers that deserialize or compare the existing shape. Preserve that field and publish descriptions separately.
| "chain_errors": { | |
| n: {"code": c.value, "description": error_descriptions.DESCRIPTIONS[n]} | |
| for n, c in sorted(error_map.NAME_TO_CODE.items()) | |
| }, | |
| "chain_errors": {n: c.value for n, c in sorted(error_map.NAME_TO_CODE.items())}, | |
| "chain_error_descriptions": { | |
| n: error_descriptions.DESCRIPTIONS[n] | |
| for n in sorted(error_map.NAME_TO_CODE) | |
| }, |
|
🔄 AI review updated — Skeptic: SAFE Auditor: 👍 |
…, and split error docs into per-error pages Co-authored-by: Cursor <cursoragent@cursor.com>
| /// (rate-limited via `OwnerHyperparamUpdate`, respects the admin freeze | ||
| /// window) or the root account (bypasses both). This supersedes the | ||
| /// absolute-blocks `sudo_set_activity_cutoff`. | ||
| #[pallet::call_index(97)] |
There was a problem hiding this comment.
[CRITICAL] Bump spec_version for runtime changes
This adds a runtime dispatchable and the PR also moves/removes calls and changes proxy filters, but runtime/src/lib.rs remains at spec_version: 430. Substrate uses this version to decide native-runtime compatibility; deploying changed Wasm under the same version can leave older nodes executing their old native implementation while other nodes execute the new Wasm, creating a consensus-fork risk. Increment spec_version in this PR.
|
🔄 AI review updated — Skeptic: VULNERABLE |
Co-authored-by: Cursor <cursoragent@cursor.com>
| /// (rate-limited via `OwnerHyperparamUpdate`, respects the admin freeze | ||
| /// window) or the root account (bypasses both). This supersedes the | ||
| /// absolute-blocks `sudo_set_activity_cutoff`. | ||
| #[pallet::call_index(97)] |
There was a problem hiding this comment.
[CRITICAL] Bump spec_version for runtime changes
This PR adds and relocates runtime calls, changes dispatch semantics, weights, proxy filters, and metadata, but runtime/src/lib.rs still has spec_version: 430, identical to the base. Nodes may treat the changed runtime as native-compatible with the old implementation, risking native/Wasm execution divergence. Increment spec_version for this runtime upgrade.
|
🔄 AI review updated — Skeptic: VULNERABLE |
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Clone last_key before iter_from so pallet-subtensor builds again, drop the e2e manifest expectation to 112 after removing set_mechanism_emission_split, and ruff-format query.py. Co-authored-by: Cursor <cursoragent@cursor.com>
| // Fold deprecated SubnetTaoProvided / SubnetAlphaInProvided residuals into the | ||
| // main AMM reserves (issue #2793). Guarded by HasMigrationRun, so it only runs once. | ||
| .saturating_add(migrations::migrate_cleanup_swap_v3::migrate_cleanup_swap_v3::<T>()) |
There was a problem hiding this comment.
[HIGH] Activated migrations perform unbounded upgrade work
migrate_cleanup_swap_v3 traverses both deprecated storage maps and clears their prefixes in one runtime upgrade, while the newly chained identity cleanup also collects every SubnetIdentitiesV3 key before returning. Their returned weights merely report work after it has occurred; they do not constrain execution to the upgrade's block budget. An attacker-inflated map can therefore make on_runtime_upgrade exceed the maximum block weight. Convert both migrations to bounded, cursor-based multi-block migrations (or prove and enforce a strict storage bound) and account every read/write.
| } | ||
| hotkeys_in_subnet.push(hot.clone()); | ||
|
|
||
| let mut inner_read_all = true; | ||
| let mut coldkey_value_vec: Vec<(T::AccountId, u128)> = Vec::new(); | ||
|
|
||
| // Handle one hotkey and all its coldkeys or skip the hotkey if the weight is not enough | ||
| // Then we just need to record the hotkey as checkpoint | ||
| // Drain the whole hotkey prefix once started. Weight is accounted when it | ||
| // still fits; overshoot is allowed so the cursor can advance past this hotkey. |
There was a problem hiding this comment.
[HIGH] Dissolution deliberately bypasses the block weight limit
Once a hotkey prefix starts, this loop continues scanning even after weight_meter is exhausted; the same pattern is added to the total-calculation and cleanup phases. A hotkey can have an attacker-influenced number of coldkey/subnet entries, so one on_idle call can perform unbounded reads/writes beyond its remaining weight and stall block production. Progress must use a two-dimensional cursor (hotkey plus inner storage key) so processing can stop exactly at the budget and resume next block.
|
🔄 AI review updated — Skeptic: VULNERABLE |
Removing the unused SetMechanismEmissionSplit intent left AdminUtils.sudo_set_mechanism_emission_split unwrapped; list it as raw-only so SDK offline checks pass. Co-authored-by: Cursor <cursoragent@cursor.com>
| .saturating_add(migrations::migrate_associated_evm_address_index::migrate_associated_evm_address_index::<T>()) | ||
| // Fold deprecated SubnetTaoProvided / SubnetAlphaInProvided residuals into the | ||
| // main AMM reserves (issue #2793). Guarded by HasMigrationRun, so it only runs once. | ||
| .saturating_add(migrations::migrate_cleanup_swap_v3::migrate_cleanup_swap_v3::<T>()) |
There was a problem hiding this comment.
[HIGH] Activated migrations perform unbounded upgrade work
This activates migrate_cleanup_swap_v3 during on_runtime_upgrade; it fully iterates and clears two storage maps. The newly chained identity migration also collects every SubnetIdentitiesV3 key before returning. Reporting weight after these unbounded traversals does not constrain execution, so attacker-influenced state size can make the upgrade exceed the block budget. Convert these migrations to bounded, resumable steps with persistent cursors before activating them.
| // Handle one hotkey and all its coldkeys or skip the hotkey if the weight is not enough | ||
| // Then we just need to record the hotkey as checkpoint | ||
| // Drain the whole hotkey prefix once started. Weight is accounted when it | ||
| // still fits; overshoot is allowed so the cursor can advance past this hotkey. |
There was a problem hiding this comment.
[HIGH] Dissolution deliberately bypasses the block weight limit
Once a hotkey is started, this path deliberately continues its complete, attacker-influenced coldkey prefix after WeightMeter is exhausted. The same pattern appears in total calculation and cleanup, including transfers and storage writes that are performed without consuming their weight. A hotkey with a sufficiently large prefix can therefore make on_idle exceed the block budget. Use a resumable inner-prefix cursor instead of allowing overshoot.
|
🔄 AI review updated — Skeptic: VULNERABLE |
Co-authored-by: Cursor <cursoragent@cursor.com>
| .saturating_add(migrations::migrate_associated_evm_address_index::migrate_associated_evm_address_index::<T>()) | ||
| // Fold deprecated SubnetTaoProvided / SubnetAlphaInProvided residuals into the | ||
| // main AMM reserves (issue #2793). Guarded by HasMigrationRun, so it only runs once. | ||
| .saturating_add(migrations::migrate_cleanup_swap_v3::migrate_cleanup_swap_v3::<T>()) |
There was a problem hiding this comment.
[HIGH] Activated migrations perform unbounded upgrade work
This activates migrate_cleanup_swap_v3, which traverses storage maps without a per-block bound, and then chains another migration that scans every SubnetIdentitiesV3 key. Returning the accumulated weight after completing the work does not constrain execution to the maximum block weight; sufficiently large attacker-influenced state can make the runtime upgrade exceed the block budget and stall chain progress. Convert these migrations to bounded, cursor-based multi-block migrations before activation.
|
|
||
| let mut inner_read_all = true; | ||
| let mut coldkey_value_vec: Vec<(T::AccountId, u128)> = Vec::new(); | ||
|
|
||
| // Handle one hotkey and all its coldkeys or skip the hotkey if the weight is not enough | ||
| // Then we just need to record the hotkey as checkpoint | ||
| // Drain the whole hotkey prefix once started. Weight is accounted when it | ||
| // still fits; overshoot is allowed so the cursor can advance past this hotkey. |
There was a problem hiding this comment.
[HIGH] Dissolution deliberately bypasses the block weight limit
Once the meter is exhausted, this loop deliberately continues scanning the complete hotkey prefix. A hotkey can have attacker-influenced stake entries across many coldkeys and subnets, so one on_idle invocation can perform unbounded reads and later writes beyond its remaining weight, potentially making the block non-viable. Persist a secondary cursor for the inner prefix and resume it in a later block instead of overshooting the meter.
|
🔄 AI review updated — Skeptic: VULNERABLE |
Co-authored-by: Cursor <cursoragent@cursor.com>
…ndices fix(runtime): preserve deprecated tempo call indices
Reject bool values other than 0/1, negative human rao/fixed128, and integers outside each parameter's bit width before composing the call. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
| .saturating_add(migrations::migrate_associated_evm_address_index::migrate_associated_evm_address_index::<T>()) | ||
| // Fold deprecated SubnetTaoProvided / SubnetAlphaInProvided residuals into the | ||
| // main AMM reserves (issue #2793). Guarded by HasMigrationRun, so it only runs once. | ||
| .saturating_add(migrations::migrate_cleanup_swap_v3::migrate_cleanup_swap_v3::<T>()) |
There was a problem hiding this comment.
[HIGH] Activated migrations perform unbounded upgrade work
This activates migrate_cleanup_swap_v3, which fully iterates two storage maps and then clears both prefixes in one runtime upgrade. It is immediately followed by migrate_clear_orphan_subnet_identities_v3, which collects and scans the entire identity map. Returning the accumulated weight after completing the work does not impose a bound; attacker-influenced storage cardinality can make the upgrade block exceed its execution budget. Convert these migrations to bounded, cursor-based work performed across blocks.
| // Handle one hotkey and all its coldkeys or skip the hotkey if the weight is not enough | ||
| // Then we just need to record the hotkey as checkpoint | ||
| // Drain the whole hotkey prefix once started. Weight is accounted when it | ||
| // still fits; overshoot is allowed so the cursor can advance past this hotkey. |
There was a problem hiding this comment.
[HIGH] Dissolution deliberately bypasses the block weight limit
Once a hotkey scan begins, this path continues reading its entire Alpha/AlphaV2 prefix after WeightMeter is exhausted; the settlement and cleanup paths similarly continue writes over budget. A hotkey can have attacker-influenced entries across subnets, so one on_idle invocation can exceed the block budget and stall block production. The inner scan needs its own resumable cursor instead of deliberately overshooting weight.
|
🔄 AI review updated — Skeptic: VULNERABLE |
Co-authored-by: Cursor <cursoragent@cursor.com>
| // Fold deprecated SubnetTaoProvided / SubnetAlphaInProvided residuals into the | ||
| // main AMM reserves (issue #2793). Guarded by HasMigrationRun, so it only runs once. | ||
| .saturating_add(migrations::migrate_cleanup_swap_v3::migrate_cleanup_swap_v3::<T>()) |
There was a problem hiding this comment.
[HIGH] Activated migrations perform unbounded upgrade work
These newly activated migrations traverse entire storage maps during on_runtime_upgrade: migrate_cleanup_swap_v3 iterates and removes both deprecated reserve maps, while the following identity migration scans every SubnetIdentitiesV3 key. Returning the measured weight only accounts for work after it has executed; it does not constrain execution to the block budget. Attacker-influenced map growth can therefore make the upgrade block exceed its weight or proof-size limits. Convert these migrations to bounded, resumable processing with persistent cursors before activation.
|
|
||
| // Handle one hotkey and all its coldkeys or skip the hotkey if the weight is not enough | ||
| // Then we just need to record the hotkey as checkpoint | ||
| // Drain the whole hotkey prefix once started. Weight is accounted when it | ||
| // still fits; overshoot is allowed so the cursor can advance past this hotkey. |
There was a problem hiding this comment.
[HIGH] Dissolution deliberately bypasses the block weight limit
The implementation intentionally drains an entire hotkey prefix after WeightMeter is exhausted. A hotkey can have attacker-influenced stake entries, so one on_idle invocation can perform arbitrarily more reads, writes, and transfers than its supplied limit. The same overshoot pattern also appears in the total-value and cleanup phases. Preserve a resumable inner cursor (hotkey, coldkey/storage key) instead, so every phase stops before exceeding the meter and continues next block.
|
🔄 AI review updated — Skeptic: VULNERABLE |
Summary
Three related pushes to make chain errors and subnet hyperparameters self-explanatory for users and agents:
Error descriptions (original scope)
bittensor/error_descriptions.py: a researched, per-name description for all 355 classified chain errors — what raised it and where to check, grounded in the pallet source.ChainError.description(andto_dict()) in the SDK, an extranote:in btcli failure diagnostics,btcli explain <Name>, the generated docs errors page, and/catalog/errors.json.codegen.check --namesgates completeness in both directions (undescribed / orphaned names fail CI).Full owner-settable hyperparameter coverage
SetHyperparameternow covers every owner-settable AdminUtils call: addsadjustment_alpha,rho,max_burn,max_difficulty,bonds_penalty,alpha_sigmoid_steepness,min_childkey_take,owner_immune_neuron_limit, andalpha_low/alpha_high(the two-valuesudo_set_alpha_valuescall: setting one side reads the current pair and preserves the other).burn_increase_mult: U64F64 fixed-point now encodes correctly ({"bits": …}) and renders as≈ 1.5with a human--value 1.5form (newfixed128kind).burn_half_life,yuma3_enabled, etc.) by querying their storage items, so the chart shows everythingsudo setcan change.Per-hyperparameter docs
codegen.check --units) shown as a dim description column in the CLI chart; names are OSC-8 hyperlinks and asee:footer links the docs.docs/hyperparameters/(researched against the pallet source) plus 7 playable React demos: burn-cost simulator, difficulty controller, rho/kappa sigmoid, weight clipping + commit-reveal timeline, liquid-alpha curve, UID pruning grid, owner-cut split.generate.py --checkfails if a parameter lacks its explainer page.Silent capping of locked alpha transfers removed
Test plan
codegen.check --names / --coverage / --unitsall greengenerate.py --checkgreen (docs and catalogs in sync, all 40 pages present)tsc --noEmitandnext buildclean (all docs paths prerender)explain, and intent build for the new names