use alloc::vec::Vec;
use ff::{Field, PrimeField};
use halo2_proofs::{
circuit::{AssignedCell, Layouter, Value},
plonk::{
self, Advice, Column, ConstraintSystem, Constraints, Expression, Selector, TableColumn,
VirtualCells,
},
poly::Rotation,
};
use pasta_curves::pallas;
use halo2_gadgets::utilities::bool_check;
use crate::vote_proof::circuit::MAX_PROPOSAL_ID;
#[derive(Clone, Debug)]
pub struct AuthorityDecrementConfig {
pub(super) q_cond_6: Selector,
pub(super) table_proposal_id: TableColumn,
pub(super) table_one_shifted: TableColumn,
pub(super) q_cond_6_init: Selector,
pub(super) q_cond_6_bits: Selector,
pub(super) q_cond_6_selected_one: Selector,
pub(super) proposal_id_inv: Column<Advice>,
pub(super) advices: [Column<Advice>; 10],
}
struct Cond6Row {
b_i: Expression<pallas::Base>,
sel_i: Expression<pallas::Base>,
b_new_i: Expression<pallas::Base>,
run_sel_pow: Expression<pallas::Base>,
run_sel_pow_prev: Expression<pallas::Base>,
run_selected: Expression<pallas::Base>,
run_selected_prev: Expression<pallas::Base>,
run_old: Expression<pallas::Base>,
run_old_prev: Expression<pallas::Base>,
run_new: Expression<pallas::Base>,
run_new_prev: Expression<pallas::Base>,
two_pow_i: Expression<pallas::Base>,
}
fn query_cond6_row(
meta: &mut VirtualCells<pallas::Base>,
advices: &[Column<Advice>],
) -> Cond6Row {
Cond6Row {
b_i: meta.query_advice(advices[0], Rotation::cur()),
sel_i: meta.query_advice(advices[1], Rotation::cur()),
b_new_i: meta.query_advice(advices[2], Rotation::cur()),
run_sel_pow: meta.query_advice(advices[3], Rotation::cur()),
run_sel_pow_prev: meta.query_advice(advices[3], Rotation::prev()),
run_selected: meta.query_advice(advices[4], Rotation::cur()),
run_selected_prev: meta.query_advice(advices[4], Rotation::prev()),
run_old: meta.query_advice(advices[5], Rotation::cur()),
run_old_prev: meta.query_advice(advices[5], Rotation::prev()),
run_new: meta.query_advice(advices[6], Rotation::cur()),
run_new_prev: meta.query_advice(advices[6], Rotation::prev()),
two_pow_i: meta.query_advice(advices[7], Rotation::cur()),
}
}
fn cond6_shared_constraints(
r: &Cond6Row,
) -> Vec<(&'static str, Expression<pallas::Base>)> {
vec![
("run_sel_pow",
r.run_sel_pow.clone() - r.run_sel_pow_prev.clone() - r.sel_i.clone() * r.two_pow_i.clone()),
("run_selected",
r.run_selected.clone() - r.run_selected_prev.clone() - r.sel_i.clone() * r.b_i.clone()),
("run_old",
r.run_old.clone() - r.run_old_prev.clone() - r.b_i.clone() * r.two_pow_i.clone()),
("run_new",
r.run_new.clone() - r.run_new_prev.clone() - r.b_new_i.clone() * r.two_pow_i.clone()),
("b_new_i = b_i*(1-sel_i)",
r.b_new_i.clone() - r.b_i.clone() + r.b_i.clone() * r.sel_i.clone()),
("bool b_i", bool_check(r.b_i.clone())),
("bool sel_i", bool_check(r.sel_i.clone())),
]
}
pub struct AuthorityDecrementChip;
impl AuthorityDecrementChip {
pub fn configure(
meta: &mut ConstraintSystem<pallas::Base>,
advices: [Column<Advice>; 10],
) -> AuthorityDecrementConfig {
let q_cond_6 = meta.complex_selector();
let table_proposal_id = meta.lookup_table_column();
let table_one_shifted = meta.lookup_table_column();
meta.lookup(|meta| {
let q = meta.query_selector(q_cond_6);
let proposal_id = meta.query_advice(advices[0], Rotation::cur());
let one_shifted = meta.query_advice(advices[1], Rotation::cur());
let input_0 = q.clone() * proposal_id;
let one = Expression::Constant(pallas::Base::one());
let input_1 = q.clone() * one_shifted + (one.clone() - q);
vec![
(input_0, table_proposal_id),
(input_1, table_one_shifted),
]
});
meta.create_gate("proposal_id != 0", |meta| {
let q = meta.query_selector(q_cond_6);
let proposal_id = meta.query_advice(advices[0], Rotation::cur());
let proposal_id_inv = meta.query_advice(advices[2], Rotation::cur());
let one = Expression::Constant(pallas::Base::one());
vec![("proposal_id * inv = 1", q * (one - proposal_id * proposal_id_inv))]
});
let q_cond_6_init = meta.selector();
let q_cond_6_bits = meta.selector();
let one_expr = Expression::Constant(pallas::Base::one());
let two_expr = Expression::Constant(pallas::Base::from(2u64));
meta.create_gate("cond6 init: two_pow_i=1, running sums", |meta| {
let q = meta.query_selector(q_cond_6_init);
let r = query_cond6_row(meta, &advices);
let mut constraints = vec![
("two_pow_i = 1", r.two_pow_i.clone() - one_expr.clone()),
];
constraints.extend(cond6_shared_constraints(&r));
Constraints::with_selector(q, constraints)
});
meta.create_gate("cond6 bits: two_pow_i*=2, running sums", |meta| {
let q = meta.query_selector(q_cond_6_bits);
let r = query_cond6_row(meta, &advices);
let two_pow_i_prev = meta.query_advice(advices[7], Rotation::prev());
let mut constraints = vec![
("two_pow_i = 2*prev", r.two_pow_i.clone() - two_expr.clone() * two_pow_i_prev),
];
constraints.extend(cond6_shared_constraints(&r));
Constraints::with_selector(q, constraints)
});
let q_cond_6_selected_one = meta.selector();
meta.create_gate("cond6 run_selected = 1", |meta| {
let q = meta.query_selector(q_cond_6_selected_one);
let run_selected = meta.query_advice(advices[4], Rotation::cur());
Constraints::with_selector(
q,
[("run_selected = 1", run_selected - one_expr)],
)
});
AuthorityDecrementConfig {
q_cond_6,
table_proposal_id,
table_one_shifted,
q_cond_6_init,
q_cond_6_bits,
q_cond_6_selected_one,
proposal_id_inv: advices[2],
advices,
}
}
pub fn load_table(
config: &AuthorityDecrementConfig,
layouter: &mut impl Layouter<pallas::Base>,
) -> Result<(), plonk::Error> {
layouter.assign_table(
|| "proposal_id one_shifted table",
|mut table| {
for i in 0..MAX_PROPOSAL_ID {
table.assign_cell(
|| "table proposal_id",
config.table_proposal_id,
i,
|| Value::known(pallas::Base::from(i as u64)),
)?;
table.assign_cell(
|| "table one_shifted",
config.table_one_shifted,
i,
|| Value::known(pallas::Base::from(1u64 << i)),
)?;
}
Ok(())
},
)
}
pub fn assign(
config: &AuthorityDecrementConfig,
layouter: &mut impl Layouter<pallas::Base>,
proposal_id: AssignedCell<pallas::Base, pallas::Base>,
proposal_authority_old: AssignedCell<pallas::Base, pallas::Base>,
one_shifted: Value<pallas::Base>,
) -> Result<AssignedCell<pallas::Base, pallas::Base>, plonk::Error> {
let (run_old_final, run_new_final, run_sel_pow_final, one_shifted_final) =
layouter.assign_region(
|| "cond6 proposal authority decrement",
|mut region| {
let proposal_authority_old_val = proposal_authority_old.value().copied();
config.q_cond_6.enable(&mut region, 0)?;
let proposal_id_cell = proposal_id.copy_advice(
|| "proposal_id",
&mut region,
config.advices[0],
0,
)?;
let one_shifted_cell = region.assign_advice(
|| "one_shifted",
config.advices[1],
0,
|| one_shifted,
)?;
region.assign_advice(
|| "proposal_id_inv",
config.proposal_id_inv,
0,
|| {
proposal_id_cell.value().map(|pid| {
Option::from(pid.invert()).unwrap_or(pallas::Base::zero())
})
},
)?;
region.assign_advice_from_constant(
|| "run_sel_pow init",
config.advices[3],
0,
pallas::Base::zero(),
)?;
region.assign_advice_from_constant(
|| "run_selected init",
config.advices[4],
0,
pallas::Base::zero(),
)?;
region.assign_advice_from_constant(
|| "run_old init",
config.advices[5],
0,
pallas::Base::zero(),
)?;
region.assign_advice_from_constant(
|| "run_new init",
config.advices[6],
0,
pallas::Base::zero(),
)?;
let zero_val = Value::known(pallas::Base::zero());
let mut run_old_prev = zero_val;
let mut run_new_prev = zero_val;
let mut run_sel_pow_prev = zero_val;
let mut run_selected_prev = zero_val;
let mut run_old_last_cell: Option<AssignedCell<pallas::Base, pallas::Base>> =
None;
let mut run_new_last_cell: Option<AssignedCell<pallas::Base, pallas::Base>> =
None;
let mut run_sel_pow_last_cell: Option<AssignedCell<pallas::Base, pallas::Base>> =
None;
for i in 0..MAX_PROPOSAL_ID {
let row = 1 + i;
let proposal_id_base = proposal_id_cell.value().copied();
let b_i_val = proposal_authority_old_val.map(|b| {
let r = b.to_repr();
let arr = r.as_ref();
let low = u64::from_le_bytes(arr[0..8].try_into().unwrap()) & 0xFFFF;
let bit = (low >> i) & 1;
pallas::Base::from(bit)
});
let sel_i_val = proposal_id_base.map(|pid| {
let r = pid.to_repr();
let arr = r.as_ref();
let pid_u64 = u64::from_le_bytes(arr[0..8].try_into().unwrap());
pallas::Base::from(if pid_u64 == i as u64 { 1u64 } else { 0 })
});
let b_new_i_val = b_i_val.zip(sel_i_val)
.map(|(b, s)| b - b * s);
let two_pow_i_val = Value::known(pallas::Base::from(1u64 << i));
run_sel_pow_prev = run_sel_pow_prev
.zip(sel_i_val)
.zip(two_pow_i_val)
.map(|((r, s), t)| r + s * t);
run_selected_prev = run_selected_prev
.zip(sel_i_val)
.zip(b_i_val)
.map(|((r, s), b)| r + s * b);
run_old_prev = run_old_prev
.zip(b_i_val)
.zip(two_pow_i_val)
.map(|((r, b), t)| r + b * t);
run_new_prev = run_new_prev
.zip(b_new_i_val)
.zip(two_pow_i_val)
.map(|((r, b), t)| r + b * t);
region.assign_advice(
|| format!("b_{}", i),
config.advices[0],
row,
|| b_i_val,
)?;
region.assign_advice(
|| format!("sel_{}", i),
config.advices[1],
row,
|| sel_i_val,
)?;
region.assign_advice(
|| format!("b_new_{}", i),
config.advices[2],
row,
|| b_new_i_val,
)?;
let run_sel_pow_cur = region.assign_advice(
|| format!("run_sel_pow {}", i),
config.advices[3],
row,
|| run_sel_pow_prev,
)?;
region.assign_advice(
|| format!("run_selected {}", i),
config.advices[4],
row,
|| run_selected_prev,
)?;
let run_old_cur = region.assign_advice(
|| format!("run_old {}", i),
config.advices[5],
row,
|| run_old_prev,
)?;
let run_new_cur = region.assign_advice(
|| format!("run_new {}", i),
config.advices[6],
row,
|| run_new_prev,
)?;
region.assign_advice(
|| format!("two_pow_i {}", i),
config.advices[7],
row,
|| two_pow_i_val,
)?;
if i == 0 {
config.q_cond_6_init.enable(&mut region, row)?;
} else {
config.q_cond_6_bits.enable(&mut region, row)?;
}
if i == MAX_PROPOSAL_ID - 1 {
config.q_cond_6_selected_one.enable(&mut region, row)?;
run_old_last_cell = Some(run_old_cur);
run_new_last_cell = Some(run_new_cur);
run_sel_pow_last_cell = Some(run_sel_pow_cur);
}
}
Ok((
run_old_last_cell.unwrap(),
run_new_last_cell.unwrap(),
run_sel_pow_last_cell.unwrap(),
one_shifted_cell,
))
},
)?;
layouter.assign_region(
|| "cond6 authority equality",
|mut region| {
let a = proposal_authority_old.copy_advice(
|| "copy proposal_authority_old",
&mut region,
config.advices[0],
0,
)?;
let b = run_old_final.copy_advice(
|| "copy run_old",
&mut region,
config.advices[1],
0,
)?;
region.constrain_equal(a.cell(), b.cell())
},
)?;
layouter.assign_region(
|| "cond6 sel_pow equality",
|mut region| {
let a = one_shifted_final.copy_advice(
|| "copy one_shifted",
&mut region,
config.advices[0],
0,
)?;
let b = run_sel_pow_final.copy_advice(
|| "copy run_sel_pow",
&mut region,
config.advices[1],
0,
)?;
region.constrain_equal(a.cell(), b.cell())
},
)?;
Ok(run_new_final)
}
}
#[cfg(test)]
mod tests {
use super::*;
use halo2_proofs::{
circuit::{Layouter, SimpleFloorPlanner},
dev::MockProver,
plonk::{Circuit, Column, ConstraintSystem, Fixed, Instance},
};
use pasta_curves::pallas;
#[derive(Clone, Debug)]
struct TestConfig {
adec: AuthorityDecrementConfig,
primary: Column<Instance>,
advices: [Column<Advice>; 10],
constants: Column<Fixed>,
}
#[derive(Default, Clone)]
struct TestCircuit {
proposal_authority_old: Value<pallas::Base>,
one_shifted: Value<pallas::Base>,
proposal_id: Value<pallas::Base>,
}
impl Circuit<pallas::Base> for TestCircuit {
type Config = TestConfig;
type FloorPlanner = SimpleFloorPlanner;
fn without_witnesses(&self) -> Self {
Self::default()
}
fn configure(meta: &mut ConstraintSystem<pallas::Base>) -> TestConfig {
let advices: [Column<Advice>; 10] = core::array::from_fn(|_| {
let col = meta.advice_column();
meta.enable_equality(col);
col
});
let primary = meta.instance_column();
meta.enable_equality(primary);
let constants = meta.fixed_column();
meta.enable_constant(constants);
TestConfig {
adec: AuthorityDecrementChip::configure(meta, advices),
primary,
advices,
constants,
}
}
fn synthesize(
&self,
config: TestConfig,
mut layouter: impl Layouter<pallas::Base>,
) -> Result<(), plonk::Error> {
AuthorityDecrementChip::load_table(&config.adec, &mut layouter)?;
let proposal_authority_old = layouter.assign_region(
|| "witness old",
|mut region| {
region.assign_advice(
|| "proposal_authority_old",
config.advices[0],
0,
|| self.proposal_authority_old,
)
},
)?;
let proposal_id = layouter.assign_region(
|| "proposal_id from instance",
|mut region| {
region.assign_advice_from_instance(
|| "proposal_id",
config.primary,
0,
config.advices[0],
0,
)
},
)?;
let proposal_authority_new = AuthorityDecrementChip::assign(
&config.adec,
&mut layouter,
proposal_id,
proposal_authority_old,
self.one_shifted,
)?;
layouter.constrain_instance(
proposal_authority_new.cell(),
config.primary,
1,
)?;
Ok(())
}
}
fn run_chip(
authority_old: u64,
proposal_id: u64,
authority_new_override: Option<pallas::Base>,
) -> Result<(), Vec<halo2_proofs::dev::VerifyFailure>> {
let one_shifted = pallas::Base::from(1u64 << proposal_id);
let authority_new_expected = authority_new_override
.unwrap_or_else(|| pallas::Base::from(authority_old) - one_shifted);
let circuit = TestCircuit {
proposal_authority_old: Value::known(pallas::Base::from(authority_old)),
one_shifted: Value::known(one_shifted),
proposal_id: Value::known(pallas::Base::from(proposal_id)),
};
let instance = vec![
pallas::Base::from(proposal_id),
authority_new_expected,
];
let prover = MockProver::run(5, &circuit, vec![instance]).unwrap();
prover.verify()
}
#[test]
fn valid_basic() {
assert_eq!(run_chip(2, 1, None), Ok(()));
}
#[test]
fn valid_full_authority_high_bit() {
assert_eq!(run_chip(0x8000, 15, None), Ok(()));
}
#[test]
fn valid_all_bits_set() {
assert_eq!(run_chip(0xFFFF, 5, None), Ok(()));
}
#[test]
fn proposal_id_zero_fails() {
assert!(run_chip(1, 0, None).is_err(), "proposal_id = 0 must be rejected");
}
#[test]
fn bit_not_set_fails() {
assert!(run_chip(4, 1, None).is_err(), "bit not set must fail");
}
#[test]
fn wrong_new_value_fails() {
let wrong_new = pallas::Base::from(0xDEAD_u64);
assert!(
run_chip(0xFFFF, 5, Some(wrong_new)).is_err(),
"tampered new value must fail equality constraint",
);
}
#[test]
fn exploit_disconnected_run_old_final_regression() {
assert!(
run_chip(0x0008, 1, None).is_err(),
"authority 0x0008 with proposal_id=1 (bit absent) must be rejected"
);
assert!(
run_chip(0x0004, 1, None).is_err(),
"authority 0x0004 with proposal_id=1 (bit absent) must be rejected"
);
assert!(
run_chip(0x00FF, 8, None).is_err(),
"authority 0x00FF with proposal_id=8 (bit absent) must be rejected"
);
assert!(
run_chip(0x0000, 5, None).is_err(),
"zero authority with any proposal_id must be rejected"
);
}
}