Custom Gates
Consider the circuit below:
flowchart LR x1((x1)) --> +1[+ gate:1] x2((x2)) --> +1 x2 --> +2[+ gate:2] w1((w1)) --> +2 +1 --> x[x gate:3] +2 --> x x --> r(( ))
For a gate number , denote the left input, right input, and the output as respectively. We would like the circuit to satisfy the following:
You can write down these in a table:
(left input) | (right input) | (output) | (selector) | |
---|---|---|---|---|
Gate 1 | 1 | |||
Gate 2 | 1 | |||
Gate 3 | 0 |
Notice the selector input , where it is denoted as 1 for addition gates, and 0 for multiplication gates. With this, the entire circuit can be captured by the following equation:
Theoretically, you can capture any kind of computation using this model, but still, using addition and multiplication alone is a bit constraining set of functionality. We would in particularly like to have specialized parts of a circuit that can be re-used, say for elliptic curve operations or hash functions; saving us a lot of time (and decreasing the number of rows in the table)!
Example: Using Hash Functions for Signature
Consider an example:
- Key generation:
- For the secret key, just sample where . The bar over denotes that it is a binary representation.
- which is the hash of mapped to some Field element.
- Signing a message :
- First, pick a random .
- Your signature is
So let us construct our circuit . This circuit must check the following:
- Ensure that given variables are in the defined binary set:
- Recreate the signature:
- Ensure that the given signature key matches the derived one:
- Ensure that public key is derived from the secret key:
- Ensure that the given public key matches the derived one:
Working with binary is expensive in circuits, because technically you are checking if a given linear combination of field elements times some multiple of two is equal to some other field element, e.g. .
Decomposing Circuits to Binary Sets
For the sake of this example, let us have 3-bit numbers only (rather than 256 as most people use). You might have a number where . This gives us the constraint of .
When we look at the second formulation, we realize that the same operation ( is happening a lot of times repetitively! Now let us do some renaming:
This way, notice that we are always doing the same operation on some other variable . Let us write a table for these.
(index) | (bits) | (accumulation) |
---|---|---|
0 | ||
1 | ||
2 | ||
3 |
There are some constraints to be written from this table:
- We need to make sure is a bit for . We can do that simply by .
- We also need to make sure is computed correctly. We can do that by: for .
- For , we have the constraint .
- Finally, for we have the constraint .
We will capture all these constraints with a polynomial. For that, we use something called Lagrange Interpolation. Denote as a root of unity. Let . We had 3 bits in our example, and our table has 3+1 rows, so that is why we use this degree. In this case, which is as it is a root of unity.
Construct a set of Lagrange polynomials which gives us a really nice property:
Now consider an element . We will also have a polynomial:
This is a really neat polynomial where we can select values of such as: . You can create one for with the same procedure.
Let us revisit the constraints from before:
Applied Indices | Constraint | Selector | Polynomial Constraint |
---|---|---|---|
1, 2, 3 | |||
1, 2, 3 | |||
0 | |||
0 |
Then you will make a single polynomial out of these, that the verifier can query at random points!
Lookup Tables
Consider a computation like where is the XOR operation. Calculating this every time in the proof and writing the constraints for it will be costly. Instead, you could have a large table that shows all possible operation for the XOR (for example if then the table has 4 rows, literally just the truth table for XOR operation) and simply make the argument "is there a row in this table with ? This is what is called a Lookup Argument. It is an optimization method to save from computation time, although generating the table could take time.
Think of the following problem, you have a and you would like to show that which is a set and is known publicly. You would like to prove is in this set without revealing what is! (Set Membership Proof)
Range Proof
For a private , we would like to prove . Proofs like this appear a lot of times in SNARKs.
One of the ways this is done is via binary decompositions: such that where all are secret too (otherwise you would reveal somewhat). We write this as:
There are additional constraints to ensure that is a bit, which is done by (one for each ):
Rank-1 Constraint System
Let us construct the R1CS for this set of constraints.
If you look at the calculations here for each element in the resulting vector, it captures all the four constraints! This literally captures the R1CS for the proof of . Again, note that this set is public, but is not.
Using a Table
So again, let us consider the same example: for some private , prove that . We will use a table this time:
0 | 5 |
1 | 6 |
2 | 7 |
3 | - |
4 | - |
We want to commit to a set of values in a table. We can do that by making a polynomial with the coefficients as these values. Suppose these values are . Then, construct a polynomial:
where are the Lagrange polynomials over the public set where is a root of unity. What is a root of unity? It is a value such that , also known as an -th root of unity.
The thing about Lagrange polynomial is that is the unique polynomial such that:
When you use roots of unity, these polynomials turn out to be really quick to compute thanks to Fast Fourier Transform, but that is a bit too much detail to go into here.
Now, if you were to open on , all you do is show that . For this, you do the following constraint:
for some quotient polynomial .
The rest of the video talks about HALO2, which I have not yet noted down.