Zero Knowledge Proofs (ZKPs) are revolutionizing the way we think about privacy and scalability in blockchain systems. When combined with smart contracts on platforms like Ethereum, ZKPs unlock powerful new capabilities—ranging from private transactions to off-chain computation with on-chain verification. This article explores how ZKP technology can be integrated into smart contract development using practical code examples, focusing on core benefits: privacy preservation and computational scalability.
We’ll walk through a hands-on example that demonstrates how to replace a traditional on-chain computation with a zero-knowledge circuit, verify proofs on-chain, and maintain sensitive data confidentiality—all while ensuring correctness.
Understanding Zero Knowledge Proofs (ZKP)
Zero Knowledge Proofs, particularly zk-SNARKs (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge), allow one party to prove they’ve performed a specific computation without revealing the inputs. The proof is succinct—meaning it can be verified quickly—even if the original computation was complex.
As described in Vitalik Buterin’s introduction to zk-SNARKs, this cryptographic tool enables trustless verification: you can confirm a result is correct without knowing or re-running the entire process.
In blockchain development, this translates to two major advantages:
- Scalability: Move heavy computations off-chain.
- Privacy: Keep input data hidden while proving its validity.
These principles form the backbone of advanced applications such as ZK Rollups and privacy-focused protocols like Tornado Cash.
A Basic Smart Contract Without ZKP
Let’s begin by examining a simple Solidity contract that performs an on-chain calculation:
contract WithoutZK {
uint256 public answer = 1770;
string public greeting;
function calculate(uint256 x) public pure returns (uint256) {
return x * x + 6;
}
function process(uint256 _secret) public {
require(calculate(_secret) == answer, "Invalid secret");
greeting = "answer to the ultimate question of life, the universe, and everything";
}
}Here, process() accepts a secret value _secret, computes f(x) = x² + 6, and checks whether the output matches the stored answer (1770). If valid, it updates the greeting.
However, there's a critical flaw: all Ethereum transactions are public. Anyone can view the _secret value on Etherscan, completely undermining privacy.
👉 Discover how ZKPs protect your data in decentralized applications
Introducing Circuits with Circom
To fix this, we shift the computation off-chain using Circom, a domain-specific language for defining arithmetic circuits used in ZKPs.
Our goal: prove that we know a secret x such that x² + 6 = 1770, without revealing x.
Here’s the circuit definition in Circom:
template Square() {
signal input in;
signal output out;
out <== in * in;
}
template Add() {
signal input in;
signal output out;
out <== in + 6;
}
template Calculator() {
signal private input secret;
signal output out;
component square = Square();
component add = Add();
square.in <== secret;
add.in <== square.out;
out <== add.out;
}
component main = Calculator();This circuit defines:
- Private input:
secret - Public output:
out(must equal 1770) - Computation steps: square the input, then add 6
The keyword private ensures the input remains confidential during proof generation.
The ZKP-Enabled Smart Contract
After compiling the circuit using tools like snarkjs, we generate a verifier contract (Verifier.sol) that includes a verifyProof() function.
We now rewrite our original contract to accept only a ZKP instead of raw secrets:
import "./Verifier.sol";
contract ZK is Verifier {
uint256 public answer = 1770;
string public greeting;
function process(uint256[] memory proof, uint256[] memory publicSignals) public {
require(isAnswer(publicSignals[0]), "Invalid output");
require(verifyProof(proof, publicSignals), "Invalid proof");
greeting = "answer to the ultimate question of life, the universe, and everything";
}
function isAnswer(uint256 output) internal view returns (bool) {
return output == answer;
}
}Key changes:
- No direct access to
_secret - Verification depends on both
proofandpublicSignals - We validate that the public output matches our expected answer before verifying the proof
Generating the Proof Off-Chain
Using JavaScript and snarkjs, users generate proofs locally:
const { proof, publicSignals } = await groth16.fullProve(
{ secret: 42 },
'circuit.wasm',
'circuit_final.zkey'
);The generated proof and publicSignals (e.g., [1770]) are submitted to the contract. The blockchain verifies the proof’s correctness and checks that the output matches the expected answer.
From the chain’s perspective, it only sees a valid proof and a matching result—never the secret.
👉 Learn how developers are building private blockchain apps today
The Role of Public Signals
A common point of confusion is the purpose of publicSignals. These represent all public outputs of the circuit—values that must be known to both prover and verifier.
In our case:
const publicSignals = [1770];Even though the proof itself is valid for any input-output pair, we must ensure that the output matches our application logic. Without checking isAnswer(publicSignals[0]), attackers could submit proofs for any equation (e.g., f(2) = 10) and still pass verification.
Thus, ZKP secures computation integrity, but application logic must enforce business rules.
Best Practices in ZKP Application Logic
While zk-SNARKs provide strong cryptographic guarantees, poor circuit design can leak information:
- Simple functions expose inputs: In our example, solving
x² + 6 = 1770revealsx = 42. Real-world circuits should use irreversible operations like hashing or Merkle paths. - Private vs Public signals: Always audit which signals are marked
private. Any unmarked input becomes public. - Cross-validate with JS: Implement the same logic in JavaScript first to generate test vectors and debug discrepancies between expected and actual outputs.
For reference, explore working examples at: https://github.com/chnejohnson/circom-playground
Real-World Applications of ZKP in Blockchain
Though our example uses a basic mathematical function, real-world ZKP applications involve more complex logic:
- Hash functions: Prove knowledge of a preimage without revealing it.
- Merkle proofs: Show membership in a set (used in Tornado Cash).
- Digital signatures: Verify ownership without exposing keys.
These primitives power:
- ZK Rollups: Scale Ethereum by batching off-chain computations.
- Privacy Pools: Enable anonymous transactions via shielded pools.
- Identity systems: Prove credentials without disclosing personal data.
👉 Explore cutting-edge ZKP use cases shaping Web3’s future
Frequently Asked Questions (FAQ)
What is a Zero Knowledge Proof (ZKP)?
A Zero Knowledge Proof allows one party to prove they know a value or performed a computation without revealing the value itself. It ensures privacy and verification integrity.
Can ZKPs be used for scalability?
Yes. By moving computation off-chain and submitting only a small proof for verification, ZKPs significantly reduce on-chain load—this is the foundation of ZK Rollups.
Do I need to understand cryptography to develop ZKP applications?
Not necessarily. Tools like Circom and snarkjs abstract much of the complexity. However, understanding circuit design and threat models is essential for secure applications.
How do public signals differ from private inputs?
Private inputs (e.g., secrets) are never revealed. Public signals are outputs or parameters visible to everyone, such as computed results or Merkle root hashes.
Is it safe to let users provide publicSignals during verification?
Only if they’re validated against on-chain state. Always cross-check public outputs (like answers or roots) before accepting a proof.
What happens if my circuit has bugs?
A flawed circuit may accept invalid proofs or leak private data. Always test thoroughly using known inputs and compare results across environments (e.g., JS vs Circom).
Conclusion
This walkthrough illustrates how Zero Knowledge Proofs enhance smart contracts by enabling off-chain computation and input privacy. While cryptographic backends like Groth16 or PLONK handle proof generation and verification, developers must carefully design application logic to prevent information leakage.
Real-world protocols such as Tornado Cash and ZK Rollups build upon these principles, combining hashing, Merkle trees, and digital signatures within circuits to create scalable and private systems.
As ZK technology matures, expect broader adoption across identity, finance, gaming, and governance layers in Web3.
To explore the full implementation: https://github.com/chnejohnson/mini-zkp
Stay tuned for our next article diving into how Tornado Cash leverages ZKPs for anonymous Ethereum transactions.