You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Align delegation circuit with ZIP spec (dom, value=1, remove is_real)
Three changes to align with zcash/zips#1199:
1. Governance nullifier → alternate nullifier with explicit dom public
input (14th instance field). dom = Poseidon("governance authorization",
vote_round_id) derived out-of-circuit. In-circuit: Poseidon(nk, dom,
real_nf) with ConstantLength<3>.
2. Dummy signed note value 0 → 1 to match PCZT construction.
3. Replace is_note_real with v * (root - anchor) = 0 (Orchard standard
dummy note mechanism). Remove condition 15.
Copy file name to clipboardExpand all lines: voting-circuits/src/delegation/README.md
+15-33Lines changed: 15 additions & 33 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,20 +2,21 @@
2
2
3
3
A single circuit proving all 15 conditions of the delegation ZKP at K=14 (16,384 rows). The circuit handles the keystone note (conditions 1–8) and five per-note slots (conditions 9–15 ×5) in one proof.
4
4
5
-
**Public inputs:**13 field elements.
5
+
**Public inputs:**14 field elements.
6
6
**Per-note slots:** 5 (unused slots are padded with zero-value notes).
7
7
8
8
## Inputs
9
9
10
-
- Public (13 field elements)
10
+
- Public (14 field elements)
11
11
***nf_signed** (offset 0): the derived nullifier of the keystone note.
12
12
***rk** (offsets 1–2): the randomized public key for spend authorization (x, y coordinates).
13
13
***cmx_new** (offset 3): the extracted note commitment (`ExtractP(cm_new)`) of the output note.
14
14
***van_comm** (offset 4): the governance commitment — a Pallas base field element identifying the governance context.
***pos**: leaf position in the note commitment tree.
49
-
***is_note_real**: boolean flag — 1 for real notes, 0 for padded notes.
50
50
***is_internal**: boolean flag — 1 for internal (change) scope notes, 0 for external scope notes.
51
51
***imt_low**: the interval start (low bound of the bracketing leaf).
52
52
***imt_width**: the interval width (`high - low`, pre-computed during tree construction).
@@ -267,16 +267,16 @@ The `v` cell is a `NoteValue` inside NoteCommit. A separate `v_base` cell (as `p
267
267
268
268
## 10. Merkle Path Validity (x5)
269
269
270
-
Purpose: prove that the note's commitment exists in the note commitment tree, gated by `is_note_real`.
270
+
Purpose: prove that the note's commitment exists in the note commitment tree. Uses Orchard's standard dummy note mechanism: the check is gated by value (ZIP §Note Padding).
271
271
272
272
```
273
273
root = MerklePath(cmx, pos, path)
274
-
is_note_real * (root - nc_root) = 0
274
+
v * (root - nc_root) = 0
275
275
```
276
276
277
-
The `GadgetMerklePath` gadget computes the Merkle root from the leaf (`cmx`) and the 32-level authentication path using Sinsemilla hashing. The `q_per_note` custom gate then enforces that either the computed root equals the public `nc_root` anchor or `is_note_real = 0` (padded note — root mismatch is allowed).
277
+
The `GadgetMerklePath` gadget computes the Merkle root from the leaf (`cmx`) and the 32-level authentication path using Sinsemilla hashing. The `q_per_note` custom gate then enforces that either the computed root equals the public `nc_root` anchor or `v = 0` (dummy note — root mismatch is allowed).
278
278
279
-
For padded notes, the path can be all-zeros; the Merkle computation still runs but the root-check gate is gated off.
279
+
For dummy notes (v=0), the path can be all-zeros; the Merkle computation still runs but the root-check gate is gated off.
280
280
281
281
**Constructions:**`MerkleChip` (configs 1+2), `SinsemillaChip` (configs 1+2 via MerkleChip), `q_per_note`.
282
282
@@ -328,7 +328,7 @@ Purpose: prove the note's nullifier has NOT been spent, using a Poseidon-based I
328
328
-`left + right = current + sibling`
329
329
-`bool_check(pos_bit)`
330
330
331
-
3.**Root check**: The `q_per_note` gate constrains `imt_root = nf_imt_root` (the public input). Unlike the Merkle root check in condition 10, this is **not gated** by `is_note_real` — padded notes must also provide a valid IMT non-membership proof.
331
+
3.**Root check**: The `q_per_note` gate constrains `imt_root = nf_imt_root` (the public input). This is unconditional — dummy notes must also provide a valid IMT non-membership proof.
Purpose: derive a domain-separated governance nullifier that is published as a public input. This prevents double-voting without revealing the note's true Orchard nullifier.
347
+
Purpose: derive an alternate nullifier (ZIP §Alternate Nullifier Derivation) that is published as a public input. This prevents double-delegation without revealing the note's true Orchard nullifier.
Single Poseidon hash (`ConstantLength<4>`, 2 permutations at rate 2):
353
+
Single Poseidon hash (`ConstantLength<3>`, 2 permutations at rate 2):
354
354
-**nk** — the nullifier deriving key, making the result unforgeable.
355
-
-**domain_tag** — `"governance authorization"` encoded as a little-endian Pallas field element, assigned via `assign_constant` so the value is baked into the verification key. Separates from other nullifier domains.
356
-
-**vote_round_id** — binds to the voting round.
355
+
-**dom** — the nullifier domain (public input at offset 13), derived out-of-circuit as `Poseidon("governance authorization", vote_round_id)`. Scopes the alternate nullifier to this application instance and voting round.
357
356
-**real_nf** — the note's true nullifier from condition 12.
358
357
359
358
The result is constrained to the public input at offset `GOV_NULL_1..5`.
360
359
361
360
**Constructions:**`PoseidonChip`.
362
361
363
-
## 15. Padded-Note Zero-Value Enforcement (x5)
364
-
365
-
Purpose: ensure padded (unused) note slots contribute zero voting weight.
366
-
367
-
```
368
-
(1 - is_note_real) * v = 0
369
-
bool_check(is_note_real)
370
-
```
371
-
372
-
The `q_per_note` custom gate enforces:
373
-
1.`is_note_real` is boolean (0 or 1).
374
-
2. If `is_note_real = 0`, then `v = 0`. A padded note cannot carry value.
375
-
376
-
For real notes (`is_note_real = 1`), the constraint is trivially satisfied and `v` can be any value.
377
-
378
-
**Constructions:**`q_per_note`.
379
-
380
362
## FAQ
381
363
382
364
-**"Why is cm_signed witnessed as a Point but ak_P as a NonIdentityPoint?"** — ak_P being identity would be a degenerate key (any signature verifies). cm_signed being identity is cryptographically negligible and caught by the equality constraint with the recomputed commitment.
@@ -387,4 +369,4 @@ For real notes (`is_note_real = 1`), the constraint is trivially satisfied and `
387
369
388
370
-**"Why two Sinsemilla configs (and two NoteCommitChips)?"** — This mirrors the audited Orchard action circuit, which uses two Sinsemilla configs (one for spend-side NoteCommit, one for output-side NoteCommit) with column assignments `advices[..5]` and `advices[5..]`. Each `SinsemillaChip::configure` call creates its own selectors and gates, and each `NoteCommitChip::configure` creates decomposition/canonicity gates tied to the Sinsemilla config it receives — so two Sinsemilla configs require two NoteCommitChips. We replicate this exact layout so the delegation circuit inherits the audited chip wiring without modification. It may be possible to collapse to a single config (condition 9 already runs 5 NoteCommits on config 1 without conflict), but reusing the known-correct pattern avoids the need for a separate audit of the chip interaction.
389
371
390
-
-**"Why do padded notes use the real ivk?"** — Padded notes must pass condition 11 (`pk_d = [selected_ivk] * g_d`) using the same ivk (or ivk_internal) derived in condition 5. The builder creates padded notes with `fvk.address_at(...)` so their addresses are valid under the real ivk. This is safe because padded notes have `v = 0` (enforced by condition 15) and `is_note_real = 0` (so condition 10 skips the Merkle root check). They contribute nothing to the vote weight but their governance nullifiers are still published (condition 14), which is harmless — the consuming protocol can ignore nullifiers for zero-value notes or treat them as no-ops.
372
+
-**"Why do dummy notes use the real ivk?"** — Dummy notes must pass condition 11 (`pk_d = [selected_ivk] * g_d`) using the same ivk (or ivk_internal) derived in condition 5. The builder creates dummy notes with `fvk.address_at(...)` so their addresses are valid under the real ivk. This is safe because dummy notes have `v = 0` (so condition 10 skips the Merkle root check via `v * (root - anchor) = 0`). They contribute nothing to the vote weight but their alternate nullifiers are still published (condition 14), which is harmless — the consuming protocol can ignore nullifiers for zero-value notes or treat them as no-ops.
0 commit comments