☰
5 · The circuit
So we were left with the task of proving that we know the openings (secrets) of our tuple in zero-knowledge and that they are the same one used to commit to our path up to the root. So we are going to do it using "circuits" and by organizing the proof in a way that we can plug it into the Generalized Bulletproof equation. Let's start defining what is a circuit in cryptography and go through an example to better understand it.
Circuit
We could start from the really basics and talk about the AND/OR operations on bits but instead we are going to skip that and focus on just what is important to understand the circuits in FCMP++, therefore we will talk here only about arithmetic circuits which perform operations with scalar fields (instead of bits) only. Each operation is called a gate and a set of operations to perform a specific task is called a gadget. Let's see with an example how it looks like:
A first example
A circuit is just a computation broken into elementary operations(addition and multiplications) called gates. Take a small one,
We compute it in two steps, naming every intermediate value so we can build our system out of elementary operations only. For example at the intermediate value :
The addition costs almost nothing so we will not take into account the computation cost for those operations and we will consider only the multiplication costs (one gate per multiplication). So this whole computation is a single multiplication gate sitting on top of some linear combination of coefficients.
A point on a short Weierstrass curve
The previous example is not really used as a gadget in our FCMP++ proof. So let's pick one that is more useful, which is proving that a point is on an elliptic-curve (in our case Helios/Selene). First let's build it theoretically and then with real numbers on a tiny made-up curve so we can have an idea about the actual matrices the prover uses to feed the system. We want to prove that a point satisfies
with and fixed public constants (these are the constants of each elliptic-curve). Let's introduce the intermediate values computed in three steps:
Now write every gate as a left input times a right input giving the output :
| gate | |||
|---|---|---|---|
| 1 | |||
| 2 | |||
| 3 |
and the wires are tied together by one final relation
which is linear (a weighted sum of intermediate values), so it costs no multiplication gate.
That is why the on_curve gadget costs only three gates.
A concrete curve and witness
Now let's put numbers on it. Let's work over the small prime field (the real system uses a field of roughly elements but the arithmetic is the same) so every value stays below . Now fix:
The point lies on it, since . Its secret values (witness) are
The three multiplication gates look like that:
| gate | product | |||
|---|---|---|---|---|
| 1 | 3 | 3 | 9 | |
| 2 | 9 | 3 | 27 | |
| 3 | 6 | 6 | 36 |
The final matrices
In order to have the full on_curve gadget, we need to fix the linear relation
between the secret vectors (witnesses), which is done by wiring the circuit so it obeys the
equation of the short Weierstrass curve:
And those vectors could be wired as following for example:
| row | meaning | constraint |
|---|---|---|
| C1 | gate 1's inputs are one | |
| C2 | gate 2 reuses that | |
| C3 | gate 2 left gate 1 output | |
| C4 | gate 3 squares one | |
| C5 | the curve equation |
Now let's write our multiplication gates (private values) as vectors:
Those multiplication gates () with , will act on the public wired circuit (from the table above), represented as matrices below:
And now we can check if it balances:
And we end up with our point (that we knew that is on curve) respecting constraints C1 to C5 and therefore balancing the equation:
Now let's see if that works for a point off the curve: take . Gate 3 now fills , but row C5 still demands , and the proof is rejected because the equation doesn't balance.
The same example in R1CS notation
R1CS (Rank-1 Constraint System) writes the identical circuit a second way: instead of three separate wire vectors it packs every value into one witness , and each of the four relations becomes one row , where , , select the left factor, the right factor, and the output. Symbolically:
| constraint | |||
|---|---|---|---|
| (0,1,0,0,0,0) | (0,1,0,0,0,0) | (0,0,0,1,0,0) | |
| (0,0,0,1,0,0) | (0,1,0,0,0,0) | (0,0,0,0,1,0) | |
| (0,0,1,0,0,0) | (0,0,1,0,0,0) | (0,0,0,0,0,1) | |
| (1,0,0,0,0,0) | (b,a,0,0,1,0) | (0,0,0,0,0,1) |
The last row keeps the linear tie inside the same product shape by making one factor the constant wire: and . Stacking the rows and filling in , gives three matrices, columns ordered :
Multiplying by and taking the entrywise product gives the same check:
Same numbers, same circuit: R1CS uses one product per row over a stacked witness, while the form above splits the products into a single Hadamard line and the sums into the weight matrices. FCMP proves the second form. This fold only shows that the two are interchangeable.
So finally, what we did was to organize our vectors so that the linear combination of them is zero, satisfying the equation below:
This equation does two jobs. First, it keeps all the constraints inside the public matrices , so the only unknowns left are our private vectors (the multiplication gates). Second, it lets us check everything later in zero-knowledge: all the rows are folded into a single number, and that number is zero only when every constraint holds. A prover who does not know a valid witness has almost no chance of making it zero.
All gadgets
Here is a list with each gadget (circuit) used in our code and their gate counts (multiplications):
| gadget | gates | what it forces |
|---|---|---|
mul | 1 | |
equality | 0 | |
inverse | 1 | |
inequality | 1 | |
on_curve | 3 | |
incomplete_add_pub | 4 | |
member_of_list | n−1 | |
tuple_member_of_list | n−1 | |
discrete_log | 7 |
Let's talk about three notable of them:
Member of list and Tuple member of list
These are the most expensive ones and they prove that the tuple (O,I,C) or a children in the tree is a valid member of a branch. We perform that using the equation below:
This product is only zero if the value (one tuple or child in our tree) is really equal to the desired member (in our case the member is or one of the intermediate children that we committed to when we proved that we know a path to the root). Notice that we have multiplications.
Incomplete addition
Here we are talking about the addition of two Points and not scalars. To prove that we will simply use the geometric operations on elliptic-curves and the definitions of a line and our curve.
So the rule for adding points on a curve is simply geometric: a straight line meets a cubic (the short Weierstrass shape) in three places, and the group law defines those three points to sum to zero. So to add and we draw the line through them, take the third point where it hits the curve, and flip that point over the x-axis.
The line through the two points is:
To find where it meets the curve, substitute it into the curve equation. The disappears and what is left is a cubic in alone, where are the curve constants:
That cubic has exactly three roots, and we already know two of them: and , because and are on both the line and the curve. For a cubic the roots must sum to minus the coefficient of , so the third root is simply:
That third point is not yet the answer, it is where the line hit. Flipping it over the x-axis gives the sum:
Since and are the same number, we can write the whole addition constraints in three formulas:
Multiplying the divisions away turns those three statements into three rows, each with exactly one multiplication in it:
Therefore, we have so far three gates. The fourth gate is the reason for the word incomplete: the formula divides by , so it simply has no answer when the two points share an x coordinate (doubling, or adding a point to its own negative). The gadget therefore has to rule that case out explicitly, and the only way a circuit can say "non-zero" is to exhibit an inverse:
That inverse is itself a multiplication and it is one of our gadgets, the inverse, which costs one gate.
Discrete log
This is the most interesting and most tricky part of the circuits and maybe of the whole proof because it takes only seven gates instead of thousands to prove knowledge of an scalar in and is one of the reasons why the whole FCMP++ works within a reasonable amount of time and space.
The explanation deserves a dedicated page which can be found here: discrete-log gadget.
Now that we know about the circuits, let's see what is happening on each layer.
The first layer
Now the gadgets assemble. The bottom layer is the only one that touches the transaction values as it connects the four published points to a real leaf in the tree. In the first layer, we do simultaneously four proofs of knowledge of the secrets and one membership. Here is the actual scheme:
O on_curve + dlog + add 3 + 7 + 4 = 14
I on_curve + dlog + add 3 + 7 + 4 = 14
R dlog + dlog + add 7 + 7 + 4 = 18
C on_curve + dlog + add 3 + 7 + 4 = 14
leaf membership tuple_member_of_list 38 − 1 = 37
----
97
Each block recovers one hidden point from the disguise that was published on chain. The four relations the circuit proves are:
The first three all have the same shape: a point (, , ) equals its public disguise (, , ) plus a blind. The point and the blinding scalar stay hidden but the disguise is public.
is the odd one, blinds the blind. It gets no on-curve check because it is already public, and it is two discrete logs rather than one. What matters here is the in it. Which is the same scalar as in the . And that is the key-image weld: it forces one single onto both and , so a prover cannot blind the key image with one scalar and claim another.
Notice that a prover who satisfies is forced to use, in the membership check, exactly the that the disguise opened to. This is what joins all circuits together. It is not possible to make a dlog proof of one and show another in the membership proof because they are wired together. Both prover and verifier must use the same consensus circuit, otherwise the proof will not be valid.
So finally we arrive at 97 gates per input for the first layer.
Every layer above
Past the leaf, every floor is the same small routine but a bit simpler because there are no leaves or direct data from the transaction to connect to. Every floor proves knowledge of the commited points of the path (using the dlog), proves the result is on the curve, adds them, and looks the result up in the branch above by its x-coordinate only:
Selene floor dlog + on_curve + add + member + node wires 7 + 3 + 4 + (38−1) + 1 = 52 Helios floor dlog + on_curve + add + member + node wires 7 + 3 + 4 + (18−1) + 1 = 32
Written out for the node on floor , with its coordinates, its blinded commitment, and the children slots of its parent :
The asymmetry is only in the branch width, 38 children on Selene against 18 on Helios. One note: hashing the x-coordinate alone would let a point and its negation share an x, so each node is committed shifted by a fixed public point (the hash-init), which breaks the sign symmetry.
What was achieved?
So finally we have a better idea on how things are linked to another. We first did SAL and commited to the disguised points (, , ), then we used circuits to prove knowledge of the openings (secrets) of our tuple (, , ) and we also used circuits to prove that it is of the leaves sitting on tree in the first layer, next we used circuits again to prove the nodes are also really sitting in the tree and we did that up to the root, where we proved that the whole thing holds because we can open to a valid root.
Alright, now we know the mechanics of the circuits. But what is the idea behind all those calculations? Basically, the prover is the only one who knows the witness vector (or the secret vectors ) and the verifier knows the public matrices (), which is part of the consensus. So the idea is that the prover folds his witness vector (secret values) in a hidden way that allows the verifier to check the validity of the proof just by using the public matrices (corresponding to all constraints on those private vectors). And showing how this is done is our next task by developing the Generalized Bulletproof equation: