A zero-knowledge proof (ZKP) is a cryptographic protocol in which a prover convinces a verifier that a statement is true without revealing anything beyond that truth. This guide explains the prover-verifier mental model, the difference between interactive and non-interactive proofs, and the soundness and zero-knowledge properties you should reason about before shipping a ZK feature. It then walks through a runnable off-chain verification loop and a procedure for measuring on-chain verification cost against an RPC endpoint you control, so you can separate documented protocol behavior from numbers you actually observe.
What a ZK proof actually guarantees
A zero-knowledge proof is a protocol between two parties: a prover, who knows a secret witness, and a verifier, who checks a claim about that witness. The proof convinces the verifier that the statement is true while conveying no information beyond the statement's truth. The canonical example is proving you know the discrete logarithm of a public value without printing the exponent, or proving a transaction balances without revealing the amounts.
Three properties matter when you evaluate any ZK scheme. Completeness means an honest prover with a valid witness can always convince 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 beyond the truth of the statement, which is usually formalized by showing a simulator can produce transcripts indistinguishable from real ones.
This article covers the concepts, a runnable verification workflow, and a measurement method. It does not claim specific gas costs, proof-generation times, or network support for any particular proving system; those depend on the circuit, the proving backend, and the chain, and you should measure them yourself using the procedure below.
- Prover: holds the private witness and produces the proof.
- Verifier: checks the proof against public inputs.
- Witness: the secret data the proof is about.
- Public inputs: the statement the verifier can see.
Assumptions and prerequisites
The code samples assume Node.js 18 or later and a shell with curl. For the on-chain measurement section you need an RPC endpoint for the network where your verifier contract is deployed. Use a placeholder such as RPC_URL and supply your own endpoint; if you are choosing a provider, the RPC endpoint guide and the multi-chain RPC endpoints guide explain what to compare, while the RPC API service describes managed access.
You should also know which proving system your project uses. zk-SNARKs typically rely on a trusted setup and produce small proofs, while zk-STARKs avoid trusted setup and tend to produce larger proofs. The verification interface differs, so treat the snippets here as a harness you adapt to your library rather than a drop-in for every scheme.
Interactive versus non-interactive proofs
In an interactive proof, the prover and verifier exchange several messages. A classic example is the Ali Baba cave: the prover repeatedly emerges from a random tunnel chosen by the verifier, and after enough rounds the verifier becomes convinced the prover knows the secret path. Each round adds soundness but requires the verifier to be online and to supply fresh randomness.
Most blockchain applications need non-interactive proofs, because a verifier contract cannot run a multi-round conversation with an offline prover. The Fiat-Shamir heuristic converts an interactive proof into a single message by deriving the verifier's challenges from a hash of the transcript. This is why non-interactive ZK proofs are sometimes called NIZKs, and it is the form you will usually verify on-chain.
The practical consequence is that your security now depends on the hash function behaving like a random oracle. If the challenge derivation is predictable or the transcript is ambiguous, a malicious prover may be able to forge proofs. Audit the Fiat-Shamir transcript domain separation carefully when you implement a custom circuit.
The prover-verifier mental model in code
The following Node.js example illustrates the shape of a verification loop without claiming to be a production proving system. It models a prover that commits to a secret, a verifier that checks a public commitment, and a tamper check that should fail. Replace the toy hash with your library's verify call when you integrate a real scheme.
Run it with node zk-harness.mjs. The expected result is that the honest proof verifies and the tampered proof is rejected. If both pass, your verification logic is not actually binding the public input to the proof.
import { createHash } from 'node:crypto';
function commit(secret, publicInput) {
return createHash('sha256')
.update(`${publicInput}:${secret}`)
.digest('hex');
}
function verify(commitment, publicInput, revealedSecret) {
return commit(revealedSecret, publicInput) === commitment;
}
const publicInput = 'balance>=0';
const secret = 'witness-42';
const commitment = commit(secret, publicInput);
console.log('honest proof verifies:', verify(commitment, publicInput, secret));
console.log('tampered proof verifies:', verify(commitment, publicInput, 'witness-43'));
console.log('wrong public input verifies:', verify(commitment, 'balance>=100', secret));Measuring on-chain verification cost
Verification cost is the number that decides whether a ZK feature is viable on a given chain. Do not trust a blog post's gas figure; measure it against your own verifier contract and your own RPC endpoint. The procedure is: deploy the verifier, prepare a valid proof and its public inputs, call the verifier's verify function through eth_call to confirm it returns true, then send a real transaction and read the gas used from the receipt.
The eth_call step is free and catches malformed proofs before you spend gas. The transaction step gives you the actual gas consumed. Record the block number, the proof system version, and the verifier bytecode hash alongside the number so the measurement is reproducible.
Use a table like the one below to record your runs. The cells are intentionally blank because the values depend on your circuit and chain; fill them from your own receipts.
- | Run | Proof system | Public inputs | eth_call result | Gas used | Block |
- | --- | --- | --- | --- | --- | --- |
- | 1 | (your system) | (your inputs) | (true/false) | (from receipt) | (block number) |
- | 2 | (your system) | (your inputs) | (true/false) | (from receipt) | (block number) |
Runnable RPC check for a verifier contract
The shell snippet below calls a verifier contract through JSON-RPC. It uses eth_call with a pre-encoded calldata payload. Replace RPC_URL, VERIFIER_ADDRESS, and CALLDATA with your own values. The expected result is a JSON response whose result field is the ABI-encoded boolean or verification output.
If the node returns an error, check that the contract is deployed at that address on that chain and that the calldata selector matches the verifier's function signature. A 0x result with no revert usually means the call succeeded but returned empty data, which often indicates a selector mismatch.
curl -s -X POST "$RPC_URL" \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_call",
"params": [
{ "to": "VERIFIER_ADDRESS", "data": "CALLDATA" },
"latest"
]
}'Verifying the result and reading the receipt
After you send the verification transaction, fetch the receipt with eth_getTransactionReceipt and read gasUsed and status. A status of 0x1 means the transaction succeeded; 0x0 means it reverted, and the gas was still consumed. Compare the gasUsed value to your eth_call result: the call does not consume gas, so the receipt is the authoritative number.
For a reproducible measurement, run the same proof at least three times and record each receipt. Variance can come from state changes, cold versus warm storage slots, and the specific block's gas schedule. If you need stable latency for a high-volume verifier, a dedicated node removes noisy-neighbor effects that shared endpoints can introduce.
If you are comparing providers for this workload, keep the methodology fixed and change only the endpoint. The RPC pricing page explains plan dimensions, and the networks directory lists supported chains so you can confirm your target chain before you benchmark.
Troubleshooting common ZK integration failures
Most ZK integration bugs fall into a few buckets. Proof verifies off-chain but reverts on-chain usually means the public inputs are encoded differently, for example field elements serialized in a different byte order. Proof always verifies means the verifier is not binding the public inputs, or the Fiat-Shamir challenge omits part of the transcript. Proof never verifies often points to a mismatched verifying key or a circuit compiled with different parameters than the deployed verifier.
For RPC-level failures, distinguish transport errors from contract reverts. A -32000 style error with revert data means the node reached the EVM and the contract rejected the call; a timeout or connection reset means the endpoint did not respond. Retry logic should treat these differently, because retrying a deterministic revert wastes time and gas.
- Check byte order and field modulus when encoding public inputs.
- Confirm the verifying key hash matches the deployed verifier.
- Log the full revert data, not just the error message.
- Separate transport retries from contract-level failures.
Limitations and tradeoffs
ZK proofs are not free. Proving is computationally expensive, and the cost grows with circuit size. Verification is cheaper but still consumes gas, and the exact cost depends on the pairing operations or hash checks your scheme requires. Trusted setups introduce a ceremony whose compromise can break soundness, which is why some teams choose transparent setups despite larger proofs.
There is also a privacy boundary to respect. A ZK proof hides the witness, but the public inputs, the timing of the transaction, and the on-chain metadata can still leak information. If your threat model includes timing correlation or input-linkability, a proof alone is not sufficient; you need to design the surrounding protocol as well.
Finally, tooling maturity varies. Some proving systems have stable audited verifiers, while others are evolving quickly. Pin library versions and re-run your measurement procedure after every upgrade, because a verifier change can alter both gas cost and security assumptions.
Next steps for a ZK integration
Start by writing down the statement you want to prove, the witness, and the public inputs. Then choose a proving system that matches your trust model and your verifier's target chain. Build the off-chain harness first, confirm honest proofs pass and tampered proofs fail, and only then deploy the verifier.
Once the verifier is live, run the measurement procedure against a stable endpoint and record the results in a table you can share with your team. If you need help selecting an endpoint for the chains you target, the RPC endpoint guide and the RPC API service are the right starting points. For high-volume verification, evaluate a dedicated node and review RPC pricing before committing to a plan.