Z mark

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

ComponentDescription
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 SnapshotMetadata(height 3118950, roots, and set sizes). This metadata is compiled directly into the verifier library and served to the browser.

Output: data/snapshot_metadata.json → SNAPSHOT
crates/host-lib

Opens a light-wallet SQLite database with zcash_client_sqlite::WalletDb, collects all notes unspent at the snapshot, rebuilds Merkle witnesses from cached trees, and fetches supporting proofs from the proof service. The result is serialized into HoldingsWitness plus prepared transparent shielding records.

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 intoProcessedHoldings.

Exports:
verify_holdingsderive_*_alternate_nullifiertransparent_utxo_commitment
crates/server

Axum service that fronts the SP1 Reserved network. It provides REST endpoints for Merkle proofs (/nullifier-exclusion, /utxo-proof, /hashes), queues proof requests, verifies fulfilled proofs locally, and stores public values in SQLite to prevent double claims.

Database: proof_requests.db tracks status + used hashes

Verification Pipeline

The verification process follows a strict sequence of cryptographic checks:

  1. 1.

    Decode UFVKs & Rebuild Notes

    AccountWitness carries 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. 2.

    Merkle Inclusion Against SNAPSHOT

    Witness paths are upgraded into MerklePath objects and the computed roots must match the canonical snapshot roots (SNAPSHOT.sapling_root, SNAPSHOT.orchard_root). Any mismatch aborts the proof immediately.

  3. 3.

    Alternate Nullifier Exclusion

    Instead of revealing real nullifiers, the verifier iterates a domain-separated PRF with personalizations ZFUN_ORCH_ALT_NF or ZFUN_SAP_ALT_NF0. The resulting alternate nullifier is checked against exclusion proofs so the original note remains unlinkable.

  4. 4.

    Transparent Shielding Audits

    Notes created after the snapshot require verifying the transparent transaction that funded the shielded output. Signatures are rechecked, transparent_utxo_commitment is recomputed, and inclusion proofs are validated against SNAPSHOT.utxo_root when available.

  5. 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:

Loading...

Exclusion Proof Format

Exclusion proofs prove a gap between predecessor and successor nodes in a sorted Merkle tree:

Loading...

Alternate Nullifier Generation

Domain-separated PRFs prevent correlation with real nullifiers:

Loading...

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:

Loading...

Running the verifier locally (with optional proof generation) uses the same codepath:

Loading...