Skip to content

Rotate a key

The rotatable v2 account holds two keys: an active key that signs everyday spends, and a backup key that exists to replace the active key when something goes wrong. Rotation is the flow that uses it.

Rotate when:

  • the device holding the active key is lost or destroyed,
  • the active key is compromised, or
  • you are doing routine hygiene and want fresh keys.

The account address does not change. Rotation replaces what the account believes, not where it lives.

The account stores the active public key behind a pointer contract (storage slot 0) and only keccak256(backupPk) for the backup (slot 1). A rotation:

  1. is authorised by a signature from the backup key over the sig-hash;
  2. carries the backup public key itself as a second witness, which the account checks against the stored hash;
  3. writes a new active key and a new backup commitment;
  4. deploys a fresh pointer contract for the new active key.

The critical property: the rotate path verifies the backup authorisation specifically. A compromised active key can spend your funds, but it cannot rotate the account and lock you out. Recovery authority and spending authority are genuinely separate.

You need: the account address, the backup seed, and two fresh seeds for the new active and new backup keys.

import { randomBytes } from 'node:crypto';
import { bytesToHex } from 'viem';
import {
buildRecoveryTransaction,
createMldsaSigner,
serializeFrameTransaction,
signFrameTransactionWitness,
} from 'viem-8141';
const newActive = createMldsaSigner({ seed: bytesToHex(randomBytes(32)) });
const newBackup = createMldsaSigner({ seed: bytesToHex(randomBytes(32)) });
const tx = buildRecoveryTransaction({
sender: account,
nonce: await client.getTransactionCount({ address: account }),
newActivePublicKey: newActive.publicKey,
newBackupPublicKeyHash: newBackup.publicKeyKeccak,
backupPublicKey: backup.publicKey, // proves the backup, checked against slot 1
});
// Signed by the BACKUP key, not the active key.
const signed = await signFrameTransactionWitness(tx, backup);
const hash = await client.sendFrameTransaction(serializeFrameTransaction(signed));

Two frames and two signature entries:

frames:
[0] VERIFY target: (self) flags: 3 gas: 30,000 data: 0x01
→ 0x01 selects the rotate path
[1] SENDER target: (self) gas: 520,000
→ data = newActivePk (1,952 B) ‖ keccak256(newBackupPk) (32 B)
signatures:
[0] {ARBITRARY, msg: empty, sig = ML-DSA-65 by the BACKUP key} 3,309 B
[1] {ARBITRARY, msg: empty, sig = the backup public key blob} 1,952 B

The second entry is not a signature at all — it is the backup public key, carried in the witness channel because it is too large to sit in calldata cheaply. It is elided from the sig-hash but authenticated against state: the account hashes it and compares against slot 1. Supplying the wrong key fails there.

Rotation is more expensive than a spend, mostly because it redeploys the pointer contract holding the new active key.

  • The old active key can no longer spend. Its signature will not verify against the new pointer.
  • The old backup key is consumed and can no longer authorise a rotation.
  • The account’s nonce advances by 2 (payment approval plus the pointer deployment).
  • The address is unchanged, and so is every balance and token held at it.

Verify the new keys work before discarding anything:

Terminal window
# slot 0 changed — a new pointer contract
curl -s https://rpc.frostfi.net -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"eth_getStorageAt",
"params":["0xACCOUNT","0x0","latest"]}'

Then send a small transfer signed by the new active key.

Rotation is the flow you will need on your worst day, when your phone is at the bottom of a river and you are working from a backup seed you wrote down months ago. That is the wrong moment to discover you transcribed a character wrong.

Do a full rotation drill on a funded testnet account now: derive, fund, first send, rotate with the backup, confirm the old active key is dead and the new one works. The reference implementation ships exactly this drill as an automated test, and it runs weekly against the network.

Fixed-key v1 accounts (code IDs 0x01 and 0x02) cannot rotate. Their public key is part of their immutable code. If you are holding value in one and want recovery, create a rotatable account and move the funds.

  • Account contracts — dispatch rules and storage layout.
  • Hardware custody — keeping the active key in a Secure Enclave, where losing the device is the expected case.