technical

Zero-Knowledge Proofs on Solana: From Theory to Practice

Learn zero-knowledge proofs from basics to Solana implementation. Covers SNARKs, STARKs, Noir circuits, and how SIP uses ZK for privacy.

15 min read

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:

  1. Victor (the verifier) waits outside while Peggy enters the cave
  2. Peggy randomly takes path A or B
  3. Victor enters and shouts which path Peggy should come back from
  4. If Peggy knows the secret word, she can always come back the correct way
  5. 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

SystemProof SizeVerification TimeSetupQuantum-Safe
Groth16~200 bytes~2msTrustedNo
PLONK~400 bytes~3msUniversalNo
Halo2~400 bytes~3msNoneNo
Bulletproofs~700 bytes~15msNoneNo
STARKs~50KB~10msNoneYes

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:

  1. Prove off-chain: Complex proof generation happens on user devices or servers
  2. 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:

  1. Takes private balance and blinding factor
  2. Verifies balance exceeds public minimum
  3. Computes Pedersen commitment
  4. 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

EnvironmentFunding ProofValidity ProofFulfillment Proof
Native (M1 Mac)1.2s3.5s2.1s
Browser (Chrome)5.8s18.2s9.4s
Browser (Safari)7.1s22.4s11.8s
Mobile (iOS)12.3s45.1s24.7s
Mobile (Android)15.8s58.3s31.2s

Browser proving is 5-10x slower than native due to WASM overhead and JavaScript single-threading.

Verification Time

EnvironmentAny Proof Type
Native1.8ms
Browser4.2ms
Solana (estimated)2.1ms

Verification is always fast - that is the magic of SNARKs.

Proof Sizes

Proof TypeSize
Funding2.1 KB
Validity2.3 KB
Fulfillment2.4 KB

UltraHonk proofs are larger than Groth16 but avoid trusted setup.

Memory Usage

EnvironmentPeak RAM
Native180 MB
Browser520 MB
Mobile680 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:

  1. Alice generates a funding proof
  2. Bob generates a fulfillment proof
  3. A recursive proof verifies both in one proof
  4. 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

  1. Explore Light Protocol: Start with their SDK for compressed accounts
  2. Use SIP SDK: Higher-level privacy abstractions built on ZK
  3. 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

  1. Read the Noir documentation
  2. Study SIP’s circuit implementations in packages/sdk/src/proofs/
  3. 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.