Skip to main content

Zero-Knowledge Proofs

Every shielded-pool operation carries a Groth16 proof over the BN254 curve. The wallet builds it in the browser from data that never leaves the device, and the runtime checks it natively against a verifying key stored on-chain.


Proof system: Groth16 over BN254​

PropertyWhat it means for Orbinum
Constant proof sizeEvery proof is exactly 128 bytes (arkworks compressed), whatever the circuit.
Constant verification costVerifying is one pairing check plus one scalar multiplication per public input. Constraint count does not matter.
Per-circuit setupEach circuit needs its own trusted setup. See Trusted Setup.
Mature toolingCircuits are written in Circom, witnesses come from snarkjs, and proving and verification use arkworks.

BN254 (also called alt_bn128) fits for three reasons:

  • Baby JubJub lives on its scalar field. Note owner keys are Baby JubJub points, so deriving and checking them inside a circuit is native field arithmetic. A secp256k1 key would cost orders of magnitude more constraints. See Keys & Identity.
  • The ecosystem supports it. Circom, snarkjs and arkworks all target BN254, and the Hermez Powers of Tau that the setup builds on was run over it.
  • Its security is adequate. BN254 gives about 100 bits of security.

Poseidon​

Poseidon is a hash function designed for arithmetic circuits. It costs a few hundred constraints per call, where SHA-256 costs tens of thousands. Every hash a circuit has to recompute is Poseidon:

UseArityFormula
Note commitment4commitment = Poseidon4(value, asset_id, owner_pk, blinding)
Nullifier2nullifier = Poseidon2(commitment, spending_key)
Merkle node2parent = Poseidon2(left, right)

What each field holds is described in Notes, and the tree in Merkle Forest.

All three sides compute the same function:

WhereImplementation
Circuitscircomlib Poseidon(n), wrapped as Poseidon2 and Poseidon4
Runtime (orbinum-zk-core)the PoseidonHasher trait with hash_2 and hash_4, plus the free function poseidon_hash_1 for a single input. LightPoseidonHasher runs in Wasm and NativePoseidonHasher calls a host function. The pool hashes Merkle nodes natively.
Clients (@orbinum/protocol)poseidon-lite

Circuits​

There are three circuits. Their signals and constraints are documented on their own pages.

CircuitIDActive versionProvesReference
Transfer13Two notes are spent and two notes are created, with value conserved and the fee paid.Transfer Circuit
Unshield23One note is spent into a public amount and a fee, with any change returned as a new note.Unshield Circuit
Shield31A new commitment opens to exactly the deposited amount and asset.Shield Circuit

Each circuit has its own verifying key, and each version of a circuit is registered separately. How a proof selects its key is covered in On-Chain Verification.


Proof flow​

  1. Gather the witness. The wallet collects the notes it spends, their Merkle paths (built locally from subtree roots), the spending key and the new notes' secrets. None of this leaves the device.
  2. Prove in the browser. The wallet computes the witness and the Groth16 proof using the circuit's proving key from @orbinum/circuits. First it checks that the chain supports the circuit version it is about to use, and that the chain's vk_hash for that version matches the prover's key. If either check fails, it stops before proving.
  3. Submit. A shield is a signed extrinsic paid by the depositor. A private transfer or an unshield is submitted unsigned. Each carries the proof, its public values, the encrypted memos and the circuit_version the proof was built under.
  4. Rebuild the statement on-chain. The pool runs its validity checks (see Spend Validity). It then builds the statement from the extrinsic's own arguments: roots, nullifiers, commitments, asset, fee, amount and recipient. It also recomputes memo_hash as the blake2_256 of the SCALE-encoded memos, reduced into the field. The prover cannot supply this value, so a proof is valid only for the exact memos it was built with.
  5. Verify natively. pallet-zk-verifier loads the prepared verifying key for that circuit version and encodes the statement in the key's input layout. It then calls the node's bn254_groth16_verify host function. Only a valid proof lets the pool record nullifiers and insert commitments.

For an end-to-end walk through shield, transfer and unshield, see Transaction Lifecycle. For input layouts and prepared keys, see the ZK Verifier Reference.


Trusted setup​

The proving and verifying keys come from a Groth16 trusted setup. Today that setup has a single contributor, and a multi-party ceremony is planned before mainnet. See Trusted Setup.