Docs
/concepts
/zkSNARKs

zkSNARKs

Zero-Knowledge Succinct Non-Interactive Argument of Knowledge

zkSNARKs

Veil utilizes Groth16 zkSNARKs on the BN254 (alt_bn128) elliptic curve to prove the validity of private transactions without revealing sensitive data. The transfer circuit contains approximately 7,000 R1CS constraints and enables complete transaction privacy.

What is a zkSNARK?

A zkSNARK (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge) allows a prover to convince a verifier that a statement is true without revealing any information beyond the truth of the statement itself.

Properties:

  • Zero-Knowledge: The proof reveals nothing about the private inputs (amounts, secrets, blinding factors)
  • Succinct: The proof is small (256 bytes for Groth16) and verification is fast (<1 second)
  • Non-Interactive: The prover generates the proof once; no back-and-forth communication needed
  • Argument of Knowledge: The prover must actually know the secret inputs, not just that they exist

What Does the Circuit Prove?

When you send a private transaction, you generate a proof that demonstrates:

"I know a secret, amounts, and blinding factors such that:

  1. The input commitment exists in the Merkle tree (membership proof via merkle_path)
  2. I derived the spending key correctly from my secret using Poseidon hash
  3. The nullifier is computed correctly from the spending key and leaf index
  4. The input commitment opens to the claimed amount using the correct blinding factor
  5. The output commitment is correctly formed with the same amount
  6. All cryptographic operations follow the protocol (no cheating)

...without revealing the secret, amounts, or which note I'm spending."

Groth16 Specifications

Curve: BN254 (alt_bn128) Constraint System: R1CS (Rank-1 Constraint System) Total Constraints: ~7,000 Proof Size: 256 bytes

  • proof_a: 64 bytes (G1 point)
  • proof_b: 128 bytes (G2 point)
  • proof_c: 64 bytes (G1 point)

Performance:

  • Proof Generation: 5-10 seconds (client-side, one-time per transaction)
  • Verification: <1 second on-chain (~200,000 compute units on Solana)
  • Security Level: ~128 bits (based on BN254 discrete log hardness)

Public Inputs (On-Chain)

These values are visible on-chain and included in the proof verification:

  1. merkle_root (32 bytes): Current root of the commitments Merkle tree
  2. nullifier (32 bytes): Unique identifier preventing double-spend
  3. new_commitment (32 bytes): Commitment to the output amount

Total Public Input Size: 96 bytes

Private Inputs (Witness)

These values are known only to the prover and never revealed:

  1. secret (32 bytes): Master secret for deriving spending key
  2. input_amount: Amount in the input commitment
  3. input_blinding (32 bytes): Blinding factor of input commitment
  4. output_blinding (32 bytes): Blinding factor for output commitment
  5. merkle_path (20 × 32 bytes): Sibling hashes proving Merkle membership
  6. leaf_index: Position of the input commitment in the tree
  7. Additional metadata: Asset IDs, domain separators, etc.

Constraint Breakdown (~7,000 Total)

The circuit enforces the following operations:

1. Spending Key Derivation (~400 constraints)

spending_key = Poseidon(secret, "NYX_SPENDING_KEY")
  • One Poseidon hash operation
  • Ensures spending key is correctly derived from secret

2. Input Commitment Verification (~500 constraints)

input_commitment = input_amount * G + input_blinding * H
  • Pedersen commitment computation
  • Scalar multiplication on BN254 G1
  • Verifies the input commitment matches the claimed amount and blinding

3. Merkle Tree Membership (~4,000 constraints)

root = VerifyMerklePath(input_commitment, merkle_path, leaf_index)
  • 20 levels of Poseidon hashing (depth 20 tree)
  • ~200 constraints per Poseidon hash × 20 levels
  • Proves the input commitment exists in the tree at the claimed position

4. Nullifier Derivation (~400 constraints)

nullifier = Poseidon(spending_key, Hash(leaf_index || "NYX_NULLIFIER"))
  • Two-step circuit-safe derivation
  • Prevents double-spending by producing unique nullifier per note
  • Ensures nullifier cannot be reused

5. Output Commitment (~500 constraints)

new_commitment = input_amount * G + output_blinding * H
  • Creates commitment for recipient
  • Ensures amount is preserved (same as input)

6. Amount Equality & Validation (~1,200 constraints)

  • Range checks (amounts are non-negative and within bounds)
  • Amount conservation (input_amount == output_amount for transfers)
  • Additional validation logic

Circuit Guarantees

The zkSNARK circuit provides the following security guarantees:

  1. No Double-Spend: Each nullifier can only be derived once per leaf_index
  2. Amount Conservation: Output amount equals input amount (for transfers)
  3. Valid Commitment: The input commitment exists in the Merkle tree
  4. Correct Derivation: All cryptographic operations follow the protocol specification
  5. Knowledge of Secret: Only the true owner knowing the secret can generate valid proofs

Proof Format

Groth16 Proof (256 bytes):

  • Used in production
  • Provides full security guarantees
  • Slower to generate (5-10s) but small and fast to verify

MVP Proof (96 bytes):

  • Ed25519 signature-based (testing/development only)
  • Fast generation (<1ms) for rapid iteration
  • NOT secure for production use
  • Auto-detected by proof length during verification

Performance Characteristics

Client-Side (Proof Generation):

  • Time: 5-10 seconds (depends on hardware)
  • RAM: ~2-4 GB peak usage
  • CPU: Heavy computation (multi-core helpful)
  • One-time cost per transaction

On-Chain (Verification):

  • Compute Units: ~200,000 CU (Solana)
  • Time: <1 second
  • Cost: ~0.0002 SOL per transaction (at current rates)
  • Performed by validators

Trusted Setup Ceremony

Groth16 requires a trusted setup to generate cryptographic parameters:

  • Proving Key (PK): Used to generate proofs (client-side)
  • Verification Key (VK): Used to verify proofs (on-chain)

Multi-Party Computation (MPC)

Veil will undergo a multi-party computation ceremony in Phase 4 where:

  1. Multiple independent participants (100+) each contribute randomness
  2. Participants combine their contributions sequentially
  3. Each participant must delete their secret contribution ("toxic waste")
  4. Security holds as long as at least one participant is honest and deletes their waste

Current Status:

  • Veil currently uses a development trusted setup (Phase 3G complete)
  • Production MPC ceremony planned for Phase 4
  • Not suitable for mainnet deployment until ceremony is complete

Security Implications

If all participants collude (extremely unlikely with 100+ participants):

  • They could generate fake proofs
  • They could NOT steal funds or decrypt amounts
  • The system would lose soundness but maintain zero-knowledge

After honest ceremony:

  • No one can generate invalid proofs
  • System is cryptographically secure
  • Parameters can be used indefinitely

Why Groth16?

Groth16 is chosen for Veil because of:

  1. Smallest Proof Size: 256 bytes (constant, regardless of circuit complexity)
  2. Fast Verification: <1s on-chain, critical for blockchain deployment
  3. Mature & Audited: Well-studied since 2016, battle-tested in Zcash
  4. Good Prover Performance: 5-10s is acceptable for privacy-critical operations
  5. Solana Integration: Excellent support via groth16-solana crate

Trade-offs vs Other zkSNARKs:

  • vs PLONK: Smaller proofs, faster verification, but requires trusted setup
  • vs STARKs: Much smaller proofs (256 bytes vs ~100KB), faster verification, but setup required
  • vs Bulletproofs: Smaller proofs, much faster verification, but slower proving

For a privacy protocol on Solana where on-chain verification cost is critical, Groth16's trade-offs are optimal.

Implementation Details

Libraries Used:

  • Proof Generation: ark-groth16 (arkworks-rs ecosystem)
  • Curve Operations: ark-bn254 (BN254/alt_bn128 curve)
  • On-Chain Verification: groth16-solana (Solana-optimized verifier)
  • Circuit Constraints: Custom R1CS constraints for Pedersen, Poseidon, Merkle trees

Witness Generation: The prover computes all private inputs off-chain, then generates the proof. This process is deterministic given the same inputs, ensuring reproducibility for debugging.

See also: