Skip to content

feat: add disassociate_hotkey extrinsic#2521

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

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

Conversation

@Rapiiidooo
Copy link

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
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']) = []


// Clean up AutoStakeDestination references.
// Other coldkeys may have set this hotkey as their auto-stake destination.
for netuid in Self::get_all_subnet_netuids() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the key not registered in any subnet, it can not be set as auto stake destination. Should we remove it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question!

You're right that a hotkey can't have auto-stake set if it's not registered on any subnet.

However, I believe there might be a case where stale entries could exist: when a hotkey gets deregistered via replace_neuron or when a subnet is dissolved via remove_network, AutoStakeDestination and AutoStakeDestinationColdkeys don't seem to be cleaned up, so orphaned entries could remain in storage from before the deregistration.

For example, a coldkey could set a registered hotkey as its auto-stake destination.
Later, that hotkey gets deregistered, but since replace_neuron and remove_network don't clean up auto-stake entries, they remain orphaned in storage.
The cleanup here is the only place (along with swap_hotkey and swap_coldkey) that catches those.

But happy to remove it if I'm missing something!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you, sometimes there are stale entries. You can see this PR #2513, we are working on such cases. But it is better to use process like migration to remove these storages.
I don't think the new extrinsic will be called frequently. It is fine to keep the extra check in it.
I will approve it after you fix the clippy in CI.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clippy issue was coming from the devnet-ready base being outdated. I've rebased on the latest devnet-ready and clippy passes clean locally. Could you re-trigger the CI?

Rapiiidooo and others added 3 commits March 20, 2026 16:08
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) <noreply@anthropic.com>
- 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) <noreply@anthropic.com>
…ociate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Rapiiidooo Rapiiidooo force-pushed the feat/disassociate-hotkey branch from e316985 to 2fbfb2b Compare March 20, 2026 15:19
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.

2 participants