halo2ccs/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
#![allow(non_snake_case)]
use folding_schemes::arith::ccs::CCS;
use folding_schemes::utils::vec::is_zero_vec;
use halo2_proofs::circuit::Value;
use halo2_proofs::dump::dump_gates;
use halo2_proofs::dump::dump_lookups;
use halo2_proofs::dump::AssignmentDumper;
use halo2_proofs::plonk;
use halo2_proofs::plonk::*;
use lookup::check_lookup_satisfiability;
use plonkish_table::PlonkishTable;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt::Display;
mod query;
use query::*;
mod cell_mapping;
mod monomial;
use cell_mapping::*;
mod ccs;
mod lookup;
use ccs::*;
mod plonkish_table;
pub use query::AbsoluteCellPosition;
pub use query::VirtualColumnType;
// Basic flow of the Plonkish -> CCS+ conversion process in this code is
// 1. Populate HashMap<AbsoluteCellPosition, CCSValue> with the assignments extracted from a Halo2 circuit
// 2. Modify the HashMap according to copy constraints
// 3. Populate the Z vector according to HashMap
// 4. Generate M_j from custom gates extracted from a Halo2 circuit
// Notes on lookup:
// Halo2 allows us to constrain an Expression evaluated at each row to be in a lookup table.
// Halo2 calls such Expression a lookup input.
// Halo2 stores these evaluation result sepalately from the assignments in the table.
// But in the case of CCS+, these evaluation results has to be in Z, and we need to commit to it together with other columns.
// Thus in this code I treat a lookup input evaluated at each row as if it's another column.
// I later constrain these lookup input evaluation result columns according to the lookup input Expression, just like we constrain advice columns according to custom gate Expression.
#[derive(Debug)]
pub enum Error {
Halo2(plonk::Error),
NoWitness,
}
impl From<plonk::Error> for Error {
fn from(src: plonk::Error) -> Self {
Error::Halo2(src)
}
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Halo2(err) => err.fmt(f),
Self::NoWitness => {
f.write_str("We're about to generate a CCS instance with n<2. Can't continue.")
}
}
}
}
/// Converts a Halo2 circuit into a sonobe CCS instance.
///
/// - `k` log_2(the height of the Plonkish table)
/// - `circuit` A Halo2 circuit you wish to convert
/// - `instance` Assignments to the instance columns. The length of this slice must equal the number of the instance columns in `circuit`.
///
/// Returns
/// - A CCS instance
/// - The witness vector Z
/// - Lookup constraints
/// - A map from a cell position in the original Plonkish table to the position in `Z`.
///
/// lookup constraints takes a form of Vec<(L, T)> where for every o ∈ L, z\[o\] must be in T.
pub fn convert_halo2_circuit<
HALO2: ff::PrimeField<Repr = [u8; 32]>,
C: Circuit<HALO2>,
ARKWORKS: ark_ff::PrimeField,
>(
k: u32,
circuit: &C,
instance: &[&[HALO2]],
) -> Result<
(
CCS<ARKWORKS>,
Vec<ARKWORKS>,
Vec<(HashSet<usize>, HashSet<ARKWORKS>)>,
HashMap<AbsoluteCellPosition, usize>,
),
Error,
> {
let mut meta = ConstraintSystem::<HALO2>::default();
// This line updates meta.num_instance_columns
let config = C::configure(&mut meta);
// This line reads meta.num_instance_columns
let mut cell_dumper: AssignmentDumper<HALO2> = AssignmentDumper::new(k, &meta);
// instance_option has the same shape as cell_dumper.instance
let mut instance_option: Vec<Vec<Option<HALO2>>> = cell_dumper
.instance
.iter()
.map(|column| vec![None; column.len()])
.collect();
for (column_index, column) in instance.iter().enumerate() {
for (row_index, cell) in column.iter().enumerate() {
// Assign an instance cell
cell_dumper.instance[column_index][row_index] = Value::known(*cell);
// Value does not implement unwrap so I need to keep track of the assignments in Option...
instance_option[column_index][row_index] = Some(*cell);
}
}
// This line updates cell_dumper
C::FloorPlanner::synthesize(&mut cell_dumper, circuit, config, meta.constants.clone())?;
let mut plonkish_table = PlonkishTable::new(k as usize, cell_dumper.usable_rows.len());
plonkish_table.fill_from_halo2(
&cell_dumper.selectors,
&cell_dumper.fixed,
&cell_dumper.advice,
&instance_option,
);
let lookups = dump_lookups::<HALO2, C>()?;
let lookup_inputs: Vec<Expression<HALO2>> =
lookups.iter().map(|(input, _)| input).cloned().collect();
plonkish_table.evaluate_lookup_inputs(&lookup_inputs)?;
let mut cell_mapping = generate_cell_mapping(
&plonkish_table,
&cell_dumper.copy_constraints,
&lookup_inputs,
)?;
let custom_gates = dump_gates::<HALO2, C>()?;
let ccs_instance: CCS<ARKWORKS> = generate_ccs_instance(
plonkish_table.k,
&custom_gates,
&mut cell_mapping,
&lookup_inputs,
)?;
let z: Vec<ARKWORKS> = generate_z(&plonkish_table, &cell_mapping)?;
// Generate fixed lookup tables.
// In the original CCS paper, there was only one lookup table T.
// However, this implementation supports multiple lookup tables.
let mut tables: Vec<HashSet<ARKWORKS>> = Vec::new();
for (_, table_expr) in lookups {
if let Expression::Fixed(query) = table_expr {
// zcash/halo2 only allows table_expr to be Expression::Fixed
// In Halo2, you use assign_table to assign a value in a lookup table.
// When you call assign_table only on some rows, and leave rest of the column unassigned, assign_table will automatically fill the rest of the column with duplicates of already assigned values.
// This means that, we won't encounter unassigned None cell here, except the case where the user didn't call assign_table, not even once.
tables.push(
plonkish_table.fixed[query.column_index]
.iter()
.take(plonkish_table.usable_rows)
.copied()
.collect(),
);
} else {
// pse/halo2 lets table_expr to be something other than FixedQuery, but we're working on zcash/halo2.
panic!("zcash/halo2 supports only fixed lookup tables.")
}
}
// Generate multiple Ls.
// In the original CCS paper, there was only one L.
// But this implementation supports multiple lookup tables, so there should be one L for each T.
let LandT = tables
.into_iter()
.enumerate()
.map(|(lookup_index, T)| {
// cell_mapping keeps track of z_index where evaluations of lookup inputs lies.
// So we read it here.
// Check generate_cell_mapping's implementation for more.
let L = cell_mapping
.iter()
.filter_map(|(position, value)| {
if position.column_type == VirtualColumnType::LookupInput
&& position.column_index == lookup_index
&& position.row_index < plonkish_table.usable_rows
{
if let CCSValue::InsideZ(z_index) = value {
// This z_index is the o in the paper.
Some(z_index)
} else {
None
}
} else {
None
}
})
.copied()
.collect();
(L, T)
})
.collect();
println!(
"The number of advice/instance cells in the original Plonkish table: {}",
(meta.num_advice_columns + meta.num_instance_columns) * (1 << k)
);
println!(
"The height of the witness vector Z in the transpiled CCS circuit: {}",
ccs_instance.n
);
let z_index_map: HashMap<AbsoluteCellPosition, usize> = cell_mapping
.into_iter()
.filter_map(|(key, value)| {
if let CCSValue::InsideZ(z_index) = value {
// Expose the mapping between advice/instance cell position and z_index,
// Users might want to add constraints to those witnesses later in Sonobe.
Some((key, z_index))
} else {
// There's no point in exposing fixed cell assignments.
None
}
})
.collect();
Ok((ccs_instance, z, LandT, z_index_map))
}
/// Takes the output of `convert_halo2_circuit`, and check if the CCS+ instance is satisfied.
pub fn is_ccs_plus_satisfied<F: ark_ff::PrimeField>(
ccs: CCS<F>,
z: &[F],
LandT: Vec<(HashSet<usize>, HashSet<F>)>,
) -> bool {
if let Ok(ccs_lhs) = ccs.eval_at_z(z) {
if !is_zero_vec(&ccs_lhs) {
return false;
}
} else {
return false;
};
for (L, T) in LandT.iter() {
let subset: HashSet<F> = L.iter().map(|o| z[*o]).collect();
if !check_lookup_satisfiability(&subset, T) {
return false;
}
}
return true;
}