use halo2_proofs::{
circuit::{AssignedCell, Layouter},
plonk,
};
use halo2_gadgets::poseidon::{
primitives::{self as poseidon, ConstantLength},
Hash as PoseidonHash, Pow5Chip as PoseidonChip,
};
use pasta_curves::pallas;
pub fn hash_share_commitment_in_circuit(
chip: PoseidonChip<pallas::Base, 3, 2>,
mut layouter: impl Layouter<pallas::Base>,
blind: AssignedCell<pallas::Base, pallas::Base>,
enc_c1_x: AssignedCell<pallas::Base, pallas::Base>,
enc_c2_x: AssignedCell<pallas::Base, pallas::Base>,
enc_c1_y: AssignedCell<pallas::Base, pallas::Base>,
enc_c2_y: AssignedCell<pallas::Base, pallas::Base>,
index: usize,
) -> Result<AssignedCell<pallas::Base, pallas::Base>, plonk::Error> {
let hasher = PoseidonHash::<
pallas::Base, _, poseidon::P128Pow5T3, ConstantLength<5>, 3, 2,
>::init(
chip,
layouter.namespace(|| alloc::format!("share_comm_{index} Poseidon init")),
)?;
hasher.hash(
layouter.namespace(|| {
alloc::format!("share_comm_{index} = Poseidon(blind, c1_x, c2_x, c1_y, c2_y)[{index}]")
}),
[blind, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y],
)
}
pub fn compute_shares_hash_in_circuit(
poseidon_chip: impl Fn() -> PoseidonChip<pallas::Base, 3, 2>,
mut layouter: impl Layouter<pallas::Base>,
blinds: [AssignedCell<pallas::Base, pallas::Base>; 16],
enc_c1_x: [AssignedCell<pallas::Base, pallas::Base>; 16],
enc_c2_x: [AssignedCell<pallas::Base, pallas::Base>; 16],
enc_c1_y: [AssignedCell<pallas::Base, pallas::Base>; 16],
enc_c2_y: [AssignedCell<pallas::Base, pallas::Base>; 16],
) -> Result<AssignedCell<pallas::Base, pallas::Base>, plonk::Error> {
let share_comms: [_; 16] = IntoIterator::into_iter(blinds)
.zip(enc_c1_x)
.zip(enc_c2_x)
.zip(enc_c1_y)
.zip(enc_c2_y)
.enumerate()
.map(|(i, ((((blind, c1x), c2x), c1y), c2y))| {
hash_share_commitment_in_circuit(
poseidon_chip(),
layouter.namespace(|| alloc::format!("share_comm_{i}")),
blind, c1x, c2x, c1y, c2y, i,
)
})
.collect::<Result<alloc::vec::Vec<_>, _>>()?
.try_into()
.expect("always 16 elements");
let hasher = PoseidonHash::<
pallas::Base,
_,
poseidon::P128Pow5T3,
ConstantLength<16>,
3, 2, >::init(
poseidon_chip(),
layouter.namespace(|| "shares_hash Poseidon init"),
)?;
hasher.hash(
layouter.namespace(|| "shares_hash = Poseidon(share_comms)"),
share_comms,
)
}
pub(crate) fn compute_shares_hash_from_comms_in_circuit(
poseidon_chip: PoseidonChip<pallas::Base, 3, 2>,
mut layouter: impl Layouter<pallas::Base>,
share_comms: [AssignedCell<pallas::Base, pallas::Base>; 16],
) -> Result<AssignedCell<pallas::Base, pallas::Base>, plonk::Error> {
let hasher = PoseidonHash::<
pallas::Base,
_,
poseidon::P128Pow5T3,
ConstantLength<16>,
3, 2, >::init(
poseidon_chip,
layouter.namespace(|| "shares_hash Poseidon init"),
)?;
hasher.hash(
layouter.namespace(|| "shares_hash = Poseidon(share_comms)"),
share_comms,
)
}
pub fn shares_hash_from_comms(share_comms: [pallas::Base; 16]) -> pallas::Base {
poseidon::Hash::<_, poseidon::P128Pow5T3, ConstantLength<16>, 3, 2>::init().hash(share_comms)
}
#[cfg(test)]
mod tests {
use super::*;
use ff::Field;
use halo2_proofs::{
circuit::{floor_planner, Value},
dev::MockProver,
plonk::{Advice, Column, ConstraintSystem, Fixed, Instance as InstanceColumn},
};
use halo2_gadgets::poseidon::Pow5Config as PoseidonConfig;
use rand::rngs::OsRng;
use crate::vote_proof::circuit::{share_commitment, shares_hash};
#[derive(Clone)]
struct TestConfig {
primary: Column<InstanceColumn>,
advice: Column<Advice>,
poseidon_config: PoseidonConfig<pallas::Base, 3, 2>,
}
impl TestConfig {
fn configure(meta: &mut ConstraintSystem<pallas::Base>) -> Self {
let primary = meta.instance_column();
meta.enable_equality(primary);
let advices: [Column<Advice>; 5] = core::array::from_fn(|_| meta.advice_column());
for col in &advices {
meta.enable_equality(*col);
}
let fixed: [Column<Fixed>; 6] = core::array::from_fn(|_| meta.fixed_column());
let constants = meta.fixed_column();
meta.enable_constant(constants);
let poseidon_config = PoseidonChip::configure::<poseidon::P128Pow5T3>(
meta,
advices[1..4].try_into().unwrap(),
advices[4],
fixed[0..3].try_into().unwrap(),
fixed[3..6].try_into().unwrap(),
);
TestConfig { primary, advice: advices[0], poseidon_config }
}
fn poseidon_chip(&self) -> PoseidonChip<pallas::Base, 3, 2> {
PoseidonChip::construct(self.poseidon_config.clone())
}
}
fn witness(
mut layouter: impl Layouter<pallas::Base>,
col: Column<Advice>,
val: Value<pallas::Base>,
) -> Result<AssignedCell<pallas::Base, pallas::Base>, plonk::Error> {
layouter.assign_region(
|| "witness",
|mut region| region.assign_advice(|| "val", col, 0, || val),
)
}
#[derive(Clone, Default)]
struct HashShareCommCircuit {
blind: pallas::Base,
c1_x: pallas::Base,
c2_x: pallas::Base,
c1_y: pallas::Base,
c2_y: pallas::Base,
}
impl plonk::Circuit<pallas::Base> for HashShareCommCircuit {
type Config = TestConfig;
type FloorPlanner = floor_planner::V1;
fn without_witnesses(&self) -> Self { Self::default() }
fn configure(meta: &mut ConstraintSystem<pallas::Base>) -> Self::Config {
TestConfig::configure(meta)
}
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<pallas::Base>,
) -> Result<(), plonk::Error> {
let blind = witness(layouter.namespace(|| "blind"), config.advice, Value::known(self.blind))?;
let c1x = witness(layouter.namespace(|| "c1_x"), config.advice, Value::known(self.c1_x))?;
let c2x = witness(layouter.namespace(|| "c2_x"), config.advice, Value::known(self.c2_x))?;
let c1y = witness(layouter.namespace(|| "c1_y"), config.advice, Value::known(self.c1_y))?;
let c2y = witness(layouter.namespace(|| "c2_y"), config.advice, Value::known(self.c2_y))?;
let result = hash_share_commitment_in_circuit(
config.poseidon_chip(),
layouter.namespace(|| "hash_share_comm"),
blind, c1x, c2x, c1y, c2y, 0,
)?;
layouter.constrain_instance(result.cell(), config.primary, 0)
}
}
#[test]
fn hash_share_commitment_matches_native() {
let mut rng = OsRng;
let blind = pallas::Base::random(&mut rng);
let c1_x = pallas::Base::random(&mut rng);
let c2_x = pallas::Base::random(&mut rng);
let c1_y = pallas::Base::random(&mut rng);
let c2_y = pallas::Base::random(&mut rng);
let expected = share_commitment(blind, c1_x, c2_x, c1_y, c2_y);
let circuit = HashShareCommCircuit { blind, c1_x, c2_x, c1_y, c2_y };
let prover = MockProver::run(10, &circuit, vec![vec![expected]])
.expect("MockProver::run failed");
assert_eq!(prover.verify(), Ok(()));
}
#[test]
fn hash_share_commitment_input_order_matters() {
let mut rng = OsRng;
let blind = pallas::Base::random(&mut rng);
let c1_x = pallas::Base::random(&mut rng);
let c2_x = pallas::Base::random(&mut rng);
let c1_y = pallas::Base::random(&mut rng);
let c2_y = pallas::Base::random(&mut rng);
let wrong = share_commitment(blind, c2_x, c1_x, c2_y, c1_y);
let circuit = HashShareCommCircuit { blind, c1_x, c2_x, c1_y, c2_y };
let prover = MockProver::run(10, &circuit, vec![vec![wrong]])
.expect("MockProver::run failed");
assert!(prover.verify().is_err());
}
#[test]
fn hash_share_commitment_y_negate_changes_hash() {
let mut rng = OsRng;
let blind = pallas::Base::random(&mut rng);
let c1_x = pallas::Base::random(&mut rng);
let c2_x = pallas::Base::random(&mut rng);
let c1_y = pallas::Base::random(&mut rng);
let c2_y = pallas::Base::random(&mut rng);
let correct = share_commitment(blind, c1_x, c2_x, c1_y, c2_y);
let negated = share_commitment(blind, c1_x, c2_x, -c1_y, c2_y);
assert_ne!(correct, negated, "negating c1_y must change the share commitment");
}
#[derive(Clone)]
struct ComputeSharesHashCircuit {
blinds: [pallas::Base; 16],
enc_c1_x: [pallas::Base; 16],
enc_c2_x: [pallas::Base; 16],
enc_c1_y: [pallas::Base; 16],
enc_c2_y: [pallas::Base; 16],
}
impl Default for ComputeSharesHashCircuit {
fn default() -> Self {
Self {
blinds: [pallas::Base::zero(); 16],
enc_c1_x: [pallas::Base::zero(); 16],
enc_c2_x: [pallas::Base::zero(); 16],
enc_c1_y: [pallas::Base::zero(); 16],
enc_c2_y: [pallas::Base::zero(); 16],
}
}
}
impl plonk::Circuit<pallas::Base> for ComputeSharesHashCircuit {
type Config = TestConfig;
type FloorPlanner = floor_planner::V1;
fn without_witnesses(&self) -> Self { Self::default() }
fn configure(meta: &mut ConstraintSystem<pallas::Base>) -> Self::Config {
TestConfig::configure(meta)
}
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<pallas::Base>,
) -> Result<(), plonk::Error> {
let mut blind_cells = alloc::vec::Vec::with_capacity(16);
let mut c1x_cells = alloc::vec::Vec::with_capacity(16);
let mut c2x_cells = alloc::vec::Vec::with_capacity(16);
let mut c1y_cells = alloc::vec::Vec::with_capacity(16);
let mut c2y_cells = alloc::vec::Vec::with_capacity(16);
for i in 0..16 {
blind_cells.push(witness(layouter.namespace(|| alloc::format!("blind_{i}")), config.advice, Value::known(self.blinds[i]))?);
c1x_cells.push(witness(layouter.namespace(|| alloc::format!("c1x_{i}")), config.advice, Value::known(self.enc_c1_x[i]))?);
c2x_cells.push(witness(layouter.namespace(|| alloc::format!("c2x_{i}")), config.advice, Value::known(self.enc_c2_x[i]))?);
c1y_cells.push(witness(layouter.namespace(|| alloc::format!("c1y_{i}")), config.advice, Value::known(self.enc_c1_y[i]))?);
c2y_cells.push(witness(layouter.namespace(|| alloc::format!("c2y_{i}")), config.advice, Value::known(self.enc_c2_y[i]))?);
}
let blinds: [AssignedCell<pallas::Base, pallas::Base>; 16] = blind_cells.try_into().unwrap();
let enc_c1_x: [AssignedCell<pallas::Base, pallas::Base>; 16] = c1x_cells.try_into().unwrap();
let enc_c2_x: [AssignedCell<pallas::Base, pallas::Base>; 16] = c2x_cells.try_into().unwrap();
let enc_c1_y: [AssignedCell<pallas::Base, pallas::Base>; 16] = c1y_cells.try_into().unwrap();
let enc_c2_y: [AssignedCell<pallas::Base, pallas::Base>; 16] = c2y_cells.try_into().unwrap();
let result = compute_shares_hash_in_circuit(
|| config.poseidon_chip(),
layouter.namespace(|| "compute_shares_hash"),
blinds,
enc_c1_x,
enc_c2_x,
enc_c1_y,
enc_c2_y,
)?;
layouter.constrain_instance(result.cell(), config.primary, 0)
}
}
#[test]
fn compute_shares_hash_matches_native() {
let mut rng = OsRng;
let blinds: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c1_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c2_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c1_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c2_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let expected = shares_hash(blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y);
let circuit = ComputeSharesHashCircuit { blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y };
let prover = MockProver::run(12, &circuit, vec![vec![expected]])
.expect("MockProver::run failed");
assert_eq!(prover.verify(), Ok(()));
}
#[test]
fn compute_shares_hash_wrong_enc_c1_fails() {
let mut rng = OsRng;
let blinds: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c1_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c2_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c1_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c2_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let correct = shares_hash(blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y);
let mut circuit = ComputeSharesHashCircuit { blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y };
circuit.enc_c1_x[2] = pallas::Base::random(&mut rng);
let prover = MockProver::run(12, &circuit, vec![vec![correct]])
.expect("MockProver::run failed");
assert!(prover.verify().is_err());
}
#[test]
fn all_16_share_positions_are_hashed() {
let mut rng = OsRng;
let blinds: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c1_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c2_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c1_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c2_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let correct = shares_hash(blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y);
for i in 0..16 {
let mut circuit = ComputeSharesHashCircuit { blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y };
circuit.enc_c1_x[i] = pallas::Base::random(&mut rng);
let prover = MockProver::run(12, &circuit, vec![vec![correct]])
.unwrap_or_else(|e| panic!("MockProver::run failed at position {i}: {e}"));
assert!(
prover.verify().is_err(),
"corrupting enc_c1_x[{i}] did not change the shares_hash — position is not hashed"
);
}
}
#[test]
fn compute_shares_hash_wrong_blind_fails() {
let mut rng = OsRng;
let blinds: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c1_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c2_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c1_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c2_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let correct = shares_hash(blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y);
let mut circuit = ComputeSharesHashCircuit { blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y };
circuit.blinds[0] = pallas::Base::random(&mut rng);
let prover = MockProver::run(12, &circuit, vec![vec![correct]])
.expect("MockProver::run failed");
assert!(prover.verify().is_err());
}
#[derive(Clone)]
struct ComputeSharesHashFromCommsCircuit {
share_comms: [pallas::Base; 16],
}
impl Default for ComputeSharesHashFromCommsCircuit {
fn default() -> Self {
Self { share_comms: [pallas::Base::zero(); 16] }
}
}
impl plonk::Circuit<pallas::Base> for ComputeSharesHashFromCommsCircuit {
type Config = TestConfig;
type FloorPlanner = floor_planner::V1;
fn without_witnesses(&self) -> Self { Self::default() }
fn configure(meta: &mut ConstraintSystem<pallas::Base>) -> Self::Config {
TestConfig::configure(meta)
}
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<pallas::Base>,
) -> Result<(), plonk::Error> {
let mut comm_cells = alloc::vec::Vec::with_capacity(16);
for i in 0..16 {
comm_cells.push(witness(
layouter.namespace(|| alloc::format!("comm_{i}")),
config.advice,
Value::known(self.share_comms[i]),
)?);
}
let comms: [AssignedCell<pallas::Base, pallas::Base>; 16] =
comm_cells.try_into().unwrap();
let result = super::compute_shares_hash_from_comms_in_circuit(
config.poseidon_chip(),
layouter.namespace(|| "hash_from_comms"),
comms,
)?;
layouter.constrain_instance(result.cell(), config.primary, 0)
}
}
#[test]
fn shares_hash_from_comms_matches_native() {
let mut rng = OsRng;
let blinds: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c1_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c2_x: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c1_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let enc_c2_y: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let comms: [pallas::Base; 16] =
core::array::from_fn(|i| share_commitment(blinds[i], enc_c1_x[i], enc_c2_x[i], enc_c1_y[i], enc_c2_y[i]));
let expected = super::shares_hash_from_comms(comms);
assert_eq!(expected, shares_hash(blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y));
let circuit = ComputeSharesHashFromCommsCircuit { share_comms: comms };
let prover = MockProver::run(12, &circuit, vec![vec![expected]])
.expect("MockProver::run failed");
assert_eq!(prover.verify(), Ok(()));
}
#[test]
fn shares_hash_from_comms_wrong_comm_fails() {
let mut rng = OsRng;
let comms: [pallas::Base; 16] = core::array::from_fn(|_| pallas::Base::random(&mut rng));
let expected = super::shares_hash_from_comms(comms);
let mut bad_comms = comms;
bad_comms[7] = pallas::Base::random(&mut rng);
let circuit = ComputeSharesHashFromCommsCircuit { share_comms: bad_comms };
let prover = MockProver::run(12, &circuit, vec![vec![expected]])
.expect("MockProver::run failed");
assert!(prover.verify().is_err());
}
}