Zero-Knowledge Proof Application Development – Turnkey Solutions

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

Blockchain Development Services

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1309
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1270
  • image_logo-advance_0.webp
    B2B Advance company logo design
    719
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    1011
  • image_logo-aider_0.webp
    AIDER company logo development
    954
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1062

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:

  1. Universal Powers of Tau (we use ready-made from Hermez/EthSnarks — these are publicly verified parameters)
  2. Phase 2 ceremony specific to your circuit: each participant adds their own randomness
  3. 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

  1. Circuit specification: Formalize the task, choose proof system, define public/private inputs.
  2. Circuit development: Write code in circom/gnark/noir, unit test constraints.
  3. Circuit audit: Search for under-constrained signals, check soundness with formal verification.
  4. Verifier contract: Generate Solidity verifier, add nullifier logic, integrate with the main protocol.
  5. 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.