Most projects that come to us with a ZKP request face one of two problems: either they need to prove a fact without revealing data (age, balance, set membership) or they need to offload heavy computation off-chain with on-chain verification. These are different tasks with different tool stacks, and confusing them is the first and most costly mistake at the start. We help you avoid errors: we assess the project, select the stack, and implement turnkey in 6–12 weeks. Contact us for a free consultation.
How to Choose the Right Proof System
The choice between Groth16, PLONK, STARK, Halo2, and FRI determines everything: proof size, generation time, trusted setup requirements, and on-chain verification cost. The table below compares them.
| System | Trusted setup | Proof size | Verification cost (EVM) | Prover time | Recursion |
|---|---|---|---|---|---|
| Groth16 | Yes (per-circuit) | ~200 bytes | ~270k gas | Fast | Hard |
| PLONK (KZG) | Yes (universal) | ~800 bytes | ~400k gas | Medium | Easier |
| PLONK (IPA) | No | ~1.5KB | More expensive | Slow | Good |
| STARK | No | 40–200KB | Very expensive in EVM | Slow | Excellent |
| Halo2 | No | ~1–5KB | Non-native | Medium | Built-in |
Groth16 is the choice for production systems with a fixed circuit and minimal gas requirements. Used by: Tornado Cash (formerly), Zcash Sapling, and most zkSNARK bridges. Downside: each circuit change requires a new ceremony. PLONK with KZG is the de facto standard for zkRollup-like systems. Gnosis, zkSync Lite, and Polygon Hermez use variants of PLONK. The universal trusted setup (Powers of Tau) is reusable — no ceremony per circuit. STARKs are chosen for tasks where no trusted setup is needed and recursion is required: StarkNet, Cairo VM. The large proof size makes native EVM verification impractical — a separate verifier contract or L3 approach is needed. Halo2 is used by Zcash Orchard and Scroll. It requires no trusted setup and has built-in recursion. Tooling is less mature, the ecosystem smaller, but actively developing.
For most practical tasks (private voting, proof of membership, zkKYC, age verification) — Groth16 via circom/snarkjs or PLONK via gnark/noir is the right starting point. At 20 gwei, one Groth16 verification costs roughly $2–5 on Ethereum mainnet, and on Arbitrum about $0.02–0.05, making L2 deployment economically justified.
What Is Vulnerable in ZK Circuits? Let's Analyze in Circom
Circom is a DSL for writing arithmetic circuits. The circuit compiles to R1CS, then a proof is generated via snarkjs or rapidsnark.
Basic scheme for proving knowledge of a hash preimage:
pragma circom 2.1.4; include "circomlib/circuits/poseidon.circom"; include "circomlib/circuits/comparators.circom"; template ProveBalance() { signal input balance; // private signal input salt; // private signal input commitment; // public signal input threshold; // public component hasher = Poseidon(2); hasher.inputs[0] <== balance; hasher.inputs[1] <== salt; hasher.out === commitment; component rangeCheck = Num2Bits(64); rangeCheck.in <== balance; component gte = GreaterEqThan(64); gte.in[0] <== balance; gte.in[1] <== threshold; gte.out === 1; } component main {public [commitment, threshold]} = ProveBalance(); "Under-constrained signals are the most common class of bugs in ZK circuits" (circom documentation, security section). If a signal is used in a computation but doesn't have enough constraints, the prover can pass an arbitrary value and the verifier will accept the proof.
Example of vulnerable code:
// VULNERABLE: no constraint that out is a bit template IsZero() { signal input in; signal output out; signal inv; inv <-- in != 0 ? 1/in : 0; out <-- in == 0 ? 1 : 0; // FORGOT: in * out === 0 and (in * inv - 1 + out) === 0 } The verifier accepts any out because there are no constraints linking out to in.
Overflow in field arithmetic: Circom works in the prime field p = 21888242871839275222246405745257275088548364400416034343698204186575808495617. Every operation is modulo p. If inputs are real-world numbers (age, timestamp), the range is safe. But when multiplying large numbers, an explicit range check via Num2Bits is needed, as shown in the example.
Gnark (Go) for More Complex Circuits
We note: when the circuit is too complex for circom (recursive proofs, BLS signature verification, zkEVM-like components) — gnark:
type Circuit struct { PreImage frontend.Variable `gnark:",secret"` Hash frontend.Variable `gnark:",public"` } func (c *Circuit) Define(api frontend.API) error { mimc, err := mimc.NewMiMC(api) if err != nil { return err } mimc.Write(c.PreImage) result := mimc.Sum() api.AssertIsEqual(result, c.Hash) return nil } gnark is 10–30x faster than snarkjs in prover time for identical circuits. For production with real users, this matters: generating a proof in the browser via WASM takes 3–15 seconds for a moderately complex Groth16 circuit, while in a Go server it takes 0.1–1 second.
On-Chain Verification
The Solidity verifier is generated automatically — snarkjs does this via snarkjs zkey export solidityverifier. But in production, the contract needs adaptation:
contract BalanceProofVerifier { IGroth16Verifier public immutable verifier; mapping(bytes32 => bool) public usedNullifiers; function verifyAndExecute( uint[2] calldata a, uint[2][2] calldata b, uint[2] calldata c, uint[2] calldata publicInputs // [commitment, threshold] ) external { bytes32 nullifier = keccak256(abi.encodePacked(a, b, c)); require(!usedNullifiers[nullifier], "Proof already used"); require(verifier.verifyProof(a, b, c, publicInputs), "Invalid proof"); usedNullifiers[nullifier] = true; // ... main logic } } Gas cost of Groth16 verification is about 270k gas. On Ethereum mainnet at 20 gwei, that's roughly $2–5 per verification. For high-frequency systems, deploying on L2 (Arbitrum, Base) reduces cost by 10–50x.
Infrastructure for Proof Generation
More about trusted setup ceremony
For Groth16, it is mandatory. The process:
- Universal Powers of Tau (we use ready-made from Hermez/EthSnarks — these are publicly verified parameters)
- Phase 2 ceremony specific to your circuit: each participant adds their own randomness
- Final beacon — a public random source (Bitcoin block hash)
If at least one participant is honest, the parameters are secure. For production projects: at least 10–20 participants, public verification of the transcript.
Client-Side Generation (Browser)
For wallet-level operations, we use the WebAssembly build of snarkjs. The .zkey file for complex circuits weighs 10–500MB — solution: split into chunks (chunked zkey) or use streaming download. For production loads, a server-side prover in Go with gnark is more efficient.
Server-Side Generation (Proving Service)
Architecture: Client → API → Queue (Bull/RabbitMQ) → Prover Worker → S3 → Webhook. Prover worker is a Go service with gnark. Horizontal scaling: each worker is independent, tasks are idempotent. For zkEVM-level circuits (billions of constraints) — GPU proving via CUDA with 100–1000x acceleration.
How to Start Developing a ZK Application: 5 Steps
- Circuit specification: Formalize the task, choose proof system, define public/private inputs.
- Circuit development: Write code in circom/gnark/noir, unit test constraints.
- Circuit audit: Search for under-constrained signals, check soundness with formal verification.
- Verifier contract: Generate Solidity verifier, add nullifier logic, integrate with the main protocol.
- Prover infrastructure: Set up WASM build or server prover, create API.
Timelines and Scope
| Phase | Content | Duration |
|---|---|---|
| Circuit specification | Formalize task, choose proof system, design public/private inputs | 1 week |
| Circuit development | Write circom/gnark/noir, unit test constraints | 2–4 weeks |
| Circuit audit | Search for under-constrained signals, check soundness | 1–2 weeks |
| Verifier contract | Solidity verifier + nullifier logic + integration with main protocol | 1–2 weeks |
| Prover infrastructure | WASM build or server prover, API | 1–2 weeks |
| Trusted setup | Organize ceremony (if Groth16/PLONK-KZG) | 1 week |
Total for a typical ZKP application (proof of membership, zkKYC, private transactions): 6–12 weeks from specification to mainnet. Complex zkRollup-like systems: 6–18 months with a dedicated team.
Want to discuss your project? Order ZK application development — we will assess the task for free and propose the architecture. For a general understanding of the technology, refer to Wikipedia. Contact us for a consultation.







