Skip to main content
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. 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.
  • jq, used to translate the state file in step 4.
  • Cosmos-KMS installed and initialized with kms init. Remote signing tutorial covers installation, which needs Go 1.26 or later, make, and git.

1. Translate the config

Create kms.yaml and translate each block from your tmkms.toml: 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:
The keys block shown is the file backend. Its fields differ per backend, which step 2 covers. 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 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:
Point key_file at converted.key. The command needs Python 3 with the cryptography package (pip install cryptography).
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.

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. 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. Ledger-held consensus keys are not currently supported in Cosmos-KMS. If you need it, open a feature request so demand for it can be gauged.

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.
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.

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:
The jq expression makes two 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.
The redirection above overwrites whatever is at that path, so a re-run or a stale tmkms_state.json silently lowers the double-sign floor. Check the file does not already exist first.

5. Start Cosmos-KMS

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

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:
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.
  • 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.

Next steps