Skip to main content
This guide rotates a PoA validator’s consensus key using the enterprise/poa module. PoA rotation drops three of the staking rails: there is no fee, no rate limit, and no rotation history. PoA chains have no slashing or evidence handling to protect. The operator can rotate its own key, and the chain admin can rotate any validator’s key. For how rotation works in general, see Key rotation.
Key rotation can introduce security implications for your chain. Read the Key rotation overview in its entirety before proceeding. In particular, vote extensions are not supported on PoA chains, and custom logic that resolves a validator from a LastCommit address will not find a rotating validator for two heights after a rotation. See Staking and PoA chains.
Commands use simd. Substitute your chain’s binary. Examples use ~/.node for the validator node’s home, ~/.poa-newkey for the scratch home holding the new key, and val for the operator key name.

Prerequisites

  • A chain binary built with the poa module. Clone the cosmos-sdk repo and build it: cd enterprise/poa/simapp && go build -o /tmp/poa-simd ./simd, then use that binary as simd. The -o is required, since ./simd is the package directory. Check with simd tx poa --help, which must list rotate-cons-pub-key.
  • jq and curl. The commands extract the new public key with jq and watch the validator set with curl.
  • A running PoA validator you operate, with the chain’s binary on your PATH and a secondary machine or scratch directory for the new key. PoA validators are set in genesis under app_state.poa.
  • For an ML-DSA rotation, ml_dsa_65 is in the chain’s consensus params. See Enable ML-DSA keys.
  • The signer is the validator’s operator, or the chain admin. Any other sender is rejected.

1. Generate a new consensus key

Create the new key on a secondary machine or offline. Do not touch the live node’s priv_validator_key.json yet:
To rotate to a post-quantum key, add --consensus-key-algo ml_dsa_65 to the command above. See Migrate a validator to ML-DSA.
The important output is ~/.poa-newkey/config/priv_validator_key.json, which is the new consensus key. Confirm it and read its public key:

2. Submit the rotation

The command takes the new public key as base64 plus its type, and identifies the validator by operator address. Both derive from earlier steps:
To rotate to a post-quantum key, pass ml_dsa_65 instead of ed25519 in the command above, and add --gas auto --gas-adjustment 1.5 since the ML-DSA public key exceeds the default gas limit. See Migrate a validator to ML-DSA.
The operator address is a regular account address (cosmos1...), not a cosmosvaloper1... address, so read it with simd keys show val -a. Add standard transaction flags as your setup requires: --chain-id and --keyring-backend if your client config does not supply them, and --fees (or --gas-prices) to meet the chain’s minimum gas price. The transaction re-keys the validator’s state and migrates its accrued fees in the same block. Power, metadata, and the operator address are unchanged. To rotate as the admin instead, see Rotate as the admin.
If the rotated validator holds more than 1/3 of voting power, the chain halts during the cutover period (steps 3 and 4) and does not resume until the node with the rotated key starts signing.

3. Wait for the validator set to switch

The chain’s state swaps immediately, so simd q poa validators shows the new key in the same block the transaction lands. CometBFT applies the actual validator set change two blocks later, and only that switch governs when the node must sign with the new key. Until it happens, CometBFT still expects the old key, so keep the live node running untouched. Watch the CometBFT validator set until the new consensus key appears and the old one is gone:
The value should match the key you read in step 1.
Do not swap the node’s key before the set switches (wait at least 2 blocks). Swapping early makes the node sign with a key CometBFT does not yet expect, and the validator misses blocks.

4. Swap the node’s key

Once the new consensus address is in the set, stop the node, replace its key, and restart:
Confirm the node signs under the new consensus address after the restart. If you run a redundant standby, make a single switch. Keep the old-key node signing until the set updates, stop it fully, and only then let the new-key node start signing.
A validator with power 0 is outside the active set. Its rotation emits no validator set update, so there is no transition to time. Swap the node’s key first, then have the admin grant power.

Rotate as the admin

The admin override changes on-chain state only. Whoever runs the node must still swap priv_validator_key.json with the timing in steps 3 and 4 above. Otherwise the validator goes dark until its node signs with the new key. Coordinate the node-side swap with the operator before submitting, unless the goal is to cut off a compromised key.
The admin rotates any validator’s key with the same command, signed by the admin key. Generate a fresh key home for it:
Then rotate the key:

Verify

Confirm the module carries the new key:
The rotation also emits a rotate_cons_pubkey event with the operator address and the old and new consensus addresses. Read it from the transaction:

What can go wrong

  • The transaction is rejected as unauthorized: the signer is neither the validator’s operator nor the admin.
  • The transaction is rejected for the key itself: the new key equals the current one, is already used by another validator, or its type is not in the chain’s consensus params.
  • The validator misses blocks right after the swap: the node’s key was replaced before the set switched. Restore the old key, wait for the set, then swap again.
  • The validator goes dark after an admin rotation: the node still holds the old key. Swap priv_validator_key.json and restart.

Next steps