Comparators

Comparators compare two numbers in an arithmetic circuit. At the most basic level, we are interested in comparing a number to zero. Then, we can derive an equality check by simply checking if the difference of inputs is equal to zero.

  • is the IsZero circuit

We usually make use of LessThan circuit which does a < b check, and derive all other remaining comparisons from that. This should make sense, since assuming both a and b are integers we can say:

  • is the LessThan circuit

IsZero

template IsZero() {
  signal input in;
  signal output out;

  signal inv <-- in != 0 ? 1 / in : 0;
  out <== 1 - (in * inv);

  in * out === 0;
}

Here, we check if the given number has a multiplicative inverse. The only number that does not have an inverse is 0. However, inverting a number is a non-quadratic constraint (hence the <-- instead of <==) so must assume that signal inv can be any arbitrary value from the perspective of an adversarial prover. To constrain inv, we can invert it again! Notice that:

We can't really add a constraint like 1 === in * inv because it will simply fail when in is 0. That is why we make use of a ternary operator to simply set inv to be zero if in is zero. So, we may expect in * inv to equal 1 if indeed the number has an inverse, meaning that the number is non-zero. By subtracting this from 1, we essentially do a logical NOT operation on the result, so that if the in has an inverse out becomes 0.

Let's examine what happens when an adversary inputs some random inv on both cases where in is zero and non-zero:

When in = 0 and inv = x all constraints are looking good:

  • out <== 1 - 0 constraint is fine
  • 0 * out <== 0 constraint is fine

When in != 0 and inv = x the constraint is indirectly implied:

  • out <== 1 - in * x
  • in * (1 - in * x) <== 0

Here, out could be anything but for in * (1 - in * x) <== 0 to pass the only option is that 1 === in * x so x must indeed be the inverse of in.

IsEqual

template IsEqual() {
  signal input in[2];
  signal output out;

  out <== IsZero()(in[1] - in[0]);
}

As we mentioned before, if a = b then a - b = 0, allowing us to re-use the IsZero circuit.

LessThan

template LessThan(n) {
  assert(n <= 252);
  signal input in[2];
  signal output out;

  component toBits = Num2Bits(n+1);
  toBits.in <== ((1 << n) + in[0]) - in[1];

  out <== 1 - toBits.out[n];
}

There is something going on here as you might notice, why are we using Num2Bits and what is that 1 << n doing there? It will all make sense.

The trick of LessThan is to think of bitwise-subtraction. Think of subtracting two n-bit numbers, a - b. Now suppose that during this subtraction the n-bit is set to 1, which is what 1 << n does. Note that the most significant bit of an n-bit number is (n-1)'th bit, as we are speaking in zero-indexed terms.

If a is less than b, then during this subtraction a will have to borrow that n'th bit. Otherwise, it is large enough that b can be subtracted without needing to borrow. To see if a < b we can simply check if n'th bit is borrowed or not during a subtraction. Since n'th bit will be 0 if it is borrwed, we must do a logical NOT operation (simply subtract the result from 1) as we assign it to the output.

The role of assert(n <= 252) is to ensure we can safely set n'th bit to 1. That would be a problem if n were to be greater than 252, as that is where the order of scalar field of our curve (BN254 in this case) is cut-off.

LessEqThan

template LessEqThan(n) {
  signal input in[2];
  signal output out;

  out <== LessThan(n)([in[0], in[1]+1]);
}

This is because .

GreaterThan

template GreaterThan(n) {
  signal input in[2];
  signal output out;

  out <== LessThan(n)([in[1], in[0]]);
}

This is because .

GreaterEqThan

template GreaterEqThan(n) {
  signal input in[2];
  signal output out;

  out <== LessThan(n)([in[1], in[0]+1]);
}

This is because .