τGuide · Wallet security
Guide

Hardening a coldkey: hashed key, Vault, multisig, pure proxy

Move an existing coldkey into a keyless pure proxy controlled by a 2-of-3 multisig — a post-quantum hashed key, a Polkadot Vault phone, and an offline key — with a narrow zero-delay proxy for daily work.

By Jacob Steeves · November 2026 · btcli 11.3.2+

A note from me before the walkthrough. A single coldkey of mine was compromised recently. Nothing about the fix needed new chain features, only the pieces that already ship in btcli, put together carefully. This is exactly the setup I moved that key into, step by step, so you can do the same with yours. — Jacob Steeves

Every coldkey that lives as one keyfile is one leak away from a total loss: a laptop, an old backup, or a password manager is enough. Whoever holds the key can drain liquid stake, and if the key owns a subnet, they can announce a coldkey swap and take the subnet too. A transfer can't fix that, because a transfer moves TAO but not stake or subnet ownership. Only a coldkey swap moves everything.

This guide moves an existing coldkey (OLDKEY) into a structure where no single key owns anything:

flowchart TD
    OLD["OLDKEY<br/>old single coldkey"] -- "announce → ~5 days → swap" --> PURE
    subgraph MSIG ["MSIG — 2-of-3 multisig"]
      Q["QKEY<br/>hashed ML-DSA-65"]
      V["VAULT<br/>Polkadot Vault phone"]
      C["COLD<br/>offline / Ledger"]
    end
    MSIG -- "Any, delay 0" --> PURE["PURE<br/>keyless pure proxy<br/>balance · stake · subnet"]
    OPS["OPS<br/>everyday key"] -- "Owner, delay 0<br/>no transfers" --> PURE

What this buys you:

Caveat — quorum strength. A multisig is as strong as its weakest quorum. Here VAULT + COLD can still reach 2-of-3 without QKEY, so the group as a whole is not quantum-resistant until at least two members are post-quantum. btcli multisig show reports this as quantum_resistant_protection. The Vault integration signs standard Sr25519 and Ed25519 only, so a Vault phone can co-sign a mixed multisig but can't itself be a hashed member.

Prerequisites

Commands use names from btcli's local wallets, address book, multisig book, and proxy book. Every mutation supports --dry-run, so use it first.

If OLDKEY may be compromised, move fast. The attacker holds the same key and can announce a swap to their own destination. Step 10 covers the race.

Step 1 — install btcli

btcli ships inside the bittensor package, RaoFoundation's 11.x SDK with a Rust core. One install gives you both the CLI and the Python SDK. Requirements:

python3 --version                       # 3.10 – 3.14
python3 -m venv ~/.venvs/btcli
source ~/.venvs/btcli/bin/activate
python -m pip install --upgrade pip
python -m pip install "bittensor>=11.3.2"

On macOS, a Python from python.org or Homebrew works. On Debian and Ubuntu, if python3 -m venv complains, install the python3-venv package.

Verify the version, and check that this build knows hashed keys:

btcli --version                                         # 11.3.2 or later
btcli wallet new-coldkey --help | grep -E -- "--type|ML-DSA"

You should see --type ("standard … or hashed (rotating keys)") and ms (ML-DSA-65) under --crypto-type. If they're missing, you're on an older build.

Not the old CLI. Opentensor's legacy bittensor-cli 9.x package also installs a command called btcli. Installed into the same environment, it silently overwrites the entry point. Tell them apart by the output of btcli --version: this package prints a bare 11.x.y, while the legacy one prints BTCLI version: 9.x. pip show bittensor also lists Author: RaoFoundation. To fix a clash:

python -m pip uninstall -y bittensor-cli
python -m pip install --force-reinstall --no-deps "bittensor>=11.3.2"
btcli --version

Keep btcli in its own venv, and pin >=11.3.2, because older bittensor releases on PyPI under the same name are the pre-v11 SDK.

Vault signing needs nothing extra on the computer. --signer vault serves a local page on 127.0.0.1 that reads QR codes through your webcam. The page uses the browser's built-in QR detector and falls back to a small decoder loaded from esm.sh. All you need is a browser with camera access.

To upgrade later, watch Releases and run:

python -m pip install --upgrade "bittensor>=11.3.2"
btcli --version

Step 2 — check the chain is ready

The hashed-account feature has a network switch, AdminUtils.HashedAccountsEnabled. While it's false, QKEY can't register. btcli has no query subcommand for this flag, so read it through the SDK that comes with btcli:

python - <<'PY'
import bittensor as bt
st = bt.Subtensor(network="finney")
print("block", st.block)
print("HashedAccountsEnabled", st.query(("AdminUtils", "HashedAccountsEnabled"), []))
print("HashedAccountRegistrationDeposit (rao)", st.query(("HashedAccounts", "HashedAccountRegistrationDeposit"), []))
print("ColdkeySwapAnnouncementDelay", st.query(("SubtensorModule", "ColdkeySwapAnnouncementDelay"), []))
PY

You need HashedAccountsEnabled True. On finney it turned on at block 9,484,619 (Nov 11, 2026). At that point the deposit was 200,000,000 rao (τ0.2) and the swap delay was 36,000 blocks, about 5 days at 12 seconds per block. Plan with the live values, not these.

Step 3 — inventory the old key

A coldkey swap moves balance, stake, and subnet ownership. Proxies do not move. Record the old key's proxies now so you can re-create them on PURE, and confirm no swap is already pending:

btcli wallet inspect --coldkey OLDKEY
btcli query proxies --coldkey OLDKEY
btcli wallet swap-check -w OLDKEY

swap-check should report status none. If it shows an announcement you didn't make, go straight to Incident response.

Step 4 — create the hashed key (QKEY)

This creates an ML-DSA-65 coldkey in hashed (rotating) mode and registers it on chain. Use a clean machine and a new password that no other wallet or vault item shares.

btcli wallet new-coldkey -w QKEY --crypto-type ms --type hashed --n-words 24 \
  --registration-payer FUNDER

Key creation has no --dry-run. Instead, btcli quotes the live registration deposit and asks y/n before it generates anything, and no leaves your files untouched. On yes, it registers through MEV Shield and waits for finalization. Write the 24 words down on paper and label them hashed, because a hashed wallet must be restored in the same mode.

If registration couldn't be confirmed, retry without generating new keys. Keys that are already registered aren't charged again:

btcli wallet register -w QKEY --key coldkey --registration-payer FUNDER

btcli wallet show -w QKEY should show the hashed ML-DSA type and a finalized registration.

Step 5 — add VAULT and COLD to the address book

Neither key ever exists on your computer. Save their public addresses and tag each one with how it signs:

btcli addr add VAULT --vault                    # scan the address QR off the Vault phone
btcli addr add COLD <COLD_SS58> --signer ledger  # or vault / extension

When a later step names --signatory VAULT, btcli serves the transaction as a QR code. You scan it with Vault, check the decoded call on the phone, approve, and hold the signature QR up to the webcam. Only the unsigned transaction, the public address, and the signature cross the camera gap. The page is local, so to sign from elsewhere you would have to put it behind your own HTTPS tunnel. Plain localhost is fine for most people.

btcli addr list should show both entries with their signer tags. Check each address character by character against its device.

Step 6 — create the 2-of-3 multisig (MSIG)

btcli multisig add MSIG --threshold 2 \
  --signatory QKEY --signatory VAULT --signatory COLD
btcli multisig show MSIG

New groups default to the safe derivation, so the multisig address can't double as a classical key. Keep safe unless you're re-creating an existing legacy group.

multisig show prints multisig_address and all three members. Have every member derive it independently on their own device, and continue only if they all agree.

Step 7 — fund the signers and rehearse 2-of-3

Opening a multisig operation reserves τ0.196 from the member who opens it, and the deposit comes back when the operation executes. Fund QKEY for opening deposits and fees, and fund MSIG because it pays the pure proxy's deposit in Step 8:

btcli wallet transfer -w FUNDER --dest QKEY --amount 0.5 --dry-run
btcli wallet transfer -w FUNDER --dest QKEY --amount 0.5
btcli wallet transfer -w FUNDER --dest MSIG --amount 0.5

Now rehearse the full signing flow with a tiny transfer out of MSIG. QKEY opens it and VAULT completes it:

btcli wallet transfer -w MSIG --signatory QKEY --dest FUNDER --amount 0.01 --dry-run
btcli wallet transfer -w MSIG --signatory QKEY --dest FUNDER --amount 0.01
btcli multisig pending -w MSIG      # approval status + the exact co-signer command
# run the printed command for VAULT (it carries --signatory VAULT and the opening timepoint)

--signatory also takes several members in one run (--signatory "QKEY VAULT"), and btcli collects the approvals in order.

multisig pending should be empty again, with τ0.01 back on FUNDER. Repeat with COLD as the second signer. A break-glass path you've never tested isn't one you can count on.

Step 8 — spawn the pure proxy (PURE)

MSIG spawns a fresh keyless account and becomes its Any proxy with zero delay. That relationship is the only control path to PURE.

btcli proxy create -w MSIG --signatory QKEY --proxy-type Any --delay 0 --index 0 \
  --name PURE --dry-run
btcli proxy create -w MSIG --signatory QKEY --proxy-type Any --delay 0 --index 0 --name PURE
btcli multisig pending -w MSIG      # VAULT executes

The executing approval emits PureCreated, which carries the address plus the creation block height and extrinsic index. Record all three. btcli proxy kill needs them, and losing the spawner relationship strands the account. If --name didn't save the entry, add it by hand:

btcli proxy book add --name PURE --address <PURE_SS58> --spawner MSIG \
  --proxy-type Any --delay 0 --index 0 --height <H> --ext-index <I> --account-mode <legacy|safe>

Check the result. btcli query proxies --coldkey PURE should list MSIG as an Any delegate with delay 0, and btcli wallet inspect --coldkey PURE should show no stake. A swap destination must hold no stake or locks and must not be a hotkey.

Step 9 — prove MSIG controls PURE

Before anything valuable moves, send a few cents into PURE and back out through the full multisig → proxy path:

btcli wallet transfer -w FUNDER --dest PURE --amount 0.05
btcli wallet transfer -w MSIG --signatory QKEY --proxy-for PURE --dest FUNDER --amount 0.01 --dry-run
btcli wallet transfer -w MSIG --signatory QKEY --proxy-for PURE --dest FUNDER --amount 0.01
btcli multisig pending -w MSIG      # VAULT completes

If PURE's balance dropped by τ0.01, the multisig can act as PURE.

Step 10 — announce the coldkey swap

The announced coldkey swap is the only path that moves balance, stake, and subnet ownership together. The announcement commits to PURE's hash and starts the 36,000-block wait. It costs τ0.1, which is recycled, plus a fee of about τ0.027.

Fund just in time. Any TAO sitting on a compromised OLDKEY is TAO an attacker can spend announcing their destination first. Dry-run first, which also validates PURE as a destination before any fee. Then send the top-up and announce back to back:

btcli wallet announce-coldkey-swap -w OLDKEY --new-coldkey PURE --dry-run
btcli wallet transfer -w FUNDER --dest OLDKEY --amount 0.15
btcli wallet announce-coldkey-swap -w OLDKEY --new-coldkey PURE --mev-shield

--mev-shield encrypts the extrinsic, so it isn't readable in the mempool before inclusion. While the announcement is pending, the chain rejects every other signed call from OLDKEY except the swap-related ones, so the account is operationally locked for the full delay.

btcli wallet swap-check -w OLDKEY should show your announcement and the block at which it becomes executable.

Step 11 — wait, and watch

About 5 days. A delay only protects you if you watch it, so poll on a schedule much shorter than the delay:

watch -n 60 'btcli wallet swap-check -w OLDKEY --json'

The committed hash and execute block should never change. If the state flips to disputed, see Incident response.

Step 12 — execute the swap

Once the execute block has passed:

btcli wallet swap-coldkey -w OLDKEY --new-coldkey PURE --dry-run
btcli wallet swap-coldkey -w OLDKEY --new-coldkey PURE

Irreversible. The revealed key must hash to exactly what you announced. That also means an attacker who runs this step for you can only send everything to PURE.

btcli wallet inspect --coldkey PURE       # balance and stake now here
btcli wallet inspect --coldkey OLDKEY     # empty
btcli subnets show <NETUID>               # owner is PURE, if you own a subnet

Step 13 — add the OPS proxy

Create an everyday key and grant it Owner on PURE with zero delay. Owner covers subnet-owner admin calls such as owner-settable hyperparameters and identity. It cannot transfer funds, move stake, or rotate the owner. If you don't own a subnet, grant the narrowest type that fits the job instead, such as Staking (see Proxy accounts). Never give a daily or agent key Any, NonTransfer, or NonCritical on PURE.

btcli wallet new-coldkey -w OPS
btcli proxy add -w MSIG --signatory QKEY --proxy-for PURE \
  --delegate OPS --proxy-type Owner --delay 0 --dry-run
btcli proxy add -w MSIG --signatory QKEY --proxy-for PURE \
  --delegate OPS --proxy-type Owner --delay 0
btcli multisig pending -w MSIG      # VAULT completes
btcli wallet transfer -w FUNDER --dest OPS --amount 0.5   # fee pocket only

From here on, routine changes take one signature:

btcli tx set-hyperparameter --netuid <NETUID> --name <PARAM> --value <VALUE> \
  -w OPS --proxy-for PURE --force-proxy-type Owner --dry-run

--force-proxy-type Owner makes btcli refuse any broader delegation. A few registration toggles are root-only in the Owner filter, so flip those through the full multisig.

btcli query proxies --coldkey PURE should show exactly two delegates: MSIG (Any, 0) and OPS (Owner, 0).

Step 14 — re-add old proxies, retire OLDKEY

Re-create the delegations you recorded in Step 3 on PURE with btcli proxy add -w MSIG --proxy-for PURE …, each with the narrowest type that does the job. Then retire OLDKEY: remove it from scripts, signers, and keychains, and never fund or reuse it. Keep one offline record of:

Incident response

Checklist