Skip to main content

voting_circuits/circuit/
share_commitment.rs

1//! Per-share commitment hash used by ZKP #2 and ZKP #3.
2//!
3//! ```text
4//! share_commitment = Poseidon(DOMAIN_SHARE_COMM, blind, c1_x, c2_x, c1_y, c2_y)
5//! ```
6//!
7//! The domain tag is assigned as a circuit constant so the value is baked into
8//! the verification keys. The blind remains a private witness and keeps
9//! observers from recomputing `shares_hash` from posted encrypted shares.
10
11use halo2_gadgets::poseidon::Pow5Chip as PoseidonChip;
12use halo2_proofs::{
13    circuit::{AssignedCell, Layouter},
14    plonk::{self, Advice, Column},
15};
16use pasta_curves::pallas;
17
18pub use crate::domain_tags::DOMAIN_SHARE_COMM;
19use crate::protocol_hash::{poseidon_hash, poseidon_hash_in_circuit};
20
21/// Out-of-circuit per-share blinded commitment.
22///
23/// The y-coordinates bind the commitment to the exact curve point, not just
24/// the x-coordinate. Without them, an attacker can negate the El Gamal
25/// ciphertext without invalidating the ZKP and corrupt the homomorphic tally.
26pub fn share_commitment(
27    blind: pallas::Base,
28    c1_x: pallas::Base,
29    c2_x: pallas::Base,
30    c1_y: pallas::Base,
31    c2_y: pallas::Base,
32) -> pallas::Base {
33    poseidon_hash([
34        pallas::Base::from(DOMAIN_SHARE_COMM),
35        blind,
36        c1_x,
37        c2_x,
38        c1_y,
39        c2_y,
40    ])
41}
42
43/// Assigns `DOMAIN_SHARE_COMM` as a circuit constant.
44pub(crate) fn assign_domain_share_comm(
45    layouter: &mut impl Layouter<pallas::Base>,
46    advice: Column<Advice>,
47) -> Result<AssignedCell<pallas::Base, pallas::Base>, plonk::Error> {
48    layouter.assign_region(
49        || "DOMAIN_SHARE_COMM constant",
50        |mut region| {
51            region.assign_advice_from_constant(
52                || "domain_share_comm",
53                advice,
54                0,
55                pallas::Base::from(DOMAIN_SHARE_COMM),
56            )
57        },
58    )
59}
60
61/// Synthesizes one per-share commitment.
62///
63/// The caller must pass a `domain_share_comm` cell assigned by
64/// [`assign_domain_share_comm`] so the domain tag is constrained as a constant.
65pub(crate) fn share_commitment_poseidon(
66    chip: PoseidonChip<pallas::Base, 3, 2>,
67    layouter: &mut impl Layouter<pallas::Base>,
68    label: &str,
69    domain_share_comm: AssignedCell<pallas::Base, pallas::Base>,
70    blind: AssignedCell<pallas::Base, pallas::Base>,
71    enc_c1_x: AssignedCell<pallas::Base, pallas::Base>,
72    enc_c2_x: AssignedCell<pallas::Base, pallas::Base>,
73    enc_c1_y: AssignedCell<pallas::Base, pallas::Base>,
74    enc_c2_y: AssignedCell<pallas::Base, pallas::Base>,
75) -> Result<AssignedCell<pallas::Base, pallas::Base>, plonk::Error> {
76    poseidon_hash_in_circuit(
77        chip,
78        layouter.namespace(|| label),
79        label,
80        [
81            domain_share_comm,
82            blind,
83            enc_c1_x,
84            enc_c2_x,
85            enc_c1_y,
86            enc_c2_y,
87        ],
88    )
89}