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