From 4afed255dc7d6522a3e9d4be9b8f98177a18ff50 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 12 Oct 2023 14:54:06 -0700 Subject: [PATCH 01/79] chore(bindings): update submodule for rot gate --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index a6b9460a6f..1f7443d077 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit a6b9460a6f113bcf82912b6f75efbf9842a4b356 +Subproject commit 1f7443d0776382bf39bf13ea0becd59712abaf2f From 6bdcbbffe90d9aa997911ed5b651e7cb4af9a618 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 12 Oct 2023 14:54:26 -0700 Subject: [PATCH 02/79] feat(snarky.d.ts): add 'rot' function to Snarky interface to support rotation operations --- src/snarky.d.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 7a0f16f129..1b3cdec993 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -298,6 +298,25 @@ declare const Snarky: { ], compact: FieldConst ): void; + + rot( + word: FieldVar, + rotated: FieldVar, + excess: FieldVar, + limbs: [0, FieldVar, FieldVar, FieldVar, FieldVar], + crumbs: [ + 0, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar + ], + two_to_rot: FieldConst + ): void; }; bool: { From ddb9200e0be35ba2903d01d588a32358b8bedb5b Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 10:32:59 -0700 Subject: [PATCH 03/79] feat(gates.ts): add rot function --- src/lib/gates.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/lib/gates.ts b/src/lib/gates.ts index 503c4d2c6a..bb2d3b9750 100644 --- a/src/lib/gates.ts +++ b/src/lib/gates.ts @@ -1,7 +1,7 @@ import { Snarky } from '../snarky.js'; import { FieldVar, FieldConst, type Field } from './field.js'; -export { rangeCheck64 }; +export { rangeCheck64, rot }; /** * Asserts that x is at most 64 bits @@ -42,6 +42,27 @@ function rangeCheck64(x: Field) { ); } +function rot( + word: FieldVar, + rotated: FieldVar, + excess: FieldVar, + limbs: [0, FieldVar, FieldVar, FieldVar, FieldVar], + crumbs: [ + 0, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar + ], + two_to_rot: FieldConst +) { + Snarky.gates.rot(word, rotated, excess, limbs, crumbs, two_to_rot); +} + function getBits(x: bigint, start: number, length: number) { return FieldConst.fromBigint( (x >> BigInt(start)) & ((1n << BigInt(length)) - 1n) From 65d97bed322dac01be0eae6097840a9a51b1af36 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 10:35:31 -0700 Subject: [PATCH 04/79] feat(rot.ts): add new rotation function for bitwise operations This commit introduces a new function 'rot' in rot.ts file which performs bitwise rotation on a given word. It supports both left and right rotation modes. The function also includes a check for the number of bits to be rotated, ensuring it is within the range of 0 to 64. The rotation function is designed to work with both constant and provable words. --- src/lib/gadgets/rot.ts | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/lib/gadgets/rot.ts diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts new file mode 100644 index 0000000000..9cc37be4b2 --- /dev/null +++ b/src/lib/gadgets/rot.ts @@ -0,0 +1,44 @@ +import { Field } from '../field.js'; +import { Provable } from '../provable.js'; +import { Field as Fp } from '../../provable/field-bigint.js'; +import * as Gates from '../gates.js'; + +export { rot }; + +type RotationMode = 'left' | 'right'; + +function rot(word: Field, mode: RotationMode, bits: number) { + // Check that the rotation bits are in range + checkBits(bits); + + if (word.isConstant()) { + return rot_constant(word, mode, bits); + } else { + return rot_provable(word, mode, bits); + } +} + +function checkBits(bits: number) { + if (bits < 0 || bits > 64) { + throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); + } +} + +function rot_constant(word: Field, mode: RotationMode, bits: number) { + let x = word.toBigInt(); + if (mode === 'left') { + return (x << BigInt(bits)) % (1n << 64n); + } else { + return (x >> BigInt(bits)) % (1n << 64n); + } +} + +function rot_provable(word: Field, mode: RotationMode, bits: number) { + // Check that the input word is at most 64 bits. + // Compute actual length depending on whether the rotation mode is "left" or "right" + // Auxiliary BigInt values + // Compute rotated word + // Compute current row + // Compute next row + // Compute following row +} From e69a93fe84e4fa8a496410d899d26a1cc8754ede Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 12:09:46 -0700 Subject: [PATCH 05/79] chore(bindings): update bindings subproject to latest commit --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 1f7443d077..13cb203feb 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 1f7443d0776382bf39bf13ea0becd59712abaf2f +Subproject commit 13cb203febb168a47c82fa83013e7e4b4193d438 From e040fe7d0f46ed4fb67a7b7851061935a4681619 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 12:14:37 -0700 Subject: [PATCH 06/79] refactor(snarky.d.ts): replace hardcoded arrays with MlArray for limbs and crumbs --- src/snarky.d.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 1b3cdec993..d5be6ac756 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -303,18 +303,8 @@ declare const Snarky: { word: FieldVar, rotated: FieldVar, excess: FieldVar, - limbs: [0, FieldVar, FieldVar, FieldVar, FieldVar], - crumbs: [ - 0, - FieldVar, - FieldVar, - FieldVar, - FieldVar, - FieldVar, - FieldVar, - FieldVar, - FieldVar - ], + limbs: MlArray, + crumbs: MlArray, two_to_rot: FieldConst ): void; }; From 82dfa33cbf8fa1a097d9ce4f71dbc734553e0ee2 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 12:15:16 -0700 Subject: [PATCH 07/79] feat(gates.ts): add MlArray import to convert limbs and crumbs arrays to MlArray --- src/lib/gates.ts | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/lib/gates.ts b/src/lib/gates.ts index bb2d3b9750..175dc72afc 100644 --- a/src/lib/gates.ts +++ b/src/lib/gates.ts @@ -1,5 +1,6 @@ import { Snarky } from '../snarky.js'; import { FieldVar, FieldConst, type Field } from './field.js'; +import { MlArray } from './ml/base.js'; export { rangeCheck64, rot }; @@ -43,24 +44,21 @@ function rangeCheck64(x: Field) { } function rot( - word: FieldVar, - rotated: FieldVar, - excess: FieldVar, - limbs: [0, FieldVar, FieldVar, FieldVar, FieldVar], - crumbs: [ - 0, - FieldVar, - FieldVar, - FieldVar, - FieldVar, - FieldVar, - FieldVar, - FieldVar, - FieldVar - ], - two_to_rot: FieldConst + word: Field, + rotated: Field, + excess: Field, + limbs: [Field, Field, Field, Field], + crumbs: [Field, Field, Field, Field, Field, Field, Field, Field], + two_to_rot: Field ) { - Snarky.gates.rot(word, rotated, excess, limbs, crumbs, two_to_rot); + Snarky.gates.rot( + word.value, + rotated.value, + excess.value, + MlArray.to(limbs.map((x) => x.value)), + MlArray.to(crumbs.map((x) => x.value)), + FieldConst.fromBigint(two_to_rot.toBigInt()) + ); } function getBits(x: bigint, start: number, length: number) { From a63c0d317dfc32613e3b9c5fe570cc377dd36ac5 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 12:16:05 -0700 Subject: [PATCH 08/79] feat(rot.ts): implement provable rotation function --- src/lib/gadgets/rot.ts | 138 +++++++++++++++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 20 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 9cc37be4b2..10a45fc81a 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -1,44 +1,142 @@ import { Field } from '../field.js'; +import { UInt64 } from '../int.js'; import { Provable } from '../provable.js'; import { Field as Fp } from '../../provable/field-bigint.js'; import * as Gates from '../gates.js'; export { rot }; -type RotationMode = 'left' | 'right'; +const MAX_BITS = 64 as const; -function rot(word: Field, mode: RotationMode, bits: number) { +function rot(word: Field, direction: 'left' | 'right' = 'left', bits: number) { // Check that the rotation bits are in range - checkBits(bits); - - if (word.isConstant()) { - return rot_constant(word, mode, bits); - } else { - return rot_provable(word, mode, bits); - } -} - -function checkBits(bits: number) { if (bits < 0 || bits > 64) { throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); } -} -function rot_constant(word: Field, mode: RotationMode, bits: number) { - let x = word.toBigInt(); - if (mode === 'left') { - return (x << BigInt(bits)) % (1n << 64n); + if (word.isConstant()) { + return new Field(Fp.rot64(word.toBigInt(), bits, direction === 'left')); } else { - return (x >> BigInt(bits)) % (1n << 64n); + // Check that the input word is at most 64 bits. + UInt64.check(UInt64.from(word)); + return rot_provable(word, direction, bits); } } -function rot_provable(word: Field, mode: RotationMode, bits: number) { +function rot_provable( + word: Field, + direction: 'left' | 'right' = 'left', + bits: number +) { // Check that the input word is at most 64 bits. + Provable.asProver(() => { + if (word.toBigInt() < 2 ** MAX_BITS) { + throw Error( + `rot: expected word to be at most 64 bits, got ${word.toBigInt()}` + ); + } + }); + // Compute actual length depending on whether the rotation mode is "left" or "right" - // Auxiliary BigInt values + let rotationBits = bits; + if (direction === 'right') { + rotationBits = MAX_BITS - bits; + } + // Compute rotated word + const [rotated, excess, shifted, bound] = computeRotatedWord( + word, + rotationBits + ); + // Compute current row + Gates.rot( + word, + rotated, + excess, + [ + witnessSlices(bound, 52, 64), + witnessSlices(bound, 40, 52), + witnessSlices(bound, 28, 40), + witnessSlices(bound, 16, 28), + ], + [ + witnessSlices(bound, 14, 16), + witnessSlices(bound, 12, 14), + witnessSlices(bound, 10, 12), + witnessSlices(bound, 8, 10), + witnessSlices(bound, 6, 8), + witnessSlices(bound, 4, 6), + witnessSlices(bound, 2, 4), + witnessSlices(bound, 0, 2), + ], + Field.from(2n ** 64n) + ); // Compute next row + Gates.rangeCheck64(shifted); // Compute following row + Gates.rangeCheck64(excess); + return rotated; +} + +function computeRotatedWord(word: Field, rotationBits: number) { + // Auxiliary BigInt values + const big2Power64 = 2n ** 64n; + const big2PowerRot = 2n ** BigInt(rotationBits); + const wordBigInt = word.toBigInt(); + + return Provable.witness(Provable.Array(Field, 4), () => { + // Assert that the word is at most 64 bits. + if (wordBigInt < big2Power64) { + throw Error( + `rot: expected word to be at most 64 bits, got ${word.toBigInt()}` + ); + } + + // Obtain rotated output, excess, and shifted for the equation + // word * 2^rot = excess * 2^64 + shifted + const { quotient: excessBigInt, remainder: shiftedBigInt } = divRem( + wordBigInt * big2PowerRot, + big2Power64 + ); + + // Compute rotated value as + // rotated = excess + shifted + const rotatedBig = shiftedBigInt + excessBigInt; + + // Compute bound that is the right input of FFAdd equation + const boundBig = excessBigInt + big2Power64 - big2PowerRot; + + // Convert back to field + const shifted = Field.from(shiftedBigInt); + const excess = Field.from(excessBigInt); + const rotated = Field.from(rotatedBig); + const bound = Field.from(boundBig); + + return [rotated, excess, shifted, bound]; + }); +} + +function witnessSlices(f: Field, start: number, stop = -1) { + if (stop !== -1 && stop <= start) { + throw Error('stop must be greater than start'); + } + + return Provable.witness(Field, () => { + let bits = f.toBits(); + if (stop > bits.length) { + throw Error('stop must be less than bit-length'); + } + if (stop === -1) { + stop = bits.length; + } + + return Field.fromBits(bits.slice(start, stop)); + }); +} + +function divRem(numerator: bigint, denominator: bigint) { + const quotient = numerator / denominator; + const remainder = numerator - denominator * quotient; + return { quotient, remainder }; } From 19bfedfbd097fb05bd744606c683a5200b59a891 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 12:27:30 -0700 Subject: [PATCH 09/79] refactor(rot.ts): simplify rot function by removing unnecessary checks and conditions --- src/lib/gadgets/rot.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 10a45fc81a..6dd10f8adb 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -1,33 +1,12 @@ import { Field } from '../field.js'; -import { UInt64 } from '../int.js'; import { Provable } from '../provable.js'; -import { Field as Fp } from '../../provable/field-bigint.js'; import * as Gates from '../gates.js'; export { rot }; const MAX_BITS = 64 as const; -function rot(word: Field, direction: 'left' | 'right' = 'left', bits: number) { - // Check that the rotation bits are in range - if (bits < 0 || bits > 64) { - throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); - } - - if (word.isConstant()) { - return new Field(Fp.rot64(word.toBigInt(), bits, direction === 'left')); - } else { - // Check that the input word is at most 64 bits. - UInt64.check(UInt64.from(word)); - return rot_provable(word, direction, bits); - } -} - -function rot_provable( - word: Field, - direction: 'left' | 'right' = 'left', - bits: number -) { +function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { // Check that the input word is at most 64 bits. Provable.asProver(() => { if (word.toBigInt() < 2 ** MAX_BITS) { From 1909678d2d6320d5b102aead523c48c35c5e03c8 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 12:28:31 -0700 Subject: [PATCH 10/79] feat(field.ts): add rot64 method to Field class for bit rotation --- src/lib/field.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/lib/field.ts b/src/lib/field.ts index 29193d64db..5e0a8fd995 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -5,6 +5,7 @@ import type { NonNegativeInteger } from '../bindings/crypto/non-negative.js'; import { asProver, inCheckedComputation } from './provable-context.js'; import { Bool } from './bool.js'; import { assert } from './errors.js'; +import { rot } from './gadgets/rot.js'; // external API export { Field }; @@ -589,6 +590,33 @@ class Field { return new Field(z); } + /** + * A (left and right) rotation is similar to the shift operation, `<<` and `>>` in JavaScript, just that bits are being appended to the other side. + * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. + * + * **Note:** You can not rotate {@link Field} elements that exceed 64 bits. For elements that exceed 64 bits this operation will fail. + * + * ```typescript + * let a = Field(12); + * let b = a.rot(2, 'left'); // left rotation by 2 bit + * c.assertEquals(20); + * ``` + * + * @param bits amount of bits to rotate this {@link Field} element with. + * @param direction (true) left or (false) right rotation direction. + */ + rot64(bits: number, direction: 'left' | 'right' = 'left') { + // Check that the rotation bits are in range + if (bits < 0 || bits > 64) { + throw Error(`rot64: expected bits to be between 0 and 64, got ${bits}`); + } + if (this.isConstant()) { + return new Field(Fp.rot64(this.toBigInt(), bits, direction === 'left')); + } else { + return rot(this, bits, direction); + } + } + /** * @deprecated use `x.equals(0)` which is equivalent */ From a94394f1dfcc66068ebbd58728d3c172e38609ac Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 12:36:50 -0700 Subject: [PATCH 11/79] feat(field.unit-test.ts, int.ts): add rotation test --- src/lib/field.unit-test.ts | 18 ++++++++++++++++++ src/lib/int.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/lib/field.unit-test.ts b/src/lib/field.unit-test.ts index f28f688bb3..f4fe4e4c4a 100644 --- a/src/lib/field.unit-test.ts +++ b/src/lib/field.unit-test.ts @@ -35,6 +35,24 @@ test(Random.field, Random.json.field, (x, y, assert) => { assert(z.toJSON() === y); }); +// rotation +test( + Random.uint64, + Random.nat(64), + Random.boolean, + (x, n, direction, assert) => { + let z = Field(x); + let r1 = Fp.rot64(x, n, direction); + Provable.runAndCheck(() => { + let r2 = Provable.witness(Field, () => z).rot64( + n, + direction ? 'left' : 'right' + ); + Provable.asProver(() => assert(r1 === r2.toBigInt())); + }); + } +); + // handles small numbers test(Random.nat(1000), (n, assert) => { assert(Field(n).toString() === String(n)); diff --git a/src/lib/int.ts b/src/lib/int.ts index f4143a9a5b..5bef34beb2 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -371,6 +371,22 @@ class UInt64 extends CircuitValue { assertGreaterThanOrEqual(y: UInt64, message?: string) { y.assertLessThanOrEqual(this, message); } + + /** + * A (left and right) rotation is similar to the shift operation, `<<` and `>>` in JavaScript, just that bits are being appended to the other side. + * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. * + * ```typescript + * let a = UInt64.from(12); + * let b = a.rot(2, true); // left rotation by 2 bit + * c.assertEquals(20); + * ``` + * + * @param bits amount of bits to rotate this {@link UInt64} element with. + * @param direction (true) left or (false) right rotation direction. + */ + rot(bits: number, direction: 'left' | 'right' = 'left') { + return new UInt64(this.value.rot64(bits, direction)); + } } /** * A 32 bit unsigned integer with values ranging from 0 to 4,294,967,295. @@ -707,6 +723,22 @@ class UInt32 extends CircuitValue { assertGreaterThanOrEqual(y: UInt32, message?: string) { y.assertLessThanOrEqual(this, message); } + + /** + * A (left and right) rotation is similar to the shift operation, `<<` and `>>` in JavaScript, just that bits are being appended to the other side. + * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. * + * ```typescript + * let a = UInt32.from(12); + * let b = a.rot(2, true); // left rotation by 2 bit + * c.assertEquals(20); + * ``` + * + * @param bits amount of bits to rotate this {@link UInt64} element with. + * @param direction (true) left or (false) right rotation direction. + */ + rot(bits: number, direction: 'left' | 'right' = 'left') { + return new UInt32(this.value.rot64(bits, direction)); + } } class Sign extends CircuitValue { From eed078cf3a528613663c70ea82a3903e17a9aeef Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 13:13:32 -0700 Subject: [PATCH 12/79] fix(rot.ts): correct condition to check if word is more than 64 bits --- src/lib/field.ts | 4 ---- src/lib/gadgets/rot.ts | 20 +++++++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/lib/field.ts b/src/lib/field.ts index 5e0a8fd995..c919432261 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -606,10 +606,6 @@ class Field { * @param direction (true) left or (false) right rotation direction. */ rot64(bits: number, direction: 'left' | 'right' = 'left') { - // Check that the rotation bits are in range - if (bits < 0 || bits > 64) { - throw Error(`rot64: expected bits to be between 0 and 64, got ${bits}`); - } if (this.isConstant()) { return new Field(Fp.rot64(this.toBigInt(), bits, direction === 'left')); } else { diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 6dd10f8adb..64d2963c50 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -7,9 +7,14 @@ export { rot }; const MAX_BITS = 64 as const; function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { + // Check that the rotation bits are in range + if (bits < 0 || bits > 64) { + throw Error(`rot64: expected bits to be between 0 and 64, got ${bits}`); + } + // Check that the input word is at most 64 bits. Provable.asProver(() => { - if (word.toBigInt() < 2 ** MAX_BITS) { + if (word.toBigInt() > 2 ** MAX_BITS) { throw Error( `rot: expected word to be at most 64 bits, got ${word.toBigInt()}` ); @@ -59,14 +64,15 @@ function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { } function computeRotatedWord(word: Field, rotationBits: number) { - // Auxiliary BigInt values - const big2Power64 = 2n ** 64n; - const big2PowerRot = 2n ** BigInt(rotationBits); - const wordBigInt = word.toBigInt(); - return Provable.witness(Provable.Array(Field, 4), () => { + const wordBigInt = word.toBigInt(); + + // Auxiliary BigInt values + const big2Power64 = 2n ** 64n; + const big2PowerRot = 2n ** BigInt(rotationBits); + // Assert that the word is at most 64 bits. - if (wordBigInt < big2Power64) { + if (wordBigInt > big2Power64) { throw Error( `rot: expected word to be at most 64 bits, got ${word.toBigInt()}` ); From 60517f3316ce5f6ae7d41affc20a502bc7832178 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 13:19:46 -0700 Subject: [PATCH 13/79] docs(CHANGELOG.md): add entry for bitwise ROT operation support for native field elements --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eb508bf0b..466bc5df9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Internal support for several custom gates (range check, bitwise operations, foreign field operations) and lookup tables https://github.com/o1-labs/o1js/pull/1176 +- Added bitwise `ROT` operation support for native field elements. https://github.com/o1-labs/o1js/pull/1182 + ## [0.13.1](https://github.com/o1-labs/o1js/compare/c2f392fe5...045faa7) ### Breaking changes From b8cc337dab014b4beb24673cb792a99b3f1ccec6 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 15:46:51 -0700 Subject: [PATCH 14/79] refactor(rot.ts): replace hardcoded value 64 with constant MAX_BITS --- src/lib/gadgets/rot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 64d2963c50..5ab1169282 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -8,7 +8,7 @@ const MAX_BITS = 64 as const; function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { // Check that the rotation bits are in range - if (bits < 0 || bits > 64) { + if (bits < 0 || bits > MAX_BITS) { throw Error(`rot64: expected bits to be between 0 and 64, got ${bits}`); } From 3c8dd487d2c3835e7497c4009ebbc728b0011d80 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 15:47:24 -0700 Subject: [PATCH 15/79] refactor(rot.ts): extract rotation logic into separate rotate function --- src/lib/gadgets/rot.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 5ab1169282..6358420700 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -2,7 +2,7 @@ import { Field } from '../field.js'; import { Provable } from '../provable.js'; import * as Gates from '../gates.js'; -export { rot }; +export { rot, rotate }; const MAX_BITS = 64 as const; @@ -21,17 +21,8 @@ function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { } }); - // Compute actual length depending on whether the rotation mode is "left" or "right" - let rotationBits = bits; - if (direction === 'right') { - rotationBits = MAX_BITS - bits; - } - // Compute rotated word - const [rotated, excess, shifted, bound] = computeRotatedWord( - word, - rotationBits - ); + const [rotated, excess, shifted, bound] = rotate(word, bits, direction); // Compute current row Gates.rot( @@ -63,7 +54,17 @@ function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { return rotated; } -function computeRotatedWord(word: Field, rotationBits: number) { +function rotate( + word: Field, + bits: number, + direction: 'left' | 'right' = 'left' +) { + // Compute actual length depending on whether the rotation mode is "left" or "right" + let rotationBits = bits; + if (direction === 'right') { + rotationBits = MAX_BITS - bits; + } + return Provable.witness(Provable.Array(Field, 4), () => { const wordBigInt = word.toBigInt(); From eba9209e851cd8f18a67ce88c3716e179cdd6d6a Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 15:48:06 -0700 Subject: [PATCH 16/79] fix(rot.ts): correct error message to match function name 'rot' --- src/lib/gadgets/rot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 6358420700..193985cbbe 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -9,7 +9,7 @@ const MAX_BITS = 64 as const; function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { // Check that the rotation bits are in range if (bits < 0 || bits > MAX_BITS) { - throw Error(`rot64: expected bits to be between 0 and 64, got ${bits}`); + throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); } // Check that the input word is at most 64 bits. From 8945d7f1d2093d189a7d20196701dd584dffef09 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 15:56:20 -0700 Subject: [PATCH 17/79] refactor(rot.ts): simplify computation and conversion of rotated, excess, shifted, bound values for readability --- src/lib/gadgets/rot.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 193985cbbe..9c54b7ffa2 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -67,7 +67,6 @@ function rotate( return Provable.witness(Provable.Array(Field, 4), () => { const wordBigInt = word.toBigInt(); - // Auxiliary BigInt values const big2Power64 = 2n ** 64n; const big2PowerRot = 2n ** BigInt(rotationBits); @@ -81,25 +80,17 @@ function rotate( // Obtain rotated output, excess, and shifted for the equation // word * 2^rot = excess * 2^64 + shifted - const { quotient: excessBigInt, remainder: shiftedBigInt } = divRem( + const { quotient: excess, remainder: shifted } = divideWithRemainder( wordBigInt * big2PowerRot, big2Power64 ); - // Compute rotated value as // rotated = excess + shifted - const rotatedBig = shiftedBigInt + excessBigInt; - + const rotated = shifted + excess; // Compute bound that is the right input of FFAdd equation - const boundBig = excessBigInt + big2Power64 - big2PowerRot; - - // Convert back to field - const shifted = Field.from(shiftedBigInt); - const excess = Field.from(excessBigInt); - const rotated = Field.from(rotatedBig); - const bound = Field.from(boundBig); + const bound = excess + big2Power64 - big2PowerRot; - return [rotated, excess, shifted, bound]; + return [rotated, excess, shifted, bound].map(Field.from); }); } @@ -121,7 +112,7 @@ function witnessSlices(f: Field, start: number, stop = -1) { }); } -function divRem(numerator: bigint, denominator: bigint) { +function divideWithRemainder(numerator: bigint, denominator: bigint) { const quotient = numerator / denominator; const remainder = numerator - denominator * quotient; return { quotient, remainder }; From 6745559171b92615ae12a90a5b08f1c751861303 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 16:10:19 -0700 Subject: [PATCH 18/79] refactor: rename rot64 method to rot in field.ts, field.unit-test.ts, int.ts --- src/lib/field.ts | 4 ++-- src/lib/field.unit-test.ts | 4 ++-- src/lib/int.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/field.ts b/src/lib/field.ts index c919432261..db7ed19e61 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -605,9 +605,9 @@ class Field { * @param bits amount of bits to rotate this {@link Field} element with. * @param direction (true) left or (false) right rotation direction. */ - rot64(bits: number, direction: 'left' | 'right' = 'left') { + rot(bits: number, direction: 'left' | 'right' = 'left') { if (this.isConstant()) { - return new Field(Fp.rot64(this.toBigInt(), bits, direction === 'left')); + return new Field(Fp.rot(this.toBigInt(), bits, direction === 'left')); } else { return rot(this, bits, direction); } diff --git a/src/lib/field.unit-test.ts b/src/lib/field.unit-test.ts index f4fe4e4c4a..7f23e76d7a 100644 --- a/src/lib/field.unit-test.ts +++ b/src/lib/field.unit-test.ts @@ -42,9 +42,9 @@ test( Random.boolean, (x, n, direction, assert) => { let z = Field(x); - let r1 = Fp.rot64(x, n, direction); + let r1 = Fp.rot(x, n, direction); Provable.runAndCheck(() => { - let r2 = Provable.witness(Field, () => z).rot64( + let r2 = Provable.witness(Field, () => z).rot( n, direction ? 'left' : 'right' ); diff --git a/src/lib/int.ts b/src/lib/int.ts index 5bef34beb2..3fd35c3006 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -385,7 +385,7 @@ class UInt64 extends CircuitValue { * @param direction (true) left or (false) right rotation direction. */ rot(bits: number, direction: 'left' | 'right' = 'left') { - return new UInt64(this.value.rot64(bits, direction)); + return new UInt64(this.value.rot(bits, direction)); } } /** @@ -737,7 +737,7 @@ class UInt32 extends CircuitValue { * @param direction (true) left or (false) right rotation direction. */ rot(bits: number, direction: 'left' | 'right' = 'left') { - return new UInt32(this.value.rot64(bits, direction)); + return new UInt32(this.value.rot(bits, direction)); } } From e0bcb0f6a43703e860500101967a419f80728d53 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 16:14:12 -0700 Subject: [PATCH 19/79] feat(field.ts, int.ts): add default values for bits parameter in rot method to improve usability This change allows users to call the rot method without specifying the bits parameter, which will default to 64 for Field class, UInt64.NUM_BITS for UInt64 class, and UInt32.NUM_BITS for UInt32 class. --- src/lib/field.ts | 2 +- src/lib/int.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/field.ts b/src/lib/field.ts index db7ed19e61..00a6aaf0d1 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -605,7 +605,7 @@ class Field { * @param bits amount of bits to rotate this {@link Field} element with. * @param direction (true) left or (false) right rotation direction. */ - rot(bits: number, direction: 'left' | 'right' = 'left') { + rot(bits: number = 64, direction: 'left' | 'right' = 'left') { if (this.isConstant()) { return new Field(Fp.rot(this.toBigInt(), bits, direction === 'left')); } else { diff --git a/src/lib/int.ts b/src/lib/int.ts index 3fd35c3006..ea84a413ac 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -384,7 +384,7 @@ class UInt64 extends CircuitValue { * @param bits amount of bits to rotate this {@link UInt64} element with. * @param direction (true) left or (false) right rotation direction. */ - rot(bits: number, direction: 'left' | 'right' = 'left') { + rot(bits: number = UInt64.NUM_BITS, direction: 'left' | 'right' = 'left') { return new UInt64(this.value.rot(bits, direction)); } } @@ -736,7 +736,7 @@ class UInt32 extends CircuitValue { * @param bits amount of bits to rotate this {@link UInt64} element with. * @param direction (true) left or (false) right rotation direction. */ - rot(bits: number, direction: 'left' | 'right' = 'left') { + rot(bits: number = UInt32.NUM_BITS, direction: 'left' | 'right' = 'left') { return new UInt32(this.value.rot(bits, direction)); } } From e8e0c51c25a189865b135688f9c1bc61fc5bf279 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 16:21:46 -0700 Subject: [PATCH 20/79] fix(field.ts, int.ts): correct the documentation for the rot function - Update the example output to match the correct result of the operation - Change the direction parameter description from boolean to 'left' or 'right' string values for better readability and understanding of the function usage --- src/lib/field.ts | 4 ++-- src/lib/int.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib/field.ts b/src/lib/field.ts index 00a6aaf0d1..6471d02456 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -599,11 +599,11 @@ class Field { * ```typescript * let a = Field(12); * let b = a.rot(2, 'left'); // left rotation by 2 bit - * c.assertEquals(20); + * b.assertEquals(48); * ``` * * @param bits amount of bits to rotate this {@link Field} element with. - * @param direction (true) left or (false) right rotation direction. + * @param direction left or right rotation direction. */ rot(bits: number = 64, direction: 'left' | 'right' = 'left') { if (this.isConstant()) { diff --git a/src/lib/int.ts b/src/lib/int.ts index ea84a413ac..6ea5f4dd75 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -377,12 +377,12 @@ class UInt64 extends CircuitValue { * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. * * ```typescript * let a = UInt64.from(12); - * let b = a.rot(2, true); // left rotation by 2 bit - * c.assertEquals(20); + * let b = a.rot(2, 'left'); // left rotation by 2 bit + * b.assertEquals(48); * ``` * * @param bits amount of bits to rotate this {@link UInt64} element with. - * @param direction (true) left or (false) right rotation direction. + * @param direction left or right rotation direction. */ rot(bits: number = UInt64.NUM_BITS, direction: 'left' | 'right' = 'left') { return new UInt64(this.value.rot(bits, direction)); @@ -729,12 +729,12 @@ class UInt32 extends CircuitValue { * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. * * ```typescript * let a = UInt32.from(12); - * let b = a.rot(2, true); // left rotation by 2 bit - * c.assertEquals(20); + * let b = a.rot(2, 'left'); // left rotation by 2 bit + * b.assertEquals(48); * ``` * * @param bits amount of bits to rotate this {@link UInt64} element with. - * @param direction (true) left or (false) right rotation direction. + * @param direction left or right rotation direction. */ rot(bits: number = UInt32.NUM_BITS, direction: 'left' | 'right' = 'left') { return new UInt32(this.value.rot(bits, direction)); From 6f3a6c568c3f8706ba0369e8f19a52ed73acae67 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 16:30:05 -0700 Subject: [PATCH 21/79] feat(gadgets.ts): add example e2e test --- src/examples/gadgets.ts | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/examples/gadgets.ts diff --git a/src/examples/gadgets.ts b/src/examples/gadgets.ts new file mode 100644 index 0000000000..1bddf250c3 --- /dev/null +++ b/src/examples/gadgets.ts @@ -0,0 +1,54 @@ +import { Field, Provable, Experimental } from 'o1js'; + +Provable.runAndCheck(() => { + let f = Field(12); + let res = f.rot(2, 'left'); + Provable.log(res); + res.assertEquals(Field(48)); +}); + +let cs = Provable.constraintSystem(() => { + let res1 = Provable.witness(Field, () => Field(12).rot(2, 'left')); + let res2 = Provable.witness(Field, () => Field(12).rot(2, 'right')); + + res1.assertEquals(Field(48)); + res2.assertEquals(Field(3)); + + Provable.log(res1); + Provable.log(res2); +}); +console.log(cs); + +const ROT = Experimental.ZkProgram({ + methods: { + baseCase: { + privateInputs: [], + method: () => { + let a = Provable.witness(Field, () => Field(48)); + let actualLeft = a.rot(2, 'left'); + let actualRight = a.rot(2, 'right'); + + let expectedLeft = Field(192); + actualLeft.assertEquals(expectedLeft); + + let expectedRight = 12; + actualRight.assertEquals(expectedRight); + }, + }, + }, +}); + +console.log('compiling..'); + +console.time('compile'); +await ROT.compile(); +console.timeEnd('compile'); + +console.log('proving..'); + +console.time('prove'); +let proof = await ROT.baseCase(); +console.timeEnd('prove'); + +if (!(await ROT.verify(proof))) throw Error('Invalid proof'); +else console.log('proof valid'); From cd024e55abc97ca648b84ba1b2d9fefe9e2351a1 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 17:52:31 -0700 Subject: [PATCH 22/79] refactor(rot.ts): split rot function into rot and rotate --- src/lib/gadgets/rot.ts | 86 +++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 9c54b7ffa2..d50ea03e1e 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -7,11 +7,26 @@ export { rot, rotate }; const MAX_BITS = 64 as const; function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { + const [rotated, ,] = rotate(word, bits, direction); + return rotated; +} + +function rotate( + word: Field, + bits: number, + direction: 'left' | 'right' = 'left' +): [Field, Field, Field] { // Check that the rotation bits are in range if (bits < 0 || bits > MAX_BITS) { throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); } + if (direction !== 'left' && direction !== 'right') { + throw Error( + `rot: expected direction to be 'left' or 'right', got ${direction}` + ); + } + // Check that the input word is at most 64 bits. Provable.asProver(() => { if (word.toBigInt() > 2 ** MAX_BITS) { @@ -21,8 +36,31 @@ function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { } }); - // Compute rotated word - const [rotated, excess, shifted, bound] = rotate(word, bits, direction); + const rotationBits = direction === 'right' ? MAX_BITS - bits : bits; + const big2Power64 = 2n ** 64n; + const big2PowerRot = 2n ** BigInt(rotationBits); + + const [rotated, excess, shifted, bound] = Provable.witness( + Provable.Array(Field, 4), + () => { + const wordBigInt = word.toBigInt(); + + // Obtain rotated output, excess, and shifted for the equation + // word * 2^rot = excess * 2^64 + shifted + const { quotient: excess, remainder: shifted } = divideWithRemainder( + wordBigInt * big2PowerRot, + big2Power64 + ); + + // Compute rotated value as + // rotated = excess + shifted + const rotated = shifted + excess; + // Compute bound that is the right input of FFAdd equation + const bound = excess + big2Power64 - big2PowerRot; + + return [rotated, excess, shifted, bound].map(Field.from); + } + ); // Compute current row Gates.rot( @@ -45,53 +83,13 @@ function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { witnessSlices(bound, 2, 4), witnessSlices(bound, 0, 2), ], - Field.from(2n ** 64n) + Field.from(big2PowerRot) ); // Compute next row Gates.rangeCheck64(shifted); // Compute following row Gates.rangeCheck64(excess); - return rotated; -} - -function rotate( - word: Field, - bits: number, - direction: 'left' | 'right' = 'left' -) { - // Compute actual length depending on whether the rotation mode is "left" or "right" - let rotationBits = bits; - if (direction === 'right') { - rotationBits = MAX_BITS - bits; - } - - return Provable.witness(Provable.Array(Field, 4), () => { - const wordBigInt = word.toBigInt(); - // Auxiliary BigInt values - const big2Power64 = 2n ** 64n; - const big2PowerRot = 2n ** BigInt(rotationBits); - - // Assert that the word is at most 64 bits. - if (wordBigInt > big2Power64) { - throw Error( - `rot: expected word to be at most 64 bits, got ${word.toBigInt()}` - ); - } - - // Obtain rotated output, excess, and shifted for the equation - // word * 2^rot = excess * 2^64 + shifted - const { quotient: excess, remainder: shifted } = divideWithRemainder( - wordBigInt * big2PowerRot, - big2Power64 - ); - // Compute rotated value as - // rotated = excess + shifted - const rotated = shifted + excess; - // Compute bound that is the right input of FFAdd equation - const bound = excess + big2Power64 - big2PowerRot; - - return [rotated, excess, shifted, bound].map(Field.from); - }); + return [rotated, excess, shifted]; } function witnessSlices(f: Field, start: number, stop = -1) { From 704c2d900bceb573fdab1102ef67ddb7bb54801f Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 18:04:17 -0700 Subject: [PATCH 23/79] feat(gadgets.ts): add more test cases --- src/examples/gadgets.ts | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/examples/gadgets.ts b/src/examples/gadgets.ts index 1bddf250c3..1ff2c67ebd 100644 --- a/src/examples/gadgets.ts +++ b/src/examples/gadgets.ts @@ -1,11 +1,34 @@ import { Field, Provable, Experimental } from 'o1js'; -Provable.runAndCheck(() => { - let f = Field(12); - let res = f.rot(2, 'left'); - Provable.log(res); - res.assertEquals(Field(48)); -}); +function testRot( + word: Field, + bits: number, + mode: 'left' | 'right', + result: Field +) { + Provable.runAndCheck(() => { + let w = Provable.witness(Field, () => word); + let r = Provable.witness(Field, () => result); + let output = w.rot(bits, mode); + output.assertEquals(r); + }); +} + +console.log('Running positive tests...'); +testRot(Field(0), 0, 'left', Field(0)); +testRot(Field(0), 32, 'right', Field(0)); +testRot(Field(1), 1, 'left', Field(2)); +testRot(Field(1), 63, 'left', Field(9223372036854775808)); +testRot(Field(256), 4, 'right', Field(16)); + +// TODO: fix this test +// testRot(Field(6510615555426900570), 4, 'left', Field(11936128518282651045)); +//testRot(Field(6510615555426900570), 4, 'right', Field(11936128518282651045)); + +testRot(Field(1234567890), 32, 'right', Field(5302428712241725440)); +testRot(Field(2651214356120862720), 32, 'right', Field(617283945)); +testRot(Field(1153202983878524928), 32, 'right', Field(268500993)); +console.log('πŸŽ‰ Passed positive tests'); let cs = Provable.constraintSystem(() => { let res1 = Provable.witness(Field, () => Field(12).rot(2, 'left')); @@ -17,7 +40,7 @@ let cs = Provable.constraintSystem(() => { Provable.log(res1); Provable.log(res2); }); -console.log(cs); +console.log('constraint system: ', cs); const ROT = Experimental.ZkProgram({ methods: { From fb347bd23726cff2d1a4ae314ffc415073c144fc Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 18:05:55 -0700 Subject: [PATCH 24/79] chore(rot.ts): minor refactors --- src/lib/gadgets/rot.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index d50ea03e1e..1a99afcc8e 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -7,7 +7,7 @@ export { rot, rotate }; const MAX_BITS = 64 as const; function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { - const [rotated, ,] = rotate(word, bits, direction); + const [rotated] = rotate(word, bits, direction); return rotated; } @@ -45,19 +45,18 @@ function rotate( () => { const wordBigInt = word.toBigInt(); - // Obtain rotated output, excess, and shifted for the equation + // Obtain rotated output, excess, and shifted for the equation: // word * 2^rot = excess * 2^64 + shifted const { quotient: excess, remainder: shifted } = divideWithRemainder( wordBigInt * big2PowerRot, big2Power64 ); - // Compute rotated value as + // Compute rotated value as: // rotated = excess + shifted const rotated = shifted + excess; // Compute bound that is the right input of FFAdd equation const bound = excess + big2Power64 - big2PowerRot; - return [rotated, excess, shifted, bound].map(Field.from); } ); From 98bd547be9c4af5979cf092954e4d0a56e6635dc Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 16 Oct 2023 18:19:17 -0700 Subject: [PATCH 25/79] feat(gadgets.ts): enhance testRot function to log output for better debugging --- src/examples/gadgets.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/examples/gadgets.ts b/src/examples/gadgets.ts index 1ff2c67ebd..cba262e689 100644 --- a/src/examples/gadgets.ts +++ b/src/examples/gadgets.ts @@ -10,7 +10,10 @@ function testRot( let w = Provable.witness(Field, () => word); let r = Provable.witness(Field, () => result); let output = w.rot(bits, mode); - output.assertEquals(r); + Provable.asProver(() => { + Provable.log(`rot(${word}, ${bits}, ${mode}) = ${output}`); + }); + output.assertEquals(r, `rot(${word}, ${bits}, ${mode})`); }); } @@ -22,7 +25,8 @@ testRot(Field(1), 63, 'left', Field(9223372036854775808)); testRot(Field(256), 4, 'right', Field(16)); // TODO: fix this test -// testRot(Field(6510615555426900570), 4, 'left', Field(11936128518282651045)); +// 0x5A5A5A5A5A5A5A5A is 0xA5A5A5A5A5A5A5A5 both when rotate 4 bits left or right +// testRot(Field(6510615555426900570), 4, 'right', Field(11936128518282651045)); //testRot(Field(6510615555426900570), 4, 'right', Field(11936128518282651045)); testRot(Field(1234567890), 32, 'right', Field(5302428712241725440)); From cc2d15882b68250d80c20154814c6da23d08a123 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 09:24:07 -0700 Subject: [PATCH 26/79] refactor(gadgets.ts): change testRot function parameters from number to string to avoid precision loss in JavaScript --- src/examples/gadgets.ts | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/examples/gadgets.ts b/src/examples/gadgets.ts index cba262e689..06c6c41b79 100644 --- a/src/examples/gadgets.ts +++ b/src/examples/gadgets.ts @@ -18,20 +18,26 @@ function testRot( } console.log('Running positive tests...'); -testRot(Field(0), 0, 'left', Field(0)); -testRot(Field(0), 32, 'right', Field(0)); -testRot(Field(1), 1, 'left', Field(2)); -testRot(Field(1), 63, 'left', Field(9223372036854775808)); -testRot(Field(256), 4, 'right', Field(16)); - -// TODO: fix this test -// 0x5A5A5A5A5A5A5A5A is 0xA5A5A5A5A5A5A5A5 both when rotate 4 bits left or right -// testRot(Field(6510615555426900570), 4, 'right', Field(11936128518282651045)); -//testRot(Field(6510615555426900570), 4, 'right', Field(11936128518282651045)); - -testRot(Field(1234567890), 32, 'right', Field(5302428712241725440)); -testRot(Field(2651214356120862720), 32, 'right', Field(617283945)); -testRot(Field(1153202983878524928), 32, 'right', Field(268500993)); +testRot(Field('0'), 0, 'left', Field('0')); +testRot(Field('0'), 32, 'right', Field('0')); +testRot(Field('1'), 1, 'left', Field('2')); +testRot(Field('1'), 63, 'left', Field('9223372036854775808')); +testRot(Field('256'), 4, 'right', Field('16')); +testRot(Field('1234567890'), 32, 'right', Field('5302428712241725440')); +testRot(Field('2651214356120862720'), 32, 'right', Field('617283945')); +testRot(Field('1153202983878524928'), 32, 'right', Field('268500993')); +testRot( + Field('6510615555426900570'), + 4, + 'right', + Field('11936128518282651045') +); +testRot( + Field('6510615555426900570'), + 4, + 'right', + Field('11936128518282651045') +); console.log('πŸŽ‰ Passed positive tests'); let cs = Provable.constraintSystem(() => { From b7698a6c85b05005fa29fc203abdb1b1e4f5738c Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 09:24:21 -0700 Subject: [PATCH 27/79] refactor(rot.ts): update comment for clarity on the role of the prover in checking the input word length --- src/lib/gadgets/rot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 1a99afcc8e..d651f05cae 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -27,7 +27,7 @@ function rotate( ); } - // Check that the input word is at most 64 bits. + // Check as the prover, that the input word is at most 64 bits. Provable.asProver(() => { if (word.toBigInt() > 2 ** MAX_BITS) { throw Error( From 1e1f46e102fb724b5d3736d2d4e11741fe3c6ff3 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 09:39:36 -0700 Subject: [PATCH 28/79] chore(bindings): update subproject commit hash to latest version for up-to-date dependencies --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 13cb203feb..62ebd762bb 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 13cb203febb168a47c82fa83013e7e4b4193d438 +Subproject commit 62ebd762bbc3f5698ec01f202d36a97a4689d375 From b7b01a3f826f13d46d6213b7d41b09ef267806e1 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 09:44:53 -0700 Subject: [PATCH 29/79] feat(gadgets.ts): add rot function to Gadgets namespace for bit rotation The rot function provides left and right bit rotation functionality. It accepts a Field element, the number of bits to rotate, and the direction of rotation. This function will throw an error if the Field element exceeds 64 bits. --- src/lib/gadgets/gadgets.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index b4c799b2cb..3566a2d2f1 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -2,6 +2,7 @@ * Wrapper file for various gadgets, with a namespace and doccomments. */ import { rangeCheck64 } from './range-check.js'; +import { rot } from './rot.js'; import { Field } from '../core.js'; export { Gadgets }; @@ -33,4 +34,27 @@ const Gadgets = { rangeCheck64(x: Field) { return rangeCheck64(x); }, + + /** + * A (left and right) rotation is similar to the shift operation, `<<` and `>>` in JavaScript, just that bits are being appended to the other side. + * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. + * + * **Note:** You can not rotate {@link Field} elements that exceed 64 bits. For elements that exceed 64 bits this operation will fail. + * + * @param word {@link Field} element to rotate. + * @param bits amount of bits to rotate this {@link Field} element with. + * @param direction left or right rotation direction. + * + * @throws Throws an error if the input value exceeds 64 bits. + * + * @example + * ```typescript + * let a = Field(12); + * let b = a.rot(2, 'left'); // left rotation by 2 bit + * b.assertEquals(48); + * ``` + */ + rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { + return rot(word, bits, direction); + }, }; From 3f1de405f6056cf07445448bdd62336eab838d8d Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 09:45:54 -0700 Subject: [PATCH 30/79] docs(field.ts, int.ts): rearrange and add more details to the rot method documentation --- src/lib/field.ts | 9 ++++++--- src/lib/int.ts | 32 ++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/lib/field.ts b/src/lib/field.ts index 6471d02456..089b11d53c 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -596,14 +596,17 @@ class Field { * * **Note:** You can not rotate {@link Field} elements that exceed 64 bits. For elements that exceed 64 bits this operation will fail. * + * @param bits amount of bits to rotate this {@link Field} element with. + * @param direction left or right rotation direction. + * + * @throws Throws an error if the input value exceeds 64 bits. + * + * @example * ```typescript * let a = Field(12); * let b = a.rot(2, 'left'); // left rotation by 2 bit * b.assertEquals(48); * ``` - * - * @param bits amount of bits to rotate this {@link Field} element with. - * @param direction left or right rotation direction. */ rot(bits: number = 64, direction: 'left' | 'right' = 'left') { if (this.isConstant()) { diff --git a/src/lib/int.ts b/src/lib/int.ts index 6ea5f4dd75..7f348eb06a 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -374,15 +374,21 @@ class UInt64 extends CircuitValue { /** * A (left and right) rotation is similar to the shift operation, `<<` and `>>` in JavaScript, just that bits are being appended to the other side. - * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. * + * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. + * + * **Note:** You can not rotate {@link Field} elements that exceed 64 bits. For elements that exceed 64 bits this operation will fail. + * + * @param bits amount of bits to rotate this {@link Field} element with. + * @param direction left or right rotation direction. + * + * @throws Throws an error if the input value exceeds 64 bits. + * + * @example * ```typescript - * let a = UInt64.from(12); + * let a = Field(12); * let b = a.rot(2, 'left'); // left rotation by 2 bit * b.assertEquals(48); * ``` - * - * @param bits amount of bits to rotate this {@link UInt64} element with. - * @param direction left or right rotation direction. */ rot(bits: number = UInt64.NUM_BITS, direction: 'left' | 'right' = 'left') { return new UInt64(this.value.rot(bits, direction)); @@ -726,15 +732,21 @@ class UInt32 extends CircuitValue { /** * A (left and right) rotation is similar to the shift operation, `<<` and `>>` in JavaScript, just that bits are being appended to the other side. - * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. * + * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. + * + * **Note:** You can not rotate {@link Field} elements that exceed 64 bits. For elements that exceed 64 bits this operation will fail. + * + * @param bits amount of bits to rotate this {@link Field} element with. + * @param direction left or right rotation direction. + * + * @throws Throws an error if the input value exceeds 64 bits. + * + * @example * ```typescript - * let a = UInt32.from(12); + * let a = Field(12); * let b = a.rot(2, 'left'); // left rotation by 2 bit * b.assertEquals(48); * ``` - * - * @param bits amount of bits to rotate this {@link UInt64} element with. - * @param direction left or right rotation direction. */ rot(bits: number = UInt32.NUM_BITS, direction: 'left' | 'right' = 'left') { return new UInt32(this.value.rot(bits, direction)); From c0210c3cc9cb69d0d83429deecb70116547168a9 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 10:03:48 -0700 Subject: [PATCH 31/79] feat(gadgets.unit-test.ts): add ROT ZkProgram and its equivalentAsync test --- src/lib/gadgets/gadgets.unit-test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts index 13a44a059d..aec2477b1b 100644 --- a/src/lib/gadgets/gadgets.unit-test.ts +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -23,7 +23,20 @@ let RangeCheck64 = ZkProgram({ }, }); +let ROT = ZkProgram({ + methods: { + run: { + privateInputs: [Field], + method(x) { + Gadgets.rot(x, 2, 'left'); + Gadgets.rot(x, 2, 'right'); + }, + }, + }, +}); + await RangeCheck64.compile(); +await ROT.compile(); let maybeUint64: Spec = { ...field, @@ -45,3 +58,14 @@ equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( return await RangeCheck64.verify(proof); } ); + +equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( + (x) => { + if (x >= 1n << 64n) throw Error('expected 64 bits'); + return true; + }, + async (x) => { + let proof = await ROT.run(x); + return await ROT.verify(proof); + } +); From 22050e10da62e6f6218e3769318a156e1f993cc9 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 10:06:49 -0700 Subject: [PATCH 32/79] feat: add rot tests to primitive_constraint_system and vk_regression --- src/examples/primitive_constraint_system.ts | 13 ++++++++++++- src/examples/vk_regression.ts | 3 ++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/examples/primitive_constraint_system.ts b/src/examples/primitive_constraint_system.ts index 17735faa04..961ff1788b 100644 --- a/src/examples/primitive_constraint_system.ts +++ b/src/examples/primitive_constraint_system.ts @@ -1,4 +1,4 @@ -import { Field, Group, Poseidon, Provable, Scalar } from 'o1js'; +import { Field, Group, Poseidon, Gadgets, Provable, Scalar } from 'o1js'; function mock(obj: { [K: string]: (...args: any) => void }, name: string) { let methodKeys = Object.keys(obj); @@ -63,4 +63,15 @@ const GroupMock = { }, }; +const BitwiseMock = { + rot() { + let a = Provable.witness(Field, () => new Field(12)); + Gadgets.rot(a, 2, 'left'); + Gadgets.rot(a, 2, 'right'); + Gadgets.rot(a, 4, 'left'); + Gadgets.rot(a, 4, 'left'); + }, +}; + export const GroupCS = mock(GroupMock, 'Group Primitive'); +export const BitwiseCS = mock(BitwiseMock, 'Bitwise Primitive'); diff --git a/src/examples/vk_regression.ts b/src/examples/vk_regression.ts index 6dbcd31373..a76b41aef4 100644 --- a/src/examples/vk_regression.ts +++ b/src/examples/vk_regression.ts @@ -3,7 +3,7 @@ import { Voting_ } from './zkapps/voting/voting.js'; import { Membership_ } from './zkapps/voting/membership.js'; import { HelloWorld } from './zkapps/hello_world/hello_world.js'; import { TokenContract, createDex } from './zkapps/dex/dex.js'; -import { GroupCS } from './primitive_constraint_system.js'; +import { GroupCS, BitwiseCS } from './primitive_constraint_system.js'; // toggle this for quick iteration when debugging vk regressions const skipVerificationKeys = false; @@ -37,6 +37,7 @@ const ConstraintSystems: MinimumConstraintSystem[] = [ TokenContract, createDex().Dex, GroupCS, + BitwiseCS, ]; let filePath = jsonPath ? jsonPath : './src/examples/regression_test.json'; From ea73c6a457efb2376156708b099c774f904d50c1 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 10:08:38 -0700 Subject: [PATCH 33/79] feat(regression_test.json): add 'Bitwise Primitive' test case to extend test coverage --- src/examples/regression_test.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/examples/regression_test.json b/src/examples/regression_test.json index e492f2d163..f664f15780 100644 --- a/src/examples/regression_test.json +++ b/src/examples/regression_test.json @@ -164,5 +164,18 @@ "data": "", "hash": "" } + }, + "Bitwise Primitive": { + "digest": "Bitwise Primitive", + "methods": { + "rot": { + "rows": 13, + "digest": "a84b4cdef2d61ddab70f47b788d096d4" + } + }, + "verificationKey": { + "data": "", + "hash": "" + } } } \ No newline at end of file From 95a4559781bf047fc2644173e44289b9a2ce85c8 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 10:09:25 -0700 Subject: [PATCH 34/79] Revert "feat(field.ts, int.ts): add default values for bits parameter in rot method to improve usability" This reverts commit e0bcb0f6a43703e860500101967a419f80728d53. --- src/lib/field.ts | 2 +- src/lib/int.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/field.ts b/src/lib/field.ts index 089b11d53c..44ad0ad6b1 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -608,7 +608,7 @@ class Field { * b.assertEquals(48); * ``` */ - rot(bits: number = 64, direction: 'left' | 'right' = 'left') { + rot(bits: number, direction: 'left' | 'right' = 'left') { if (this.isConstant()) { return new Field(Fp.rot(this.toBigInt(), bits, direction === 'left')); } else { diff --git a/src/lib/int.ts b/src/lib/int.ts index 7f348eb06a..25bc76bc28 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -390,7 +390,7 @@ class UInt64 extends CircuitValue { * b.assertEquals(48); * ``` */ - rot(bits: number = UInt64.NUM_BITS, direction: 'left' | 'right' = 'left') { + rot(bits: number, direction: 'left' | 'right' = 'left') { return new UInt64(this.value.rot(bits, direction)); } } @@ -748,7 +748,7 @@ class UInt32 extends CircuitValue { * b.assertEquals(48); * ``` */ - rot(bits: number = UInt32.NUM_BITS, direction: 'left' | 'right' = 'left') { + rot(bits: number, direction: 'left' | 'right' = 'left') { return new UInt32(this.value.rot(bits, direction)); } } From 8f53db3ea0a24bcdcdf49fd2c0431beae1e15012 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 10:31:27 -0700 Subject: [PATCH 35/79] feat(rot): fix unit tests by only using Gadgets namespace for public API --- src/examples/gadgets.ts | 18 ++++++++++------ src/lib/field.ts | 27 ----------------------- src/lib/field.unit-test.ts | 7 +++--- src/lib/gadgets/gadgets.ts | 10 +++++---- src/lib/gadgets/rot.ts | 4 ++++ src/lib/int.ts | 44 -------------------------------------- 6 files changed, 25 insertions(+), 85 deletions(-) diff --git a/src/examples/gadgets.ts b/src/examples/gadgets.ts index 06c6c41b79..21a9c0f48b 100644 --- a/src/examples/gadgets.ts +++ b/src/examples/gadgets.ts @@ -1,4 +1,4 @@ -import { Field, Provable, Experimental } from 'o1js'; +import { Field, Provable, Experimental, Gadgets } from 'o1js'; function testRot( word: Field, @@ -9,7 +9,7 @@ function testRot( Provable.runAndCheck(() => { let w = Provable.witness(Field, () => word); let r = Provable.witness(Field, () => result); - let output = w.rot(bits, mode); + let output = Gadgets.rot(w, bits, mode); Provable.asProver(() => { Provable.log(`rot(${word}, ${bits}, ${mode}) = ${output}`); }); @@ -41,8 +41,14 @@ testRot( console.log('πŸŽ‰ Passed positive tests'); let cs = Provable.constraintSystem(() => { - let res1 = Provable.witness(Field, () => Field(12).rot(2, 'left')); - let res2 = Provable.witness(Field, () => Field(12).rot(2, 'right')); + let res1 = Provable.witness(Field, () => { + let f = Field(12); + return Gadgets.rot(f, 2, 'left'); + }); + let res2 = Provable.witness(Field, () => { + let f = Field(12); + return Gadgets.rot(f, 2, 'right'); + }); res1.assertEquals(Field(48)); res2.assertEquals(Field(3)); @@ -58,8 +64,8 @@ const ROT = Experimental.ZkProgram({ privateInputs: [], method: () => { let a = Provable.witness(Field, () => Field(48)); - let actualLeft = a.rot(2, 'left'); - let actualRight = a.rot(2, 'right'); + let actualLeft = Gadgets.rot(a, 2, 'left'); + let actualRight = Gadgets.rot(a, 2, 'right'); let expectedLeft = Field(192); actualLeft.assertEquals(expectedLeft); diff --git a/src/lib/field.ts b/src/lib/field.ts index 44ad0ad6b1..29193d64db 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -5,7 +5,6 @@ import type { NonNegativeInteger } from '../bindings/crypto/non-negative.js'; import { asProver, inCheckedComputation } from './provable-context.js'; import { Bool } from './bool.js'; import { assert } from './errors.js'; -import { rot } from './gadgets/rot.js'; // external API export { Field }; @@ -590,32 +589,6 @@ class Field { return new Field(z); } - /** - * A (left and right) rotation is similar to the shift operation, `<<` and `>>` in JavaScript, just that bits are being appended to the other side. - * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. - * - * **Note:** You can not rotate {@link Field} elements that exceed 64 bits. For elements that exceed 64 bits this operation will fail. - * - * @param bits amount of bits to rotate this {@link Field} element with. - * @param direction left or right rotation direction. - * - * @throws Throws an error if the input value exceeds 64 bits. - * - * @example - * ```typescript - * let a = Field(12); - * let b = a.rot(2, 'left'); // left rotation by 2 bit - * b.assertEquals(48); - * ``` - */ - rot(bits: number, direction: 'left' | 'right' = 'left') { - if (this.isConstant()) { - return new Field(Fp.rot(this.toBigInt(), bits, direction === 'left')); - } else { - return rot(this, bits, direction); - } - } - /** * @deprecated use `x.equals(0)` which is equivalent */ diff --git a/src/lib/field.unit-test.ts b/src/lib/field.unit-test.ts index 6706e7b7fd..28e17b90eb 100644 --- a/src/lib/field.unit-test.ts +++ b/src/lib/field.unit-test.ts @@ -17,6 +17,7 @@ import { bool, Spec, } from './testing/equivalent.js'; +import { Gadgets } from './gadgets/gadgets.js'; // types Field satisfies Provable; @@ -53,10 +54,8 @@ test( let z = Field(x); let r1 = Fp.rot(x, n, direction); Provable.runAndCheck(() => { - let r2 = Provable.witness(Field, () => z).rot( - n, - direction ? 'left' : 'right' - ); + let f = Provable.witness(Field, () => z); + let r2 = Gadgets.rot(f, n, direction ? 'left' : 'right'); Provable.asProver(() => assert(r1 === r2.toBigInt())); }); } diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 3566a2d2f1..bf13001128 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -48,10 +48,12 @@ const Gadgets = { * @throws Throws an error if the input value exceeds 64 bits. * * @example - * ```typescript - * let a = Field(12); - * let b = a.rot(2, 'left'); // left rotation by 2 bit - * b.assertEquals(48); + * ```ts + * const x = Provable.witness(Field, () => Field(12)); + * const y = rot(x, 2, 'left'); // left rotation by 2 bit + * const z = rot(x, 2, 'right'); // right rotation by 2 bit + * y.assertEquals(48); + * z.assertEquals(3) * ``` */ rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index d651f05cae..e7763b7ff1 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -1,5 +1,6 @@ import { Field } from '../field.js'; import { Provable } from '../provable.js'; +import { Fp } from '../../bindings/crypto/finite_field.js'; import * as Gates from '../gates.js'; export { rot, rotate }; @@ -7,6 +8,9 @@ export { rot, rotate }; const MAX_BITS = 64 as const; function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { + if (word.isConstant()) { + return new Field(Fp.rot(word.toBigInt(), bits, direction === 'left')); + } const [rotated] = rotate(word, bits, direction); return rotated; } diff --git a/src/lib/int.ts b/src/lib/int.ts index 25bc76bc28..f4143a9a5b 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -371,28 +371,6 @@ class UInt64 extends CircuitValue { assertGreaterThanOrEqual(y: UInt64, message?: string) { y.assertLessThanOrEqual(this, message); } - - /** - * A (left and right) rotation is similar to the shift operation, `<<` and `>>` in JavaScript, just that bits are being appended to the other side. - * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. - * - * **Note:** You can not rotate {@link Field} elements that exceed 64 bits. For elements that exceed 64 bits this operation will fail. - * - * @param bits amount of bits to rotate this {@link Field} element with. - * @param direction left or right rotation direction. - * - * @throws Throws an error if the input value exceeds 64 bits. - * - * @example - * ```typescript - * let a = Field(12); - * let b = a.rot(2, 'left'); // left rotation by 2 bit - * b.assertEquals(48); - * ``` - */ - rot(bits: number, direction: 'left' | 'right' = 'left') { - return new UInt64(this.value.rot(bits, direction)); - } } /** * A 32 bit unsigned integer with values ranging from 0 to 4,294,967,295. @@ -729,28 +707,6 @@ class UInt32 extends CircuitValue { assertGreaterThanOrEqual(y: UInt32, message?: string) { y.assertLessThanOrEqual(this, message); } - - /** - * A (left and right) rotation is similar to the shift operation, `<<` and `>>` in JavaScript, just that bits are being appended to the other side. - * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. - * - * **Note:** You can not rotate {@link Field} elements that exceed 64 bits. For elements that exceed 64 bits this operation will fail. - * - * @param bits amount of bits to rotate this {@link Field} element with. - * @param direction left or right rotation direction. - * - * @throws Throws an error if the input value exceeds 64 bits. - * - * @example - * ```typescript - * let a = Field(12); - * let b = a.rot(2, 'left'); // left rotation by 2 bit - * b.assertEquals(48); - * ``` - */ - rot(bits: number, direction: 'left' | 'right' = 'left') { - return new UInt32(this.value.rot(bits, direction)); - } } class Sign extends CircuitValue { From d86a4c81adc4a89cc3929a83a84b53fe52ee1a88 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 10:39:47 -0700 Subject: [PATCH 36/79] chore(rot): minor doc and vk updates --- src/examples/primitive_constraint_system.ts | 2 +- src/examples/regression_test.json | 2 +- src/lib/gadgets/gadgets.ts | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/examples/primitive_constraint_system.ts b/src/examples/primitive_constraint_system.ts index 961ff1788b..c680a1855b 100644 --- a/src/examples/primitive_constraint_system.ts +++ b/src/examples/primitive_constraint_system.ts @@ -69,7 +69,7 @@ const BitwiseMock = { Gadgets.rot(a, 2, 'left'); Gadgets.rot(a, 2, 'right'); Gadgets.rot(a, 4, 'left'); - Gadgets.rot(a, 4, 'left'); + Gadgets.rot(a, 4, 'right'); }, }; diff --git a/src/examples/regression_test.json b/src/examples/regression_test.json index f664f15780..a4eadbd725 100644 --- a/src/examples/regression_test.json +++ b/src/examples/regression_test.json @@ -170,7 +170,7 @@ "methods": { "rot": { "rows": 13, - "digest": "a84b4cdef2d61ddab70f47b788d096d4" + "digest": "2c0dadbba96fd7ddb9adb7d643425ce3" } }, "verificationKey": { diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index bf13001128..01fe77b052 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -50,10 +50,13 @@ const Gadgets = { * @example * ```ts * const x = Provable.witness(Field, () => Field(12)); - * const y = rot(x, 2, 'left'); // left rotation by 2 bit - * const z = rot(x, 2, 'right'); // right rotation by 2 bit + * const y = rot(x, 2, 'left'); // left rotation by 2 bits + * const z = rot(x, 2, 'right'); // right rotation by 2 bits * y.assertEquals(48); * z.assertEquals(3) + * + * const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n)); + * rot(xLarge, 32, "left"); // throws an error since input exceeds 64 bits * ``` */ rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { From 39328bfec96a953376d998d44d3f43d1176310a1 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 10:53:13 -0700 Subject: [PATCH 37/79] chore(bindings): update bindings for compiled ROT artifacts --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 62ebd762bb..038a16eac1 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 62ebd762bbc3f5698ec01f202d36a97a4689d375 +Subproject commit 038a16eac1bf5dc62779a079b95fa36e21aa4202 From 300427cb1de6ce36903f9996234e745db5f68947 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 11:12:56 -0700 Subject: [PATCH 38/79] feat(rot): minor refactor to rot gate checks and add range check to rotated word --- src/lib/gadgets/rot.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index e7763b7ff1..c1763d1cbb 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -8,6 +8,17 @@ export { rot, rotate }; const MAX_BITS = 64 as const; function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { + // Check that the rotation bits are in range + if (bits < 0 || bits > MAX_BITS) { + throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); + } + + if (direction !== 'left' && direction !== 'right') { + throw Error( + `rot: expected direction to be 'left' or 'right', got ${direction}` + ); + } + if (word.isConstant()) { return new Field(Fp.rot(word.toBigInt(), bits, direction === 'left')); } @@ -20,17 +31,6 @@ function rotate( bits: number, direction: 'left' | 'right' = 'left' ): [Field, Field, Field] { - // Check that the rotation bits are in range - if (bits < 0 || bits > MAX_BITS) { - throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); - } - - if (direction !== 'left' && direction !== 'right') { - throw Error( - `rot: expected direction to be 'left' or 'right', got ${direction}` - ); - } - // Check as the prover, that the input word is at most 64 bits. Provable.asProver(() => { if (word.toBigInt() > 2 ** MAX_BITS) { @@ -41,7 +41,7 @@ function rotate( }); const rotationBits = direction === 'right' ? MAX_BITS - bits : bits; - const big2Power64 = 2n ** 64n; + const big2Power64 = 2n ** BigInt(MAX_BITS); const big2PowerRot = 2n ** BigInt(rotationBits); const [rotated, excess, shifted, bound] = Provable.witness( @@ -92,6 +92,7 @@ function rotate( Gates.rangeCheck64(shifted); // Compute following row Gates.rangeCheck64(excess); + Gates.rangeCheck64(rotated); return [rotated, excess, shifted]; } From cd2c3444202fe515edebb353a2f55ec131d96317 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 11:17:15 -0700 Subject: [PATCH 39/79] fix(gadgets.unit-test.ts): add await keyword to equivalentAsync function calls to ensure tests run asynchronously and complete before proceeding --- src/lib/gadgets/gadgets.unit-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts index aec2477b1b..11def1e147 100644 --- a/src/lib/gadgets/gadgets.unit-test.ts +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -48,7 +48,7 @@ let maybeUint64: Spec = { // do a couple of proofs // TODO: we use this as a test because there's no way to check custom gates quickly :( -equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( +await equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( (x) => { if (x >= 1n << 64n) throw Error('expected 64 bits'); return true; @@ -59,7 +59,7 @@ equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( } ); -equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( +await equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( (x) => { if (x >= 1n << 64n) throw Error('expected 64 bits'); return true; From 7de9b160346ad499badc9981536f7647d0e152e0 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 17 Oct 2023 11:35:17 -0700 Subject: [PATCH 40/79] fix(regression_test.json): update 'rows' and 'digest' values in 'rot' method to reflect recent changes in the test data --- src/examples/regression_test.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/examples/regression_test.json b/src/examples/regression_test.json index a4eadbd725..ef26739ab3 100644 --- a/src/examples/regression_test.json +++ b/src/examples/regression_test.json @@ -169,8 +169,8 @@ "digest": "Bitwise Primitive", "methods": { "rot": { - "rows": 13, - "digest": "2c0dadbba96fd7ddb9adb7d643425ce3" + "rows": 17, + "digest": "5253728d9fd357808be58a39c8375c07" } }, "verificationKey": { From a77d0a801ea8c4a6825dd82487ccc6df43f95f2d Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 18 Oct 2023 09:27:44 -0700 Subject: [PATCH 41/79] refactor(rot.ts): simplify witnessSlices function --- src/lib/gadgets/rot.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index c1763d1cbb..c1b45bcd0c 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -96,21 +96,12 @@ function rotate( return [rotated, excess, shifted]; } -function witnessSlices(f: Field, start: number, stop = -1) { - if (stop !== -1 && stop <= start) { - throw Error('stop must be greater than start'); - } +function witnessSlices(f: Field, start: number, length: number) { + if (length <= 0) throw Error('Length must be a positive number'); return Provable.witness(Field, () => { - let bits = f.toBits(); - if (stop > bits.length) { - throw Error('stop must be less than bit-length'); - } - if (stop === -1) { - stop = bits.length; - } - - return Field.fromBits(bits.slice(start, stop)); + let n = f.toBigInt(); + return new Field((n >> BigInt(start)) & ((1n << BigInt(length)) - 1n)); }); } From fdc1061e0aac86d6af81d592023f051a70b52c4e Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 18 Oct 2023 09:38:38 -0700 Subject: [PATCH 42/79] Revert "refactor(rot.ts): simplify witnessSlices function" This reverts commit a77d0a801ea8c4a6825dd82487ccc6df43f95f2d. --- src/lib/gadgets/rot.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index c1b45bcd0c..c1763d1cbb 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -96,12 +96,21 @@ function rotate( return [rotated, excess, shifted]; } -function witnessSlices(f: Field, start: number, length: number) { - if (length <= 0) throw Error('Length must be a positive number'); +function witnessSlices(f: Field, start: number, stop = -1) { + if (stop !== -1 && stop <= start) { + throw Error('stop must be greater than start'); + } return Provable.witness(Field, () => { - let n = f.toBigInt(); - return new Field((n >> BigInt(start)) & ((1n << BigInt(length)) - 1n)); + let bits = f.toBits(); + if (stop > bits.length) { + throw Error('stop must be less than bit-length'); + } + if (stop === -1) { + stop = bits.length; + } + + return Field.fromBits(bits.slice(start, stop)); }); } From e795fb46984017737b029f9cb55d37db6f655b0c Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Sat, 21 Oct 2023 16:58:02 -0700 Subject: [PATCH 43/79] chore(bindings): update subproject commit hash to aa7f880 for latest changes in the subproject --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 038a16eac1..aa7f880eb7 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 038a16eac1bf5dc62779a079b95fa36e21aa4202 +Subproject commit aa7f880eb752c91408c90a40cf7e8bcb7dec87ff From 044267254a643d2f7e2e18811ae6d8d0625c15f7 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Sat, 21 Oct 2023 17:08:58 -0700 Subject: [PATCH 44/79] refactor(gadgets.ts): move testRot function and related tests to gadgets.unit-test.ts --- src/examples/gadgets.ts | 40 ------------- src/lib/gadgets/gadgets.unit-test.ts | 86 ++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/examples/gadgets.ts b/src/examples/gadgets.ts index 21a9c0f48b..345d5b9d71 100644 --- a/src/examples/gadgets.ts +++ b/src/examples/gadgets.ts @@ -1,45 +1,5 @@ import { Field, Provable, Experimental, Gadgets } from 'o1js'; -function testRot( - word: Field, - bits: number, - mode: 'left' | 'right', - result: Field -) { - Provable.runAndCheck(() => { - let w = Provable.witness(Field, () => word); - let r = Provable.witness(Field, () => result); - let output = Gadgets.rot(w, bits, mode); - Provable.asProver(() => { - Provable.log(`rot(${word}, ${bits}, ${mode}) = ${output}`); - }); - output.assertEquals(r, `rot(${word}, ${bits}, ${mode})`); - }); -} - -console.log('Running positive tests...'); -testRot(Field('0'), 0, 'left', Field('0')); -testRot(Field('0'), 32, 'right', Field('0')); -testRot(Field('1'), 1, 'left', Field('2')); -testRot(Field('1'), 63, 'left', Field('9223372036854775808')); -testRot(Field('256'), 4, 'right', Field('16')); -testRot(Field('1234567890'), 32, 'right', Field('5302428712241725440')); -testRot(Field('2651214356120862720'), 32, 'right', Field('617283945')); -testRot(Field('1153202983878524928'), 32, 'right', Field('268500993')); -testRot( - Field('6510615555426900570'), - 4, - 'right', - Field('11936128518282651045') -); -testRot( - Field('6510615555426900570'), - 4, - 'right', - Field('11936128518282651045') -); -console.log('πŸŽ‰ Passed positive tests'); - let cs = Provable.constraintSystem(() => { let res1 = Provable.witness(Field, () => { let f = Field(12); diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts index 11def1e147..f758a57be6 100644 --- a/src/lib/gadgets/gadgets.unit-test.ts +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -1,5 +1,5 @@ import { mod } from '../../bindings/crypto/finite_field.js'; -import { Field } from '../field.js'; +import { Field } from '../../lib/core.js'; import { ZkProgram } from '../proof_system.js'; import { Spec, @@ -9,8 +9,19 @@ import { } from '../testing/equivalent.js'; import { Random } from '../testing/random.js'; import { Gadgets } from './gadgets.js'; +import { Provable } from '../provable.js'; + +let maybeUint64: Spec = { + ...field, + rng: Random.map(Random.oneOf(Random.uint64, Random.uint64.invalid), (x) => + mod(x, Field.ORDER) + ), +}; // TODO: make a ZkFunction or something that doesn't go through Pickles +// -------------------------- +// RangeCheck64 Gate +// -------------------------- let RangeCheck64 = ZkProgram({ methods: { @@ -23,31 +34,9 @@ let RangeCheck64 = ZkProgram({ }, }); -let ROT = ZkProgram({ - methods: { - run: { - privateInputs: [Field], - method(x) { - Gadgets.rot(x, 2, 'left'); - Gadgets.rot(x, 2, 'right'); - }, - }, - }, -}); - await RangeCheck64.compile(); -await ROT.compile(); -let maybeUint64: Spec = { - ...field, - rng: Random.map(Random.oneOf(Random.uint64, Random.uint64.invalid), (x) => - mod(x, Field.ORDER) - ), -}; - -// do a couple of proofs // 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 })( (x) => { if (x >= 1n << 64n) throw Error('expected 64 bits'); @@ -59,6 +48,22 @@ await equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( } ); +// -------------------------- +// ROT Gate +// -------------------------- +let ROT = ZkProgram({ + methods: { + run: { + privateInputs: [Field], + method(x) { + Gadgets.rot(x, 2, 'left'); + Gadgets.rot(x, 2, 'right'); + }, + }, + }, +}); + +await ROT.compile(); await equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( (x) => { if (x >= 1n << 64n) throw Error('expected 64 bits'); @@ -69,3 +74,38 @@ await equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( return await ROT.verify(proof); } ); + +function testRot( + word: Field, + bits: number, + mode: 'left' | 'right', + result: Field +) { + Provable.runAndCheck(() => { + let w = Provable.witness(Field, () => word); + let r = Provable.witness(Field, () => result); + let output = Gadgets.rot(w, bits, mode); + output.assertEquals(r, `rot(${word}, ${bits}, ${mode})`); + }); +} + +testRot(Field('0'), 0, 'left', Field('0')); +testRot(Field('0'), 32, 'right', Field('0')); +testRot(Field('1'), 1, 'left', Field('2')); +testRot(Field('1'), 63, 'left', Field('9223372036854775808')); +testRot(Field('256'), 4, 'right', Field('16')); +testRot(Field('1234567890'), 32, 'right', Field('5302428712241725440')); +testRot(Field('2651214356120862720'), 32, 'right', Field('617283945')); +testRot(Field('1153202983878524928'), 32, 'right', Field('268500993')); +testRot( + Field('6510615555426900570'), + 4, + 'right', + Field('11936128518282651045') +); +testRot( + Field('6510615555426900570'), + 4, + 'right', + Field('11936128518282651045') +); From 5b0de018434176bab2f55f56b7ee20bf0742ecc3 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Sat, 21 Oct 2023 17:13:54 -0700 Subject: [PATCH 45/79] refactor(gadgets.unit-test.ts): convert inputs for rot to strings --- src/lib/gadgets/gadgets.unit-test.ts | 30 ++++++++++------------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts index f758a57be6..914fed654c 100644 --- a/src/lib/gadgets/gadgets.unit-test.ts +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -89,23 +89,13 @@ function testRot( }); } -testRot(Field('0'), 0, 'left', Field('0')); -testRot(Field('0'), 32, 'right', Field('0')); -testRot(Field('1'), 1, 'left', Field('2')); -testRot(Field('1'), 63, 'left', Field('9223372036854775808')); -testRot(Field('256'), 4, 'right', Field('16')); -testRot(Field('1234567890'), 32, 'right', Field('5302428712241725440')); -testRot(Field('2651214356120862720'), 32, 'right', Field('617283945')); -testRot(Field('1153202983878524928'), 32, 'right', Field('268500993')); -testRot( - Field('6510615555426900570'), - 4, - 'right', - Field('11936128518282651045') -); -testRot( - Field('6510615555426900570'), - 4, - 'right', - Field('11936128518282651045') -); +testRot(Field(0), 0, 'left', Field(0)); +testRot(Field(0), 32, 'right', Field(0)); +testRot(Field(1), 1, 'left', Field(2)); +testRot(Field(1), 63, 'left', Field('9223372036854775808')); +testRot(Field(256), 4, 'right', Field(16)); +testRot(Field(1234567890), 32, 'right', Field(5302428712241725440)); +testRot(Field(2651214356120862720), 32, 'right', Field(617283945)); +testRot(Field(1153202983878524928), 32, 'right', Field(268500993)); +testRot(Field(6510615555426900570n), 4, 'right', Field(11936128518282651045n)); +testRot(Field(6510615555426900570n), 4, 'right', Field(11936128518282651045n)); From de2c09139b67d7fdb392a12a4524b3a8bc240e5d Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Sat, 21 Oct 2023 17:16:56 -0700 Subject: [PATCH 46/79] refactor(gadgets.ts): simplify witness creation and rotation operations --- src/examples/gadgets.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/examples/gadgets.ts b/src/examples/gadgets.ts index 345d5b9d71..78800439ca 100644 --- a/src/examples/gadgets.ts +++ b/src/examples/gadgets.ts @@ -1,14 +1,10 @@ import { Field, Provable, Experimental, Gadgets } from 'o1js'; let cs = Provable.constraintSystem(() => { - let res1 = Provable.witness(Field, () => { - let f = Field(12); - return Gadgets.rot(f, 2, 'left'); - }); - let res2 = Provable.witness(Field, () => { - let f = Field(12); - return Gadgets.rot(f, 2, 'right'); - }); + let f = Provable.witness(Field, () => Field(12)); + + let res1 = Gadgets.rot(f, 2, 'left'); + let res2 = Gadgets.rot(f, 2, 'right'); res1.assertEquals(Field(48)); res2.assertEquals(Field(3)); From e3264ecfc9c5bf824da51c4eaee30564b8e05e6a Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Sun, 22 Oct 2023 13:45:13 -0700 Subject: [PATCH 47/79] refactor(rot.ts): modify witnessSlices and change inputs to rot gate --- src/lib/gadgets/rot.ts | 44 +++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index c1763d1cbb..eaacb5ebd4 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -71,20 +71,20 @@ function rotate( rotated, excess, [ - witnessSlices(bound, 52, 64), - witnessSlices(bound, 40, 52), - witnessSlices(bound, 28, 40), - witnessSlices(bound, 16, 28), + witnessSlices(bound, 52, 12), // bits 52-64 + witnessSlices(bound, 40, 12), // bits 40-52 + witnessSlices(bound, 28, 12), // bits 28-40 + witnessSlices(bound, 16, 12), // bits 16-28 ], [ - witnessSlices(bound, 14, 16), - witnessSlices(bound, 12, 14), - witnessSlices(bound, 10, 12), - witnessSlices(bound, 8, 10), - witnessSlices(bound, 6, 8), - witnessSlices(bound, 4, 6), - witnessSlices(bound, 2, 4), - witnessSlices(bound, 0, 2), + witnessSlices(bound, 14, 2), // bits 14-16 + witnessSlices(bound, 12, 2), // bits 12-14 + witnessSlices(bound, 10, 2), // bits 10-12 + witnessSlices(bound, 8, 2), // bits 8-10 + witnessSlices(bound, 6, 2), // bits 6-8 + witnessSlices(bound, 4, 2), // bits 4-6 + witnessSlices(bound, 2, 2), // bits 2-4 + witnessSlices(bound, 0, 2), // bits 0-2 ], Field.from(big2PowerRot) ); @@ -96,21 +96,13 @@ function rotate( return [rotated, excess, shifted]; } -function witnessSlices(f: Field, start: number, stop = -1) { - if (stop !== -1 && stop <= start) { - throw Error('stop must be greater than start'); - } - +// TODO: move to utils once https://github.com/o1-labs/o1js/pull/1177 is merged +function witnessSlices(f: Field, start: number, length: number) { + if (length <= 0) throw Error('Length must be a positive number'); return Provable.witness(Field, () => { - let bits = f.toBits(); - if (stop > bits.length) { - throw Error('stop must be less than bit-length'); - } - if (stop === -1) { - stop = bits.length; - } - - return Field.fromBits(bits.slice(start, stop)); + let mask = (1n << BigInt(length)) - 1n; + let n = f.toBigInt(); + return new Field((n >> BigInt(start)) & mask); }); } From 97af12c9978e926138dd90275774675b19602567 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 23 Oct 2023 09:29:51 -0700 Subject: [PATCH 48/79] refactor(rot.ts): simplify direction param usage --- src/bindings | 2 +- src/lib/gadgets/rot.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bindings b/src/bindings index aa7f880eb7..8ee9bde95e 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit aa7f880eb752c91408c90a40cf7e8bcb7dec87ff +Subproject commit 8ee9bde95e0ad6947eb303e4346f147deba10ed2 diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index eaacb5ebd4..43b3cdaeb7 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -20,7 +20,7 @@ function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { } if (word.isConstant()) { - return new Field(Fp.rot(word.toBigInt(), bits, direction === 'left')); + return new Field(Fp.rot(word.toBigInt(), bits, direction)); } const [rotated] = rotate(word, bits, direction); return rotated; From 20f10a0652a80de0f479132f249dad8d72f78c07 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 23 Oct 2023 09:36:08 -0700 Subject: [PATCH 49/79] refactor(field.unit-test.ts): remove rotation test from field.unit-test.ts --- src/lib/field.unit-test.ts | 17 ----------------- src/lib/gadgets/gadgets.unit-test.ts | 19 ++++++++++++++++++- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/lib/field.unit-test.ts b/src/lib/field.unit-test.ts index 28e17b90eb..8f7d8843f5 100644 --- a/src/lib/field.unit-test.ts +++ b/src/lib/field.unit-test.ts @@ -17,7 +17,6 @@ import { bool, Spec, } from './testing/equivalent.js'; -import { Gadgets } from './gadgets/gadgets.js'; // types Field satisfies Provable; @@ -45,22 +44,6 @@ test(Random.field, Random.json.field, (x, y, assert) => { assert(z.toJSON() === y); }); -// rotation -test( - Random.uint64, - Random.nat(64), - Random.boolean, - (x, n, direction, assert) => { - let z = Field(x); - let r1 = Fp.rot(x, n, direction); - Provable.runAndCheck(() => { - let f = Provable.witness(Field, () => z); - let r2 = Gadgets.rot(f, n, direction ? 'left' : 'right'); - Provable.asProver(() => assert(r1 === r2.toBigInt())); - }); - } -); - // handles small numbers test(Random.nat(1000), (n, assert) => { assert(Field(n).toString() === String(n)); diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts index 914fed654c..74380b1862 100644 --- a/src/lib/gadgets/gadgets.unit-test.ts +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -7,7 +7,8 @@ import { equivalentAsync, field, } from '../testing/equivalent.js'; -import { Random } from '../testing/random.js'; +import { test, Random } from '../testing/property.js'; +import { Field as Fp } from '../../provable/field-bigint.js'; import { Gadgets } from './gadgets.js'; import { Provable } from '../provable.js'; @@ -99,3 +100,19 @@ testRot(Field(2651214356120862720), 32, 'right', Field(617283945)); testRot(Field(1153202983878524928), 32, 'right', Field(268500993)); testRot(Field(6510615555426900570n), 4, 'right', Field(11936128518282651045n)); testRot(Field(6510615555426900570n), 4, 'right', Field(11936128518282651045n)); + +// rotation +test( + Random.uint64, + Random.nat(64), + Random.boolean, + (x, n, direction, assert) => { + let z = Field(x); + let r1 = Fp.rot(x, n, direction ? 'left' : 'right'); + Provable.runAndCheck(() => { + let f = Provable.witness(Field, () => z); + let r2 = Gadgets.rot(f, n, direction ? 'left' : 'right'); + Provable.asProver(() => assert(r1 === r2.toBigInt())); + }); + } +); From 9696ff0a714f48356bd76735efc7c6116df96290 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 23 Oct 2023 09:44:03 -0700 Subject: [PATCH 50/79] fix(gadgets.ts): change expectedRight from integer to Field object to match the type of actualRight for correct comparison --- src/examples/gadgets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/examples/gadgets.ts b/src/examples/gadgets.ts index 78800439ca..58b1ef882f 100644 --- a/src/examples/gadgets.ts +++ b/src/examples/gadgets.ts @@ -26,7 +26,7 @@ const ROT = Experimental.ZkProgram({ let expectedLeft = Field(192); actualLeft.assertEquals(expectedLeft); - let expectedRight = 12; + let expectedRight = Field(12); actualRight.assertEquals(expectedRight); }, }, From 27efd595a3b7f5c779024ebc54aa74c0c1f0227e Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 23 Oct 2023 09:44:44 -0700 Subject: [PATCH 51/79] fix(rot.ts): use BigInt for comparison to handle large numbers correctly The change was necessary as the previous comparison could fail for large numbers. Now, the comparison is done using BigInt which can handle larger numbers accurately. --- src/lib/gadgets/rot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 43b3cdaeb7..4b093dc1fa 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -33,7 +33,7 @@ function rotate( ): [Field, Field, Field] { // Check as the prover, that the input word is at most 64 bits. Provable.asProver(() => { - if (word.toBigInt() > 2 ** MAX_BITS) { + if (word.toBigInt() > BigInt(2 ** MAX_BITS)) { throw Error( `rot: expected word to be at most 64 bits, got ${word.toBigInt()}` ); From ef864c58ae6956c1d77cc1d49cc54fd43d1019ab Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 23 Oct 2023 09:56:54 -0700 Subject: [PATCH 52/79] refactor(rot.ts): extract max bits check into a separate function to reduce code duplication --- src/lib/gadgets/rot.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 4b093dc1fa..695b5d974c 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -20,6 +20,7 @@ function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { } if (word.isConstant()) { + checkMaxBits(word); return new Field(Fp.rot(word.toBigInt(), bits, direction)); } const [rotated] = rotate(word, bits, direction); @@ -33,11 +34,7 @@ function rotate( ): [Field, Field, Field] { // Check as the prover, that the input word is at most 64 bits. Provable.asProver(() => { - if (word.toBigInt() > BigInt(2 ** MAX_BITS)) { - throw Error( - `rot: expected word to be at most 64 bits, got ${word.toBigInt()}` - ); - } + checkMaxBits(word); }); const rotationBits = direction === 'right' ? MAX_BITS - bits : bits; @@ -96,6 +93,14 @@ function rotate( return [rotated, excess, shifted]; } +function checkMaxBits(x: Field) { + if (x.toBigInt() > BigInt(2 ** MAX_BITS)) { + throw Error( + `rot: expected word to be at most 64 bits, got ${x.toBigInt()}` + ); + } +} + // TODO: move to utils once https://github.com/o1-labs/o1js/pull/1177 is merged function witnessSlices(f: Field, start: number, length: number) { if (length <= 0) throw Error('Length must be a positive number'); From c4a261e41175addbbdf4cb9f0c7dbb8fe1bc67d7 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 23 Oct 2023 12:43:18 -0700 Subject: [PATCH 53/79] refactor(gadgets.ts, gadgets.unit-test.ts, rot.ts, gates.ts, snarky.d.ts): rename 'word' to 'field' for better clarity and consistency in code --- src/lib/gadgets/gadgets.ts | 6 +++--- src/lib/gadgets/gadgets.unit-test.ts | 6 +++--- src/lib/gadgets/rot.ts | 26 +++++++++++++------------- src/lib/gates.ts | 4 ++-- src/snarky.d.ts | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 01fe77b052..dc834c2218 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -41,7 +41,7 @@ const Gadgets = { * * **Note:** You can not rotate {@link Field} elements that exceed 64 bits. For elements that exceed 64 bits this operation will fail. * - * @param word {@link Field} element to rotate. + * @param field {@link Field} element to rotate. * @param bits amount of bits to rotate this {@link Field} element with. * @param direction left or right rotation direction. * @@ -59,7 +59,7 @@ const Gadgets = { * rot(xLarge, 32, "left"); // throws an error since input exceeds 64 bits * ``` */ - rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { - return rot(word, bits, direction); + rot(field: Field, bits: number, direction: 'left' | 'right' = 'left') { + return rot(field, bits, direction); }, }; diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts index 74380b1862..cfd91adedd 100644 --- a/src/lib/gadgets/gadgets.unit-test.ts +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -77,16 +77,16 @@ await equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( ); function testRot( - word: Field, + field: Field, bits: number, mode: 'left' | 'right', result: Field ) { Provable.runAndCheck(() => { - let w = Provable.witness(Field, () => word); + let w = Provable.witness(Field, () => field); let r = Provable.witness(Field, () => result); let output = Gadgets.rot(w, bits, mode); - output.assertEquals(r, `rot(${word}, ${bits}, ${mode})`); + output.assertEquals(r, `rot(${field}, ${bits}, ${mode})`); }); } diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 695b5d974c..32efb313f5 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -7,7 +7,7 @@ export { rot, rotate }; const MAX_BITS = 64 as const; -function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { +function rot(field: Field, bits: number, direction: 'left' | 'right' = 'left') { // Check that the rotation bits are in range if (bits < 0 || bits > MAX_BITS) { throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); @@ -19,22 +19,22 @@ function rot(word: Field, bits: number, direction: 'left' | 'right' = 'left') { ); } - if (word.isConstant()) { - checkMaxBits(word); - return new Field(Fp.rot(word.toBigInt(), bits, direction)); + if (field.isConstant()) { + checkMaxBits(field); + return new Field(Fp.rot(field.toBigInt(), bits, direction)); } - const [rotated] = rotate(word, bits, direction); + const [rotated] = rotate(field, bits, direction); return rotated; } function rotate( - word: Field, + field: Field, bits: number, direction: 'left' | 'right' = 'left' ): [Field, Field, Field] { - // Check as the prover, that the input word is at most 64 bits. + // Check as the prover, that the input is at most 64 bits. Provable.asProver(() => { - checkMaxBits(word); + checkMaxBits(field); }); const rotationBits = direction === 'right' ? MAX_BITS - bits : bits; @@ -44,12 +44,12 @@ function rotate( const [rotated, excess, shifted, bound] = Provable.witness( Provable.Array(Field, 4), () => { - const wordBigInt = word.toBigInt(); + const f = field.toBigInt(); // Obtain rotated output, excess, and shifted for the equation: - // word * 2^rot = excess * 2^64 + shifted + // f * 2^rot = excess * 2^64 + shifted const { quotient: excess, remainder: shifted } = divideWithRemainder( - wordBigInt * big2PowerRot, + f * big2PowerRot, big2Power64 ); @@ -64,7 +64,7 @@ function rotate( // Compute current row Gates.rot( - word, + field, rotated, excess, [ @@ -96,7 +96,7 @@ function rotate( function checkMaxBits(x: Field) { if (x.toBigInt() > BigInt(2 ** MAX_BITS)) { throw Error( - `rot: expected word to be at most 64 bits, got ${x.toBigInt()}` + `rot: expected field to be at most 64 bits, got ${x.toBigInt()}` ); } } diff --git a/src/lib/gates.ts b/src/lib/gates.ts index 175dc72afc..30701da882 100644 --- a/src/lib/gates.ts +++ b/src/lib/gates.ts @@ -44,7 +44,7 @@ function rangeCheck64(x: Field) { } function rot( - word: Field, + field: Field, rotated: Field, excess: Field, limbs: [Field, Field, Field, Field], @@ -52,7 +52,7 @@ function rot( two_to_rot: Field ) { Snarky.gates.rot( - word.value, + field.value, rotated.value, excess.value, MlArray.to(limbs.map((x) => x.value)), diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 8c80abff4a..3bee503990 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -308,7 +308,7 @@ declare const Snarky: { ): void; rot( - word: FieldVar, + field: FieldVar, rotated: FieldVar, excess: FieldVar, limbs: MlArray, From 0f31fee49e26d693100a9f7efecb7e8ccb1b0fb1 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 23 Oct 2023 12:45:27 -0700 Subject: [PATCH 54/79] fix(rot.ts): replace 'rotated' with 'field' in rangeCheck64 function to correct the variable being checked --- src/lib/gadgets/rot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 32efb313f5..565d616be7 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -89,7 +89,7 @@ function rotate( Gates.rangeCheck64(shifted); // Compute following row Gates.rangeCheck64(excess); - Gates.rangeCheck64(rotated); + Gates.rangeCheck64(field); return [rotated, excess, shifted]; } From 10b8be92d9dd547e3f91d99b16e5b743a13a9041 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 23 Oct 2023 12:58:04 -0700 Subject: [PATCH 55/79] fix(regression_test.json): update digest value in rot method to ensure test consistency with latest changes --- src/examples/regression_test.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/examples/regression_test.json b/src/examples/regression_test.json index ef26739ab3..3ee8cc35e5 100644 --- a/src/examples/regression_test.json +++ b/src/examples/regression_test.json @@ -170,7 +170,7 @@ "methods": { "rot": { "rows": 17, - "digest": "5253728d9fd357808be58a39c8375c07" + "digest": "916f4017a60f48d56d487c6869919b9c" } }, "verificationKey": { From 358105d09931199e14e58c7176e18db2b86f361e Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:12:06 -0700 Subject: [PATCH 56/79] chore(bindings): update subproject commit hash to fcda5090bc6bf433188c5152253d8370c25685c6 for latest changes --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 8ee9bde95e..fcda5090bc 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 8ee9bde95e0ad6947eb303e4346f147deba10ed2 +Subproject commit fcda5090bc6bf433188c5152253d8370c25685c6 From 87d56012445ab9dfc46741ec8562195e044381e5 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:14:12 -0700 Subject: [PATCH 57/79] feat(gadgets.ts): update doc comment Co-authored-by: Gregor Mitscha-Baude --- src/lib/gadgets/gadgets.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index dc834c2218..8f44170c78 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -39,7 +39,10 @@ const Gadgets = { * A (left and right) rotation is similar to the shift operation, `<<` and `>>` in JavaScript, just that bits are being appended to the other side. * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. * - * **Note:** You can not rotate {@link Field} elements that exceed 64 bits. For elements that exceed 64 bits this operation will fail. + * **Important:** The gadgets assumes that its input is at most 64 bits in size. + * If the input exceeds 64 bits, the gadget is invalid and does not prove correct execution of the rotation. + * Therefore, to safely use `rot()`, you need to make sure that the values passed in are range checked to 64 bits. + * For example, this can be done with {@link Gadgets.rangeCheck64}. * * @param field {@link Field} element to rotate. * @param bits amount of bits to rotate this {@link Field} element with. From 33aff5c6848aa2421b27a6bf21c70137271e8954 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:15:06 -0700 Subject: [PATCH 58/79] refactor(rot.ts): remove direction check in rot function --- src/lib/gadgets/rot.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 565d616be7..913d1dcbfc 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -13,12 +13,6 @@ function rot(field: Field, bits: number, direction: 'left' | 'right' = 'left') { throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); } - if (direction !== 'left' && direction !== 'right') { - throw Error( - `rot: expected direction to be 'left' or 'right', got ${direction}` - ); - } - if (field.isConstant()) { checkMaxBits(field); return new Field(Fp.rot(field.toBigInt(), bits, direction)); From 00530afef51180d9e10fac8a619c7c6c478597d3 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:22:08 -0700 Subject: [PATCH 59/79] refactor(rot.ts, gates.ts): replace Field type with bigint for two_to_rot variable --- src/lib/gadgets/rot.ts | 2 +- src/lib/gates.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 913d1dcbfc..d28d23af5a 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -77,7 +77,7 @@ function rotate( witnessSlices(bound, 2, 2), // bits 2-4 witnessSlices(bound, 0, 2), // bits 0-2 ], - Field.from(big2PowerRot) + big2PowerRot ); // Compute next row Gates.rangeCheck64(shifted); diff --git a/src/lib/gates.ts b/src/lib/gates.ts index 30701da882..bab62f0bf2 100644 --- a/src/lib/gates.ts +++ b/src/lib/gates.ts @@ -49,7 +49,7 @@ function rot( excess: Field, limbs: [Field, Field, Field, Field], crumbs: [Field, Field, Field, Field, Field, Field, Field, Field], - two_to_rot: Field + two_to_rot: bigint ) { Snarky.gates.rot( field.value, @@ -57,7 +57,7 @@ function rot( excess.value, MlArray.to(limbs.map((x) => x.value)), MlArray.to(crumbs.map((x) => x.value)), - FieldConst.fromBigint(two_to_rot.toBigInt()) + FieldConst.fromBigint(two_to_rot) ); } From 1948ea41cc9ba1814a3b1874f854668b471672ff Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:25:24 -0700 Subject: [PATCH 60/79] fix(rot.ts): remove unnecessary range check on 'field' variable --- src/lib/gadgets/rot.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index d28d23af5a..feead8c7ad 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -83,7 +83,6 @@ function rotate( Gates.rangeCheck64(shifted); // Compute following row Gates.rangeCheck64(excess); - Gates.rangeCheck64(field); return [rotated, excess, shifted]; } From c188a56a50ac573c3580e6d475dc6fb00f74ca59 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:32:36 -0700 Subject: [PATCH 61/79] refactor(gadgets.ts): change numeric literals to binary --- src/lib/gadgets/gadgets.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 8f44170c78..f3a819d581 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -52,11 +52,11 @@ const Gadgets = { * * @example * ```ts - * const x = Provable.witness(Field, () => Field(12)); + * const x = Provable.witness(Field, () => Field(0b001100)); * const y = rot(x, 2, 'left'); // left rotation by 2 bits * const z = rot(x, 2, 'right'); // right rotation by 2 bits - * y.assertEquals(48); - * z.assertEquals(3) + * y.assertEquals(0b110000); + * z.assertEquals(0b000011) * * const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n)); * rot(xLarge, 32, "left"); // throws an error since input exceeds 64 bits From 8a98da9ba385371526c6a8214d8baeac05d9f8f7 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:34:05 -0700 Subject: [PATCH 62/79] docs(CHANGELOG.md): update method description for bitwise rotation operation to provide more clarity on its functionality --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8287c5a4fc..96eccd1953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `Gadgets.rangeCheck64()`, new provable method to do efficient 64-bit range checks using lookup tables https://github.com/o1-labs/o1js/pull/1181 -- Added bitwise `ROT` operation support for native field elements. https://github.com/o1-labs/o1js/pull/1182 +- `Gadgets.rotate()`, new provable method to support bitwise rotation for native field elements. https://github.com/o1-labs/o1js/pull/1182 - `Proof.dummy()` to create dummy proofs https://github.com/o1-labs/o1js/pull/1188 - You can use this to write ZkPrograms that handle the base case and the inductive case in the same method. From 1f9fb240d3a848729760efc8b425b6430a3f9e4a Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:42:59 -0700 Subject: [PATCH 63/79] feat(gadgets): rename rot to rotate --- src/bindings | 2 +- src/lib/gadgets/gadgets.ts | 6 +++--- src/lib/gadgets/gadgets.unit-test.ts | 8 ++++---- src/lib/gadgets/rot.ts | 14 +++++++++----- src/lib/gates.ts | 6 +++--- src/snarky.d.ts | 2 +- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/bindings b/src/bindings index fcda5090bc..5e5befc857 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit fcda5090bc6bf433188c5152253d8370c25685c6 +Subproject commit 5e5befc8579393dadb96be1917642f860624ed07 diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index f3a819d581..9f781507d9 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -2,7 +2,7 @@ * Wrapper file for various gadgets, with a namespace and doccomments. */ import { rangeCheck64 } from './range-check.js'; -import { rot } from './rot.js'; +import { rotate } from './rot.js'; import { Field } from '../core.js'; export { Gadgets }; @@ -62,7 +62,7 @@ const Gadgets = { * rot(xLarge, 32, "left"); // throws an error since input exceeds 64 bits * ``` */ - rot(field: Field, bits: number, direction: 'left' | 'right' = 'left') { - return rot(field, bits, direction); + rotate(field: Field, bits: number, direction: 'left' | 'right' = 'left') { + return rotate(field, bits, direction); }, }; diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts index cfd91adedd..bb03a3ae1f 100644 --- a/src/lib/gadgets/gadgets.unit-test.ts +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -57,8 +57,8 @@ let ROT = ZkProgram({ run: { privateInputs: [Field], method(x) { - Gadgets.rot(x, 2, 'left'); - Gadgets.rot(x, 2, 'right'); + Gadgets.rotate(x, 2, 'left'); + Gadgets.rotate(x, 2, 'right'); }, }, }, @@ -85,7 +85,7 @@ function testRot( Provable.runAndCheck(() => { let w = Provable.witness(Field, () => field); let r = Provable.witness(Field, () => result); - let output = Gadgets.rot(w, bits, mode); + let output = Gadgets.rotate(w, bits, mode); output.assertEquals(r, `rot(${field}, ${bits}, ${mode})`); }); } @@ -111,7 +111,7 @@ test( let r1 = Fp.rot(x, n, direction ? 'left' : 'right'); Provable.runAndCheck(() => { let f = Provable.witness(Field, () => z); - let r2 = Gadgets.rot(f, n, direction ? 'left' : 'right'); + let r2 = Gadgets.rotate(f, n, direction ? 'left' : 'right'); Provable.asProver(() => assert(r1 === r2.toBigInt())); }); } diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index feead8c7ad..fdad9f271b 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -3,11 +3,15 @@ import { Provable } from '../provable.js'; import { Fp } from '../../bindings/crypto/finite_field.js'; import * as Gates from '../gates.js'; -export { rot, rotate }; +export { rotate, rot }; const MAX_BITS = 64 as const; -function rot(field: Field, bits: number, direction: 'left' | 'right' = 'left') { +function rotate( + field: Field, + bits: number, + direction: 'left' | 'right' = 'left' +) { // Check that the rotation bits are in range if (bits < 0 || bits > MAX_BITS) { throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); @@ -17,11 +21,11 @@ function rot(field: Field, bits: number, direction: 'left' | 'right' = 'left') { checkMaxBits(field); return new Field(Fp.rot(field.toBigInt(), bits, direction)); } - const [rotated] = rotate(field, bits, direction); + const [rotated] = rot(field, bits, direction); return rotated; } -function rotate( +function rot( field: Field, bits: number, direction: 'left' | 'right' = 'left' @@ -57,7 +61,7 @@ function rotate( ); // Compute current row - Gates.rot( + Gates.rotate( field, rotated, excess, diff --git a/src/lib/gates.ts b/src/lib/gates.ts index bab62f0bf2..a82c500744 100644 --- a/src/lib/gates.ts +++ b/src/lib/gates.ts @@ -2,7 +2,7 @@ import { Snarky } from '../snarky.js'; import { FieldVar, FieldConst, type Field } from './field.js'; import { MlArray } from './ml/base.js'; -export { rangeCheck64, rot }; +export { rangeCheck64, rotate }; /** * Asserts that x is at most 64 bits @@ -43,7 +43,7 @@ function rangeCheck64(x: Field) { ); } -function rot( +function rotate( field: Field, rotated: Field, excess: Field, @@ -51,7 +51,7 @@ function rot( crumbs: [Field, Field, Field, Field, Field, Field, Field, Field], two_to_rot: bigint ) { - Snarky.gates.rot( + Snarky.gates.rotate( field.value, rotated.value, excess.value, diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 3bee503990..b89327d138 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -307,7 +307,7 @@ declare const Snarky: { compact: FieldConst ): void; - rot( + rotate( field: FieldVar, rotated: FieldVar, excess: FieldVar, From b3959506db7be28bbf4952484cb93e3042f77286 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:50:33 -0700 Subject: [PATCH 64/79] refactor(rot.ts): simplify comments for better readability --- src/lib/gadgets/rot.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index fdad9f271b..6afce0e4f0 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -51,10 +51,9 @@ function rot( big2Power64 ); - // Compute rotated value as: - // rotated = excess + shifted + // Compute rotated value as: rotated = excess + shifted const rotated = shifted + excess; - // Compute bound that is the right input of FFAdd equation + // Compute bound to check excess < 2^rot const bound = excess + big2Power64 - big2PowerRot; return [rotated, excess, shifted, bound].map(Field.from); } From 142e30f47c34352cfb3fe88853de8321911410f3 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:52:19 -0700 Subject: [PATCH 65/79] fix(rot.ts): adjust validation range for rotation bits to exclude 0 and MAX_BITS --- src/lib/gadgets/rot.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 6afce0e4f0..79e72e961a 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -13,8 +13,10 @@ function rotate( direction: 'left' | 'right' = 'left' ) { // Check that the rotation bits are in range - if (bits < 0 || bits > MAX_BITS) { - throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); + if (bits <= 0 || bits >= MAX_BITS) { + throw Error( + `rot: expected bits to be in range [1, ${MAX_BITS - 1}], got ${bits}` + ); } if (field.isConstant()) { From b9ef95e90edf8959f7af0df1d0e2ed506a4a08ce Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:54:08 -0700 Subject: [PATCH 66/79] refactor(gadgets.unit-test.ts): simplify testRot function by removing unnecessary witness calls --- src/lib/gadgets/gadgets.unit-test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts index bb03a3ae1f..e956765a1a 100644 --- a/src/lib/gadgets/gadgets.unit-test.ts +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -83,10 +83,8 @@ function testRot( result: Field ) { Provable.runAndCheck(() => { - let w = Provable.witness(Field, () => field); - let r = Provable.witness(Field, () => result); - let output = Gadgets.rotate(w, bits, mode); - output.assertEquals(r, `rot(${field}, ${bits}, ${mode})`); + let output = Gadgets.rotate(field, bits, mode); + output.assertEquals(result, `rot(${field}, ${bits}, ${mode})`); }); } From fbe8aa59fb2c41e1713924298a5c296b68249a93 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 25 Oct 2023 12:56:32 -0700 Subject: [PATCH 67/79] docs(gadgets.ts): enhance explanation of rotation operation and its constraints --- src/lib/gadgets/gadgets.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 9f781507d9..b3e37620fd 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -36,12 +36,15 @@ const Gadgets = { }, /** - * A (left and right) rotation is similar to the shift operation, `<<` and `>>` in JavaScript, just that bits are being appended to the other side. - * `direction` is a string which accepts either `'left'` or `'right'`, defining the direction of the rotation. + * A (left and right) rotation operates similarly to the shift operation (`<<` for left and `>>` for right) in JavaScript, with the distinction that the bits are circulated to the opposite end rather than being discarded. + * For a left rotation, this means that bits shifted off the left end reappear at the right end. Conversely, for a right rotation, bits shifted off the right end reappear at the left end. + * It’s important to note that these operations are performed considering the binary representation of the number in big-endian format, where the most significant bit is on the left end and the least significant bit is on the right end. + * The `direction` parameter is a string that accepts either `'left'` or `'right'`, determining the direction of the rotation. * * **Important:** The gadgets assumes that its input is at most 64 bits in size. + * * If the input exceeds 64 bits, the gadget is invalid and does not prove correct execution of the rotation. - * Therefore, to safely use `rot()`, you need to make sure that the values passed in are range checked to 64 bits. + * Therefore, to safely use `rotate()`, you need to make sure that the values passed in are range checked to 64 bits. * For example, this can be done with {@link Gadgets.rangeCheck64}. * * @param field {@link Field} element to rotate. From 8c5079233f28aae3c4d40255867288a8cd5ed8d8 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 08:38:30 -0700 Subject: [PATCH 68/79] Revert "fix(rot.ts): adjust validation range for rotation bits to exclude 0 and MAX_BITS" This reverts commit 142e30f47c34352cfb3fe88853de8321911410f3. --- src/lib/gadgets/rot.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 79e72e961a..6afce0e4f0 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -13,10 +13,8 @@ function rotate( direction: 'left' | 'right' = 'left' ) { // Check that the rotation bits are in range - if (bits <= 0 || bits >= MAX_BITS) { - throw Error( - `rot: expected bits to be in range [1, ${MAX_BITS - 1}], got ${bits}` - ); + if (bits < 0 || bits > MAX_BITS) { + throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); } if (field.isConstant()) { From 36415ad666933783b8a3596dd196c75b31b68c69 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 08:40:01 -0700 Subject: [PATCH 69/79] refactor(rot.ts): remove redundant checkMaxBits function call to improve performance The checkMaxBits function was previously used to ensure that the input is at most 64 bits. However, this check is no longer necessary as the input size is now guaranteed by the type system. --- src/lib/gadgets/rot.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts index 6afce0e4f0..11a2b997dc 100644 --- a/src/lib/gadgets/rot.ts +++ b/src/lib/gadgets/rot.ts @@ -30,11 +30,6 @@ function rot( bits: number, direction: 'left' | 'right' = 'left' ): [Field, Field, Field] { - // Check as the prover, that the input is at most 64 bits. - Provable.asProver(() => { - checkMaxBits(field); - }); - const rotationBits = direction === 'right' ? MAX_BITS - bits : bits; const big2Power64 = 2n ** BigInt(MAX_BITS); const big2PowerRot = 2n ** BigInt(rotationBits); From b07707807e76db51c51015b85215ceeb8df43616 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 09:04:00 -0700 Subject: [PATCH 70/79] fix(regression_test.json): update 'rot' method's rows and digest values to reflect recent changes --- src/examples/regression_test.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/examples/regression_test.json b/src/examples/regression_test.json index 182f3a4b0c..40e164b481 100644 --- a/src/examples/regression_test.json +++ b/src/examples/regression_test.json @@ -169,8 +169,8 @@ "digest": "Bitwise Primitive", "methods": { "rot": { - "rows": 17, - "digest": "916f4017a60f48d56d487c6869919b9c" + "rows": 13, + "digest": "2c0dadbba96fd7ddb9adb7d643425ce3" }, "xor": { "rows": 15, @@ -182,4 +182,4 @@ "hash": "" } } -} +} \ No newline at end of file From 9075a94b046ff121d76289dbb6124add5617b291 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 09:04:28 -0700 Subject: [PATCH 71/79] refactor(gadgets.ts): rename 'rot' function to 'rotate' --- src/examples/gadgets.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/examples/gadgets.ts b/src/examples/gadgets.ts index 109bb19529..85f9bb2d45 100644 --- a/src/examples/gadgets.ts +++ b/src/examples/gadgets.ts @@ -3,8 +3,8 @@ import { Field, Provable, Experimental, Gadgets } from 'o1js'; let cs = Provable.constraintSystem(() => { let f = Provable.witness(Field, () => Field(12)); - let res1 = Gadgets.rot(f, 2, 'left'); - let res2 = Gadgets.rot(f, 2, 'right'); + let res1 = Gadgets.rotate(f, 2, 'left'); + let res2 = Gadgets.rotate(f, 2, 'right'); res1.assertEquals(Field(48)); res2.assertEquals(Field(3)); From 03b4988c1bfe382bb9451899e6b3e17ce1974b98 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 09:05:05 -0700 Subject: [PATCH 72/79] docs(CHANGELOG.md): update description for XOR operation to match format of other entries for consistency --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7918685120..e2a8874328 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `Gadgets.rotate()`, new provable method to support bitwise rotation for native field elements. https://github.com/o1-labs/o1js/pull/1182 -- Added bitwise `XOR` operation support for native field elements. https://github.com/o1-labs/o1js/pull/1177 +- `Gadgets.xor()`, new provable method to support bitwise xor for native field elements. https://github.com/o1-labs/o1js/pull/1177 - `Proof.dummy()` to create dummy proofs https://github.com/o1-labs/o1js/pull/1188 - You can use this to write ZkPrograms that handle the base case and the inductive case in the same method. From e2ff9926e2edc00180437ff40a85897bff1f20b4 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 09:08:02 -0700 Subject: [PATCH 73/79] refactor(bitwise.ts): merge rotate function from rot.ts into bitwise.ts --- src/lib/gadgets/bitwise.ts | 95 +++++++++++++++++++++++++++++++- src/lib/gadgets/gadgets.ts | 3 +- src/lib/gadgets/rot.ts | 109 ------------------------------------- 3 files changed, 95 insertions(+), 112 deletions(-) delete mode 100644 src/lib/gadgets/rot.ts diff --git a/src/lib/gadgets/bitwise.ts b/src/lib/gadgets/bitwise.ts index 53e6c432a4..4a48ef2123 100644 --- a/src/lib/gadgets/bitwise.ts +++ b/src/lib/gadgets/bitwise.ts @@ -3,7 +3,9 @@ import { Field as Fp } from '../../provable/field-bigint.js'; import { Field } from '../field.js'; import * as Gates from '../gates.js'; -export { xor }; +export { xor, rotate }; + +const MAX_BITS = 64 as const; function xor(a: Field, b: Field, length: number) { // check that both input lengths are positive @@ -111,6 +113,83 @@ function buildXor( zero.assertEquals(expectedOutput); } +function rotate( + field: Field, + bits: number, + direction: 'left' | 'right' = 'left' +) { + // Check that the rotation bits are in range + if (bits < 0 || bits > MAX_BITS) { + throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); + } + + if (field.isConstant()) { + checkMaxBits(field); + return new Field(Fp.rot(field.toBigInt(), bits, direction)); + } + const [rotated] = rot(field, bits, direction); + return rotated; +} + +function rot( + field: Field, + bits: number, + direction: 'left' | 'right' = 'left' +): [Field, Field, Field] { + const rotationBits = direction === 'right' ? MAX_BITS - bits : bits; + const big2Power64 = 2n ** BigInt(MAX_BITS); + const big2PowerRot = 2n ** BigInt(rotationBits); + + const [rotated, excess, shifted, bound] = Provable.witness( + Provable.Array(Field, 4), + () => { + const f = field.toBigInt(); + + // Obtain rotated output, excess, and shifted for the equation: + // f * 2^rot = excess * 2^64 + shifted + const { quotient: excess, remainder: shifted } = divideWithRemainder( + f * big2PowerRot, + big2Power64 + ); + + // Compute rotated value as: rotated = excess + shifted + const rotated = shifted + excess; + // Compute bound to check excess < 2^rot + const bound = excess + big2Power64 - big2PowerRot; + return [rotated, excess, shifted, bound].map(Field.from); + } + ); + + // Compute current row + Gates.rotate( + field, + rotated, + excess, + [ + witnessSlices(bound, 52, 12), // bits 52-64 + witnessSlices(bound, 40, 12), // bits 40-52 + witnessSlices(bound, 28, 12), // bits 28-40 + witnessSlices(bound, 16, 12), // bits 16-28 + ], + [ + witnessSlices(bound, 14, 2), // bits 14-16 + witnessSlices(bound, 12, 2), // bits 12-14 + witnessSlices(bound, 10, 2), // bits 10-12 + witnessSlices(bound, 8, 2), // bits 8-10 + witnessSlices(bound, 6, 2), // bits 6-8 + witnessSlices(bound, 4, 2), // bits 4-6 + witnessSlices(bound, 2, 2), // bits 2-4 + witnessSlices(bound, 0, 2), // bits 0-2 + ], + big2PowerRot + ); + // Compute next row + Gates.rangeCheck64(shifted); + // Compute following row + Gates.rangeCheck64(excess); + return [rotated, excess, shifted]; +} + function assert(stmt: boolean, message?: string) { if (!stmt) { throw Error(message ?? 'Assertion failed'); @@ -129,3 +208,17 @@ function witnessSlices(f: Field, start: number, length: number) { function witnessNextValue(current: Field) { return Provable.witness(Field, () => new Field(current.toBigInt() >> 16n)); } + +function checkMaxBits(x: Field) { + if (x.toBigInt() > BigInt(2 ** MAX_BITS)) { + throw Error( + `rot: expected field to be at most 64 bits, got ${x.toBigInt()}` + ); + } +} + +function divideWithRemainder(numerator: bigint, denominator: bigint) { + const quotient = numerator / denominator; + const remainder = numerator - denominator * quotient; + return { quotient, remainder }; +} diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 942f7bcccf..8234bb400c 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -2,8 +2,7 @@ * Wrapper file for various gadgets, with a namespace and doccomments. */ import { rangeCheck64 } from './range-check.js'; -import { rotate } from './rot.js'; -import { xor } from './bitwise.js'; +import { xor, rotate } from './bitwise.js'; import { Field } from '../core.js'; export { Gadgets }; diff --git a/src/lib/gadgets/rot.ts b/src/lib/gadgets/rot.ts deleted file mode 100644 index 11a2b997dc..0000000000 --- a/src/lib/gadgets/rot.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Field } from '../field.js'; -import { Provable } from '../provable.js'; -import { Fp } from '../../bindings/crypto/finite_field.js'; -import * as Gates from '../gates.js'; - -export { rotate, rot }; - -const MAX_BITS = 64 as const; - -function rotate( - field: Field, - bits: number, - direction: 'left' | 'right' = 'left' -) { - // Check that the rotation bits are in range - if (bits < 0 || bits > MAX_BITS) { - throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); - } - - if (field.isConstant()) { - checkMaxBits(field); - return new Field(Fp.rot(field.toBigInt(), bits, direction)); - } - const [rotated] = rot(field, bits, direction); - return rotated; -} - -function rot( - field: Field, - bits: number, - direction: 'left' | 'right' = 'left' -): [Field, Field, Field] { - const rotationBits = direction === 'right' ? MAX_BITS - bits : bits; - const big2Power64 = 2n ** BigInt(MAX_BITS); - const big2PowerRot = 2n ** BigInt(rotationBits); - - const [rotated, excess, shifted, bound] = Provable.witness( - Provable.Array(Field, 4), - () => { - const f = field.toBigInt(); - - // Obtain rotated output, excess, and shifted for the equation: - // f * 2^rot = excess * 2^64 + shifted - const { quotient: excess, remainder: shifted } = divideWithRemainder( - f * big2PowerRot, - big2Power64 - ); - - // Compute rotated value as: rotated = excess + shifted - const rotated = shifted + excess; - // Compute bound to check excess < 2^rot - const bound = excess + big2Power64 - big2PowerRot; - return [rotated, excess, shifted, bound].map(Field.from); - } - ); - - // Compute current row - Gates.rotate( - field, - rotated, - excess, - [ - witnessSlices(bound, 52, 12), // bits 52-64 - witnessSlices(bound, 40, 12), // bits 40-52 - witnessSlices(bound, 28, 12), // bits 28-40 - witnessSlices(bound, 16, 12), // bits 16-28 - ], - [ - witnessSlices(bound, 14, 2), // bits 14-16 - witnessSlices(bound, 12, 2), // bits 12-14 - witnessSlices(bound, 10, 2), // bits 10-12 - witnessSlices(bound, 8, 2), // bits 8-10 - witnessSlices(bound, 6, 2), // bits 6-8 - witnessSlices(bound, 4, 2), // bits 4-6 - witnessSlices(bound, 2, 2), // bits 2-4 - witnessSlices(bound, 0, 2), // bits 0-2 - ], - big2PowerRot - ); - // Compute next row - Gates.rangeCheck64(shifted); - // Compute following row - Gates.rangeCheck64(excess); - return [rotated, excess, shifted]; -} - -function checkMaxBits(x: Field) { - if (x.toBigInt() > BigInt(2 ** MAX_BITS)) { - throw Error( - `rot: expected field to be at most 64 bits, got ${x.toBigInt()}` - ); - } -} - -// TODO: move to utils once https://github.com/o1-labs/o1js/pull/1177 is merged -function witnessSlices(f: Field, start: number, length: number) { - if (length <= 0) throw Error('Length must be a positive number'); - return Provable.witness(Field, () => { - let mask = (1n << BigInt(length)) - 1n; - let n = f.toBigInt(); - return new Field((n >> BigInt(start)) & mask); - }); -} - -function divideWithRemainder(numerator: bigint, denominator: bigint) { - const quotient = numerator / denominator; - const remainder = numerator - denominator * quotient; - return { quotient, remainder }; -} From 6405b755d5126459e6eaa03cdf3ff321e1bf2a55 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 09:11:07 -0700 Subject: [PATCH 74/79] docs(gadgets.ts): enhance documentation for XOR gadget function - Change single line comment to JSDoc style for better IDE support - Add parameter descriptions for better understanding - Improve wording and formatting for better readability - Add @throws tag to highlight error conditions - Update example code to match new parameter descriptions --- src/lib/gadgets/gadgets.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 8234bb400c..59e4a8e305 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -69,7 +69,7 @@ const Gadgets = { return rotate(field, bits, direction); }, - /* + /** * Bitwise XOR gadget on {@link Field} elements. Equivalent to the [bitwise XOR `^` operator in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_XOR). * A XOR gate works by comparing two bits and returning `1` if two bits differ, and `0` if two bits are equal. * @@ -78,17 +78,26 @@ const Gadgets = { * The `length` parameter lets you define how many bits should be compared. `length` is rounded to the nearest multiple of 16, `paddedLength = ceil(length / 16) * 16`, and both input values are constrained to fit into `paddedLength` bits. The output is guaranteed to have at most `paddedLength` bits as well. * * **Note:** Specifying a larger `length` parameter adds additional constraints. + * * It is also important to mention that specifying a smaller `length` allows the verifier to infer the length of the original input data (e.g. smaller than 16 bit if only one XOR gate has been used). * A zkApp developer should consider these implications when choosing the `length` parameter and carefully weigh the trade-off between increased amount of constraints and security. * - * **Note:** Both {@link Field} elements need to fit into `2^paddedLength - 1`. Otherwise, an error is thrown and no proof can be generated.. + * **Important:** Both {@link Field} elements need to fit into `2^paddedLength - 1`. Otherwise, an error is thrown and no proof can be generated. + * * For example, with `length = 2` (`paddedLength = 16`), `xor()` will fail for any input that is larger than `2**16`. * - * ```typescript - * let a = Field(5); // ... 000101 - * let b = Field(3); // ... 000011 + * @param a {@link Field} element to compare. + * @param b {@link Field} element to compare. + * @param length amount of bits to compare. + * + * @throws Throws an error if the input values exceed `2^paddedLength - 1`. + * + * @example + * ```ts + * let a = Field(5); // ... 000101 + * let b = Field(3); // ... 000011 * - * let c = xor(a, b, 2); // ... 000110 + * let c = xor(a, b, 2); // ... 000110 * c.assertEquals(6); * ``` */ From 131bce24e7e36d3758b987363d912bfd8e2eacb6 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 09:16:39 -0700 Subject: [PATCH 75/79] refactor(bitwise.ts, common.ts): extract common functions and constants to a separate file --- src/lib/gadgets/bitwise.ts | 54 +++++++++++--------------------------- src/lib/gadgets/common.ts | 37 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 39 deletions(-) create mode 100644 src/lib/gadgets/common.ts diff --git a/src/lib/gadgets/bitwise.ts b/src/lib/gadgets/bitwise.ts index 4a48ef2123..70a6c9b6b1 100644 --- a/src/lib/gadgets/bitwise.ts +++ b/src/lib/gadgets/bitwise.ts @@ -2,11 +2,16 @@ import { Provable } from '../provable.js'; import { Field as Fp } from '../../provable/field-bigint.js'; import { Field } from '../field.js'; import * as Gates from '../gates.js'; +import { + MAX_BITS, + assert, + witnessSlices, + witnessNextValue, + divideWithRemainder, +} from './common.js'; export { xor, rotate }; -const MAX_BITS = 64 as const; - function xor(a: Field, b: Field, length: number) { // check that both input lengths are positive assert(length > 0, `Input lengths need to be positive values.`); @@ -119,12 +124,16 @@ function rotate( direction: 'left' | 'right' = 'left' ) { // Check that the rotation bits are in range - if (bits < 0 || bits > MAX_BITS) { - throw Error(`rot: expected bits to be between 0 and 64, got ${bits}`); - } + assert( + bits > 0 && bits < MAX_BITS, + `rotation: expected bits to be between 0 and 64, got ${bits}` + ); if (field.isConstant()) { - checkMaxBits(field); + assert( + field.toBigInt() < 2n ** BigInt(MAX_BITS), + `rotation: expected field to be at most 64 bits, got ${field.toBigInt()}` + ); return new Field(Fp.rot(field.toBigInt(), bits, direction)); } const [rotated] = rot(field, bits, direction); @@ -189,36 +198,3 @@ function rot( Gates.rangeCheck64(excess); return [rotated, excess, shifted]; } - -function assert(stmt: boolean, message?: string) { - if (!stmt) { - throw Error(message ?? 'Assertion failed'); - } -} - -function witnessSlices(f: Field, start: number, length: number) { - if (length <= 0) throw Error('Length must be a positive number'); - - return Provable.witness(Field, () => { - let n = f.toBigInt(); - return new Field((n >> BigInt(start)) & ((1n << BigInt(length)) - 1n)); - }); -} - -function witnessNextValue(current: Field) { - return Provable.witness(Field, () => new Field(current.toBigInt() >> 16n)); -} - -function checkMaxBits(x: Field) { - if (x.toBigInt() > BigInt(2 ** MAX_BITS)) { - throw Error( - `rot: expected field to be at most 64 bits, got ${x.toBigInt()}` - ); - } -} - -function divideWithRemainder(numerator: bigint, denominator: bigint) { - const quotient = numerator / denominator; - const remainder = numerator - denominator * quotient; - return { quotient, remainder }; -} diff --git a/src/lib/gadgets/common.ts b/src/lib/gadgets/common.ts new file mode 100644 index 0000000000..cade7e3417 --- /dev/null +++ b/src/lib/gadgets/common.ts @@ -0,0 +1,37 @@ +import { Provable } from '../provable.js'; +import { Field } from '../field.js'; + +const MAX_BITS = 64 as const; + +export { + MAX_BITS, + assert, + witnessSlices, + witnessNextValue, + divideWithRemainder, +}; + +function assert(stmt: boolean, message?: string) { + if (!stmt) { + throw Error(message ?? 'Assertion failed'); + } +} + +function witnessSlices(f: Field, start: number, length: number) { + if (length <= 0) throw Error('Length must be a positive number'); + + return Provable.witness(Field, () => { + let n = f.toBigInt(); + return new Field((n >> BigInt(start)) & ((1n << BigInt(length)) - 1n)); + }); +} + +function witnessNextValue(current: Field) { + return Provable.witness(Field, () => new Field(current.toBigInt() >> 16n)); +} + +function divideWithRemainder(numerator: bigint, denominator: bigint) { + const quotient = numerator / denominator; + const remainder = numerator - denominator * quotient; + return { quotient, remainder }; +} From eeabe7ceae3431a138a3273df2b46e5614c817a1 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 09:23:20 -0700 Subject: [PATCH 76/79] feat(range-check.unit-test.ts): add name property to ROT ZkProgram --- src/lib/gadgets/range-check.unit-test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/gadgets/range-check.unit-test.ts b/src/lib/gadgets/range-check.unit-test.ts index fc812a6454..88a991e6ae 100644 --- a/src/lib/gadgets/range-check.unit-test.ts +++ b/src/lib/gadgets/range-check.unit-test.ts @@ -54,6 +54,7 @@ await equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( // ROT Gate // -------------------------- let ROT = ZkProgram({ + name: 'rot', methods: { run: { privateInputs: [Field], From 6f2a0dd71298bb514c713b1138ff779773a6ca57 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 09:24:06 -0700 Subject: [PATCH 77/79] fix(range-check.unit-test.ts): change Field value from string to BigInt --- src/lib/gadgets/range-check.unit-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/gadgets/range-check.unit-test.ts b/src/lib/gadgets/range-check.unit-test.ts index 88a991e6ae..4d0d77fb69 100644 --- a/src/lib/gadgets/range-check.unit-test.ts +++ b/src/lib/gadgets/range-check.unit-test.ts @@ -93,7 +93,7 @@ function testRot( testRot(Field(0), 0, 'left', Field(0)); testRot(Field(0), 32, 'right', Field(0)); testRot(Field(1), 1, 'left', Field(2)); -testRot(Field(1), 63, 'left', Field('9223372036854775808')); +testRot(Field(1), 63, 'left', Field(9223372036854775808n)); testRot(Field(256), 4, 'right', Field(16)); testRot(Field(1234567890), 32, 'right', Field(5302428712241725440)); testRot(Field(2651214356120862720), 32, 'right', Field(617283945)); From e48c5e4d860e9156eaa22bfb69131ed9393b0466 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 09:42:35 -0700 Subject: [PATCH 78/79] fix(bitwise.ts): adjust rotation bits range check to include 0 and MAX_BITS --- src/lib/gadgets/bitwise.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/gadgets/bitwise.ts b/src/lib/gadgets/bitwise.ts index 70a6c9b6b1..b51f90a801 100644 --- a/src/lib/gadgets/bitwise.ts +++ b/src/lib/gadgets/bitwise.ts @@ -125,7 +125,7 @@ function rotate( ) { // Check that the rotation bits are in range assert( - bits > 0 && bits < MAX_BITS, + bits >= 0 && bits <= MAX_BITS, `rotation: expected bits to be between 0 and 64, got ${bits}` ); From 7cd98ee22d90dac15f012ede76a60b4633607554 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 26 Oct 2023 09:45:32 -0700 Subject: [PATCH 79/79] refactor(bitwise.unit-test.ts): move rot tests from range-check.unit-test.ts to bitwise.unit-test.ts --- src/lib/gadgets/bitwise.unit-test.ts | 61 +++++++++++++++++++- src/lib/gadgets/range-check.unit-test.ts | 71 +----------------------- 2 files changed, 59 insertions(+), 73 deletions(-) diff --git a/src/lib/gadgets/bitwise.unit-test.ts b/src/lib/gadgets/bitwise.unit-test.ts index f546e90108..f395b9ebb6 100644 --- a/src/lib/gadgets/bitwise.unit-test.ts +++ b/src/lib/gadgets/bitwise.unit-test.ts @@ -7,9 +7,10 @@ import { fieldWithRng, } from '../testing/equivalent.js'; import { Fp, mod } from '../../bindings/crypto/finite_field.js'; -import { Field } from '../field.js'; +import { Field } from '../core.js'; import { Gadgets } from './gadgets.js'; -import { Random } from '../testing/property.js'; +import { test, Random } from '../testing/property.js'; +import { Provable } from '../provable.js'; let Bitwise = ZkProgram({ name: 'bitwise', @@ -21,6 +22,12 @@ let Bitwise = ZkProgram({ return Gadgets.xor(a, b, 64); }, }, + rot: { + privateInputs: [Field], + method(a: Field) { + return Gadgets.rotate(a, 12, 'left'); + }, + }, }, }); @@ -35,6 +42,21 @@ let uint = (length: number) => fieldWithRng(Random.biguint(length)); ); }); +test( + Random.uint64, + Random.nat(64), + Random.boolean, + (x, n, direction, assert) => { + let z = Field(x); + let r1 = Fp.rot(x, n, direction ? 'left' : 'right'); + Provable.runAndCheck(() => { + let f = Provable.witness(Field, () => z); + let r2 = Gadgets.rotate(f, n, direction ? 'left' : 'right'); + Provable.asProver(() => assert(r1 === r2.toBigInt())); + }); + } +); + let maybeUint64: Spec = { ...field, rng: Random.map(Random.oneOf(Random.uint64, Random.uint64.invalid), (x) => @@ -42,7 +64,6 @@ let maybeUint64: Spec = { ), }; -// do a couple of proofs await equivalentAsync( { from: [maybeUint64, maybeUint64], to: field }, { runs: 3 } @@ -57,3 +78,37 @@ await equivalentAsync( return proof.publicOutput; } ); + +await equivalentAsync({ from: [field], to: field }, { runs: 3 })( + (x) => { + if (x >= 2n ** 64n) throw Error('Does not fit into 64 bits'); + return Fp.rot(x, 12, 'left'); + }, + async (x) => { + let proof = await Bitwise.rot(x); + return proof.publicOutput; + } +); + +function testRot( + field: Field, + bits: number, + mode: 'left' | 'right', + result: Field +) { + Provable.runAndCheck(() => { + let output = Gadgets.rotate(field, bits, mode); + output.assertEquals(result, `rot(${field}, ${bits}, ${mode})`); + }); +} + +testRot(Field(0), 0, 'left', Field(0)); +testRot(Field(0), 32, 'right', Field(0)); +testRot(Field(1), 1, 'left', Field(2)); +testRot(Field(1), 63, 'left', Field(9223372036854775808n)); +testRot(Field(256), 4, 'right', Field(16)); +testRot(Field(1234567890), 32, 'right', Field(5302428712241725440)); +testRot(Field(2651214356120862720), 32, 'right', Field(617283945)); +testRot(Field(1153202983878524928), 32, 'right', Field(268500993)); +testRot(Field(6510615555426900570n), 4, 'right', Field(11936128518282651045n)); +testRot(Field(6510615555426900570n), 4, 'right', Field(11936128518282651045n)); diff --git a/src/lib/gadgets/range-check.unit-test.ts b/src/lib/gadgets/range-check.unit-test.ts index 4d0d77fb69..4466f5e187 100644 --- a/src/lib/gadgets/range-check.unit-test.ts +++ b/src/lib/gadgets/range-check.unit-test.ts @@ -7,10 +7,8 @@ import { equivalentAsync, field, } from '../testing/equivalent.js'; -import { test, Random } from '../testing/property.js'; -import { Field as Fp } from '../../provable/field-bigint.js'; +import { Random } from '../testing/property.js'; import { Gadgets } from './gadgets.js'; -import { Provable } from '../provable.js'; let maybeUint64: Spec = { ...field, @@ -49,70 +47,3 @@ await equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( return await RangeCheck64.verify(proof); } ); - -// -------------------------- -// ROT Gate -// -------------------------- -let ROT = ZkProgram({ - name: 'rot', - methods: { - run: { - privateInputs: [Field], - method(x) { - Gadgets.rotate(x, 2, 'left'); - Gadgets.rotate(x, 2, 'right'); - }, - }, - }, -}); - -await ROT.compile(); -await equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( - (x) => { - if (x >= 1n << 64n) throw Error('expected 64 bits'); - return true; - }, - async (x) => { - let proof = await ROT.run(x); - return await ROT.verify(proof); - } -); - -function testRot( - field: Field, - bits: number, - mode: 'left' | 'right', - result: Field -) { - Provable.runAndCheck(() => { - let output = Gadgets.rotate(field, bits, mode); - output.assertEquals(result, `rot(${field}, ${bits}, ${mode})`); - }); -} - -testRot(Field(0), 0, 'left', Field(0)); -testRot(Field(0), 32, 'right', Field(0)); -testRot(Field(1), 1, 'left', Field(2)); -testRot(Field(1), 63, 'left', Field(9223372036854775808n)); -testRot(Field(256), 4, 'right', Field(16)); -testRot(Field(1234567890), 32, 'right', Field(5302428712241725440)); -testRot(Field(2651214356120862720), 32, 'right', Field(617283945)); -testRot(Field(1153202983878524928), 32, 'right', Field(268500993)); -testRot(Field(6510615555426900570n), 4, 'right', Field(11936128518282651045n)); -testRot(Field(6510615555426900570n), 4, 'right', Field(11936128518282651045n)); - -// rotation -test( - Random.uint64, - Random.nat(64), - Random.boolean, - (x, n, direction, assert) => { - let z = Field(x); - let r1 = Fp.rot(x, n, direction ? 'left' : 'right'); - Provable.runAndCheck(() => { - let f = Provable.witness(Field, () => z); - let r2 = Gadgets.rotate(f, n, direction ? 'left' : 'right'); - Provable.asProver(() => assert(r1 === r2.toBigInt())); - }); - } -);