Skip to main content

On-Chain ZK Verification

Each circuit has a verifying key (VK), the public half of its trusted setup that a proof is checked against. Orbinum keeps every VK in the state of pallet-zk-verifier rather than compiling it into the runtime. Each key is versioned, controlled by governance and readable by anyone.


Why verifying keys live on-chain​

PropertyHow it holds
Governance-controlledEvery call that registers, activates, retires, denies or removes a key requires the Root origin. No account can change what verifies on its own.
VersionedKeys are stored per (circuit_id, version). A registered version is never overwritten, a removed version can only take the same key again, and a new version sits beside the old ones. When a key is rotated, only a new public-input layout needs a runtime upgrade.
AuditableThe registry is plain storage, also served by the zkVerifier_* RPCs. Each version exposes a vk_hash that anyone can compare with the published artifacts. Every registration, activation, retirement, denial and removal emits an event.

There is no event per proof. Verifications made for the shielded pool emit nothing from pallet-zk-verifier. The version that checked a spend or shield is the circuit_version argument of the extrinsic itself, and the registry at that block shows which key that version held.


How a proof selects its key​

A proof is checked against exactly one key, identified by two values:

ValueWhere it comes from
Circuit idThe call: shield uses circuit 3, private_transfer circuit 1 and unshield circuit 2.
VersionThe circuit_version argument, chosen by whoever builds the proof.

The version must be supported, meaning registered, not retired and holding a key that is not denied. Otherwise the call fails with UnsupportedCircuitVersion. For a private transfer or an unshield, the transaction pool refuses it before any other check.

The key then decides how the statement is encoded. Its arity (how many public inputs it takes) selects one of the layouts the circuit admits. A caller cannot claim one shape while the key describes another.

Each circuit also has one active version. Clients build new shield proofs under it, and the standalone verify_proof extrinsic checks against it. Spends do not have to use it: any supported version verifies. The ZK Verifier Reference covers the exact rules and the input order of each layout.


Prepared keys and native verification​

When a key is registered, the pallet validates it and stores a prepared form beside it: the pairing inputs that depend only on the key. A proof is verified from that prepared form, which skips about half the work. The pairing itself runs natively in the node through the bn254_groth16_verify host function, at the same weight as in Wasm and with the same results. Every node must run version 0.4.0 or later, which provides the function, to import blocks. For details, see Prepared Keys.


Rotating a key never invalidates a note​

A note exists as a commitment in the Merkle forest. Rotating a key changes the registry and nothing else. The forest, the commitments in it and the nullifier set stay exactly as they were.

Every version of a circuit accepts the same notes, with the same commitment and the same nullifier (the rule is stated in the ZK Verifier Reference). So a note can be spent under whichever version is currently supported. The version recorded in a note's memo is informational: the app proves every spend under the spend circuit version it is built for, never under the note's own.

What a rotation affectsWhat it does not affect
New proofs are built under the new versionExisting commitments and their leaf positions
A proof built under a retired version and not yet included is rejected, and the wallet builds a new oneNullifiers already spent and notes still unspent
A retired key stops verifyingThe ability to spend any existing note

Retiring an old version therefore never leaves funds stranded, and no note has to be migrated. The governance steps are in Verification Key Rotation.

Retire a superseded version promptly

The submitter picks the version, so any version still supported can be used. That includes using it to resubmit a copy of a pending spend, or to exploit the weakness the new version fixes. Activating a new version does not stop the old one from verifying. Only retire_version does, or deny_version for an unsound key, which keeps it from ever verifying again.