Skip to main content

Overview

identiPay uses zero-knowledge circuits built with Circom to enable privacy-preserving identity verification and anonymous payments. The circuits are compiled to Groth16 proofs, which are verified on-chain in the Sui smart contracts.
All circuits use the BN254 elliptic curve and Groth16 proving system for efficient on-chain verification.

Circuit Architecture

The protocol implements three core circuits:
CircuitConstraintsPurpose
identity_registration~700Creates a privacy-preserving identity commitment
age_check~1,500Proves minimum age without revealing birthdate
pool_spend~35,000Proves ownership of shielded pool note for withdrawal

Circuit Components

Circomlib Dependencies

All circuits depend on circomlib for cryptographic primitives:
include "node_modules/circomlib/circuits/poseidon.circom";
include "node_modules/circomlib/circuits/comparators.circom";
include "node_modules/circomlib/circuits/bitify.circom";
Key components:
  • Poseidon: ZK-friendly hash function used for commitments and Merkle trees
  • Comparators: Range checks and inequality proofs (GreaterEqThan, LessEqThan, IsZero)
  • Bitify: Bit decomposition for range proofs (Num2Bits)

Field Arithmetic

All circuits operate in the BN254 scalar field:
  • Field size: ~254 bits
  • Modulus: 21888242871839275222246405745257275088548364400416034343698204186575808495617
  • Compatible with Ethereum and Sui zkSNARK verifiers

Compilation Process

Circuits are compiled using the compile.sh script via npm scripts:
{
  "scripts": {
    "compile:identity": "bash scripts/compile.sh identity_registration",
    "compile:age": "bash scripts/compile.sh age_check",
    "compile:pool": "bash scripts/compile.sh pool_spend",
    "compile:all": "npm run compile:identity && npm run compile:age"
  }
}

Compilation Steps

The compilation pipeline consists of four stages:
# Generates R1CS constraints, WASM witness generator, and symbols
circom identity_registration.circom \
    --r1cs \
    --wasm \
    --sym \
    -o build/

Build Artifacts

After compilation, the following artifacts are generated:
{circuit}_js/{circuit}.wasm
file
WebAssembly witness generator. Used client-side to compute witness from private inputs.
{circuit}_final.zkey
file
Final proving key. Used by the prover to generate Groth16 proofs.
{circuit}_verification_key.json
file
Verification key in JSON format. Must be converted to Sui Move format and deployed on-chain.
{circuit}.r1cs
file
Rank-1 Constraint System. Defines the circuit’s constraints.

Proof Generation

Client-Side Workflow

Proofs are generated in the user’s browser or wallet:
import { groth16 } from 'snarkjs';

// 1. Prepare inputs (private + public)
const inputs = {
  // Private inputs (not revealed in proof)
  issuerCertHash: "0x1234...",
  docNumberHash: "0x5678...",
  dobHash: "0xabcd...",
  userSalt: "0xef01..."
};

// 2. Generate witness
const { proof, publicSignals } = await groth16.fullProve(
  inputs,
  "identity_registration.wasm",
  "identity_registration_final.zkey"
);

// 3. Format for Sui transaction
const proofBytes = serializeGroth16Proof(proof);
const publicInputs = serializePublicInputs(publicSignals);
The proving key (.zkey) and WASM files are typically 5-50MB each. Host them on a CDN or IPFS for production deployments.

On-Chain Verification

Proofs are verified on Sui using the zk_verifier module:
use identipay::zk_verifier::{Self, VerificationKey};

// Verification key is stored on-chain as a shared object
public fun verify_identity(
    vk: &VerificationKey,
    proof: vector<u8>,
    public_inputs: vector<u8>,
): bool {
    zk_verifier::verify_proof(vk, &proof, &public_inputs)
}
See ZK Verifier for details on the on-chain verification implementation.

Security Considerations

Trusted Setup

The Groth16 proving system requires a trusted setup ceremony. If the toxic waste from the ceremony is not properly destroyed, proofs could be forged.
Mitigations:
  • Use multi-party computation (MPC) for the trusted setup
  • Perform public ceremony with multiple contributors
  • For production, use the Perpetual Powers of Tau ceremony

Circuit Security

Best practices implemented:
  1. Constraint completeness: All inputs are properly constrained
    • Example: Date parsing uses explicit range checks (1 ≤ month ≤ 12)
  2. Underflow prevention: Comparisons use proper bit decomposition
    • Example: pool_spend.circom:129 uses Num2Bits(64) to prove non-negativity
  3. Public input binding: Public inputs are constrained to prevent malleability
    • Example: age_check.circom:154-158 squares public inputs to prevent optimization
  4. Nullifier uniqueness: Deterministic nullifiers prevent double-spending
    • Example: nullifier = Poseidon(commitment, ownerKey) in pool_spend.circom:115

Performance

Proving Time

Estimated client-side proving times (browser, M1 MacBook):
CircuitConstraintsProving TimeProof Size
identity_registration~700~0.5s128 bytes
age_check~1,500~1s128 bytes
pool_spend~35,000~15s128 bytes

Verification Cost

On-chain verification gas costs on Sui:
  • Base cost: ~0.001 SUI per proof verification
  • Linear in number of public inputs
  • Independent of constraint count (Groth16 has constant verification time)

Next Steps

Identity Registration

Create privacy-preserving identity commitments

Age Check

Prove age without revealing birthdate

Pool Spend

Anonymous withdrawals from shielded pools

Build docs developers (and LLMs) love