Swapping Two Items in an Array in Circom
This chapter shows how to swap two signals in a list of signals. This is an important subroutine for a sorting algorithm. More generally, lists are a fundamental building block for more interesting functions like hash functions or modeling memory in a CPU, so we must learn how to update their values.
Swapping two items in a list is one of the first things programmers learn, a typical solution looks like the following:
# s and t are indexes of array arr
def swap(arr, s, t):
temp = arr[s];
arr[s] = arr[t];
arr[t] = temp;
return arr
However, in a ZK circuit, this operation can be surprisingly tricky.
- First, we cannot directly index an array of signals. For that, we need to use a Quin selector.
- Second, we cannot “write to” a signal in an array of signals because signals are immutable.
Instead, we need to create a new array and copy the old values to the new array, subject to the following conditions:
- If we are at index
s
, write the value at arr[t]
- If we are at index
t
, write the value at arr[s]
- Otherwise, write the original value
Every write we make to the new array is a conditional operation.
The Quin selector was explained in a prior chapter — we won’t replicate the code here to save space.
Swap in Circom
The component below will swap the item at index s
with the item at index t
and return a new array. (The following code has a bug, try to find it! The answer is given later.)
template Swap(n) {
signal input in[n];
signal input s;
signal input t;
signal output out[n];
// we do not check that
// s < n or t < n
// because the Quin selector
// does that
// get the value at s
component qss = QuinSelector(n);
qss.idx <== s;
for (var i = 0; i < n; i++) {
qss.in[i] <== in[i];
}
// get the value at t
component qst = QuinSelector(n);
qst.idx <== t;
for (var i = 0; i < n; i++) {
qst.in[i] <== in[i];
}
// qss.out holds in[s]
// qst.out holds in[t]
component IdxEqS[n];
component IdxEqT[n];
component IdxNorST[n];
signal branchS[n];
signal branchT[n];
signal branchNorST[n];
for (var i = 0; i < n; i++) {
IdxEqS[i] = IsEqual();
IdxEqS[i].in[0] <== i;
IdxEqS[i].in[1] <== s;
IdxEqT[i] = IsEqual();
IdxEqT[i].in[0] <== i;
IdxEqT[i].in[1] <== t;
// if IdxEqS[i].out + IdxEqT[i].out
// equals 0, then it is not i ≠ s and i ≠ t
IdxNorST[i] = IsZero();
IdxNorST[i].in <== IdxEqS[i].out + IdxEqT[i].out;
// if we are at index s,
// write in[t]
// if we are at index t,
// write in[s]
// else write in[i]
branchS[i] <== IdxEqS[i].out * qst.out;
branchT[i] <== IdxEqT[i].out * qss.out;
branchNorST[i] <== IdxNorST[i].out * in[i];
out[i] <== branchS[i] + branchT[i] + branchNorST[i];
}
}
Note that the final conditional statement
branchS[i] <== IdxEqS[i].out * qst.out;
branchT[i] <== IdxEqT[i].out * qss.out;
branchNorST[i] <== IdxNorST[i].out * in[i];
out[i] <== branchS[i] + branchT[i] + branchNorST[i];
cannot be written as
out[i] <== IdxEqS[i].out * qst.out + IdxEqT[i].out * qss.out + IdxNorST[i].out * in[i]
because that would produce a non-quadratic constraints error (there is more than one multiplication in the constraint).
Catch the bug
There is a bug in the code above — can you catch it before scrolling down?
The bug in the code
The problem with the code above is that it doesn’t account for the fact that the value at s
might equal the value at t
(s == t
). In that circumstance, the value written to the index will be the value at that index added to itself.
Fixing the problem
To prevent this, we need to explicitly detect if s == t
and multiply one of either branchS
or branchT
by zero to avoid doubling the value. In other words, if the switches for s
and t
are both active, then the resulting value would be s + t
. But we don’t want that, we want the value to effectively remain unchanged by selecting branchS
or branchT
arbitrarily (they will have the same value):
template Swap(n) {
signal input in[n];
signal input s;
signal input t;
signal output out[n];
// NEW CODE to detect if s == t
signal sEqT;
sEqT <== IsEqual()([s, t]);
// get the value at s
component qss = QuinSelector(n);
qss.idx <== s;
for (var i = 0; i < n; i++) {
qss.in[i] <== in[i];
}
// get the value at t
component qst = QuinSelector(n);
qst.idx <== t;
for (var i = 0; i < n; i++) {
qst.in[i] <== in[i];
}
component IdxEqS[n];
component IdxEqT[n];
component IdxNorST[n];
signal branchS[n];
signal branchT[n];
signal branchNorST[n];
for (var i = 0; i < n; i++) {
IdxEqS[i] = IsEqual();
IdxEqS[i].in[0] <== i;
IdxEqS[i].in[1] <== s;
IdxEqT[i] = IsEqual();
IdxEqT[i].in[0] <== i;
IdxEqT[i].in[1] <== t;
// if IdxEqS[i].out + IdxEqT[i].out
// equals 0, then it is not i ≠ s and i ≠ t
IdxNorST[i] = IsZero();
IdxNorST[i].in <== IdxEqS[i].out + IdxEqT[i].out;
// if we are at index s, write in[t]
// if we are at index t, write in[s]
// else write in[i]
branchS[i] <== IdxEqS[i].out * qst.out;
branchT[i] <== IdxEqT[i].out * qss.out;
branchNorST[i] <== IdxNorST[i].out * in[i];
// multiply branchS by zero if s equals T
out[i] <== (1-sEqT) * (branchS[i]) + branchT[i] + branchNorST[i];
}
}
Conclusion
Any array manipulation in Circom requires creating a new array and copying the old values to the new one, except where the update happens.
By using this pattern in a loop, we can do things like sort a list, model data structures like stacks and queues, and even change the state of a CPU or VM. We will see examples of those in the following chapters.