Skip to content

Commit

Permalink
Merge pull request #1259 from o1-labs/feature/gadgets-uint
Browse files Browse the repository at this point in the history
add gadgets to API surface, UInt32, UInt64, implement rot32
  • Loading branch information
mitschabaude committed Dec 19, 2023
2 parents 3480c54 + e2785ab commit d68441b
Show file tree
Hide file tree
Showing 14 changed files with 923 additions and 106 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,31 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased](https://github.com/o1-labs/o1js/compare/7acf19d0d...HEAD)

### Breaking changes

- Rename `Gadgets.rotate()` to `Gadgets.rotate64()` to better reflect the amount of bits the gadget operates on. https://github.com/o1-labs/o1js/pull/1259
- Rename `Gadgets.{leftShift(), rightShift()}` to `Gadgets.{leftShift64(), rightShift64()}` to better reflect the amount of bits the gadget operates on. https://github.com/o1-labs/o1js/pull/1259

### Added

- Non-native elliptic curve operations exposed through `createForeignCurve()` class factory https://github.com/o1-labs/o1js/pull/1007
- **ECDSA signature verification** exposed through `createEcdsa()` class factory https://github.com/o1-labs/o1js/pull/1240 https://github.com/o1-labs/o1js/pull/1007

- For an example, see `./src/examples/crypto/ecdsa`

- `Gadgets.rotate32()` for rotation over 32 bit values https://github.com/o1-labs/o1js/pull/1259
- `Gadgets.leftShift32()` for left shift over 32 bit values https://github.com/o1-labs/o1js/pull/1259
- `Gadgets.divMod32()` division modulo 2^32 that returns the remainder and quotient of the operation https://github.com/o1-labs/o1js/pull/1259
- `Gadgets.rangeCheck32()` range check for 32 bit values https://github.com/o1-labs/o1js/pull/1259
- `Gadgets.addMod32()` addition modulo 2^32 https://github.com/o1-labs/o1js/pull/1259
- Expose new bitwise gadgets on `UInt32` and `UInt64` https://github.com/o1-labs/o1js/pull/1259
- bitwise XOR via `{UInt32, UInt64}.xor()`
- bitwise NOT via `{UInt32, UInt64}.not()`
- bitwise ROTATE via `{UInt32, UInt64}.rotate()`
- bitwise LEFTSHIFT via `{UInt32, UInt64}.leftShift()`
- bitwise RIGHTSHIFT via `{UInt32, UInt64}.rightShift()`
- bitwise AND via `{UInt32, UInt64}.and()`

### Fixed

- Fix stack overflows when calling provable methods with large inputs https://github.com/o1-labs/o1js/pull/1334
Expand Down
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
48 changes: 48 additions & 0 deletions src/lib/gadgets/arithmetic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { provableTuple } from '../circuit_value.js';
import { Field } from '../core.js';
import { assert } from '../errors.js';
import { Provable } from '../provable.js';
import { rangeCheck32 } from './range-check.js';

export { divMod32, addMod32 };

function divMod32(n: Field) {
if (n.isConstant()) {
assert(
n.toBigInt() < 1n << 64n,
`n needs to fit into 64 bit, but got ${n.toBigInt()}`
);

let nBigInt = n.toBigInt();
let q = nBigInt >> 32n;
let r = nBigInt - (q << 32n);
return {
remainder: new Field(r),
quotient: new Field(q),
};
}

let [quotient, remainder] = Provable.witness(
provableTuple([Field, Field]),
() => {
let nBigInt = n.toBigInt();
let q = nBigInt >> 32n;
let r = nBigInt - (q << 32n);
return [new Field(q), new Field(r)];
}
);

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;
}
59 changes: 59 additions & 0 deletions src/lib/gadgets/arithmetic.unit-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ZkProgram } from '../proof_system.js';
import {
equivalentProvable as equivalent,
equivalentAsync,
field,
record,
} from '../testing/equivalent.js';
import { Field } from '../core.js';
import { Gadgets } from './gadgets.js';
import { provable } from '../circuit_value.js';
import { assert } from './common.js';

let Arithmetic = ZkProgram({
name: 'arithmetic',
publicOutput: provable({
remainder: Field,
quotient: Field,
}),
methods: {
divMod32: {
privateInputs: [Field],
method(a: Field) {
return Gadgets.divMod32(a);
},
},
},
});

await Arithmetic.compile();

const divMod32Helper = (x: bigint) => {
let quotient = x >> 32n;
let remainder = x - (quotient << 32n);
return { remainder, quotient };
};
const divMod32Output = record({ remainder: field, quotient: field });

equivalent({
from: [field],
to: divMod32Output,
})(
(x) => {
assert(x < 1n << 64n, `x needs to fit in 64bit, but got ${x}`);
return divMod32Helper(x);
},
(x) => {
return Gadgets.divMod32(x);
}
);

await equivalentAsync({ from: [field], to: divMod32Output }, { runs: 3 })(
(x) => {
assert(x < 1n << 64n, `x needs to fit in 64bit, but got ${x}`);
return divMod32Helper(x);
},
async (x) => {
return (await Arithmetic.divMod32(x)).publicOutput;
}
);
63 changes: 52 additions & 11 deletions src/lib/gadgets/bitwise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,19 @@ import {
exists,
bitSlice,
} from './common.js';
import { rangeCheck64 } from './range-check.js';

export { xor, not, rotate, and, rightShift, leftShift };
import { rangeCheck32, rangeCheck64 } from './range-check.js';
import { divMod32 } from './arithmetic.js';

export {
xor,
not,
rotate64,
rotate32,
and,
rightShift64,
leftShift64,
leftShift32,
};

function not(a: Field, length: number, checked: boolean = false) {
// check that input length is positive
Expand Down Expand Up @@ -187,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 @@ -200,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(), BigInt(bits), direction));
}
const [rotated] = rot(field, bits, direction);
const [rotated] = rot64(field, bits, direction);
return rotated;
}

function rotate32(
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(), BigInt(bits), direction, 32n));
}

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 @@ -281,7 +317,7 @@ function rot(
return [rotated, excess, shifted];
}

function rightShift(field: Field, bits: number) {
function rightShift64(field: Field, bits: number) {
assert(
bits >= 0 && bits <= MAX_BITS,
`rightShift: expected bits to be between 0 and 64, got ${bits}`
Expand All @@ -294,11 +330,11 @@ 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;
}

function leftShift(field: Field, bits: number) {
function leftShift64(field: Field, bits: number) {
assert(
bits >= 0 && bits <= MAX_BITS,
`rightShift: expected bits to be between 0 and 64, got ${bits}`
Expand All @@ -311,6 +347,11 @@ 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;
}

function leftShift32(field: Field, bits: number) {
let { remainder: shifted } = divMod32(field.mul(1n << BigInt(bits)));
return shifted;
}
Loading

0 comments on commit d68441b

Please sign in to comment.