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

implements XOR gadget #1177

Merged
merged 63 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
ca2959b
add Field.sizeInBits()
Trivo25 Oct 11, 2023
990730d
add gate definitions
Trivo25 Oct 12, 2023
b108d96
add gates
Trivo25 Oct 12, 2023
9584382
first draft of bitwise XOR gadget
Trivo25 Oct 12, 2023
09e3f37
bump bindings
Trivo25 Oct 12, 2023
ca54b50
WIP bitwise XOR draft
Trivo25 Oct 12, 2023
b69ac01
bump bindings
Trivo25 Oct 12, 2023
f3bfc03
initial refactor
Trivo25 Oct 12, 2023
77cd4ae
add basic example, dummy example
Trivo25 Oct 12, 2023
03c138a
make proving work, temporary
Trivo25 Oct 12, 2023
957cd54
fix stackoverflow
Trivo25 Oct 12, 2023
24acc09
more example code
Trivo25 Oct 12, 2023
a1b5c94
expose helper
Trivo25 Oct 12, 2023
89aebb7
simplify
Trivo25 Oct 12, 2023
f9def76
remove debug code
Trivo25 Oct 12, 2023
8235424
bump bindings
Trivo25 Oct 12, 2023
6d79928
simplify more
Trivo25 Oct 12, 2023
ea423df
integrate API surface
Trivo25 Oct 12, 2023
bb61d3c
basic unit tests
Trivo25 Oct 12, 2023
6f081c9
minor
Trivo25 Oct 12, 2023
cd1cbbe
minor
Trivo25 Oct 12, 2023
f9a0935
simplify
Trivo25 Oct 16, 2023
e29c3bf
Merge branch 'main' into feature/XOR-gadget
Trivo25 Oct 16, 2023
54f0a74
Merge branch 'main' into feature/XOR-gadget
Trivo25 Oct 17, 2023
03fe713
Merge branch 'main' into feature/XOR-gadget
Trivo25 Oct 17, 2023
33db00d
add tests
Trivo25 Oct 17, 2023
bce7ffe
dump vks
Trivo25 Oct 17, 2023
70ba811
bump bindings
Trivo25 Oct 17, 2023
458bd46
add doc commnets to gate
Trivo25 Oct 17, 2023
5b87fb7
remove circular dependency
Trivo25 Oct 17, 2023
8f9c9a8
move gadget tests
Trivo25 Oct 17, 2023
f3505cf
cleanup
Trivo25 Oct 17, 2023
08c5d10
cleanup example
Trivo25 Oct 17, 2023
92de8ec
clean up unit test
Trivo25 Oct 17, 2023
8991e3b
fix unit test
Trivo25 Oct 17, 2023
1c25be8
address feedback
Trivo25 Oct 17, 2023
598bdd0
address feedback
Trivo25 Oct 18, 2023
35dfd8a
change zero gate name
Trivo25 Oct 18, 2023
d0c0592
fix merge conflict
Trivo25 Oct 18, 2023
1d4a2dd
fix merge conflict
Trivo25 Oct 18, 2023
a24bc2d
fix tests
Trivo25 Oct 18, 2023
fd91ab1
improve doc comments
Trivo25 Oct 18, 2023
eec0cb8
bump bindings
Trivo25 Oct 18, 2023
0540b67
bump bindings
Trivo25 Oct 18, 2023
ac9eb59
Merge branch 'feature/XOR-gadget' of ssh://github.com/o1-labs/snarkyj…
Trivo25 Oct 18, 2023
82c89e5
fix unit tests
Trivo25 Oct 18, 2023
7817dde
fix prover error and dump vks
Trivo25 Oct 19, 2023
aa9b9ae
fix chain end values
Trivo25 Oct 19, 2023
b5b34b0
revert precalculation of inputs
Trivo25 Oct 19, 2023
e1c22af
dump vks
Trivo25 Oct 19, 2023
eb8143b
move constants
Trivo25 Oct 19, 2023
043f0ef
dump regression test vks
Trivo25 Oct 19, 2023
36830a8
hardcode chunk size
Trivo25 Oct 19, 2023
1f54f64
fix length doc comment
Trivo25 Oct 20, 2023
7c01398
fix doc comment
Trivo25 Oct 20, 2023
d162672
fix size assertion
Trivo25 Oct 20, 2023
09ad52e
Update src/lib/gadgets/bitwise.ts
Trivo25 Oct 20, 2023
5df6d8b
address feedback
Trivo25 Oct 24, 2023
13b0701
changelog
Trivo25 Oct 24, 2023
4a8978f
Merge branch 'main' into feature/XOR-gadget
Trivo25 Oct 24, 2023
fa408e8
bump bindings
Trivo25 Oct 24, 2023
08a5115
adjust comments
Trivo25 Oct 24, 2023
09cad2c
add disclaimer to doc comment
Trivo25 Oct 24, 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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ 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 `XOR` operation support 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.

Expand Down
2 changes: 1 addition & 1 deletion src/bindings
31 changes: 31 additions & 0 deletions src/examples/gadgets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Field, Provable, Gadgets, Experimental } from 'o1js';

const XOR = Experimental.ZkProgram({
methods: {
baseCase: {
privateInputs: [],
method: () => {
let a = Provable.witness(Field, () => Field(5));
let b = Provable.witness(Field, () => Field(2));
let actual = Gadgets.xor(a, b, 4);
let expected = Field(7);
actual.assertEquals(expected);
},
},
},
});

console.log('compiling..');

console.time('compile');
await XOR.compile();
console.timeEnd('compile');

console.log('proving..');

console.time('prove');
let proof = await XOR.baseCase();
console.timeEnd('prove');

if (!(await XOR.verify(proof))) throw Error('Invalid proof');
else console.log('proof valid');
14 changes: 13 additions & 1 deletion src/examples/primitive_constraint_system.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Field, Group, Poseidon, Provable, Scalar } from 'o1js';
import { Field, Group, Gadgets, Provable, Scalar } from 'o1js';

function mock(obj: { [K: string]: (...args: any) => void }, name: string) {
let methodKeys = Object.keys(obj);
Expand Down Expand Up @@ -63,4 +63,16 @@ const GroupMock = {
},
};

const BitwiseMock = {
xor() {
let a = Provable.witness(Field, () => new Field(5n));
let b = Provable.witness(Field, () => new Field(5n));
Gadgets.xor(a, b, 16);
Gadgets.xor(a, b, 32);
Gadgets.xor(a, b, 48);
Gadgets.xor(a, b, 64);
},
};

export const GroupCS = mock(GroupMock, 'Group Primitive');
export const BitwiseCS = mock(BitwiseMock, 'Bitwise Primitive');
13 changes: 13 additions & 0 deletions src/examples/regression_test.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,5 +164,18 @@
"data": "",
"hash": ""
}
},
"Bitwise Primitive": {
"digest": "Bitwise Primitive",
"methods": {
"xor": {
"rows": 15,
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
"digest": "b3595a9cc9562d4f4a3a397b6de44971"
}
},
"verificationKey": {
"data": "",
"hash": ""
}
}
}
3 changes: 2 additions & 1 deletion src/examples/vk_regression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -37,6 +37,7 @@ const ConstraintSystems: MinimumConstraintSystem[] = [
TokenContract,
createDex().Dex,
GroupCS,
BitwiseCS,
];

let filePath = jsonPath ? jsonPath : './src/examples/regression_test.json';
Expand Down
16 changes: 14 additions & 2 deletions src/lib/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export {
withMessage,
readVarMessage,
toConstantField,
toFp,
};

type FieldConst = [0, bigint];
Expand Down Expand Up @@ -1245,13 +1246,24 @@ class Field {
/**
* **Warning**: This function is mainly for internal use. Normally it is not intended to be used by a zkApp developer.
*
* As all {@link Field} elements have 31 bits, this function returns 31.
* As all {@link Field} elements have 32 bytes, this function returns 32.
*
* @return The size of a {@link Field} element - 31.
* @return The size of a {@link Field} element - 32.
*/
static sizeInBytes() {
return Fp.sizeInBytes();
}

/**
* **Warning**: This function is mainly for internal use. Normally it is not intended to be used by a zkApp developer.
mitschabaude marked this conversation as resolved.
Show resolved Hide resolved
*
* As all {@link Field} elements have 255 bits, this function returns 255.
*
* @return The size of a {@link Field} element in bits - 255.
*/
static sizeInBits() {
return Fp.sizeInBits;
}
}

const FieldBinable = defineBinable({
Expand Down
131 changes: 131 additions & 0 deletions src/lib/gadgets/bitwise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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';

export { xor };

function xor(a: Field, b: Field, length: number) {
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
// check that both input lengths are positive
assert(length > 0, `Input lengths need to be positive values.`);

// check that length does not exceed maximum field size in bits
assert(
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
length <= Field.sizeInBits(),
`Length ${length} exceeds maximum of ${Field.sizeInBits()} bits.`
);

// obtain pad length until the length is a multiple of 16 for n-bit length lookup table
let padLength = Math.ceil(length / 16) * 16;

// handle constant case
if (a.isConstant() && b.isConstant()) {
let max = 1n << BigInt(padLength);

assert(
a.toBigInt() < max,
`${a.toBigInt()} does not fit into ${padLength} bits`
);

assert(
b.toBigInt() < max,
`${b.toBigInt()} does not fit into ${padLength} bits`
);

return new Field(Fp.xor(a.toBigInt(), b.toBigInt()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not familiar with the types in o1js so this might be a basic question. Is converting to BigInt the desired behavior here? Since the inputs are Field, I wonder why this conversion needs to take place.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case only happens if both elements are constants (not variables that the prover needs to provide). Because of that, we can just convert them to bigint and do our computation there and convert them back. Doing so is more efficient than going through snarky and letting snarky do the calculation on constants. This is a change we did somewhat recently: Computations on constants are done on native javascript types

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooh, interesting, thank you

}

// calculate expected xor output
let outputXor = Provable.witness(
Field,
() => new Field(Fp.xor(a.toBigInt(), b.toBigInt()))
);

// builds the xor gadget chain
buildXor(a, b, outputXor, padLength);

// return the result of the xor operation
return outputXor;
}

// builds a xor chain
function buildXor(
a: Field,
b: Field,
expectedOutput: Field,
padLength: number
) {
// construct the chain of XORs until padLength is 0
while (padLength !== 0) {
// slices the inputs into 4x 4bit-sized chunks
// slices of a
let in1_0 = witnessSlices(a, 0, 4);
let in1_1 = witnessSlices(a, 4, 4);
let in1_2 = witnessSlices(a, 8, 4);
let in1_3 = witnessSlices(a, 12, 4);

// slices of b
let in2_0 = witnessSlices(b, 0, 4);
let in2_1 = witnessSlices(b, 4, 4);
let in2_2 = witnessSlices(b, 8, 4);
let in2_3 = witnessSlices(b, 12, 4);

// slices of expected output
let out0 = witnessSlices(expectedOutput, 0, 4);
let out1 = witnessSlices(expectedOutput, 4, 4);
let out2 = witnessSlices(expectedOutput, 8, 4);
let out3 = witnessSlices(expectedOutput, 12, 4);

// assert that the xor of the slices is correct, 16 bit at a time
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
Gates.xor(
a,
b,
expectedOutput,
in1_0,
in1_1,
in1_2,
in1_3,
in2_0,
in2_1,
in2_2,
in2_3,
out0,
out1,
out2,
out3
);

// update the values for the next loop iteration
a = witnessNextValue(a);
b = witnessNextValue(b);
expectedOutput = witnessNextValue(expectedOutput);
padLength = padLength - 16;
}

// inputs are zero and length is zero, add the zero check - we reached the end of our chain
Gates.zero(a, b, expectedOutput);
querolita marked this conversation as resolved.
Show resolved Hide resolved

let zero = new Field(0);
zero.assertEquals(a);
zero.assertEquals(b);
zero.assertEquals(expectedOutput);
}

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');
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved

return Provable.witness(Field, () => {
let n = f.toBigInt();
return new Field((n >> BigInt(start)) & ((1n << BigInt(length)) - 1n));
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
});
}

function witnessNextValue(current: Field) {
return Provable.witness(Field, () => new Field(current.toBigInt() >> 16n));
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
}
58 changes: 58 additions & 0 deletions src/lib/gadgets/bitwise.unit-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ZkProgram } from '../proof_system.js';
import {
Spec,
equivalent,
equivalentAsync,
field,
fieldWithRng,
} from '../testing/equivalent.js';
import { Fp, mod } from '../../bindings/crypto/finite_field.js';
import { Field } from '../field.js';
import { Gadgets } from './gadgets.js';
import { Random } from '../testing/property.js';

let Bitwise = ZkProgram({
publicOutput: Field,
methods: {
xor: {
privateInputs: [Field, Field],
method(a: Field, b: Field) {
return Gadgets.xor(a, b, 64);
},
},
},
});

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 })(
Fp.xor,
(x, y) => Gadgets.xor(x, y, length)
);
});

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

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

export { Gadgets };
Expand Down Expand Up @@ -33,4 +34,31 @@ const Gadgets = {
rangeCheck64(x: Field) {
return rangeCheck64(x);
},

/**
* 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.
*
* This gadget builds a chain of XOR gates recursively. Each XOR gate can verify 16 bit at most. If your input elements exceed 16 bit, another XOR gate will be added to the chain.
*
* 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, note that using a smaller length can let the verifier infer the length of the inputs. So even if the zkapp developer knew that the inputs to some program will be <32bits, they should still consider if they truly want a smaller gadget or a larger one that leaks less side information about the data.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhhh this is actually a super good point, I love that!

* 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..
* 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
*
* let c = xor(a, b, 2); // ... 000110
* c.assertEquals(6);
* ```
*/
xor(a: Field, b: Field, length: number) {
return xor(a, b, length);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ let maybeUint64: Spec<bigint, Field> = {
// 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;
Expand Down
Loading
Loading