voting-circuits 0.8.0

Governance ZKP circuits (delegation, vote proof, share reveal) for the Zcash shielded-voting protocol.
Documentation
//! Share Reveal bundle builder.
//!
//! Constructs the [`Circuit`] and [`Instance`] from high-level inputs
//! (Merkle path, share commitments, vote metadata). The builder computes
//! all derived values (shares_hash, vote_commitment, share_nullifier,
//! tree root) so the caller only provides raw witness data.

use halo2_proofs::circuit::Value;
use pasta_curves::pallas;

use super::circuit::{share_nullifier_hash, Circuit, Instance};
use crate::{
    gadgets::vote_commitment::vote_commitment_hash as compute_vote_commitment_hash,
    params::VOTE_COMM_TREE_DEPTH, protocol_hash::poseidon_hash_2,
    shares_hash::shares_hash_from_comms,
};

/// Complete share reveal bundle: circuit + public inputs.
#[derive(Clone, Debug)]
pub struct ShareRevealBundle {
    /// The share reveal circuit with all witnesses populated.
    pub circuit: Circuit,
    /// Public inputs (9 field elements).
    pub instance: Instance,
}

/// Build a share reveal bundle from high-level inputs.
///
/// # Arguments
///
/// - `merkle_auth_path`: The 24 sibling hashes from the vote commitment tree.
/// - `merkle_position`: Leaf position in the vote commitment tree.
/// - `share_comms`: Pre-computed per-share Poseidon commitments
///   (`share_comm_i = Poseidon(blind_i, c1_i_x, c2_i_x, c1_i_y, c2_i_y)`).
/// - `primary_blind`: Blind factor for the revealed share (at `share_index`).
/// - `enc_c1_x`: X-coordinate of the revealed share's El Gamal C1. This is
///   reveal data supplied by the caller and bound to `share_comms[share_index]`
///   through the share-commitment Poseidon hash.
/// - `enc_c2_x`: X-coordinate of the revealed share's El Gamal C2. Same
///   transitive binding as `enc_c1_x`.
/// - `enc_c1_y`: Y-coordinate of the revealed share's El Gamal C1. Included so
///   the reveal binds the exact curve point, not only its x-coordinate.
/// - `enc_c2_y`: Y-coordinate of the revealed share's El Gamal C2. Included for
///   exact-point binding and tied through the selected share commitment.
/// - `share_index`: Which of the 16 shares is being revealed (0..15).
/// - `proposal_id`: Proposal identifier (as a field element).
/// - `vote_decision`: The voter's choice (as a field element).
/// - `voting_round_id`: Voting round identifier (as a field element).
///
/// # Caller contract
///
/// `share_comms`, `primary_blind`, and the encrypted-share coordinates are
/// cross-circuit outputs from `vote_proof::build_vote_proof_from_delegation`.
/// Pass the selected `VoteProofBundle::share_blinds[share_index]`,
/// `VoteProofBundle::encrypted_shares[share_index]`, and full
/// `VoteProofBundle::share_comms` array unchanged; drawing a fresh blind breaks
/// the share-commitment constraint and can invalidate the reveal. `proposal_id`,
/// `vote_decision`, `voting_round_id`, and the vote commitment tree witness are
/// authenticated session parameters supplied by the caller.
pub fn build_share_reveal(
    merkle_auth_path: [pallas::Base; VOTE_COMM_TREE_DEPTH],
    merkle_position: u32,
    share_comms: [pallas::Base; 16],
    primary_blind: pallas::Base,
    enc_c1_x: pallas::Base,
    enc_c2_x: pallas::Base,
    enc_c1_y: pallas::Base,
    enc_c2_y: pallas::Base,
    share_index: u32,
    proposal_id: pallas::Base,
    vote_decision: pallas::Base,
    voting_round_id: pallas::Base,
) -> ShareRevealBundle {
    let shares_hash = shares_hash_from_comms(share_comms);

    let vote_commitment =
        compute_vote_commitment_hash(voting_round_id, shares_hash, proposal_id, vote_decision);

    let vote_comm_tree_root = {
        let mut current = vote_commitment;
        for (i, sibling) in merkle_auth_path
            .iter()
            .enumerate()
            .take(VOTE_COMM_TREE_DEPTH)
        {
            let bit = (merkle_position >> i) & 1;
            let (left, right) = if bit == 0 {
                (current, *sibling)
            } else {
                (*sibling, current)
            };
            current = poseidon_hash_2(left, right);
        }
        current
    };

    let share_index_fp = pallas::Base::from(share_index as u64);
    let share_nullifier = share_nullifier_hash(vote_commitment, share_index_fp, primary_blind);

    let circuit = Circuit {
        vote_comm_tree_path: Value::known(merkle_auth_path),
        vote_comm_tree_position: Value::known(merkle_position),
        share_comms: share_comms.map(Value::known),
        primary_blind: Value::known(primary_blind),
        share_index: Value::known(share_index_fp),
        vote_commitment: Value::known(vote_commitment),
    };

    let instance = Instance::from_parts(
        share_nullifier,
        enc_c1_x,
        enc_c2_x,
        proposal_id,
        vote_decision,
        vote_comm_tree_root,
        voting_round_id,
        enc_c1_y,
        enc_c2_y,
    );

    ShareRevealBundle { circuit, instance }
}

#[cfg(test)]
mod tests {
    use super::*;
    use group::Curve;
    use halo2_proofs::dev::MockProver;
    use pasta_curves::pallas;

    use crate::gadgets::elgamal::{elgamal_encrypt, spend_auth_g_affine};
    use crate::shares_hash::share_commitment;

    use super::super::circuit::K;

    #[test]
    #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
    fn test_builder_round_trip() {
        let ea_sk = pallas::Scalar::from(42u64);
        let ea_pk = (spend_auth_g_affine() * ea_sk).to_affine();

        let shares: [u64; 16] = [625; 16];
        let randomness: [pallas::Base; 16] =
            core::array::from_fn(|i| pallas::Base::from((i as u64 + 1) * 101));
        let share_blinds: [pallas::Base; 16] =
            core::array::from_fn(|i| pallas::Base::from(1001u64 + i as u64));
        let mut c1_x = [pallas::Base::zero(); 16];
        let mut c2_x = [pallas::Base::zero(); 16];
        let mut c1_y = [pallas::Base::zero(); 16];
        let mut c2_y = [pallas::Base::zero(); 16];
        for i in 0..16 {
            let (cx1, cx2, cy1, cy2) =
                elgamal_encrypt(pallas::Base::from(shares[i]), randomness[i], ea_pk)
                    .expect("test encryption inputs should be valid");
            c1_x[i] = cx1;
            c2_x[i] = cx2;
            c1_y[i] = cy1;
            c2_y[i] = cy2;
        }

        let share_comms: [pallas::Base; 16] = core::array::from_fn(|i| {
            share_commitment(share_blinds[i], c1_x[i], c2_x[i], c1_y[i], c2_y[i])
        });

        let mut empty_roots = [pallas::Base::zero(); VOTE_COMM_TREE_DEPTH];
        empty_roots[0] = poseidon_hash_2(pallas::Base::zero(), pallas::Base::zero());
        for i in 1..VOTE_COMM_TREE_DEPTH {
            empty_roots[i] = poseidon_hash_2(empty_roots[i - 1], empty_roots[i - 1]);
        }

        let share_idx: u32 = 2;
        let bundle = build_share_reveal(
            empty_roots,
            0,
            share_comms,
            share_blinds[share_idx as usize],
            c1_x[share_idx as usize],
            c2_x[share_idx as usize],
            c1_y[share_idx as usize],
            c2_y[share_idx as usize],
            share_idx,
            pallas::Base::from(3u64),
            pallas::Base::from(1u64),
            pallas::Base::from(999u64),
        );

        let prover = MockProver::run(
            K,
            &bundle.circuit,
            vec![bundle.instance.to_halo2_instance()],
        )
        .unwrap();
        assert_eq!(prover.verify(), Ok(()));
    }
}