Skip to content
VOUCHVOUCHDocs

Architecture

The circuit

Vouch’s proofs are Halo2 proofs of the Ironwood delegation circuit, with two small, marked modifications: a threshold for every proof, and revealed nullifiers for exit certificates.

On this page

#The base circuit

The evidence in a .pof is a Halo2 proof (IPA commitments over the Pasta curves, K = 12, no trusted setup) of the delegation circuit from voting-circuits 0.12.1 by Valar Group, driven the way zcash_voting (Chainapsis) drives it. Upstream, holders used it to delegate voting weight in the NU7 poll. For up to five notes it proves:

  • each note is a leaf of the Ironwood note-commitment tree at nc_root;
  • each note is unspent: its real nullifier is not in the spent-nullifier IMT at nf_imt_root, and the nullifier stays private;
  • the prover holds the keys the notes are addressed to: rk is a re-randomisation of the notes’ ak;
  • each note yields a governance nullifier (a tag) under the domain dom, derived from vote_round_id.

#Modification 1: the threshold

Upstream the circuit hides the value sum and enforces only that the notes are worth at least one ballot (0.125 ZEC). Vouch adds a 15th public input, min_ballots (offset 14), and one range constraint:

Text
num_ballots − min_ballots ∈ [0, 2³⁰)        num_ballots = Σ note values / 12,500,000 (committed, not revealed)
min_ballots = claim.zatoshi / 12,500,000    (derived by the verifier from the claim)

So the proof says “at least min_ballots × 0.125 ZEC” and nothing more. The test vouch_threshold in the vendored crate checks it, together with the upstream suite.

#Modification 2: revealed nullifiers (circuit 2)

Exit certificates need the rail to match a deposit to the certified notes. Circuit 2 adds a boolean reveal (offset 15) and five revealed_nf_i (offsets 16–20), with two gates:

Text
revealed_nf_i − reveal · real_nf_i = 0      for each slot i
reveal · (1 − reveal) = 0                  reveal is 0 or 1

With reveal = 1 the five real nullifiers are public; with 0 they are forced to zero. It uses the same configuration, so it still fits K = 12, but it is a different circuit with its own verifying key (vk-reveal.bin). Circuit 1’s verifying key and parameters are byte-identical to before, so every existing proof still verifies. The test vouch_reveal covers reveal on and off, a forged revealed nullifier, and a flag of 2.

#Public inputs

Public inputs: carried in the file or derived by the verifiernf_signed#0 · carriedrk (x, y)#1–2 · carriedcmx_new#3 · carriedvan_comm#4 · carriedvote_round_id#5 · derivednc_root#6 · derivednf_imt_root#7 · derivedgov_null 1–5#8–12 · carrieddom#13 · derivedmin_ballots#14 · derivedreveal#15 · carriedrevealed_nf 1–5#16–20 · carriedCircuit 1 · 15 inputsCircuit 2 adds · 6 inputscarried in the .pofderived by the verifier
The verifier never trusts a field it can derive: the round id, the nullifier domain, both roots and the threshold are recomputed from the statement and the anchor table. Circuit 2 (exit certificates) adds six inputs.

The file carries only what the verifier cannot derive: nf_signed, rk, cmx_new, van_comm, gov_null_1..5 (nine 32-byte values; rk is one compressed point that fills two circuit inputs), and for circuit 2 reveal, revealed_nf_1..5. pof-zk::instance_for assembles the full instance from those, the statement and the anchor.

#What binds the file to the proof

What binds the file to the proofEnvelope headclaim · audience · epochbinding · circuitnc_height · heightnc_root · nf_rootissued · expires · revocationevery field except the evidencestatementblake2b · vouch-stmt-v1scope (v2)blake2b · vouch-scope-v2circuit ‖ audience ‖ epochpublic inputs9 carried · 15 for circuit 2signing messagestatement ‖ public inputsRedPallas signatureunder rk: spend authorityround_idv1: the statementv2: the scopeHalo2 circuitdom from round_idmin_ballots from claimv1v2and the proof is checked against them
The holder signs the statement and every public input under the key the circuit proves controls the notes. Edit any field after proving and the signature fails; in version 1 the circuit itself is also bound to the statement through round_id.
Text
statement  = blake2b-256[personal "vouch-stmt-v1"](head)
scope      = blake2b-256[personal "vouch-scope-v2"](circuit ‖ audience ‖ epoch_le)   (version 2)
round_id   = statement (v1) or scope (v2), bits 254..255 cleared, as a Pallas base element
dom        = Poseidon("governance authorization", round_id)
min_units  = claim.zatoshi / 12_500_000
message    = blake2b-256[personal "vouch-sig-v1"](statement ‖ public_inputs)
signature  = RedPallas SpendAuth over message, under rk
subject    = blake2b-256[personal "vouch-subject-v1"](statement)                      (the proof id)

#One note, one slot

A finding the verifier closes

The circuit checks each of the five slots on its own, so on its own it would accept one note placed in several slots and count its value more than once. Upstream, the vote chain refuses a repeated governance nullifier. Vouch has no chain, so pof-zk refuses any proof whose five tags are not all distinct, before Halo2 runs; a repeated note always produces the same tag. A real-proof test checks that a note counted five times is refused.

#Keys and parameters

FileWhat it isGuarded by
pof-zk/keys/params-k12.binThe IPA parameters for K = 12.embedded_vk_matches_keygen
pof-zk/keys/vk-delegation.binCircuit 1’s verifying key.embedded_vk_matches_keygen
pof-zk/keys/vk-reveal.binCircuit 2’s verifying key.embedded_vk_matches_keygen