ZK cryptography is the family of protocols that let a prover convince a verifier that a statement is true without revealing the witness behind it. This article explains the prover-verifier mental model, contrasts SNARK and STARK design choices, and gives a runnable workflow for verifying proofs and measuring proving and verification costs. It also covers the failure modes that break real deployments, such as mismatched verification keys, wrong public inputs, and trust-setup assumptions. The goal is a reproducible method you can apply to any ZK stack rather than a vendor-specific walkthrough.
What ZK cryptography guarantees and what it does not
A zero-knowledge proof (ZKP) is a protocol in which a prover convinces a verifier that a statement is true without conveying anything beyond the truth of that statement. In ZK cryptography the statement is usually expressed as a relation over a public instance and a private witness: the prover knows a witness w such that R(instance, w) holds, and the verifier learns only that such a w exists. A canonical example is proving knowledge of a private key corresponding to a public key without revealing the key.
Three properties define the security contract. Completeness means an honest prover with a valid witness always convinces the verifier. Soundness means a dishonest prover cannot convince the verifier of a false statement except with negligible probability. Zero-knowledge means the verifier learns nothing about the witness beyond the statement's truth. A fourth property, succinctness, is what makes SNARKs and STARKs practical: proof size and verification time stay small relative to the size of the computation.
What ZK cryptography does not give you: it does not hide the fact that a proof was made, it does not protect the witness if your prover environment is compromised, and it does not remove the need to bind proofs to the correct public inputs. Most production incidents are not breaks of the underlying math; they are mismatches between the circuit, the verification key, and the public inputs.
- Completeness: valid witness always verifies.
- Soundness: false statements cannot be proven (except with negligible probability).
- Zero-knowledge: the verifier learns nothing about the witness.
- Succinctness: proof size and verification cost are sublinear in the computation.
Assumptions and prerequisites before you build
Before writing a circuit or calling a verifier, fix the following assumptions so your measurements are meaningful. You need a defined statement and witness, a chosen proof system (SNARK or STARK family), a trusted setup or transparent setup decision, and a verifier environment that can reproduce the same public inputs. For on-chain verification you also need an RPC endpoint you trust for reading verification contracts and event logs; the multi-chain RPC endpoint guide covers how to select one, and OnFinality's API service can serve as the read path for verification transactions.
Toolchain versions matter more than in most software. Circom, snarkjs, Halo2, Plonky, and Noir releases change constraint systems and verification keys between versions. Pin every dependency and record the exact versions alongside any benchmark you publish. If you are verifying on-chain, also pin the chain and contract address, because a verifier deployed on one network does not exist on another; check supported networks before assuming a verifier is reachable.
- Statement and witness defined in writing.
- Proof system and setup model chosen (trusted vs transparent).
- Pinned toolchain versions and circuit commit hash.
- Verifier environment and public-input encoding documented.
- RPC read path selected for on-chain verification checks.
The prover-verifier mental model
Think of a ZK protocol as a compiler pipeline. Your application logic is compiled into an arithmetic circuit or a constraint system. The prover takes the witness and produces a proof object. The verifier takes the proof, the verification key, and the public inputs, and returns accept or reject. The verification key is derived from the circuit and, for SNARKs with a trusted setup, from the ceremony parameters. If any of these three inputs changes, verification fails even when the proof itself is valid.
Interactive proofs require back-and-forth messages between prover and verifier. Non-interactive proofs collapse this into a single message, typically via the Fiat-Shamir transform, which hashes the transcript to derive challenge values. That transform is where many implementation bugs live: if the prover and verifier hash different transcript fields, or hash in a different order, the derived challenges diverge and verification fails. When debugging, always compare the exact byte encoding of public inputs and the transcript domain separator on both sides.
- Circuit defines the relation.
- Prover consumes witness and emits proof.
- Verifier consumes proof, verification key, and public inputs.
- Fiat-Shamir transcript must match byte-for-byte on both sides.
SNARK versus STARK: choosing a proof system
SNARKs (succinct non-interactive arguments of knowledge) generally produce small proofs and fast verification, which is why they dominate on-chain verification. Many SNARK constructions rely on elliptic-curve pairings and a trusted setup, so the ceremony and its toxic waste handling become part of your security model. STARKs (scalable transparent arguments of knowledge) use hash-based commitments, avoid trusted setup, and scale to very large computations, but proofs are larger and verification is heavier.
The choice is a tradeoff, not a ranking. If your verifier runs inside a smart contract with tight gas budgets, small SNARK proofs are usually the practical option. If you need transparency and post-quantum leaning assumptions, STARK-family systems are attractive. Recursive proofs, where a proof verifies other proofs, let you amortize verification cost across batches and are common in rollup designs. Whichever you choose, document the setup assumptions and the security level explicitly, because auditors will ask.
- SNARK: small proofs, fast verification, often trusted setup.
- STARK: transparent setup, larger proofs, heavier verification.
- Recursive proofs: amortize verification across batches.
- Document setup assumptions and security level for auditors.
Runnable verification workflow with snarkjs
The following procedure verifies a Groth16 proof using snarkjs, a widely used JavaScript toolchain. It assumes you already have proof.json, public.json, and verification_key.json from a prior proving run. The command is deterministic: it exits 0 and prints OK! on success, and exits non-zero on failure. Run it in CI so every circuit change is gated by a verification check.
The critical detail is that public.json must contain the public inputs in the exact order the circuit declares them. Reordering signals, or encoding a field element as a decimal string in one place and a hex string in another, is the most common cause of a valid proof failing verification.
npm install -g snarkjs@latest
# Verify a Groth16 proof against the verification key and public inputs
snarkjs groth16 verify verification_key.json public.json proof.json
# Expected output on success:
# [INFO] snarkJS: OK!Verifying proofs on-chain through an RPC endpoint
For on-chain verifiers, the proof is submitted in a transaction and the contract's verifyProof function returns a boolean. You can reproduce the verification result off-chain by calling the view function with the same calldata. The script below reads the verifier contract through an RPC endpoint and prints the result. Replace RPC_URL, VERIFIER_ADDRESS, and the proof arguments with your own values; never commit real keys.
Use a dedicated endpoint for repeated verification checks so you are not rate-limited mid-run. OnFinality's dedicated node offering provides isolated capacity, and RPC pricing explains how request volume maps to cost. For a quick provider comparison before committing, the RPC Assistant note on the best Ethereum RPC API is a useful starting point.
const { ethers } = require("ethers");
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const verifier = new ethers.Contract(
process.env.VERIFIER_ADDRESS,
["function verifyProof(uint256[2] a, uint256[2][2] b, uint256[2] c, uint256[] publicInputs) view returns (bool)"],
provider
);
async function main() {
const ok = await verifier.verifyProof(
["0x...", "0x..."],
[["0x...", "0x..."], ["0x...", "0x..."]],
["0x...", "0x..."],
["1", "2"]
);
console.log("verified:", ok);
}
main().catch((e) => { console.error(e); process.exit(1); });Measuring proving and verification cost reproducibly
ZK performance claims are only useful if the measurement method is reproducible. Record the circuit commit, toolchain versions, hardware (CPU model, core count, RAM), and the exact command used. Run each measurement at least five times and report the median and spread, not a single run. Separate proving time from verification time, because they scale differently: proving grows with circuit size, while verification stays roughly constant for succinct systems.
Use the table below as your results template. Fill it in with your own runs; do not copy numbers from documentation, because proving time is highly hardware- and version-dependent. The procedure is: (1) freeze the circuit and witness, (2) run the prover five times, (3) run the verifier five times, (4) record median and min/max, (5) repeat after any toolchain upgrade to detect regressions.
- | Metric | Run 1 | Run 2 | Run 3 | Median | Spread |
- | Proving time (s) | | | | | |
- | Verification time (ms) | | | | | |
- | Proof size (bytes) | | | | | |
- | Peak RAM (MB) | | | | | |
- | On-chain gas (verify) | | | | | |
Expected verification result and how to read it
A successful off-chain verification prints OK! and exits 0. A successful on-chain verification returns true from the view call and emits no revert. Treat any other outcome as a failure and capture the full error, because the error message usually identifies which input diverged. If the verifier reverts with InvalidProof, the proof or public inputs are wrong. If it reverts with a gas error, the proof encoding may be malformed or the verifier contract may be on a different network than you expect.
Log the verification key hash and the public-input hash alongside every verification result. When a proof that passed in staging fails in production, comparing those two hashes immediately tells you whether the circuit changed or the inputs changed. This single practice resolves most cross-environment verification failures.
- Exit code 0 and
OK!means off-chain verification succeeded. truereturn value means on-chain verification succeeded.InvalidProofrevert points to proof or public-input mismatch.- Log verification-key hash and public-input hash on every run.
Troubleshooting common ZK verification failures
Mismatched verification key. The proof was generated against a different circuit version than the verifier expects. Regenerate the verification key from the exact circuit commit and redeploy the verifier. Wrong public-input order. The circuit declares signals in one order and the caller supplies them in another. Compare the circuit's public signal list against the array you pass. Encoding mismatch. Field elements encoded as decimal strings in one component and hex in another will not match. Standardize on one encoding at the boundary.
Fiat-Shamir transcript divergence. The prover and verifier hash different fields or use different domain separators. Compare the transcript construction in both implementations. Trusted setup mismatch. The proof was generated with ceremony parameters that do not match the verification key. Re-run proving with the correct parameters. Stale RPC state. On-chain verification reads a verifier contract that was upgraded or redeployed; confirm the address and chain ID before debugging the proof itself.
- Regenerate verification key from the exact circuit commit.
- Match public-input order to the circuit's declared signals.
- Standardize field-element encoding at every boundary.
- Align Fiat-Shamir transcript fields and domain separators.
- Confirm verifier address and chain ID before debugging math.
Limitations, tradeoffs, and operational risks
ZK cryptography shifts trust rather than eliminating it. Trusted setups concentrate risk in the ceremony; a compromised ceremony can forge proofs. Transparent setups remove that risk but increase proof size and verification cost. Recursive proofs add complexity and new failure modes. None of these systems protect a witness that leaks through side channels in the prover's runtime, so isolate proving environments and treat witness material as production secrets.
Operationally, proving is expensive and bursty. A prover that runs comfortably in development may exhaust memory on a large witness in production. Budget for dedicated hardware, monitor peak RAM, and queue proving jobs rather than running them concurrently on shared machines. On-chain verification costs gas that varies with proof encoding and verifier implementation, so measure gas on the target network rather than estimating from another chain.
- Trusted setup concentrates ceremony risk.
- Transparent setups trade proof size for trust removal.
- Prover side channels can leak witnesses.
- Proving is memory-intensive; isolate and queue jobs.
- Measure gas on the target network, not another chain.
Next steps for a production ZK integration
Start by writing the statement and witness formally, then build the smallest circuit that expresses the relation and verify it end-to-end before adding features. Add the snarkjs verification command to CI so every circuit change is gated. For on-chain verifiers, add a view-call check against a pinned contract address and chain ID. Record the results table from the measurement section and re-run it after every toolchain upgrade.
When you are ready to connect verification to live networks, choose an RPC read path that matches your reliability requirements. Review the RPC endpoints guide for selection criteria, compare RPC pricing for cost planning, and use dedicated nodes when verification traffic needs isolation. For concrete endpoint and provider recommendations, consult RPC Assistant rather than guessing from documentation alone.
- Formalize statement and witness before coding.
- Gate circuit changes with CI verification.
- Pin verifier address and chain ID for on-chain checks.
- Re-run the measurement table after every upgrade.
- Select an RPC read path that matches reliability needs.