The arithmetic circuits built using circom operate on signals, which contain field elements in Z/pZ. Signals can be named with an identifier or can be stored in arrays and declared using the keyword signal. Signals can be defined as input or output, and are considered intermediate signals otherwise.
Let's break down this description.
Analogy to Electrical Circuits
It is important to remember that arithmetic circuits are analogous to electrical circuits, and that in many ways the movement of a signal throughout our ZK circuits is again analogous to an electrical signal's traversal of an integrated circuit.
Obviously, the signals in physical integrated circuits are bound to the binary number system while Arithmetic circuits' signals can carry a field.
As mentioned in the Theory section, computations occur on primefinite fields. If you are working with smaller numbers, you will likely never need to think about this fact. However, Circom's default definition of p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
If you are dealing with numbers that exceed the value of p, they will wrap around (very similar to an integer overflow) causing unexpected behavior if not addressed.
Visibility
Signal Visibility Types
Signals come in three flavors (visibility or access types).
Public: The default visibility for input signals. All output signals will be public as well
Private: The only signals that can be private are input signals and they must be specified as private or they will be public by default.
Intermediate: Never visible from outside of a circuit. They represent the values outputted by arithmetic gates that are then fed into other gates. When there are no more gates in the flow of the circuit then the final signal will be an output signal.
Signals are declared with the keyword signal followed by specifying whether the type is input, intermediate, or output. The following shows declarations without the use of signal tagging (advanced, technically unnecessary safety feature detailed later on).
// input signals
signal input in1;
signal input in2[3]; // 1d array of signals
signal input in3[2][9]; // 2d array of signals
// intermediate singals
signal inter;
// output signals
signal out out1;
signal out out2[2]; // array of signals
Inline Output Signal Declaration
As of Circom 2.0.4, output signals can be defined inline, as shown below:
pragma circom 2.0.4;
template Multiplier2(){
//Declaration of signals
signal input in1;
signal input in2;
signal output out <== in1 * in2; // inline output signal declaration
}
component main {public [in1,in2]} = Multiplier2();
Making Input Signals Public
As mentioned, by default all input signals are private. The main component can define which input signals are public, as shown below
pragma circom 2.0.4;
template Multiplier2(){
//Declaration of signals
signal input inPub;
signal input inPriv;
signal output out <== inPub * inPriv;
}
component main {public [inPub]} = Multiplier2(); // set inPub as public input
What is a Variable?
Signals are immutable, meaning that they can only be assigned once. This is a feature, not a bug! Before understanding variables, let's quickly go over the concept of unknowns. From the Circom docs:
At compilation time, the content of a signal is always considered unknown (see Unknowns), even if a constant is already assigned to them. The reason for that is to provide a precise decidable definition of which constructions are allowed and which are not, without depending on the power of the compiler to detect whether a signal has always a constant value or not.
pragma circom 2.0.0;
template A(n1, n2){ // known
signal input in1; // unknown
signal input in2; // unknown
var x = 0; // known
var y = n1; // known
var z = in1; // unknown
}
component main = A(1, 2);
Here we get the first introduction to variables. Variables assist in the computation of information that does not need immediate constraint. For instance, BattleZips constructs the two possible board states (horizontal and vertical, see conditional statements for elaboration) as variables before assigning the value held in the variable to the multiplexer input signals.
"Signal Tags circom 2.1.0 introduces a new feature called signal tags. Tags can be defined during the declaration of any signal in a template. The tag list is indicated between brackets right before the signal name"
signal (input/output) {tag_1,...,tag_n} signalname;
The example can initially be a bit confusing, perhaps leading one to believe that there is a predefined list of tags:
template IsZero() {
signal input in;
signal output {binary} out;
signal inv;
inv <-- in!=0 ? 1/in : 0;
out <== -in*inv +1;
in*out === 0;
}
In practice, however, this is an example of a properly built component that will facilitate tagging. The binary tag is arbitrary, however the constraints applied within the component mean that, when extending IsZero in your codebase, input signals will be forced to use the binary tag, and signals tagged with binary that are not actually binary will fail the constraint.
This feature is not required for a functioning code base, but is recommended for a polished, secure circuit.