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
| Property | What it means for Orbinum |
|---|---|
| Constant proof size | Every proof is exactly 128 bytes (arkworks compressed), whatever the circuit. |
| Constant verification cost | Verifying is one pairing check plus one scalar multiplication per public input. Constraint count does not matter. |
| Per-circuit setup | Each circuit needs its own trusted setup. See Trusted Setup. |
| Mature tooling | Circuits 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:
| Use | Arity | Formula |
|---|---|---|
| Note commitment | 4 | commitment = Poseidon4(value, asset_id, owner_pk, blinding) |
| Nullifier | 2 | nullifier = Poseidon2(commitment, spending_key) |
| Merkle node | 2 | parent = Poseidon2(left, right) |
What each field holds is described in Notes, and the tree in Merkle Forest.
All three sides compute the same function:
| Where | Implementation |
|---|---|
| Circuits | circomlib 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.
| Circuit | ID | Active version | Proves | Reference |
|---|---|---|---|---|
| Transfer | 1 | 3 | Two notes are spent and two notes are created, with value conserved and the fee paid. | Transfer Circuit |
| Unshield | 2 | 3 | One note is spent into a public amount and a fee, with any change returned as a new note. | Unshield Circuit |
| Shield | 3 | 1 | A 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
- 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.
- 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'svk_hashfor that version matches the prover's key. If either check fails, it stops before proving. - 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_versionthe proof was built under. - 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_hashas theblake2_256of 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. - Verify natively.
pallet-zk-verifierloads the prepared verifying key for that circuit version and encodes the statement in the key's input layout. It then calls the node'sbn254_groth16_verifyhost 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.
Related
- On-Chain Verification: why verifying keys live on-chain, and how a proof selects one
- Trusted Setup: where the keys come from
- ZK Verifier Reference: storage, extrinsics, input layouts, prepared keys
- Notes: commitments and nullifiers
- Merkle Forest: the trees a spend proves membership in
- Transaction Lifecycle: shield, transfer and unshield end to end