Skip to main content

voting_circuits/circuit/
vote_commitment.rs

1//! Vote Commitment integrity gadget.
2//!
3//! Shared 5-input Poseidon hash used by both ZKP #2 (vote proof,
4//! condition 12) and ZKP #3 (share reveal, condition 2):
5//!
6//! ```text
7//! vote_commitment = Poseidon(DOMAIN_VC, voting_round_id,
8//!                            shares_hash, proposal_id, vote_decision)
9//! ```
10//!
11//! The domain tag bakes into the verification key, preventing malicious
12//! provers from substituting VAN commitments for vote commitments in the
13//! shared tree.
14
15use pasta_curves::pallas;
16
17use halo2_gadgets::poseidon::{
18    primitives::{self as poseidon, ConstantLength},
19    Hash as PoseidonHash, Pow5Chip as PoseidonChip, Pow5Config as PoseidonConfig,
20};
21use halo2_proofs::{
22    circuit::{AssignedCell, Layouter},
23    plonk,
24};
25
26// ================================================================
27// Constants
28// ================================================================
29
30/// Domain tag for Vote Commitments.
31///
32/// Prepended as the first Poseidon input for domain separation from
33/// VANs (`DOMAIN_VAN = 0`) in the shared vote commitment tree.
34pub const DOMAIN_VC: u64 = 1;
35
36// ================================================================
37// Out-of-circuit helper
38// ================================================================
39
40/// Out-of-circuit vote commitment hash.
41///
42/// Computes:
43/// ```text
44/// Poseidon(DOMAIN_VC, voting_round_id, shares_hash, proposal_id, vote_decision)
45/// ```
46///
47/// Used by builders and tests to compute the expected vote commitment.
48/// Must produce identical output to the in-circuit gadget.
49pub fn vote_commitment_hash(
50    voting_round_id: pallas::Base,
51    shares_hash: pallas::Base,
52    proposal_id: pallas::Base,
53    vote_decision: pallas::Base,
54) -> pallas::Base {
55    poseidon::Hash::<_, poseidon::P128Pow5T3, ConstantLength<5>, 3, 2>::init().hash([
56        pallas::Base::from(DOMAIN_VC),
57        voting_round_id,
58        shares_hash,
59        proposal_id,
60        vote_decision,
61    ])
62}
63
64// ================================================================
65// In-circuit gadget
66// ================================================================
67
68/// In-circuit vote commitment hash.
69///
70/// Computes `Poseidon(domain_vc, voting_round_id, shares_hash, proposal_id, vote_decision)`
71/// matching the out-of-circuit helper above.
72///
73/// Takes a `PoseidonConfig` so it can be used by any circuit that
74/// configures a compatible Poseidon chip (P128Pow5T3, width 3, rate 2).
75/// The `domain_vc` cell must be assigned via `assign_advice_from_constant`
76/// so the value is baked into the verification key.
77///
78/// Used by ZKP #2 (vote proof, condition 12) and ZKP #3 (share reveal,
79/// condition 2).
80pub fn vote_commitment_poseidon(
81    poseidon_config: &PoseidonConfig<pallas::Base, 3, 2>,
82    layouter: &mut impl Layouter<pallas::Base>,
83    label: &str,
84    domain_vc: AssignedCell<pallas::Base, pallas::Base>,
85    voting_round_id: AssignedCell<pallas::Base, pallas::Base>,
86    shares_hash: AssignedCell<pallas::Base, pallas::Base>,
87    proposal_id: AssignedCell<pallas::Base, pallas::Base>,
88    vote_decision: AssignedCell<pallas::Base, pallas::Base>,
89) -> Result<AssignedCell<pallas::Base, pallas::Base>, plonk::Error> {
90    let message = [domain_vc, voting_round_id, shares_hash, proposal_id, vote_decision];
91    let hasher = PoseidonHash::<
92        pallas::Base,
93        _,
94        poseidon::P128Pow5T3,
95        ConstantLength<5>,
96        3,
97        2,
98    >::init(
99        PoseidonChip::construct(poseidon_config.clone()),
100        layouter.namespace(|| alloc::format!("{label} Poseidon init")),
101    )?;
102    hasher.hash(
103        layouter.namespace(|| alloc::format!("{label} Poseidon(DOMAIN_VC, ...)")),
104        message,
105    )
106}