Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
duyuefeng0708
GitHub Repository: duyuefeng0708/Cryptography-From-First-Principle
Path: blob/main/frontier/10-snarks-starks/connect/groth16-zcash.ipynb
483 views
unlisted
Kernel: SageMath 10.0

Connect: Groth16 in Zcash Shielded Transactions

Module 10 | Real-World Connections

Every Zcash shielded transaction uses a Groth16 proof to verify validity without revealing sender, receiver, or amount.

Introduction

Zcash is a cryptocurrency that supports shielded transactions: transfers where the sender, receiver, and amount are all hidden from the public blockchain. The blockchain still needs to verify that transactions are valid (no double-spending, no money creation).

How? Every shielded transaction includes a Groth16 proof that the transaction is valid, without revealing any private details.

BitcoinZcash (shielded)
Sender visibleSender hidden
Receiver visibleReceiver hidden
Amount visibleAmount hidden
Verified by inspecting valuesVerified by Groth16 proof
Transaction ~250 bytesTransaction ~2 KB (includes 192-byte proof)

The Statement Being Proved

Each shielded spend proves (in zero knowledge) the conjunction of:

  1. Spend authority: "I know the spending key for this note"

  2. Note existence: "The note I'm spending exists in the commitment tree" (Merkle path)

  3. Nullifier correctness: "I computed the nullifier correctly" (prevents double-spending)

  4. Value balance: "Input values = output values + fee" (no money created)

The circuit encoding all of this has roughly 100,000 R1CS constraints (Sapling). Yet the proof is only 192 bytes and verification takes ~5 ms.

Let's build a toy analogy that captures the core structure.

# === Toy Zcash: a balance proof circuit === # Prove: input_value = output1 + output2 (without revealing any values) p = 101 F = GF(p) # Private witness: the actual values input_value = F(47) output1 = F(30) output2 = F(17) print("=== Toy Balance Proof ===") print(f"Private witness (hidden from verifier):") print(f" input_value = {input_value}") print(f" output1 = {output1}") print(f" output2 = {output2}") print(f" Balance check: {input_value} = {output1} + {output2} = {output1 + output2}") print(f" Valid? {input_value == output1 + output2}") print() print("Public statement: 'I know values that balance.'") print("The verifier learns NOTHING about the actual values.")
# === The R1CS for the balance proof === # # Variables: z = (1, input_value, output1, output2) # We need: input_value - output1 - output2 = 0 # Rewrite as a multiplication constraint (R1CS requires A*B = C): # (input_value - output1 - output2) * 1 = 0 # # A = (0, 1, -1, -1) selects input - out1 - out2 # B = (1, 0, 0, 0) selects 1 # C = (0, 0, 0, 0) result is 0 A_vec = vector(F, [0, 1, -1, -1]) B_vec = vector(F, [1, 0, 0, 0]) C_vec = vector(F, [0, 0, 0, 0]) # Witness vector z = vector(F, [1, input_value, output1, output2]) # Check R1CS satisfaction lhs = A_vec.dot_product(z) * B_vec.dot_product(z) rhs = C_vec.dot_product(z) print("=== R1CS Constraint ===") print(f"A.z = {A_vec.dot_product(z)} (input - out1 - out2)") print(f"B.z = {B_vec.dot_product(z)} (constant 1)") print(f"C.z = {C_vec.dot_product(z)} (target: 0)") print(f"Check: A.z * B.z = {lhs}, C.z = {rhs}") print(f"Satisfied? {lhs == rhs}") print() print("In Zcash Sapling, this is ONE of ~100,000 constraints.") print("Other constraints encode Pedersen commitments, Merkle paths, etc.")
# === Simulate the Groth16 proof and verification === # Trusted setup (toxic waste) set_random_seed(7) alpha = F.random_element() while alpha == 0: alpha = F.random_element() beta = F.random_element() while beta == 0: beta = F.random_element() delta = F.random_element() while delta == 0: delta = F.random_element() alpha_beta = alpha * beta # Prover computes proof elements from witness # Simplified: a = alpha + A.z, b = beta + B.z a_proof = alpha + A_vec.dot_product(z) b_proof = beta + B_vec.dot_product(z) # Public input term (in Zcash: Pedersen commitment to values, nullifiers) # Here: no public input (the balance check is purely private) pub_term = F(0) # c is computed so verification equation holds c_proof = (a_proof * b_proof - alpha_beta - pub_term) * delta^(-1) print("=== Groth16 Proof ===") print(f" Proof elements: a = {a_proof}, b = {b_proof}, c = {c_proof}") print(f" Proof size: 3 field elements (192 bytes on BLS12-381)") print() # Verification lhs_verify = a_proof * b_proof rhs_verify = alpha_beta + pub_term + c_proof * delta print("=== Verification (what Zcash nodes compute) ===") print(f" a*b = {lhs_verify}") print(f" alpha*beta + pub + c*delta = {rhs_verify}") print(f" Valid? {lhs_verify == rhs_verify}") print() print("The verifier accepted without learning:") print(f" - input_value = {input_value} (hidden)") print(f" - output1 = {output1} (hidden)") print(f" - output2 = {output2} (hidden)")

The Zcash Trusted Setup

Zcash has conducted two major trusted setup ceremonies:

CeremonyYearParticipantsCircuit
Sprout20166 participants~2,000 constraints
Sapling (Powers of Tau)201887 participants (phase 1) + hundreds (phase 2)~100,000 constraints

The Sapling ceremony was split into two phases:

  • Phase 1 (Powers of Tau): circuit-independent, reusable for any Groth16 circuit

  • Phase 2: circuit-specific, tied to the Sapling circuit

Security guarantee: as long as any one participant in the ceremony destroyed their share of the toxic waste, no one can forge proofs.

# === Simulate a mini MPC ceremony === print("=== Mini Powers of Tau Ceremony ===") print() # Phase 1: each participant contributes to tau n_participants = 5 shares = [] # Cumulative tau (starts at 1, each participant multiplies in their share) tau_combined = F(1) for i in range(n_participants): # Each participant generates a random share share = F.random_element() while share == 0: share = F.random_element() shares.append(share) # Multiply into cumulative tau tau_combined = tau_combined * share print(f" Participant {i+1}: contributes tau_{i+1} = {share}") print(f" Cumulative tau so far: {tau_combined}") print(f" (participant destroys their share)") print() print(f"Final tau = {tau_combined}") print(f"No single participant knows this value!") print(f"") print(f"Even if {n_participants - 1} of {n_participants} are malicious,") print(f"the 1 honest participant's destroyed share makes tau unrecoverable.")
# === Zcash proof size and verification cost === print("=== Zcash Sapling: By the Numbers ===") print() print("Metric Value")print("Circuit constraints ~100,000")print("Witness size ~3.2 MB")print("Proof size 192 bytes")print("Verification time ~5 ms")print("Proving time ~7 seconds")print("Pairing checks 3")print() print("The proof is CONSTANT size regardless of circuit complexity.") print("This is Groth16's killer feature for blockchain applications:") print("every full node verifies every shielded transaction in ~5 ms.") print() print("Compression ratio:") witness_bytes = 100000 * 32 # ~100k constraints, 32 bytes per wire proof_bytes = 192 print(f" Witness: {witness_bytes:,} bytes") print(f" Proof: {proof_bytes} bytes") print(f" Ratio: {witness_bytes / proof_bytes:,.0f}x compression")

Concept Map: Module 10 Concepts in Zcash

Module 10 ConceptZcash Application
Arithmetic circuitThe Sapling circuit: Pedersen hashes, Merkle paths, range checks
R1CS constraints~100,000 constraints encoding spend validity
QAP polynomial encodingCircuit compiled to polynomials for the Groth16 prover
Trusted setup (CRS)Powers of Tau + circuit-specific phase 2 ceremony
Toxic wasteτ,α,β,γ,δ\tau, \alpha, \beta, \gamma, \delta destroyed after ceremony
Proof elements (A,B,CA, B, C)3 BLS12-381 curve points = 192 bytes
Pairing checke(A,B)=e(α,β)e(pub,γ)e(C,δ)e(A, B) = e(\alpha, \beta) \cdot e(\text{pub}, \gamma) \cdot e(C, \delta)
Zero-knowledgeSender, receiver, and amount are hidden
SoundnessNo double-spending, no money creation
# === The Zcash circuit in detail (simplified) === print("=== What the Zcash Sapling Circuit Proves ===") print() print("For each SPEND (consuming a note):") print(" 1. [Pedersen commitment] note_cm = Commit(value, rcm)") print(" Proves: the note exists with specific value and randomness") print(" R1CS constraints: ~800 (for Pedersen hash)") print() print(" 2. [Merkle path] note_cm is in the commitment tree") print(" Proves: the note was previously created on-chain") print(" R1CS constraints: ~32 * 800 = ~25,600 (32-deep tree, Pedersen per level)") print() print(" 3. [Nullifier] nf = PRF(nsk, rho)") print(" Proves: the nullifier is correctly derived (prevents double-spend)") print(" R1CS constraints: ~20,000 (PRF evaluation)") print() print(" 4. [Spend authority] ak = SpendAuth(ask)") print(" Proves: the spender knows the secret key") print(" R1CS constraints: ~10,000 (key derivation)") print() print("For the TRANSACTION:") print(" 5. [Value balance] sum(input_values) = sum(output_values) + fee") print(" Proves: no money created or destroyed") print(" R1CS constraints: ~100 (addition and range checks)") print() print(f"Total: ~100,000 constraints") print(f"All compressed into a 192-byte proof.")

What's Next

Zcash is evolving beyond Groth16:

  • Orchard (2022): uses Halo 2 (recursive, no trusted setup)

  • The Zcash community is moving toward transparent proof systems

This mirrors the broader industry trend from trusted-setup SNARKs toward transparent systems (STARKs, Halo, Plonk with FRI).

Summary

ConceptKey idea
Sapling circuitEncodes transaction validity as ~100,000 R1CS constraints
QAP encodingThe circuit is compiled to polynomials and evaluated at the secret tau from the ceremony
Groth16 proof192 bytes (3 BLS12-381 curve points), constant regardless of circuit size
VerificationEvery full node checks 3 pairing equations in ~5 ms
Trusted setupMulti-party ceremony with hundreds of participants, secure if even one is honest
Zero-knowledgeSender, receiver, and amount are all hidden from the blockchain

Every concept from this module (arithmetic circuits, R1CS, QAP, pairings, trusted setup) is load-bearing in Zcash. None of it was abstract for its own sake.


Back to Module 10: SNARKs and STARKs