#5 Circom: Witness, Proof generation and Verification
Recap: Signal operators
===
: used to constraint two signals←—
: used to assign a value to a signal<==
: assign and constraint (combines the above two operators)
Example
x <-- a + 3
:x
is assigned the value of input signala
incremented by 3.x === a + 3
:x
is constrained against the value of input signala
incremented by 3.y <== a + 3
:y
is assigned & constrained against the value of input signala
incremented by 3.
Essentially, the two operations with x
can be simplified to one operation as seen with y
.
Use <==
with intermediate signals
<==
with intermediate signalsIn the previous article, we use a combination of <--
and ===
to introduce our intermediate signal prod
. We could have simply used a single <==
operator:
The <==
operator does two operations under the hood:
assign
prod
the value ofa * b
,constraint
prod
against said value.
The question we left the reader with was:
‘Why constraint prod === a * b
? Why is assignment alone insufficient?’.
To answer this question we first explore what happens to circuits during and after compilation.
Witness, Proof generation and Verification
Let’s examine how a proof is generated and verified using the example above:
The process of compiling this circuit creates a number of artifacts that are meant to help with:
Witness generation
Low-level representation of constraints
Witness generator
The artifacts pertaining to witness generation assists with calculating intermediate signals from the primary inputs (signal inputs).
This is helpful since the prover expects a witness as input. Meaning to say, we are expected to supply the intermediate signal values as well.
Hence, we see that the witness generator accounts for operations involving <--
.
Low-level representation of constraints
This is to be fed into a zkSNARK generator like snarkjs, to generate a prover and verifier.
The prover component can be integrated with some web app, while the verifying component could be integrated into a smart contract or server.
Since the compiler places restrictions on how the===and<==can be used, the resulting system of constraints are of R1CS form.
Example illustration
The prover expects inputs values in-line with the dimensions of the witness vector; intermediate signals and all.
From this, it generates a proof.
The verifier serves to confirm if the proof provided to it is valid or not.
Consequently, an attacker has two channels of attack:
Supply the prover with tailored witness values, to generate a valid proof
Attack the verifier directly by supplying it with a tailored proof
Now let’s review and answer the original question in the next section.
Why assign and constraint prod
?
prod
?Why is prod <== a * b
necessary, and prod <-- a * b
insufficient?
Using only <--
will leave the prover vulnerable to be exploited via tailored witness values. It will produce seemingly valid proofs without checking to ensure prod is constrained. Similarly, the verifier will accept these proofs.
Let us illustrate this:
Witness generation remains unchanged
System of constraints changes;
prod === a * b
is dropped
This means that when we feed the constraints generated on compilation to a zkSNARK generator, the generated prover and verifier will not ensure that prod
is equivalent to a * b
.
prover expects
prod
to be supplied together with the other witness values,prover will not check that
prod
honours the relationshipprod = a * b;
prove will only generate a proof based on
res === prod * c;
verifier will only check that proofs honour the constraint
res === prod * c;
In short, the circuit only verifies that res === prod * c
. This is why it is crucial to assign and constraint intermediate signals. Else, the outcome is an under-constrained circuit.
Remember, while the compiler produces witness generator artifacts for our convenience, it does not mean an attacker must use it. An attacker could pass tailored values to the prover, especially if its under-constrained. We will illustrate this in the exercise section.
It is crucial to understand that we can pass any values we want to a prover. The prover will only check that those inputs honour defined constraints and generate a proof. The prover has no concept of witness generation.
Takeaway: use <==
when assigning to intermediate signals
<==
when assigning to intermediate signalsusing only
<--
with intermediate signals is dangerous, because there is no constraint that ‘protects’ the assignment.hence, the common use of
<==
in circuit definitions.
That leaves us with a final question; when should we be using <--
all by its lonesome?
When to use assignment operator: <—-
<—-
Let’s say you want to verify that you know some values x1, x2, x3, x4, out, such that, out = [(x1 + x2) / x3] - x4
. How would this circuit be constructed?
Since constraints must be constructed as quadratic expressions, we need to breakdown the equation into intermediate parts. Like so:
y1 = x1 + x2
y2 = y1 / x3
out = y2 - x4
This is how the circuit would look like:
division is non-quadratic; cannot be used in constraint definition.
cannot express the
y2 = y1 / x3
as a constraint in Circom
Therefore, we compute the values of the intermediate signals separately, before defining the constraints.
y2 = y1 / x3
is expressed in two parts:
y2 <-- y1 / x3;
y1 === y2 * x3;
At this point, you should have gotten a good understanding of using <==
and <--
.
Other common places you will notice and be expected to use <==
is with components and output signals.
Output signals (signal output out
) would be new to the reader. We will cover this in the next article.
Exercise
This exercise will serve to illustrate the folly of using <--
alone, without constraining the intermediate signal.
In an empty directory create negative.circom
, with the following code:
Compile the circuit
circom negative.circom --r1cs --sym --wasm
Sanity check on terminal output:
non-linear constraints: 1
Examine the R1CS file
snarkjs r1cs print negative.r1cs
There is only 1 constraint in this circuit, which is correctly reflected on compilation and in the R1CS file.
Generate the witness
create
inputs.json
file in./negative_js
directoryvalues passed will be as below
run:
node generate_witness.js **negative**.wasm inputs.json witness.wtns
export to json:
snarkjs wtns export json witness.wtns
cat witness.json
should output:
The reader will observe in the layout of the witness, that intermediate signals are included, and come after input signals.
However, the first two input signals, a
and b
have been omitted. This is because they are not involved in any constraint definition, and therefore have no place in the witness vector.
Verify witness.wtns
against the circuit: snarkjs wtns check negative.r1cs witness.wtns
this checks that the witness produced honors the systems of constraints
think of this as a proxy means to check if a computed witness would create a valid proof
The witness clears. Now let’s see how to create a tailored witness that would be accepted just the same.
Exploiting the circuit
First we re-compile our circuit with the following command: circom negative.circom --r1cs --sym --wasm --O0
We add the --O0
flag, to instruct the compiler to avoid simplifying the circuit on compilation. This will surface both a
and b
in the witness. See constraint simplification for more info.
Generate the witness
create
inputs.json
file in./negative_js
directoryvalues passed will be as below
run:
node generate_witness.js **negative**.wasm inputs.json witness.wtns
export to json:
snarkjs wtns export json witness.wtns
cat witness.json
, should output:
Input signals a
and b
are now present in the witness. This is the consequence of using the --O0
flag.
Creating an exploit witness
We are going to show that as long res = prod * c
, the values of a
and b
do not matter.
To that end, we will create a malicious witness by altering witness.wtns
.
While it would be preferable to alter witness.json
to produce the required witness.wtns
, neither Circom nor snarkjs have the necessary APIs. Hence, the following grease-monkey approach.
Create view.js
in the same directory as witness.wtns
with the following code:
Running view.js
will give us the following output in terminal:
Observe that the values of our witness are laid out in the same sequential order as witness.json.
Modify witness values
Overwrite
witness.wtns
where the values for signalsa
,b
are stored, with arbitrary values.Create
edit.js
file in the same directory:
The After output should be like so:
Observe that only signals a
, b
are modified; the rest remain unchanged.
Verify exploit_witness.wtns
against the circuit:
snarkjs wtns check negative.r1cs exploit_witness.wtns
This indicates that the exploit witness will be accepted, even though prod
is not the resulting product of a
and b
.
This should be instructive in educating the reader the importance of assigning and constraining (<==
) intermediate variables.
Last updated
Was this helpful?