#6 Circom: signal output
`signal output`: values that the component will produce as output. using <== with components and output signals.
signal output
signal outputLet's begin by reviewing the example from the previous article:
template Main() {
// input signals
signal input x1;
signal input x2;
signal input x3;
signal input x4;
signal output out;
// intermediate signals
signal y1;
signal y2;
// calc. intermediate values
y2 <-- y1 / x3;
// constraints can only have quadratic expressions
// can only use + or * or -
y1 <== x1 + x2;
y1 === y2 * x3;
out <== y2 - x4;
}
component main = Main();The reader would have two questions:
if we denote
outassignal out, why did we group it with the other input signals?does
signal outputoutput 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 outputThe 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.
outis 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:
template Mul() {
signal input a;
signal input b;
signal input c;
c === a * b;
}
template Main() {
signal input in[6];
// instantiate subcomponents
component mul1 = Mul();
component mul2 = Mul();
// check: in[0] * in[1] = in[2]
mul1.a <== in[0];
mul1.b <== in[1];
mul1.c <== in[2];
// check: in[3] * in[4] = in[5]
mul2.a <== in[3];
mul2.b <== in[4];
mul2.c <== in[5];
}
component main = Main();Instantiations of sub-components are done using the keyword
component; not unlike main.Instances of components cannot be reused - to use
Multwice, it must be instantiated twice.mul1cannot 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
mul1Low-level representation of constraints ensure that
mul1inputs are constrained to the circuit’s input signals.Additionally, there is the
mul1constraint 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
pragma circom 2.1.8;
template Mul() {
signal input a;
signal input b;
signal input c;
c === a * b;
}
template Main() {
signal input in[3];
// instantiate subcomponents
component mul = Mul();
// assign & constraints inputs
mul.a <== in[0];
mul.b <== in[1];
mul.c <== in[2];
}
component main = Main();Compile the circuit
circom positive.circom --r1cs --sym --wasm --O0Sanity check on terminal output:
non-linear constraints: 1linear 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.r1csTerminal prints all 3 constraints

Generate the witness
create
inputs.jsonfile in./positive_jsdirectoryvalues passed will be as below
{"in": ["1", "2", "2"]}run:
node generate_witness.js positive.wasm inputs.json witness.wtnsexport to json:
snarkjs wtns export json witness.wtnscat witness.jsonshould output the following:
[
"1", // 1
"1", // in[0]
"2", // in[1]
"2", // in[2]
"1", // a
"2", // b
"2" // c
]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:
const fs = require('fs');
const path = require('path');
const witnessPath = path.join(__dirname, 'witness.wtns');
try {
const witness = fs.readFileSync(witnessPath);
let data_arr = new Uint8Array(witness);
console.dir(data_arr, {'maxArrayLength': null});
} catch (error) {
console.error(`Error reading witness file: ${error.message}`);
}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.wtnswhere the values for signalsa,b,care stored.Create
edit.jsfile in the same directory:
const fs = require('fs');
const path = require('path');
// Use path.join to create a full path to the witness.wtns file
const witnessPath = path.join(__dirname, 'witness.wtns');
try {
const witness = fs.readFileSync(witnessPath);
let data_arr = new Uint8Array(witness);
console.log("Before");
console.dir(data_arr, {'maxArrayLength': null});
data_arr[204] = 4; // `a`
data_arr[236] = 5; // `b`
data_arr[268] = 20; // `c`
console.log("After");
console.dir(data_arr, {'maxArrayLength': null});
// create exploit witness
const exploitWitnessPath = path.join(__dirname, 'exploit_witness.wtns');
fs.writeFileSync(exploitWitnessPath, data_arr);
} catch (error) {
console.error(`Error reading witness file: ${error.message}`);
}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
pragma circom 2.1.8;
template Mul() {
signal input a;
signal input b;
signal input c;
c === a * b;
}
template Main() {
signal input in[3];
// instantiate subcomponents
component mul = Mul();
// assign inputs
mul.a <-- in[0];
mul.b <-- in[1];
mul.c <-- in[2];
}
component main = Main();Compile
circom negative.circom --r1cs --sym --wasm --O0Sanity check on terminal output:
non-linear constraints: 1linear constraints: 0private 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.r1csTerminal prints a single constraint:
main.add1.a + main.add1.b = main.add1.c
Generate the witness
create
inputs.jsonfile in./negative_jsdirectory:
{"in": ["1", "2", "2"]}run:
node generate_witness.js negative.wasm inputs.json witness.wtnsexport to json:
snarkjs wtns export json witness.wtnscat witness.jsonshould output the following:run:
node generate_witness.js negative.wasm inputs.json witness.wtnsexport to json:
snarkjs wtns export json witness.wtnscat witness.jsonshould output the following:
[
"1", // 1
"1", // in[0]
"2", // in[1]
"2", // in[2]
"1", // a
"2", // b
"2" // c
]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.jsfile in the same directory aswitness.wtnslayout of the witness variables are the same, and so the same modifications will work
verify
exploit_witness.wtnsagainst 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?