Zcash Verifier
Architecture
How the snapshot, witness builder, SP1 program, and prover service fit together
System Overview
The verifier shared across the `zfun` monorepo revolves around a single data contract: `HoldingsWitness`. A snapshot builder materializes Merkle forests and snapshot metadata (`crates/setup-script`), a wallet ingestion library creates witnesses from SQLite wallets (`crates/host-lib`), the core verifier enforces all proofs (`crates/lib`), and an SP1 zkVM program simply runs that verifier inside a zero-knowledge circuit (`crates/program`).
Proof generation happens in two environments: native Rust via the `zfun-host` CLI and the browser via WebAssembly. Both run `verify_holdings` locally before handing the witness to the SP1 prover network.
Core Components
| Component | Description |
|---|---|
crates/setup-script | Streams Zebra blocks with high concurrency, sorts all transparent creates/spends, generates Merkle proofs for both the UTXO set and the nullifier set, and emits Output: data/snapshot_metadata.json → SNAPSHOT |
crates/host-lib | Opens a light-wallet SQLite database with Env: ZFUN_PROOF_SERVICE_URL (default http://localhost:3000) |
crates/lib | Everything that must hold inside the zkVM lives here: note reconstruction from UFVKs, Sapling/Orchard Merkle verification, alternate nullifier derivation, nullifier exclusion proofs, transparent signature verification, and summing the final balance into Exports: verify_holdingsderive_*_alternate_nullifiertransparent_utxo_commitment |
crates/server | Axum service that fronts the SP1 Reserved network. It provides REST endpoints for Merkle proofs ( Database: proof_requests.db tracks status + used hashes |
Verification Pipeline
The verification process follows a strict sequence of cryptographic checks:
- 1.
Decode UFVKs & Rebuild Notes
AccountWitnesscarries string-encoded UFVKs. The verifier decodes them, reconstructs Sapling/Orchard notes from stored payloads (recipient,value,rseed,rho), and ensures the derived addresses match the UFVK components. - 2.
Merkle Inclusion Against
SNAPSHOTWitness paths are upgraded into
MerklePathobjects and the computed roots must match the canonical snapshot roots (SNAPSHOT.sapling_root,SNAPSHOT.orchard_root). Any mismatch aborts the proof immediately. - 3.
Alternate Nullifier Exclusion
Instead of revealing real nullifiers, the verifier iterates a domain-separated PRF with personalizations
ZFUN_ORCH_ALT_NForZFUN_SAP_ALT_NF0. The resulting alternate nullifier is checked against exclusion proofs so the original note remains unlinkable. - 4.
Transparent Shielding Audits
Notes created after the snapshot require verifying the transparent transaction that funded the shielded output. Signatures are rechecked,
transparent_utxo_commitmentis recomputed, and inclusion proofs are validated againstSNAPSHOT.utxo_rootwhen available. - 5.
Balance Commitment & Rounding
Values accumulate in 128-bit space, then the total is rounded down to the nearest 1,000,000 zatoshis before being exposed publicly. Alternate nullifiers and transparent commitments are emitted alongside the total for downstream deduplication.
Technical Details
Merkle Proof Structure
Merkle witnesses captured in the witness format map directly into proof-friendly paths:
Exclusion Proof Format
Exclusion proofs prove a gap between predecessor and successor nodes in a sorted Merkle tree:
Alternate Nullifier Generation
Domain-separated PRFs prevent correlation with real nullifiers:
Implementation
The SP1 program is intentionally tiny — it reads the witness, runs the verifier, and commits the result. The native CLI reuses the exact same function:
Running the verifier locally (with optional proof generation) uses the same codepath: