Skip to main content

voting_circuits/vote_proof/
circuit.rs

1//! The Vote Proof circuit implementation (ZKP #2).
2//!
3//! Proves that a registered voter is casting a valid vote, without
4//! revealing which VAN they hold. Currently implements:
5//!
6//! - **Condition 1**: VAN Membership (Poseidon Merkle path, `constrain_instance`).
7//! - **Condition 2**: VAN Integrity (Poseidon hash).
8//! - **Condition 3**: Diversified Address Integrity (`vpk_pk_d = [ivk_v] * vpk_g_d` via CommitIvk).
9//! - **Condition 4**: Spend Authority — `r_vpk = vsk.ak + [alpha_v] * G` (fixed-base mul + point add, `constrain_instance`).
10//! - **Condition 5**: VAN Nullifier Integrity (nested Poseidon, `constrain_instance`).
11//! - **Condition 6**: Proposal Authority Decrement (custom bit-decomposition chip with a `(proposal_id, 2^proposal_id)` lookup; see `gadgets/authority_decrement.rs`).
12//! - **Condition 7**: New VAN Integrity (Poseidon hash, `constrain_instance`).
13//! - **Condition 8**: Shares Sum Correctness (AddChip, `constrain_equal`).
14//! - **Condition 9**: Shares Range (LookupRangeCheck, `[0, 2^30)`).
15//! - **Condition 10**: Shares Hash Integrity (Poseidon `ConstantLength<16>` over 16 blinded share commitments; output flows to condition 12).
16//! - **Condition 11**: Encryption Integrity (ECC variable-base mul, `constrain_equal`).
17//! - **Condition 12**: Vote Commitment Integrity (Poseidon `ConstantLength<5>`, `constrain_instance`).
18//!
19//! Conditions 1–4 and 5–12 are fully constrained in-circuit.
20//!
21//! ## Conditions overview
22//!
23//! VAN ownership and spending:
24//! - **Condition 1**: VAN Membership — Merkle path from `vote_authority_note_old`
25//!   to `vote_comm_tree_root`.
26//! - **Condition 2**: VAN Integrity — `vote_authority_note_old` is the two-layer
27//!   Poseidon hash (ZKP 1–compatible: core then finalize with rand). *(implemented)*
28//! - **Condition 3**: Diversified Address Integrity — `vpk_pk_d = [ivk_v] * vpk_g_d`
29//!   where `ivk_v = CommitIvk(ExtractP([vsk]*SpendAuthG), vsk.nk)`. *(implemented)*
30//! - **Condition 4**: Spend Authority — `r_vpk = vsk.ak + [alpha_v] * G`; enforced in-circuit (fixed-base mul + point add, `constrain_instance`).
31//! - **Condition 5**: VAN Nullifier Integrity — `van_nullifier` is correctly
32//!   derived from `vsk.nk`. *(implemented)*
33//!
34//! New VAN construction:
35//! - **Condition 6**: Proposal Authority Decrement — `proposal_authority_new =
36//!   proposal_authority_old - (1 << proposal_id)`, with bitmask range [0, 2^16). *(implemented)*
37//! - **Condition 7**: New VAN Integrity — same two-layer structure as condition 2
38//!   but with decremented authority. *(implemented)*
39//!
40//! Vote commitment construction:
41//! - **Condition 8**: Shares Sum Correctness — `sum(shares_1..16) = total_note_value`.
42//!   *(implemented)*
43//! - **Condition 9**: Shares Range — each `shares_j` in `[0, 2^30)`.
44//!   *(implemented)*
45//! - **Condition 10**: Shares Hash Integrity — `shares_hash = H(enc_share_1..16)`.
46//!   `shares_hash` is an internal wire, not a public instance. *(implemented)*
47//! - **Condition 11**: Encryption Integrity — each `enc_share_i = ElGamal(shares_i, r_i, ea_pk)`.
48//!   *(implemented)*
49//! - **Condition 12**: Vote Commitment Integrity — `vote_commitment = H(DOMAIN_VC, voting_round_id,
50//!   shares_hash, proposal_id, vote_decision)`, and this terminal commitment is
51//!   the public binding for condition 10. *(implemented)*
52
53use std::vec::Vec;
54
55use halo2_gadgets::{
56    ecc::{
57        chip::{EccChip, EccConfig},
58        NonIdentityPoint, ScalarFixed,
59    },
60    poseidon::{
61        primitives::{self as poseidon, ConstantLength},
62        Hash as PoseidonHash, Pow5Chip as PoseidonChip, Pow5Config as PoseidonConfig,
63    },
64    sinsemilla::chip::{SinsemillaChip, SinsemillaConfig},
65    utilities::lookup_range_check::{LookupRangeCheck, LookupRangeCheckConfig},
66};
67use halo2_proofs::{
68    circuit::{floor_planner, AssignedCell, Layouter, Value},
69    plonk::{self, Advice, Column, ConstraintSystem, Fixed, Instance as InstanceColumn},
70};
71use orchard::{
72    circuit::{
73        commit_ivk::{CommitIvkChip, CommitIvkConfig},
74        gadget::{
75            add_chip::{AddChip, AddConfig},
76            assign_free_advice, AddInstruction,
77        },
78    },
79    constants::{OrchardCommitDomains, OrchardFixedBases, OrchardHashDomains},
80};
81use pasta_curves::{pallas, vesta};
82
83use super::gadgets::authority_decrement::{AuthorityDecrementChip, AuthorityDecrementConfig};
84use crate::{
85    domain_tags,
86    gadgets::{
87        address_ownership::{prove_address_ownership, spend_auth_g_mul},
88        elgamal::{prove_elgamal_encryptions, EaPkInstanceLoc},
89        nonzero::NonZeroConfig,
90        poseidon_merkle::{synthesize_poseidon_merkle_path, MerkleSwapGate},
91        van_integrity, vote_commitment,
92    },
93    params::VOTE_COMM_TREE_DEPTH,
94    shares_hash::compute_shares_hash_in_circuit,
95};
96
97// ================================================================
98// Constants
99// ================================================================
100
101/// Circuit size (2^K rows).
102///
103/// K=13 (8,192 rows). `CircuitCost::measure` reports a floor-planner
104/// high-water mark of **7,945 rows** (97.0% of 8,192). The `V1` floor planner
105/// packs non-overlapping regions into the same row range across different
106/// columns, so the high-water mark is much lower than a naive sum-of-heights
107/// estimate.
108///
109/// Key contributors (rough per-region heights, not per-column sums):
110/// - 24-level Merkle path: 24 Poseidon regions stacked sequentially — the
111///   tallest single stack in the circuit.
112/// - ECC fixed- and variable-base multiplications packed alongside the
113///   Poseidon regions in non-overlapping columns.
114/// - 10-bit Sinsemilla/range-check lookup table: 1,024 fixed rows.
115///
116/// Run the `row_budget` diagnostic to re-measure after circuit changes:
117///   `cargo test vote_proof::circuit::tests::row_budget -- --nocapture --ignored --test-threads=1`
118pub const K: u32 = 13;
119
120pub(super) use van_integrity::DOMAIN_VAN;
121pub(super) use vote_commitment::DOMAIN_VC;
122
123/// Maximum proposal_id bit index (exclusive upper bound). `proposal_id` is in `[1, MAX_PROPOSAL_ID)`,
124/// i.e. valid values are 1–15. Bit 0 is permanently reserved as the sentinel/unset value and is
125/// rejected by the non-zero gate in `AuthorityDecrementChip` (`q_cond_6`). This means a voting
126/// round supports at most 15 proposals, not 16.
127/// Spec: "The number of proposals for a polling session must be <= 16."
128///
129/// # Indexing Convention
130///
131/// `proposal_id` is **1-indexed** throughout the entire stack:
132///
133/// - **On-chain (`MsgCreateVotingSession`)**: proposals carry `Id = 1, 2, …, N`.
134/// - **On-chain (`ValidateProposalId`)**: rejects `proposal_id < 1`.
135/// - **Circuit (this file)**: `proposal_id` serves as the bit-position in the
136///   16-bit `proposal_authority` bitmask. The `proposal_id != 0` gate ensures
137///   bit 0 is never selected, so the effective bit range is `[1, 15]`.
138/// - **Client (`zcash_voting::zkp2`)**: validates `proposal_id` in `[1, 15]`
139///   before building the proof.
140///
141/// Bit 0 of `proposal_authority` is always set (initial value `0xFFFF`) and
142/// never decremented, acting as a structural invariant rather than a usable slot.
143pub(super) const MAX_PROPOSAL_ID: usize = 16;
144
145// ================================================================
146// Public input offsets (11 field elements).
147// ================================================================
148
149/// Public input offset for the VAN nullifier (prevents double-vote).
150const VAN_NULLIFIER_PUBLIC_OFFSET: usize = 0;
151/// Public input offset for the randomized voting public key (condition 4: Spend Authority).
152/// x-coordinate of r_vpk = vsk.ak + [alpha_v] * G.
153const R_VPK_X_PUBLIC_OFFSET: usize = 1;
154/// Public input offset for r_vpk y-coordinate.
155const R_VPK_Y_PUBLIC_OFFSET: usize = 2;
156/// Public input offset for the new VAN commitment (with decremented authority).
157const VOTE_AUTHORITY_NOTE_NEW_PUBLIC_OFFSET: usize = 3;
158/// Public input offset for the vote commitment hash.
159const VOTE_COMMITMENT_PUBLIC_OFFSET: usize = 4;
160/// Public input offset for the vote commitment tree root.
161const VOTE_COMM_TREE_ROOT_PUBLIC_OFFSET: usize = 5;
162/// Public input offset for the tree anchor height.
163// The circuit does not constrain this slot to a witness cell. It is
164// transcript-bound metadata whose meaning is authenticated by the verifier's
165// caller. In the chain path, the ante handler looks up the commitment root at
166// msg.VoteCommTreeAnchorHeight and passes that root as VOTE_COMM_TREE_ROOT_PUBLIC_OFFSET,
167// which the circuit does constrain. This keeps the binding between height and
168// root in the chain state lookup rather than in this proof.
169#[allow(dead_code)]
170const VOTE_COMM_TREE_ANCHOR_HEIGHT_PUBLIC_OFFSET: usize = 6;
171/// Public input offset for the proposal identifier.
172///
173/// In-circuit constraint: `proposal_id` is in `[1, MAX_PROPOSAL_ID)` via the
174/// authority-decrement lookup. The caller must additionally verify that this
175/// ID is in the active proposal set for `voting_round_id`.
176const PROPOSAL_ID_PUBLIC_OFFSET: usize = 7;
177/// Public input offset for the governance voting round identifier.
178///
179/// The circuit binds this value into the VAN nullifier, new VAN, and vote
180/// commitment, but the caller must authenticate it from the active round's
181/// governance announcement.
182const VOTING_ROUND_ID_PUBLIC_OFFSET: usize = 8;
183/// Public input offset for the election authority public key x-coordinate.
184const EA_PK_X_PUBLIC_OFFSET: usize = 9;
185/// Public input offset for the election authority public key y-coordinate.
186const EA_PK_Y_PUBLIC_OFFSET: usize = 10;
187
188// ================================================================
189// Out-of-circuit helpers
190// ================================================================
191
192pub(super) use van_integrity::van_integrity_hash;
193pub(super) use vote_commitment::vote_commitment_hash;
194
195/// Returns the domain separator for the VAN nullifier inner hash.
196///
197/// The tag is defined in [`crate::domain_tags`], the central registry for
198/// domain-separation constants and encoding rules.
199pub(super) fn domain_van_nullifier() -> pallas::Base {
200    domain_tags::vote_authority_spend()
201}
202
203/// Out-of-circuit VAN nullifier hash (condition 5).
204///
205/// ```text
206/// van_nullifier = Poseidon(vsk_nk, domain_tag, voting_round_id, vote_authority_note_old)
207/// ```
208///
209/// Single `ConstantLength<4>` call (2 permutations at rate=2).
210/// Used by the builder and tests to compute the expected VAN nullifier.
211pub(super) fn van_nullifier_hash(
212    vsk_nk: pallas::Base,
213    voting_round_id: pallas::Base,
214    vote_authority_note_old: pallas::Base,
215) -> pallas::Base {
216    poseidon::Hash::<_, poseidon::P128Pow5T3, ConstantLength<4>, 3, 2>::init().hash([
217        vsk_nk,
218        domain_van_nullifier(),
219        voting_round_id,
220        vote_authority_note_old,
221    ])
222}
223
224// ================================================================
225// Config
226// ================================================================
227
228/// Configuration for the Vote Proof circuit.
229///
230/// Holds chip configs for Poseidon (conditions 1, 2, 5, 7, 10, 12), AddChip
231/// (condition 8), LookupRangeCheck (condition 9), ECC (conditions 3, 4, 11),
232/// the Merkle swap gate (condition 1), and the custom
233/// `AuthorityDecrementChip` (condition 6; see `gadgets/authority_decrement.rs` —
234/// uses neither AddChip nor LookupRangeCheck).
235#[derive(Clone, Debug)]
236pub struct Config {
237    /// Public input column (11 field elements).
238    primary: Column<InstanceColumn>,
239    /// 10 advice columns for private witness data.
240    ///
241    /// Column layout follows the delegation circuit for consistency:
242    /// - `advices[0..5]`: general witness assignment + Merkle swap gate.
243    /// - `advices[5]`: Poseidon partial S-box column.
244    /// - `advices[6..9]`: Poseidon state columns + AddChip output.
245    /// - `advices[9]`: range check running sum.
246    advices: [Column<Advice>; 10],
247    /// Poseidon hash chip configuration.
248    ///
249    /// P128Pow5T3 with width 3, rate 2. Used for VAN integrity (condition 2),
250    /// VAN nullifier (condition 5), new VAN integrity (condition 7),
251    /// vote commitment Merkle path (condition 1), and vote commitment
252    /// integrity (conditions 10, 12).
253    poseidon_config: PoseidonConfig<pallas::Base, 3, 2>,
254    /// AddChip: constrains `a + b = c` on a single row.
255    ///
256    /// Uses advices[7] (a), advices[8] (b), advices[6] (c), matching
257    /// the delegation circuit's column assignment.
258    /// Used in condition 8 (shares sum correctness). Condition 6
259    /// (proposal authority decrement) uses the dedicated
260    /// `AuthorityDecrementChip` instead — it does not call `AddChip`.
261    add_config: AddConfig,
262    /// ECC chip configuration (condition 3: diversified address integrity, condition 11: El Gamal).
263    ///
264    /// Condition 3 proves `vpk_pk_d = [ivk_v] * vpk_g_d` via the CommitIvk chain:
265    /// `[vsk] * SpendAuthG → ak → CommitIvk(ExtractP(ak), nk, rivk_v) → ivk_v → [ivk_v] * vpk_g_d`.
266    /// Shares advice and fixed columns with Poseidon per delegation layout.
267    ecc_config: EccConfig<OrchardFixedBases>,
268    /// Sinsemilla chip configuration (condition 3: CommitIvk requires Sinsemilla).
269    ///
270    /// Uses advices[0..5] for Sinsemilla message hashing, advices[6] for
271    /// witnessing message pieces, and lagrange_coeffs[0] for the fixed y_Q column.
272    /// Also loads the 10-bit lookup table used by LookupRangeCheckConfig.
273    sinsemilla_config:
274        SinsemillaConfig<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases>,
275    /// CommitIvk chip configuration (condition 3: canonicity checks on ak || nk).
276    ///
277    /// Provides the custom gate and decomposition logic for the
278    /// Sinsemilla-based `CommitIvk` commitment.
279    commit_ivk_config: CommitIvkConfig,
280    /// 10-bit lookup range check configuration.
281    ///
282    /// Uses advices[9] as the running-sum column. Each word is 10 bits,
283    /// so `num_words` × 10 gives the total bit-width checked.
284    /// Used in condition 6 to ensure authority values and diff are in [0, 2^16)
285    /// (16-bit bitmask), and condition 9 to ensure each share is in `[0, 2^30)`.
286    range_check: LookupRangeCheckConfig<pallas::Base, 10>,
287    /// Merkle conditional swap gate (condition 1).
288    ///
289    /// At each of the 24 Merkle tree levels, conditionally swaps
290    /// (current, sibling) into (left, right) based on the position bit.
291    /// Uses advices[0..5]: pos_bit, current, sibling, left, right.
292    merkle_swap: MerkleSwapGate,
293    /// Configuration for condition 6 (Proposal Authority Decrement).
294    authority_decrement: AuthorityDecrementConfig,
295    /// Shared inverse-witness checks for zero randomizer/randomness rejection.
296    nonzero: NonZeroConfig,
297}
298
299impl Config {
300    /// Constructs a Poseidon chip from this configuration.
301    ///
302    /// Width 3 (P128Pow5T3 state size), rate 2 (absorbs 2 field elements
303    /// per permutation — halves the number of rounds vs rate 1).
304    fn poseidon_chip(&self) -> PoseidonChip<pallas::Base, 3, 2> {
305        PoseidonChip::construct(self.poseidon_config.clone())
306    }
307
308    /// Constructs an AddChip for field element addition (`c = a + b`).
309    fn add_chip(&self) -> AddChip {
310        AddChip::construct(self.add_config.clone())
311    }
312
313    /// Constructs an ECC chip for curve operations (conditions 3, 11).
314    fn ecc_chip(&self) -> EccChip<OrchardFixedBases> {
315        EccChip::construct(self.ecc_config.clone())
316    }
317
318    /// Constructs a Sinsemilla chip (condition 3: CommitIvk).
319    fn sinsemilla_chip(
320        &self,
321    ) -> SinsemillaChip<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases> {
322        SinsemillaChip::construct(self.sinsemilla_config.clone())
323    }
324
325    /// Constructs a CommitIvk chip for canonicity checks (condition 3).
326    fn commit_ivk_chip(&self) -> CommitIvkChip {
327        CommitIvkChip::construct(self.commit_ivk_config.clone())
328    }
329
330    /// Returns the range check configuration (10-bit words).
331    fn range_check_config(&self) -> LookupRangeCheckConfig<pallas::Base, 10> {
332        self.range_check
333    }
334}
335
336// ================================================================
337// Circuit
338// ================================================================
339
340/// The Vote Proof circuit (ZKP #2).
341///
342/// Proves that a registered voter is casting a valid vote, without
343/// revealing which VAN they hold. Contains witness fields and constraint logic
344/// for all 12 conditions.
345///
346/// Condition 4 constrains `r_vpk` in-circuit. The vote signature is verified
347/// out-of-circuit under that constrained key.
348#[derive(Clone, Debug, Default)]
349pub struct Circuit {
350    // === VAN ownership and spending (conditions 1–5) ===
351
352    // Condition 1 (VAN Membership): Poseidon-based Merkle path from
353    // vote_authority_note_old to vote_comm_tree_root.
354    /// Merkle authentication path (sibling hashes at each tree level).
355    pub(super) vote_comm_tree_path: Value<[pallas::Base; VOTE_COMM_TREE_DEPTH]>,
356    /// Leaf position in the vote commitment tree.
357    pub(super) vote_comm_tree_position: Value<u32>,
358
359    // Condition 2 (VAN Integrity): two-layer hash matching ZKP 1 (delegation):
360    // van_comm_core = Poseidon(DOMAIN_VAN, vpk_g_d.x, vpk_pk_d.x, total_note_value,
361    //                          voting_round_id, proposal_authority_old);
362    // vote_authority_note_old = Poseidon(van_comm_core, van_comm_rand).
363    //
364    // Condition 3 (Diversified Address Integrity): vpk_pk_d = [ivk_v] * vpk_g_d
365    // where ivk_v = CommitIvk(ExtractP([vsk]*SpendAuthG), vsk.nk, rivk_v).
366    // Full affine points are needed for condition 3's ECC operations;
367    // x-coordinates are extracted in-circuit for Poseidon hashing (conditions 2, 7).
368    /// Voting public key — diversified base point (from DiversifyHash(d)).
369    /// This is the vpk_g_d component of the voting hotkey address.
370    /// Condition 3 performs `[ivk_v] * vpk_g_d` to derive vpk_pk_d.
371    pub(super) vpk_g_d: Value<pallas::Affine>,
372    /// Voting public key — diversified transmission key (pk_d = [ivk_v] * g_d).
373    /// This is the vpk_pk_d component of the voting hotkey address.
374    /// Condition 3 (Diversified Address Integrity) constrains this to equal `[ivk_v] * vpk_g_d`.
375    pub(super) vpk_pk_d: Value<pallas::Affine>,
376    /// The voter's total delegated weight, denominated in ballots
377    /// (1 ballot = 0.125 ZEC; converted from zatoshi by ZKP #1 condition 8 —
378    /// see the delegation README §8 for the proven relation).
379    pub(super) total_note_value: Value<pallas::Base>,
380    // Condition 6:
381    /// Remaining proposal authority bitmask in the old VAN.
382    pub(super) proposal_authority_old: Value<pallas::Base>,
383    /// Blinding randomness for the VAN commitment.
384    pub(super) van_comm_rand: Value<pallas::Base>,
385    /// The old VAN commitment (Poseidon hash output). Used as the Merkle
386    /// leaf in condition 1 and constrained to equal the derived hash here.
387    pub(super) vote_authority_note_old: Value<pallas::Base>,
388
389    // Condition 3 (Diversified Address Integrity): prover controls the VAN address.
390    // vpk_pk_d = [ivk_v] * vpk_g_d
391    //   where ivk_v = CommitIvk_rivk_v(ExtractP([vsk]*SpendAuthG), vsk.nk)
392    /// Voting spending key (scalar for ECC multiplication).
393    /// Used in condition 3 for `[vsk] * SpendAuthG`.
394    pub(super) vsk: Value<pallas::Scalar>,
395    /// CommitIvk randomness for the ivk_v derivation (condition 3).
396    /// Used as the blinding scalar in `CommitIvk(ak, nk, rivk_v)`.
397    pub(super) rivk_v: Value<pallas::Scalar>,
398    /// Spend auth randomizer for condition 4: r_vpk = vsk.ak + [alpha_v] * G.
399    pub(super) alpha_v: Value<pallas::Scalar>,
400
401    // Condition 5 (VAN Nullifier Integrity): nullifier deriving key.
402    // Also used in condition 3 as the nk input to CommitIvk.
403    /// Nullifier deriving key derived from vsk.
404    pub(super) vsk_nk: Value<pallas::Base>,
405
406    // Condition 6 (Proposal Authority Decrement): one_shifted = 2^proposal_id.
407    /// `2^proposal_id`, supplied as a private witness and constrained by a lookup.
408    ///
409    /// Field arithmetic cannot express variable-exponent exponentiation as a
410    /// polynomial gate, so the prover witnesses `one_shifted` directly. The lookup
411    /// table `(0,1), (1,2), ..., (15,32768)` then proves `one_shifted == 2^proposal_id`.
412    /// The bit-decomposition region uses this value to compute
413    /// `proposal_authority_new = proposal_authority_old - one_shifted`.
414    pub(super) one_shifted: Value<pallas::Base>,
415
416    // === Vote commitment construction (conditions 8–12) ===
417
418    // Condition 8 (Shares Sum): sum(shares_1..16) = total_note_value.
419    // Condition 9 (Shares Range): each share in [0, 2^30).
420    /// Voting share vector (16 random shares that sum to total_note_value).
421    /// The decomposition is chosen by the prover for amount privacy: the
422    /// on-chain El Gamal ciphertexts reveal no weight fingerprint.
423    pub(super) shares: [Value<pallas::Base>; 16],
424
425    // Condition 10 (Shares Hash Integrity): El Gamal ciphertext coordinates.
426    // These are the coordinates of the curve points comprising each
427    // El Gamal ciphertext. Condition 11 constrains these to be correct
428    // encryptions; condition 10 hashes them (including y-coordinates to
429    // prevent ciphertext sign-malleability).
430    /// X-coordinates of C1_i = r_i * G for each share (via ExtractP).
431    pub(super) enc_share_c1_x: [Value<pallas::Base>; 16],
432    /// X-coordinates of C2_i = shares_i * G + r_i * ea_pk for each share (via ExtractP).
433    pub(super) enc_share_c2_x: [Value<pallas::Base>; 16],
434    /// Y-coordinates of C1_i (bound to the exact curve point, preventing sign-malleability).
435    pub(super) enc_share_c1_y: [Value<pallas::Base>; 16],
436    /// Y-coordinates of C2_i (bound to the exact curve point, preventing sign-malleability).
437    pub(super) enc_share_c2_y: [Value<pallas::Base>; 16],
438
439    // Condition 10 (Shares Hash Integrity): per-share blind factors for blinded commitments.
440    /// Random blind factors: share_comm_i = Poseidon(blind_i, c1_i_x, c2_i_x, c1_i_y, c2_i_y).
441    pub(super) share_blinds: [Value<pallas::Base>; 16],
442
443    // Condition 11 (Encryption Integrity): El Gamal randomness and public key.
444    /// El Gamal encryption randomness for each share (base field element,
445    /// converted to scalar via ScalarVar::from_base in-circuit).
446    pub(super) share_randomness: [Value<pallas::Base>; 16],
447    /// Election authority public key (Pallas curve point).
448    /// The El Gamal encryption key — published as a round parameter.
449    /// Both coordinates are public inputs (EA_PK_X_PUBLIC_OFFSET, EA_PK_Y_PUBLIC_OFFSET).
450    /// The caller must authenticate this key against the governance
451    /// announcement; the circuit only binds encryption to the supplied key.
452    pub(super) ea_pk: Value<pallas::Affine>,
453
454    // Condition 12 (Vote Commitment Integrity): vote decision.
455    /// The voter's choice (hidden inside the vote commitment).
456    pub(super) vote_decision: Value<pallas::Base>,
457}
458
459impl Circuit {
460    /// Creates a circuit with conditions 1–3 and 5–7 witnesses populated.
461    ///
462    /// All other witness fields are set to `Value::unknown()`.
463    /// - Condition 1 uses `vote_authority_note_old` as the Merkle leaf,
464    ///   with `vote_comm_tree_path` and `vote_comm_tree_position` for
465    ///   the authentication path.
466    /// - Condition 2 binds `vote_authority_note_old` to the Poseidon hash
467    ///   of its components (using x-coordinates extracted from vpk_g_d, vpk_pk_d).
468    /// - Condition 3 proves diversified address integrity via CommitIvk chain:
469    ///   `[vsk] * SpendAuthG → ak → CommitIvk(ak, nk, rivk_v) → ivk_v → [ivk_v] * vpk_g_d = vpk_pk_d`.
470    /// - Condition 5 reuses `vote_authority_note_old` and `voting_round_id`.
471    /// - Condition 6 derives `proposal_authority_new` from
472    ///   `proposal_authority_old`.
473    /// - Condition 7 reuses all condition 2 witnesses except
474    ///   `proposal_authority_old`, which is replaced by the
475    ///   in-circuit `proposal_authority_new` from condition 6.
476    pub(super) fn with_van_witnesses(
477        vote_comm_tree_path: Value<[pallas::Base; VOTE_COMM_TREE_DEPTH]>,
478        vote_comm_tree_position: Value<u32>,
479        vpk_g_d: Value<pallas::Affine>,
480        vpk_pk_d: Value<pallas::Affine>,
481        total_note_value: Value<pallas::Base>,
482        proposal_authority_old: Value<pallas::Base>,
483        van_comm_rand: Value<pallas::Base>,
484        vote_authority_note_old: Value<pallas::Base>,
485        vsk: Value<pallas::Scalar>,
486        rivk_v: Value<pallas::Scalar>,
487        vsk_nk: Value<pallas::Base>,
488        alpha_v: Value<pallas::Scalar>,
489    ) -> Self {
490        Circuit {
491            vote_comm_tree_path,
492            vote_comm_tree_position,
493            vpk_g_d,
494            vpk_pk_d,
495            total_note_value,
496            proposal_authority_old,
497            van_comm_rand,
498            vote_authority_note_old,
499            vsk,
500            rivk_v,
501            alpha_v,
502            vsk_nk,
503            ..Default::default()
504        }
505    }
506}
507
508/// In-circuit Poseidon hash for one share commitment: `Poseidon(blind, c1_x, c2_x, c1_y, c2_y)`.
509///
510/// Uses the same parameters as the out-of-circuit
511/// [`crate::shares_hash::share_commitment`] (P128Pow5T3, ConstantLength<5>,
512/// width 3, rate 2) so that native and in-circuit hashes match.
513impl plonk::Circuit<pallas::Base> for Circuit {
514    type Config = Config;
515    type FloorPlanner = floor_planner::V1;
516
517    fn without_witnesses(&self) -> Self {
518        Self::default()
519    }
520
521    fn configure(meta: &mut ConstraintSystem<pallas::Base>) -> Self::Config {
522        // 10 advice columns, matching the delegation circuit layout so the two
523        // circuits share the same column assignment and chip configurations.
524        // The count is driven by the ECC chip, which is the largest consumer
525        // and requires all 10 columns for its internal scalar-multiplication
526        // gates.  The remaining chips tile within that same 10-column window:
527        //
528        //   advices[0..5]  — general witness assignment, Sinsemilla pair 1
529        //                    message columns, and the Merkle swap gate
530        //                    (pos_bit / current / sibling / left / right).
531        //   advices[5]     — Poseidon partial S-box column; also the start of
532        //                    Sinsemilla pair 2 main columns (advices[5..10]).
533        //   advices[6..9]  — Poseidon width-3 state columns; AddChip uses these
534        //                    same three columns (a=advices[7], b=advices[8],
535        //                    c=advices[6]).
536        //   advices[9]     — LookupRangeCheck running-sum column.
537        let advices: [Column<Advice>; 10] = core::array::from_fn(|_| meta.advice_column());
538        for col in &advices {
539            meta.enable_equality(*col);
540        }
541
542        // Instance column for public inputs.
543        let primary = meta.instance_column();
544        meta.enable_equality(primary);
545
546        // 8 fixed columns shared between ECC and Poseidon chips.
547        // Indices 0–1: Lagrange coefficients (ECC chip only).
548        // Indices 2–4: Poseidon round constants A (rc_a).
549        // Indices 5–7: Poseidon round constants B (rc_b).
550        let lagrange_coeffs: [Column<Fixed>; 8] = core::array::from_fn(|_| meta.fixed_column());
551        let rc_a = lagrange_coeffs[2..5].try_into().unwrap();
552        let rc_b = lagrange_coeffs[5..8].try_into().unwrap();
553
554        // Dedicated constants column, separate from the Lagrange coefficient
555        // columns used by the ECC chip. This prevents collisions between
556        // the ECC chip's fixed-base scalar multiplication tables and the
557        // constant-zero cells created by strict range checks.
558        let constants = meta.fixed_column();
559        meta.enable_constant(constants);
560
561        // AddChip: constrains `a + b = c` in a single row.
562        // Column assignment matches the delegation circuit:
563        //   a = advices[7], b = advices[8], c = advices[6].
564        let add_config = AddChip::configure(meta, advices[7], advices[8], advices[6]);
565
566        // Lookup table columns for Sinsemilla (3 columns) and range checks.
567        // The first column (table_idx) is shared between Sinsemilla and
568        // LookupRangeCheckConfig. SinsemillaChip::load populates all three
569        // during synthesis (replacing the manual table loading).
570        let table_idx = meta.lookup_table_column();
571        let lookup = (
572            table_idx,
573            meta.lookup_table_column(),
574            meta.lookup_table_column(),
575        );
576
577        // Range check configuration: 10-bit lookup words in advices[9].
578        let range_check = LookupRangeCheckConfig::configure(meta, advices[9], table_idx);
579
580        // ECC chip: fixed- and variable-base scalar multiplication for
581        // condition 3 (diversified address integrity via CommitIvk chain) and condition 11
582        // (El Gamal encryption integrity).
583        // Shares columns with Poseidon per delegation circuit layout.
584        let ecc_config =
585            EccChip::<OrchardFixedBases>::configure(meta, advices, lagrange_coeffs, range_check);
586
587        // Sinsemilla chip: required by CommitIvk for condition 3.
588        // Uses advices[0..5] for Sinsemilla message hashing, advices[6] for
589        // witnessing message pieces, and lagrange_coeffs[0] for the fixed
590        // y_Q column. Shares the lookup table with LookupRangeCheckConfig.
591        let sinsemilla_config = SinsemillaChip::configure(
592            meta,
593            advices[..5].try_into().unwrap(),
594            advices[6],
595            lagrange_coeffs[0],
596            lookup,
597            range_check,
598            false,
599        );
600
601        // CommitIvk chip: canonicity checks on the ak || nk decomposition
602        // inside the CommitIvk Sinsemilla commitment (condition 3).
603        let commit_ivk_config = CommitIvkChip::configure(meta, advices);
604
605        // Poseidon chip: P128Pow5T3 with width 3, rate 2.
606        // State columns: advices[6..9] (3 columns for the width-3 state).
607        // Partial S-box column: advices[5].
608        // Round constants: lagrange_coeffs[2..5] (rc_a), [5..8] (rc_b).
609        let poseidon_config = PoseidonChip::configure::<poseidon::P128Pow5T3>(
610            meta,
611            advices[6..9].try_into().unwrap(),
612            advices[5],
613            rc_a,
614            rc_b,
615        );
616
617        // Merkle conditional swap gate (condition 1).
618        let merkle_swap = MerkleSwapGate::configure(
619            meta,
620            [advices[0], advices[1], advices[2], advices[3], advices[4]],
621        );
622
623        // Condition 6: Proposal Authority Decrement.
624        let authority_decrement = AuthorityDecrementChip::configure(meta, advices);
625        let nonzero = NonZeroConfig::configure(meta, [advices[0], advices[1]]);
626
627        Config {
628            primary,
629            advices,
630            poseidon_config,
631            add_config,
632            ecc_config,
633            sinsemilla_config,
634            commit_ivk_config,
635            range_check,
636            merkle_swap,
637            authority_decrement,
638            nonzero,
639        }
640    }
641
642    #[allow(non_snake_case)]
643    fn synthesize(
644        &self,
645        config: Self::Config,
646        mut layouter: impl Layouter<pallas::Base>,
647    ) -> Result<(), plonk::Error> {
648        // ---------------------------------------------------------------
649        // Load the Sinsemilla generator lookup table.
650        //
651        // Populates the 10-bit lookup table and Sinsemilla generator
652        // points. Required by CommitIvk (condition 3), and also provides
653        // the range check table used by conditions 5 and 8.
654        // ---------------------------------------------------------------
655        SinsemillaChip::load(config.sinsemilla_config.clone(), &mut layouter)?;
656
657        // Load (proposal_id, 2^proposal_id) lookup table for condition 6.
658        AuthorityDecrementChip::load_table(&config.authority_decrement, &mut layouter)?;
659
660        // Construct the ECC chip (used in conditions 3 and 10).
661        let ecc_chip = config.ecc_chip();
662
663        // ---------------------------------------------------------------
664        // Witness assignment for condition 2.
665        // ---------------------------------------------------------------
666
667        // Copy voting_round_id from the instance column into an advice cell.
668        // This creates an equality constraint between the advice cell and the
669        // instance at offset VOTING_ROUND_ID_PUBLIC_OFFSET, ensuring the in-circuit value
670        // matches the public input.
671        let voting_round_id = layouter.assign_region(
672            || "copy voting_round_id from instance",
673            |mut region| {
674                region.assign_advice_from_instance(
675                    || "voting_round_id",
676                    config.primary,
677                    VOTING_ROUND_ID_PUBLIC_OFFSET,
678                    config.advices[0],
679                    0,
680                )
681            },
682        )?;
683        // Clone for condition 12 (vote commitment integrity) before
684        // condition 2 consumes the original via van_integrity_poseidon.
685        let voting_round_id_cond12 = voting_round_id.clone();
686
687        // Witness vpk_g_d as a full non-identity curve point (condition 3 needs
688        // the point for variable-base ECC mul; conditions 2/6 need the x-coordinate
689        // for Poseidon hashing).
690        let vpk_g_d_point = NonIdentityPoint::new(
691            ecc_chip.clone(),
692            layouter.namespace(|| "witness vpk_g_d"),
693            self.vpk_g_d.map(|p| p),
694        )?;
695        let vpk_g_d = vpk_g_d_point.extract_p().inner().clone();
696
697        // Witness vpk_pk_d as a full non-identity curve point (condition 3
698        // constrains the derived point to equal this; conditions 2/6 use x-coordinate).
699        let vpk_pk_d_point = NonIdentityPoint::new(
700            ecc_chip.clone(),
701            layouter.namespace(|| "witness vpk_pk_d"),
702            self.vpk_pk_d.map(|p| p),
703        )?;
704        let vpk_pk_d = vpk_pk_d_point.extract_p().inner().clone();
705
706        let total_note_value = assign_free_advice(
707            layouter.namespace(|| "witness total_note_value"),
708            config.advices[0],
709            self.total_note_value,
710        )?;
711
712        let proposal_authority_old = assign_free_advice(
713            layouter.namespace(|| "witness proposal_authority_old"),
714            config.advices[0],
715            self.proposal_authority_old,
716        )?;
717
718        let van_comm_rand = assign_free_advice(
719            layouter.namespace(|| "witness van_comm_rand"),
720            config.advices[0],
721            self.van_comm_rand,
722        )?;
723
724        let vote_authority_note_old = assign_free_advice(
725            layouter.namespace(|| "witness vote_authority_note_old"),
726            config.advices[0],
727            self.vote_authority_note_old,
728        )?;
729
730        // DOMAIN_VAN — constant-constrained so the value is baked into the
731        // verification key and cannot be altered by a malicious prover.
732        let domain_van = layouter.assign_region(
733            || "DOMAIN_VAN constant",
734            |mut region| {
735                region.assign_advice_from_constant(
736                    || "domain_van",
737                    config.advices[0],
738                    0,
739                    pallas::Base::from(DOMAIN_VAN),
740                )
741            },
742        )?;
743
744        // ---------------------------------------------------------------
745        // Witness assignment for conditions 3 and 4.
746        //
747        // vsk_nk is shared between condition 3 (CommitIvk input) and
748        // condition 5 (VAN nullifier). Witnessed here so it's available
749        // for condition 3 which runs before condition 5.
750        // ---------------------------------------------------------------
751
752        // Private witness: nullifier deriving key (shared by conditions 3, 4).
753        let vsk_nk = assign_free_advice(
754            layouter.namespace(|| "witness vsk_nk"),
755            config.advices[0],
756            self.vsk_nk,
757        )?;
758
759        // Clone cells that are consumed by condition 2's Poseidon hash but
760        // reused in later conditions:
761        // - vote_authority_note_old: also used in condition 1 (Merkle leaf).
762        // - voting_round_id: also used in condition 5 (VAN nullifier).
763        // - vpk_g_d, vpk_pk_d, total_note_value, voting_round_id,
764        //   van_comm_rand, domain_van: also used in condition 7 (new VAN integrity).
765        // - total_note_value: also used in condition 8 (shares sum check).
766        // - vsk_nk: also used in condition 5 (VAN nullifier).
767        let vote_authority_note_old_cond1 = vote_authority_note_old.clone();
768        let voting_round_id_cond4 = voting_round_id.clone();
769        let domain_van_cond6 = domain_van.clone();
770        let vpk_g_d_cond6 = vpk_g_d.clone();
771        let vpk_pk_d_cond6 = vpk_pk_d.clone();
772        let total_note_value_cond6 = total_note_value.clone();
773        let total_note_value_cond8 = total_note_value.clone();
774        let voting_round_id_cond6 = voting_round_id.clone();
775        let van_comm_rand_cond6 = van_comm_rand.clone();
776        let vsk_nk_cond4 = vsk_nk.clone();
777
778        // ---------------------------------------------------------------
779        // Condition 2: VAN Integrity (ZKP 1–compatible two-layer hash).
780        // van_comm_core = Poseidon(DOMAIN_VAN, vpk_g_d, vpk_pk_d, total_note_value,
781        //                          voting_round_id, proposal_authority_old)
782        // vote_authority_note_old = Poseidon(van_comm_core, van_comm_rand)
783        // ---------------------------------------------------------------
784
785        let derived_van = van_integrity::van_integrity_poseidon(
786            &config.poseidon_config,
787            &mut layouter,
788            "Old VAN integrity",
789            domain_van,
790            vpk_g_d,
791            vpk_pk_d,
792            total_note_value,
793            voting_round_id,
794            proposal_authority_old.clone(),
795            van_comm_rand,
796        )?;
797
798        // Constrain: derived VAN hash == witnessed vote_authority_note_old.
799        layouter.assign_region(
800            || "VAN integrity check",
801            |mut region| region.constrain_equal(derived_van.cell(), vote_authority_note_old.cell()),
802        )?;
803
804        // ---------------------------------------------------------------
805        // Condition 3: Diversified Address Integrity.
806        //
807        // vpk_pk_d = [ivk_v] * vpk_g_d where ivk_v = CommitIvk(ExtractP([vsk]*SpendAuthG), vsk_nk, rivk_v).
808        // ---------------------------------------------------------------
809        let vsk_scalar = ScalarFixed::new(
810            ecc_chip.clone(),
811            layouter.namespace(|| "cond3 vsk"),
812            self.vsk,
813        )?;
814        let vsk_ak_point = spend_auth_g_mul(
815            ecc_chip.clone(),
816            layouter.namespace(|| "cond3 [vsk]G"),
817            "cond3: [vsk] SpendAuthG",
818            vsk_scalar,
819        )?;
820        let ak = vsk_ak_point.extract_p().inner().clone();
821        let rivk_v_scalar = ScalarFixed::new(
822            ecc_chip.clone(),
823            layouter.namespace(|| "cond3 rivk_v"),
824            self.rivk_v,
825        )?;
826        prove_address_ownership(
827            config.sinsemilla_chip(),
828            ecc_chip.clone(),
829            config.commit_ivk_chip(),
830            layouter.namespace(|| "cond3 address"),
831            "cond3",
832            ak,
833            vsk_nk.clone(),
834            rivk_v_scalar,
835            &vpk_g_d_point,
836            &vpk_pk_d_point,
837        )?;
838
839        // ---------------------------------------------------------------
840        // Condition 4: Spend authority.
841        // r_vpk = [alpha_v] * SpendAuthG + vsk_ak_point
842        // ---------------------------------------------------------------
843        // Spend authority: proves that the public r_vpk is a valid rerandomization of the prover's ak.
844        // The out-of-circuit verifier checks that the vote signature is valid under r_vpk,
845        // so this links the ZKP to the signature without revealing ak.
846        //
847        // Uses the shared gadget from crate::gadgets::spend_authority – a 1:1 copy of
848        // the upstream Orchard spend authority check:
849        //   https://github.com/zcash/orchard/blob/main/src/circuit.rs#L542-L558
850        crate::gadgets::spend_authority::prove_spend_authority(
851            ecc_chip.clone(),
852            layouter.namespace(|| "cond4 spend authority"),
853            self.alpha_v,
854            &vsk_ak_point,
855            config.primary,
856            R_VPK_X_PUBLIC_OFFSET,
857            R_VPK_Y_PUBLIC_OFFSET,
858        )?;
859
860        // ---------------------------------------------------------------
861        // Condition 1: VAN Membership.
862        //
863        // MerklePath(vote_authority_note_old, position, path) = vote_comm_tree_root
864        //
865        // Poseidon-based Merkle path verification (24 levels). At each
866        // level, the position bit determines child ordering: if bit=0,
867        // current is the left child; if bit=1, current is the right child.
868        //
869        // The leaf is vote_authority_note_old, which is already constrained
870        // to be a correct Poseidon hash by condition 2. This creates a
871        // binding: the VAN integrity check and the Merkle membership proof
872        // are tied to the same commitment.
873        //
874        // The hash function is Poseidon(left, right) with no level tag,
875        // matching vote_commitment_tree::MerkleHashVote::combine.
876        // ---------------------------------------------------------------
877        {
878            let root = synthesize_poseidon_merkle_path::<VOTE_COMM_TREE_DEPTH>(
879                &config.merkle_swap,
880                &config.poseidon_config,
881                &mut layouter,
882                config.advices[0],
883                vote_authority_note_old_cond1,
884                self.vote_comm_tree_position,
885                self.vote_comm_tree_path,
886                "cond1: merkle",
887            )?;
888
889            // Bind the computed Merkle root to the VOTE_COMM_TREE_ROOT_PUBLIC_OFFSET
890            // public input. The verifier checks that the voter's VAN is
891            // a leaf in the published vote commitment tree.
892            layouter.constrain_instance(
893                root.cell(),
894                config.primary,
895                VOTE_COMM_TREE_ROOT_PUBLIC_OFFSET,
896            )?;
897        }
898
899        // ---------------------------------------------------------------
900        // Witness assignment for condition 5.
901        //
902        // vsk_nk was already witnessed before condition 3 (shared between
903        // conditions 3 and 5). The vsk_nk_cond4 clone is used here.
904        // ---------------------------------------------------------------
905
906        // "vote authority spend" domain tag — constant-constrained so the
907        // value is baked into the verification key.
908        let domain_van_nf = layouter.assign_region(
909            || "DOMAIN_VAN_NULLIFIER constant",
910            |mut region| {
911                region.assign_advice_from_constant(
912                    || "domain_van_nullifier",
913                    config.advices[0],
914                    0,
915                    domain_van_nullifier(),
916                )
917            },
918        )?;
919
920        // ---------------------------------------------------------------
921        // Condition 5: VAN Nullifier Integrity.
922        // van_nullifier = Poseidon(vsk_nk, domain_tag, voting_round_id, vote_authority_note_old)
923        //
924        // Single ConstantLength<4> Poseidon hash (2 permutations at rate=2).
925        //
926        // voting_round_id and vote_authority_note_old are reused from
927        // condition 2 via cell equality — these cells flow directly into
928        // the Poseidon state without being re-witnessed.
929        // ---------------------------------------------------------------
930
931        let van_nullifier = {
932            let hasher = PoseidonHash::<
933                pallas::Base,
934                _,
935                poseidon::P128Pow5T3,
936                ConstantLength<4>,
937                3, // WIDTH
938                2, // RATE
939            >::init(
940                config.poseidon_chip(),
941                layouter.namespace(|| "VAN nullifier Poseidon init"),
942            )?;
943            hasher.hash(
944                layouter.namespace(|| "Poseidon(vsk_nk, domain, round_id, van_old)"),
945                [
946                    vsk_nk_cond4,
947                    domain_van_nf,
948                    voting_round_id_cond4,
949                    vote_authority_note_old,
950                ],
951            )?
952        };
953
954        // Bind the derived nullifier to the VAN_NULLIFIER_PUBLIC_OFFSET public input.
955        // The verifier checks that the prover's computed nullifier matches
956        // the publicly posted value, preventing double-voting.
957        layouter.constrain_instance(
958            van_nullifier.cell(),
959            config.primary,
960            VAN_NULLIFIER_PUBLIC_OFFSET,
961        )?;
962
963        // ---------------------------------------------------------------
964        // Condition 6: Proposal Authority Decrement (bit decomposition).
965        //
966        // Step 1: Decompose proposal_authority_old into 16 bits b_i (boolean).
967        // Step 2: Selector sel_i = 1 iff proposal_id == i; exactly one active;
968        //         selected bit = sum(sel_i * b_i) = 1 (voter has authority).
969        // Step 3: b_new_i = b_i*(1-sel_i); recompose to proposal_authority_new.
970        // No diff/gap range check; decomposition proves [0, 2^16).
971        // ---------------------------------------------------------------
972
973        // Copy proposal_id from the public instance into an advice cell.
974        let proposal_id = layouter.assign_region(
975            || "copy proposal_id from instance",
976            |mut region| {
977                region.assign_advice_from_instance(
978                    || "proposal_id",
979                    config.primary,
980                    PROPOSAL_ID_PUBLIC_OFFSET,
981                    config.advices[0],
982                    0,
983                )
984            },
985        )?;
986
987        let proposal_authority_new = AuthorityDecrementChip::assign(
988            &config.authority_decrement,
989            &mut layouter,
990            proposal_id.clone(),
991            proposal_authority_old,
992            self.one_shifted,
993        )?;
994
995        // ---------------------------------------------------------------
996        // Condition 7: New VAN Integrity (ZKP 1–compatible two-layer hash).
997        //
998        // Same structure as condition 2; proposal_authority_new (from
999        // condition 6) replaces proposal_authority_old. vpk_g_d and vpk_pk_d
1000        // are unchanged (same diversified address).
1001        // ---------------------------------------------------------------
1002
1003        let derived_van_new = van_integrity::van_integrity_poseidon(
1004            &config.poseidon_config,
1005            &mut layouter,
1006            "New VAN integrity",
1007            domain_van_cond6,
1008            vpk_g_d_cond6,
1009            vpk_pk_d_cond6,
1010            total_note_value_cond6,
1011            voting_round_id_cond6,
1012            proposal_authority_new,
1013            van_comm_rand_cond6,
1014        )?;
1015
1016        // Bind the derived new VAN to the VOTE_AUTHORITY_NOTE_NEW_PUBLIC_OFFSET public input.
1017        // The verifier checks that the new VAN commitment posted on-chain is
1018        // correctly formed with decremented proposal authority.
1019        layouter.constrain_instance(
1020            derived_van_new.cell(),
1021            config.primary,
1022            VOTE_AUTHORITY_NOTE_NEW_PUBLIC_OFFSET,
1023        )?;
1024
1025        // ---------------------------------------------------------------
1026        // Condition 8: Shares Sum Correctness.
1027        //
1028        // sum(share_0, ..., share_15) = total_note_value
1029        //
1030        // Proves the voting share decomposition is consistent with the
1031        // total delegated weight (in ballots). Uses 15 chained AddChip additions:
1032        //   partial_1  = share_0  + share_1
1033        //   partial_2  = partial_1  + share_2
1034        //   ...
1035        //   shares_sum = partial_14 + share_15
1036        // Then constrains shares_sum == total_note_value (from condition 2).
1037        // ---------------------------------------------------------------
1038
1039        // Witness the 16 plaintext shares. These cells are also used
1040        // by condition 9 (range check) and condition 11 (El Gamal
1041        // encryption inputs).
1042        let share_cells: [_; 16] = (0..16usize)
1043            .map(|i| {
1044                assign_free_advice(
1045                    layouter.namespace(|| format!("witness share_{i}")),
1046                    config.advices[0],
1047                    self.shares[i],
1048                )
1049            })
1050            .collect::<Result<Vec<_>, _>>()?
1051            .try_into()
1052            .expect("always 16 elements");
1053
1054        // Chain 15 additions: share_0 + share_1 + ... + share_15.
1055        let shares_sum = share_cells[1..].iter().enumerate().try_fold(
1056            share_cells[0].clone(),
1057            |acc, (i, share)| {
1058                config.add_chip().add(
1059                    layouter.namespace(|| format!("shares sum step {}", i + 1)),
1060                    &acc,
1061                    share,
1062                )
1063            },
1064        )?;
1065
1066        // Constrain: shares_sum == total_note_value.
1067        // This ensures the 16 shares decompose the voter's total delegated
1068        // weight without creating or destroying value.
1069        layouter.assign_region(
1070            || "shares sum == total_note_value",
1071            |mut region| region.constrain_equal(shares_sum.cell(), total_note_value_cond8.cell()),
1072        )?;
1073
1074        // ---------------------------------------------------------------
1075        // Condition 9: Shares Range.
1076        //
1077        // Each share_i in [0, 2^30)
1078        //
1079        // Motivation: the sum constraint (condition 8) holds in the
1080        // base field F_p, but El Gamal encryption operates in the
1081        // scalar field F_q via `share_i * G`. For Pallas, p ≠ q, so a
1082        // large base-field element (e.g. p − 50) reduces to a different
1083        // value mod q, breaking the correspondence between the
1084        // constrained sum and the encrypted values. Bounding each share
1085        // to [0, 2^30) guarantees both representations agree (no
1086        // modular reduction in either field), so the homomorphic tally
1087        // faithfully reflects condition 8's sum.
1088        //
1089        // Secondary benefit: after accumulation the EA decrypts to
1090        // `total_value * G` and must solve a bounded DLOG (BSGS) to
1091        // recover `total_value`. Bounded shares keep the per-decision
1092        // aggregate small enough for efficient recovery.
1093        //
1094        // Shares are denominated in ballots (1 ballot = 0.125 ZEC),
1095        // converted from zatoshi in ZKP #1's condition 8 (ballot
1096        // scaling). Uses 3 × 10-bit lookup words with strict mode,
1097        // giving [0, 2^30). halo2_gadgets v0.3's `short_range_check`
1098        // is private, so exact non-10-bit-aligned bounds (e.g. 24-bit)
1099        // are unavailable. 2^30 ballots ≈ 134M ZEC — well above the
1100        // 21M ZEC supply — so the bound is never binding in practice.
1101        //
1102        // If a share exceeds 2^30 (or wraps around the field, e.g.
1103        // from underflow), the 3-word decomposition produces a non-zero
1104        // z_3 running sum, which fails the strict check.
1105        // ---------------------------------------------------------------
1106
1107        // Share cells are cloned because copy_check takes ownership;
1108        // the originals remain available for condition 11 (El Gamal).
1109        for (i, cell) in share_cells.iter().enumerate() {
1110            config.range_check_config().copy_check(
1111                layouter.namespace(|| format!("share_{i} < 2^30")),
1112                cell.clone(),
1113                3,    // num_words: 3 × 10 = 30 bits
1114                true, // strict: running sum terminates at 0
1115            )?;
1116        }
1117
1118        // ---------------------------------------------------------------
1119        // Condition 10: Shares Hash Integrity (blinded commitments).
1120        //
1121        // share_comm_i = Poseidon(blind_i, c1_i_x, c2_i_x, c1_i_y, c2_i_y)
1122        // shares_hash  = Poseidon(share_comm_0, ..., share_comm_15)
1123        //
1124        // The y-coordinates bind each share commitment to the exact curve
1125        // point, preventing ciphertext sign-malleability attacks.
1126        // The blind factors prevent on-chain observers from recomputing
1127        // shares_hash. shares_hash is an internal wire; it is not bound to
1128        // the instance column. Condition 11 constrains that each
1129        // (c1_i_x, c2_i_x, c1_i_y, c2_i_y) is a valid El Gamal encryption
1130        // of shares_i. Condition 12 computes the full vote commitment
1131        // H(DOMAIN_VC, voting_round_id, shares_hash, proposal_id, vote_decision)
1132        // and binds that value to the VOTE_COMMITMENT_PUBLIC_OFFSET public input.
1133        // ---------------------------------------------------------------
1134
1135        let blinds: [AssignedCell<pallas::Base, pallas::Base>; 16] = (0..16)
1136            .map(|i| {
1137                assign_free_advice(
1138                    layouter.namespace(|| format!("witness share_blind[{i}]")),
1139                    config.advices[0],
1140                    self.share_blinds[i],
1141                )
1142            })
1143            .collect::<Result<Vec<_>, _>>()?
1144            .try_into()
1145            .expect("always 16 elements");
1146
1147        let enc_c1: [AssignedCell<pallas::Base, pallas::Base>; 16] = (0..16)
1148            .map(|i| {
1149                assign_free_advice(
1150                    layouter.namespace(|| format!("witness enc_c1_x[{i}]")),
1151                    config.advices[0],
1152                    self.enc_share_c1_x[i],
1153                )
1154            })
1155            .collect::<Result<Vec<_>, _>>()?
1156            .try_into()
1157            .expect("always 16 elements");
1158
1159        let enc_c2: [AssignedCell<pallas::Base, pallas::Base>; 16] = (0..16)
1160            .map(|i| {
1161                assign_free_advice(
1162                    layouter.namespace(|| format!("witness enc_c2_x[{i}]")),
1163                    config.advices[0],
1164                    self.enc_share_c2_x[i],
1165                )
1166            })
1167            .collect::<Result<Vec<_>, _>>()?
1168            .try_into()
1169            .expect("always 16 elements");
1170
1171        let enc_c1_y: [AssignedCell<pallas::Base, pallas::Base>; 16] = (0..16)
1172            .map(|i| {
1173                assign_free_advice(
1174                    layouter.namespace(|| format!("witness enc_c1_y[{i}]")),
1175                    config.advices[0],
1176                    self.enc_share_c1_y[i],
1177                )
1178            })
1179            .collect::<Result<Vec<_>, _>>()?
1180            .try_into()
1181            .expect("always 16 elements");
1182
1183        let enc_c2_y: [AssignedCell<pallas::Base, pallas::Base>; 16] = (0..16)
1184            .map(|i| {
1185                assign_free_advice(
1186                    layouter.namespace(|| format!("witness enc_c2_y[{i}]")),
1187                    config.advices[0],
1188                    self.enc_share_c2_y[i],
1189                )
1190            })
1191            .collect::<Result<Vec<_>, _>>()?
1192            .try_into()
1193            .expect("always 16 elements");
1194
1195        // Clone for Condition 11 before compute_shares_hash_in_circuit takes ownership.
1196        let enc_c1_cond11: [AssignedCell<pallas::Base, pallas::Base>; 16] =
1197            core::array::from_fn(|i| enc_c1[i].clone());
1198        let enc_c2_cond11: [AssignedCell<pallas::Base, pallas::Base>; 16] =
1199            core::array::from_fn(|i| enc_c2[i].clone());
1200        let enc_c1_y_cond11: [AssignedCell<pallas::Base, pallas::Base>; 16] =
1201            core::array::from_fn(|i| enc_c1_y[i].clone());
1202        let enc_c2_y_cond11: [AssignedCell<pallas::Base, pallas::Base>; 16] =
1203            core::array::from_fn(|i| enc_c2_y[i].clone());
1204
1205        let shares_hash = compute_shares_hash_in_circuit(
1206            || config.poseidon_chip(),
1207            layouter.namespace(|| "cond10: shares hash"),
1208            blinds,
1209            enc_c1,
1210            enc_c2,
1211            enc_c1_y,
1212            enc_c2_y,
1213        )?;
1214
1215        // ---------------------------------------------------------------
1216        // Condition 11: Encryption Integrity.
1217        //
1218        // For each share i: C1_i = [r_i]*G, C2_i = [v_i]*G + [r_i]*ea_pk;
1219        // Both coordinates of C1_i and C2_i are constrained to the
1220        // witnessed enc_share cells. Implemented by the shared
1221        // circuit::elgamal::prove_elgamal_encryptions gadget.
1222        // ---------------------------------------------------------------
1223        {
1224            let r_cells: [_; 16] = (0..16usize)
1225                .map(|i| {
1226                    assign_free_advice(
1227                        layouter.namespace(|| format!("witness r[{i}]")),
1228                        config.advices[0],
1229                        self.share_randomness[i],
1230                    )
1231                })
1232                .collect::<Result<Vec<_>, _>>()?
1233                .try_into()
1234                .expect("always 16 elements");
1235
1236            prove_elgamal_encryptions(
1237                ecc_chip.clone(),
1238                config.nonzero,
1239                layouter.namespace(|| "cond11 El Gamal"),
1240                "cond11",
1241                self.ea_pk,
1242                EaPkInstanceLoc {
1243                    instance: config.primary,
1244                    x_row: EA_PK_X_PUBLIC_OFFSET,
1245                    y_row: EA_PK_Y_PUBLIC_OFFSET,
1246                },
1247                config.advices[0],
1248                share_cells,
1249                r_cells,
1250                enc_c1_cond11,
1251                enc_c2_cond11,
1252                enc_c1_y_cond11,
1253                enc_c2_y_cond11,
1254            )?;
1255        }
1256
1257        // ---------------------------------------------------------------
1258        // Condition 12: Vote Commitment Integrity.
1259        //
1260        // vote_commitment = Poseidon(DOMAIN_VC, voting_round_id,
1261        //                            shares_hash, proposal_id, vote_decision)
1262        //
1263        // Binds the voting round, encrypted shares (via shares_hash from
1264        // condition 10), the proposal choice, and the vote decision into a
1265        // single commitment with domain separation from VANs (DOMAIN_VC = 1).
1266        //
1267        // This is the value posted on-chain and later inserted into the
1268        // vote commitment tree. ZKP #3 (vote reveal) will open individual
1269        // shares from this commitment.
1270        // ---------------------------------------------------------------
1271
1272        // DOMAIN_VC — constant-constrained so the value is baked into the
1273        // verification key and cannot be altered by a malicious prover.
1274        let domain_vc = layouter.assign_region(
1275            || "DOMAIN_VC constant",
1276            |mut region| {
1277                region.assign_advice_from_constant(
1278                    || "domain_vc",
1279                    config.advices[0],
1280                    0,
1281                    pallas::Base::from(DOMAIN_VC),
1282                )
1283            },
1284        )?;
1285
1286        // proposal_id was already copied from instance in condition 6; reuse that cell.
1287
1288        // Private witness: vote decision.
1289        let vote_decision = assign_free_advice(
1290            layouter.namespace(|| "witness vote_decision"),
1291            config.advices[0],
1292            self.vote_decision,
1293        )?;
1294
1295        // Compute vote_commitment = Poseidon(DOMAIN_VC, voting_round_id,
1296        //                                    shares_hash, proposal_id, vote_decision).
1297        let vote_commitment = vote_commitment::vote_commitment_poseidon(
1298            &config.poseidon_config,
1299            &mut layouter,
1300            "cond12",
1301            domain_vc,
1302            voting_round_id_cond12,
1303            shares_hash,
1304            proposal_id,
1305            vote_decision,
1306        )?;
1307
1308        // Bind the derived vote commitment to the VOTE_COMMITMENT_PUBLIC_OFFSET public input.
1309        layouter.constrain_instance(
1310            vote_commitment.cell(),
1311            config.primary,
1312            VOTE_COMMITMENT_PUBLIC_OFFSET,
1313        )?;
1314
1315        Ok(())
1316    }
1317}
1318
1319// ================================================================
1320// Instance (public inputs)
1321// ================================================================
1322
1323/// Public inputs to the Vote Proof circuit (11 field elements).
1324///
1325/// The voting client (prover) chooses these values when assembling the
1326/// proof; the verifier accepts them as the binding the proof must
1327/// satisfy and checks the proof without seeing any private witnesses.
1328/// The relationship is asymmetric: a malicious-custody client can
1329/// choose any public-input vector it likes, so the verifier must source
1330/// the *correct* values from authenticated chain state (see
1331/// [`crate::vote_proof::prove::verify_vote_proof`] for which fields
1332/// require caller authentication versus which are proof-attested
1333/// outputs).
1334///
1335/// Binding contract: `shares_hash` is deliberately absent from this public
1336/// instance vector. The circuit computes it as an internal condition-10 cell
1337/// and exposes it to the verifier only through `vote_commitment`.
1338#[derive(Clone, Debug)]
1339pub struct Instance {
1340    /// The nullifier of the old VAN being spent (prevents double-vote).
1341    pub van_nullifier: pallas::Base,
1342    /// Randomized voting public key (condition 4): x-coordinate of r_vpk = vsk.ak + [alpha_v] * G.
1343    pub r_vpk_x: pallas::Base,
1344    /// Randomized voting public key: y-coordinate.
1345    pub r_vpk_y: pallas::Base,
1346    /// The new VAN commitment (with decremented proposal authority).
1347    pub vote_authority_note_new: pallas::Base,
1348    /// The vote commitment hash.
1349    pub vote_commitment: pallas::Base,
1350    /// Root of the vote commitment tree at anchor height.
1351    pub vote_comm_tree_root: pallas::Base,
1352    /// Caller-authenticated chain height used to source `vote_comm_tree_root`.
1353    ///
1354    /// This public input is transcript-bound but not constrained to a witness
1355    /// cell. Verifiers must check that `vote_comm_tree_root` is the chain root
1356    /// at this height.
1357    pub vote_comm_tree_anchor_height: pallas::Base,
1358    /// Governance session parameter: which proposal this vote is for.
1359    ///
1360    /// The circuit constrains this to `[1, 15]` through condition 6 and binds
1361    /// it into the new VAN and vote commitment. The verifier must separately
1362    /// check that it is active for `voting_round_id`.
1363    pub proposal_id: pallas::Base,
1364    /// Governance session parameter: the voting round identifier.
1365    ///
1366    /// The circuit binds this into the VAN nullifier, new VAN, and vote
1367    /// commitment, but cannot authenticate that it is the active round.
1368    pub voting_round_id: pallas::Base,
1369    /// Governance-announced election authority public key x-coordinate.
1370    ///
1371    /// The verifier must pin this from the active round's governance
1372    /// announcement. The circuit proves encryption under this coordinate pair,
1373    /// but cannot authenticate that it is the legitimate EA key.
1374    pub ea_pk_x: pallas::Base,
1375    /// Governance-announced election authority public key y-coordinate.
1376    ///
1377    /// Must be authenticated with `ea_pk_x`; both coordinates are public so a
1378    /// prover cannot substitute a negated curve point while preserving x.
1379    pub ea_pk_y: pallas::Base,
1380}
1381
1382impl Instance {
1383    /// Number of public inputs serialized by [`Self::to_halo2_instance`].
1384    pub const NUM_PUBLIC_INPUTS: usize = 11;
1385
1386    /// Constructs an [`Instance`] from its constituent parts.
1387    ///
1388    /// Callers should authenticate `vote_comm_tree_root`,
1389    /// `vote_comm_tree_anchor_height`, `proposal_id`, `voting_round_id`,
1390    /// `ea_pk_x`, and `ea_pk_y` out-of-band before passing them here.
1391    /// `proposal_id` must be active for `voting_round_id`; the circuit only
1392    /// checks the authority-bit index range. The EA key must come from the
1393    /// active round's governance announcement, not from the prover bundle. See
1394    /// [`crate::vote_proof::prove::verify_vote_proof`] for the trust contract
1395    /// and why wiring `ea_pk_*` from the same bundle as the proof is a
1396    /// custody-attack surface. The remaining fields are proof-attested outputs
1397    /// derived outside the circuit but constrained in-circuit against
1398    /// authenticated inputs and private witnesses.
1399    pub fn from_parts(
1400        van_nullifier: pallas::Base,
1401        r_vpk_x: pallas::Base,
1402        r_vpk_y: pallas::Base,
1403        vote_authority_note_new: pallas::Base,
1404        vote_commitment: pallas::Base,
1405        vote_comm_tree_root: pallas::Base,
1406        vote_comm_tree_anchor_height: pallas::Base,
1407        proposal_id: pallas::Base,
1408        voting_round_id: pallas::Base,
1409        ea_pk_x: pallas::Base,
1410        ea_pk_y: pallas::Base,
1411    ) -> Self {
1412        Instance {
1413            van_nullifier,
1414            r_vpk_x,
1415            r_vpk_y,
1416            vote_authority_note_new,
1417            vote_commitment,
1418            vote_comm_tree_root,
1419            vote_comm_tree_anchor_height,
1420            proposal_id,
1421            voting_round_id,
1422            ea_pk_x,
1423            ea_pk_y,
1424        }
1425    }
1426
1427    /// Serializes public inputs for halo2 proof creation/verification.
1428    ///
1429    /// The order must match the instance column offsets defined at the
1430    /// top of this file (`VAN_NULLIFIER_PUBLIC_OFFSET`, `R_VPK_X_PUBLIC_OFFSET`,
1431    /// `R_VPK_Y_PUBLIC_OFFSET`, etc.).
1432    pub fn to_halo2_instance(&self) -> Vec<vesta::Scalar> {
1433        vec![
1434            self.van_nullifier,
1435            self.r_vpk_x,
1436            self.r_vpk_y,
1437            self.vote_authority_note_new,
1438            self.vote_commitment,
1439            self.vote_comm_tree_root,
1440            self.vote_comm_tree_anchor_height,
1441            self.proposal_id,
1442            self.voting_round_id,
1443            self.ea_pk_x,
1444            self.ea_pk_y,
1445        ]
1446    }
1447}
1448
1449// ================================================================
1450// Tests
1451// ================================================================
1452
1453#[cfg(test)]
1454mod tests {
1455    use super::*;
1456    use crate::gadgets::elgamal::{base_to_scalar, elgamal_encrypt, spend_auth_g_affine};
1457    use crate::protocol_hash::poseidon_hash_2;
1458    use crate::shares_hash::{hash_share_commitment_in_circuit, share_commitment, shares_hash};
1459    use core::iter;
1460    use ff::{Field, PrimeField};
1461    use group::ff::PrimeFieldBits;
1462    use group::{Curve, Group};
1463    use halo2_gadgets::sinsemilla::primitives::CommitDomain;
1464    use halo2_proofs::dev::MockProver;
1465    use pasta_curves::arithmetic::CurveAffine;
1466    use pasta_curves::pallas;
1467    use rand::rngs::OsRng;
1468
1469    use orchard::constants::{fixed_bases::COMMIT_IVK_PERSONALIZATION, L_ORCHARD_BASE};
1470
1471    /// Generates an El Gamal keypair for testing.
1472    fn generate_ea_keypair() -> (pallas::Scalar, pallas::Affine) {
1473        let ea_sk = pallas::Scalar::from(42u64);
1474        let ea_pk = (spend_auth_g_affine() * ea_sk).to_affine();
1475        (ea_sk, ea_pk)
1476    }
1477
1478    /// Computes real El Gamal encryptions for 16 shares.
1479    ///
1480    /// Returns `(c1_x, c2_x, c1_y, c2_y, randomness, share_blinds, shares_hash_value)` where:
1481    /// - `c1_x[i]` and `c2_x[i]` are correct ciphertext x-coordinates
1482    /// - `c1_y[i]` and `c2_y[i]` are correct ciphertext y-coordinates
1483    /// - `randomness[i]` is the base field randomness used for each share
1484    /// - `share_blinds[i]` is the blind factor for each share commitment
1485    /// - `shares_hash_value` is the blinded Poseidon hash of all shares
1486    fn encrypt_shares(
1487        shares: [u64; 16],
1488        ea_pk: pallas::Affine,
1489    ) -> (
1490        [pallas::Base; 16],
1491        [pallas::Base; 16],
1492        [pallas::Base; 16],
1493        [pallas::Base; 16],
1494        [pallas::Base; 16],
1495        [pallas::Base; 16],
1496        pallas::Base,
1497    ) {
1498        let mut c1_x = [pallas::Base::zero(); 16];
1499        let mut c2_x = [pallas::Base::zero(); 16];
1500        let mut c1_y = [pallas::Base::zero(); 16];
1501        let mut c2_y = [pallas::Base::zero(); 16];
1502        // Use small deterministic randomness (fits in both Base and Scalar).
1503        let randomness: [pallas::Base; 16] =
1504            core::array::from_fn(|i| pallas::Base::from((i as u64 + 1) * 101));
1505        // Deterministic blind factors for tests.
1506        let share_blinds: [pallas::Base; 16] =
1507            core::array::from_fn(|i| pallas::Base::from(1001u64 + i as u64));
1508        for i in 0..16 {
1509            let (cx1, cx2, cy1, cy2) =
1510                elgamal_encrypt(pallas::Base::from(shares[i]), randomness[i], ea_pk)
1511                    .expect("test encryption inputs should be valid");
1512            c1_x[i] = cx1;
1513            c2_x[i] = cx2;
1514            c1_y[i] = cy1;
1515            c2_y[i] = cy2;
1516        }
1517        let hash = shares_hash(share_blinds, c1_x, c2_x, c1_y, c2_y);
1518        (c1_x, c2_x, c1_y, c2_y, randomness, share_blinds, hash)
1519    }
1520
1521    /// Out-of-circuit voting key derivation for tests.
1522    ///
1523    /// Given a voting spending key (vsk), nullifier key (nk), and CommitIvk
1524    /// randomness (rivk_v), derives the full voting address:
1525    ///
1526    /// 1. `ak = [vsk] * SpendAuthG` (spend validating key)
1527    /// 2. `ak_x = ExtractP(ak)` (x-coordinate)
1528    /// 3. `ivk_v = CommitIvk(ak_x, nk, rivk_v)` (incoming viewing key)
1529    /// 4. `g_d = random non-identity point` (diversified base)
1530    /// 5. `pk_d = [ivk_v] * g_d` (diversified transmission key)
1531    ///
1532    /// Returns `(g_d_affine, pk_d_affine, ak_x)` for use as circuit witnesses.
1533    fn derive_voting_address(
1534        vsk: pallas::Scalar,
1535        nk: pallas::Base,
1536        rivk_v: pallas::Scalar,
1537    ) -> (pallas::Affine, pallas::Affine) {
1538        // Step 1: ak = [vsk] * SpendAuthG
1539        let g = spend_auth_g_affine();
1540        let ak_point = g * vsk;
1541        let ak_x = *ak_point.to_affine().coordinates().unwrap().x();
1542
1543        // Step 2: ivk_v = CommitIvk(ak_x, nk, rivk_v)
1544        let domain = CommitDomain::new(COMMIT_IVK_PERSONALIZATION);
1545        let ivk_v = domain
1546            .short_commit(
1547                iter::empty()
1548                    .chain(ak_x.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
1549                    .chain(nk.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE)),
1550                &rivk_v,
1551            )
1552            .expect("CommitIvk should not produce ⊥ for random inputs");
1553
1554        // Step 3: g_d = random non-identity point
1555        // Using a deterministic point derived from a fixed seed ensures
1556        // reproducibility while avoiding the identity point.
1557        let g_d = pallas::Point::generator() * pallas::Scalar::from(12345u64);
1558        let g_d_affine = g_d.to_affine();
1559
1560        // Step 4: pk_d = [ivk_v] * g_d
1561        let ivk_v_scalar = base_to_scalar(ivk_v).expect("ivk_v must be < scalar field modulus");
1562        let pk_d = g_d * ivk_v_scalar;
1563        let pk_d_affine = pk_d.to_affine();
1564
1565        (g_d_affine, pk_d_affine)
1566    }
1567
1568    /// Default proposal_id and vote_decision for tests.
1569    const TEST_PROPOSAL_ID: u64 = 3;
1570    const TEST_VOTE_DECISION: u64 = 1;
1571
1572    /// Sets condition 12 fields on a circuit and returns the vote_commitment.
1573    ///
1574    /// Computes `H(DOMAIN_VC, voting_round_id, shares_hash, proposal_id, vote_decision)`
1575    /// and sets `circuit.vote_decision`. Returns the vote_commitment
1576    /// for use in the Instance. The `proposal_id` must match the
1577    /// instance's proposal_id so the circuit's condition 12 (which
1578    /// copies proposal_id from the instance) agrees with the instance.
1579    fn set_condition_11(
1580        circuit: &mut Circuit,
1581        shares_hash_val: pallas::Base,
1582        proposal_id: u64,
1583        voting_round_id: pallas::Base,
1584    ) -> pallas::Base {
1585        let proposal_id_base = pallas::Base::from(proposal_id);
1586        let vote_decision = pallas::Base::from(TEST_VOTE_DECISION);
1587        circuit.vote_decision = Value::known(vote_decision);
1588        vote_commitment_hash(
1589            voting_round_id,
1590            shares_hash_val,
1591            proposal_id_base,
1592            vote_decision,
1593        )
1594    }
1595
1596    /// Build valid test data for all 12 conditions.
1597    ///
1598    /// Returns a circuit with correctly-hashed VAN witnesses, valid
1599    /// shares, real El Gamal ciphertexts, and a matching instance.
1600    fn build_single_leaf_merkle_path(
1601        leaf: pallas::Base,
1602    ) -> ([pallas::Base; VOTE_COMM_TREE_DEPTH], u32, pallas::Base) {
1603        let auth_path = empty_vote_comm_tree_path();
1604        let mut current = leaf;
1605        for i in 0..VOTE_COMM_TREE_DEPTH {
1606            current = poseidon_hash_2(current, auth_path[i]);
1607        }
1608        (auth_path, 0, current)
1609    }
1610
1611    fn empty_vote_comm_tree_path() -> [pallas::Base; VOTE_COMM_TREE_DEPTH] {
1612        let mut empty_roots = [pallas::Base::zero(); VOTE_COMM_TREE_DEPTH];
1613        empty_roots[0] = poseidon_hash_2(pallas::Base::zero(), pallas::Base::zero());
1614        for i in 1..VOTE_COMM_TREE_DEPTH {
1615            empty_roots[i] = poseidon_hash_2(empty_roots[i - 1], empty_roots[i - 1]);
1616        }
1617        empty_roots
1618    }
1619
1620    fn build_left_leaf_merkle_path_with_sibling(
1621        left_leaf: pallas::Base,
1622        right_leaf: pallas::Base,
1623    ) -> ([pallas::Base; VOTE_COMM_TREE_DEPTH], u32, pallas::Base) {
1624        let mut auth_path = empty_vote_comm_tree_path();
1625        auth_path[0] = right_leaf;
1626
1627        let mut current = left_leaf;
1628        for i in 0..VOTE_COMM_TREE_DEPTH {
1629            current = poseidon_hash_2(current, auth_path[i]);
1630        }
1631        (auth_path, 0, current)
1632    }
1633
1634    struct VoteReuseFixture {
1635        vsk: pallas::Scalar,
1636        vsk_nk: pallas::Base,
1637        rivk_v: pallas::Scalar,
1638        alpha_v: pallas::Scalar,
1639        vpk_g_d_affine: pallas::Affine,
1640        vpk_pk_d_affine: pallas::Affine,
1641        total_note_value: pallas::Base,
1642        proposal_authority_old: pallas::Base,
1643        proposal_id: u64,
1644        van_comm_rand: pallas::Base,
1645        shares_u64: [u64; 16],
1646        ea_pk: pallas::Affine,
1647    }
1648
1649    impl VoteReuseFixture {
1650        fn new() -> Self {
1651            let mut rng = OsRng;
1652            let vsk = pallas::Scalar::random(&mut rng);
1653            let vsk_nk = pallas::Base::random(&mut rng);
1654            let rivk_v = pallas::Scalar::random(&mut rng);
1655            let alpha_v = pallas::Scalar::random(&mut rng);
1656            let (vpk_g_d_affine, vpk_pk_d_affine) = derive_voting_address(vsk, vsk_nk, rivk_v);
1657            let (_ea_sk, ea_pk) = generate_ea_keypair();
1658
1659            Self {
1660                vsk,
1661                vsk_nk,
1662                rivk_v,
1663                alpha_v,
1664                vpk_g_d_affine,
1665                vpk_pk_d_affine,
1666                total_note_value: pallas::Base::from(10_000u64),
1667                proposal_authority_old: pallas::Base::from(13u64),
1668                proposal_id: TEST_PROPOSAL_ID,
1669                van_comm_rand: pallas::Base::random(&mut rng),
1670                shares_u64: [625; 16],
1671                ea_pk,
1672            }
1673        }
1674
1675        fn vpk_x_coordinates(&self) -> (pallas::Base, pallas::Base) {
1676            (
1677                *self.vpk_g_d_affine.coordinates().unwrap().x(),
1678                *self.vpk_pk_d_affine.coordinates().unwrap().x(),
1679            )
1680        }
1681
1682        fn vote_authority_note_old(&self, voting_round_id: pallas::Base) -> pallas::Base {
1683            let (vpk_g_d_x, vpk_pk_d_x) = self.vpk_x_coordinates();
1684            van_integrity_hash(
1685                vpk_g_d_x,
1686                vpk_pk_d_x,
1687                self.total_note_value,
1688                voting_round_id,
1689                self.proposal_authority_old,
1690                self.van_comm_rand,
1691            )
1692        }
1693
1694        fn vote_authority_note_new(&self, voting_round_id: pallas::Base) -> pallas::Base {
1695            let (vpk_g_d_x, vpk_pk_d_x) = self.vpk_x_coordinates();
1696            let proposal_authority_new =
1697                self.proposal_authority_old - pallas::Base::from(1u64 << self.proposal_id);
1698            van_integrity_hash(
1699                vpk_g_d_x,
1700                vpk_pk_d_x,
1701                self.total_note_value,
1702                voting_round_id,
1703                proposal_authority_new,
1704                self.van_comm_rand,
1705            )
1706        }
1707
1708        fn build_vote_data(
1709            &self,
1710            voting_round_id: pallas::Base,
1711            auth_path: [pallas::Base; VOTE_COMM_TREE_DEPTH],
1712            position: u32,
1713            vote_comm_tree_root: pallas::Base,
1714            anchor_height: u64,
1715        ) -> (Circuit, Instance) {
1716            let vote_authority_note_old = self.vote_authority_note_old(voting_round_id);
1717            let vote_authority_note_new = self.vote_authority_note_new(voting_round_id);
1718            let van_nullifier =
1719                van_nullifier_hash(self.vsk_nk, voting_round_id, vote_authority_note_old);
1720
1721            let g = spend_auth_g_affine();
1722            let r_vpk = (g * (self.vsk + self.alpha_v)).to_affine();
1723            let r_vpk_x = *r_vpk.coordinates().unwrap().x();
1724            let r_vpk_y = *r_vpk.coordinates().unwrap().y();
1725
1726            let (enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y, randomness, share_blinds, shares_hash_val) =
1727                encrypt_shares(self.shares_u64, self.ea_pk);
1728
1729            let mut circuit = Circuit::with_van_witnesses(
1730                Value::known(auth_path),
1731                Value::known(position),
1732                Value::known(self.vpk_g_d_affine),
1733                Value::known(self.vpk_pk_d_affine),
1734                Value::known(self.total_note_value),
1735                Value::known(self.proposal_authority_old),
1736                Value::known(self.van_comm_rand),
1737                Value::known(vote_authority_note_old),
1738                Value::known(self.vsk),
1739                Value::known(self.rivk_v),
1740                Value::known(self.vsk_nk),
1741                Value::known(self.alpha_v),
1742            );
1743            circuit.one_shifted = Value::known(pallas::Base::from(1u64 << self.proposal_id));
1744            circuit.shares = self.shares_u64.map(|s| Value::known(pallas::Base::from(s)));
1745            circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
1746            circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
1747            circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
1748            circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
1749            circuit.share_blinds = share_blinds.map(Value::known);
1750            circuit.share_randomness = randomness.map(Value::known);
1751            circuit.ea_pk = Value::known(self.ea_pk);
1752            let vote_commitment = set_condition_11(
1753                &mut circuit,
1754                shares_hash_val,
1755                self.proposal_id,
1756                voting_round_id,
1757            );
1758
1759            let instance = Instance::from_parts(
1760                van_nullifier,
1761                r_vpk_x,
1762                r_vpk_y,
1763                vote_authority_note_new,
1764                vote_commitment,
1765                vote_comm_tree_root,
1766                pallas::Base::from(anchor_height),
1767                pallas::Base::from(self.proposal_id),
1768                voting_round_id,
1769                *self.ea_pk.coordinates().unwrap().x(),
1770                *self.ea_pk.coordinates().unwrap().y(),
1771            );
1772
1773            (circuit, instance)
1774        }
1775    }
1776
1777    /// Build test (circuit, instance) with given proposal_authority_old,
1778    /// proposal_id, and optional spend-authority randomizer.
1779    /// proposal_authority_old must have the proposal_id-th bit set (spec bitmask).
1780    fn make_test_data_with_authority_proposal_and_alpha(
1781        proposal_authority_old: pallas::Base,
1782        proposal_id: u64,
1783        alpha_v_override: Option<pallas::Scalar>,
1784    ) -> (Circuit, Instance) {
1785        let mut rng = OsRng;
1786
1787        // Condition 3 (spend authority): derive proper voting key hierarchy.
1788        // vsk → ak → ivk_v → (vpk_g_d, vpk_pk_d) through CommitIvk chain.
1789        let vsk = pallas::Scalar::random(&mut rng);
1790        let vsk_nk = pallas::Base::random(&mut rng);
1791        let rivk_v = pallas::Scalar::random(&mut rng);
1792        let alpha_v = alpha_v_override.unwrap_or_else(|| pallas::Scalar::random(&mut rng));
1793
1794        let (vpk_g_d_affine, vpk_pk_d_affine) = derive_voting_address(vsk, vsk_nk, rivk_v);
1795
1796        // Condition 4: r_vpk = ak + [alpha_v] * G = [vsk + alpha_v] * G
1797        let g = spend_auth_g_affine();
1798        let r_vpk = (g * (vsk + alpha_v)).to_affine();
1799        let r_vpk_x = *r_vpk.coordinates().unwrap().x();
1800        let r_vpk_y = *r_vpk.coordinates().unwrap().y();
1801
1802        // Extract x-coordinates for Poseidon hashing (conditions 2, 6).
1803        let vpk_g_d_x = *vpk_g_d_affine.coordinates().unwrap().x();
1804        let vpk_pk_d_x = *vpk_pk_d_affine.coordinates().unwrap().x();
1805
1806        // total_note_value must be small enough that all 16 shares
1807        // fit in [0, 2^30) for condition 9's range check.
1808        let total_note_value = pallas::Base::from(10_000u64);
1809        let voting_round_id = pallas::Base::random(&mut rng);
1810        let van_comm_rand = pallas::Base::random(&mut rng);
1811
1812        let vote_authority_note_old = van_integrity_hash(
1813            vpk_g_d_x,
1814            vpk_pk_d_x,
1815            total_note_value,
1816            voting_round_id,
1817            proposal_authority_old,
1818            van_comm_rand,
1819        );
1820        let (auth_path, position, vote_comm_tree_root) =
1821            build_single_leaf_merkle_path(vote_authority_note_old);
1822        let van_nullifier = van_nullifier_hash(vsk_nk, voting_round_id, vote_authority_note_old);
1823        // Spec: proposal_authority_new = proposal_authority_old - (1 << proposal_id).
1824        let one_shifted = pallas::Base::from(1u64 << proposal_id);
1825        let proposal_authority_new = proposal_authority_old - one_shifted;
1826        let vote_authority_note_new = van_integrity_hash(
1827            vpk_g_d_x,
1828            vpk_pk_d_x,
1829            total_note_value,
1830            voting_round_id,
1831            proposal_authority_new,
1832            van_comm_rand,
1833        );
1834
1835        // Create shares that sum to total_note_value (conditions 8 + 9).
1836        // Each share must be in [0, 2^30) for condition 9's range check.
1837        let shares_u64: [u64; 16] = [625; 16]; // sum = 10000
1838
1839        // Condition 11: El Gamal encryption of shares under ea_pk.
1840        let (_ea_sk, ea_pk) = generate_ea_keypair();
1841        let ea_pk_x = *ea_pk.coordinates().unwrap().x();
1842        let ea_pk_y = *ea_pk.coordinates().unwrap().y();
1843        let (enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y, randomness, share_blinds, shares_hash_val) =
1844            encrypt_shares(shares_u64, ea_pk);
1845
1846        let mut circuit = Circuit::with_van_witnesses(
1847            Value::known(auth_path),
1848            Value::known(position),
1849            Value::known(vpk_g_d_affine),
1850            Value::known(vpk_pk_d_affine),
1851            Value::known(total_note_value),
1852            Value::known(proposal_authority_old),
1853            Value::known(van_comm_rand),
1854            Value::known(vote_authority_note_old),
1855            Value::known(vsk),
1856            Value::known(rivk_v),
1857            Value::known(vsk_nk),
1858            Value::known(alpha_v),
1859        );
1860        circuit.one_shifted = Value::known(one_shifted);
1861        circuit.shares = shares_u64.map(|s| Value::known(pallas::Base::from(s)));
1862        circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
1863        circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
1864        circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
1865        circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
1866        circuit.share_blinds = share_blinds.map(Value::known);
1867        circuit.share_randomness = randomness.map(Value::known);
1868        circuit.ea_pk = Value::known(ea_pk);
1869
1870        // Condition 12: vote commitment from shares_hash + proposal + decision.
1871        let vote_commitment =
1872            set_condition_11(&mut circuit, shares_hash_val, proposal_id, voting_round_id);
1873
1874        let instance = Instance::from_parts(
1875            van_nullifier,
1876            r_vpk_x,
1877            r_vpk_y,
1878            vote_authority_note_new,
1879            vote_commitment,
1880            vote_comm_tree_root,
1881            pallas::Base::zero(),
1882            pallas::Base::from(proposal_id),
1883            voting_round_id,
1884            ea_pk_x,
1885            ea_pk_y,
1886        );
1887
1888        (circuit, instance)
1889    }
1890
1891    fn make_test_data_with_authority_and_proposal(
1892        proposal_authority_old: pallas::Base,
1893        proposal_id: u64,
1894    ) -> (Circuit, Instance) {
1895        make_test_data_with_authority_proposal_and_alpha(proposal_authority_old, proposal_id, None)
1896    }
1897
1898    fn make_test_data_with_authority(proposal_authority_old: pallas::Base) -> (Circuit, Instance) {
1899        make_test_data_with_authority_and_proposal(proposal_authority_old, TEST_PROPOSAL_ID)
1900    }
1901
1902    fn make_test_data() -> (Circuit, Instance) {
1903        // proposal_authority_old must have bit TEST_PROPOSAL_ID set (spec bitmask).
1904        // 5 | (1 << 3) = 13 so we can vote on proposal 3 and get new = 5.
1905        make_test_data_with_authority(pallas::Base::from(13u64))
1906    }
1907
1908    // ================================================================
1909    // Condition 2 (VAN Integrity) tests
1910    // ================================================================
1911
1912    #[test]
1913    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1914    fn van_integrity_valid_proof() {
1915        let (circuit, instance) = make_test_data();
1916
1917        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
1918
1919        assert_eq!(prover.verify(), Ok(()));
1920    }
1921
1922    #[test]
1923    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1924    fn van_integrity_wrong_hash_fails() {
1925        let mut rng = OsRng;
1926        let (_, mut instance) = make_test_data();
1927
1928        // Deliberately wrong VAN value — condition 2 constrain_equal will fail.
1929        let wrong_van = pallas::Base::random(&mut rng);
1930        let (auth_path, position, root) = build_single_leaf_merkle_path(wrong_van);
1931        instance.vote_comm_tree_root = root;
1932
1933        // Use properly derived keys (condition 3 would pass) but the VAN
1934        // hash won't match wrong_van, so condition 2 fails.
1935        let vsk = pallas::Scalar::random(&mut rng);
1936        let vsk_nk = pallas::Base::random(&mut rng);
1937        let rivk_v = pallas::Scalar::random(&mut rng);
1938        let alpha_v = pallas::Scalar::random(&mut rng);
1939        let (vpk_g_d_affine, vpk_pk_d_affine) = derive_voting_address(vsk, vsk_nk, rivk_v);
1940        let g = spend_auth_g_affine();
1941        let r_vpk = (g * (vsk + alpha_v)).to_affine();
1942        instance.r_vpk_x = *r_vpk.coordinates().unwrap().x();
1943        instance.r_vpk_y = *r_vpk.coordinates().unwrap().y();
1944
1945        let shares_u64: [u64; 16] = [625; 16];
1946        let (_ea_sk, ea_pk) = generate_ea_keypair();
1947        let (enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y, randomness, share_blinds, shares_hash_val) =
1948            encrypt_shares(shares_u64, ea_pk);
1949
1950        // Use authority 13 (bit 3 set) and one_shifted = 8 so condition 6 is consistent;
1951        // only condition 2 (VAN hash) should fail due to wrong_van.
1952        let proposal_authority_old = pallas::Base::from(13u64);
1953        let van_comm_rand = pallas::Base::random(&mut rng);
1954        let mut circuit = Circuit::with_van_witnesses(
1955            Value::known(auth_path),
1956            Value::known(position),
1957            Value::known(vpk_g_d_affine),
1958            Value::known(vpk_pk_d_affine),
1959            Value::known(pallas::Base::from(10_000u64)),
1960            Value::known(proposal_authority_old),
1961            Value::known(van_comm_rand),
1962            Value::known(wrong_van),
1963            Value::known(vsk),
1964            Value::known(rivk_v),
1965            Value::known(vsk_nk),
1966            Value::known(alpha_v),
1967        );
1968        circuit.one_shifted = Value::known(pallas::Base::from(1u64 << TEST_PROPOSAL_ID));
1969        circuit.shares = shares_u64.map(|s| Value::known(pallas::Base::from(s)));
1970        circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
1971        circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
1972        circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
1973        circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
1974        circuit.share_blinds = share_blinds.map(Value::known);
1975        circuit.share_randomness = randomness.map(Value::known);
1976        circuit.ea_pk = Value::known(ea_pk);
1977        let vc = set_condition_11(
1978            &mut circuit,
1979            shares_hash_val,
1980            TEST_PROPOSAL_ID,
1981            instance.voting_round_id,
1982        );
1983        instance.vote_commitment = vc;
1984        instance.proposal_id = pallas::Base::from(TEST_PROPOSAL_ID);
1985        instance.ea_pk_x = *ea_pk.coordinates().unwrap().x();
1986        instance.ea_pk_y = *ea_pk.coordinates().unwrap().y();
1987
1988        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
1989        // Should fail: derived hash ≠ witnessed vote_authority_note_old.
1990        assert!(prover.verify().is_err());
1991    }
1992
1993    #[test]
1994    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1995    fn van_integrity_wrong_round_id_fails() {
1996        let (circuit, mut instance) = make_test_data();
1997
1998        // Supply a DIFFERENT voting_round_id in the instance.
1999        instance.voting_round_id = pallas::Base::random(&mut OsRng);
2000
2001        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2002        // Should fail: the voting_round_id from the instance doesn't match
2003        // the one hashed into the VAN (condition 2).
2004        assert!(prover.verify().is_err());
2005    }
2006
2007    #[test]
2008    fn round_scoped_van_redelegation_changes_nullifier() {
2009        let fixture = VoteReuseFixture::new();
2010        let round_1 = pallas::Base::from(0xCAFEu64);
2011        let round_2 = pallas::Base::from(0xCAFFu64);
2012
2013        let van_round_1 = fixture.vote_authority_note_old(round_1);
2014        let van_round_2 = fixture.vote_authority_note_old(round_2);
2015        assert_ne!(
2016            van_round_1, van_round_2,
2017            "voting_round_id is part of the VAN preimage"
2018        );
2019
2020        let nullifier_round_1 = van_nullifier_hash(fixture.vsk_nk, round_1, van_round_1);
2021        let nullifier_round_2 = van_nullifier_hash(fixture.vsk_nk, round_2, van_round_2);
2022        assert_ne!(
2023            nullifier_round_1, nullifier_round_2,
2024            "honest redelegation in a new round must not collide with the old round"
2025        );
2026    }
2027
2028    #[test]
2029    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2030    fn round_scoped_van_redelegation_verifies_with_distinct_nullifiers() {
2031        let fixture = VoteReuseFixture::new();
2032        let round_1 = pallas::Base::from(0xCAFEu64);
2033        let round_2 = pallas::Base::from(0xCAFFu64);
2034
2035        let van_round_1 = fixture.vote_authority_note_old(round_1);
2036        let (path_round_1, position_round_1, root_round_1) =
2037            build_single_leaf_merkle_path(van_round_1);
2038        let (circuit_round_1, instance_round_1) =
2039            fixture.build_vote_data(round_1, path_round_1, position_round_1, root_round_1, 10);
2040
2041        let van_round_2 = fixture.vote_authority_note_old(round_2);
2042        let (path_round_2, position_round_2, root_round_2) =
2043            build_single_leaf_merkle_path(van_round_2);
2044        let (circuit_round_2, instance_round_2) =
2045            fixture.build_vote_data(round_2, path_round_2, position_round_2, root_round_2, 20);
2046
2047        assert_ne!(van_round_1, van_round_2);
2048        assert_ne!(
2049            instance_round_1.van_nullifier,
2050            instance_round_2.van_nullifier
2051        );
2052
2053        let prover_round_1 = MockProver::run(
2054            K,
2055            &circuit_round_1,
2056            vec![instance_round_1.to_halo2_instance()],
2057        )
2058        .unwrap();
2059        assert_eq!(prover_round_1.verify(), Ok(()));
2060
2061        let prover_round_2 = MockProver::run(
2062            K,
2063            &circuit_round_2,
2064            vec![instance_round_2.to_halo2_instance()],
2065        )
2066        .unwrap();
2067        assert_eq!(prover_round_2.verify(), Ok(()));
2068    }
2069
2070    /// Verifies the out-of-circuit helper produces deterministic results.
2071    #[test]
2072    fn van_integrity_hash_deterministic() {
2073        let mut rng = OsRng;
2074
2075        let vpk_g_d = pallas::Base::random(&mut rng);
2076        let vpk_pk_d = pallas::Base::random(&mut rng);
2077        let val = pallas::Base::random(&mut rng);
2078        let round = pallas::Base::random(&mut rng);
2079        let auth = pallas::Base::random(&mut rng);
2080        let rand = pallas::Base::random(&mut rng);
2081
2082        let h1 = van_integrity_hash(vpk_g_d, vpk_pk_d, val, round, auth, rand);
2083        let h2 = van_integrity_hash(vpk_g_d, vpk_pk_d, val, round, auth, rand);
2084        assert_eq!(h1, h2);
2085
2086        // Changing any input changes the hash.
2087        let h3 = van_integrity_hash(
2088            pallas::Base::random(&mut rng),
2089            vpk_pk_d,
2090            val,
2091            round,
2092            auth,
2093            rand,
2094        );
2095        assert_ne!(h1, h3);
2096    }
2097
2098    // ================================================================
2099    // Condition 3 (Diversified Address Integrity / Address Ownership) tests
2100    //
2101    // These tests ensure the circuit rejects witnesses that violate
2102    // vpk_pk_d = [ivk_v] * vpk_g_d. Without condition 3 enabled, they
2103    // would pass (invalid address ownership would not be detected).
2104    // ================================================================
2105
2106    /// Using a different vsk in the circuit than was used to derive
2107    /// (vpk_g_d, vpk_pk_d) should fail condition 3 only: in-circuit
2108    /// [ivk']*vpk_g_d ≠ vpk_pk_d while VAN hash and nullifier stay valid.
2109    #[test]
2110    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2111    fn condition_3_wrong_vsk_fails() {
2112        let mut rng = OsRng;
2113
2114        let vsk = pallas::Scalar::random(&mut rng);
2115        let vsk_nk = pallas::Base::random(&mut rng);
2116        let rivk_v = pallas::Scalar::random(&mut rng);
2117        let (vpk_g_d_affine, vpk_pk_d_affine) = derive_voting_address(vsk, vsk_nk, rivk_v);
2118        let vpk_g_d_x = *vpk_g_d_affine.coordinates().unwrap().x();
2119        let vpk_pk_d_x = *vpk_pk_d_affine.coordinates().unwrap().x();
2120
2121        let total_note_value = pallas::Base::from(10_000u64);
2122        let voting_round_id = pallas::Base::random(&mut rng);
2123        let proposal_authority_old = pallas::Base::from(13u64);
2124        let proposal_id = 3u64;
2125        let van_comm_rand = pallas::Base::random(&mut rng);
2126
2127        let vote_authority_note_old = van_integrity_hash(
2128            vpk_g_d_x,
2129            vpk_pk_d_x,
2130            total_note_value,
2131            voting_round_id,
2132            proposal_authority_old,
2133            van_comm_rand,
2134        );
2135        let (auth_path, position, vote_comm_tree_root) =
2136            build_single_leaf_merkle_path(vote_authority_note_old);
2137        let van_nullifier = van_nullifier_hash(vsk_nk, voting_round_id, vote_authority_note_old);
2138        let one_shifted = pallas::Base::from(1u64 << proposal_id);
2139        let proposal_authority_new = proposal_authority_old - one_shifted;
2140        let vote_authority_note_new = van_integrity_hash(
2141            vpk_g_d_x,
2142            vpk_pk_d_x,
2143            total_note_value,
2144            voting_round_id,
2145            proposal_authority_new,
2146            van_comm_rand,
2147        );
2148
2149        let shares_u64: [u64; 16] = [625; 16];
2150        let (_ea_sk, ea_pk) = generate_ea_keypair();
2151        let (enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y, randomness, share_blinds, shares_hash_val) =
2152            encrypt_shares(shares_u64, ea_pk);
2153
2154        let wrong_vsk = pallas::Scalar::random(&mut rng);
2155        assert_ne!(
2156            wrong_vsk, vsk,
2157            "test assumes distinct vsk with high probability"
2158        );
2159        let alpha_v = pallas::Scalar::random(&mut rng);
2160        let g = spend_auth_g_affine();
2161        let r_vpk = (g * (vsk + alpha_v)).to_affine();
2162        let r_vpk_x = *r_vpk.coordinates().unwrap().x();
2163        let r_vpk_y = *r_vpk.coordinates().unwrap().y();
2164
2165        let mut circuit = Circuit::with_van_witnesses(
2166            Value::known(auth_path),
2167            Value::known(position),
2168            Value::known(vpk_g_d_affine),
2169            Value::known(vpk_pk_d_affine),
2170            Value::known(total_note_value),
2171            Value::known(proposal_authority_old),
2172            Value::known(van_comm_rand),
2173            Value::known(vote_authority_note_old),
2174            Value::known(wrong_vsk),
2175            Value::known(rivk_v),
2176            Value::known(vsk_nk),
2177            Value::known(alpha_v),
2178        );
2179        circuit.one_shifted = Value::known(one_shifted);
2180        circuit.shares = shares_u64.map(|s| Value::known(pallas::Base::from(s)));
2181        circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
2182        circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
2183        circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
2184        circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
2185        circuit.share_blinds = share_blinds.map(Value::known);
2186        circuit.share_randomness = randomness.map(Value::known);
2187        circuit.ea_pk = Value::known(ea_pk);
2188        let vc = set_condition_11(&mut circuit, shares_hash_val, proposal_id, voting_round_id);
2189
2190        let instance = Instance::from_parts(
2191            van_nullifier,
2192            r_vpk_x,
2193            r_vpk_y,
2194            vote_authority_note_new,
2195            vc,
2196            vote_comm_tree_root,
2197            pallas::Base::zero(),
2198            pallas::Base::from(proposal_id),
2199            voting_round_id,
2200            *ea_pk.coordinates().unwrap().x(),
2201            *ea_pk.coordinates().unwrap().y(),
2202        );
2203
2204        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2205        assert!(
2206            prover.verify().is_err(),
2207            "condition 3 must reject wrong vsk"
2208        );
2209    }
2210
2211    /// Using a vpk_pk_d that does not equal [ivk_v]*vpk_g_d should fail
2212    /// condition 3. Instance is built with a wrong vpk_pk_d for the VAN
2213    /// hash so condition 2 still passes; only condition 3 fails.
2214    #[test]
2215    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2216    fn condition_3_wrong_vpk_pk_d_fails() {
2217        let mut rng = OsRng;
2218
2219        let vsk = pallas::Scalar::random(&mut rng);
2220        let vsk_nk = pallas::Base::random(&mut rng);
2221        let rivk_v = pallas::Scalar::random(&mut rng);
2222        let (vpk_g_d_affine, _vpk_pk_d_correct) = derive_voting_address(vsk, vsk_nk, rivk_v);
2223        let vpk_g_d_x = *vpk_g_d_affine.coordinates().unwrap().x();
2224
2225        let wrong_vpk_pk_d_affine =
2226            (pallas::Point::generator() * pallas::Scalar::from(99999u64)).to_affine();
2227        let wrong_vpk_pk_d_x = *wrong_vpk_pk_d_affine.coordinates().unwrap().x();
2228
2229        let total_note_value = pallas::Base::from(10_000u64);
2230        let voting_round_id = pallas::Base::random(&mut rng);
2231        let proposal_authority_old = pallas::Base::from(13u64);
2232        let proposal_id = 3u64;
2233        let van_comm_rand = pallas::Base::random(&mut rng);
2234
2235        let vote_authority_note_old = van_integrity_hash(
2236            vpk_g_d_x,
2237            wrong_vpk_pk_d_x,
2238            total_note_value,
2239            voting_round_id,
2240            proposal_authority_old,
2241            van_comm_rand,
2242        );
2243        let (auth_path, position, vote_comm_tree_root) =
2244            build_single_leaf_merkle_path(vote_authority_note_old);
2245        let van_nullifier = van_nullifier_hash(vsk_nk, voting_round_id, vote_authority_note_old);
2246        let one_shifted = pallas::Base::from(1u64 << proposal_id);
2247        let proposal_authority_new = proposal_authority_old - one_shifted;
2248        let vote_authority_note_new = van_integrity_hash(
2249            vpk_g_d_x,
2250            wrong_vpk_pk_d_x,
2251            total_note_value,
2252            voting_round_id,
2253            proposal_authority_new,
2254            van_comm_rand,
2255        );
2256
2257        let shares_u64: [u64; 16] = [625; 16];
2258        let (_ea_sk, ea_pk) = generate_ea_keypair();
2259        let (enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y, randomness, share_blinds, shares_hash_val) =
2260            encrypt_shares(shares_u64, ea_pk);
2261
2262        let alpha_v = pallas::Scalar::random(&mut rng);
2263        let g = spend_auth_g_affine();
2264        let r_vpk = (g * (vsk + alpha_v)).to_affine();
2265        let r_vpk_x = *r_vpk.coordinates().unwrap().x();
2266        let r_vpk_y = *r_vpk.coordinates().unwrap().y();
2267
2268        let mut circuit = Circuit::with_van_witnesses(
2269            Value::known(auth_path),
2270            Value::known(position),
2271            Value::known(vpk_g_d_affine),
2272            Value::known(wrong_vpk_pk_d_affine),
2273            Value::known(total_note_value),
2274            Value::known(proposal_authority_old),
2275            Value::known(van_comm_rand),
2276            Value::known(vote_authority_note_old),
2277            Value::known(vsk),
2278            Value::known(rivk_v),
2279            Value::known(vsk_nk),
2280            Value::known(alpha_v),
2281        );
2282        circuit.one_shifted = Value::known(one_shifted);
2283        circuit.shares = shares_u64.map(|s| Value::known(pallas::Base::from(s)));
2284        circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
2285        circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
2286        circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
2287        circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
2288        circuit.share_blinds = share_blinds.map(Value::known);
2289        circuit.share_randomness = randomness.map(Value::known);
2290        circuit.ea_pk = Value::known(ea_pk);
2291        let vc = set_condition_11(&mut circuit, shares_hash_val, proposal_id, voting_round_id);
2292
2293        let instance = Instance::from_parts(
2294            van_nullifier,
2295            r_vpk_x,
2296            r_vpk_y,
2297            vote_authority_note_new,
2298            vc,
2299            vote_comm_tree_root,
2300            pallas::Base::zero(),
2301            pallas::Base::from(proposal_id),
2302            voting_round_id,
2303            *ea_pk.coordinates().unwrap().x(),
2304            *ea_pk.coordinates().unwrap().y(),
2305        );
2306
2307        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2308        assert!(
2309            prover.verify().is_err(),
2310            "condition 3 must reject wrong vpk_pk_d"
2311        );
2312    }
2313
2314    // ================================================================
2315    // Condition 4 (Spend Authority) tests
2316    // ================================================================
2317
2318    /// Wrong r_vpk public input should fail condition 4.
2319    #[test]
2320    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2321    fn condition_4_wrong_r_vpk_fails() {
2322        let (circuit, mut instance) = make_test_data();
2323
2324        instance.r_vpk_x = pallas::Base::random(&mut OsRng);
2325
2326        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2327        assert!(
2328            prover.verify().is_err(),
2329            "condition 4 must reject wrong r_vpk"
2330        );
2331    }
2332
2333    /// Documents the current upstream-compatible relation: alpha_v = 0 is
2334    /// accepted when the public r_vpk is correspondingly equal to ak_P. This is
2335    /// a self-linking/coercion surface, not a proof-soundness failure; see
2336    /// THREAT_MODEL.md.
2337    #[test]
2338    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2339    fn condition_4_alpha_zero_is_accepted_by_relation() {
2340        let (circuit, instance) = make_test_data_with_authority_proposal_and_alpha(
2341            pallas::Base::from(13u64),
2342            TEST_PROPOSAL_ID,
2343            Some(pallas::Scalar::zero()),
2344        );
2345
2346        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2347        assert_eq!(prover.verify(), Ok(()));
2348    }
2349
2350    // ================================================================
2351    // Condition 5 (VAN Nullifier Integrity) tests
2352    // ================================================================
2353
2354    /// Wrong VAN_NULLIFIER_PUBLIC_OFFSET public input should fail condition 5.
2355    #[test]
2356    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2357    fn van_nullifier_wrong_public_input_fails() {
2358        let (circuit, mut instance) = make_test_data();
2359
2360        // Corrupt the VAN nullifier public input.
2361        instance.van_nullifier = pallas::Base::random(&mut OsRng);
2362
2363        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2364
2365        // Should fail: circuit-derived nullifier ≠ corrupted instance value.
2366        assert!(prover.verify().is_err());
2367    }
2368
2369    /// Using a different vsk_nk in the circuit than was used to compute
2370    /// the instance nullifier should fail condition 5.
2371    /// Note: since vsk_nk is also used in CommitIvk (condition 3), the
2372    /// wrong value also breaks condition 3 — but the test still verifies
2373    /// that the proof fails as expected.
2374    #[test]
2375    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2376    fn van_nullifier_wrong_vsk_nk_fails() {
2377        let mut rng = OsRng;
2378
2379        // Derive proper keys with the CORRECT vsk_nk.
2380        let vsk = pallas::Scalar::random(&mut rng);
2381        let vsk_nk = pallas::Base::random(&mut rng);
2382        let rivk_v = pallas::Scalar::random(&mut rng);
2383        let (vpk_g_d_affine, vpk_pk_d_affine) = derive_voting_address(vsk, vsk_nk, rivk_v);
2384        let vpk_g_d_x = *vpk_g_d_affine.coordinates().unwrap().x();
2385        let vpk_pk_d_x = *vpk_pk_d_affine.coordinates().unwrap().x();
2386
2387        let total_note_value = pallas::Base::from(10_000u64);
2388        let voting_round_id = pallas::Base::random(&mut rng);
2389        let proposal_authority_old = pallas::Base::from(5u64); // bits 0 and 2 set
2390        let van_comm_rand = pallas::Base::random(&mut rng);
2391        let proposal_id = 0u64; // vote on proposal 0 so one_shifted = 1, new = 4
2392
2393        let vote_authority_note_old = van_integrity_hash(
2394            vpk_g_d_x,
2395            vpk_pk_d_x,
2396            total_note_value,
2397            voting_round_id,
2398            proposal_authority_old,
2399            van_comm_rand,
2400        );
2401        let (auth_path, position, vote_comm_tree_root) =
2402            build_single_leaf_merkle_path(vote_authority_note_old);
2403        let van_nullifier = van_nullifier_hash(vsk_nk, voting_round_id, vote_authority_note_old);
2404        let one_shifted = pallas::Base::from(1u64 << proposal_id);
2405        let proposal_authority_new = proposal_authority_old - one_shifted;
2406        let vote_authority_note_new = van_integrity_hash(
2407            vpk_g_d_x,
2408            vpk_pk_d_x,
2409            total_note_value,
2410            voting_round_id,
2411            proposal_authority_new,
2412            van_comm_rand,
2413        );
2414
2415        // Use a DIFFERENT vsk_nk in the circuit.
2416        let wrong_vsk_nk = pallas::Base::random(&mut rng);
2417        let alpha_v = pallas::Scalar::random(&mut rng);
2418        let g = spend_auth_g_affine();
2419        let r_vpk = (g * (vsk + alpha_v)).to_affine();
2420        let r_vpk_x = *r_vpk.coordinates().unwrap().x();
2421        let r_vpk_y = *r_vpk.coordinates().unwrap().y();
2422
2423        // Shares that sum to total_note_value (conditions 8 + 9).
2424        let shares_u64: [u64; 16] = [625; 16];
2425
2426        // Condition 11: real El Gamal encryption.
2427        let (_ea_sk, ea_pk) = generate_ea_keypair();
2428        let (enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y, randomness, share_blinds, shares_hash_val) =
2429            encrypt_shares(shares_u64, ea_pk);
2430
2431        let mut circuit = Circuit::with_van_witnesses(
2432            Value::known(auth_path),
2433            Value::known(position),
2434            Value::known(vpk_g_d_affine),
2435            Value::known(vpk_pk_d_affine),
2436            Value::known(total_note_value),
2437            Value::known(proposal_authority_old),
2438            Value::known(van_comm_rand),
2439            Value::known(vote_authority_note_old),
2440            Value::known(vsk),
2441            Value::known(rivk_v),
2442            Value::known(wrong_vsk_nk),
2443            Value::known(alpha_v),
2444        );
2445        circuit.one_shifted = Value::known(one_shifted);
2446        circuit.shares = shares_u64.map(|s| Value::known(pallas::Base::from(s)));
2447        circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
2448        circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
2449        circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
2450        circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
2451        circuit.share_blinds = share_blinds.map(Value::known);
2452        circuit.share_randomness = randomness.map(Value::known);
2453        circuit.ea_pk = Value::known(ea_pk);
2454        let vc = set_condition_11(&mut circuit, shares_hash_val, proposal_id, voting_round_id);
2455
2456        let instance = Instance::from_parts(
2457            van_nullifier,
2458            r_vpk_x,
2459            r_vpk_y,
2460            vote_authority_note_new,
2461            vc,
2462            vote_comm_tree_root,
2463            pallas::Base::zero(),
2464            pallas::Base::from(proposal_id),
2465            voting_round_id,
2466            *ea_pk.coordinates().unwrap().x(),
2467            *ea_pk.coordinates().unwrap().y(),
2468        );
2469
2470        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2471        // Should fail: circuit computes Poseidon(wrong_vsk_nk, inner_hash)
2472        // which ≠ the instance van_nullifier (computed with correct vsk_nk).
2473        // Also fails condition 3 since wrong_vsk_nk breaks CommitIvk derivation.
2474        assert!(prover.verify().is_err());
2475    }
2476
2477    /// Verifies the out-of-circuit nullifier helper produces deterministic results.
2478    #[test]
2479    fn van_nullifier_hash_deterministic() {
2480        let mut rng = OsRng;
2481
2482        let nk = pallas::Base::random(&mut rng);
2483        let round = pallas::Base::random(&mut rng);
2484        let van = pallas::Base::random(&mut rng);
2485
2486        let h1 = van_nullifier_hash(nk, round, van);
2487        let h2 = van_nullifier_hash(nk, round, van);
2488        assert_eq!(h1, h2);
2489
2490        // Changing any input changes the hash.
2491        let h3 = van_nullifier_hash(pallas::Base::random(&mut rng), round, van);
2492        assert_ne!(h1, h3);
2493    }
2494
2495    #[test]
2496    fn van_nullifier_hash_frozen_vector() {
2497        assert_eq!(
2498            van_nullifier_hash(
2499                pallas::Base::from(1u64),
2500                pallas::Base::from(42u64),
2501                pallas::Base::from(100u64),
2502            ),
2503            pallas::Base::from_repr([
2504                114, 56, 62, 208, 155, 244, 76, 209, 125, 210, 149, 109, 176, 88, 34, 116, 123, 56,
2505                62, 216, 108, 204, 55, 120, 28, 155, 217, 186, 29, 159, 128, 2,
2506            ])
2507            .expect("frozen vector must be canonical")
2508        );
2509    }
2510
2511    #[test]
2512    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2513    fn stale_and_current_anchor_proofs_for_same_van_share_nullifier() {
2514        let fixture = VoteReuseFixture::new();
2515        let voting_round_id = pallas::Base::from(0xCAFEu64);
2516        let stale_van = fixture.vote_authority_note_old(voting_round_id);
2517        let successor_van = fixture.vote_authority_note_new(voting_round_id);
2518
2519        let (stale_path, stale_position, stale_root) = build_single_leaf_merkle_path(stale_van);
2520        let (stale_circuit, stale_instance) =
2521            fixture.build_vote_data(voting_round_id, stale_path, stale_position, stale_root, 10);
2522
2523        let (current_path, current_position, current_root) =
2524            build_left_leaf_merkle_path_with_sibling(stale_van, successor_van);
2525        let (current_circuit, current_instance) = fixture.build_vote_data(
2526            voting_round_id,
2527            current_path,
2528            current_position,
2529            current_root,
2530            11,
2531        );
2532
2533        assert_ne!(
2534            stale_root, current_root,
2535            "the successor VAN changes the supplied tree anchor"
2536        );
2537        assert_eq!(
2538            stale_instance.van_nullifier, current_instance.van_nullifier,
2539            "same (vsk_nk, voting_round_id, VAN) must collide for chain-side nullifier uniqueness"
2540        );
2541
2542        // The circuit only proves membership in the supplied root. Freshness of
2543        // the height-to-root mapping is enforced by the chain ante handler.
2544        let stale_prover =
2545            MockProver::run(K, &stale_circuit, vec![stale_instance.to_halo2_instance()]).unwrap();
2546        assert_eq!(stale_prover.verify(), Ok(()));
2547
2548        let current_prover = MockProver::run(
2549            K,
2550            &current_circuit,
2551            vec![current_instance.to_halo2_instance()],
2552        )
2553        .unwrap();
2554        assert_eq!(current_prover.verify(), Ok(()));
2555    }
2556
2557    /// Verifies the domain tag is non-zero and deterministic.
2558    #[test]
2559    fn domain_van_nullifier_deterministic() {
2560        let d1 = domain_van_nullifier();
2561        let d2 = domain_van_nullifier();
2562        assert_eq!(d1, d2);
2563
2564        // Must differ from DOMAIN_VAN (which is 0).
2565        assert_ne!(d1, pallas::Base::zero());
2566    }
2567
2568    // ================================================================
2569    // Condition 6 (Proposal Authority Decrement) tests
2570    // ================================================================
2571
2572    /// Proposal authority with only bit 0 set (value 1): vote on proposal 0, new = 0.
2573    #[test]
2574    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2575    fn proposal_authority_decrement_minimum_valid() {
2576        // proposal_id = 0 is now forbidden (sentinel value); use the next smallest valid id.
2577        // Authority = 2 = 0b0010 has exactly bit 1 set, so proposal_id = 1 is valid.
2578        // After decrement: proposal_authority_new = 0 (minimum possible outcome).
2579        let (circuit, instance) =
2580            make_test_data_with_authority_and_proposal(pallas::Base::from(2u64), 1);
2581
2582        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2583        assert_eq!(prover.verify(), Ok(()));
2584    }
2585
2586    /// With proposal_authority_old = 0, the selected bit is 0 so the
2587    /// "run_selected = 1" constraint (selected bit was set) fails.
2588    #[test]
2589    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2590    fn proposal_authority_zero_fails() {
2591        let (circuit, instance) = make_test_data_with_authority(pallas::Base::zero());
2592
2593        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2594
2595        assert!(prover.verify().is_err());
2596    }
2597
2598    /// proposal_id = 0 is the dummy sentinel value and must be rejected (Cond 6, gate).
2599    #[test]
2600    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2601    fn proposal_id_zero_fails() {
2602        // Authority = 1 = 0b0001 has bit 0 set, so this is otherwise a structurally
2603        // valid decrement — the only reason it must fail is the non-zero gate.
2604        let (circuit, instance) =
2605            make_test_data_with_authority_and_proposal(pallas::Base::one(), 0);
2606
2607        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2608        assert!(prover.verify().is_err(), "proposal_id = 0 must be rejected");
2609    }
2610
2611    /// Full authority (65535), proposal_id 1 → new = 65533 (e2e scenario).
2612    #[test]
2613    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2614    fn proposal_authority_full_authority_proposal_1_passes() {
2615        const MAX_PROPOSAL_AUTHORITY: u64 = 65535;
2616        let (circuit, instance) = make_test_data_with_authority_and_proposal(
2617            pallas::Base::from(MAX_PROPOSAL_AUTHORITY),
2618            1,
2619        );
2620
2621        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2622        assert_eq!(prover.verify(), Ok(()));
2623    }
2624
2625    /// Wrong vote_authority_note_new (e.g. not clearing the bit) fails condition 6.
2626    #[test]
2627    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2628    fn proposal_authority_wrong_new_fails() {
2629        let (circuit, mut instance) =
2630            make_test_data_with_authority_and_proposal(pallas::Base::from(65535u64), 1);
2631
2632        instance.vote_authority_note_new = pallas::Base::random(&mut OsRng);
2633
2634        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2635        assert!(prover.verify().is_err());
2636    }
2637
2638    /// authority=4 (0b0100, bit 2 set only), proposal_id=1 (bit 1 absent) →
2639    /// run_selected=0 at the terminal row, so "run_selected = 1" fails.
2640    /// Uses proposal_id=1 (not 0) to isolate this constraint from the
2641    /// proposal_id != 0 sentinel gate.
2642    #[test]
2643    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2644    fn proposal_authority_bit_not_set_fails() {
2645        let (circuit, instance) =
2646            make_test_data_with_authority_and_proposal(pallas::Base::from(4u64), 1);
2647
2648        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2649        assert!(prover.verify().is_err());
2650    }
2651
2652    /// Condition 6 enforces run_sel = 1 (exactly one selector active) at the last bit row;
2653    /// see CONDITION_6_RUN_SEL_FIX.md. This test runs a valid proof (one selector) and
2654    /// verifies it passes; a zero-selector witness would be rejected by that gate.
2655    #[test]
2656    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2657    fn proposal_authority_condition6_run_sel_constraint() {
2658        let (circuit, instance) =
2659            make_test_data_with_authority_and_proposal(pallas::Base::from(3u64), 1);
2660
2661        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2662        assert_eq!(prover.verify(), Ok(()));
2663    }
2664
2665    /// proposal_authority_old = 65536 = 2^16 lies outside the valid 16-bit bitmask
2666    /// range [0, 65535]. The authority_decrement gadget decomposes the value into
2667    /// exactly 16 bits (positions 0–15); a value with bit 16 set cannot be represented
2668    /// in that decomposition and must be rejected by the range check.
2669    #[test]
2670    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2671    fn proposal_authority_exceeds_16_bits_fails() {
2672        // 65536 = 2^16 is the first value not representable as a 16-bit bitmask.
2673        let (circuit, instance) = make_test_data_with_authority(pallas::Base::from(65536u64));
2674        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2675        assert!(
2676            prover.verify().is_err(),
2677            "authority > 65535 must be rejected by the 16-bit bit decomposition"
2678        );
2679    }
2680
2681    // ================================================================
2682    // Condition 7 (New VAN Integrity) tests
2683    // ================================================================
2684
2685    /// Wrong vote_authority_note_new public input should fail condition 7.
2686    #[test]
2687    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2688    fn new_van_integrity_wrong_public_input_fails() {
2689        let (circuit, mut instance) = make_test_data();
2690
2691        // Corrupt the new VAN public input.
2692        instance.vote_authority_note_new = pallas::Base::random(&mut OsRng);
2693
2694        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2695
2696        // Should fail: circuit-derived new VAN ≠ corrupted instance value.
2697        assert!(prover.verify().is_err());
2698    }
2699
2700    /// New VAN integrity with a large (but valid) 16-bit proposal authority.
2701    /// Authority 0xFFF8 has bits 3..15 set; voting on proposal 3 gives new = 0xFFF0.
2702    #[test]
2703    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2704    fn new_van_integrity_large_authority() {
2705        let (circuit, instance) = make_test_data_with_authority(pallas::Base::from(0xFFF8u64));
2706
2707        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2708        assert_eq!(prover.verify(), Ok(()));
2709    }
2710
2711    // ================================================================
2712    // Condition 1 (VAN Membership) tests
2713    // ================================================================
2714
2715    /// Wrong vote_comm_tree_root in the instance should fail condition 1.
2716    #[test]
2717    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2718    fn van_membership_wrong_root_fails() {
2719        let (circuit, mut instance) = make_test_data();
2720
2721        // Corrupt the tree root.
2722        instance.vote_comm_tree_root = pallas::Base::random(&mut OsRng);
2723
2724        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2725        assert!(prover.verify().is_err());
2726    }
2727
2728    /// A VAN at a non-zero position in the tree should verify.
2729    #[test]
2730    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2731    fn van_membership_nonzero_position() {
2732        let mut rng = OsRng;
2733
2734        // Derive proper voting key hierarchy.
2735        let vsk = pallas::Scalar::random(&mut rng);
2736        let vsk_nk = pallas::Base::random(&mut rng);
2737        let rivk_v = pallas::Scalar::random(&mut rng);
2738        let (vpk_g_d_affine, vpk_pk_d_affine) = derive_voting_address(vsk, vsk_nk, rivk_v);
2739        let vpk_g_d_x = *vpk_g_d_affine.coordinates().unwrap().x();
2740        let vpk_pk_d_x = *vpk_pk_d_affine.coordinates().unwrap().x();
2741
2742        let total_note_value = pallas::Base::from(10_000u64);
2743        let voting_round_id = pallas::Base::random(&mut rng);
2744        let proposal_authority_old = pallas::Base::from(5u64); // bits 0 and 2 set
2745                                                               // proposal_id = 0 is now forbidden (sentinel); use proposal_id = 2 (bit 2 is set in 5).
2746        let proposal_id = 2u64;
2747        let van_comm_rand = pallas::Base::random(&mut rng);
2748
2749        let vote_authority_note_old = van_integrity_hash(
2750            vpk_g_d_x,
2751            vpk_pk_d_x,
2752            total_note_value,
2753            voting_round_id,
2754            proposal_authority_old,
2755            van_comm_rand,
2756        );
2757
2758        // Place the leaf at position 7 (binary: ...0111).
2759        let position: u32 = 7;
2760        let mut empty_roots = [pallas::Base::zero(); VOTE_COMM_TREE_DEPTH];
2761        empty_roots[0] = poseidon_hash_2(pallas::Base::zero(), pallas::Base::zero());
2762        for i in 1..VOTE_COMM_TREE_DEPTH {
2763            empty_roots[i] = poseidon_hash_2(empty_roots[i - 1], empty_roots[i - 1]);
2764        }
2765        let auth_path = empty_roots;
2766        let mut current = vote_authority_note_old;
2767        for i in 0..VOTE_COMM_TREE_DEPTH {
2768            if (position >> i) & 1 == 0 {
2769                current = poseidon_hash_2(current, auth_path[i]);
2770            } else {
2771                current = poseidon_hash_2(auth_path[i], current);
2772            }
2773        }
2774        let vote_comm_tree_root = current;
2775
2776        let van_nullifier = van_nullifier_hash(vsk_nk, voting_round_id, vote_authority_note_old);
2777        let one_shifted = pallas::Base::from(1u64 << proposal_id);
2778        let proposal_authority_new = proposal_authority_old - one_shifted;
2779        let vote_authority_note_new = van_integrity_hash(
2780            vpk_g_d_x,
2781            vpk_pk_d_x,
2782            total_note_value,
2783            voting_round_id,
2784            proposal_authority_new,
2785            van_comm_rand,
2786        );
2787
2788        let alpha_v = pallas::Scalar::random(&mut rng);
2789        let g = spend_auth_g_affine();
2790        let r_vpk = (g * (vsk + alpha_v)).to_affine();
2791        let r_vpk_x = *r_vpk.coordinates().unwrap().x();
2792        let r_vpk_y = *r_vpk.coordinates().unwrap().y();
2793
2794        // Shares that sum to total_note_value (conditions 8 + 9).
2795        let shares_u64: [u64; 16] = [625; 16];
2796
2797        // Condition 11: real El Gamal encryption.
2798        let (_ea_sk, ea_pk) = generate_ea_keypair();
2799        let (enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y, randomness, share_blinds, shares_hash_val) =
2800            encrypt_shares(shares_u64, ea_pk);
2801
2802        let mut circuit = Circuit::with_van_witnesses(
2803            Value::known(auth_path),
2804            Value::known(position),
2805            Value::known(vpk_g_d_affine),
2806            Value::known(vpk_pk_d_affine),
2807            Value::known(total_note_value),
2808            Value::known(proposal_authority_old),
2809            Value::known(van_comm_rand),
2810            Value::known(vote_authority_note_old),
2811            Value::known(vsk),
2812            Value::known(rivk_v),
2813            Value::known(vsk_nk),
2814            Value::known(alpha_v),
2815        );
2816        circuit.one_shifted = Value::known(one_shifted);
2817        circuit.shares = shares_u64.map(|s| Value::known(pallas::Base::from(s)));
2818        circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
2819        circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
2820        circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
2821        circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
2822        circuit.share_blinds = share_blinds.map(Value::known);
2823        circuit.share_randomness = randomness.map(Value::known);
2824        circuit.ea_pk = Value::known(ea_pk);
2825        let vc = set_condition_11(&mut circuit, shares_hash_val, proposal_id, voting_round_id);
2826
2827        let instance = Instance::from_parts(
2828            van_nullifier,
2829            r_vpk_x,
2830            r_vpk_y,
2831            vote_authority_note_new,
2832            vc,
2833            vote_comm_tree_root,
2834            pallas::Base::zero(),
2835            pallas::Base::from(proposal_id),
2836            voting_round_id,
2837            *ea_pk.coordinates().unwrap().x(),
2838            *ea_pk.coordinates().unwrap().y(),
2839        );
2840
2841        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2842        assert_eq!(prover.verify(), Ok(()));
2843    }
2844
2845    /// Poseidon hash-2 helper is deterministic.
2846    #[test]
2847    fn poseidon_hash_2_deterministic() {
2848        let mut rng = OsRng;
2849        let a = pallas::Base::random(&mut rng);
2850        let b = pallas::Base::random(&mut rng);
2851
2852        assert_eq!(poseidon_hash_2(a, b), poseidon_hash_2(a, b));
2853        // Non-commutative.
2854        assert_ne!(poseidon_hash_2(a, b), poseidon_hash_2(b, a));
2855    }
2856
2857    // ================================================================
2858    // Condition 8 (Shares Sum Correctness) tests
2859    // ================================================================
2860
2861    /// Shares that do NOT sum to total_note_value should fail condition 8.
2862    #[test]
2863    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2864    fn shares_sum_wrong_total_fails() {
2865        let (mut circuit, instance) = make_test_data();
2866
2867        // Corrupt shares[3] so the sum no longer equals total_note_value.
2868        // Use a small value that still passes condition 9's range check,
2869        // isolating the condition 8 failure.
2870        circuit.shares[3] = Value::known(pallas::Base::from(999u64));
2871
2872        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2873        // Should fail: shares sum ≠ total_note_value.
2874        assert!(prover.verify().is_err());
2875    }
2876
2877    // ================================================================
2878    // Condition 9 (Shares Range) tests
2879    // ================================================================
2880
2881    /// A share at the maximum valid value (2^30 - 1) should pass.
2882    #[test]
2883    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2884    fn shares_range_max_valid() {
2885        let max_share = pallas::Base::from((1u64 << 30) - 1); // 1,073,741,823
2886        let total = (0..16).fold(pallas::Base::zero(), |acc, _| acc + max_share);
2887
2888        let mut rng = OsRng;
2889        // Derive proper voting key hierarchy.
2890        let vsk = pallas::Scalar::random(&mut rng);
2891        let vsk_nk = pallas::Base::random(&mut rng);
2892        let rivk_v = pallas::Scalar::random(&mut rng);
2893        let (vpk_g_d_affine, vpk_pk_d_affine) = derive_voting_address(vsk, vsk_nk, rivk_v);
2894        let vpk_g_d_x = *vpk_g_d_affine.coordinates().unwrap().x();
2895        let vpk_pk_d_x = *vpk_pk_d_affine.coordinates().unwrap().x();
2896
2897        let voting_round_id = pallas::Base::random(&mut rng);
2898        let proposal_authority_old = pallas::Base::from(5u64); // bits 0 and 2 set
2899                                                               // proposal_id = 0 is now forbidden (sentinel); use proposal_id = 2 (bit 2 is set in 5).
2900        let proposal_id = 2u64;
2901        let van_comm_rand = pallas::Base::random(&mut rng);
2902
2903        let vote_authority_note_old = van_integrity_hash(
2904            vpk_g_d_x,
2905            vpk_pk_d_x,
2906            total,
2907            voting_round_id,
2908            proposal_authority_old,
2909            van_comm_rand,
2910        );
2911        let (auth_path, position, vote_comm_tree_root) =
2912            build_single_leaf_merkle_path(vote_authority_note_old);
2913        let van_nullifier = van_nullifier_hash(vsk_nk, voting_round_id, vote_authority_note_old);
2914        let one_shifted = pallas::Base::from(1u64 << proposal_id);
2915        let proposal_authority_new = proposal_authority_old - one_shifted;
2916        let vote_authority_note_new = van_integrity_hash(
2917            vpk_g_d_x,
2918            vpk_pk_d_x,
2919            total,
2920            voting_round_id,
2921            proposal_authority_new,
2922            van_comm_rand,
2923        );
2924
2925        // Condition 11: real El Gamal encryption with max-value shares.
2926        let max_share_u64 = (1u64 << 30) - 1;
2927        let shares_u64: [u64; 16] = [max_share_u64; 16];
2928        let (_ea_sk, ea_pk) = generate_ea_keypair();
2929        let (enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y, randomness, share_blinds, shares_hash_val) =
2930            encrypt_shares(shares_u64, ea_pk);
2931
2932        let alpha_v = pallas::Scalar::random(&mut rng);
2933        let g = spend_auth_g_affine();
2934        let r_vpk = (g * (vsk + alpha_v)).to_affine();
2935        let r_vpk_x = *r_vpk.coordinates().unwrap().x();
2936        let r_vpk_y = *r_vpk.coordinates().unwrap().y();
2937
2938        let mut circuit = Circuit::with_van_witnesses(
2939            Value::known(auth_path),
2940            Value::known(position),
2941            Value::known(vpk_g_d_affine),
2942            Value::known(vpk_pk_d_affine),
2943            Value::known(total),
2944            Value::known(proposal_authority_old),
2945            Value::known(van_comm_rand),
2946            Value::known(vote_authority_note_old),
2947            Value::known(vsk),
2948            Value::known(rivk_v),
2949            Value::known(vsk_nk),
2950            Value::known(alpha_v),
2951        );
2952        circuit.one_shifted = Value::known(one_shifted);
2953        circuit.shares = [Value::known(max_share); 16];
2954        circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
2955        circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
2956        circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
2957        circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
2958        circuit.share_blinds = share_blinds.map(Value::known);
2959        circuit.share_randomness = randomness.map(Value::known);
2960        circuit.ea_pk = Value::known(ea_pk);
2961        let vc = set_condition_11(&mut circuit, shares_hash_val, proposal_id, voting_round_id);
2962
2963        let instance = Instance::from_parts(
2964            van_nullifier,
2965            r_vpk_x,
2966            r_vpk_y,
2967            vote_authority_note_new,
2968            vc,
2969            vote_comm_tree_root,
2970            pallas::Base::zero(),
2971            pallas::Base::from(proposal_id),
2972            voting_round_id,
2973            *ea_pk.coordinates().unwrap().x(),
2974            *ea_pk.coordinates().unwrap().y(),
2975        );
2976
2977        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2978        assert_eq!(prover.verify(), Ok(()));
2979    }
2980
2981    /// A share at exactly 2^30 should fail the range check.
2982    #[test]
2983    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2984    fn shares_range_overflow_fails() {
2985        let (mut circuit, instance) = make_test_data();
2986
2987        // Set share_0 to 2^30 (one above the max valid value).
2988        // This will fail condition 9 AND condition 8 (sum mismatch),
2989        // but the important thing is the circuit rejects it.
2990        circuit.shares[0] = Value::known(pallas::Base::from(1u64 << 30));
2991
2992        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
2993        assert!(prover.verify().is_err());
2994    }
2995
2996    /// A share that is a large field element (simulating underflow
2997    /// from subtraction) should fail the range check.
2998    #[test]
2999    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3000    fn shares_range_field_wrap_fails() {
3001        let (mut circuit, instance) = make_test_data();
3002
3003        // Set share_0 to p - 1 (a wrapped negative value).
3004        // The 10-bit decomposition will produce a huge residual.
3005        circuit.shares[0] = Value::known(-pallas::Base::one());
3006
3007        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3008        assert!(prover.verify().is_err());
3009    }
3010
3011    /// Shares that sum correctly to total_note_value but with shares[0] = 2^30
3012    /// (one above the per-share maximum). Condition 8 (sum check) passes because
3013    /// total_note_value is set to match the sum. Condition 9 (range check) must
3014    /// still reject the individual overflow, confirming it checks each share
3015    /// independently — a correct sum does not bypass the per-share range gate.
3016    #[test]
3017    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3018    fn shares_range_single_overflow_correct_sum_fails() {
3019        let mut rng = OsRng;
3020
3021        let overflow_share = pallas::Base::from(1u64 << 30); // 2^30 — just above [0, 2^30)
3022        let normal_share_u64 = 625u64;
3023        // total_note_value = 2^30 + 15 * 625 so sum(shares) == total_note_value.
3024        let total_note_value = overflow_share + pallas::Base::from(15u64 * normal_share_u64);
3025
3026        let vsk = pallas::Scalar::random(&mut rng);
3027        let vsk_nk = pallas::Base::random(&mut rng);
3028        let rivk_v = pallas::Scalar::random(&mut rng);
3029        let alpha_v = pallas::Scalar::random(&mut rng);
3030        let (vpk_g_d_affine, vpk_pk_d_affine) = derive_voting_address(vsk, vsk_nk, rivk_v);
3031        let vpk_g_d_x = *vpk_g_d_affine.coordinates().unwrap().x();
3032        let vpk_pk_d_x = *vpk_pk_d_affine.coordinates().unwrap().x();
3033
3034        let voting_round_id = pallas::Base::random(&mut rng);
3035        let proposal_authority_old = pallas::Base::from(13u64); // bit 3 set
3036        let proposal_id = TEST_PROPOSAL_ID;
3037        let van_comm_rand = pallas::Base::random(&mut rng);
3038
3039        let vote_authority_note_old = van_integrity_hash(
3040            vpk_g_d_x,
3041            vpk_pk_d_x,
3042            total_note_value,
3043            voting_round_id,
3044            proposal_authority_old,
3045            van_comm_rand,
3046        );
3047        let (auth_path, position, vote_comm_tree_root) =
3048            build_single_leaf_merkle_path(vote_authority_note_old);
3049        let van_nullifier = van_nullifier_hash(vsk_nk, voting_round_id, vote_authority_note_old);
3050        let one_shifted = pallas::Base::from(1u64 << proposal_id);
3051        let proposal_authority_new = proposal_authority_old - one_shifted;
3052        let vote_authority_note_new = van_integrity_hash(
3053            vpk_g_d_x,
3054            vpk_pk_d_x,
3055            total_note_value,
3056            voting_round_id,
3057            proposal_authority_new,
3058            van_comm_rand,
3059        );
3060
3061        // shares[0] overflows (2^30); shares[1..16] are valid (625 each).
3062        // The encryption is computed with these exact values so condition 11 is consistent.
3063        let shares_u64: [u64; 16] = {
3064            let mut arr = [normal_share_u64; 16];
3065            arr[0] = 1u64 << 30;
3066            arr
3067        };
3068        let (_ea_sk, ea_pk) = generate_ea_keypair();
3069        let (enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y, randomness, share_blinds, shares_hash_val) =
3070            encrypt_shares(shares_u64, ea_pk);
3071
3072        let g = spend_auth_g_affine();
3073        let r_vpk = (g * (vsk + alpha_v)).to_affine();
3074
3075        let mut circuit = Circuit::with_van_witnesses(
3076            Value::known(auth_path),
3077            Value::known(position),
3078            Value::known(vpk_g_d_affine),
3079            Value::known(vpk_pk_d_affine),
3080            Value::known(total_note_value),
3081            Value::known(proposal_authority_old),
3082            Value::known(van_comm_rand),
3083            Value::known(vote_authority_note_old),
3084            Value::known(vsk),
3085            Value::known(rivk_v),
3086            Value::known(vsk_nk),
3087            Value::known(alpha_v),
3088        );
3089        circuit.one_shifted = Value::known(one_shifted);
3090        circuit.shares = shares_u64.map(|s| Value::known(pallas::Base::from(s)));
3091        circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
3092        circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
3093        circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
3094        circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
3095        circuit.share_blinds = share_blinds.map(Value::known);
3096        circuit.share_randomness = randomness.map(Value::known);
3097        circuit.ea_pk = Value::known(ea_pk);
3098
3099        let vote_commitment =
3100            set_condition_11(&mut circuit, shares_hash_val, proposal_id, voting_round_id);
3101
3102        let instance = Instance::from_parts(
3103            van_nullifier,
3104            *r_vpk.coordinates().unwrap().x(),
3105            *r_vpk.coordinates().unwrap().y(),
3106            vote_authority_note_new,
3107            vote_commitment,
3108            vote_comm_tree_root,
3109            pallas::Base::zero(),
3110            pallas::Base::from(proposal_id),
3111            voting_round_id,
3112            *ea_pk.coordinates().unwrap().x(),
3113            *ea_pk.coordinates().unwrap().y(),
3114        );
3115
3116        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3117        // Condition 8 (sum check) passes: shares sum to total_note_value.
3118        // Condition 9 (range check) must reject shares[0] = 2^30 regardless.
3119        assert!(
3120            prover.verify().is_err(),
3121            "range check must reject a share equal to 2^30 even when the total sum is correct"
3122        );
3123    }
3124
3125    // ================================================================
3126    // Condition 10 (Shares Hash Integrity) tests
3127    // ================================================================
3128
3129    /// Valid enc_share witnesses with matching shares_hash should pass.
3130    #[test]
3131    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3132    fn shares_hash_valid_proof() {
3133        let (circuit, instance) = make_test_data();
3134
3135        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3136        assert_eq!(prover.verify(), Ok(()));
3137    }
3138
3139    /// A corrupted enc_share_c1_x[0] should cause condition 10 failure:
3140    /// the in-circuit hash won't match the VOTE_COMMITMENT_PUBLIC_OFFSET instance.
3141    #[test]
3142    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3143    fn shares_hash_wrong_enc_share_fails() {
3144        let (mut circuit, instance) = make_test_data();
3145
3146        // Corrupt one enc_share component — the Poseidon hash will
3147        // change, so it won't match the instance's vote_commitment.
3148        circuit.enc_share_c1_x[0] = Value::known(pallas::Base::random(&mut OsRng));
3149
3150        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3151        assert!(prover.verify().is_err());
3152    }
3153
3154    /// A wrong vote_commitment instance value (shares_hash mismatch)
3155    /// should fail, even with correct enc_share witnesses.
3156    #[test]
3157    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3158    fn shares_hash_wrong_instance_fails() {
3159        let (circuit, mut instance) = make_test_data();
3160
3161        // Supply a random (wrong) vote_commitment in the instance.
3162        instance.vote_commitment = pallas::Base::random(&mut OsRng);
3163
3164        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3165        assert!(prover.verify().is_err());
3166    }
3167
3168    /// Verifies the out-of-circuit shares_hash helper is deterministic.
3169    #[test]
3170    fn shares_hash_deterministic() {
3171        let mut rng = OsRng;
3172
3173        let blinds: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
3174        let c1_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
3175        let c2_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
3176        let c1_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
3177        let c2_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
3178
3179        let h1 = shares_hash(blinds, c1_x, c2_x, c1_y, c2_y);
3180        let h2 = shares_hash(blinds, c1_x, c2_x, c1_y, c2_y);
3181        assert_eq!(h1, h2);
3182
3183        // Changing any component changes the hash.
3184        let mut c1_x_alt = c1_x;
3185        c1_x_alt[2] = pallas::Base::random(&mut rng);
3186        let h3 = shares_hash(blinds, c1_x_alt, c2_x, c1_y, c2_y);
3187        assert_ne!(h1, h3);
3188
3189        // Swapping c1 and c2 also changes the hash.
3190        let h4 = shares_hash(blinds, c2_x, c1_x, c2_y, c1_y);
3191        assert_ne!(h1, h4);
3192
3193        // Different blinds produce different hash.
3194        let blinds_alt: [pallas::Base; 16] =
3195            core::array::from_fn(|_| pallas::Base::random(&mut rng));
3196        let h5 = shares_hash(blinds_alt, c1_x, c2_x, c1_y, c2_y);
3197        assert_ne!(h1, h5);
3198    }
3199
3200    /// Verifies the out-of-circuit share_commitment helper is deterministic
3201    /// and that input order matters (Poseidon(blind, c1_x, c2_x, c1_y, c2_y) ≠
3202    /// Poseidon(blind, c2_x, c1_x, c2_y, c1_y)).
3203    #[test]
3204    fn share_commitment_deterministic() {
3205        let mut rng = OsRng;
3206        let blind = pallas::Base::random(&mut rng);
3207        let c1_x = pallas::Base::random(&mut rng);
3208        let c2_x = pallas::Base::random(&mut rng);
3209        let c1_y = pallas::Base::random(&mut rng);
3210        let c2_y = pallas::Base::random(&mut rng);
3211
3212        let h1 = share_commitment(blind, c1_x, c2_x, c1_y, c2_y);
3213        let h2 = share_commitment(blind, c1_x, c2_x, c1_y, c2_y);
3214        assert_eq!(h1, h2);
3215
3216        // Swapping c1 and c2 changes the hash.
3217        let h3 = share_commitment(blind, c2_x, c1_x, c2_y, c1_y);
3218        assert_ne!(h1, h3);
3219
3220        // Different blind changes the hash.
3221        let blind_alt = pallas::Base::random(&mut rng);
3222        let h4 = share_commitment(blind_alt, c1_x, c2_x, c1_y, c2_y);
3223        assert_ne!(h1, h4);
3224    }
3225
3226    /// Minimal circuit that computes one share commitment in-circuit and constrains
3227    /// the result to the instance column. Used to verify the in-circuit hash matches
3228    /// the native share_commitment.
3229    #[derive(Clone, Default)]
3230    struct ShareCommitmentTestCircuit {
3231        blind: pallas::Base,
3232        c1_x: pallas::Base,
3233        c2_x: pallas::Base,
3234        c1_y: pallas::Base,
3235        c2_y: pallas::Base,
3236    }
3237
3238    #[derive(Clone)]
3239    struct ShareCommitmentTestConfig {
3240        primary: Column<InstanceColumn>,
3241        advices: [Column<Advice>; 5],
3242        poseidon_config: PoseidonConfig<pallas::Base, 3, 2>,
3243    }
3244
3245    impl plonk::Circuit<pallas::Base> for ShareCommitmentTestCircuit {
3246        type Config = ShareCommitmentTestConfig;
3247        type FloorPlanner = floor_planner::V1;
3248
3249        fn without_witnesses(&self) -> Self {
3250            Self::default()
3251        }
3252
3253        fn configure(meta: &mut ConstraintSystem<pallas::Base>) -> Self::Config {
3254            let primary = meta.instance_column();
3255            meta.enable_equality(primary);
3256            let advices: [Column<Advice>; 5] = core::array::from_fn(|_| meta.advice_column());
3257            for col in &advices {
3258                meta.enable_equality(*col);
3259            }
3260            let fixed: [Column<Fixed>; 6] = core::array::from_fn(|_| meta.fixed_column());
3261            let constants = meta.fixed_column();
3262            meta.enable_constant(constants);
3263            let rc_a = fixed[0..3].try_into().unwrap();
3264            let rc_b = fixed[3..6].try_into().unwrap();
3265            let poseidon_config = PoseidonChip::configure::<poseidon::P128Pow5T3>(
3266                meta,
3267                advices[1..4].try_into().unwrap(),
3268                advices[4],
3269                rc_a,
3270                rc_b,
3271            );
3272            ShareCommitmentTestConfig {
3273                primary,
3274                advices,
3275                poseidon_config,
3276            }
3277        }
3278
3279        fn synthesize(
3280            &self,
3281            config: Self::Config,
3282            mut layouter: impl Layouter<pallas::Base>,
3283        ) -> Result<(), plonk::Error> {
3284            let blind_cell = assign_free_advice(
3285                layouter.namespace(|| "blind"),
3286                config.advices[0],
3287                Value::known(self.blind),
3288            )?;
3289            let c1_x_cell = assign_free_advice(
3290                layouter.namespace(|| "c1_x"),
3291                config.advices[0],
3292                Value::known(self.c1_x),
3293            )?;
3294            let c2_x_cell = assign_free_advice(
3295                layouter.namespace(|| "c2_x"),
3296                config.advices[0],
3297                Value::known(self.c2_x),
3298            )?;
3299            let c1_y_cell = assign_free_advice(
3300                layouter.namespace(|| "c1_y"),
3301                config.advices[0],
3302                Value::known(self.c1_y),
3303            )?;
3304            let c2_y_cell = assign_free_advice(
3305                layouter.namespace(|| "c2_y"),
3306                config.advices[0],
3307                Value::known(self.c2_y),
3308            )?;
3309            let chip = PoseidonChip::construct(config.poseidon_config.clone());
3310            let result = hash_share_commitment_in_circuit(
3311                chip,
3312                layouter.namespace(|| "share_comm"),
3313                blind_cell,
3314                c1_x_cell,
3315                c2_x_cell,
3316                c1_y_cell,
3317                c2_y_cell,
3318                0,
3319            )?;
3320            layouter.constrain_instance(result.cell(), config.primary, 0)?;
3321            Ok(())
3322        }
3323    }
3324
3325    /// Verifies that the in-circuit share commitment hash matches the native
3326    /// share_commitment(blind, c1_x, c2_x, c1_y, c2_y). The test builds a minimal circuit
3327    /// that computes the hash and constrains it to the instance column, then
3328    /// runs MockProver with the native hash as the public input.
3329    #[test]
3330    fn hash_share_commitment_in_circuit_matches_native() {
3331        let mut rng = OsRng;
3332        let blind = pallas::Base::random(&mut rng);
3333        let c1_x = pallas::Base::random(&mut rng);
3334        let c2_x = pallas::Base::random(&mut rng);
3335        let c1_y = pallas::Base::random(&mut rng);
3336        let c2_y = pallas::Base::random(&mut rng);
3337
3338        let expected = share_commitment(blind, c1_x, c2_x, c1_y, c2_y);
3339        let circuit = ShareCommitmentTestCircuit {
3340            blind,
3341            c1_x,
3342            c2_x,
3343            c1_y,
3344            c2_y,
3345        };
3346        let instance = vec![vec![expected]];
3347        // K=10 (1024 rows) is enough for one Poseidon(3) region.
3348        const TEST_K: u32 = 10;
3349        let prover = MockProver::run(TEST_K, &circuit, instance).expect("MockProver::run failed");
3350        assert_eq!(prover.verify(), Ok(()));
3351    }
3352
3353    // ================================================================
3354    // Condition 11 (Encryption Integrity) tests
3355    // ================================================================
3356
3357    /// Valid El Gamal encryptions should produce a valid proof.
3358    #[test]
3359    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3360    fn encryption_integrity_valid_proof() {
3361        let (circuit, instance) = make_test_data();
3362
3363        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3364        assert_eq!(prover.verify(), Ok(()));
3365    }
3366
3367    /// r_i = 0 would reveal C2_i = [v_i]G for a small share value, so the
3368    /// hardened circuit rejects the exact degenerate randomness witness.
3369    #[test]
3370    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3371    fn encryption_integrity_randomness_zero_is_rejected() {
3372        let (mut circuit, mut instance) = make_test_data();
3373        let shares_u64 = [625u64; 16];
3374        let (_ea_sk, ea_pk) = generate_ea_keypair();
3375        let (mut c1_x, mut c2_x, mut c1_y, mut c2_y, mut randomness, blinds, _) =
3376            encrypt_shares(shares_u64, ea_pk);
3377        let c2 = spend_auth_g_affine() * pallas::Scalar::from(shares_u64[0]);
3378        let c2_coords = c2.to_affine().coordinates().unwrap();
3379
3380        randomness[0] = pallas::Base::zero();
3381        c1_x[0] = pallas::Base::zero();
3382        c1_y[0] = pallas::Base::zero();
3383        c2_x[0] = *c2_coords.x();
3384        c2_y[0] = *c2_coords.y();
3385
3386        circuit.share_randomness = randomness.map(Value::known);
3387        circuit.enc_share_c1_x = c1_x.map(Value::known);
3388        circuit.enc_share_c1_y = c1_y.map(Value::known);
3389        circuit.enc_share_c2_x = c2_x.map(Value::known);
3390        circuit.enc_share_c2_y = c2_y.map(Value::known);
3391        let shares_hash_val = shares_hash(blinds, c1_x, c2_x, c1_y, c2_y);
3392        instance.vote_commitment = set_condition_11(
3393            &mut circuit,
3394            shares_hash_val,
3395            TEST_PROPOSAL_ID,
3396            instance.voting_round_id,
3397        );
3398
3399        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3400        assert!(prover.verify().is_err());
3401    }
3402
3403    /// A corrupted share_randomness[0] should fail condition 11:
3404    /// the computed C1[0] won't match enc_share_c1_x[0].
3405    #[test]
3406    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3407    fn encryption_integrity_wrong_randomness_fails() {
3408        let (mut circuit, instance) = make_test_data();
3409
3410        // Corrupt the randomness for share 0 — C1 will change.
3411        circuit.share_randomness[0] = Value::known(pallas::Base::from(9999u64));
3412
3413        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3414        assert!(prover.verify().is_err());
3415    }
3416
3417    /// A wrong ea_pk in the instance should fail condition 11:
3418    /// the computed r * ea_pk won't match the ciphertexts.
3419    #[test]
3420    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3421    fn encryption_integrity_wrong_ea_pk_instance_fails() {
3422        let (circuit, mut instance) = make_test_data();
3423
3424        // Corrupt ea_pk_x in the instance — the constraint linking
3425        // the witnessed ea_pk to the public input will fail.
3426        instance.ea_pk_x = pallas::Base::from(12345u64);
3427
3428        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3429        assert!(prover.verify().is_err());
3430    }
3431
3432    /// A corrupted share value (plaintext) should fail condition 11:
3433    /// C2_i = [v_i]*G + [r_i]*ea_pk will not match enc_share_c2_x[i].
3434    #[test]
3435    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3436    fn encryption_integrity_wrong_share_fails() {
3437        let (mut circuit, instance) = make_test_data();
3438
3439        // Corrupt share 0 — enc_share and randomness are unchanged (from
3440        // make_test_data), so the in-circuit C2_0 will not match enc_c2_x[0].
3441        circuit.shares[0] = Value::known(pallas::Base::from(9999u64));
3442
3443        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3444        assert!(prover.verify().is_err());
3445    }
3446
3447    /// A corrupted enc_share_c2_x witness should cause verification to fail:
3448    /// condition 11 constrains ExtractP(C2_i) == enc_c2_x[i].
3449    #[test]
3450    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3451    fn encryption_integrity_wrong_enc_c2_x_fails() {
3452        let (mut circuit, instance) = make_test_data();
3453
3454        // Corrupt one C2 x-coordinate — the ECC will compute the real C2_0
3455        // from share_0 and r_0; constrain_equal will fail (or the resulting
3456        // shares_hash will not match the instance vote_commitment).
3457        circuit.enc_share_c2_x[0] = Value::known(pallas::Base::random(&mut OsRng));
3458
3459        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3460        assert!(prover.verify().is_err());
3461    }
3462
3463    /// The out-of-circuit elgamal_encrypt helper is deterministic.
3464    #[test]
3465    fn elgamal_encrypt_deterministic() {
3466        let (_ea_sk, ea_pk) = generate_ea_keypair();
3467
3468        let v = pallas::Base::from(1000u64);
3469        let r = pallas::Base::from(42u64);
3470
3471        let (c1_a, c2_a, _, _) =
3472            elgamal_encrypt(v, r, ea_pk).expect("test encryption inputs should be valid");
3473        let (c1_b, c2_b, _, _) =
3474            elgamal_encrypt(v, r, ea_pk).expect("test encryption inputs should be valid");
3475        assert_eq!(c1_a, c1_b);
3476        assert_eq!(c2_a, c2_b);
3477
3478        // Different randomness → different C1.
3479        let (c1_c, _, _, _) = elgamal_encrypt(v, pallas::Base::from(99u64), ea_pk)
3480            .expect("test encryption inputs should be valid");
3481        assert_ne!(c1_a, c1_c);
3482    }
3483
3484    /// base_to_scalar (used by El Gamal) accepts share-sized values and
3485    /// the fixed randomness used in encrypt_shares.
3486    #[test]
3487    fn base_to_scalar_accepts_elgamal_inputs() {
3488        // Share-sized values (condition 9: [0, 2^30)) must convert.
3489        assert!(base_to_scalar(pallas::Base::zero()).is_some());
3490        assert!(base_to_scalar(pallas::Base::from(1u64)).is_some());
3491        assert!(base_to_scalar(pallas::Base::from(1_000u64)).is_some());
3492        assert!(base_to_scalar(pallas::Base::from(404u64)).is_some()); // encrypt_shares randomness
3493
3494        // encrypt_shares uses (i+1)*101 for i in 0..16 → 101, 202, ..., 1616.
3495        for r in (1u64..=16).map(|i| i * 101) {
3496            assert!(
3497                base_to_scalar(pallas::Base::from(r)).is_some(),
3498                "r = {} must convert for El Gamal",
3499                r
3500            );
3501        }
3502    }
3503
3504    // ================================================================
3505    // Condition 12 (Vote Commitment Integrity) tests
3506    // ================================================================
3507
3508    /// Valid vote commitment (full Poseidon chain) should pass.
3509    #[test]
3510    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3511    fn vote_commitment_integrity_valid_proof() {
3512        let (circuit, instance) = make_test_data();
3513
3514        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3515        assert_eq!(prover.verify(), Ok(()));
3516    }
3517
3518    /// A wrong vote_decision in the circuit should fail condition 12:
3519    /// the derived vote_commitment won't match the instance.
3520    #[test]
3521    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3522    fn vote_commitment_wrong_decision_fails() {
3523        let (mut circuit, instance) = make_test_data();
3524
3525        // Corrupt the vote decision — the Poseidon hash will change.
3526        circuit.vote_decision = Value::known(pallas::Base::from(99u64));
3527
3528        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3529        assert!(prover.verify().is_err());
3530    }
3531
3532    /// A wrong proposal_id in the instance should fail condition 12:
3533    /// the in-circuit proposal_id (copied from instance) will produce
3534    /// a different vote_commitment.
3535    #[test]
3536    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3537    fn vote_commitment_wrong_proposal_id_fails() {
3538        let (circuit, mut instance) = make_test_data();
3539
3540        // Corrupt the proposal_id in the instance.
3541        instance.proposal_id = pallas::Base::from(999u64);
3542
3543        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3544        assert!(prover.verify().is_err());
3545    }
3546
3547    /// A wrong vote_commitment in the instance should fail.
3548    #[test]
3549    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3550    fn vote_commitment_wrong_instance_fails() {
3551        let (circuit, mut instance) = make_test_data();
3552
3553        // Corrupt the vote_commitment public input.
3554        instance.vote_commitment = pallas::Base::random(&mut OsRng);
3555
3556        let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
3557        assert!(prover.verify().is_err());
3558    }
3559
3560    /// The out-of-circuit vote_commitment_hash helper is deterministic.
3561    #[test]
3562    fn vote_commitment_hash_deterministic() {
3563        let mut rng = OsRng;
3564
3565        let rid = pallas::Base::random(&mut rng);
3566        let sh = pallas::Base::random(&mut rng);
3567        let pid = pallas::Base::from(5u64);
3568        let dec = pallas::Base::from(1u64);
3569
3570        let h1 = vote_commitment_hash(rid, sh, pid, dec);
3571        let h2 = vote_commitment_hash(rid, sh, pid, dec);
3572        assert_eq!(h1, h2);
3573
3574        // Changing any input changes the hash.
3575        let h3 = vote_commitment_hash(rid, sh, pallas::Base::from(6u64), dec);
3576        assert_ne!(h1, h3);
3577
3578        // Changing voting_round_id changes the hash.
3579        let h4 = vote_commitment_hash(pallas::Base::from(999u64), sh, pid, dec);
3580        assert_ne!(h1, h4);
3581
3582        // DOMAIN_VC ensures separation from VAN hashes.
3583        // (Different arity prevents confusion, but domain tag adds defense-in-depth.)
3584        assert_ne!(h1, pallas::Base::zero());
3585    }
3586
3587    // ================================================================
3588    // Instance and circuit sanity
3589    // ================================================================
3590
3591    /// Instance must serialize to exactly 11 public inputs.
3592    #[test]
3593    fn instance_has_eleven_public_inputs() {
3594        let (_, instance) = make_test_data();
3595        assert_eq!(instance.to_halo2_instance().len(), 11);
3596    }
3597
3598    /// Default circuit (all witnesses unknown) must not produce a valid proof.
3599    #[test]
3600    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
3601    fn default_circuit_with_valid_instance_fails() {
3602        let (_, instance) = make_test_data();
3603        let circuit = Circuit::default();
3604
3605        // Synthesis failure is also acceptable.
3606        if let Ok(prover) = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]) {
3607            assert!(prover.verify().is_err());
3608        }
3609    }
3610
3611    /// Measures actual rows used by the vote-proof circuit via `CircuitCost::measure`.
3612    ///
3613    /// `CircuitCost` runs the floor planner against the circuit and tracks the
3614    /// highest row offset assigned in any column, giving the real "rows consumed"
3615    /// number rather than the theoretical 2^K capacity.
3616    ///
3617    /// Run with:
3618    ///   cargo test vote_proof::circuit::tests::row_budget -- --nocapture --ignored --test-threads=1
3619    #[test]
3620    #[ignore = "long-running row-budget diagnostic; run with `cargo test vote_proof::circuit::tests::row_budget -- --ignored --nocapture --test-threads=1`"]
3621    fn row_budget() {
3622        use halo2_proofs::dev::CircuitCost;
3623        use pasta_curves::vesta;
3624        use std::println;
3625
3626        let (circuit, _) = make_test_data();
3627
3628        // CircuitCost::measure runs the floor planner and returns layout statistics.
3629        // Fields are private, so extract them from the Debug representation.
3630        let cost = CircuitCost::<vesta::Point, _>::measure(K, &circuit);
3631        let debug = format!("{cost:?}");
3632
3633        // Parse max_rows, max_advice_rows, max_fixed_rows from Debug string.
3634        let extract = |field: &str| -> usize {
3635            let prefix = format!("{field}: ");
3636            debug
3637                .split(&prefix)
3638                .nth(1)
3639                .and_then(|s| s.split([',', ' ', '}']).next())
3640                .and_then(|n| n.parse().ok())
3641                .unwrap_or(0)
3642        };
3643
3644        let max_rows = extract("max_rows");
3645        let max_advice_rows = extract("max_advice_rows");
3646        let max_fixed_rows = extract("max_fixed_rows");
3647        let total_available = 1usize << K;
3648
3649        println!("=== vote-proof circuit row budget (K={K}) ===");
3650        println!("  max_rows (floor-planner high-water mark): {max_rows}");
3651        println!("  max_advice_rows:                          {max_advice_rows}");
3652        println!("  max_fixed_rows:                           {max_fixed_rows}");
3653        println!("  2^K  (total available rows):              {total_available}");
3654        println!(
3655            "  headroom:                                 {}",
3656            total_available.saturating_sub(max_rows)
3657        );
3658        println!(
3659            "  utilisation:                              {:.1}%",
3660            100.0 * max_rows as f64 / total_available as f64
3661        );
3662        println!();
3663        println!("  Full debug: {debug}");
3664
3665        // ---------------------------------------------------------------
3666        // Witness-independence check: Circuit::default() (all unknowns)
3667        // must produce exactly the same layout as the filled circuit.
3668        // If these differ, the row count depends on witness values and
3669        // the measurement above cannot be trusted as a production bound.
3670        // ---------------------------------------------------------------
3671        let cost_default = CircuitCost::<vesta::Point, _>::measure(K, &Circuit::default());
3672        let debug_default = format!("{cost_default:?}");
3673        let max_rows_default = debug_default
3674            .split("max_rows: ")
3675            .nth(1)
3676            .and_then(|s| s.split([',', ' ', '}']).next())
3677            .and_then(|n| n.parse::<usize>().ok())
3678            .unwrap_or(0);
3679        if max_rows_default == max_rows {
3680            println!(
3681                "  Witness-independence: PASS \
3682                (Circuit::default() max_rows={max_rows_default} == filled max_rows={max_rows})"
3683            );
3684        } else {
3685            println!(
3686                "  Witness-independence: FAIL \
3687                (Circuit::default() max_rows={max_rows_default} != filled max_rows={max_rows}) \
3688                — row count depends on witness values!"
3689            );
3690        }
3691
3692        // ---------------------------------------------------------------
3693        // VOTE_COMM_TREE_DEPTH sanity check: confirm the circuit constant
3694        // matches the canonical value in vote_commitment_tree::TREE_DEPTH
3695        // (24 as of this writing). A mismatch would mean test data uses a
3696        // shallower tree than production.
3697        // ---------------------------------------------------------------
3698        println!("  VOTE_COMM_TREE_DEPTH (circuit constant): {VOTE_COMM_TREE_DEPTH}");
3699
3700        // ---------------------------------------------------------------
3701        // Minimum-K probe: find the smallest K at which MockProver passes.
3702        // Useful for evaluating whether K can be reduced.
3703        // ---------------------------------------------------------------
3704        for probe_k in 11u32..=K {
3705            let (c, inst) = make_test_data();
3706            match MockProver::run(probe_k, &c, vec![inst.to_halo2_instance()]) {
3707                Err(_) => {
3708                    println!("  K={probe_k}: not enough rows (synthesizer rejected)");
3709                    continue;
3710                }
3711                Ok(p) => match p.verify() {
3712                    Ok(()) => {
3713                        println!("  Minimum viable K: {probe_k} (2^{probe_k} = {} rows, {:.1}% headroom)",
3714                            1usize << probe_k,
3715                            100.0 * (1.0 - max_rows as f64 / (1usize << probe_k) as f64));
3716                        break;
3717                    }
3718                    Err(_) => println!("  K={probe_k}: too small"),
3719                },
3720            }
3721        }
3722    }
3723}