Infrastructure
Cosmos Wrote a Warning Where a Check Should Have Gone
The Cosmos SDK's own documentation states plainly that removing a validator key type still in use will halt the chain. The message that does it is accepted without complaint. A patch filed on Tuesday night proposes to close the gap — and post-quantum key migration is what makes anyone care now.
Somewhere in the Cosmos SDK documentation, on the page that explains how to turn on post-quantum validator keys, there is a sentence that does the work a line of code should be doing:
"Do not remove a key type while validators still use it. Existing validators are not re-checked when a type leaves the list, but every later voting-power update for such a validator fails validation, and a failed validator update halts the chain."
That is not a bug report. It is the SDK's published guidance, and it is accurate in every particular. What it describes is a governance transaction that any Cosmos chain can pass today, that will succeed, that will emit a success event, and that will stop the network at an unpredictable point in the future. On Tuesday night a contributor going by Bruce039 filed issue #26837 against cosmos-sdk proposing that the software enforce its own warning, and opened a pull request thirty-nine seconds later. This desk verified the mechanism against the shipped code rather than the report.
Three functions, none of which look at the validators
Consensus parameters on a Cosmos chain are changed with MsgUpdateParams, handled by the x/consensus keeper. The handler is short. It checks that the sender holds the governance authority, converts the message into a consensus-params structure, merges it over the stored parameters, and then runs two validations: ValidateBasic on the merged result and ValidateUpdate against the current ones. If both pass, it writes the new parameters to the store and emits an event.
Neither validation has any notion of a validator set. They check that parameters are internally coherent — block limits are sane, evidence windows make sense, values that may not change have not changed. A list of permitted consensus key types containing only secp256k1 is a perfectly coherent list. Nothing in the module's world knows that ninety validators are currently signing with ed25519.
The enforcement happens one layer down and much later. CometBFT's block executor runs validateValidatorUpdates over every validator change the application returns at the end of a block. For each update carrying voting power above zero, it decodes the public key and tests it against the permitted list. If the key type is not on it, the function returns an error worded exactly like this: validator … is using pubkey ed25519, which is unsupported for consensus.
That error does not reject one validator. It fails FinalizeBlock, and it fails it identically on every node, because every node is running the same deterministic state machine over the same block. The chain stops.
The trigger is ordinary, which is the problem
Updates with power above zero are not rare events. A validator's power changes when somebody delegates to it, when somebody undelegates, when it re-bonds after unbonding, when it is unjailed. Any of these, from any user, at any time.
So the sequence is: governance passes a parameter change, the chain keeps producing blocks normally, and then at some later point — an hour later, a week later — a delegator stakes a few tokens to a validator that never rotated its key, and the network halts on a transaction that had nothing wrong with it. The party who triggers the halt is not the party who caused it, and the gap between cause and effect can be arbitrarily long.
Why this became live in 2026
A hazard that requires governance to remove a key type nobody is using is mostly theoretical. What changed is that Cosmos chains now have a concrete reason to remove one.
The SDK added ML-DSA-65 — the FIPS 204 lattice signature scheme — as a validator consensus key type, alongside key rotation through MsgRotateConsPubKey. The documented migration runs in one direction: a chain adds ml_dsa_65 to the permitted list first, because, as the migration guide puts it, if the list "does not include ml_dsa_65, the rotation is rejected." Validators then rotate one by one, and a migrated validator reports cometbft/PubKeyMlDsa65 where it used to report tendermint/PubKeyEd25519.
A chain running that migration ends up with both types permitted and a set that is gradually emptying of the old one. The obvious tidy-up at the end — and the step that actually makes the chain's consensus post-quantum rather than merely capable of it — is to drop ed25519 from the list. It is worth being precise here: the SDK's guides do not instruct anyone to do that. They instruct the opposite, telling operators to "keep ed25519 in the list unless every genesis validator starts on an ML-DSA key," and they carry the warning quoted at the top of this piece. The inference that a migration invites the removal is the filer's, and it is a reasonable one. It is also the kind of inference an operator makes at the end of a long migration, at the point where the remaining validators are assumed to be done.
Assumed is the operative word, and the patch is blunt about why. Its validator check deliberately walks unbonded validators too, on the grounds that an unbonded validator can bond again — so a validator that has been dark for months, and would not appear in any active-set roll call an operator ran before the vote, is still enough to stop the chain when it comes back.
Eight years of one-sided protection
The SDK already defends the other direction, and has since almost the beginning. A validator cannot be created with an unsupported key type, and cannot rotate into one; x/staking rejects both. That guard exists because of issue #2945, titled "Invalid validator pubKey halts state machine," filed on November 29, 2018 and closed the same day. The report read, in part: "We can however still create validators with invalid key types which halts the chain."
It is the same halt. The 2018 fix closed the path where a validator walks into a list that does not allow it. Nearly eight years on, the path where the list walks away from the validator is still open, guarded by a paragraph of documentation.
PR #26838 adds 185 lines across nine files and removes none. It defines a checker interface in x/consensus, implemented by x/staking, which iterates the validators and reuses the same key-type validation the staking module already applies elsewhere; the error names the offending validator rather than leaving an operator to find it. The check runs only when the permitted-types list actually changes, so ordinary parameter updates are untouched, and a chain that does not wire the checker behaves exactly as before. The author reports that a test sending the message through the message router passes on the patch and that the offending update "goes through" on main.
As of publication the issue and the pull request are both open, carry no labels, and have no response from a cosmos-sdk maintainer.
The Take
There is a particular kind of defect that is worse for being known, and this is one. Nobody has to be told about this hazard; the project wrote it down, clearly, in the place an operator would be reading right before they did the dangerous thing. That is genuinely good documentation. It is also an admission, in prose, that the state machine will accept an instruction it knows will kill the chain — and prose does not run in FinalizeBlock. The right instinct, when you find yourself writing "do not do X" in a guide, is to ask why X is reachable. Chains are about to run these migrations for real, because the post-quantum clock is the one deadline in this industry that no governance vote can extend, and they will run them with validator sets containing stragglers, dormant operators, and one node whose keys are on a laptop somebody left at a previous job. The patch is 185 lines and deletes nothing. The cost of merging it is an afternoon of review. The cost of not merging it is paid by whichever chain finishes its migration first and tidies up.