use alloc::vec::Vec;
use halo2_proofs::{
circuit::{floor_planner, AssignedCell, Layouter, Value},
plonk::{
self, Advice, Column, Constraints, ConstraintSystem, Expression, Fixed,
Instance as InstanceColumn, Selector,
},
poly::Rotation,
};
use pasta_curves::{pallas, vesta};
use halo2_gadgets::{
poseidon::{
primitives::{self as poseidon, ConstantLength},
Hash as PoseidonHash, Pow5Chip as PoseidonChip, Pow5Config as PoseidonConfig,
},
utilities::bool_check,
};
use orchard::circuit::gadget::assign_free_advice;
use crate::circuit::poseidon_merkle::{MerkleSwapGate, synthesize_poseidon_merkle_path};
use crate::circuit::vote_commitment;
use crate::vote_proof::VOTE_COMM_TREE_DEPTH;
use crate::shares_hash::{
compute_shares_hash_from_comms_in_circuit,
hash_share_commitment_in_circuit,
};
pub const K: u32 = 11;
const SHARE_NULLIFIER: usize = 0;
const ENC_SHARE_C1_X: usize = 1;
const ENC_SHARE_C1_Y: usize = 2;
const ENC_SHARE_C2_X: usize = 3;
const ENC_SHARE_C2_Y: usize = 4;
const PROPOSAL_ID: usize = 5;
const VOTE_DECISION: usize = 6;
const VOTE_COMM_TREE_ROOT: usize = 7;
const VOTING_ROUND_ID: usize = 8;
pub fn domain_tag_share_spend() -> pallas::Base {
use ff::PrimeField;
let mut bytes = [0u8; 32];
let tag = b"share spend";
bytes[..tag.len()].copy_from_slice(tag);
pallas::Base::from_repr(bytes).unwrap()
}
pub fn share_nullifier_hash(
vote_commitment: pallas::Base,
share_index: pallas::Base,
blind: pallas::Base,
) -> pallas::Base {
poseidon::Hash::<_, poseidon::P128Pow5T3, ConstantLength<4>, 3, 2>::init().hash([
domain_tag_share_spend(),
vote_commitment,
share_index,
blind,
])
}
#[derive(Clone, Debug)]
pub struct Config {
primary: Column<InstanceColumn>,
advices: [Column<Advice>; 9],
poseidon_config: PoseidonConfig<pallas::Base, 3, 2>,
merkle_swap: MerkleSwapGate,
q_share_comm_mux: Selector,
}
impl Config {
pub(crate) fn poseidon_chip(&self) -> PoseidonChip<pallas::Base, 3, 2> {
PoseidonChip::construct(self.poseidon_config.clone())
}
pub(crate) fn assign_constant(
&self,
layouter: &mut impl Layouter<pallas::Base>,
label: &'static str,
value: pallas::Base,
) -> Result<AssignedCell<pallas::Base, pallas::Base>, plonk::Error> {
layouter.assign_region(
|| label,
|mut region| {
region.assign_advice_from_constant(|| label, self.advices[0], 0, value)
},
)
}
}
#[derive(Clone, Debug)]
pub struct Circuit {
pub(crate) vote_comm_tree_path: Value<[pallas::Base; VOTE_COMM_TREE_DEPTH]>,
pub(crate) vote_comm_tree_position: Value<u32>,
pub(crate) share_comms: [Value<pallas::Base>; 16],
pub(crate) primary_blind: Value<pallas::Base>,
pub(crate) share_index: Value<pallas::Base>,
pub(crate) vote_commitment: Value<pallas::Base>,
}
impl Default for Circuit {
fn default() -> Self {
Self {
vote_comm_tree_path: Value::unknown(),
vote_comm_tree_position: Value::unknown(),
share_comms: [Value::unknown(); 16],
primary_blind: Value::unknown(),
share_index: Value::unknown(),
vote_commitment: Value::unknown(),
}
}
}
impl plonk::Circuit<pallas::Base> for Circuit {
type Config = Config;
type FloorPlanner = floor_planner::V1;
fn without_witnesses(&self) -> Self {
Self::default()
}
fn configure(meta: &mut ConstraintSystem<pallas::Base>) -> Self::Config {
let advices: [Column<Advice>; 9] = core::array::from_fn(|_| meta.advice_column());
for col in &advices {
meta.enable_equality(*col);
}
let primary = meta.instance_column();
meta.enable_equality(primary);
let lagrange_coeffs: [Column<Fixed>; 8] =
core::array::from_fn(|_| meta.fixed_column());
let rc_a = lagrange_coeffs[2..5].try_into().unwrap();
let rc_b = lagrange_coeffs[5..8].try_into().unwrap();
meta.enable_constant(lagrange_coeffs[0]);
let poseidon_config = PoseidonChip::configure::<poseidon::P128Pow5T3>(
meta,
advices[6..9].try_into().unwrap(),
advices[5],
rc_a,
rc_b,
);
let merkle_swap = MerkleSwapGate::configure(
meta,
[advices[0], advices[1], advices[2], advices[3], advices[4]],
);
let q_share_comm_mux = meta.selector();
meta.create_gate("share commitment multiplexer", |meta| {
let q = meta.query_selector(q_share_comm_mux);
let sel: [_; 16] = [
meta.query_advice(advices[0], Rotation::cur()),
meta.query_advice(advices[1], Rotation::cur()),
meta.query_advice(advices[2], Rotation::cur()),
meta.query_advice(advices[3], Rotation::cur()),
meta.query_advice(advices[4], Rotation::cur()),
meta.query_advice(advices[5], Rotation::cur()),
meta.query_advice(advices[6], Rotation::cur()),
meta.query_advice(advices[7], Rotation::cur()),
meta.query_advice(advices[8], Rotation::cur()),
meta.query_advice(advices[0], Rotation::next()),
meta.query_advice(advices[1], Rotation::next()),
meta.query_advice(advices[2], Rotation::next()),
meta.query_advice(advices[3], Rotation::next()),
meta.query_advice(advices[4], Rotation::next()),
meta.query_advice(advices[5], Rotation::next()),
meta.query_advice(advices[6], Rotation::next()),
];
let comm: [_; 16] = [
meta.query_advice(advices[7], Rotation::next()),
meta.query_advice(advices[8], Rotation::next()),
meta.query_advice(advices[0], Rotation(2)),
meta.query_advice(advices[1], Rotation(2)),
meta.query_advice(advices[2], Rotation(2)),
meta.query_advice(advices[3], Rotation(2)),
meta.query_advice(advices[4], Rotation(2)),
meta.query_advice(advices[5], Rotation(2)),
meta.query_advice(advices[6], Rotation(2)),
meta.query_advice(advices[7], Rotation(2)),
meta.query_advice(advices[8], Rotation(2)),
meta.query_advice(advices[0], Rotation(3)),
meta.query_advice(advices[1], Rotation(3)),
meta.query_advice(advices[2], Rotation(3)),
meta.query_advice(advices[3], Rotation(3)),
meta.query_advice(advices[4], Rotation(3)),
];
let selected_comm = meta.query_advice(advices[5], Rotation(3));
let share_index = meta.query_advice(advices[6], Rotation(3));
let one = Expression::Constant(pallas::Base::one());
let bool_checks: Vec<(&'static str, Expression<pallas::Base>)> = (0..16)
.map(|i| ("bool sel_i", bool_check(sel[i].clone())))
.collect();
let sum_expr = sel.iter().skip(1).fold(sel[0].clone(), |acc, s| acc + s.clone());
let sum_check = ("sum sel == 1", sum_expr - one);
let reconstructed = sel.iter().enumerate().skip(1).fold(
Expression::Constant(pallas::Base::zero()),
|acc, (i, s)| acc + Expression::Constant(pallas::Base::from(i as u64)) * s.clone(),
);
let index_reconstruct = ("index reconstruct", share_index.clone() - reconstructed);
let comm_mux_expr = comm.iter().zip(sel.iter())
.fold(selected_comm, |acc, (c, s)| acc - s.clone() * c.clone());
let comm_mux = ("comm mux", comm_mux_expr);
let mut constraints: Vec<(&'static str, Expression<pallas::Base>)> = bool_checks;
constraints.push(sum_check);
constraints.push(index_reconstruct);
constraints.push(comm_mux);
Constraints::with_selector(q, constraints)
});
Config {
primary,
advices,
poseidon_config,
merkle_swap,
q_share_comm_mux,
}
}
#[allow(non_snake_case)]
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<pallas::Base>,
) -> Result<(), plonk::Error> {
let vote_commitment = assign_free_advice(
layouter.namespace(|| "witness vote_commitment"),
config.advices[0],
self.vote_commitment,
)?;
let vote_commitment_cond2 = vote_commitment.clone();
let vote_commitment_cond5 = vote_commitment.clone();
let share_index = assign_free_advice(
layouter.namespace(|| "witness share_index"),
config.advices[0],
self.share_index,
)?;
let share_index_cond5 = share_index.clone();
let primary_blind = assign_free_advice(
layouter.namespace(|| "witness primary_blind"),
config.advices[0],
self.primary_blind,
)?;
let primary_blind_cond5 = primary_blind.clone();
let proposal_id = layouter.assign_region(
|| "copy proposal_id from instance",
|mut region| {
region.assign_advice_from_instance(
|| "proposal_id",
config.primary,
PROPOSAL_ID,
config.advices[0],
0,
)
},
)?;
let vote_decision = layouter.assign_region(
|| "copy vote_decision from instance",
|mut region| {
region.assign_advice_from_instance(
|| "vote_decision",
config.primary,
VOTE_DECISION,
config.advices[0],
0,
)
},
)?;
let voting_round_id = layouter.assign_region(
|| "copy voting_round_id from instance",
|mut region| {
region.assign_advice_from_instance(
|| "voting_round_id",
config.primary,
VOTING_ROUND_ID,
config.advices[0],
0,
)
},
)?;
let voting_round_id_cond2 = voting_round_id;
let share_comms: [AssignedCell<pallas::Base, pallas::Base>; 16] = {
let mut cells = Vec::with_capacity(16);
for i in 0..16 {
cells.push(assign_free_advice(
layouter.namespace(|| alloc::format!("witness share_comm[{i}]")),
config.advices[0],
self.share_comms[i],
)?);
}
cells.try_into().unwrap()
};
let share_comms_cond4: [AssignedCell<pallas::Base, pallas::Base>; 16] =
core::array::from_fn(|i| share_comms[i].clone());
let shares_hash = compute_shares_hash_from_comms_in_circuit(
config.poseidon_chip(),
layouter.namespace(|| "cond3: shares_hash from comms"),
share_comms,
)?;
let shares_hash_cond2 = shares_hash.clone();
let enc_c1_x = layouter.assign_region(
|| "copy enc_share_c1_x from instance",
|mut region| {
region.assign_advice_from_instance(
|| "enc_c1_x",
config.primary,
ENC_SHARE_C1_X,
config.advices[0],
0,
)
},
)?;
let enc_c2_x = layouter.assign_region(
|| "copy enc_share_c2_x from instance",
|mut region| {
region.assign_advice_from_instance(
|| "enc_c2_x",
config.primary,
ENC_SHARE_C2_X,
config.advices[0],
0,
)
},
)?;
let enc_c1_y = layouter.assign_region(
|| "copy enc_share_c1_y from instance",
|mut region| {
region.assign_advice_from_instance(
|| "enc_c1_y",
config.primary,
ENC_SHARE_C1_Y,
config.advices[0],
0,
)
},
)?;
let enc_c2_y = layouter.assign_region(
|| "copy enc_share_c2_y from instance",
|mut region| {
region.assign_advice_from_instance(
|| "enc_c2_y",
config.primary,
ENC_SHARE_C2_Y,
config.advices[0],
0,
)
},
)?;
let derived_comm = hash_share_commitment_in_circuit(
config.poseidon_chip(),
layouter.namespace(|| "cond4: Poseidon(blind, c1_x, c2_x, c1_y, c2_y)"),
primary_blind,
enc_c1_x,
enc_c2_x,
enc_c1_y,
enc_c2_y,
0,
)?;
let selected_comm = layouter.assign_region(
|| "cond4: share commitment mux",
|mut region| {
config.q_share_comm_mux.enable(&mut region, 0)?;
let sel_values: [Value<pallas::Base>; 16] = core::array::from_fn(|i| {
self.share_index.map(|idx| {
if idx == pallas::Base::from(i as u64) {
pallas::Base::one()
} else {
pallas::Base::zero()
}
})
});
for (sel_start, count, col_off, row) in [(0, 9, 0, 0), (9, 7, 0, 1)] {
for i in 0..count {
region.assign_advice(
|| alloc::format!("sel_{}", sel_start + i),
config.advices[col_off + i],
row,
|| sel_values[sel_start + i],
)?;
}
}
for (comm_start, count, col_off, row) in [(0, 2, 7, 1), (2, 9, 0, 2), (11, 5, 0, 3)] {
for i in 0..count {
share_comms_cond4[comm_start + i].copy_advice(
|| alloc::format!("comm_{}", comm_start + i),
&mut region,
config.advices[col_off + i],
row,
)?;
}
}
let selected_comm_val = (0..16).fold(Value::known(pallas::Base::zero()), |acc, i| {
acc.zip(sel_values[i]).zip(share_comms_cond4[i].value().copied())
.map(|((a, s), c)| a + s * c)
});
let selected_comm = region.assign_advice(
|| "selected_comm",
config.advices[5],
3,
|| selected_comm_val,
)?;
share_index.copy_advice(
|| "share_index",
&mut region,
config.advices[6],
3,
)?;
Ok(selected_comm)
},
)?;
layouter.assign_region(
|| "cond4: derived_comm == selected_comm",
|mut region| region.constrain_equal(derived_comm.cell(), selected_comm.cell()),
)?;
let domain_vc = config.assign_constant(
&mut layouter,
"cond2: DOMAIN_VC constant",
pallas::Base::from(vote_commitment::DOMAIN_VC),
)?;
let derived_vc = vote_commitment::vote_commitment_poseidon(
&config.poseidon_config,
&mut layouter,
"cond2",
domain_vc,
voting_round_id_cond2,
shares_hash_cond2,
proposal_id,
vote_decision,
)?;
layouter.assign_region(
|| "cond2: vote_commitment equality",
|mut region| {
region.constrain_equal(derived_vc.cell(), vote_commitment_cond2.cell())
},
)?;
{
let root = synthesize_poseidon_merkle_path::<VOTE_COMM_TREE_DEPTH>(
&config.merkle_swap,
&config.poseidon_config,
&mut layouter,
config.advices[0],
vote_commitment,
self.vote_comm_tree_position,
self.vote_comm_tree_path,
"cond1: merkle",
)?;
layouter.constrain_instance(
root.cell(),
config.primary,
VOTE_COMM_TREE_ROOT,
)?;
}
{
let domain_tag = config.assign_constant(
&mut layouter,
"cond5: DOMAIN_SHARE_SPEND constant",
domain_tag_share_spend(),
)?;
let share_nullifier = PoseidonHash::<
pallas::Base,
_,
poseidon::P128Pow5T3,
ConstantLength<4>,
3,
2,
>::init(
config.poseidon_chip(),
layouter.namespace(|| "cond5: share nullifier Poseidon init"),
)?
.hash(
layouter.namespace(|| "cond5: Poseidon(tag, vc, idx, blind)"),
[domain_tag, vote_commitment_cond5, share_index_cond5,
primary_blind_cond5],
)?;
layouter.constrain_instance(
share_nullifier.cell(),
config.primary,
SHARE_NULLIFIER,
)?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct Instance {
pub share_nullifier: pallas::Base,
pub enc_share_c1_x: pallas::Base,
pub enc_share_c2_x: pallas::Base,
pub proposal_id: pallas::Base,
pub vote_decision: pallas::Base,
pub vote_comm_tree_root: pallas::Base,
pub voting_round_id: pallas::Base,
pub enc_share_c1_y: pallas::Base,
pub enc_share_c2_y: pallas::Base,
}
impl Instance {
#[allow(clippy::too_many_arguments)]
pub fn from_parts(
share_nullifier: pallas::Base,
enc_share_c1_x: pallas::Base,
enc_share_c2_x: pallas::Base,
proposal_id: pallas::Base,
vote_decision: pallas::Base,
vote_comm_tree_root: pallas::Base,
voting_round_id: pallas::Base,
enc_share_c1_y: pallas::Base,
enc_share_c2_y: pallas::Base,
) -> Self {
Instance {
share_nullifier,
enc_share_c1_x,
enc_share_c2_x,
proposal_id,
vote_decision,
vote_comm_tree_root,
voting_round_id,
enc_share_c1_y,
enc_share_c2_y,
}
}
pub fn to_halo2_instance(&self) -> Vec<vesta::Scalar> {
alloc::vec![
self.share_nullifier,
self.enc_share_c1_x,
self.enc_share_c1_y,
self.enc_share_c2_x,
self.enc_share_c2_y,
self.proposal_id,
self.vote_decision,
self.vote_comm_tree_root,
self.voting_round_id,
]
}
}
#[cfg(test)]
mod tests {
use super::*;
use group::Curve;
use halo2_proofs::dev::MockProver;
use pasta_curves::pallas;
use crate::vote_proof::{
elgamal_encrypt, poseidon_hash_2, share_commitment,
shares_hash as compute_shares_hash,
spend_auth_g_affine, vote_commitment_hash as compute_vote_commitment_hash,
};
fn generate_ea_keypair() -> (pallas::Scalar, pallas::Point, pallas::Affine) {
let ea_sk = pallas::Scalar::from(42u64);
let g = pallas::Point::from(spend_auth_g_affine());
let ea_pk = g * ea_sk;
let ea_pk_affine = ea_pk.to_affine();
(ea_sk, ea_pk, ea_pk_affine)
}
fn encrypt_shares(
shares: [u64; 16],
ea_pk: pallas::Point,
) -> (
[pallas::Base; 16],
[pallas::Base; 16],
[pallas::Base; 16],
[pallas::Base; 16],
[pallas::Base; 16],
[pallas::Base; 16],
pallas::Base,
) {
let mut c1_x = [pallas::Base::zero(); 16];
let mut c2_x = [pallas::Base::zero(); 16];
let mut c1_y = [pallas::Base::zero(); 16];
let mut c2_y = [pallas::Base::zero(); 16];
let randomness: [pallas::Base; 16] = core::array::from_fn(|i| {
pallas::Base::from((i as u64 + 1) * 101)
});
let share_blinds: [pallas::Base; 16] = core::array::from_fn(|i| {
pallas::Base::from(1001u64 + i as u64)
});
for i in 0..16 {
let (cx1, cx2, cy1, cy2) = elgamal_encrypt(
pallas::Base::from(shares[i]),
randomness[i],
ea_pk,
);
c1_x[i] = cx1;
c2_x[i] = cx2;
c1_y[i] = cy1;
c2_y[i] = cy2;
}
let comms: [pallas::Base; 16] = core::array::from_fn(|i| {
share_commitment(share_blinds[i], c1_x[i], c2_x[i], c1_y[i], c2_y[i])
});
let hash = compute_shares_hash(share_blinds, c1_x, c2_x, c1_y, c2_y);
(c1_x, c2_x, c1_y, c2_y, share_blinds, comms, hash)
}
fn make_test_data(
share_idx: u32,
) -> (Circuit, Instance) {
let proposal_id = pallas::Base::from(3u64);
let vote_decision = pallas::Base::from(1u64);
let voting_round_id = pallas::Base::from(999u64);
let (_ea_sk, ea_pk_point, _ea_pk_affine) = generate_ea_keypair();
let shares_u64: [u64; 16] = [625; 16];
let (enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y, share_blinds, share_comms, shares_hash_val) =
encrypt_shares(shares_u64, ea_pk_point);
let vote_commitment =
compute_vote_commitment_hash(voting_round_id, shares_hash_val, proposal_id, vote_decision);
let (auth_path, position, vote_comm_tree_root) =
build_single_leaf_merkle_path(vote_commitment);
let share_index_fp = pallas::Base::from(share_idx as u64);
let share_nullifier = share_nullifier_hash(
vote_commitment,
share_index_fp,
share_blinds[share_idx as usize],
);
let circuit = Circuit {
vote_comm_tree_path: Value::known(auth_path),
vote_comm_tree_position: Value::known(position),
share_comms: share_comms.map(Value::known),
primary_blind: Value::known(share_blinds[share_idx as usize]),
share_index: Value::known(share_index_fp),
vote_commitment: Value::known(vote_commitment),
};
let instance = Instance::from_parts(
share_nullifier,
enc_c1_x[share_idx as usize],
enc_c2_x[share_idx as usize],
proposal_id,
vote_decision,
vote_comm_tree_root,
voting_round_id,
enc_c1_y[share_idx as usize],
enc_c2_y[share_idx as usize],
);
(circuit, instance)
}
fn build_single_leaf_merkle_path(
leaf: pallas::Base,
) -> ([pallas::Base; VOTE_COMM_TREE_DEPTH], u32, pallas::Base) {
let mut empty_roots = [pallas::Base::zero(); VOTE_COMM_TREE_DEPTH];
empty_roots[0] = poseidon_hash_2(pallas::Base::zero(), pallas::Base::zero());
for i in 1..VOTE_COMM_TREE_DEPTH {
empty_roots[i] = poseidon_hash_2(empty_roots[i - 1], empty_roots[i - 1]);
}
let auth_path = empty_roots;
let mut current = leaf;
for i in 0..VOTE_COMM_TREE_DEPTH {
current = poseidon_hash_2(current, auth_path[i]);
}
(auth_path, 0, current)
}
#[test]
fn test_share_reveal_valid() {
let (circuit, instance) = make_test_data(0);
let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
assert_eq!(prover.verify(), Ok(()));
}
#[test]
fn test_share_reveal_valid_index_1() {
let (circuit, instance) = make_test_data(1);
let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
assert_eq!(prover.verify(), Ok(()));
}
#[test]
fn test_share_reveal_valid_index_2() {
let (circuit, instance) = make_test_data(2);
let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
assert_eq!(prover.verify(), Ok(()));
}
#[test]
fn test_share_reveal_valid_index_3() {
let (circuit, instance) = make_test_data(3);
let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
assert_eq!(prover.verify(), Ok(()));
}
#[test]
fn test_share_reveal_valid_index_15() {
let (circuit, instance) = make_test_data(15);
let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
assert_eq!(prover.verify(), Ok(()));
}
#[test]
fn test_share_reveal_wrong_merkle_root() {
let (circuit, mut instance) = make_test_data(0);
instance.vote_comm_tree_root = pallas::Base::from(12345u64);
let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn test_share_reveal_wrong_nullifier() {
let (circuit, mut instance) = make_test_data(0);
instance.share_nullifier = pallas::Base::from(99999u64);
let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn test_share_reveal_wrong_share_index() {
let (circuit, instance) = make_test_data(0);
let bad_instance = Instance::from_parts(
instance.share_nullifier,
pallas::Base::from(999u64),
pallas::Base::from(888u64),
instance.proposal_id,
instance.vote_decision,
instance.vote_comm_tree_root,
instance.voting_round_id,
instance.enc_share_c1_y,
instance.enc_share_c2_y,
);
let prover = MockProver::run(K, &circuit, vec![bad_instance.to_halo2_instance()]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn test_share_reveal_wrong_vote_decision() {
let (circuit, mut instance) = make_test_data(0);
instance.vote_decision = pallas::Base::from(42u64);
let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn test_share_reveal_wrong_voting_round_id() {
let (circuit, mut instance) = make_test_data(0);
instance.voting_round_id = pallas::Base::from(12345u64);
let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn test_share_reveal_sign_flip_detected() {
let (circuit, mut instance) = make_test_data(0);
instance.enc_share_c1_y = -instance.enc_share_c1_y;
let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn test_share_reveal_tampered_share_comms_fails() {
let (mut circuit, instance) = make_test_data(0);
circuit.share_comms[5] = Value::known(pallas::Base::from(99999u64));
let prover = MockProver::run(K, &circuit, vec![instance.to_halo2_instance()]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn test_share_reveal_domain_tag_matches_server() {
use ff::PrimeField;
let mut bytes = [0u8; 32];
let tag = b"share spend";
bytes[..tag.len()].copy_from_slice(tag);
let server_tag = pallas::Base::from_repr(bytes).unwrap();
assert_eq!(domain_tag_share_spend(), server_tag);
}
#[test]
#[ignore]
fn row_budget() {
use std::println;
use halo2_proofs::dev::CircuitCost;
use pasta_curves::vesta;
let (circuit, _) = make_test_data(0);
let cost = CircuitCost::<vesta::Point, _>::measure(K, &circuit);
let debug = alloc::format!("{cost:?}");
let extract = |field: &str| -> usize {
let prefix = alloc::format!("{field}: ");
debug.split(&prefix)
.nth(1)
.and_then(|s| s.split([',', ' ', '}']).next())
.and_then(|n| n.parse().ok())
.unwrap_or(0)
};
let max_rows = extract("max_rows");
let max_advice_rows = extract("max_advice_rows");
let max_fixed_rows = extract("max_fixed_rows");
let total_available = 1usize << K;
println!("=== share-reveal circuit row budget (K={K}) ===");
println!(" max_rows (floor-planner high-water mark): {max_rows}");
println!(" max_advice_rows: {max_advice_rows}");
println!(" max_fixed_rows: {max_fixed_rows}");
println!(" 2^K (total available rows): {total_available}");
println!(" headroom: {}", total_available.saturating_sub(max_rows));
println!(" utilisation: {:.1}%",
100.0 * max_rows as f64 / total_available as f64);
println!();
println!(" Full debug: {debug}");
let cost_default = CircuitCost::<vesta::Point, _>::measure(K, &Circuit::default());
let debug_default = alloc::format!("{cost_default:?}");
let max_rows_default = debug_default
.split("max_rows: ").nth(1)
.and_then(|s| s.split([',', ' ', '}']).next())
.and_then(|n| n.parse::<usize>().ok())
.unwrap_or(0);
if max_rows_default == max_rows {
println!(" Witness-independence: PASS \
(Circuit::default() max_rows={max_rows_default} == filled max_rows={max_rows})");
} else {
println!(" Witness-independence: FAIL \
(Circuit::default() max_rows={max_rows_default} != filled max_rows={max_rows}) \
— row count depends on witness values!");
}
println!(" VOTE_COMM_TREE_DEPTH (circuit constant): {VOTE_COMM_TREE_DEPTH}");
for probe_k in 11u32..=K {
let (c, inst) = make_test_data(0);
match MockProver::run(probe_k, &c, vec![inst.to_halo2_instance()]) {
Err(_) => {
println!(" K={probe_k}: not enough rows (synthesizer rejected)");
continue;
}
Ok(p) => match p.verify() {
Ok(()) => {
println!(" Minimum viable K: {probe_k} (2^{probe_k} = {} rows, {:.1}% headroom)",
1usize << probe_k,
100.0 * (1.0 - max_rows as f64 / (1usize << probe_k) as f64));
break;
}
Err(_) => println!(" K={probe_k}: too small"),
},
}
}
}
}