Halo 2 for Dummies
Halo 2 is succint non-interactive zero-knowledge argument of knowledge (zkSNARK) library for developing applications with an associated zkSNARK in order to prove their honesty in computing the programs. In this chapter, I present a simple implementation of a program, under the form of a -variable polynomial, by using Halo 2.
Author(s): khaihanhtang
PLONKish Arithemetization
We recommend readers who are not familiar with PlonK's arithmetization to read the article PlonK's Arithmetization. In this chapter, we further discuss a more customized version of PlonK's arithmetization, namely, PLONKish arithmetization. Customization aims to handle more general gates with more complicated structures rather than employing only multiplication, addition, multiplication with constants and addition with constants gates.
PLONKish arithmetization can be pictured as a table of the following types of columns:
- Constant columns for putting constants,
- Instant columns for putting public inputs,
- Advice columns for putting private inputs, literally known as witnesses,
- Selector columns for putting selectors.
For simplicity, in this section, we present a transformation from a program, represented by an arithmetic circuit, to the form of PLONKish arithmetization. This transformation can be showed by an example that proves the knowledge of a -variable polynomial specified in A Simple Arithmetic Circuit. Then, we explain the method for transforming this polynomial to the form of PLONKish arithmetization in Transforming to PLONKish Arithmetization and programming in A Simple Halo 2 Program.
A Simple Arithmetic Circuit
Let be some finite field. We would like to show in this section the transformation from the program computing polynomial where . With inputs , the arithmetic circuit for this polynomial is equivalently represented by topologically following the sequence of computations below.
- Compute .
- Compute .
- Compute .
- Compute .
- Compute .
- Compute .
Transforming to PLONKish Arithmetization
To setup the above sequence of computations, in A Simple Arithmetic Circuit, into PLONKish arithmetization, we specify a table to contain all possible variables appeared during the computing the arithmetic circuit for . Then, for each row, we setup a the set gates, or equivalently gate constraints, that applies to the row.
Specifying columns
We define the following tuple of columns
where
- are columns containing private inputs, i.e., values belonging to these columns are hidden to any verifier,
- is the column containing public constants appearing during the computation,
- contain public selectors corresponding to addition, multiplication, addition with constant, multiplication with constant, respectively, gates.
Transforming to Constraints and Meaning of Columns
We now explain the intuition to the above setting of columns. To do this, we need to transform the sequence of computations in A Simple Arithmetic Circuit into parts, namely, gate constraints (or gate identities) and wire constraints (or wire identities). In particular, we transform
to gate constraints
and wire constraints
We note that are not set. To deal with these values, we simple set them to be equal to any random vales, since they do not affect the constraints defined above.
Notice that each equation in gate constrains receives either secret inputs or secret input and constant and returns secret output. Therefore, we use columns, namely, and , to contain the secret inputs and column, namely, , to contain the secret output. Moreover, we also use the column to contain possible public constants.
The remaining columns, namely, , are employed to contain the selectors indicating the required constraints for each row, which corresponds to a constraint in the set of gate constraints specified above. We now clarify the employment of selectors to guarantee the correctness of gate constraints.
Specifying Table Values and Gate Constraints
For each row of the table, we denote by the tuple
corresponding to the tuple of columns
devised above.
For each row , we set the following constraints
Example. Assume that the -th row corresponds to a multiplication gate. Hence, in this case, we set . We can see that only while remaining selectors are set to .
Therefore, whatever the values are set, the results always hold with respect to the gates and .
However, since , we can see that the gate must guarantee .
A Simple Halo 2 Program
Based on the specifications in Transforming to PLONKish Arithmetization, we now show an implementation in Rust for proving knowledge of input of mentioned in A Simple Arithmetic Circuit. The implementation can be found in Example of Halo2-PSE.
The following are a recommended for setting up a Rust implementation for Halo 2.
In Cargo.toml, specify the dependency
[dependencies]
halo2_proofs = { git = "https://github.com/privacy-scaling-explorations/halo2.git" }
In main.rs, we implement step-by-step as follows:
- Specify columns by putting all possible columns into a custom
struct MyConfig
.
#[derive(Clone, Debug)]
struct MyConfig {
advice: [Column<Advice>; 3],
instance: Column<Instance>,
constant: Column<Fixed>,
// selectors
s_add: Selector,
s_mul: Selector,
s_add_c: Selector,
s_mul_c: Selector,
}
- Define gates based on elements of the above defined
struct
.
impl<Field: FieldExt> FChip<Field> {
fn configure(
meta: &mut ConstraintSystem<Field>,
advice: [Column<Advice>; 3],
instance: Column<Instance>,
constant: Column<Fixed>,
) -> <Self as Chip<Field>>::Config {
// specify columns used for proving copy constraints
meta.enable_equality(instance);
meta.enable_constant(constant);
for column in &advice {
meta.enable_equality(*column);
}
// extract columns with respect to selectors
let s_add = meta.selector();
let s_mul = meta.selector();
let s_add_c = meta.selector();
let s_mul_c = meta.selector();
// define addition gate
meta.create_gate("add", |meta| {
let s_add = meta.query_selector(s_add);
let lhs = meta.query_advice(advice[0], Rotation::cur());
let rhs = meta.query_advice(advice[1], Rotation::cur());
let out = meta.query_advice(advice[2], Rotation::cur());
vec![s_add * (lhs + rhs - out)]
});
// define multiplication gate
meta.create_gate("mul", |meta| {
let s_mul = meta.query_selector(s_mul);
let lhs = meta.query_advice(advice[0], Rotation::cur());
let rhs = meta.query_advice(advice[1], Rotation::cur());
let out = meta.query_advice(advice[2], Rotation::cur());
vec![s_mul * (lhs * rhs - out)]
});
// define addition with constant gate
meta.create_gate("add with constant", |meta| {
let s_add_c = meta.query_selector(s_add_c);
let lhs = meta.query_advice(advice[0], Rotation::cur());
let fixed = meta.query_fixed(constant, Rotation::cur());
let out = meta.query_advice(advice[2], Rotation::cur());
vec![s_add_c * (lhs + fixed - out)]
});
// define multiplication with constant gate
meta.create_gate("mul with constant", |meta| {
let s_mul_c = meta.query_selector(s_mul_c);
let lhs = meta.query_advice(advice[0], Rotation::cur());
let fixed = meta.query_fixed(constant, Rotation::cur());
let out = meta.query_advice(advice[2], Rotation::cur());
vec![s_mul_c * (lhs * fixed - out)]
});
MyConfig {
advice,
instance,
constant,
s_add,
s_mul,
s_add_c,
s_mul_c,
}
}
}
- Put values to the table and define wire constraints.
impl<Field: FieldExt> Circuit<Field> for MyCircuit<Field> {
type Config = MyConfig;
type FloorPlanner = SimpleFloorPlanner;
fn without_witnesses(&self) -> Self {
Self::default()
}
fn configure(meta: &mut ConstraintSystem<Field>) -> Self::Config {
let advice = [meta.advice_column(), meta.advice_column(), meta.advice_column()];
let instance = meta.instance_column();
let constant = meta.fixed_column();
FChip::configure(meta, advice, instance, constant)
}
fn synthesize(
&self, config: Self::Config, mut layouter: impl Layouter<Field>
) -> Result<(), Error> {
// handling multiplication region
let t1 = self.u * self.u;
let t2 = self.u * self.v;
let t3 = t2 * Value::known(Field::from(3));
// define multiplication region
let (
(x_a1, x_b1, x_c1),
(x_a2, x_b2, x_c2),
(x_a3, x_c3)
) = layouter.assign_region(
|| "multiplication region",
|mut region| {
// first row
config.s_mul.enable(&mut region, 0)?;
let x_a1 = region.assign_advice(|| "x_a1",
config.advice[0].clone(), 0, || self.u)?;
let x_b1 = region.assign_advice(|| "x_b1",
config.advice[1].clone(), 0, || self.u)?;
let x_c1 = region.assign_advice(|| "x_c1",
config.advice[2].clone(), 0, || t1)?;
// second row
config.s_mul.enable(&mut region, 1)?;
let x_a2 = region.assign_advice(|| "x_a2",
config.advice[0].clone(), 1, || self.u)?;
let x_b2 = region.assign_advice(|| "x_b2",
config.advice[1].clone(), 1, || self.v)?;
let x_c2 = region.assign_advice(|| "x_c2",
config.advice[2].clone(), 1, || t2)?;
// third row
config.s_mul_c.enable(&mut region, 2)?;
let x_a3 = region.assign_advice(|| "x_a3",
config.advice[0].clone(), 2, || t2)?;
region.assign_fixed(|| "constant 3",
config.constant.clone(), 2, || Value::known(Field::from(3)))?;
let x_c3 = region.assign_advice(|| "x_c3",
config.advice[2].clone(), 2, || t3)?;
Ok((
(x_a1.cell(), x_b1.cell(), x_c1.cell()),
(x_a2.cell(), x_b2.cell(), x_c2.cell()),
(x_a3.cell(), x_c3.cell())
))
}
)?;
let t4 = t1 + t3;
let t5 = t4 + self.v;
let t6 = t5 + Value::known(Field::from(5));
// define addition region
let (
(x_a4, x_b4, x_c4),
(x_a5, x_b5, x_c5),
(x_a6, x_c6)
) = layouter.assign_region(
|| "addition region",
|mut region| {
// first row
config.s_add.enable(&mut region, 0)?;
let x_a4 = region.assign_advice(|| "x_a4",
config.advice[0].clone(), 0, || t1)?;
let x_b4 = region.assign_advice(|| "x_b4",
config.advice[1].clone(), 0, || t3)?;
let x_c4 = region.assign_advice(|| "x_c4",
config.advice[2].clone(), 0, || t4)?;
// second row
config.s_add.enable(&mut region, 1)?;
let x_a5 = region.assign_advice(|| "x_a5",
config.advice[0].clone(), 1, || t4)?;
let x_b5 = region.assign_advice(|| "x_b5",
config.advice[1].clone(), 1, || self.v)?;
let x_c5 = region.assign_advice(|| "x_c5",
config.advice[2].clone(), 1, || t5)?;
// third row
config.s_add_c.enable(&mut region, 2)?;
let x_a6 = region.assign_advice(|| "x_a6",
config.advice[0].clone(), 2, || t5)?;
region.assign_fixed(|| "constant 5",
config.constant.clone(), 2, || Value::known(Field::from(5)))?;
let x_c6 = region.assign_advice(|| "x_c6",
config.advice[2].clone(), 2, || t6)?;
Ok((
(x_a4.cell(), x_b4.cell(), x_c4.cell()),
(x_a5.cell(), x_b5.cell(), x_c5.cell()),
(x_a6.cell(), x_c6.cell())
))
}
)?;
// t6 is result, assign instance
layouter.constrain_instance(x_c6, config.instance, 0)?;
// enforce copy constraints
layouter.assign_region(|| "equality",
|mut region| {
region.constrain_equal(x_a1, x_a2)?; // namely, x_a1 = x_a2
region.constrain_equal(x_a2, x_b1)?; // namely, x_a2 = x_b1
region.constrain_equal(x_b2, x_b5)?; // namely, x_b2 = x_b5
region.constrain_equal(x_a4, x_c1)?; // namely, x_a4 = x_c1
region.constrain_equal(x_a3, x_c2)?; // namely, x_a3 = x_c2
region.constrain_equal(x_b4, x_c3)?; // namely, x_b4 = x_c3
region.constrain_equal(x_a5, x_c4)?; // namely, x_a5 = x_c4
region.constrain_equal(x_a6, x_c5)?; // namely, x_a6 = x_c5
Ok(())
}
)?;
Ok(())
}
}