o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
288 lines • 10.7 kB
JavaScript
import { Snarky } from '../../../bindings.js';
import { Fp } from '../../../bindings/crypto/finite-field.js';
import { BinableFp } from '../../../mina-signer/src/field-bigint.js';
import { Gates } from '../gates.js';
import { assert, bitSlice, toVar, toVars } from './common.js';
import { exists } from '../core/exists.js';
import { createBool, createField } from '../core/field-constructor.js';
export { rangeCheck64, rangeCheck32, multiRangeCheck, compactMultiRangeCheck, rangeCheckN, isDefinitelyInRangeN, rangeCheck8, rangeCheck16, rangeCheckLessThan16, rangeCheckLessThan64, };
export { l, l2, l3, lMask, l2Mask };
/**
* Asserts that x is in the range [0, 2^32)
*/
function rangeCheck32(x) {
if (x.isConstant()) {
if (x.toBigInt() >= 1n << 32n) {
throw Error(`rangeCheck32: expected field to fit in 32 bits, got ${x}`);
}
return;
}
let actual = rangeCheckHelper(32, x);
actual.assertEquals(x);
}
/**
* Asserts that x is in the range [0, 2^64).
*
* Returns the 4 highest 12-bit limbs of x in reverse order: [x52, x40, x28, x16].
*/
function rangeCheck64(x) {
if (x.isConstant()) {
let xx = x.toBigInt();
if (xx >= 1n << 64n) {
throw Error(`rangeCheck64: expected field to fit in 64 bits, got ${x}`);
}
// returned for consistency with the provable case
return [
createField(bitSlice(xx, 52, 12)),
createField(bitSlice(xx, 40, 12)),
createField(bitSlice(xx, 28, 12)),
createField(bitSlice(xx, 16, 12)),
];
}
// crumbs (2-bit limbs)
let [x0, x2, x4, x6, x8, x10, x12, x14] = exists(8, () => {
let xx = x.toBigInt();
return [
bitSlice(xx, 0, 2),
bitSlice(xx, 2, 2),
bitSlice(xx, 4, 2),
bitSlice(xx, 6, 2),
bitSlice(xx, 8, 2),
bitSlice(xx, 10, 2),
bitSlice(xx, 12, 2),
bitSlice(xx, 14, 2),
];
});
// 12-bit limbs
let [x16, x28, x40, x52] = exists(4, () => {
let xx = x.toBigInt();
return [bitSlice(xx, 16, 12), bitSlice(xx, 28, 12), bitSlice(xx, 40, 12), bitSlice(xx, 52, 12)];
});
Gates.rangeCheck0(x, [createField(0), createField(0), x52, x40, x28, x16], [x14, x12, x10, x8, x6, x4, x2, x0], false // not using compact mode
);
return [x52, x40, x28, x16];
}
// default bigint limb size
const l = 88n;
const l2 = 2n * l;
const l3 = 3n * l;
const lMask = (1n << l) - 1n;
const l2Mask = (1n << l2) - 1n;
/**
* Asserts that x, y, z \in [0, 2^88)
*/
function multiRangeCheck([x, y, z]) {
if (x.isConstant() && y.isConstant() && z.isConstant()) {
if (x.toBigInt() >> l || y.toBigInt() >> l || z.toBigInt() >> l) {
throw Error(`Expected fields to fit in ${l} bits, got ${x}, ${y}, ${z}`);
}
return;
}
// ensure we are using pure variables
[x, y, z] = toVars([x, y, z]);
let zero = toVar(0n);
let [x64, x76] = rangeCheck0Helper(x);
let [y64, y76] = rangeCheck0Helper(y);
rangeCheck1Helper({ x64, x76, y64, y76, z, yz: zero });
}
/**
* Compact multi-range-check - checks
* - xy = x + 2^88*y
* - x, y, z \in [0, 2^88)
*
* Returns the full limbs x, y, z
*/
function compactMultiRangeCheck(xy, z) {
// constant case
if (xy.isConstant() && z.isConstant()) {
if (xy.toBigInt() >> l2 || z.toBigInt() >> l) {
throw Error(`Expected fields to fit in ${l2} and ${l} bits respectively, got ${xy}, ${z}`);
}
let [x, y] = splitCompactLimb(xy.toBigInt());
return [createField(x), createField(y), z];
}
// ensure we are using pure variables
[xy, z] = toVars([xy, z]);
let [x, y] = exists(2, () => splitCompactLimb(xy.toBigInt()));
let [z64, z76] = rangeCheck0Helper(z, false);
let [x64, x76] = rangeCheck0Helper(x, true);
rangeCheck1Helper({ x64: z64, x76: z76, y64: x64, y76: x76, z: y, yz: xy });
return [x, y, z];
}
function splitCompactLimb(x01) {
return [x01 & lMask, x01 >> l];
}
function rangeCheck0Helper(x, isCompact = false) {
// crumbs (2-bit limbs)
let [x0, x2, x4, x6, x8, x10, x12, x14] = exists(8, () => {
let xx = x.toBigInt();
return [
bitSlice(xx, 0, 2),
bitSlice(xx, 2, 2),
bitSlice(xx, 4, 2),
bitSlice(xx, 6, 2),
bitSlice(xx, 8, 2),
bitSlice(xx, 10, 2),
bitSlice(xx, 12, 2),
bitSlice(xx, 14, 2),
];
});
// 12-bit limbs
let [x16, x28, x40, x52, x64, x76] = exists(6, () => {
let xx = x.toBigInt();
return [
bitSlice(xx, 16, 12),
bitSlice(xx, 28, 12),
bitSlice(xx, 40, 12),
bitSlice(xx, 52, 12),
bitSlice(xx, 64, 12),
bitSlice(xx, 76, 12),
];
});
Gates.rangeCheck0(x, [x76, x64, x52, x40, x28, x16], [x14, x12, x10, x8, x6, x4, x2, x0], isCompact);
// the two highest 12-bit limbs are returned because another gate
// is needed to add lookups for them
return [x64, x76];
}
function rangeCheck1Helper(inputs) {
let { x64, x76, y64, y76, z, yz } = inputs;
// create limbs for current row
let [z22, z24, z26, z28, z30, z32, z34, z36, z38, z50, z62, z74, z86] = exists(13, () => {
let zz = z.toBigInt();
return [
bitSlice(zz, 22, 2),
bitSlice(zz, 24, 2),
bitSlice(zz, 26, 2),
bitSlice(zz, 28, 2),
bitSlice(zz, 30, 2),
bitSlice(zz, 32, 2),
bitSlice(zz, 34, 2),
bitSlice(zz, 36, 2),
bitSlice(zz, 38, 12),
bitSlice(zz, 50, 12),
bitSlice(zz, 62, 12),
bitSlice(zz, 74, 12),
bitSlice(zz, 86, 2),
];
});
// create limbs for next row
let [z0, z2, z4, z6, z8, z10, z12, z14, z16, z18, z20] = exists(11, () => {
let zz = z.toBigInt();
return [
bitSlice(zz, 0, 2),
bitSlice(zz, 2, 2),
bitSlice(zz, 4, 2),
bitSlice(zz, 6, 2),
bitSlice(zz, 8, 2),
bitSlice(zz, 10, 2),
bitSlice(zz, 12, 2),
bitSlice(zz, 14, 2),
bitSlice(zz, 16, 2),
bitSlice(zz, 18, 2),
bitSlice(zz, 20, 2),
];
});
Gates.rangeCheck1(z, yz, [z86, z74, z62, z50, z38, z36, z34, z32, z30, z28, z26, z24, z22], [z20, z18, z16, x76, x64, y76, y64, z14, z12, z10, z8, z6, z4, z2, z0]);
}
/**
* Helper function that creates a new {@link Field} element from the first `length` bits of this {@link Field} element.
*
* This returns the `x` truncated to `length` bits. However, it does **not** prove this truncation or any
* other relation of the output with `x`.
*
* This only proves that the output value is in the range [0, 2^length), and so can be combined
* with the initial value to prove a range check.
*/
function rangeCheckHelper(length, x) {
assert(length <= Fp.sizeInBits, `bit length must be ${Fp.sizeInBits} or less, got ${length}`);
assert(length > 0, `bit length must be positive, got ${length}`);
assert(length % 16 === 0, '`length` has to be a multiple of 16.');
let lengthDiv16 = length / 16;
if (x.isConstant()) {
let bits = BinableFp.toBits(x.toBigInt())
.slice(0, length)
.concat(Array(Fp.sizeInBits - length).fill(false));
return createField(BinableFp.fromBits(bits));
}
let y = Snarky.field.truncateToBits16(lengthDiv16, x.value);
return createField(y);
}
/**
* Asserts that x is in the range [0, 2^n)
*/
function rangeCheckN(n, x, message = '') {
assert(n <= Fp.sizeInBits, `bit length must be ${Fp.sizeInBits} or less, got ${n}`);
assert(n > 0, `bit length must be positive, got ${n}`);
assert(n % 16 === 0, '`length` has to be a multiple of 16.');
if (x.isConstant()) {
if (x.toBigInt() >= 1n << BigInt(n)) {
throw Error(`rangeCheckN: expected field to fit in ${n} bits, got ${x}.\n${message}`);
}
return;
}
let actual = rangeCheckHelper(n, x);
actual.assertEquals(x, message);
}
/**
* Returns a boolean which, being true, proves that x is in the range [0, 2^n).
*
* **Beware**: The output being false does **not** prove that x is not in the range [0, 2^n).
* In other words, it can happen that this returns false even if x is in the range [0, 2^n).
*
* This should not be viewed as a standalone provable method but as an advanced helper function
* for gadgets which need a weakened form of range check.
*/
function isDefinitelyInRangeN(n, x) {
assert(n <= Fp.sizeInBits, `bit length must be ${Fp.sizeInBits} or less, got ${n}`);
assert(n > 0, `bit length must be positive, got ${n}`);
assert(n % 16 === 0, '`length` has to be a multiple of 16.');
if (x.isConstant()) {
return createBool(x.toBigInt() < 1n << BigInt(n));
}
let actual = rangeCheckHelper(n, x);
return actual.equals(x);
}
function rangeCheck16(x) {
if (x.isConstant()) {
assert(x.toBigInt() < 1n << 16n, `rangeCheck16: expected field to fit in 16 bits, got ${x}`);
return;
}
// check that x fits in 16 bits
rangeCheckHelper(16, x).assertEquals(x);
}
function rangeCheck8(x) {
if (x.isConstant()) {
assert(x.toBigInt() < 1n << 8n, `rangeCheck8: expected field to fit in 8 bits, got ${x}`);
return;
}
// check that x fits in 16 bits
rangeCheckHelper(16, x).assertEquals(x);
// check that 2^8 x fits in 16 bits
let x256 = x.mul(1 << 8).seal();
rangeCheckHelper(16, x256).assertEquals(x256);
}
function rangeCheckLessThan16(bits, x) {
assert(bits < 16, `bits must be less than 16, got ${bits}`);
if (x.isConstant()) {
assert(x.toBigInt() < 1n << BigInt(bits), `rangeCheckLessThan16: expected field to fit in ${bits} bits, got ${x}`);
return;
}
// check that x fits in 16 bits
rangeCheckHelper(16, x).assertEquals(x);
// check that 2^(16 - bits)*x < 2^16, i.e. x < 2^bits
let xM = x.mul(1 << (16 - bits)).seal();
rangeCheckHelper(16, xM).assertEquals(xM);
}
function rangeCheckLessThan64(bits, x) {
assert(bits < 64, `bits must be less than 64, got ${bits}`);
if (x.isConstant()) {
assert(x.toBigInt() < 1n << BigInt(bits), `rangeCheckLessThan16: expected field to fit in ${bits} bits, got ${x}`);
return;
}
// check that x fits in 64 bits
rangeCheck64(x);
// check that 2^(64 - bits)*x < 2^64, i.e. x < 2^bits
let xM = x.mul(1 << (64 - bits)).seal();
rangeCheck64(xM);
}
//# sourceMappingURL=range-check.js.map