diff --git a/CHANGELOG.md b/CHANGELOG.md index ba6b4605dc..3acbab93a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +31,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - `Lightnet` namespace to interact with the account manager provided by the [lightnet Mina network](https://hub.docker.com/r/o1labs/mina-local-network). https://github.com/o1-labs/o1js/pull/1167 + - Internal support for several custom gates (range check, bitwise operations, foreign field operations) and lookup tables https://github.com/o1-labs/o1js/pull/1176 + - `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 `XOR` operation support for native field elements. https://github.com/o1-labs/o1js/pull/1177 + +- `Gadgets.rotate()`, new provable method to support bitwise rotation for native field elements. https://github.com/o1-labs/o1js/pull/1182 + +- `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. diff --git a/src/bindings b/src/bindings index dbe878db43..5e5befc857 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit dbe878db43d256ac3085f248551b05b75ffecfda +Subproject commit 5e5befc8579393dadb96be1917642f860624ed07 diff --git a/src/examples/gadgets.ts b/src/examples/gadgets.ts index 48113d8771..5d3504be2f 100644 --- a/src/examples/gadgets.ts +++ b/src/examples/gadgets.ts @@ -1,5 +1,39 @@ import { Field, Provable, Gadgets, ZkProgram } from 'o1js'; +let cs = Provable.constraintSystem(() => { + let f = Provable.witness(Field, () => Field(12)); + + let res1 = Gadgets.rotate(f, 2, 'left'); + let res2 = Gadgets.rotate(f, 2, 'right'); + + res1.assertEquals(Field(48)); + res2.assertEquals(Field(3)); + + Provable.log(res1); + Provable.log(res2); +}); +console.log('constraint system: ', cs); + +const ROT = ZkProgram({ + name: 'rot-example', + methods: { + baseCase: { + privateInputs: [], + method: () => { + let a = Provable.witness(Field, () => Field(48)); + let actualLeft = Gadgets.rotate(a, 2, 'left'); + let actualRight = Gadgets.rotate(a, 2, 'right'); + + let expectedLeft = Field(192); + actualLeft.assertEquals(expectedLeft); + + let expectedRight = Field(12); + actualRight.assertEquals(expectedRight); + }, + }, + }, +}); + const XOR = ZkProgram({ name: 'xor-example', methods: { @@ -19,14 +53,18 @@ const XOR = ZkProgram({ console.log('compiling..'); console.time('compile'); +await ROT.compile(); await XOR.compile(); console.timeEnd('compile'); console.log('proving..'); -console.time('prove'); -let proof = await XOR.baseCase(); -console.timeEnd('prove'); +console.time('rotation prove'); +let rotProof = await ROT.baseCase(); +console.timeEnd('rotation prove'); +if (!(await ROT.verify(rotProof))) throw Error('rotate: Invalid proof'); +console.time('xor prove'); +let proof = await XOR.baseCase(); +console.timeEnd('xor prove'); if (!(await XOR.verify(proof))) throw Error('Invalid proof'); -else console.log('proof valid'); diff --git a/src/examples/primitive_constraint_system.ts b/src/examples/primitive_constraint_system.ts index 1ef5a5f87c..4f1f3f95ad 100644 --- a/src/examples/primitive_constraint_system.ts +++ b/src/examples/primitive_constraint_system.ts @@ -64,6 +64,13 @@ const GroupMock = { }; const BitwiseMock = { + rot() { + let a = Provable.witness(Field, () => new Field(12)); + Gadgets.rotate(a, 2, 'left'); + Gadgets.rotate(a, 2, 'right'); + Gadgets.rotate(a, 4, 'left'); + Gadgets.rotate(a, 4, 'right'); + }, xor() { let a = Provable.witness(Field, () => new Field(5n)); let b = Provable.witness(Field, () => new Field(5n)); diff --git a/src/examples/regression_test.json b/src/examples/regression_test.json index 817796fa3a..40e164b481 100644 --- a/src/examples/regression_test.json +++ b/src/examples/regression_test.json @@ -168,6 +168,10 @@ "Bitwise Primitive": { "digest": "Bitwise Primitive", "methods": { + "rot": { + "rows": 13, + "digest": "2c0dadbba96fd7ddb9adb7d643425ce3" + }, "xor": { "rows": 15, "digest": "b3595a9cc9562d4f4a3a397b6de44971" diff --git a/src/lib/gadgets/bitwise.ts b/src/lib/gadgets/bitwise.ts index 53e6c432a4..b51f90a801 100644 --- a/src/lib/gadgets/bitwise.ts +++ b/src/lib/gadgets/bitwise.ts @@ -2,8 +2,15 @@ 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 }; +export { xor, rotate }; function xor(a: Field, b: Field, length: number) { // check that both input lengths are positive @@ -111,21 +118,83 @@ function buildXor( zero.assertEquals(expectedOutput); } -function assert(stmt: boolean, message?: string) { - if (!stmt) { - throw Error(message ?? 'Assertion failed'); +function rotate( + field: Field, + bits: number, + direction: 'left' | 'right' = 'left' +) { + // Check that the rotation bits are in range + assert( + bits >= 0 && bits <= MAX_BITS, + `rotation: expected bits to be between 0 and 64, got ${bits}` + ); + + if (field.isConstant()) { + 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); + return rotated; } -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 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); + } + ); -function witnessNextValue(current: Field) { - return Provable.witness(Field, () => new Field(current.toBigInt() >> 16n)); + // 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]; } 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/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 }; +} diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 5f993a9727..59e4a8e305 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 { xor } from './bitwise.js'; +import { xor, rotate } from './bitwise.js'; import { Field } from '../core.js'; export { Gadgets }; @@ -35,6 +35,40 @@ const Gadgets = { return rangeCheck64(x); }, + /** + * 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 `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. + * @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 + * ```ts + * 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(0b110000); + * z.assertEquals(0b000011) + * + * const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n)); + * rot(xLarge, 32, "left"); // throws an error since input exceeds 64 bits + * ``` + */ + rotate(field: Field, bits: number, direction: 'left' | 'right' = 'left') { + 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. @@ -44,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); * ``` */ diff --git a/src/lib/gadgets/range-check.unit-test.ts b/src/lib/gadgets/range-check.unit-test.ts index f8cda9ead1..4466f5e187 100644 --- a/src/lib/gadgets/range-check.unit-test.ts +++ b/src/lib/gadgets/range-check.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, @@ -7,10 +7,20 @@ import { equivalentAsync, field, } from '../testing/equivalent.js'; -import { Random } from '../testing/random.js'; +import { Random } from '../testing/property.js'; import { Gadgets } from './gadgets.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({ name: 'range-check-64', @@ -26,16 +36,7 @@ let RangeCheck64 = ZkProgram({ await RangeCheck64.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'); diff --git a/src/lib/gates.ts b/src/lib/gates.ts index 1a5a9a3c00..62da6f8271 100644 --- a/src/lib/gates.ts +++ b/src/lib/gates.ts @@ -1,7 +1,8 @@ import { Snarky } from '../snarky.js'; import { FieldVar, FieldConst, type Field } from './field.js'; +import { MlArray } from './ml/base.js'; -export { rangeCheck64, xor, zero }; +export { rangeCheck64, xor, zero, rotate }; /** * Asserts that x is at most 64 bits @@ -42,6 +43,24 @@ function rangeCheck64(x: Field) { ); } +function rotate( + field: Field, + rotated: Field, + excess: Field, + limbs: [Field, Field, Field, Field], + crumbs: [Field, Field, Field, Field, Field, Field, Field, Field], + two_to_rot: bigint +) { + Snarky.gates.rotate( + field.value, + rotated.value, + excess.value, + MlArray.to(limbs.map((x) => x.value)), + MlArray.to(crumbs.map((x) => x.value)), + FieldConst.fromBigint(two_to_rot) + ); +} + /** * Asserts that 16 bit limbs of input two elements are the correct XOR output */ diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 512ac61ed1..4d3d7172cd 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -307,6 +307,15 @@ declare const Snarky: { compact: FieldConst ): void; + rotate( + field: FieldVar, + rotated: FieldVar, + excess: FieldVar, + limbs: MlArray, + crumbs: MlArray, + two_to_rot: FieldConst + ): void; + xor( in1: FieldVar, in2: FieldVar,