> ## Documentation Index
> Fetch the complete documentation index at: https://cosmos-docs-security-release.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from TMKMS

> Move a validator from TMKMS to Cosmos-KMS: translate the config, move or rotate the key, and cut over without double signing.

This guide moves a validator's signing from TMKMS to Cosmos-KMS, the recommended remote signer going forward. Cosmos-KMS includes several upgrades over TMKMS: it adds AWS KMS and PKCS#11 backends and post-quantum ML-DSA signing. The migration translates the config, gets the key into a Cosmos-KMS backend, and cuts over with exactly one signer alive at every moment. For what Cosmos-KMS is and how it relates to TMKMS, see [Cosmos-KMS and remote signing](/sdk/next/kms/remote-signing).

The validator node itself needs no changes: both signers speak the same privval protocol to the same `priv_validator_laddr` listener. TMKMS connects over CometBFT's SecretConnection, and the Cosmos-KMS `tcp://` transport is the same, so the listener works unchanged.

## Prerequisites

* A validator currently signing through TMKMS, with access to its `tmkms.toml` and state file.
* Cosmos-KMS installed and initialized with `kms init`. [Remote signing tutorial](/sdk/next/kms/tutorial-file-backend) covers installation, which needs [Go](https://go.dev/doc/install) 1.26 or later, [make](https://www.gnu.org/software/make/), and [git](https://git-scm.com/).

## 1. Translate the config

Create `kms.yaml` and translate each block from your `tmkms.toml`:

| tmkms.toml                         | kms.yaml                        | Notes                                                                                                                                                                   |
| ---------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `[[chain]]` `id`                   | `chains[].id`                   | Same value.                                                                                                                                                             |
| `[[chain]]` `state_file`           | `chains[].state_file`           | See [step 4](#4-translate-the-double-sign-state) before reusing a path.                                                                                                 |
| `[[chain]]` `key_format`           | none                            | Not needed; Cosmos-KMS has no per-chain serialization config.                                                                                                           |
| `[[validator]]` `addr`             | `validators[].addr`             | Drop the `<node-id>@` prefix; `tcp://host:port` uses CometBFT's SecretConnection, matching TMKMS. For a node running libp2p, use `noise://<peer-id>@host:port` instead. |
| `[[validator]]` `chain_id`         | `validators[].chain_id`         | Same value.                                                                                                                                                             |
| `[[validator]]` `secret_key`       | `validators[].identity_key`     | Different format; use the `identity.json` from `kms init` rather than converting.                                                                                       |
| `[[validator]]` `protocol_version` | none                            | Not needed.                                                                                                                                                             |
| `[[providers.softsign]]`           | `keys[]` with `backend: file`   | See [step 2](#from-softsign).                                                                                                                                           |
| `[[providers.yubihsm]]`            | `keys[]` with `backend: pkcs11` | PKCS#11 HSM; see [step 2](#from-yubihsm-or-another-hsm).                                                                                                                |
| `[[providers.ledgertm]]`           | none                            | Not yet supported in Cosmos-KMS; see [step 2](#from-yubihsm-or-another-hsm).                                                                                            |

A standard cosmos-sdk node has no peer ID on its `priv_validator_laddr` listener, so there is usually no `<node-id>@` prefix to carry over, and both TMKMS and Cosmos-KMS use a bare `tcp://host:port`.

Below is a complete `kms.yaml` example for a single validator on the file backend:

```yaml theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
chains:
  - id: my-chain-1
    state_file: state/my-chain-1.json
validators:
  - chain_id: my-chain-1
    addr: tcp://127.0.0.1:26659
    identity_key: identity.json
keys:
  - chain_ids: [my-chain-1]
    backend: file
    algorithm: ed25519
    key_file: priv_validator_key.json
```

The `keys` block shown is the file backend; it differs per backend, which step 2 fills in. It also needs `keys[].algorithm`, which has no `tmkms.toml` equivalent, so set it explicitly (`ed25519` for a softsign key), as the example shows. The [configuration reference](/sdk/next/kms/configuration-reference) lists every field.

## 2. Move the consensus key into a backend

Get the consensus key into the backend named in your `keys` block. The path depends on where TMKMS holds it today.

### From softsign

The softsign backend keeps the key in a file, but the format differs from what the Cosmos-KMS file backend reads. A TMKMS softsign key is the base64-encoded 32-byte ed25519 seed, while the file backend expects a CometBFT `priv_validator_key.json` or the base64-encoded 64-byte ed25519 key. Pointing `key_file` at a softsign key directly fails with `expected 64-byte ed25519 key, got 32`.

If you still have the validator's original `priv_validator_key.json` (TMKMS softsign was imported from it), point `key_file` at that file directly. No conversion is needed.

From only the softsign key (the file named by the `path` in your `[[providers.softsign]]` block), expand the 32-byte seed into the 64-byte `seed||pubkey` form the file backend accepts. Replace `tmkms_softsign.key` in the command with that file:

```shell theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
python3 - tmkms_softsign.key converted.key <<'PY'
import base64, sys
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
seed = base64.b64decode(open(sys.argv[1]).read().strip())
assert len(seed) == 32, f"expected 32-byte seed, got {len(seed)}"
sk  = Ed25519PrivateKey.from_private_bytes(seed)
pub = sk.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
open(sys.argv[2], "w").write(base64.b64encode(seed + pub).decode())
PY
```

Point `key_file` at `converted.key`. The command needs Python 3 with the `cryptography` package (`pip install cryptography`).

<Warning>`converted.key` and the exported softsign key are unencrypted private keys. Shred them after import (`shred -u` or `rm -P`) and keep them out of shell history and backups.</Warning>

### From YubiHSM or another HSM

Most HSM-held keys do not move. A YubiHSM, a Fortanix device, or any HSM that exposes the key over PKCS#11 works with the Cosmos-KMS `pkcs11` backend directly: wire up a `keys` block against the same module and keep signing with the same key. Keys generated inside an HSM are typically non-exportable, which is the point of an HSM, so this no-move path is the normal case. For the PKCS#11 config, see [Configure a signing backend](/sdk/next/kms/configure-backend).

When you are changing custodian rather than keeping the key in place, migrate by rotation: generate a new key in the target custodian and rotate the validator to it on chain, which retires the TMKMS-held key entirely. See [Rotate a consensus key, Staking](/sdk/next/keys/rotate-validator-key).

Ledger-held consensus keys are not currently supported in Cosmos-KMS. If you need it, open a [feature request](https://github.com/cosmos/kms/issues) so it can be evaluated by demand.

## 3. Stop TMKMS

Stop TMKMS and confirm the process is gone. The validator misses blocks until Cosmos-KMS takes over. That gap is expected: missed blocks are recoverable, a double sign from two live signers is not. Keep it short to avoid downtime jailing.

<Danger>Never run TMKMS and Cosmos-KMS at the same time against the same validator key. Each keeps its own last-signed state, so together they can sign the same height, which is a double sign. Stop one fully before starting the other, in both directions, including any rollback.</Danger>

## 4. Translate the double-sign state

TMKMS and Cosmos-KMS both track the last signed height, round, and step per chain, and the protection only works if the new signer starts at or above the old signer's high-water mark. The two store this state differently, so it needs translating.

Point the following command at the TMKMS state file named by `state_file` in your `[[chain]]` block (`tmkms_state.json` is a placeholder). It writes the translated state into the Cosmos-KMS location:

```shell theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
mkdir -p <home>/state
jq '{height, round: (.round|tonumber), step: (.step + 1)}' tmkms_state.json > <home>/state/<chain-id>.json
```

The `jq` makes two needed conversions: `round` from string to number (the source of the `int32` error otherwise), and `step + 1` to remap TMKMS's 0/1/2 signing steps to CometBFT's 1/2/3.

## 5. Start Cosmos-KMS

Start Cosmos-KMS with the completed `kms.yaml`. It dials the validator and resumes signing.

```shell theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
kms start --home <home>
```

## 6. Confirm signing resumed

Confirm the chain is producing again and the signer's state file is advancing. Run the status check twice; a climbing height means signing resumed:

```shell theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
curl -s localhost:26657/status | jq '.result.sync_info.latest_block_height'
```

The signer's `<home>/state/<chain-id>.json` should also advance past the last height TMKMS signed.

## What can go wrong

* Both signers briefly alive: the double-sign risk above. Cut over with the old process confirmed dead, not just signaled.
* The validator stays dark after cutover: the new signer is not reaching the listener. Check the `validators[].addr` translation and the firewall between the hosts.
* `dial tcp: lookup node-id@host: no such host`: the `<node-id>@` prefix was left in `validators[].addr`. Drop it and keep only host and port.
* `file: parse key file "<path>": expected 64-byte ed25519 key, got 32`: a TMKMS softsign key was pointed at directly. See the [key section](#from-softsign).
* `chain "<chain-id>": reload sign-state: json: cannot unmarshal string into Go value of type int32`: the TMKMS state file was reused as is. Translate it first; see [step 4](#4-translate-the-double-sign-state).

## Next steps

* Harden the new setup. See [Remote signing best practices](/sdk/next/kms/best-practices).
* Full field reference for the translated config. See the [configuration reference](/sdk/next/kms/configuration-reference).
