Zero-Knowledge Module
Caatinga ships a ZK workflow for BLS12-381 Groth16 proofs verified on Soroban via the official groth16_verifier pattern.
Curve choice
| Choice | Why |
|---|---|
| BLS12-381 + Groth16 | Matches Stellar Protocol 25+ host functions and stellar/soroban-examples/groth16_verifier. |
| Not BN254 | EVM-centric; Soroban does not expose BN254 pairing precompiles today. |
Requirements
- Protocol 25+ on the target network (testnet/mainnet must be at Protocol 25 before deploy).
- Verifier contract:
soroban-sdk = "25.1.0", Rust1.89.0. - Tooling: Circom 2 and snarkjs (installed on first use into
~/.caatinga/zk-tools). The firstcaatinga zk buildprints download and setup progress in the terminal (circom binary, snarkjs cache, dev powers-of-tau).
Quick start
For a step-by-step tutorial, see ZK project. Command reference and library API remain in this document.
# New ZK project with the example multiplier template
caatinga zk init my-zk-dapp
# New ZK-only project with an empty starter circuit
caatinga zk init my-zk-dapp --minimal
# Or add ZK files to an existing project
caatinga zk init
cd my-zk-dapp
npm install
# Build Soroban verifier + Circom circuit trusted setup
caatinga build
caatinga zk build
# Deploy and prove
caatinga deploy verifier --network testnet --source <identity>
caatinga zk prove
caatinga zk invoke --source <identity>Commands
| Command | Purpose |
|---|---|
caatinga zk init [project] | Scaffold zk-starter (multiplier circuit + verifier). |
caatinga zk init [project] --minimal | Scaffold a ZK-only project with a minimal identity circuit and verifier. |
caatinga zk init [project] --template <name> | Use a specific template instead of the default zk-starter. |
caatinga zk init [project] --force | Overwrite existing scaffold files. |
caatinga zk build [circuit] | Compile Circom (-p bls12381) and run dev trusted setup. |
caatinga zk build [circuit] --embed-vk | Experimental: emit contracts/verifier/src/vk.rs (not end-to-end). |
caatinga zk prove [circuit] | Generate proof.json and public.json from input.json. |
caatinga zk prove [circuit] --debug | Emit intermediate witness.wtns for debugging. |
caatinga zk invoke [circuit] | Serialize snarkjs output and call verify_proof on-chain (dynamic VK). |
caatinga zk invoke [circuit] --embed-vk | Blocked — experimental; use dynamic VK flow today. |
Artifacts land in .artifacts/zk/<circuit>/.
Minimal scaffold vs template
Use caatinga zk init my-zk-dapp when you want a working multiplier example with an interactive Vite + React shell (circuit inputs, wallet verify, placeholder bindings). Use caatinga zk init my-zk-dapp --minimal when you want a ZK-only starting point: no frontend config, no Vite files, and a template Main() circuit that simply exposes one output.
Both flows keep the same conventions: circuits/main.circom, circuits/input.json, contracts.verifier, and artifacts under .artifacts/zk/main/.
Browser + CLI hybrid (zk-starter)
The default template frontend follows the same wallet-driven pattern as react-vite-counter:
- Set circuit inputs in the UI and download
input.json - Save it to
circuits/input.json - Run
caatinga zk prove main(CLI) - Connect a wallet and simulate
verify_prooffrom the browser viacaatingaClient.contract("verifier").read(...)(@caatinga/client)
Serialization for browser verification uses @caatinga/zk/browser (buildVerifyProofBindingArgs). The dev server exposes proof artifacts at /zk-artifacts/proof.json, /zk-artifacts/verification_key.json, and /zk-artifacts/public.json after a local prove. Files live under .artifacts/zk/<circuit>/ on disk (gitignored) — not in public/zk-artifacts/.
Circuit inputs
circuits/input.json must contain private signals only. Do not include public outputs — for the multiplier scaffold, use a and b but not c. Snarkjs derives public signals during proving and writes them to .artifacts/zk/<circuit>/public.json.
If you include a public output in input.json, caatinga zk prove fails with a witness error (for example Too many values for input signal c).
Build artifacts
After caatinga zk build, Circom emits WASM under:
.artifacts/zk/<circuit>/main_js/main.wasmThe CLI resolves this path automatically during caatinga zk prove; you do not configure it in caatinga.config.ts.
Library API (@caatinga/zk)
Serialization (Node)
import { serializeProof, serializeVk, serializePublicSignals } from "@caatinga/zk";Full workflow (Node)
import {
buildCircuit, // compile Circom circuit
proveCircuit, // generate proof from input.json
invokeVerifier, // call verify_proof on-chain
buildStellarVerifyProofArgs, // build args for browser read/simulate
ptauSizeForConstraints, // determine powers-of-tau file size
} from "@caatinga/zk";Browser subpath
import {
buildVerifyProofBindingArgs, // build args for browser verify_proof read
concatG1Bytes, // concatenate G1 point bytes
concatG2Bytes, // concatenate G2 point bytes
} from "@caatinga/zk/browser";Types
| Type | Source | Purpose |
|---|---|---|
SnarkjsProof | @caatinga/zk | snarkjs JSON proof shape |
SnarkjsVk | @caatinga/zk | snarkjs JSON verification key shape |
SerializedProof | @caatinga/zk | Byte-serialized proof for Soroban |
SerializedVk | @caatinga/zk | Byte-serialized VK for Soroban |
BuildCircuitOptions | @caatinga/zk | Options for buildCircuit() |
ProveCircuitOptions | @caatinga/zk | Options for proveCircuit() |
InvokeVerifierOptions | @caatinga/zk | Options for invokeVerifier() |
InvokeVerifierResult | @caatinga/zk | Result of invokeVerifier() |
SerializedG1 | @caatinga/zk/browser | Byte-serialized G1 point |
SerializedG2 | @caatinga/zk/browser | Byte-serialized G2 point |
VerifyProofBindingArgs | @caatinga/zk/browser | Args for browser binding read |
VerifyProofBindingBuffers | @caatinga/zk/browser | Raw buffer form of binding args |
Error handling
ZkError is thrown by ZK library functions with a code property:
| Code | Meaning |
|---|---|
ZK_VK_REQUIRED | Verification key is required but not provided |
ZK_INVOKE_FAILED | On-chain verification call failed |
ZK_VERIFY_FAILED | On-chain verifier returned false (maps to CAATINGA_ZK_VERIFICATION_FAILED in the CLI) |
ZK_DEV_CEREMONY_BLOCKED | Mainnet blocked for dev-ceremony artifacts (maps to CAATINGA_ZK_DEV_CEREMONY_BLOCKED) |
ZK_UNSUPPORTED_PLATFORM | Operation not supported on current platform |
Dynamic VK vs embedded VK
| Mode | When to use |
|---|---|
| Dynamic VK (default) | VK passed as a contract argument; flexible across circuit changes. |
--embed-vk | Experimental — writes vk.rs only; zk invoke --embed-vk is blocked until E2E is complete. |
Embedded VK is opt-in and visible in your repo — never a hidden dependency.
Experimental: --embed-vk (not end-to-end)
caatinga zk build --embed-vk writes contracts/verifier/src/vk.rs with real BLS12-381 coordinates from verification_key.json. Re-run the same command after circuit changes to regenerate the file.
The default zk-starter verifier scaffold still expects a dynamic VK argument. caatinga zk invoke --embed-vk is blocked until an embedded-VK entrypoint exists in the contract. Use the default dynamic VK flow for end-to-end verification today.
Production guardrails
caatinga zk build always records a dev ceremony manifest (.artifacts/zk/<circuit>/ceremony.json). Caatinga blocks mainnet operations that would use those artifacts:
| Command | Guardrail |
|---|---|
caatinga zk build | Fails when defaultNetwork is mainnet (ceremony is always dev single-party) |
caatinga deploy <verifier> | Fails on mainnet when dev ceremony artifacts exist for linked circuits |
caatinga zk invoke | Fails on mainnet when dev ceremony artifacts exist |
Pass --allow-dev-ceremony only for conscious testing — not for production deployments. The CLI surfaces CAATINGA_ZK_DEV_CEREMONY_BLOCKED when a guardrail trips.
Production ZK still requires an external MPC powers-of-tau ceremony and audited circuit artifacts; Caatinga does not run MPC today.
Trusted setup warning
caatinga zk build runs a single-party development ceremony by default. Suitable for local testing only. Production deployments require a proper MPC powers-of-tau ceremony and audited circuit artifacts.
Cost reference
From the reference groth16_verifier budget report (one public input):
- ~41M CPU insn for a single public signal
- ~+2.46M CPU per additional public input
- ~294 KB memory for pairing
Run env.cost_estimate().budget().print() in contract tests for your circuit shape.
Config (caatinga.config.ts)
export default defineConfig({
// ...
zk: {
circuits: {
main: {
path: "./circuits",
protocol: "groth16",
curve: "bls12381",
verifierContract: "verifier", // optional: contract name for on-chain verification
},
},
},
});Only bls12381 is accepted today; other curves fail config validation. verifierContract is optional — when omitted, zk invoke targets the default verifier.
Phase note
The zk-starter verifier scaffold compiles against the reference contract shape. End-to-end proof verification on a live network should be validated after zk build + zk prove + zk invoke on Protocol 25+ testnet.