Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add gadgets to API surface, UInt32, UInt64, implement rot32 #1259

Merged
merged 62 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
fb543ed
feat(int.ts): add support for bitwise XOR, NOT, rotate, leftShift, ri…
Trivo25 Nov 14, 2023
840fc7a
fix(int.ts): update parameter name in rotate method from 'direction' …
Trivo25 Nov 14, 2023
f6f9177
fix(int.ts): fix instantiation of UInt64 in example code by using the…
Trivo25 Nov 14, 2023
5020ce3
simplify doc comments, avoid confusion
Trivo25 Nov 21, 2023
63fa85f
same for uint32
Trivo25 Nov 21, 2023
14f2051
Merge branch 'main' into feature/gadgets-uint
Trivo25 Nov 21, 2023
3932338
bump bindings
Trivo25 Nov 21, 2023
8c524b6
add divMod32, addMod32
Trivo25 Nov 21, 2023
7d8ff8e
rotate32, rename rotate to rotate64
Trivo25 Nov 21, 2023
27d6f8a
add rotate32 tests
Trivo25 Nov 21, 2023
0d5e09b
epose rot32
Trivo25 Nov 21, 2023
81c90c4
rangeCheck32
Trivo25 Nov 21, 2023
aafd4b8
add rotate32 to UInt32
Trivo25 Nov 21, 2023
6419176
fix tests
Trivo25 Nov 21, 2023
e8cac1a
fix vk regression test
Trivo25 Nov 21, 2023
4685946
add constant check
Trivo25 Nov 22, 2023
4013f14
re-add range checks
Trivo25 Nov 22, 2023
bc9a20e
add doc comments
Trivo25 Nov 22, 2023
14a7717
rename r and q to remainder and quotient
Trivo25 Nov 22, 2023
51d0bfd
undo chain tests because not needed
Trivo25 Nov 22, 2023
ce34be8
Undo comment
Trivo25 Nov 22, 2023
4d78fc0
bump bindings
Trivo25 Nov 22, 2023
d370e29
fix return type opf witness
Trivo25 Nov 22, 2023
08069bb
limit input to 64bit, add tests
Trivo25 Nov 22, 2023
954607e
adjust error message
Trivo25 Nov 22, 2023
0878630
Merge branch 'main' into feature/gadgets-uint
Trivo25 Nov 22, 2023
e25d896
add gadgets to regression tests
Trivo25 Nov 22, 2023
62c1635
simplify tests
Trivo25 Nov 28, 2023
93be2b0
refactor rangeCheckHelper
Trivo25 Nov 28, 2023
12f388d
fix import
Trivo25 Nov 28, 2023
982d3b0
Merge branch 'main' into feature/gadgets-uint
Trivo25 Nov 28, 2023
2b091e4
changelog
Trivo25 Nov 28, 2023
f6e43a2
Update src/lib/gadgets/gadgets.ts
Trivo25 Nov 29, 2023
89d806b
Update src/lib/gadgets/gadgets.ts
Trivo25 Nov 29, 2023
72d38b4
Update src/lib/gadgets/gadgets.ts
Trivo25 Nov 29, 2023
84f0823
address feedback
Trivo25 Nov 29, 2023
d790149
Merge branch 'feature/gadgets-uint' of https://github.com/o1-labs/o1j…
Trivo25 Nov 29, 2023
cf16482
fix changelog
Trivo25 Nov 29, 2023
2042e02
add rangeCheckN and isInRangeN
Trivo25 Nov 29, 2023
b97c717
add correctly typed constructor for UInt32 and UInt64
Trivo25 Nov 29, 2023
973a366
fix vk tests
Trivo25 Nov 29, 2023
1165c5f
remove constant check for now
Trivo25 Nov 29, 2023
8cefdbb
add left shift(temp)
Trivo25 Nov 29, 2023
6da47bd
remove debug code
Trivo25 Nov 29, 2023
2f18e5f
fix tests
Trivo25 Nov 29, 2023
1b3ce9b
handle constant case
Trivo25 Nov 29, 2023
ff91e1d
fix tests
Trivo25 Nov 29, 2023
a0f1e5e
undo spacing in example
Trivo25 Nov 29, 2023
de71797
fix compile issue
Trivo25 Nov 29, 2023
fb2aabd
clean up
Trivo25 Nov 29, 2023
b3d74df
doc comment fix
Trivo25 Nov 29, 2023
12942ed
Merge branch 'main' into feature/gadgets-uint
Trivo25 Dec 15, 2023
cfa4883
return UInt instead of field
Trivo25 Dec 15, 2023
9702793
fix compilation, bump bindings
Trivo25 Dec 15, 2023
7b5e8fa
fix doc comment
Trivo25 Dec 15, 2023
673c822
remove unused imports
Trivo25 Dec 15, 2023
108513c
resolve dependency issue
Trivo25 Dec 19, 2023
fc70678
resolve dependency issue, yet again
Trivo25 Dec 19, 2023
a8ff0aa
return UInt32 instead of Field
Trivo25 Dec 19, 2023
d4f27be
changelog
Trivo25 Dec 19, 2023
c1e6087
Merge branch 'main' into feature/gadgets-uint
Trivo25 Dec 19, 2023
e2785ab
fix correct bindings
Trivo25 Dec 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/bindings
8 changes: 4 additions & 4 deletions src/examples/zkprogram/gadgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ 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');
let res1 = Gadgets.rotate64(f, 2, 'left');
let res2 = Gadgets.rotate64(f, 2, 'right');

res1.assertEquals(Field(48));
res2.assertEquals(Field(3));
Expand All @@ -21,8 +21,8 @@ const BitwiseProver = ZkProgram({
privateInputs: [],
method: () => {
let a = Provable.witness(Field, () => Field(48));
let actualLeft = Gadgets.rotate(a, 2, 'left');
let actualRight = Gadgets.rotate(a, 2, 'right');
let actualLeft = Gadgets.rotate64(a, 2, 'left');
let actualRight = Gadgets.rotate64(a, 2, 'right');

let expectedLeft = Field(192);
actualLeft.assertEquals(expectedLeft);
Expand Down
39 changes: 39 additions & 0 deletions src/lib/gadgets/arithmetic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Field } from '../core.js';
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
import { Provable } from '../provable.js';
import { rangeCheck32 } from './range-check.js';

export { divMod32, addMod32 };

function divMod32(n: Field) {
if (n.isConstant()) {
let nBigInt = n.toBigInt();
let q = nBigInt / (1n << 32n);
let r = nBigInt - q * (1n << 32n);
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
return {
remainder: new Field(r),
quotient: new Field(q),
};
}

let qr = Provable.witness(Provable.Array(Field, 2), () => {
let nBigInt = n.toBigInt();
let q = nBigInt / (1n << 32n);
let r = nBigInt - q * (1n << 32n);
return [new Field(q), new Field(r)];
});
let [quotient, remainder] = qr;
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved

rangeCheck32(quotient);
rangeCheck32(remainder);

n.assertEquals(quotient.mul(1n << 32n).add(remainder));

return {
remainder,
quotient,
};
}

function addMod32(x: Field, y: Field) {
return divMod32(x.add(y)).remainder;
}
43 changes: 35 additions & 8 deletions src/lib/gadgets/bitwise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
divideWithRemainder,
toVar,
} from './common.js';
import { rangeCheck64 } from './range-check.js';
import { rangeCheck32, rangeCheck64 } from './range-check.js';
import { divMod32 } from './arithmetic.js';

export { xor, not, rotate, and, rightShift, leftShift };
export { xor, not, rotate64, rotate32, and, rightShift, leftShift };

function not(a: Field, length: number, checked: boolean = false) {
// check that input length is positive
Expand Down Expand Up @@ -196,7 +197,7 @@ function and(a: Field, b: Field, length: number) {
return outputAnd;
}

function rotate(
function rotate64(
field: Field,
bits: number,
direction: 'left' | 'right' = 'left'
Expand All @@ -209,16 +210,42 @@ function rotate(

if (field.isConstant()) {
assert(
field.toBigInt() < 2n ** BigInt(MAX_BITS),
field.toBigInt() < 1n << 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);
const [rotated] = rot64(field, bits, direction);
return rotated;
}

function rotate32(
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
field: Field,
bits: number,
direction: 'left' | 'right' = 'left'
) {
assert(bits <= 32 && bits > 0, 'bits must be between 0 and 32');

if (field.isConstant()) {
assert(
field.toBigInt() < 1n << 32n,
`rotation: expected field to be at most 32 bits, got ${field.toBigInt()}`
);
return new Field(Fp.rot(field.toBigInt(), bits, direction, 32));
}

let { quotient: excess, remainder: shifted } = divMod32(
field.mul(1n << BigInt(direction === 'left' ? bits : 32 - bits))
);

let rotated = shifted.add(excess);

rangeCheck32(rotated);

return rotated;
}

function rot(
function rot64(
field: Field,
bits: number,
direction: 'left' | 'right' = 'left'
Expand Down Expand Up @@ -296,7 +323,7 @@ function rightShift(field: Field, bits: number) {
);
return new Field(Fp.rightShift(field.toBigInt(), bits));
}
const [, excess] = rot(field, bits, 'right');
const [, excess] = rot64(field, bits, 'right');
return excess;
}

Expand All @@ -313,6 +340,6 @@ function leftShift(field: Field, bits: number) {
);
return new Field(Fp.leftShift(field.toBigInt(), bits));
}
const [, , shifted] = rot(field, bits, 'left');
const [, , shifted] = rot64(field, bits, 'left');
return shifted;
}
34 changes: 29 additions & 5 deletions src/lib/gadgets/bitwise.unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,16 @@ let Bitwise = ZkProgram({
return Gadgets.and(a, b, 64);
},
},
rot: {
rot32: {
privateInputs: [Field],
method(a: Field) {
return Gadgets.rotate(a, 12, 'left');
return Gadgets.rotate32(a, 12, 'left');
},
},
rot64: {
privateInputs: [Field],
method(a: Field) {
return Gadgets.rotate64(a, 12, 'left');
},
},
leftShift: {
Expand Down Expand Up @@ -104,7 +110,7 @@ await Bitwise.compile();
[2, 4, 8, 16, 32, 64].forEach((length) => {
equivalent({ from: [uint(length)], to: field })(
(x) => Fp.rot(x, 12, 'left'),
(x) => Gadgets.rotate(x, 12, 'left')
(x) => Gadgets.rotate64(x, 12, 'left')
);
equivalent({ from: [uint(length)], to: field })(
(x) => Fp.leftShift(x, 12),
Expand All @@ -116,6 +122,13 @@ await Bitwise.compile();
);
});

[2, 4, 8, 16, 32].forEach((length) => {
equivalent({ from: [uint(length)], to: field })(
(x) => Fp.rot(x, 12, 'left', 32),
(x) => Gadgets.rotate32(x, 12, 'left')
);
});

await equivalentAsync({ from: [uint(64), uint(64)], to: field }, { runs: 3 })(
(x, y) => {
return x ^ y;
Expand Down Expand Up @@ -167,7 +180,18 @@ await equivalentAsync({ from: [field], to: field }, { runs: 3 })(
return Fp.rot(x, 12, 'left');
},
async (x) => {
let proof = await Bitwise.rot(x);
let proof = await Bitwise.rot64(x);
return proof.publicOutput;
}
);

await equivalentAsync({ from: [field], to: field }, { runs: 3 })(
(x) => {
if (x >= 2n ** 32n) throw Error('Does not fit into 32 bits');
return Fp.rot(x, 12, 'left', 32);
},
async (x) => {
let proof = await Bitwise.rot32(x);
return proof.publicOutput;
}
);
Expand Down Expand Up @@ -229,6 +253,6 @@ let isJustRotate = ifNotAllConstant(
and(contains(rotChain), withoutGenerics(equals(rotChain)))
);

constraintSystem.fromZkProgram(Bitwise, 'rot', isJustRotate);
constraintSystem.fromZkProgram(Bitwise, 'rot64', isJustRotate);
constraintSystem.fromZkProgram(Bitwise, 'leftShift', isJustRotate);
constraintSystem.fromZkProgram(Bitwise, 'rightShift', isJustRotate);
121 changes: 114 additions & 7 deletions src/lib/gadgets/gadgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@ import {
compactMultiRangeCheck,
multiRangeCheck,
rangeCheck64,
rangeCheck32,
} from './range-check.js';
import { not, rotate, xor, and, leftShift, rightShift } from './bitwise.js';
import {
not,
rotate32,
rotate64,
xor,
and,
leftShift,
rightShift,
} from './bitwise.js';
import { Field } from '../core.js';
import { ForeignField, Field3 } from './foreign-field.js';
import { divMod32, addMod32 } from './arithmetic.js';

export { Gadgets };

Expand Down Expand Up @@ -39,6 +49,33 @@ const Gadgets = {
rangeCheck64(x: Field) {
return rangeCheck64(x);
},

/**
* Asserts that the input value is in the range [0, 2^32).
*
* This function proves that the provided field element can be represented with 32 bits.
* If the field element exceeds 32 bits, an error is thrown.
*
* @param x - The value to be range-checked.
*
* @throws Throws an error if the input value exceeds 32 bits.
*
* @example
* ```ts
* const x = Provable.witness(Field, () => Field(12345678n));
* Gadgets.rangeCheck32(x); // successfully proves 32-bit range
*
* const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));
* Gadgets.rangeCheck32(xLarge); // throws an error since input exceeds 32 bits
* ```
*
* **Note**: Small "negative" field element inputs are interpreted as large integers close to the field size,
* and don't pass the 32-bit check. If you want to prove that a value lies in the int64 range [-2^31, 2^31),
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
* you could use `rangeCheck32(x.add(1n << 31n))`.
*/
rangeCheck32(x: Field) {
return rangeCheck32(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 of a 64-bit representation rather than being discarded.
Expand All @@ -52,7 +89,7 @@ const Gadgets = {
* **Important:** The gadget assumes that its input is at most 64 bits in size.
*
* If the input exceeds 64 bits, the gadget is invalid and fails to prove correct execution of the rotation.
* To safely use `rotate()`, you need to make sure that the value passed in is range-checked to 64 bits;
* To safely use `rotate64()`, you need to make sure that the value passed in is range-checked to 64 bits;
* for example, using {@link Gadgets.rangeCheck64}.
*
* You can find more details about the implementation in the [Mina book](https://o1-labs.github.io/proof-systems/specs/kimchi.html?highlight=gates#rotation)
Expand All @@ -66,17 +103,55 @@ const Gadgets = {
* @example
* ```ts
* const x = Provable.witness(Field, () => Field(0b001100));
* const y = Gadgets.rotate(x, 2, 'left'); // left rotation by 2 bits
* const z = Gadgets.rotate(x, 2, 'right'); // right rotation by 2 bits
* const y = Gadgets.rotate64(x, 2, 'left'); // left rotation by 2 bits
* const z = Gadgets.rotate64(x, 2, 'right'); // right rotation by 2 bits
* y.assertEquals(0b110000);
* z.assertEquals(0b000011);
*
* const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));
* Gadgets.rotate64(xLarge, 32, "left"); // throws an error since input exceeds 64 bits
* ```
*/
rotate64(field: Field, bits: number, direction: 'left' | 'right' = 'left') {
return rotate64(field, bits, direction);
},
/**
* 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 of a 32-bit representation 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 big-endian 32-bit representation of the number,
* where the most significant (32th) 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 gadget assumes that its input is at most 32 bits in size.
*
* If the input exceeds 32 bits, the gadget is invalid and fails to prove correct execution of the rotation.
* To safely use `rotate32()`, you need to make sure that the value passed in is range-checked to 32 bits;
* for example, using {@link Gadgets.rangeCheck32}.
*
*
* @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 32 bits.
*
* @example
* ```ts
* const x = Provable.witness(Field, () => Field(0b001100));
* const y = Gadgets.rotate32(x, 2, 'left'); // left rotation by 2 bits
* const z = Gadgets.rotate32(x, 2, 'right'); // right rotation by 2 bits
* y.assertEquals(0b110000);
* z.assertEquals(0b000011);
*
* const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));
* Gadgets.rotate(xLarge, 32, "left"); // throws an error since input exceeds 64 bits
* Gadgets.rotate32(xLarge, 32, "left"); // throws an error since input exceeds 32 bits
* ```
*/
rotate(field: Field, bits: number, direction: 'left' | 'right' = 'left') {
return rotate(field, bits, direction);
rotate32(field: Field, bits: number, direction: 'left' | 'right' = 'left') {
return rotate32(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).
Expand Down Expand Up @@ -420,6 +495,38 @@ const Gadgets = {
* **Note:** This interface does not contain any provable methods.
*/
Field3,
/**
* Division modulo 2^32. The operation decomposes a {@link Field} element into two 32-bit limbs, `remainder` and `quotient`, using the following equation: `n = quotient * 2^32 + remainder`.
*
* Asserts that both `remainder` and `quotient` are in the range [0, 2^32) using {@link Gadgets.rangeCheck32}.
*
* @example
* ```ts
* let n = Field((1n << 32n) + 8n)
* let { remainder, quotient } = Gadgets.divMod32(n);
* // remainder = 8, quotient = 1
*
* n.assertEquals(quotient.mul(1n << 32n).add(remainder));
* ```
*/
divMod32,

/**
* Addition modulo 2^32. The operation adds two {@link Field} elements and returns the result modulo 2^32.
*
* Asserts that the result is in the range [0, 2^32) using {@link Gadgets.rangeCheck32}.
*
* It uses {@link Gadgets.divMod32} internally by adding the two {@link Field} elements and then decomposing the result into `remainder` and `quotient` and returning the `remainder`.
*
* @example
* ```ts
* let a = Field(8n);
* let b = Field(1n << 32n);
*
* Gadgets.addMod32(a, b).assertEquals(Field(8n));
* ```
* */
addMod32,
};

export namespace Gadgets {
Expand Down
Loading