Skip to content

Commit

Permalink
Merge pull request #1194 from o1-labs/feat/LCL-LSR-gadget
Browse files Browse the repository at this point in the history
Add left shift and right shift gadgets to o1js
  • Loading branch information
MartinMinkov authored Nov 2, 2023
2 parents 56975fc + 358e3d8 commit 1500523
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 52 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

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

> No unreleased changes yet
### Added

- `Gadgets.leftShift() / Gadgets.rightShift()`, new provable method to support bitwise shifting for native field elements. https://github.com/o1-labs/o1js/pull/1194
- `Gadgets.and()`, new provable method to support bitwise and for native field elements. https://github.com/o1-labs/o1js/pull/1193

## [0.14.0](https://github.com/o1-labs/o1js/compare/045faa7...e8e7510e1)

Expand Down
2 changes: 1 addition & 1 deletion src/bindings
10 changes: 10 additions & 0 deletions src/examples/primitive_constraint_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ const BitwiseMock = {
Gadgets.xor(a, b, 48);
Gadgets.xor(a, b, 64);
},
leftShift() {
let a = Provable.witness(Field, () => new Field(12));
Gadgets.leftShift(a, 2);
Gadgets.leftShift(a, 4);
},
rightShift() {
let a = Provable.witness(Field, () => new Field(12));
Gadgets.rightShift(a, 2);
Gadgets.rightShift(a, 4);
},
and() {
let a = Provable.witness(Field, () => new Field(5n));
let b = Provable.witness(Field, () => new Field(5n));
Expand Down
8 changes: 8 additions & 0 deletions src/examples/regression_test.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@
"rows": 15,
"digest": "b3595a9cc9562d4f4a3a397b6de44971"
},
"leftShift": {
"rows": 7,
"digest": "66de39ad3dd5807f760341ec85a6cc41"
},
"rightShift": {
"rows": 7,
"digest": "a32264f2d4c3092f30d600fa9506385b"
},
"and": {
"rows": 19,
"digest": "647e6fd1852873d1c326ba1cd269cff2"
Expand Down
36 changes: 35 additions & 1 deletion src/lib/gadgets/bitwise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from './common.js';
import { rangeCheck64 } from './range-check.js';

export { xor, and, rotate };
export { xor, rotate, and, rightShift, leftShift };

function xor(a: Field, b: Field, length: number) {
// check that both input lengths are positive
Expand Down Expand Up @@ -245,3 +245,37 @@ function rot(
rangeCheck64(excess);
return [rotated, excess, shifted];
}

function rightShift(field: Field, bits: number) {
assert(
bits >= 0 && bits <= MAX_BITS,
`rightShift: expected bits to be between 0 and 64, got ${bits}`
);

if (field.isConstant()) {
assert(
field.toBigInt() < 2n ** BigInt(MAX_BITS),
`rightShift: expected field to be at most 64 bits, got ${field.toBigInt()}`
);
return new Field(Fp.rightShift(field.toBigInt(), bits));
}
const [, excess] = rot(field, bits, 'right');
return excess;
}

function leftShift(field: Field, bits: number) {
assert(
bits >= 0 && bits <= MAX_BITS,
`rightShift: expected bits to be between 0 and 64, got ${bits}`
);

if (field.isConstant()) {
assert(
field.toBigInt() < 2n ** BigInt(MAX_BITS),
`rightShift: expected field to be at most 64 bits, got ${field.toBigInt()}`
);
return new Field(Fp.leftShift(field.toBigInt(), bits));
}
const [, , shifted] = rot(field, bits, 'left');
return shifted;
}
102 changes: 56 additions & 46 deletions src/lib/gadgets/bitwise.unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ import {
import { Fp, mod } from '../../bindings/crypto/finite_field.js';
import { Field } from '../core.js';
import { Gadgets } from './gadgets.js';
import { test, Random } from '../testing/property.js';
import { Provable } from '../provable.js';
import { Random } from '../testing/property.js';

let maybeUint64: Spec<bigint, Field> = {
...field,
rng: Random.map(Random.oneOf(Random.uint64, Random.uint64.invalid), (x) =>
mod(x, Field.ORDER)
),
};

let uint = (length: number) => fieldWithRng(Random.biguint(length));

let Bitwise = ZkProgram({
name: 'bitwise',
Expand All @@ -34,13 +42,23 @@ let Bitwise = ZkProgram({
return Gadgets.rotate(a, 12, 'left');
},
},
leftShift: {
privateInputs: [Field],
method(a: Field) {
return Gadgets.leftShift(a, 12);
},
},
rightShift: {
privateInputs: [Field],
method(a: Field) {
return Gadgets.rightShift(a, 12);
},
},
},
});

await Bitwise.compile();

let uint = (length: number) => fieldWithRng(Random.biguint(length));

[2, 4, 8, 16, 32, 64, 128].forEach((length) => {
equivalent({ from: [uint(length), uint(length)], to: field })(
(x, y) => x ^ y,
Expand All @@ -52,27 +70,20 @@ 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<bigint, Field> = {
...field,
rng: Random.map(Random.oneOf(Random.uint64, Random.uint64.invalid), (x) =>
mod(x, Field.ORDER)
),
};
[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')
);
equivalent({ from: [uint(length)], to: field })(
(x) => Fp.leftShift(x, 12),
(x) => Gadgets.leftShift(x, 12)
);
equivalent({ from: [uint(length)], to: field })(
(x) => Fp.rightShift(x, 12),
(x) => Gadgets.rightShift(x, 12)
);
});

await equivalentAsync(
{ from: [maybeUint64, maybeUint64], to: field },
Expand Down Expand Up @@ -115,25 +126,24 @@ await equivalentAsync({ from: [field], to: field }, { runs: 3 })(
}
);

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})`);
});
}
await equivalentAsync({ from: [field], to: field }, { runs: 3 })(
(x) => {
if (x >= 2n ** 64n) throw Error('Does not fit into 64 bits');
return Fp.leftShift(x, 12);
},
async (x) => {
let proof = await Bitwise.leftShift(x);
return proof.publicOutput;
}
);

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));
await equivalentAsync({ from: [field], to: field }, { runs: 3 })(
(x) => {
if (x >= 2n ** 64n) throw Error('Does not fit into 64 bits');
return Fp.rightShift(x, 12);
},
async (x) => {
let proof = await Bitwise.rightShift(x);
return proof.publicOutput;
}
);
73 changes: 70 additions & 3 deletions src/lib/gadgets/gadgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Wrapper file for various gadgets, with a namespace and doccomments.
*/
import { rangeCheck64 } from './range-check.js';
import { rotate, xor, and } from './bitwise.js';
import { rotate, xor, and, leftShift, rightShift } from './bitwise.js';
import { Field } from '../core.js';

export { Gadgets };
Expand Down Expand Up @@ -46,7 +46,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 does not prove correct execution of the rotation.
* 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;
* for example, using {@link Gadgets.rangeCheck64}.
*
Expand Down Expand Up @@ -106,6 +106,72 @@ const Gadgets = {
xor(a: Field, b: Field, length: number) {
return xor(a, b, length);
},

/**
* Performs a left shift operation on the provided {@link Field} element.
* This operation is similar to the `<<` shift operation in JavaScript,
* where bits are shifted to the left, and the overflowing bits are discarded.
*
* It’s important to note that these operations are performed considering the big-endian 64-bit representation of the number,
* where the most significant (64th) bit is on the left end and the least significant bit is on the right end.
*
* **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 fails to prove correct execution of the shift.
* Therefore, to safely use `leftShift()`, 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 shift.
* @param bits Amount of bits to shift the {@link Field} element to the left. The amount should be between 0 and 64 (or else the shift will fail).
*
* @throws Throws an error if the input value exceeds 64 bits.
*
* @example
* ```ts
* const x = Provable.witness(Field, () => Field(0b001100)); // 12 in binary
* const y = Gadgets.leftShift(x, 2); // left shift by 2 bits
* y.assertEquals(0b110000); // 48 in binary
*
* const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));
* leftShift(xLarge, 32); // throws an error since input exceeds 64 bits
* ```
*/
leftShift(field: Field, bits: number) {
return leftShift(field, bits);
},

/**
* Performs a right shift operation on the provided {@link Field} element.
* This is similar to the `>>` shift operation in JavaScript, where bits are moved to the right.
* The `rightShift` function utilizes the rotation method internally to implement this operation.
*
* * It’s important to note that these operations are performed considering the big-endian 64-bit representation of the number,
* where the most significant (64th) bit is on the left end and the least significant bit is on the right end.
*
* **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 fails to prove correct execution of the shift.
* To safely use `rightShift()`, you need to make sure that the value passed in is range-checked to 64 bits;
* for example, using {@link Gadgets.rangeCheck64}.
*
* @param field {@link Field} element to shift.
* @param bits Amount of bits to shift the {@link Field} element to the right. The amount should be between 0 and 64 (or else the shift will fail).
*
* @throws Throws an error if the input value exceeds 64 bits.
*
* @example
* ```ts
* const x = Provable.witness(Field, () => Field(0b001100)); // 12 in binary
* const y = Gadgets.rightShift(x, 2); // right shift by 2 bits
* y.assertEquals(0b000011); // 3 in binary
*
* const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));
* rightShift(xLarge, 32); // throws an error since input exceeds 64 bits
* ```
*/
rightShift(field: Field, bits: number) {
return rightShift(field, bits);
},
/**
* Bitwise AND gadget on {@link Field} elements. Equivalent to the [bitwise AND `&` operator in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_AND).
* The AND gate works by comparing two bits and returning `1` if both bits are `1`, and `0` otherwise.
Expand All @@ -128,11 +194,12 @@ const Gadgets = {
* **Note:** 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`), `and()` will fail for any input that is larger than `2**16`.
*
* @example
* ```typescript
* let a = Field(3); // ... 000011
* let b = Field(5); // ... 000101
*
* let c = and(a, b, 2); // ... 000001
* let c = Gadgets.and(a, b, 2); // ... 000001
* c.assertEquals(1);
* ```
*/
Expand Down

0 comments on commit 1500523

Please sign in to comment.