Skip to content
VOUCHVOUCHDocs

Getting started

How it works

A proof stands on three facts the Zcash chain already keeps. Vouch proves them about your notes without showing the notes, binds the proof to one statement, and carries the verdict wherever it is needed.

On this page

#The life of a proof

The life of one proofVerifierrail · lenderHolderpof-proveholder's machineAnchor table/api/anchorsrequest linkclaim · audience · period · expiryrequest + seed + snapshotfind notes, pick ≤ 5trial decryption, offlineprove, then signHalo2 · RedPallasproof.pofnothing broadcastthe .pof file, or a linkfetch anchors + revocationstrusted rootssix checks→ a typed verdictValid → release the deposit, open the line, or attest it for Solana
The request and the proof are files or links; nothing else passes between the parties. The verifier needs only public data to check it.

Four parties, and only files between them. The verifier writes a request (a link). The holder’s prover reads public chain data and the holder’s seed, and writes a proof. The verifier checks the proof against public roots. Nothing is sent to a server that the holder does not choose to send, and nothing on chain changes.

#Three facts the chain keeps

Chain dataWhat it provesIn the proof
Note-commitment treeA note existed at a block: the tree only grows.Each note is a leaf under nc_root at nc_height.
Nullifier setA note has not been spent: spending reveals its nullifier.Each note’s nullifier is absent from the set at nf_root.
Spend authorityWho controls the notes.A RedPallas signature under a re-randomisation of the notes’ key.
Membership in the note tree, non-membership in the spent setnc_rootsiblingpath··pathsiblingcm0cm1cm2cm3cm4cm5cm6cm7your noteNote-commitment tree at nc_heightthe tree only grows: a leaf at H₀ is a leaf for ever0pyour nfIMT leaf (low, next)low < nf < nextnf_roothashes up to● a revealed (spent) nullifierSpent-nullifier IMT at heightspending a note reveals its nullifier, for ever
Left: a Merkle path shows the note is a leaf of the tree whose root is nc_root. Right: an IMT leaf (low, next) with low < nf < next shows the note's nullifier was never revealed, so the note is unspent. Neither the note nor the nullifier is shown to the verifier.

#The proof

The proof is the Ironwood delegation circuit from Valar Group’s voting-circuits 0.12.1, the circuit the NU7 poll used to let holders delegate voting weight without revealing their notes. Upstream it proves only that the notes hold some value. Vouch adds one public input, min_ballots, and one constraint, so it proves the notes hold at least a public minimum. Halo2 over the Pasta curves (K = 12), so there is no trusted setup.

  • Up to five notes per proof; value counts in units of 0.125 ZEC; unused slots are filled, so the count is hidden too.
  • The statement (claim, audience, expiry, binding, heights, period) is signed with the notes’ spend authority, so no field can be edited after proving.
  • A viewing key alone cannot make a proof: it can find the notes but cannot sign.

Details, with the binding and public-input diagrams, are in The circuit.

#Unmoved since block H

Unmoved since block H₀: what passes and what is refusedblock heightH₀ · nc_heightH₁ · heighttree root taken herespent-set root taken hereNote A✓ counts: a leaf at H₀, its nullifier absent at H₁Note B✗ arrived after H₀: refused before any proving startsNote C✗ moved before H₁: its nullifier is in the spent set
The tree root comes from H₀ and the spent-set root from the later H₁. Only a note that was already a leaf at H₀ and whose nullifier is still absent at H₁ can be proven. A self-transfer makes new notes, so it restarts the clock.

The tree root and the spent-set root are independent public inputs. Taking the tree at an earlier block H₀ and the spent set at a later block H₁ proves the notes were already in the chain at H₀ and have not moved by H₁. No circuit change was needed: only the verifier’s rule that tied the two heights together was relaxed, with a check that nc_height ≤ height.

The prover refuses a note created after H₀ with a clear error before any proving starts; the verifier answers NotDormantLongEnough when a proof’s H₀ is later than the verifier requires.

#Scopes and reuse

Scopes: reuse is visible to one verifier, linkable by nobody elseYour notereal nullifier nfkey nkScope A, 1st proofcircuit 1 · rail:x · OctScope A, 2nd proofcircuit 1 · rail:x · OctScope Bcircuit 1 · lender:y · Octtag 7f3a…tag 7f3a…tag c91e…AlreadyUsedrail:x sees the same coinspledged twice this monthUnrelatedlender:y and rail:x cannotlink their two proofs
tag = Poseidon(nk, dom, nf), with dom derived from the scope (circuit, audience, period). The tag is deterministic per note and scope, and reveals nothing about nf or nk.

In format version 2 the circuit’s round id is the scope hash, so each note’s tag is stable within a scope and unlinkable across scopes. A verifier passes its registry of accepted tags to the verifier; a repeat is AlreadyUsed, judged only after the proof itself verifies. Reserves batches use the same tags to refuse a note counted twice across members.

#Exit certificates

Matching a deposit to a certificateCertificate namesnf₁nf₂Deposit Anf₁nf₂Deposit B · with a dummy spendnf₁nf₂nf ?Matchedevery spent note was certifiedMismatchone nullifier was never named
Matched means every nullifier the deposit reveals is one the certificate named. A dummy spend reveals a nullifier that looks exactly like an uncertified note, so exit transactions must be built without them.

Circuit 2 adds a reveal flag and the notes’ real nullifiers as public inputs. With the flag on, the certificate names the exact notes; when the deposit lands, the rail reads its nullifiers from the chain and checks every one was certified. Circuit 2 has its own verifying key; circuit 1 is byte-for-byte unchanged. See Exit certificates.

#Verification

One Rust function, pof-verify::verify, runs six checks cheapest first and returns a typed verdict. It runs as a CLI, as WASM in the browser and inside the attestor, and CI checks all three agree on every test vector. See The verification pipeline.

#Getting to Solana

A Solana program cannot verify Halo2 over Pallas or read Zcash state. So an attestor runs the same verifier and signs the verdict with Ed25519, and the programs check that signature through instruction introspection:

ProgramDoes
pof-gateAllowlisted, k-of-n attestations become one receipt per proof, bound to the holder’s wallet.
pof-creditOpens a credit line from a receipt; draws only while the line’s latest proof is fresh.
pof-reserveAn attested reserves batch becomes a feed; its token mints only within the proven total, only while fresh.

See Solana programs for every instruction, account and error, and Credit lines for the flow.