Skip to content

feat: add disassociate_hotkey extrinsic#2521

Open
Rapiiidooo wants to merge 9 commits intoopentensor:devnet-readyfrom
Rapiiidooo:feat/disassociate-hotkey
Open

feat: add disassociate_hotkey extrinsic#2521
Rapiiidooo wants to merge 9 commits intoopentensor:devnet-readyfrom
Rapiiidooo:feat/disassociate-hotkey

Conversation

@Rapiiidooo
Copy link
Copy Markdown

Description

Implements the reverse of try_associate_hotkey. The new disassociate_hotkey extrinsic allows a coldkey owner to remove the ownership link to a hotkey, provided the hotkey is not registered on any subnet and has no outstanding stake.

This was requested in #2519.

Related Issue(s)

Type of Change

  • New feature (non-breaking change which adds functionality)

Breaking Change

No breaking change. This is a purely additive extrinsic.

Checklist

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have run ./scripts/fix_rust.sh to ensure my code is formatted and linted correctly
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Implementation Details

New Extrinsic: disassociate_hotkey(origin, hotkey)

  • call_index: 133
  • Pays: Yes

Preconditions (all enforced with descriptive errors)

Check Error
Hotkey must exist HotKeyAccountNotExists
Caller must own the hotkey NonAssociatedColdKey
Hotkey must not be registered on any subnet HotkeyIsStillRegistered
Hotkey must have no outstanding stake (Alpha) HotkeyHasOutstandingStake

State cleaned up on success

  • Owner entry removed
  • Hotkey removed from OwnedHotkeys
  • Hotkey removed from StakingHotkeys
  • Delegates entry removed (if present)
  • HotkeyDisassociated event emitted

Tests (6 cases)

  1. Happy path — associate then disassociate, verify full cleanup
  2. Hotkey does not exist — returns HotKeyAccountNotExists
  3. Non-owner — returns NonAssociatedColdKey
  4. Still registered on subnet — returns HotkeyIsStillRegistered
  5. Outstanding stake — returns HotkeyHasOutstandingStake
  6. Reassociation — disassociate then re-associate with a different coldkey

Benchmark

Included for the new extrinsic.

Additional Notes

No runtime panics are possible in the implementation — all checks use ensure! macros and safe storage operations. The Alpha iterator check (iter_prefix().next().is_none()) short-circuits on the first entry, keeping the worst-case cost bounded.

@Rapiiidooo Rapiiidooo force-pushed the feat/disassociate-hotkey branch from f3f338d to f58ab36 Compare March 19, 2026 20:43
@Rapiiidooo
Copy link
Copy Markdown
Author

Rapiiidooo commented Mar 20, 2026

Tested also with local-dev-node :

Connecting to ws://127.0.0.1:9944...

== Associate hotkey ==
   coldkey (signer): Alice 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
   hotkey:           Bob   5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty

>>> SubtensorModule.try_associate_hotkey({'hotkey': '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'})
    signer: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
    [OK] block: 0x4a226d5e172db4b182c919773d422f474d881cc79499b7ab5f39d1fdfd7c2fc5
    event: {'phase': 'ApplyExtrinsic', 'extrinsic_idx': 6, 'event': {'event_index': '0508', 'module_id': 'Balances', 'event_id': 'Withdraw', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'amount': 255292}}, 'event_index': 5, 'module_id': 'Balances', 'event_id': 'Withdraw', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'amount': 255292}, 'topics': []}
    event: {'phase': 'ApplyExtrinsic', 'extrinsic_idx': 6, 'event': {'event_index': '0507', 'module_id': 'Balances', 'event_id': 'Deposit', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'amount': 0}}, 'event_index': 5, 'module_id': 'Balances', 'event_id': 'Deposit', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'amount': 0}, 'topics': []}
    event: {'phase': 'ApplyExtrinsic', 'extrinsic_idx': 6, 'event': {'event_index': '0507', 'module_id': 'Balances', 'event_id': 'Deposit', 'attributes': {'who': '5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n', 'amount': 255292}}, 'event_index': 5, 'module_id': 'Balances', 'event_id': 'Deposit', 'attributes': {'who': '5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n', 'amount': 255292}, 'topics': []}
    event: {'phase': 'ApplyExtrinsic', 'extrinsic_idx': 6, 'event': {'event_index': '0600', 'module_id': 'TransactionPayment', 'event_id': 'TransactionFeePaid', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'actual_fee': 255292, 'tip': 0}}, 'event_index': 6, 'module_id': 'TransactionPayment', 'event_id': 'TransactionFeePaid', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'actual_fee': 255292, 'tip': 0}, 'topics': []}
    event: {'phase': 'ApplyExtrinsic', 'extrinsic_idx': 6, 'event': {'event_index': '0000', 'module_id': 'System', 'event_id': 'ExtrinsicSuccess', 'attributes': {'dispatch_info': {'weight': {'ref_time': 510307000, 'proof_size': 0}, 'class': 'Normal', 'pays_fee': 'Yes'}}}, 'event_index': 0, 'module_id': 'System', 'event_id': 'ExtrinsicSuccess', 'attributes': {'dispatch_info': {'weight': {'ref_time': 510307000, 'proof_size': 0}, 'class': 'Normal', 'pays_fee': 'Yes'}}, 'topics': []}

<<< SubtensorModule.Owner(['5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty']) = 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY

<<< SubtensorModule.OwnedHotkeys(['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY']) = ['5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty']

== Disassociate hotkey ==
   coldkey (signer): Alice 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
   hotkey:           Bob   5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty

>>> SubtensorModule.disassociate_hotkey({'hotkey': '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'})
    signer: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
    [OK] block: 0x32d6c210834829812ab15c6125a7f6fa3e62a5fd9e941d98f8dc995df4e8741f
    event: {'phase': 'ApplyExtrinsic', 'extrinsic_idx': 6, 'event': {'event_index': '0508', 'module_id': 'Balances', 'event_id': 'Withdraw', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'amount': 606367}}, 'event_index': 5, 'module_id': 'Balances', 'event_id': 'Withdraw', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'amount': 606367}, 'topics': []}
    event: {'phase': 'ApplyExtrinsic', 'extrinsic_idx': 6, 'event': {'event_index': '077a', 'module_id': 'SubtensorModule', 'event_id': 'HotkeyDisassociated', 'attributes': {'coldkey': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'hotkey': '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'}}, 'event_index': 7, 'module_id': 'SubtensorModule', 'event_id': 'HotkeyDisassociated', 'attributes': {'coldkey': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'hotkey': '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'}, 'topics': []}
    event: {'phase': 'ApplyExtrinsic', 'extrinsic_idx': 6, 'event': {'event_index': '0507', 'module_id': 'Balances', 'event_id': 'Deposit', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'amount': 0}}, 'event_index': 5, 'module_id': 'Balances', 'event_id': 'Deposit', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'amount': 0}, 'topics': []}
    event: {'phase': 'ApplyExtrinsic', 'extrinsic_idx': 6, 'event': {'event_index': '0507', 'module_id': 'Balances', 'event_id': 'Deposit', 'attributes': {'who': '5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n', 'amount': 606367}}, 'event_index': 5, 'module_id': 'Balances', 'event_id': 'Deposit', 'attributes': {'who': '5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n', 'amount': 606367}, 'topics': []}
    event: {'phase': 'ApplyExtrinsic', 'extrinsic_idx': 6, 'event': {'event_index': '0600', 'module_id': 'TransactionPayment', 'event_id': 'TransactionFeePaid', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'actual_fee': 606367, 'tip': 0}}, 'event_index': 6, 'module_id': 'TransactionPayment', 'event_id': 'TransactionFeePaid', 'attributes': {'who': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 'actual_fee': 606367, 'tip': 0}, 'topics': []}
    event: {'phase': 'ApplyExtrinsic', 'extrinsic_idx': 6, 'event': {'event_index': '0000', 'module_id': 'System', 'event_id': 'ExtrinsicSuccess', 'attributes': {'dispatch_info': {'weight': {'ref_time': 1212457000, 'proof_size': 0}, 'class': 'Normal', 'pays_fee': 'Yes'}}}, 'event_index': 0, 'module_id': 'System', 'event_id': 'ExtrinsicSuccess', 'attributes': {'dispatch_info': {'weight': {'ref_time': 1212457000, 'proof_size': 0}, 'class': 'Normal', 'pays_fee': 'Yes'}}, 'topics': []}

<<< SubtensorModule.Owner(['5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty']) = 5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM

<<< SubtensorModule.OwnedHotkeys(['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY']) = []

Comment thread pallets/subtensor/src/staking/account.rs Outdated
Comment thread pallets/subtensor/src/staking/account.rs
Comment thread pallets/subtensor/src/staking/account.rs Outdated
@Rapiiidooo Rapiiidooo force-pushed the feat/disassociate-hotkey branch from e316985 to 2fbfb2b Compare March 20, 2026 15:19
@open-junius open-junius added the skip-cargo-audit This PR fails cargo audit but needs to be merged anyway label Mar 24, 2026
Comment thread pallets/subtensor/src/staking/account.rs Outdated
@open-junius
Copy link
Copy Markdown
Contributor

I think we can remove the storage added in create_account_if_non_existent function. For other data like AutoStakeDestination and AutoStakeDestinationColdkeys, I am not sure if we should remove them in disassociate method. will add team to review it.

Rapiiidooo and others added 6 commits April 21, 2026 17:07
Implements the reverse of try_associate_hotkey (closes opentensor#2519).

The new disassociate_hotkey extrinsic allows a coldkey owner to remove
the ownership link to a hotkey, provided the hotkey is not registered
on any subnet and has no outstanding stake.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Remove AutoStakeDestination entries for coldkeys pointing to this hotkey
- Remove AutoStakeDestinationColdkeys entries for this hotkey
- Increase weight estimate to account for subnet iteration
- Add test for auto-stake cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Clippy CI fails with clippy::expect_used denial on .expect() calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Use alpha_iter_single_prefix() which merges both legacy Alpha (U64F64)
and new AlphaV2 (SafeFloat) storage maps. During the lazy migration,
stake entries can exist in either map.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Ensures disassociation is blocked when stake exists in the new AlphaV2
storage map (SafeFloat format), not just the legacy Alpha map.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@Rapiiidooo Rapiiidooo force-pushed the feat/disassociate-hotkey branch from 0961609 to e231ddb Compare April 21, 2026 15:08
Rapiiidooo and others added 3 commits April 22, 2026 16:04
- account.rs: replace `alpha_iter_single_prefix` with direct
  `Alpha`/`AlphaV2` iter checks so the precondition short-circuits on
  the first entry instead of materializing both maps into a BTreeMap.
- dispatches.rs: expand the docstring to document side-effect cleanups
  (`Delegates`, `AutoStakeDestination*`) and the error variants. Bump
  the hand-tuned weight to cover the per-subnet auto-stake cleanup loop
  until `WeightInfo::disassociate_hotkey()` is regenerated from the
  benchmark.
- benchmarks.rs: populate `Delegates` and `AutoStakeDestination*` across
  several subnets/coldkeys so a future benchmark run reflects realistic
  worst-case storage churn.
- tests/staking2.rs: add tests for `Delegates` cleanup, multi-subnet
  auto-stake cleanup, and `HotkeyDisassociated` event emission.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- Fold the existence + ownership preconditions into a single
  `Owner::try_get(hotkey)` lookup. The previous pair of helpers
  (`hotkey_account_exists` + `coldkey_owns_hotkey`) re-read `Owner`
  three times to answer two questions; both errors are still raised
  distinctly for UX.
- Replace the `for netuid in get_all_subnet_netuids()` cleanup loop
  with `AutoStakeDestinationColdkeys::iter_prefix(hotkey)` so we only
  touch subnets where this hotkey was actually an auto-stake target.
  Common case (no auto-stake usage) drops from O(N_subnets) reads to
  zero; worst case is unchanged.
- Lower the hand-tuned weight to reflect the reduced worst-case
  storage footprint.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- format account.rs: collapse let owner declaration onto one line
- bump spec_version 397 -> 402 to clear the spec-version gate vs network
- regenerate weights for pallet_subtensor (adds disassociate_hotkey) and
  pallet_subtensor_proxy from the validate-benchmarks job patch

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
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