Zero to Zcash
A developer tutorial that takes you from "what even is a ZK proof" to building with the Zcash protocol. No fluff. No hand-waving. Every claim verified.
When I first opened the Zcash docs, I felt like someone handed me a math PhD thesis and said "here, build something." Elliptic curves, polynomial commitments, trusted ceremonies... none of it made sense.
Turns out, it's not actually that complicated once someone explains it like a normal person. That's what this tutorial does. No academic gatekeeping. No skipping the hard parts.
WTF Is a Zero-Knowledge Proof?
You go to a bank for a loan. They ask: "Do you make more than $5,000/month?" Normally, you'd show them your entire bank statement – every transaction, every embarrassing late-night impulse buy, everything.
But what if you could just... prove you make more than $5K without showing a single number? That's a zero-knowledge proof.
Three properties make it work:
Your friend is color-blind. You have a red ball and a green ball. You want to prove they're different colors without telling which is which.
Your friend hides the balls behind their back. They either swap or don't, then show you. You say "swapped" or "not swapped." If the balls were the same, you'd be guessing (50%). Since they're different, you get it right every time.
After 20 rounds, the probability you're guessing is (1/2)^20 = 0.0001%. Your friend is convinced. But they still don't know which is red and which is green. Proof happened. Information didn't leak.
Why does blockchain need this?
Regular blockchains like Bitcoin and Ethereum make everything public. Your wallet, balance, every transaction. It's pseudo-anonymous at best – one slip connecting your wallet to your identity, and your entire financial history is exposed.
Zcash uses ZK proofs so the network can verify transactions are valid without anyone seeing the sender, receiver, or amount.
✅ Check yourself
In a zero-knowledge proof, what does the verifier learn?
What "Shielded" Actually Means
When Zcash says a transaction is "shielded," it means the details are cryptographically hidden on-chain using zk-SNARKs, while the network can still verify everything checks out.
The blockchain proves the math is valid without knowing what the math is about.
Two worlds in one chain
Transparent
Shielded
Unified
Unified Addresses (u1...) bundle both types. Modern wallets should use these.
Transparent (t-addresses): Work like Bitcoin. Everyone sees sender, receiver, amount. Start with t.
Shielded (z-addresses): Sender, receiver, and amount are all encrypted. Only a short proof exists publicly. Start with zs.
Unified Addresses: Start with u1. Bundle multiple receiver types. Your wallet picks the most private option automatically.
zk-SNARKs: The engine behind shielding
Shielded doesn't mean "hidden forever from everyone." Zcash gives you view keys you can share with auditors, tax authorities, or anyone who needs to see your transactions. You control who sees what.
Privacy by default. Transparency by choice.
✅ Check yourself
How is Zcash shielding different from regular encryption?
The Math
Don't panic. I'm walking you through the entire pipeline, from a computation you want to prove, all the way to a tiny proof anyone can verify.
Step 1: Arithmetic Circuits
Any computation can be broken into addition (+) and multiplication (×) gates over a finite field. Like a logic circuit, but with math.
Gate 1: a = x × x (x²)
Gate 2: b = a × x (x³)
Gate 3: c = b + x (x³ + x)
Gate 4: out = c + 5 (x³ + x + 5)
Constraint: out == 35
Solution: x = 3
9 = 3 × 3 ✓
27 = 9 × 3 ✓
30 = 27 + 3 ✓
35 = 30 + 5 ✓
In Zcash, the "computation" is the full transaction validity check – ownership, balance, no double-spending. Thousands of gates.
Step 2: R1CS (Rank-1 Constraint System)
Each multiplication gate becomes a constraint:
Where s is a vector of all wire values, and A, B, C are matrices encoding the circuit. Addition is "free" – absorbed into the matrices.
s = [1, x, out, a, b, c] // witness vector
Constraint 1 (a = x × x):
A picks x, B picks x, C picks a
x × x = a ✓
Constraint 2 (b = a × x):
A picks a, B picks x, C picks b
a × x = b ✓
Each multiplication = one R1CS constraint
Each addition = absorbed into the matrices
Step 3: QAP (Quadratic Arithmetic Program)
The constraint matrices get converted into polynomials via Lagrange interpolation. Why? Because polynomial divisibility is cheap to check and impossible to fake.
L(x) · R(x) - O(x) is perfectly divisible by a target polynomial T(x). The prover computes the quotient H(x). If they can produce it, the witness is valid. Forging H(x) without knowing the witness is computationally impossible.Step 4: Elliptic Curve Commitment (BLS12-381)
Now we hide the polynomials using elliptic curve points. Zcash uses BLS12-381, designed by Sean Bowe in 2017.
The security relies on the discrete logarithm problem:
The prover commits to polynomials using curve points. Values are hidden, but verification through pairings still works.
Step 5: The final proof
In Groth16 (Zcash Sapling), the final proof is 3 elliptic curve elements – ~192 bytes. Doesn't matter if the circuit had millions of constraints. The proof is constant size and verifies in milliseconds.
✅ Check yourself
What comes right after Arithmetic Circuit in the pipeline?
Inside a Shielded Transaction
Let's trace what actually happens when you send shielded ZEC. Step by step, nothing skipped.
Interactive: Transaction Lifecycle
Click "Next Step" to walk through each stage.
V and R are generator points, v is value, rcv is randomness. These commitments are additively homomorphic: the network verifies that input commitments minus output commitments equals zero, without knowing any individual value.
✅ Check yourself
What prevents double-spending in Zcash's shielded pool?
Sapling vs Orchard
Two shielded protocols coexist in Zcash today. Sapling (2018) uses Groth16. Orchard (2022) uses Halo 2. The fundamental difference: trust.
The trust problem
Groth16 needs a trusted setup ceremony. Secret parameters are generated; if anyone kept them, they could forge proofs and create fake ZEC. Zcash ran a Multi-Party Computation with 87+ participants – secure if even one was honest. But it's still a trust assumption.
Halo 2 eliminates the trusted setup entirely. It uses an Inner Product Argument for polynomial commitments. No hidden structure. No toxic waste. No ceremony.
| Feature | Sapling (Groth16) | Orchard (Halo 2) |
|---|---|---|
| Trusted Setup | Required | Not needed |
| Proof Size | ~192 bytes (smallest) | Larger (IPA-based) |
| Verification Speed | Fastest | Slightly slower |
| Arithmetization | R1CS | PLONKish |
| Curve | Jubjub (on BLS12-381) | Pallas / Vesta cycle |
| Hash Function | Bowe-Hopwood Pedersen | Sinsemilla |
| Nullifier PRF | BLAKE2s | Poseidon |
| Recursive Proofs | No | Yes |
Orchard uses the Pasta curves (Pallas + Vesta). Their key property: each curve's scalar field equals the other's base field. This creates a cycle enabling recursive proof composition – verify a proof inside another proof, infinitely. This is what makes Halo 2 powerful for future scalability.
Hands-On: librustzcash
Enough theory. Here's what you'd actually touch as a developer.
The protocol spec
The bible lives at zips.z.cash/protocol/protocol.pdf (400+ pages). And ZIPs at zips.z.cash:
ZIP 0 – The ZIP process (how Zcash evolves)
ZIP 32 – HD wallets for shielded addresses
ZIP 224 – Orchard shielded protocol
ZIP 316 – Unified Addresses
ZIP 321 – Payment Request URIs
Explore the crate suite
Click any crate to see details:
Transaction, TransactionData, block headers, incremental Merkle tree, and serialization. Import this first – almost everything else depends on it.zcash_client_backend. Drop this in and you have persistent wallet state.Architecture
Quick start
[dependencies]
zcash_primitives = "0.17"
zcash_proofs = "0.17"
zcash_keys = { version = "0.4", features = ["orchard"] }
zcash_client_backend = "0.14"
zcash_protocol = "0.4"
use zcash_keys::keys::UnifiedSpendingKey;
use zcash_protocol::consensus::Network;
use zip32::AccountId;
let seed: [u8; 32] = /* BIP-39 seed bytes */;
let account = AccountId::try_from(0).unwrap();
// Derive spending key for Sapling + Orchard
let usk = UnifiedSpendingKey::from_seed(
&Network::MainNetwork,
&seed,
account,
).expect("key derivation failed");
// Get unified full viewing key
let ufvk = usk.to_unified_full_viewing_key();
// Generate a unified address (ZIP 316)
let (ua, _idx) = ufvk
.default_address(None)
.expect("address gen failed");
Build Your Own
Three paths depending on what you want to do.
Path A: Run a node (Zebrad)
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf \
https://sh.rustup.rs | sh
# Install Zebra
cargo install --locked \
--git https://github.com/ZcashFoundation/zebra \
zebrad
# Generate config + start
zebrad generate -o ~/.config/zebrad.toml
zebrad start
Path B: Light client (Zingolib)
git clone https://github.com/zingolabs/zingolib.git
cd zingolib
cargo build --release --package zingo-cli
./target/release/zingo-cli
# Inside: help, balance, addresses, send, shield
Path C: Build a wallet from librustzcash
// 1. Storage: WalletDb (SQLite-backed)
// 2. Keys: UnifiedSpendingKey from seed
// 3. Scan: Connect to lightwalletd, scan blocks
// 4. Build tx: Add spends + outputs, generate proofs
// 5. Broadcast: Send raw tx to the network
The Development Stack