o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
242 lines • 9.79 kB
JavaScript
import { createBool, createBoolUnsafe, createField } from '../core/field-constructor.js';
import { Fp } from '../../../bindings/crypto/finite-field.js';
import { assert } from '../../../lib/util/assert.js';
import { exists, existsOne } from '../core/exists.js';
import { assertMul } from './compatible.js';
import { Field3, ForeignField } from './foreign-field.js';
import { l, l2, multiRangeCheck } from './range-check.js';
import { witness } from '../types/witness.js';
export {
// generic comparison gadgets for inputs in a narrower range < p/2
assertLessThanGeneric, assertLessThanOrEqualGeneric, lessThanGeneric, lessThanOrEqualGeneric,
// comparison gadgets for full range inputs
assertLessThanFull, assertLessThanOrEqualFull, lessThanFull, lessThanOrEqualFull,
// gadgets that are based on full comparisons
isOddAndHigh,
// legacy, unused
compareCompatible,
// internal helper
fieldToField3, };
/**
* Prove x <= y assuming 0 <= x, y < c.
* The upper bound c must satisfy 2c <= p, where p is the field order.
*
* Expects a function `rangeCheck(v: Field)` which proves that v is in [0, p-c).
* (Note: the range check on v can be looser than the assumption on x and y, but it doesn't have to be)
* The efficiency of the gadget largely depends on the efficiency of `rangeCheck()`.
*
* **Warning:** The gadget does not prove x <= y if either 2c > p or x or y are not in [0, c).
* Neither of these conditions are enforced by the gadget.
*/
function assertLessThanOrEqualGeneric(x, y, rangeCheck) {
// since 0 <= x, y < c, we have y - x in [0, c) u (p-c, p)
// because of c <= p-c, the two ranges are disjoint. therefore,
// y - x in [0, p-c) is equivalent to x <= y
rangeCheck(y.sub(x).seal());
}
/**
* Prove x < y assuming 0 <= x, y < c.
*
* Assumptions are the same as in {@link assertLessThanOrEqualGeneric}.
*/
function assertLessThanGeneric(x, y, rangeCheck) {
// since 0 <= x, y < c, we have y - 1 - x in [0, c) u [p-c, p)
// because of c <= p-c, the two ranges are disjoint. therefore,
// y - 1 - x in [0, p-c) is equivalent to x <= y - 1 which is equivalent to x < y
rangeCheck(y.sub(1).sub(x).seal());
}
/**
* Return a Bool b that is true if and only if x < y.
*
* Assumptions are similar as in {@link assertLessThanOrEqualGeneric}, with some important differences:
* - c is a required input
* - the `rangeCheck` function must fully prove that its input is in [0, c)
*/
function lessThanGeneric(x, y, c, rangeCheck) {
// we prove that there exists b such that b*c + x - y is in [0, c)
// if b = 0, this implies x - y is in [0, c), and so x >= y
// if b = 1, this implies x - y is in [p-c, p), and so x < y because p-c >= c
let b = existsOne(() => BigInt(x.toBigInt() < y.toBigInt()));
let isLessThan = b.assertBool();
// b*c + x - y in [0, c)
rangeCheck(b.mul(c).add(x).sub(y).seal());
return isLessThan;
}
/**
* Return a Bool b that is true if and only if x <= y.
*
* Assumptions are similar as in {@link assertLessThanOrEqualGeneric}, with some important differences:
* - c is a required input
* - the `rangeCheck` function must fully prove that its input is in [0, c)
*/
function lessThanOrEqualGeneric(x, y, c, rangeCheck) {
// we prove that there exists b such that b*c + x - y - 1 is in [0, c)
// if b = 0, this implies x - y - 1 is in [0, c), and so x > y
// if b = 1, this implies x - y - 1 is in [p-c, p), and so x <= y because p-c >= c
let b = existsOne(() => BigInt(x.toBigInt() <= y.toBigInt()));
let isLessThanOrEqual = b.assertBool();
// b*c + x - y - 1 in [0, c)
rangeCheck(b.mul(c).add(x).sub(y).sub(1).seal());
return isLessThanOrEqual;
}
/**
* Assert that x < y.
*
* There are no assumptions on the range of x and y, they can occupy the full range [0, p).
*/
function assertLessThanFull(x, y) {
let xBig = fieldToField3(x);
let yBig = fieldToField3(y);
// x < y as bigints
ForeignField.assertLessThan(xBig, yBig);
// y < p, so y is canonical. implies x < p as well.
// (if we didn't do this check, we would prove nothing.
// e.g. yBig could be the bigint representation of y + p, and only _therefore_ larger than xBig)
ForeignField.assertLessThan(yBig, Fp.modulus);
}
/**
* Assert that x <= y.
*
* There are no assumptions on the range of x and y, they can occupy the full range [0, p).
*/
function assertLessThanOrEqualFull(x, y) {
let xBig = fieldToField3(x);
let yBig = fieldToField3(y);
ForeignField.assertLessThanOrEqual(xBig, yBig);
ForeignField.assertLessThan(yBig, Fp.modulus);
}
/**
* Return a Bool b that is true if and only if x < y.
*
* There are no assumptions on the range of x and y, they can occupy the full range [0, p).
*/
function lessThanFull(x, y) {
// same logic as in lessThanGeneric:
// we witness b such that b*p + x - y is in [0, p), where the sum is done in bigint arithmetic
// if b = 0, x - y is in [0, p), and so x >= y
// if b = 1, x - y is in [-p, 0), and so x < y
// we must also check that both x and y are canonical, or else the connection between the bigint and the Field is lost
let b = existsOne(() => BigInt(x.toBigInt() < y.toBigInt()));
let isLessThan = b.assertBool();
let xBig = fieldToField3(x);
let yBig = fieldToField3(y);
ForeignField.assertLessThan(xBig, Fp.modulus);
ForeignField.assertLessThan(yBig, Fp.modulus);
let [p0, p1, p2] = Field3.from(Fp.modulus);
let bTimesP = [p0.mul(b), p1.mul(b), p2.mul(b)];
// b*p + x - y in [0, p)
let z = ForeignField.sum([bTimesP, xBig, yBig], [1n, -1n], 0n);
ForeignField.assertLessThan(z, Fp.modulus);
return isLessThan;
}
/**
* Return a Bool b that is true if and only if x <= y.
*
* There are no assumptions on the range of x and y, they can occupy the full range [0, p).
*/
function lessThanOrEqualFull(x, y) {
// keep it simple and just use x <= y <=> !(y < x)
return lessThanFull(y, x).not();
}
/**
* Splits a field element into a low bit `isOdd` and a 254-bit `high` part.
*
* There are no assumptions on the range of x and y, they can occupy the full range [0, p).
*/
function isOddAndHigh(x) {
if (x.isConstant()) {
let x0 = x.toBigInt();
return { isOdd: createBool((x0 & 1n) === 1n), high: createField(x0 >> 1n) };
}
// witness a bit b such that x = b + 2z for some z <= (p-1)/2
// this is always possible, and unique _except_ in the edge case where x = 0 = 0 + 2*0 = 1 + 2*(p-1)/2
// so we must assert that x = 0 implies b = 0
let [b, z] = exists(2, () => {
let x0 = x.toBigInt();
return [x0 & 1n, x0 >> 1n];
});
let isOdd = b.assertBool();
z.assertLessThan((Fp.modulus + 1n) / 2n);
// x == b + 2z
b.add(z.mul(2n)).assertEquals(x);
// prevent overflow case when x = 0
// we witness x' such that b == x * x', which makes it impossible to have x = 0 and b = 1
let x_ = existsOne(() => (b.toBigInt() === 0n ? 0n : Fp.inverse(x.toBigInt()) ?? 0n));
x.mul(x_).assertEquals(b);
return { isOdd, high: z };
}
/**
* internal helper, split Field into a 3-limb bigint
*
* **Warning:** the output is underconstrained up to a multiple of the modulus that could be added to the bigint.
*/
function fieldToField3(x) {
if (x.isConstant())
return Field3.from(x.toBigInt());
let xBig = witness(Field3, () => x.toBigInt());
multiRangeCheck(xBig);
let [x0, x1, x2] = xBig;
// prove that x == x0 + x1*2^l + x2*2^2l
let x_ = x0.add(x1.mul(1n << l)).add(x2.mul(1n << l2));
x_.assertEquals(x);
return xBig;
}
/**
* Compare x and y assuming both have at most `n` bits.
*
* **Important:** If `x` and `y` have more than `n` bits, this doesn't prove the comparison correctly.
* It is up to the caller to prove that `x` and `y` have at most `n` bits.
*
* **Warning:** This was created for 1:1 compatibility with snarky's `compare` gadget.
* It was designed for R1CS and is extremely inefficient when used with plonkish arithmetization.
*/
function compareCompatible(x, y, n = Fp.sizeInBits - 2) {
let maxLength = Fp.sizeInBits - 2;
assert(n <= maxLength, `bitLength must be at most ${maxLength}`);
// z = 2^n + y - x
let z = createField(1n << BigInt(n))
.add(y)
.sub(x);
let zBits = unpack(z, n + 1);
// highest (n-th) bit tells us if z >= 2^n
// which is equivalent to x <= y
let lessOrEqual = zBits[n];
// other bits tell us if x = y
let prefix = zBits.slice(0, n);
let notAllZeros = any(prefix);
let less = lessOrEqual.and(notAllZeros);
return { lessOrEqual, less };
}
// helper functions for `compareCompatible()`
// custom version of toBits to be compatible
function unpack(x, length) {
let bits = exists(length, () => {
let x0 = x.toBigInt();
return Array.from({ length }, (_, k) => (x0 >> BigInt(k)) & 1n);
});
bits.forEach((b) => b.assertBool());
let lc = bits.reduce((acc, b, i) => acc.add(b.mul(1n << BigInt(i))), createField(0));
assertMul(lc, createField(1), x);
return bits.map((b) => createBoolUnsafe(b));
}
function any(xs) {
let sum = xs.reduce((a, b) => a.add(b.toField()), createField(0));
let allZero = isZero(sum);
return allZero.not();
}
// custom isZero to be compatible
function isZero(x) {
// create witnesses z = 1/x (or z=0 if x=0), and b = 1 - zx
let [b, z] = exists(2, () => {
let xmy = x.toBigInt();
let z = Fp.inverse(xmy) ?? 0n;
let b = Fp.sub(1n, Fp.mul(z, xmy));
return [b, z];
});
// b * x === 0
assertMul(b, x, createField(0));
// z * x === 1 - b
assertMul(z, x, createField(1).sub(b));
return createBoolUnsafe(b);
}
//# sourceMappingURL=comparison.js.map