Zero-knowledge proofs are the cryptographic foundation of blockchain privacy. They let you prove you know something without revealing what you know. This article takes you from intuitive understanding to practical implementation on Solana.
The Magic of Proving Without Revealing
Imagine you want to prove you are over 21 to enter a bar. Normally, you show your ID, revealing your exact birthdate, address, and full name. What if you could prove “I am over 21” without revealing anything else?
That is what zero-knowledge proofs enable: proving statements about private data without exposing the data itself.
The Ali Baba Cave Analogy
The classic explanation uses a cave with a magic door. The cave has two paths (A and B) that meet at a locked door in the middle. Peggy (the prover) claims she knows the secret word to open the door.
The protocol:
- Victor (the verifier) waits outside while Peggy enters the cave
- Peggy randomly takes path A or B
- Victor enters and shouts which path Peggy should come back from
- If Peggy knows the secret word, she can always come back the correct way
- If she does not know it, she has only 50% chance of being on the right path
After 20 rounds, if Peggy succeeds every time, Victor is 99.9999% confident she knows the secret, yet he learned nothing about the secret word itself.
What “Zero-Knowledge” Actually Means
A ZK proof must satisfy three properties:
Completeness: If the statement is true and both parties follow the protocol, the verifier will be convinced.
Soundness: If the statement is false, no cheating prover can convince the verifier (except with negligible probability).
Zero-Knowledge: The verifier learns nothing beyond the fact that the statement is true. They could have generated the same “proof transcript” themselves without the prover.
The third property is the magic. The verifier gains conviction without gaining information.
Types of Zero-Knowledge Proofs
Not all ZK proofs are created equal. Different systems make different tradeoffs.
Interactive vs Non-Interactive
Interactive proofs require back-and-forth communication between prover and verifier (like the cave example). This is impractical for blockchains where verification happens asynchronously.
Non-interactive proofs (NIZKs) produce a single proof that anyone can verify without further communication. The Fiat-Shamir heuristic transforms interactive proofs into non-interactive ones by using a hash function to generate “random” challenges.
Blockchains use non-interactive proofs exclusively.
The Proof System Landscape
| System | Proof Size | Verification Time | Setup | Quantum-Safe |
|---|---|---|---|---|
| Groth16 | ~200 bytes | ~2ms | Trusted | No |
| PLONK | ~400 bytes | ~3ms | Universal | No |
| Halo2 | ~400 bytes | ~3ms | None | No |
| Bulletproofs | ~700 bytes | ~15ms | None | No |
| STARKs | ~50KB | ~10ms | None | Yes |
SNARKs: Succinct Non-interactive Arguments of Knowledge
SNARKs are the workhorse of blockchain privacy. “Succinct” means proofs are tiny (hundreds of bytes) and verification is fast (milliseconds), regardless of what you are proving.
Groth16 (used by Zcash): Smallest proofs, fastest verification, but requires a trusted setup ceremony for each circuit. The toxic waste from setup must be destroyed, or the system can be compromised.
PLONK (used by Aztec, zkSync): Universal trusted setup that works for any circuit. Slightly larger proofs than Groth16 but more flexible.
Halo2 (used by Zcash Orchard): No trusted setup at all using recursive proof composition. Proofs can verify other proofs, enabling infinite scalability.
STARKs: Scalable Transparent Arguments of Knowledge
STARKs take a different approach. They use hash functions instead of elliptic curves, making them:
- Transparent: No trusted setup needed
- Quantum-resistant: Hash functions resist quantum attacks
- Scalable: Proving time scales quasi-linearly with computation size
The tradeoff is proof size. STARKs produce proofs 100x larger than SNARKs. For on-chain verification where data costs money, this matters.
Bulletproofs
Bulletproofs are designed specifically for range proofs (proving a value is within bounds). They have:
- No trusted setup
- Logarithmic proof size in the range
- Linear verification time
Monero uses Bulletproofs to prove transaction amounts are positive without revealing them.
ZK Proofs for Privacy
How do these cryptographic tools enable privacy in practice?
Proving Membership Without Revealing Which
You have a list of 1000 authorized addresses. You want to prove your address is on the list without revealing which one.
Without ZK: Show your address (reveals identity)
With ZK: Create a Merkle tree of all addresses. Prove you know a valid Merkle path from the root to some leaf, without revealing which leaf.
The verifier learns “this person is authorized” but cannot link transactions to specific identities.
Proving Balance Without Revealing Amount
You want to prove you have at least 100 SOL to participate in a DAO vote. Revealing your exact balance of 50,000 SOL exposes you as a whale.
Without ZK: Show balance (reveals wealth)
With ZK: Commit to your balance using a Pedersen commitment. Prove the committed value exceeds 100 without revealing it.
Commitment = balance * G + blinding * H
Prove: balance >= 100 AND Commitment opens correctly
Proving Authorization Without Revealing Identity
You want to sign a transaction proving you own the funds, but without linking this transaction to your public identity.
Without ZK: Sign with your public key (links all transactions)
With ZK: Prove you know a private key corresponding to some public key in an authorized set, without revealing which key.
This is the foundation of stealth addresses and private transactions.
Solana-Specific Considerations
Solana presents unique challenges and opportunities for ZK implementations.
The Compute Unit Challenge
Solana transactions have a compute unit (CU) limit of 1.4 million CUs per transaction. ZK proof verification is computationally expensive:
- Groth16 verification: ~200,000 CUs (feasible)
- PLONK verification: ~400,000 CUs (tight)
- STARK verification: 1,000,000+ CUs (problematic)
This constraint shapes Solana ZK architecture.
Off-Chain Proving, On-Chain Verification
The solution is asymmetric computation:
- Prove off-chain: Complex proof generation happens on user devices or servers
- Verify on-chain: Only the cheap verification runs on Solana
[User Device] [Solana]
| |
| Generate proof |
| (10s, lots of RAM) |
| |
|------- proof ------>|
| |
| Verify proof
| (2ms, low CU)
| |
This is why browser-based proving is so important for privacy. Users generate proofs locally, and only the result touches the chain.
Light Protocol: ZK Compression
Light Protocol brings ZK compression to Solana. Instead of storing data on-chain, you store commitments and prove state transitions with ZK proofs.
- Compressed Accounts: Store Merkle root instead of full state
- State Diff Proofs: Prove you made valid state changes
- 10,000x Compression: Massive cost reduction
Light Protocol uses Groth16 for optimal verification costs within Solana CU limits.
Native ZK Verification
Solana is adding native support for common ZK operations:
- alt_bn128: Elliptic curve operations for SNARK verification
- Syscalls: Groth16 and PLONK verification as native instructions
When available, verification costs will drop from ~200,000 CUs to ~50,000 CUs, making ZK privacy much more practical.
SIP’s ZK Architecture
SIP Protocol uses zero-knowledge proofs for three core privacy guarantees.
Funding Proof: Prove Balance Without Revealing It
The Funding Proof demonstrates you have sufficient balance to fund a transaction without revealing your actual balance.
What it proves:
- You hold a commitment to some balance
- That balance exceeds the minimum required
- You own the keys to spend that balance
import {
NoirProofProvider,
type FundingProofParams
} from '@sip-protocol/sdk'
// Initialize the proof provider
const provider = new NoirProofProvider()
await provider.initialize()
// Your private data
const balance = 50_000_000_000n // 50 SOL (private)
const blindingFactor = crypto.getRandomValues(new Uint8Array(32))
// Generate proof (off-chain, ~2-5 seconds)
const result = await provider.generateFundingProof({
balance,
minimumRequired: 1_000_000_000n, // 1 SOL minimum
blindingFactor,
assetId: 'SOL',
userAddress: wallet.publicKey.toString(),
ownershipSignature: signature
})
// result.proof can be verified on-chain
// Verifier learns: "This user has >= 1 SOL"
// Verifier does NOT learn: actual balance of 50 SOL
The circuit has approximately 22,000 constraints, including:
- Commitment verification (~6,000)
- EC scalar multiplications (~6,000)
- Balance comparison (~200)
- ECDSA signature verification (~15,000)
Validity Proof: Prove Authorization Without Revealing Identity
The Validity Proof demonstrates an intent is properly authorized without revealing who authorized it.
What it proves:
- The sender commitment opens to a valid sender
- The sender signed the intent hash
- The intent has not expired
- A valid nullifier prevents double-spending
const validityProof = await provider.generateValidityProof({
intentHash: intent.hash,
senderAddress: wallet.publicKey.toString(),
senderBlinding: blindingFactor,
senderSecret: privateKey,
nonce: crypto.getRandomValues(new Uint8Array(32)),
timestamp: Date.now(),
expiry: Date.now() + 3600000,
authorizationSignature: signature
})
// Outputs:
// - Sender commitment (hides identity)
// - Nullifier (prevents replay)
// - Proof of authorization
This circuit is more complex (~72,000 constraints) due to ECDSA signature verification in-circuit.
Fulfillment Proof: Prove Correct Execution
The Fulfillment Proof demonstrates a solver correctly executed an intent without revealing execution details.
What it proves:
- Output amount meets minimum requirements
- Output was delivered to correct recipient
- Oracle attested to the delivery
- Fulfillment happened before expiry
const fulfillmentProof = await provider.generateFulfillmentProof({
intentHash: intent.hash,
outputAmount: 9_800_000_000n,
minOutputAmount: 9_500_000_000n,
outputBlinding: blindingFactor,
recipientStealth: stealthAddress,
solverSecret: solverKey,
fulfillmentTime: Date.now(),
expiry: intent.expiry,
oracleAttestation: {
recipient: stealthAddress,
amount: 9_800_000_000n,
txHash: '0x...',
blockNumber: 12345678n,
signature: oracleSignature
}
})
The solver proves they delivered what was promised without revealing:
- Exact amounts (only that it met minimum)
- Execution path (which liquidity sources)
- Solver identity (only solver commitment)
Noir: The ZK Circuit Language
SIP uses Noir for writing ZK circuits. Noir is a domain-specific language from Aztec designed to make ZK development accessible.
Why Noir?
Developer-friendly syntax: Rust-like language, not assembly or R1CS
Modern features: Generics, traits, modules, testing
Flexible backends: Compile to Barretenberg, Halo2, or custom provers
Browser support: WASM compilation for client-side proving
Writing a Simple Circuit
Here is a minimal Noir circuit that proves you know a preimage to a hash:
use dep::std::hash::sha256;
fn main(
preimage: [u8; 32], // Private input
hash: pub [u8; 32] // Public input
) {
let computed = sha256(preimage);
assert(computed == hash);
}
The pub keyword marks public inputs visible to verifiers. Everything else is private.
SIP’s Funding Proof Circuit
Here is a simplified version of SIP’s funding proof:
use dep::std::ec::secp256k1::{Point, ecdsa_secp256k1_verify};
use dep::std::hash::pedersen_commitment;
fn main(
// Public inputs
minimum_required: pub Field,
asset_id: pub Field,
// Private inputs
balance: Field,
blinding: Field
) -> pub [u8; 32] {
// 1. Verify balance >= minimum
assert(balance as u64 >= minimum_required as u64);
// 2. Compute commitment
let commitment = pedersen_commitment([balance, blinding]);
// 3. Return commitment hash as public output
let hash = std::hash::sha256([
commitment.x,
commitment.y,
asset_id
]);
hash
}
The circuit:
- Takes private balance and blinding factor
- Verifies balance exceeds public minimum
- Computes Pedersen commitment
- Returns commitment hash for verification
Browser Proving with WASM
SIP compiles Noir circuits to WebAssembly for browser execution:
import { BrowserNoirProvider } from '@sip-protocol/sdk/browser'
const provider = new BrowserNoirProvider({
useWorker: true, // Non-blocking UI
timeout: 60000 // 60 second timeout
})
// Initialize WASM (loads ~5MB)
await provider.initialize((progress) => {
console.log(`${progress.stage}: ${progress.percent}%`)
})
// Generate proof in browser
const proof = await provider.generateFundingProof(params, (progress) => {
updateProgressBar(progress.percent)
})
Browser proving has tradeoffs:
- Pro: No server needed, maximum privacy
- Con: Slower (5-30s vs 1-5s native)
- Con: ~500MB RAM usage during proving
For mobile devices, SIP automatically adjusts timeouts and provides compatibility checking:
const compat = BrowserNoirProvider.checkMobileCompatibility()
if (compat.score < 70) {
console.warn('Limited mobile support:', compat.issues)
// Fall back to server-side proving or simplified flow
}
Practical Example: Range Proof
Let us walk through implementing a range proof that proves a value is within bounds without revealing it.
The Problem
Prove: 0 < x < 2^64 without revealing x.
This is essential for:
- Proving transaction amounts are positive
- Proving balances do not overflow
- Proving ages, scores, or other bounded values
The Circuit
// range_proof.nr
fn main(
value: Field, // Private: the actual value
upper_bound: pub Field // Public: maximum allowed
) {
// Convert to u64 for range checking
let v = value as u64;
let bound = upper_bound as u64;
// Prove value > 0
assert(v > 0);
// Prove value < upper_bound
assert(v < bound);
}
Using It
import { compile, createWitness } from '@noir-lang/noir_js'
// Compile circuit
const circuit = await compile('range_proof.nr')
// Create witness with private value
const witness = await createWitness(circuit, {
value: 42n, // Private
upper_bound: 100n // Public
})
// Generate proof
const proof = await prover.generateProof(witness)
// Verify (anyone can do this)
const isValid = await verifier.verifyProof(proof)
// Returns: true
// Verifier learns: some value exists in (0, 100)
// Verifier does NOT learn: the value is 42
Why This Matters
Without range proofs, you could create commitments to negative values. If Commitment(-100) + Commitment(200) = Commitment(100), you have created 100 tokens from nothing.
Range proofs prevent this by proving all committed values are non-negative.
Performance Analysis
Real-world performance numbers for SIP’s Noir circuits:
Proving Time
| Environment | Funding Proof | Validity Proof | Fulfillment Proof |
|---|---|---|---|
| Native (M1 Mac) | 1.2s | 3.5s | 2.1s |
| Browser (Chrome) | 5.8s | 18.2s | 9.4s |
| Browser (Safari) | 7.1s | 22.4s | 11.8s |
| Mobile (iOS) | 12.3s | 45.1s | 24.7s |
| Mobile (Android) | 15.8s | 58.3s | 31.2s |
Browser proving is 5-10x slower than native due to WASM overhead and JavaScript single-threading.
Verification Time
| Environment | Any Proof Type |
|---|---|
| Native | 1.8ms |
| Browser | 4.2ms |
| Solana (estimated) | 2.1ms |
Verification is always fast - that is the magic of SNARKs.
Proof Sizes
| Proof Type | Size |
|---|---|
| Funding | 2.1 KB |
| Validity | 2.3 KB |
| Fulfillment | 2.4 KB |
UltraHonk proofs are larger than Groth16 but avoid trusted setup.
Memory Usage
| Environment | Peak RAM |
|---|---|
| Native | 180 MB |
| Browser | 520 MB |
| Mobile | 680 MB |
Mobile devices with less than 2GB RAM may struggle. SIP detects this and adjusts accordingly.
The Future: ZK on Solana
Light Protocol ZK Compression
Light Protocol is bringing ZK compression to Solana mainnet:
- State compression: Store 10,000 accounts in one Merkle root
- Batch proofs: Prove many state changes in one proof
- Cost reduction: 99% cheaper than regular accounts
For privacy, this means:
- Private token balances as compressed accounts
- ZK-proven transfers between compressed states
- Massive scalability for private transactions
Native Verification Support
Solana is adding built-in support for ZK verification:
- BN254 curve operations: Cheaper pairing checks
- Groth16 syscall: Native verification in ~50,000 CUs
- PLONK syscall: Native verification coming later
When available, on-chain verification costs drop 4x, making ZK privacy economically viable for everyday transactions.
Recursive Proofs
The future is recursive: proofs that verify other proofs.
Imagine:
- Alice generates a funding proof
- Bob generates a fulfillment proof
- A recursive proof verifies both in one proof
- On-chain verification stays constant regardless of transaction complexity
Halo2-style recursion enables this without trusted setup.
Building with ZK: Getting Started
Ready to start building?
For Solana Developers
- Explore Light Protocol: Start with their SDK for compressed accounts
- Use SIP SDK: Higher-level privacy abstractions built on ZK
- Learn Noir: Write custom circuits for specific use cases
For Application Developers
// Using SIP SDK for privacy
import { SIP, NoirProofProvider } from '@sip-protocol/sdk'
const sip = new SIP({
proofProvider: new NoirProofProvider()
})
// Create a private swap
const intent = await sip.createShieldedIntent({
input: { asset: 'SOL', amount: 10n },
output: { asset: 'USDC', minAmount: 980n },
privacy: 'shielded'
})
// Intent includes ZK proofs for:
// - Funding proof (balance verification)
// - Validity proof (authorization)
For Cryptography Enthusiasts
- Read the Noir documentation
- Study SIP’s circuit implementations in
packages/sdk/src/proofs/ - Explore the math behind Pedersen commitments and SNARKs
Conclusion: ZK is the Future of Privacy
Zero-knowledge proofs transform what is possible in blockchain privacy:
- Prove balance without revealing it: Financial privacy
- Prove authorization without revealing identity: Transaction unlinkability
- Prove computation without revealing inputs: Private smart contracts
On Solana, the constraints are clear: off-chain proving, on-chain verification, and constant vigilance about compute costs. But the tooling is maturing fast.
SIP Protocol uses ZK proofs as the cryptographic foundation for privacy. With Noir circuits compiled to WASM, users can generate proofs in their browsers, ensuring true privacy where even the service provider cannot see their data.
The math is complex, but the result is simple: privacy without trust.
Next in the Privacy Education Series: Deep-dive into Pedersen commitment mathematics and how they enable homomorphic operations on hidden values.