Skip to main content

Zero-Knowledge Proofs

Orbinum uses Groth16 zero-knowledge proofs over the BN254 curve to let users prove ownership, transaction validity, and identity claims without revealing private data. Proofs are generated client-side, submitted on-chain, and verified in ~3ms by pallet-zk-verifier.

This page covers the cryptographic foundations: proof system, hash function, circuit designs, and the end-to-end proof flow.


Proof System: Groth16 over BN254

Why Groth16?

Constant proof size — Always 192 bytes, no matter how complex your transaction
Lightning-fast verification — Only ~3ms on-chain using 3 pairing operations
Battle-tested tooling — Used by Zcash, Tornado Cash, and many DeFi protocols
EVM compatibility — Native Ethereum precompiles for BN254 pairing (EIP-196, EIP-197)

Why BN254 Curve?

The BN254 (also called alt_bn128) is an elliptic curve specifically designed for efficient pairing operations:

254-bit prime field — Large enough for security, small enough for speed
~100 bits of security — Industry-standard security level
Efficient pairings — Critical for fast proof verification
Wide ecosystem support — Ethereum, Substrate, and most ZK frameworks

Cryptographic Primitives

Poseidon Hash Function

Poseidon is the primary hash function used by all Orbinum circuits. It is specifically designed for ZK arithmetic: where SHA-256 costs ~25,000 R1CS constraints, Poseidon costs ~300 — an 80× reduction in proving time.

// From primitives/zk-core
pub fn poseidon_hash_2(left: [u8; 32], right: [u8; 32]) -> [u8; 32]
pub fn poseidon_hash_4(inputs: [[u8; 32]; 4]) -> [u8; 32]

Constraint cost comparison

Hash FunctionR1CS ConstraintsZK-FriendlyStandardized
Poseidon~300✅ Yes✅ Yes
SHA-256~25,000❌ No✅ Yes
Pedersen~750✅ Yes⚠️ No

How We Use Poseidon

1. Note Commitments

When you create a private note, we hash all its details into a single commitment:

commitment = Poseidon(value, asset_id, owner_pubkey, blinding)

This commitment goes into the Merkle tree. It's public but reveals nothing about the note's content.

2. Nullifiers

When you spend a note, we compute a nullifier to prevent double-spending:

nullifier = Poseidon(commitment, spending_key)

Without your spending key, no one can link the nullifier back to the original commitment.

3. Merkle Tree Hashing

The Merkle tree uses Poseidon to combine leaves:

parent = Poseidon(left_child, right_child)

This allows efficient proof of inclusion with only ~20 hash operations for 1 million notes.


Circuits

Each circuit defines the constraints that constitute a valid operation. The Groth16 prover runs these constraints offline; the on-chain verifier checks the resulting proof in ~3ms regardless of constraint count.

The Transfer Circuit

When you make a private transfer, the circuit checks these constraints:

Private Inputs (Your Secrets)

• Input note values: value_a, value_b
• Input blinding factors: blinding_a, blinding_b
• Merkle proofs: path_a, path_b (proving notes exist)
• Your spending key: sk
• Output values and blinding: value_c, value_d, blinding_c, blinding_d

Public Inputs (Visible to Everyone)

• Nullifiers: nullifier_a, nullifier_b (prevent double-spending)
• Output commitments: commitment_c, commitment_d
• Merkle root: root (snapshot of tree state)

The 4 Critical Constraints

1
Input notes exist in the Merkle tree

merkle_verify(commitment_a, path_a, root) == 1 merkle_verify(commitment_b, path_b, root) == 1

2
Nullifiers are computed correctly

nullifier_a == poseidon(commitment_a, sk) nullifier_b == poseidon(commitment_b, sk)

3
Value is conserved (no money created or destroyed)

value_a + value_b == value_c + value_d

4
Output commitments are valid

commitment_c == poseidon(value_c, asset_id, owner_pk_c, blinding_c) commitment_d == poseidon(value_d, asset_id, owner_pk_d, blinding_d)


When an account dispatches a call via a private link, the circuit proves two things simultaneously: that the caller controls the address behind a stored Poseidon commitment, and that the address signed the specific call being dispatched — without ever writing the address to chain storage.

Private Inputs (Your Secrets)

• The external wallet address: address
• The blinding scalar used at registration: blinding
• External chain identifier: chain_id_fe
• Signature from the external wallet over the call hash: signature

Public Inputs (Visible to Everyone)

• On-chain commitment: commitment
• Blake2-256 hash of the dispatched call: call_hash_fe

The 2 Critical Constraints

1
Commitment preimage is valid

inner = poseidon2(chain_id_fe, address_fe) commitment == poseidon2(inner, blinding)

2
External wallet signed this exact call

ecdsa_verify(address, call_hash_fe, signature) == 1

Circuit Complexity

Different operations require different circuit sizes:

CircuitR1CS ConstraintsProving TimeProof SizeUse Case
Shield~5,000~1s192 bytesDeposit to pool
Transfer~50,000~10s192 bytesPrivate transfer (2 in → 2 out)
Unshield~25,000~5s192 bytesWithdraw to public
Private Link~1,450~100ms192 bytesDispatch call via private identity
Understanding Constraints

Each constraint is a mathematical equation that must hold true. More constraints = larger circuit = longer proving time. But verification time stays constant (~3ms) thanks to Groth16!


Proof Flow

End-to-end walkthrough of generating and submitting a private transfer:

Step-by-step: Private Transfer

1
Prepare your private inputs

Your wallet selects input notes (with values, blinding factors, spending key) and creates output notes. This all happens locally — nothing is shared yet.

2
Load the circuit and proving key

Your wallet loads transfer.wasm (the circuit) and transfer.zkey (proving key) from artifacts. These are generated during trusted setup.

3
Compute witness (intermediate values)

The circuit runs locally to compute all intermediate values: commitments, nullifiers, Merkle path verification, value balance checks. This creates the "witness" — proof that your inputs satisfy all constraints.

4
Generate the ZK proof

Using the witness and proving key, Groth16 generates a 192-byte proof. This takes ~10 seconds on a typical laptop. The proof says "I know secret inputs that satisfy all circuit constraints" without revealing what those inputs are.

5
Submit transaction with proof and public inputs

Your wallet sends: the proof (192 bytes), nullifiers, output commitments, and Merkle root. These public inputs allow verification without revealing private data.

6
On-chain verification (~3ms)

The runtime's pallet-zk-verifier loads the active verification key for the transfer circuit and runs Groth16 verification. If valid, nullifiers are marked as spent and new commitments are added to the Merkle tree. See On-Chain ZK Verification for how this VK registry works and how it is upgraded.


Trusted Setup

Groth16 requires a trusted setup ceremony to generate the proving and verification keys. This is a one-time process per circuit. The output is:

  • A proving key (.zkey / .ark) — used client-side to generate proofs
  • A verification key — deployed on-chain into pallet-zk-verifier

Ceremony Phases

1
Phase 1: Powers of Tau

Multiple participants contribute randomness. Each adds their secret and passes it to the next person. This creates a large file of random values (~10-50 GB).

2
Phase 2: Circuit-Specific Setup

Using the Powers of Tau, we generate proving keys (.zkey) and verification keys for each circuit (shield, transfer, unshield).

3
Destroy the "Toxic Waste"

All participants must permanently delete their random secrets. These secrets are called "toxic waste" because they could be used to create fake proofs.

The 1-of-N Trust Assumption

The beautiful property of trusted setup ceremonies:

As long as ONE participant is honest and destroys their secret, the setup is secure.

Even if 99 out of 100 participants collude or leak their secrets, if just ONE person follows the protocol correctly, the entire system remains secure.

Current Status

MVP Status

The current proving/verification keys in /artifacts are for testing only. They were generated in a local, non-production setup.

Before mainnet launch (Q4 2026), we will conduct a public multi-party ceremony with community participation and full transparency.


This page covers the cryptographic foundations. The following pages explain how this system is deployed and operated on Orbinum: