# Halo2 - Circuit Implementation ## Halo2 Libraries The [original Halo2](https://github.com/zcash/halo2) by the Zcash foundation: it does not support KZG. The Privacy Scaling Explorations ([PSE](https://github.com/privacy-scaling-explorations/halo2)) fork which adds KZG support. ## Halo2 Books [Zcash/halo2 books](https://zcash.github.io/halo2/) [zksecurity/halo2 book](https://halo2.zksecurity.xyz/) ## Create a Circuit [An example code](https://zcash.github.io/halo2/user/simple-example.html) ### `configure` function The `configure` function sets up the constraints, enabling the use of custom gates (e.g. multiplication and addition gates) within the circuit: 1. Declaring the columns (advice, instance, fixed) and selectors required for the circuit. 2. Defining gates (constraints). #### Step 1 in `configure`: Declaring the columns - `Advice` columns: Used for the prover to assign witness values. - `Instance` column: Used for public inputs or outputs. - `Fixed` column: Stores constant values. - `Selector` columns: special Fixed columns (fixed columns with a fixed value of 1 or 0) that can be used to turn gates on and off (enable or disable constraints inside the gates). In the `synthesize` function we will program the selector on or off. - `meta: &mut ConstraintSystem<F>`: Represents the constraint system for the ZKP circuit. It's where you define and manage constraints. -- `meta.enable_equality(column)`: Enable the ability to **enforce equality** over cells in this column. This function is included in `meta.enable_constant`. -- `meta.advice_column()`: Allocate a new advice column -- `meta.instance_column()`: Allocate a new instance column -- `meta.fixed_column()`: Allocate a new fixed column -- `meta.selector()`: Allocate a new (simple) selector. **An example** ```rust fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config { let advice = meta.advice_column(); meta.enable_equality(advice); let q_enable = meta.selector(); ... } ``` #### Step 2 in `configure`: Defining gates (constraints) - `meta: &mut ConstraintSystem<F>`: Represents the constraint system for the ZKP circuit. It's where you define and manage constraints. -- `meta.create_gate` Define a gate -- `meta.query_advice`: Query an advice column at a relative position -- `meta.query_selector`: Query a selector at the current position. ```rust fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config { ... // define a new gate: // next = curr + 1 if q_enable is 1 meta.create_gate("step", |meta| { let curr = meta.query_advice(advice, Rotation::cur()); let next = meta.query_advice(advice, Rotation::next()); let q_enable = meta.query_selector(q_enable); vec![q_enable * (curr - next + Expression::Constant(F::ONE))] }); } ``` - `curr`: the expression value in the current row of the `advice` column. - `next`: the expression value in the next row of the `advice` column. - `Expression::Constant`: a constant expression value. The constraint is `q_enable * (curr - next + 1) = 0` ### `synthesize` function The `synthesize` function 1. assigns values, 2. performs computations by combining the methods provided by chips (modular arithmetic units), and 3. exposes the outputs to compare with the public inputs for verification. - `layouter`: The circuit layouter for arranging rows and assigning values in the circuit. - `region`: A region is a contiguous block of rows in the circuit. We can -- create a region using `layouter.assign_region` -- assign a (advice) cell using `region.assign_advice` - enable the selector at offset i using `config.q_enable.enable(&mut region, i)?;` - Equality Constraints: `region.constrain_equal` and `copy_advice` can be used to enforce equality between cells. :::info Regions ensure local isolation, create distinct "logical" units that we can compose. Not only synthesize, but also chip functions (e.g. `mul` and `add` functions) that allocates new separate regions for gates (e.g., multiplication and addition gates, respectively) that utilize the region. ::: :::info A common optimization in PlonKish (e.g. Halo2) is to implement: 1. Arithmetic Gates (in `configure`): Include gates for operations like multiplication, addition, etc. 2. Arithmetic Chip: Implements methods for operations like multiplication, addition, etc., tailored for modular arithmetic, and these methods can be invoked within the `synthesize` function. 3. Synthesize: Can utilize the methods provided by the Arithmetic Chip to assign cells and enforce constraints within the circuit. ::: **Examples** ```rust fn synthesize( &self, config: Self::Config, // mut layouter: impl Layouter<F>, ) -> Result<(), Error> { layouter.assign_region( || "steps", |mut region| { // apply the "step" gate STEPS = 5 times for i in 0..STEPS { // assign the witness value to the advice column region.assign_advice( || "assign advice", config.advice, i, || self.values.as_ref().map(|values| values[i]), )?; // turn on the gate config.q_enable.enable(&mut region, i)?; } // assign the final "next" value region.assign_advice( || "assign advice", config.advice, STEPS, || self.values.as_ref().map(|values| values[STEPS]), )?; Ok(()) }, )?; Ok(()) } ``` ```rust fn mul( &self, mut layouter: impl Layouter<F>, a: Self::Num, b: Self::Num, ) -> Result<Self::Num, Error> { let config = self.config(); layouter.assign_region( || "mul", |mut region: Region<'_, F>| { // We only want to use a single multiplication gate in this region, // so we enable it at region offset 0; this means it will constrain // cells at offsets 0 and 1. config.s_mul.enable(&mut region, 0)?; // The inputs we've been given could be located anywhere in the circuit, // but we can only rely on relative offsets inside this region. So we // assign new cells inside the region and constrain them to have the // same values as the inputs. a.0.copy_advice(|| "lhs", &mut region, config.advice[0], 0)?; b.0.copy_advice(|| "rhs", &mut region, config.advice[1], 0)?; // Now we can assign the multiplication result, which is to be assigned // into the output position. let value = a.0.value().copied() * b.0.value(); // Finally, we do the assignment to the output, returning a // variable to be used in another part of the circuit. region .assign_advice(|| "lhs * rhs", config.advice[0], 1, || value) .map(Number) }, ) } ``` ### Lookup Lookup is useful for range check and bitwise operations. - A lookup table is a pre-computed table of values. - A lookup constraint ensures that the values (or tuples of values) computed in a circuit can be found in a pre-computed lookup table, Suppose the lookup table is: | number | alphabet | operation | | -------- | -------- | -------- | | 0 | a | + | | 1 | b | - | | 2 | c | * | | 3 | d | / | The lookup constraint can verify if the following tuples belong to the table above: - (0,a,+), YES! - c, YES! - (0,d), NO! ##### Lookup Implementation Steps **1. Load the table in `synthesize`** - Create a table using `layouter.assign_table` - Assigns a fixed value to a table cell using `table.assign_cell` This following code ([source](https://github.com/zcash/halo2/blob/main/halo2_gadgets/src/utilities/lookup_range_check.rs)) shows how to load a lookup table with a column indexed by `table_idx`. The table contains K-bit values, enabling it to verify whether a given value is a valid K-bit number. ```rust fn load(&self, layouter: &mut impl Layouter<F>) -> Result<(), Error> { layouter.assign_table( || "table_idx", |mut table| { // We generate the row values lazily (we only need them during keygen). for index in 0..(1 << K) { table.assign_cell( || "table_idx", self.table_idx, index, || Value::known(F::from(index as u64)), )?; } Ok(()) }, ) } ``` **2. Lookup Gate (Constraint) in `configure`** - `meta.lookup` returns a map between input expressions and the table columns they need to match. - Constraint ``` vec![ (q_lookup * input_expression_1, table_column_1), (q_lookup * input_expression_2, table_column_2), ... ] ``` if `q_lookup = 1`, then the lookup checks: - `(input_expression_1, input_expression_2, ...)` in `(table_column_1, table_column_2, ...)` The following code is sourced from [here](https://github.com/zcash/halo2/blob/main/halo2_gadgets/src/utilities/lookup_range_check.rs). ```rust meta.lookup(|meta| { let q_lookup = meta.query_selector(config.q_lookup); let q_running = meta.query_selector(config.q_running); let z_cur = meta.query_advice(config.running_sum, Rotation::cur()); // In the case of a running sum decomposition, we recover the word from // the difference of the running sums: // z_i = 2^{K}⋅z_{i + 1} + a_i // => a_i = z_i - 2^{K}⋅z_{i + 1} let running_sum_lookup = { let running_sum_word = { let z_next = meta.query_advice(config.running_sum, Rotation::next()); z_cur.clone() - z_next * F::from(1 << K) }; q_running.clone() * running_sum_word }; // In the short range check, the word is directly witnessed. let short_lookup = { let short_word = z_cur; let q_short = Expression::Constant(F::ONE) - q_running; q_short * short_word }; // Combine the running sum and short lookups: vec![( q_lookup * (running_sum_lookup + short_lookup), config.table_idx, )] }); ``` ## Testing the circuit Use `halo2_proofs::dev::MockProver`, see [here](https://zcash.github.io/halo2/user/simple-example.html#testing-the-circuit). :::warning The public inputs taken by the verifier is a `Vec<Vec<F>>`. This is to support multiple Instance columns: one vector of values per column. :::