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}