From e9557c5246f22acc9a3315cbd1f29c8e9ae64e02 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 1 Nov 2023 16:42:05 +0100 Subject: [PATCH 01/10] add multi range check gadget --- src/lib/gadgets/range-check.ts | 123 ++++++++++++++++++++++++++++++++- src/lib/gates.ts | 20 +++++- 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/range-check.ts b/src/lib/gadgets/range-check.ts index 9a271c24af..f9476455aa 100644 --- a/src/lib/gadgets/range-check.ts +++ b/src/lib/gadgets/range-check.ts @@ -2,10 +2,10 @@ import { Field } from '../field.js'; import * as Gates from '../gates.js'; import { bitSlice, exists } from './common.js'; -export { rangeCheck64 }; +export { rangeCheck64, multiRangeCheck }; /** - * Asserts that x is in the range [0, 2^64), handles constant case + * Asserts that x is in the range [0, 2^64) */ function rangeCheck64(x: Field) { if (x.isConstant()) { @@ -48,3 +48,122 @@ function rangeCheck64(x: Field) { false // not using compact mode ); } + +/** + * Asserts that x, y, z \in [0, 2^88) + */ +function multiRangeCheck(x: Field, y: Field, z: Field) { + if (x.isConstant() && y.isConstant() && z.isConstant()) { + if ( + x.toBigInt() >= 1n << 88n || + y.toBigInt() >= 1n << 88n || + z.toBigInt() >= 1n << 88n + ) { + throw Error( + `multiRangeCheck: expected fields to fit in 88 bits, got ${x}, ${y}, ${z}` + ); + } + + let [x64, x76] = rangeCheck0Helper(x); + let [y64, y76] = rangeCheck0Helper(y); + rangeCheck1Helper({ x64, x76, y64, y76, z, yz: new Field(0) }); + } +} + +function rangeCheck0Helper(x: Field, isCompact = false): [Field, Field] { + // 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: { + x64: Field; + x76: Field; + y64: Field; + y76: Field; + z: Field; + yz: Field; +}) { + 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] + ); +} diff --git a/src/lib/gates.ts b/src/lib/gates.ts index 7dd88ae712..16bfe7e050 100644 --- a/src/lib/gates.ts +++ b/src/lib/gates.ts @@ -3,7 +3,7 @@ import { FieldConst, type Field } from './field.js'; import { MlArray, MlTuple } from './ml/base.js'; import { TupleN } from './util/types.js'; -export { rangeCheck0, xor, zero, rotate, generic }; +export { rangeCheck0, rangeCheck1, xor, zero, rotate, generic }; function rangeCheck0( x: Field, @@ -19,6 +19,24 @@ function rangeCheck0( ); } +/** + * the rangeCheck1 gate is used in combination with the rangeCheck0, + * for doing a 3x88-bit range check + */ +function rangeCheck1( + v2: Field, + v12: Field, + vCurr: TupleN, + vNext: TupleN +) { + Snarky.gates.rangeCheck1( + v2.value, + v12.value, + MlTuple.mapTo(vCurr, (x) => x.value), + MlTuple.mapTo(vNext, (x) => x.value) + ); +} + function rotate( field: Field, rotated: Field, From 3f93ac6066846d5ed50b853ba35aca1dbcad1d1d Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 1 Nov 2023 17:16:41 +0100 Subject: [PATCH 02/10] compact multi range check --- src/lib/gadgets/range-check.ts | 49 +++++++++++++++++++++++++++------- src/lib/util/types.ts | 8 +++--- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/lib/gadgets/range-check.ts b/src/lib/gadgets/range-check.ts index f9476455aa..16ba1bd640 100644 --- a/src/lib/gadgets/range-check.ts +++ b/src/lib/gadgets/range-check.ts @@ -2,7 +2,7 @@ import { Field } from '../field.js'; import * as Gates from '../gates.js'; import { bitSlice, exists } from './common.js'; -export { rangeCheck64, multiRangeCheck }; +export { rangeCheck64, multiRangeCheck, compactMultiRangeCheck }; /** * Asserts that x is in the range [0, 2^64) @@ -49,19 +49,18 @@ function rangeCheck64(x: Field) { ); } +// default bigint limb size +const L = 88n; +const twoL = 2n * L; +const lMask = (1n << L) - 1n; + /** * Asserts that x, y, z \in [0, 2^88) */ function multiRangeCheck(x: Field, y: Field, z: Field) { if (x.isConstant() && y.isConstant() && z.isConstant()) { - if ( - x.toBigInt() >= 1n << 88n || - y.toBigInt() >= 1n << 88n || - z.toBigInt() >= 1n << 88n - ) { - throw Error( - `multiRangeCheck: expected fields to fit in 88 bits, got ${x}, ${y}, ${z}` - ); + if (x.toBigInt() >> L || y.toBigInt() >> L || z.toBigInt() >> L) { + throw Error(`Expected fields to fit in ${L} bits, got ${x}, ${y}, ${z}`); } let [x64, x76] = rangeCheck0Helper(x); @@ -70,6 +69,38 @@ function multiRangeCheck(x: Field, y: Field, z: Field) { } } +/** + * 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: Field, z: Field): [Field, Field, Field] { + // constant case + if (xy.isConstant() && z.isConstant()) { + if (xy.toBigInt() >> twoL || z.toBigInt() >> L) { + throw Error( + `Expected fields to fit in ${twoL} and ${L} bits respectively, got ${xy}, ${z}` + ); + } + let [x, y] = splitCompactLimb(xy.toBigInt()); + return [new Field(x), new Field(y), 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: bigint): [bigint, bigint] { + return [x01 & lMask, x01 >> L]; +} + function rangeCheck0Helper(x: Field, isCompact = false): [Field, Field] { // crumbs (2-bit limbs) let [x0, x2, x4, x6, x8, x10, x12, x14] = exists(8, () => { diff --git a/src/lib/util/types.ts b/src/lib/util/types.ts index dad0611f4a..201824ec48 100644 --- a/src/lib/util/types.ts +++ b/src/lib/util/types.ts @@ -23,10 +23,10 @@ type TupleN = N extends N : never; const TupleN = { - map( - tuple: TupleN, - f: (a: T) => S - ): TupleN { + map, B>( + tuple: T, + f: (a: T[number]) => B + ): [...{ [i in keyof T]: B }] { return tuple.map(f) as any; }, From 4491f7c18c3203eea2c466d2fafc40f4a1be2994 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 1 Nov 2023 17:43:59 +0100 Subject: [PATCH 03/10] random distribution for positive bigints --- src/lib/testing/random.ts | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/lib/testing/random.ts b/src/lib/testing/random.ts index 3324908e45..1cae85806e 100644 --- a/src/lib/testing/random.ts +++ b/src/lib/testing/random.ts @@ -311,6 +311,7 @@ const Random = Object.assign(Random_, { uint32, uint64, biguint: biguintWithInvalid, + bignat: bignatWithInvalid, privateKey, publicKey, scalar, @@ -658,14 +659,14 @@ function int(min: number, max: number): Random { * log-uniform distribution over range [0, max] * with bias towards 0, 1, 2 */ -function nat(max: number): Random { - if (max < 0) throw Error('max < 0'); - if (max === 0) return constant(0); +function bignat(max: bigint): Random { + if (max < 0n) throw Error('max < 0'); + if (max === 0n) return constant(0n); let bits = max.toString(2).length; let bitBits = bits.toString(2).length; // set of special numbers that will appear more often in tests - let special = [0, 0, 1]; - if (max > 1) special.push(2); + let special = [0n, 0n, 1n]; + if (max > 1n) special.push(2n); let nSpecial = special.length; return { create: () => () => { @@ -681,13 +682,21 @@ function nat(max: number): Random { let bitLength = 1 + drawUniformUintBits(bitBits); if (bitLength > bits) continue; // draw number from [0, 2**bitLength); reject if > max - let n = drawUniformUintBits(bitLength); + let n = drawUniformBigUintBits(bitLength); if (n <= max) return n; } }, }; } +/** + * log-uniform distribution over range [0, max] + * with bias towards 0, 1, 2 + */ +function nat(max: number): Random { + return map(bignat(BigInt(max)), (n) => Number(n)); +} + function fraction(fixedPrecision = 3) { let denom = 10 ** fixedPrecision; if (fixedPrecision < 1) throw Error('precision must be > 1'); @@ -825,6 +834,15 @@ function biguintWithInvalid(bits: number): RandomWithInvalid { return Object.assign(valid, { invalid }); } +function bignatWithInvalid(max: bigint): RandomWithInvalid { + let valid = bignat(max); + let double = bignat(2n * max); + let negative = map(double, (uint) => -uint - 1n); + let tooLarge = map(valid, (uint) => uint + max); + let invalid = oneOf(negative, tooLarge); + return Object.assign(valid, { invalid }); +} + function fieldWithInvalid( F: typeof Field | typeof Scalar ): RandomWithInvalid { From 00814a310c50dc3d7b66e9945ce1ee96547748ec Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 1 Nov 2023 17:44:07 +0100 Subject: [PATCH 04/10] test multi range checks --- src/lib/gadgets/gadgets.ts | 24 +++++++- src/lib/gadgets/range-check.ts | 2 +- src/lib/gadgets/range-check.unit-test.ts | 78 +++++++++++++++++++----- 3 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 3ad0de8f43..ab76a96bdd 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -1,7 +1,11 @@ /** * Wrapper file for various gadgets, with a namespace and doccomments. */ -import { rangeCheck64 } from './range-check.js'; +import { + compactMultiRangeCheck, + multiRangeCheck, + rangeCheck64, +} from './range-check.js'; import { rotate, xor, and } from './bitwise.js'; import { Field } from '../core.js'; @@ -139,4 +143,22 @@ const Gadgets = { and(a: Field, b: Field, length: number) { return and(a, b, length); }, + + /** + * Multi-range check + * + * TODO + */ + multiRangeCheck(x: Field, y: Field, z: Field) { + multiRangeCheck(x, y, z); + }, + + /** + * Compact multi-range check + * + * TODO + */ + compactMultiRangeCheck(xy: Field, z: Field) { + return compactMultiRangeCheck(xy, z); + }, }; diff --git a/src/lib/gadgets/range-check.ts b/src/lib/gadgets/range-check.ts index 16ba1bd640..e64108bd49 100644 --- a/src/lib/gadgets/range-check.ts +++ b/src/lib/gadgets/range-check.ts @@ -2,7 +2,7 @@ import { Field } from '../field.js'; import * as Gates from '../gates.js'; import { bitSlice, exists } from './common.js'; -export { rangeCheck64, multiRangeCheck, compactMultiRangeCheck }; +export { rangeCheck64, multiRangeCheck, compactMultiRangeCheck, L }; /** * Asserts that x is in the range [0, 2^64) diff --git a/src/lib/gadgets/range-check.unit-test.ts b/src/lib/gadgets/range-check.unit-test.ts index 4466f5e187..3a732c7ca6 100644 --- a/src/lib/gadgets/range-check.unit-test.ts +++ b/src/lib/gadgets/range-check.unit-test.ts @@ -5,16 +5,23 @@ import { Spec, boolean, equivalentAsync, - field, + fieldWithRng, } from '../testing/equivalent.js'; import { Random } from '../testing/property.js'; +import { assert } from './common.js'; import { Gadgets } from './gadgets.js'; +import { L } from './range-check.js'; -let maybeUint64: Spec = { - ...field, - rng: Random.map(Random.oneOf(Random.uint64, Random.uint64.invalid), (x) => - mod(x, Field.ORDER) - ), +let uint = (n: number | bigint): Spec => { + let uint = Random.bignat((1n << BigInt(n)) - 1n); + return fieldWithRng(uint); +}; + +let maybeUint = (n: number | bigint): Spec => { + let uint = Random.bignat((1n << BigInt(n)) - 1n); + return fieldWithRng( + Random.map(Random.oneOf(uint, uint.invalid), (x) => mod(x, Field.ORDER)) + ); }; // TODO: make a ZkFunction or something that doesn't go through Pickles @@ -22,28 +29,71 @@ let maybeUint64: Spec = { // RangeCheck64 Gate // -------------------------- -let RangeCheck64 = ZkProgram({ - name: 'range-check-64', +let RangeCheck = ZkProgram({ + name: 'range-check', methods: { - run: { + check64: { privateInputs: [Field], method(x) { Gadgets.rangeCheck64(x); }, }, + checkMulti: { + privateInputs: [Field, Field, Field], + method(x, y, z) { + Gadgets.multiRangeCheck(x, y, z); + }, + }, + checkCompact: { + privateInputs: [Field, Field], + method(xy, z) { + let [x, y] = Gadgets.compactMultiRangeCheck(xy, z); + x.add(y.mul(1n << 176n)).assertEquals(xy); + }, + }, }, }); -await RangeCheck64.compile(); +await RangeCheck.compile(); // TODO: we use this as a test because there's no way to check custom gates quickly :( -await equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( + +await equivalentAsync({ from: [maybeUint(64)], to: boolean }, { runs: 3 })( (x) => { - if (x >= 1n << 64n) throw Error('expected 64 bits'); + assert(x < 1n << 64n); return true; }, async (x) => { - let proof = await RangeCheck64.run(x); - return await RangeCheck64.verify(proof); + let proof = await RangeCheck.check64(x); + return await RangeCheck.verify(proof); + } +); + +await equivalentAsync( + { from: [maybeUint(L), uint(L), uint(L)], to: boolean }, + { runs: 3 } +)( + (x, y, z) => { + console.log(x, y, z); + assert(!(x >> L) && !(y >> L) && !(z >> L)); + return true; + }, + async (x, y, z) => { + let proof = await RangeCheck.checkMulti(x, y, z); + return await RangeCheck.verify(proof); + } +); + +await equivalentAsync( + { from: [maybeUint(2n * L), uint(L)], to: boolean }, + { runs: 3 } +)( + (xy, z) => { + assert(!(xy >> (2n * L)) && !(z >> L)); + return true; + }, + async (xy, z) => { + let proof = await RangeCheck.checkCompact(xy, z); + return await RangeCheck.verify(proof); } ); From ca1cf2789ccce341dee6ba2a0b300ab56978bff9 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 1 Nov 2023 18:00:51 +0100 Subject: [PATCH 05/10] ouch - good that we have tests --- src/lib/gadgets/range-check.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib/gadgets/range-check.ts b/src/lib/gadgets/range-check.ts index e64108bd49..d48356d480 100644 --- a/src/lib/gadgets/range-check.ts +++ b/src/lib/gadgets/range-check.ts @@ -62,11 +62,12 @@ function multiRangeCheck(x: Field, y: Field, z: Field) { if (x.toBigInt() >> L || y.toBigInt() >> L || z.toBigInt() >> L) { throw Error(`Expected fields to fit in ${L} bits, got ${x}, ${y}, ${z}`); } - - let [x64, x76] = rangeCheck0Helper(x); - let [y64, y76] = rangeCheck0Helper(y); - rangeCheck1Helper({ x64, x76, y64, y76, z, yz: new Field(0) }); + return; } + + let [x64, x76] = rangeCheck0Helper(x); + let [y64, y76] = rangeCheck0Helper(y); + rangeCheck1Helper({ x64, x76, y64, y76, z, yz: new Field(0) }); } /** From c2a2e0a1c453e91467fc3b4ae610f40dc71db4da Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 1 Nov 2023 18:13:27 +0100 Subject: [PATCH 06/10] fixup test and add cs check --- src/lib/gadgets/range-check.unit-test.ts | 38 ++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/lib/gadgets/range-check.unit-test.ts b/src/lib/gadgets/range-check.unit-test.ts index 3a732c7ca6..0796183871 100644 --- a/src/lib/gadgets/range-check.unit-test.ts +++ b/src/lib/gadgets/range-check.unit-test.ts @@ -1,6 +1,8 @@ +import type { Gate } from '../../snarky.js'; import { mod } from '../../bindings/crypto/finite_field.js'; import { Field } from '../../lib/core.js'; import { ZkProgram } from '../proof_system.js'; +import { Provable } from '../provable.js'; import { Spec, boolean, @@ -8,9 +10,10 @@ import { fieldWithRng, } from '../testing/equivalent.js'; import { Random } from '../testing/property.js'; -import { assert } from './common.js'; +import { assert, exists } from './common.js'; import { Gadgets } from './gadgets.js'; import { L } from './range-check.js'; +import { expect } from 'expect'; let uint = (n: number | bigint): Spec => { let uint = Random.bignat((1n << BigInt(n)) - 1n); @@ -24,6 +27,32 @@ let maybeUint = (n: number | bigint): Spec => { ); }; +// constraint system sanity check + +function csWithoutGenerics(gates: Gate[]) { + return gates.map((g) => g.type).filter((type) => type !== 'Generic'); +} + +let check64 = Provable.constraintSystem(() => { + let [x] = exists(1, () => [0n]); + Gadgets.rangeCheck64(x); +}); +let multi = Provable.constraintSystem(() => { + let [x, y, z] = exists(3, () => [0n, 0n, 0n]); + Gadgets.multiRangeCheck(x, y, z); +}); +let compact = Provable.constraintSystem(() => { + let [xy, z] = exists(2, () => [0n, 0n]); + Gadgets.compactMultiRangeCheck(xy, z); +}); + +let expectedLayout64 = ['RangeCheck0']; +let expectedLayoutMulti = ['RangeCheck0', 'RangeCheck0', 'RangeCheck1', 'Zero']; + +expect(csWithoutGenerics(check64.gates)).toEqual(expectedLayout64); +expect(csWithoutGenerics(multi.gates)).toEqual(expectedLayoutMulti); +expect(csWithoutGenerics(compact.gates)).toEqual(expectedLayoutMulti); + // TODO: make a ZkFunction or something that doesn't go through Pickles // -------------------------- // RangeCheck64 Gate @@ -48,7 +77,7 @@ let RangeCheck = ZkProgram({ privateInputs: [Field, Field], method(xy, z) { let [x, y] = Gadgets.compactMultiRangeCheck(xy, z); - x.add(y.mul(1n << 176n)).assertEquals(xy); + x.add(y.mul(1n << L)).assertEquals(xy); }, }, }, @@ -74,8 +103,7 @@ await equivalentAsync( { runs: 3 } )( (x, y, z) => { - console.log(x, y, z); - assert(!(x >> L) && !(y >> L) && !(z >> L)); + assert(!(x >> L) && !(y >> L) && !(z >> L), 'multi: not out of range'); return true; }, async (x, y, z) => { @@ -89,7 +117,7 @@ await equivalentAsync( { runs: 3 } )( (xy, z) => { - assert(!(xy >> (2n * L)) && !(z >> L)); + assert(!(xy >> (2n * L)) && !(z >> L), 'compact: not out of range'); return true; }, async (xy, z) => { From f11d393c024ca2a697009244bf12534c27c8754d Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 1 Nov 2023 18:24:44 +0100 Subject: [PATCH 07/10] add some doccomments --- src/lib/gadgets/gadgets.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index ab76a96bdd..32ad29acb7 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -145,9 +145,15 @@ const Gadgets = { }, /** - * Multi-range check + * Multi-range check. * - * TODO + * Proves that x, y, z are all in the range [0, 2^88). + * + * This takes 4 rows, so it checks 88*3/4 = 66 bits per row. This is slightly more efficient + * than 64-bit range checks, which can do 64 bits in 1 row. + * + * In particular, the 3x88-bit range check supports bigints up to 264 bits, which in turn is enough + * to support foreign field multiplication with moduli up to 2^259. */ multiRangeCheck(x: Field, y: Field, z: Field) { multiRangeCheck(x, y, z); @@ -156,7 +162,15 @@ const Gadgets = { /** * Compact multi-range check * - * TODO + * This is a variant of {@link multiRangeCheck} where the first two variables are passed in + * combined form xy = x + 2^88*y. + * + * The gadget + * - splits up xy into x and y + * - proves that xy = x + 2^88*y + * - proves that x, y, z are all in the range [0, 2^88). + * + * The split form [x, y, z] is returned. */ compactMultiRangeCheck(xy: Field, z: Field) { return compactMultiRangeCheck(xy, z); From 302add7760f97912163cc7ec609f08c46d1bc9e3 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 1 Nov 2023 18:26:16 +0100 Subject: [PATCH 08/10] changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2588d44c1f..5df1ca6d4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased](https://github.com/o1-labs/o1js/compare/e8e7510e1...HEAD) -> No unreleased changes yet +### Added + +- `Gadgets.multiRangeCheck()` and `Gadgets.compactMultiRangeCheck()`, two building blocks for non-native arithmetic with bigints of size up to 264 bits. https://github.com/o1-labs/o1js/pull/1216 ## [0.14.0](https://github.com/o1-labs/o1js/compare/045faa7...e8e7510e1) From 1a666e86155275d731d59afd464164e351fc91b3 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 Nov 2023 15:50:51 +0100 Subject: [PATCH 09/10] match doc style of other gadgets and tweak function signature --- src/lib/gadgets/gadgets.ts | 22 ++++++++++++++++++---- src/lib/gadgets/range-check.ts | 8 ++++++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 570cd7b211..ab010921b0 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -221,9 +221,16 @@ const Gadgets = { * * In particular, the 3x88-bit range check supports bigints up to 264 bits, which in turn is enough * to support foreign field multiplication with moduli up to 2^259. + * + * @example + * ```ts + * Gadgets.multiRangeCheck([x, y, z]); + * ``` + * + * @throws Throws an error if one of the input values exceeds 88 bits. */ - multiRangeCheck(x: Field, y: Field, z: Field) { - multiRangeCheck(x, y, z); + multiRangeCheck(limbs: [Field, Field, Field]) { + multiRangeCheck(limbs); }, /** @@ -238,8 +245,15 @@ const Gadgets = { * - proves that x, y, z are all in the range [0, 2^88). * * The split form [x, y, z] is returned. + * + * @example + * ```ts + * let [x, y] = Gadgets.compactMultiRangeCheck([xy, z]); + * ``` + * + * @throws Throws an error if `xy` exceeds 2*88 = 176 bits, or if z exceeds 88 bits. */ - compactMultiRangeCheck(xy: Field, z: Field) { - return compactMultiRangeCheck(xy, z); + compactMultiRangeCheck(limbs: [Field, Field]) { + return compactMultiRangeCheck(limbs); }, }; diff --git a/src/lib/gadgets/range-check.ts b/src/lib/gadgets/range-check.ts index d48356d480..bc806f430e 100644 --- a/src/lib/gadgets/range-check.ts +++ b/src/lib/gadgets/range-check.ts @@ -57,7 +57,7 @@ const lMask = (1n << L) - 1n; /** * Asserts that x, y, z \in [0, 2^88) */ -function multiRangeCheck(x: Field, y: Field, z: Field) { +function multiRangeCheck([x, y, z]: [Field, Field, Field]) { 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}`); @@ -77,7 +77,11 @@ function multiRangeCheck(x: Field, y: Field, z: Field) { * * Returns the full limbs x, y, z */ -function compactMultiRangeCheck(xy: Field, z: Field): [Field, Field, Field] { +function compactMultiRangeCheck([xy, z]: [Field, Field]): [ + Field, + Field, + Field +] { // constant case if (xy.isConstant() && z.isConstant()) { if (xy.toBigInt() >> twoL || z.toBigInt() >> L) { From 9a594c902d40d221f2349ac6831b5e5a935fc529 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 Nov 2023 15:54:59 +0100 Subject: [PATCH 10/10] revert compact rc signature tweak, and fixup test --- src/lib/gadgets/gadgets.ts | 4 ++-- src/lib/gadgets/range-check.ts | 6 +----- src/lib/gadgets/range-check.unit-test.ts | 6 +++--- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index ab010921b0..00e11a7a72 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -253,7 +253,7 @@ const Gadgets = { * * @throws Throws an error if `xy` exceeds 2*88 = 176 bits, or if z exceeds 88 bits. */ - compactMultiRangeCheck(limbs: [Field, Field]) { - return compactMultiRangeCheck(limbs); + compactMultiRangeCheck(xy: Field, z: Field) { + return compactMultiRangeCheck(xy, z); }, }; diff --git a/src/lib/gadgets/range-check.ts b/src/lib/gadgets/range-check.ts index bc806f430e..1f4f369157 100644 --- a/src/lib/gadgets/range-check.ts +++ b/src/lib/gadgets/range-check.ts @@ -77,11 +77,7 @@ function multiRangeCheck([x, y, z]: [Field, Field, Field]) { * * Returns the full limbs x, y, z */ -function compactMultiRangeCheck([xy, z]: [Field, Field]): [ - Field, - Field, - Field -] { +function compactMultiRangeCheck(xy: Field, z: Field): [Field, Field, Field] { // constant case if (xy.isConstant() && z.isConstant()) { if (xy.toBigInt() >> twoL || z.toBigInt() >> L) { diff --git a/src/lib/gadgets/range-check.unit-test.ts b/src/lib/gadgets/range-check.unit-test.ts index 0796183871..c66c6a806d 100644 --- a/src/lib/gadgets/range-check.unit-test.ts +++ b/src/lib/gadgets/range-check.unit-test.ts @@ -38,8 +38,8 @@ let check64 = Provable.constraintSystem(() => { Gadgets.rangeCheck64(x); }); let multi = Provable.constraintSystem(() => { - let [x, y, z] = exists(3, () => [0n, 0n, 0n]); - Gadgets.multiRangeCheck(x, y, z); + let x = exists(3, () => [0n, 0n, 0n]); + Gadgets.multiRangeCheck(x); }); let compact = Provable.constraintSystem(() => { let [xy, z] = exists(2, () => [0n, 0n]); @@ -70,7 +70,7 @@ let RangeCheck = ZkProgram({ checkMulti: { privateInputs: [Field, Field, Field], method(x, y, z) { - Gadgets.multiRangeCheck(x, y, z); + Gadgets.multiRangeCheck([x, y, z]); }, }, checkCompact: {