voting-circuits 0.7.0

Governance ZKP circuits (delegation, vote proof, share reveal) for the Zcash shielded-voting protocol.
Documentation
//! VAN (Vote Authority Note) integrity gadget.
//!
//! Authoritative in-tree definition of the two-layer Poseidon hash used by
//! both ZKP #1 (delegation, condition 7) and ZKP #2 (vote proof, conditions 2
//! and 7). README prose should cite this module instead of restating the
//! preimage shape as an independent source of truth:
//!
//! ```text
//! van_comm_core = Poseidon(DOMAIN_VAN, g_d_x, pk_d_x, value,
//!                          voting_round_id, proposal_authority)
//! result = Poseidon(van_comm_core, van_comm_rand)
//! ```
//!
//! Address encoding note: `g_d_x` and `pk_d_x` are x-coordinates of the
//! voting hotkey address points. The VAN hash therefore identifies `(g_d,
//! pk_d)` only up to coordinated point negation and must not be treated as a
//! standalone commitment to unique full points. The current protocol pairs it
//! with full-point constraints: delegation also binds the output address into
//! `cmx_new` through NoteCommit's point encoding, and vote proof binds the
//! witnessed address to the voting key hierarchy before deriving a per-VAN
//! nullifier from the same commitment. Future callers must preserve equivalent
//! constraints or widen the VAN preimage to include full point data.
//!
//! The first layer commits to the structural fields (domain tag,
//! diversified address, value, round, authority). The second layer
//! blinds the result with a random scalar, preventing observers from
//! brute-forcing the address or weight from the public commitment.

use halo2_gadgets::poseidon::{
    primitives::{self as poseidon, ConstantLength},
    Hash as PoseidonHash, Pow5Chip as PoseidonChip, Pow5Config as PoseidonConfig,
};
use halo2_proofs::{
    circuit::{AssignedCell, Layouter},
    plonk,
};
use pasta_curves::pallas;

use crate::protocol_hash::{poseidon_hash_2, poseidon_hash_in_circuit};

pub use crate::domain_tags::DOMAIN_VAN;

// ================================================================
// Out-of-circuit helper
// ================================================================

/// Out-of-circuit VAN integrity hash.
///
/// This is the authoritative native implementation of the two-layer VAN
/// commitment preimage shared by delegation and vote proof:
/// ```text
/// van_comm_core = Poseidon(DOMAIN_VAN, g_d_x, pk_d_x, value,
///                          voting_round_id, proposal_authority)
/// result = Poseidon(van_comm_core, van_comm_rand)
/// ```
///
/// The address inputs are x-coordinates only. See the module docs for the
/// full-point constraints that make this preimage safe in the current protocol.
///
/// Used by builders and tests to compute the expected VAN commitment.
pub(crate) fn van_integrity_hash(
    g_d_x: pallas::Base,
    pk_d_x: pallas::Base,
    value: pallas::Base,
    voting_round_id: pallas::Base,
    proposal_authority: pallas::Base,
    van_comm_rand: pallas::Base,
) -> pallas::Base {
    let van_comm_core = poseidon::Hash::<_, poseidon::P128Pow5T3, ConstantLength<6>, 3, 2>::init()
        .hash([
            pallas::Base::from(DOMAIN_VAN),
            g_d_x,
            pk_d_x,
            value,
            voting_round_id,
            proposal_authority,
        ]);
    poseidon_hash_2(van_comm_core, van_comm_rand)
}

// ================================================================
// In-circuit gadget
// ================================================================

/// In-circuit VAN integrity hash.
///
/// Two-layer structure matching the out-of-circuit helper:
/// ```text
/// van_comm_core = Poseidon(domain_van, g_d_x, pk_d_x, value,
///                          voting_round_id, proposal_authority)
/// result = Poseidon(van_comm_core, van_comm_rand)
/// ```
///
/// Takes a `PoseidonConfig` so it can be used by any circuit that
/// configures a compatible Poseidon chip (P128Pow5T3, width 3, rate 2).
/// Like the native helper, it absorbs address x-coordinates only and does not
/// by itself prove a unique full-point address binding.
///
/// In ZKP #1 (delegation, condition 7) `proposal_authority` is
/// `MAX_PROPOSAL_AUTHORITY` (fresh delegation). In ZKP #2 (vote
/// proof) condition 2 passes `_old`, condition 7 passes `_new`
/// (from condition 5's decrement).
pub(crate) fn van_integrity_poseidon(
    poseidon_config: &PoseidonConfig<pallas::Base, 3, 2>,
    layouter: &mut impl Layouter<pallas::Base>,
    label: &str,
    domain_van: AssignedCell<pallas::Base, pallas::Base>,
    g_d_x: AssignedCell<pallas::Base, pallas::Base>,
    pk_d_x: AssignedCell<pallas::Base, pallas::Base>,
    value: AssignedCell<pallas::Base, pallas::Base>,
    voting_round_id: AssignedCell<pallas::Base, pallas::Base>,
    proposal_authority: AssignedCell<pallas::Base, pallas::Base>,
    van_comm_rand: AssignedCell<pallas::Base, pallas::Base>,
) -> Result<AssignedCell<pallas::Base, pallas::Base>, plonk::Error> {
    let core_message = [
        domain_van,
        g_d_x,
        pk_d_x,
        value,
        voting_round_id,
        proposal_authority,
    ];
    let poseidon_hasher_6 =
        PoseidonHash::<pallas::Base, _, poseidon::P128Pow5T3, ConstantLength<6>, 3, 2>::init(
            PoseidonChip::construct(poseidon_config.clone()),
            layouter.namespace(|| format!("{label} core Poseidon init")),
        )?;
    let van_comm_core = poseidon_hasher_6.hash(
        layouter.namespace(|| format!("{label} Poseidon(core)")),
        core_message,
    )?;
    poseidon_hash_in_circuit(
        PoseidonChip::construct(poseidon_config.clone()),
        layouter.namespace(|| format!("{label} final Poseidon")),
        "Poseidon(core, rand)",
        [van_comm_core, van_comm_rand],
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use ff::PrimeField;

    fn base_from_repr(bytes: [u8; 32]) -> pallas::Base {
        pallas::Base::from_repr(bytes).expect("frozen vector must be canonical")
    }

    #[test]
    fn van_integrity_hash_frozen_vector() {
        let actual = van_integrity_hash(
            pallas::Base::from(1u64),
            pallas::Base::from(2u64),
            pallas::Base::from(3u64),
            pallas::Base::from(4u64),
            pallas::Base::from(5u64),
            pallas::Base::from(6u64),
        );

        assert_eq!(
            actual,
            base_from_repr([
                170, 254, 87, 142, 76, 163, 228, 29, 153, 102, 1, 140, 237, 128, 137, 3, 137, 237,
                179, 252, 168, 232, 117, 56, 227, 199, 242, 20, 178, 33, 130, 59,
            ])
        );
    }
}