#6 Circom: signal output
`signal output`: values that the component will produce as output. using <== with components and output signals.
signal output
signal output
Let's begin by reviewing the example from the previous article:
The reader would have two questions:
if we denote
out
assignal out
, why did we group it with the other input signals?does
signal output
output anything if at all?
signal output
is an input signal
signal output
is an input signalDespite the "output" keyword, from the circuit's perspective, out
is actually an input signal. The circuit creates a set of R1CS constraints, and out
is simply part of this system.
The prover receives values of the witness vector, which must satisfy the circuit's constraints. In this case, the witness vector looks like: [1, out, x1, x2, x3, x4, y1, y2]
.
Remember, we are not computing anything - only verifying if a computation is correct. Hence, the outcome of the computation must be provided
This raises a question: if out
is an input signal, shouldn't we be using ===
instead of <==
?
Why use <==
and not ===
for signal output
<==
and not ===
for signal output
The reason for constraining and assigning out
is similar to why we use <==
with intermediate signals, as explained previously. Although considered an input from the prover's perspective, out
is still a value computed by the circuit.
Think of it this way: when defining the circuit, we're establishing relationships between inputs. From the circuit's viewpoint, out
is simply another value calculated from the inputs.
The assignment operation (<==
) is syntactic sugar—it instructs the compiler to include the necessary calculations in the witness generation artifacts for our convenience.
So why bother with the output
designation at all?
Does signal output
output anything if at all?
signal output
output anything if at all?Yes, it does serve a purpose.
out
is the output of this circuitA circuit can pass values to another circuit, to be used as input
However, a circuit doesn't "output" data during execution in the traditional sense of programming
Therefore, a signal labelled signal output
can be viewed as an input signal that we intend to pass along to another circuit. Circuits do not output anything to the world.
Subcomponents
As circuits get more complex, Circom lets you break them into smaller, manageable pieces (subcomponents).
Input signals pass data to these subcomponents, while output signals relay data from one circuit to another.
Example
Two templates are defined in this example, Mul
and Main
.
Mul
serves to verify that of the supplied inputs, a*b=c
. Main
does the same repeatedly as seen below:
Instantiations of sub-components are done using the keyword
component
; not unlike main.Instances of components cannot be reused - to use
Mul
twice, it must be instantiated twice.mul1
cannot be reused with a different set if inputs.Inputs are passed into sub-components via dot notation. E.g.,
mul1.
a
Why use <==
to pass inputs?
<==
to pass inputs?To explain this we examine the artifacts produced by the Circom compiler.
For simplicity we will focus on sub-component mul1
only and ignore mul2
; rest assured the same logic applies.
Scenario 1: Inputs are assigned and constrained:
Witness generation artifacts assign the circuit’s input signals to be inputs to
mul1
Low-level representation of constraints ensure that
mul1
inputs are constrained to the circuit’s input signals.Additionally, there is the
mul1
constraint that verifies thata*b=c
.
When we feed this system of constraints to a zkSNARK generator (snarkJs), the generated prover and verifier will ensure that all these constraints are adhered to.
Scenario 2: Inputs are assigned, but not constrained
Witness generation remains unchanged.
But there are is only a single constraint that verifies that
a*b=c
.
When we feed this system of constraints to a zkSNARK generator, the generated prover and verifier will only look to verify that a*b=c
.
The input signals to the Main circuit, in[6]
, are disregarded.
The prover will not check and generate a proof that is based on in[6]
- an attacker could pass arbitrary values (differing from in[6]
) to the subcomponent to achieve verification.
We will explore practical methods of achieving this in the exercise section.
In the next article we will see how outputs are used in foundational circuits found within Circomlib (primarily in comparators.circom).
Exercise
This exercise will serve to illustrate the importance of using the <==
operator when passing inputs to subcomponents.
We begin by illustrating what happens under the hood when we use <==
.
Positive Example
Compile the circuit
circom positive.circom --r1cs --sym --wasm --O0
Sanity check on terminal output:
non-linear constraints: 1
linear constraints: 3
This checks out.
one non-linear constraint accounting for
a*b===c
.three linear constraints cover the subcomponent inputs’ constraints
Examine the R1CS file
snarkjs r1cs print positive.r1cs
Terminal prints all 3 constraints
Generate the witness
create
inputs.json
file in./positive_js
directoryvalues passed will be as below
run:
node generate_witness.js positive.wasm inputs.json witness.wtns
export to json:
snarkjs wtns export json witness.wtns
cat witness.json
should output the following:
The witness produced follows the expected layout.
Next, we check if a malicious witness would be accepted in this scenario.
Informational: --O0
flag
--O0
flagIf we had compiled without the --O0
flag, in the terminal output linear constraints would list linear constraints as 0 and this would subsequently affect the structure of the witness later on.
The reader is welcome to repeat the entire process with and without the --O0
flag as a learning exercise.
Exploiting the circuit
We will attempt to exploit the circuit by creating a malicious witness and verifying it. The malicious witness will be created by modifying the valid witness.wtns
file.
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
,c
are stored.Create
edit.js
file in the same directory:
The After
output should be like so:
Observe that only signals a, b, c are modified; the rest remain unchanged.
Verify exploit_witness.wtns
against the circuit:
snarkjs wtns check positive.r1cs exploit_witness.wtns
This indicates that the malicious witness will not be accepted, as there input constraints to ensure that the circuit’s top-level signals match the inputs to Mul().
Observe that the unmodified witness is accepted: snarkjs wtns check positive.r1cs witness.wtns
Next, the negative example of using <--
.
Negative Example
Compile
circom negative.circom --r1cs --sym --wasm --O0
Sanity check on terminal output:
non-linear constraints: 1
linear constraints: 0
private inputs: 3 (none belong to witness)
The above comment is a warning that there are 3 private inputs as per in[3]
, however none of them are in the witness vector. This will be made apparent as we go along.
The other point of note, is that we no longer have 3 linear constraints, as we do not have input constraints in this scenario.
Examine the R1CS file
snarkjs r1cs print negative.r1cs
Terminal prints a single constraint:
main.add1.a + main.add1.b = main.add1.c
Generate the witness
create
inputs.json
file in./negative_js
directory:
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 following: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 following:
The witness produced follows the expected layout. Let’s check if a malicious witness would be accepted in this scenario.
Exploiting the circuit
create and run
edit.js
file in the same directory aswitness.wtns
layout of the witness variables are the same, and so the same modifications will work
verify
exploit_witness.wtns
against the circuit:snarkjs wtns check negative.r1cs exploit_witness.wtns
The exploit works because we passed values [a=4
, b=5
, c=20
] to the subcomponent, satisfying its constraint, while in[3]
held different values.
We successfully exploited the circuit by passing inconsistent values, demonstrating that it's under-constrained. This vulnerability allows an attacker to craft a fake witness using unrelated data, highlighting the importance of constraining inputs when passing them to subcomponents.
Last updated
Was this helpful?