Skip to main content

Verification Key Rotation

Rotating a key means registering a new verifying key as a new version of a circuit, moving new proofs onto it and retiring the old version. Notes stay spendable throughout. This page is the runbook for that sequence and a record of what the registry holds now.

For why versioning is safe, see On-Chain Verification. For every call and storage item, see the ZK Verifier Reference.


When a key is rotated​

SituationOutcome
The circuit gains or tightens a constraint (for example a canonical-key check, memo binding, or one root per input)New version of the same circuit id
The same circuit gets a new trusted setup (for example the multi-party ceremony)New version: a different key is a different version
The change alters what a note is (its commitment or its nullifier)Not a rotation. It needs a new circuit id
The operation itself goes awayNot a rotation. See Removing a circuit
What a new version may change

The submitter picks the version a spend is verified under. So every version of a circuit must accept the same notes, under rules at least as strict. The full rule is in Version resolution.


The sequence​

Every key-management call requires Root. On the testnet, Root is reached through sudo.

1. Publish the artifacts​

The circuits package publishes the new version's witness generator, proving key, verifying key and manifest.json, which records each version's vk_hash.

2. Upgrade the runtime, only if needed​

The pallet admits a fixed set of layouts per circuit (see Public input layouts). A key with an arity the runtime does not admit is refused. A rotation that introduces a new layout therefore needs a runtime upgrade first. A new setup or a constraint change that keeps the layout does not.

3. Register and activate​

batch_register_verification_keys(entries) takes up to 10 entries and applies them all or not at all, so a rotation of several circuits cannot land half-registered. Set set_active: true on each new entry.

Registration refuses:

  • a denied key (DeniedVerificationKey)
  • a key that does not deserialize as a BN254 Groth16 key, or whose delta shows phase 2 never ran (InvalidVerificationKey)
  • a key whose arity gives no admitted layout, including any base-layout (memo-less) spend key
  • a version that already exists (CircuitAlreadyExists)
  • a removed version with a key other than the one it held (VersionReused)
  • a 65th version of one circuit (TooManyVersions)

The key's prepared form is stored with it, so the first proof under the new version costs the same as any other.

4. Check the hash​

curl -s -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"zkVerifier_getAllCircuitVersions","params":[]}' \
https://rpc-1.testnet.orbinum.io

Each vk_hash must equal the manifest's value for that version. A mismatch is cheapest to catch here. Afterwards, every proof built from the published artifacts would fail on-chain after the user has spent time proving it.

5. Release the clients​

Clients refuse to prove unless the chain supports their version and the chain's vk_hash matches their artifacts.

CircuitClient behaviourWhat a rotation needs
ShieldProves under the chain's active versionNothing beyond the published artifacts
Transfer, unshieldProve under the spend version the client is built forA client release that ships alongside activation

6. Retire the old version​

retire_version(circuit_id, old_version) stops proofs under that version from verifying. Run it as soon as clients prove under the new version. While the old version is supported, anyone can use it, including to resubmit a copy of a pending spend or to exploit what the new version fixes.

Prefer retire_version over remove_verification_key. Retiring keeps the key data for auditing, and unretire_version can reverse it (the key is re-checked against the registration rules). Removal deletes the key data, and the version can then only take the same key again. Neither call can touch the active version.

When the old version is unsound, run deny_version(circuit_id, old_version) instead. It retires the version and records its key's hash in DeniedVkHashes, so no later Root call can unretire it or register the key under another version. No call undoes it. See Denied keys.


What happens to existing notes​

Nothing. Commitments, leaf positions and the nullifier set are untouched, and every version accepts the same notes. So any unspent note is spent under the new version with no migration step.

The one casualty is a proof built under the retired version but not yet included. It is refused: a transfer or unshield at pool admission (invalid-transaction code 10), a shield at dispatch with UnsupportedCircuitVersion. The wallet builds a new proof. See Spend Validity.


Removing a circuit​

purge_circuit(circuit_id) erases every trace of a circuit. That covers every version in VerificationKeys, PreparedKeys, VkHashes, VerificationStats and RetiredVersions, plus the active pointer. The CircuitPurged { circuit_id, removed } event counts entries, not versions. DeniedVkHashes and RemovedVkHashes are kept.

The guard is that the runtime must not know the circuit id. While transfer (1), unshield (2) and shield (3) are compiled in, purge_circuit refuses them with CircuitStillInUse, whatever storage holds. It refuses an id with nothing stored with CircuitHasNoStorage.

So removing a circuit takes two steps:

  1. A runtime upgrade drops the id from the runtime's known circuits.
  2. purge_circuit clears the storage left behind.

This call is the only way to clear a circuit's last version. retire_version and remove_verification_key both refuse the active version, so a live circuit always keeps a key to verify against.


Current on-chain state​

Testnet runtime: spec_version 20, transaction_version 7.

CircuitActiveSupportedRetired and deniedLayoutvk_hash (active)
1 — transfer3[3]1, 2Cross-tree, 9 inputs0x04e05d74b320601fe1630ffa5b07c617117340c71d9f8d7b639a479f621a769a
2 — unshield3[3]1, 2Memo-bound, 8 inputs0x24fb6a97b53300b2cf2effa83547ac5a913c0b83f99788dd428ce54498f4e9f8
3 — shield1[1]—Base, 3 inputs0x4835d34f3bb5972c13c2995249bcb0dd8b82b01841d7432b92c96a810f435cb7

No other circuit id holds keys. Only the three active versions verify. Transfer and unshield v1 and v2 let spending-key aliases give one note several nullifiers, so their four keys are in DeniedVkHashes. These keys are the ones published in @orbinum/circuits 0.17.1. To read the live registry, run the RPC call in step 4.