From 00baab94486e474b4da15803b5263d1d2895bed1 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 09:39:16 +0200 Subject: [PATCH 01/14] lift logic from bindings test into test lib --- src/lib/testing/equivalent.ts | 71 ++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index c50c662a0a..0a60f7986e 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -5,7 +5,70 @@ import { test, Random } from '../testing/property.js'; import { Provable } from '../provable.js'; import { deepEqual } from 'node:assert/strict'; -export { createEquivalenceTesters, throwError, handleErrors }; +export { + equivalent, + createEquivalenceTesters, + throwError, + handleErrors, + deepEqual as defaultAssertEqual, + id, +}; +export { Spec, ToSpec, FromSpec, SpecFromFunctions }; + +// a `Spec` tells us how to compare two functions + +type FromSpec = { + // `rng` creates random inputs to the first function + rng: Random; + + // `there` converts to inputs to the second function + there: (x: In1) => In2; +}; + +type ToSpec = { + // `back` converts outputs of the second function back to match the first function + back: (x: Out2) => Out1; + + // `assertEqual` to compare outputs against each other; defaults to `deepEqual` + assertEqual?: (x: Out1, y: Out1, message: string) => void; +}; + +type Spec = FromSpec & ToSpec; + +type FuncSpec, Out1, In2 extends Tuple, Out2> = { + from: { + [k in keyof In1]: k extends keyof In2 ? FromSpec : never; + }; + to: ToSpec; +}; + +type SpecFromFunctions< + F1 extends AnyFunction, + F2 extends AnyFunction +> = FuncSpec, ReturnType, Parameters, ReturnType>; + +function equivalent, Out1, In2 extends Tuple, Out2>( + { from, to }: FuncSpec, + f1: (...args: In1) => Out1, + f2: (...args: In2) => Out2, + label?: string +) { + let generators = from.map((spec) => spec.rng); + let assertEqual = to.assertEqual ?? deepEqual; + test(...(generators as any[]), (...args) => { + args.pop(); + let inputs = args as any as In1; + handleErrors( + () => f1(...inputs), + () => + to.back(f2(...(inputs.map((x, i) => from[i].there(x)) as any as In2))), + (x, y) => assertEqual(x, y, label ?? 'same results'), + label + ); + }); +} + +let id = (x: T) => x; function createEquivalenceTesters( Field: Provable, @@ -165,3 +228,9 @@ function handleErrors( function throwError(message?: string): any { throw Error(message); } + +// helper types + +type AnyFunction = (...args: any) => any; + +type Tuple = [] | [T, ...T[]]; From 0876d8f71e5d4172749a2807e50df5a51eadc6eb Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 11:35:34 +0200 Subject: [PATCH 02/14] abstract provable equivalence tests using function specs --- src/lib/field.unit-test.ts | 2 +- src/lib/testing/equivalent.ts | 206 +++++++++++++++++----------------- 2 files changed, 106 insertions(+), 102 deletions(-) diff --git a/src/lib/field.unit-test.ts b/src/lib/field.unit-test.ts index f28f688bb3..67f007ab79 100644 --- a/src/lib/field.unit-test.ts +++ b/src/lib/field.unit-test.ts @@ -63,7 +63,7 @@ let SmallField = Random.reject( ); let { equivalent1, equivalent2, equivalentVoid1, equivalentVoid2 } = - createEquivalenceTesters(Field, Field); + createEquivalenceTesters(); // arithmetic, both in- and outside provable code equivalent2((x, y) => x.add(y), Fp.add); diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index 0a60f7986e..bf4aa22882 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -4,6 +4,7 @@ import { test, Random } from '../testing/property.js'; import { Provable } from '../provable.js'; import { deepEqual } from 'node:assert/strict'; +import { Field } from '../core.js'; export { equivalent, @@ -51,7 +52,7 @@ function equivalent, Out1, In2 extends Tuple, Out2>( { from, to }: FuncSpec, f1: (...args: In1) => Out1, f2: (...args: In2) => Out2, - label?: string + label = 'expect equal results' ) { let generators = from.map((spec) => spec.rng); let assertEqual = to.assertEqual ?? deepEqual; @@ -62,134 +63,122 @@ function equivalent, Out1, In2 extends Tuple, Out2>( () => f1(...inputs), () => to.back(f2(...(inputs.map((x, i) => from[i].there(x)) as any as In2))), - (x, y) => assertEqual(x, y, label ?? 'same results'), + (x, y) => assertEqual(x, y, label), label ); }); } -let id = (x: T) => x; +function id(x: T) { + return x; +} -function createEquivalenceTesters( - Field: Provable, - newField: (x: bigint) => Field -) { - function equivalent1( - op1: (x: Field) => Field, - op2: (x: bigint) => bigint, - rng: Random = Random.field +// equivalence in provable code + +type ProvableSpec = Spec & { provable: Provable }; +type MaybeProvableFromSpec = FromSpec & { + provable?: Provable; +}; + +function equivalentProvable< + In extends Tuple>, + Out extends ToSpec +>({ from, to }: { from: In; to: Out }) { + return function run( + f1: (...args: Params1) => Result1, + f2: (...args: Params2) => Result2, + label = 'expect equal results' ) { - test(rng, (x0, assert) => { - let x = newField(x0); + let generators = from.map((spec) => spec.rng); + let assertEqual = to.assertEqual ?? deepEqual; + test(...(generators as any[]), (...args) => { + args.pop(); + let inputs = args as any as Params1; + let inputs2 = inputs.map((x, i) => + from[i].there(x) + ) as any as Params2; + // outside provable code handleErrors( - () => op1(x), - () => op2(x0), - (a, b) => assert(a.toBigInt() === b, 'equal results') + () => f1(...inputs), + () => f2(...inputs2), + (x, y) => assertEqual(x, to.back(y), label), + label ); + // inside provable code Provable.runAndCheck(() => { - x = Provable.witness(Field, () => x); + let inputWitnesses = inputs2.map((x, i) => { + let provable = from[i].provable; + return provable !== undefined + ? Provable.witness(provable, () => x) + : x; + }) as any as Params2; handleErrors( - () => op1(x), - () => op2(x0), - (a, b) => - Provable.asProver(() => assert(a.toBigInt() === b, 'equal results')) + () => f1(...inputs), + () => f2(...inputWitnesses), + (x, y) => Provable.asProver(() => assertEqual(x, to.back(y), label)) ); }); }); + }; +} + +// some useful specs + +let unit: ToSpec = { back: id, assertEqual() {} }; + +let field: ProvableSpec = { + rng: Random.field, + there: (x) => new Field(x), + back: (x) => x.toBigInt(), + provable: Field, +}; + +let fieldBigint: Spec = { + rng: Random.field, + there: id, + back: id, +}; + +// old equivalence testers + +function createEquivalenceTesters() { + function equivalent1( + f1: (x: Field) => Field, + f2: (x: bigint) => bigint, + rng: Random = Random.field + ) { + let field_ = { ...field, rng }; + equivalentProvable({ from: [field_], to: field_ })(f2, f1); } function equivalent2( - op1: (x: Field, y: Field | bigint) => Field, - op2: (x: bigint, y: bigint) => bigint, + f1: (x: Field, y: Field | bigint) => Field, + f2: (x: bigint, y: bigint) => bigint, rng: Random = Random.field ) { - test(rng, rng, (x0, y0, assert) => { - let x = newField(x0); - let y = newField(y0); - // outside provable code - handleErrors( - () => op1(x, y), - () => op2(x0, y0), - (a, b) => assert(a.toBigInt() === b, 'equal results') - ); - handleErrors( - () => op1(x, y0), - () => op2(x0, y0), - (a, b) => assert(a.toBigInt() === b, 'equal results') - ); - // inside provable code - Provable.runAndCheck(() => { - x = Provable.witness(Field, () => x); - y = Provable.witness(Field, () => y); - handleErrors( - () => op1(x, y), - () => op2(x0, y0), - (a, b) => - Provable.asProver(() => assert(a.toBigInt() === b, 'equal results')) - ); - handleErrors( - () => op1(x, y0), - () => op2(x0, y0), - (a, b) => - Provable.asProver(() => assert(a.toBigInt() === b, 'equal results')) - ); - }); - }); + let field_ = { ...field, rng }; + let fieldBigint_ = { ...fieldBigint, rng }; + equivalentProvable({ from: [field_, field_], to: field_ })(f2, f1); + equivalentProvable({ from: [field_, fieldBigint_], to: field_ })(f2, f1); } function equivalentVoid1( - op1: (x: Field) => void, - op2: (x: bigint) => void, + f1: (x: Field) => void, + f2: (x: bigint) => void, rng: Random = Random.field ) { - test(rng, (x0) => { - let x = newField(x0); - // outside provable code - handleErrors( - () => op1(x), - () => op2(x0) - ); - // inside provable code - Provable.runAndCheck(() => { - x = Provable.witness(Field, () => x); - handleErrors( - () => op1(x), - () => op2(x0) - ); - }); - }); + let field_ = { ...field, rng }; + equivalentProvable({ from: [field_], to: unit })(f2, f1); } function equivalentVoid2( - op1: (x: Field, y: Field | bigint) => void, - op2: (x: bigint, y: bigint) => void, + f1: (x: Field, y: Field | bigint) => void, + f2: (x: bigint, y: bigint) => void, rng: Random = Random.field ) { - test(rng, rng, (x0, y0) => { - let x = newField(x0); - let y = newField(y0); - // outside provable code - handleErrors( - () => op1(x, y), - () => op2(x0, y0) - ); - handleErrors( - () => op1(x, y0), - () => op2(x0, y0) - ); - // inside provable code - Provable.runAndCheck(() => { - x = Provable.witness(Field, () => x); - y = Provable.witness(Field, () => y); - handleErrors( - () => op1(x, y), - () => op2(x0, y0) - ); - handleErrors( - () => op1(x, y0), - () => op2(x0, y0) - ); - }); - }); + let field_ = { ...field, rng }; + let fieldBigint_ = { ...fieldBigint, rng }; + equivalentProvable({ from: [field_, field_], to: unit })(f2, f1); + equivalentProvable({ from: [field_, fieldBigint_], to: unit })(f2, f1); } return { equivalent1, equivalent2, equivalentVoid1, equivalentVoid2 }; @@ -234,3 +223,18 @@ function throwError(message?: string): any { type AnyFunction = (...args: any) => any; type Tuple = [] | [T, ...T[]]; + +// infer input types from specs + +type Params1>> = { + [k in keyof Ins]: Ins[k] extends FromSpec ? In : never; +}; +type Params2>> = { + [k in keyof Ins]: Ins[k] extends FromSpec ? In : never; +}; +type Result1> = Out extends ToSpec + ? Out1 + : never; +type Result2> = Out extends ToSpec + ? Out2 + : never; From a2eca3d0ba7fccd1b516b5122cabce9a10bf0351 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 15:16:31 +0200 Subject: [PATCH 03/14] support union types in function parameters nicely --- src/lib/testing/equivalent.ts | 99 ++++++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 24 deletions(-) diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index bf4aa22882..4557cb47c6 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -8,12 +8,15 @@ import { Field } from '../core.js'; export { equivalent, + equivalentProvable, + oneOf, createEquivalenceTesters, throwError, handleErrors, deepEqual as defaultAssertEqual, id, }; +export { field, fieldBigint }; export { Spec, ToSpec, FromSpec, SpecFromFunctions }; // a `Spec` tells us how to compare two functions @@ -24,6 +27,11 @@ type FromSpec = { // `there` converts to inputs to the second function there: (x: In1) => In2; + + // `provable` tells us how to create witnesses, to test provable code + // note: we only allow the second function to be provable; + // the second because it's more natural to have non-provable types as random generator output + provable?: Provable; }; type ToSpec = { @@ -36,6 +44,8 @@ type ToSpec = { type Spec = FromSpec & ToSpec; +type ProvableSpec = Spec & { provable: Provable }; + type FuncSpec, Out1, In2 extends Tuple, Out2> = { from: { [k in keyof In1]: k extends keyof In2 ? FromSpec : never; @@ -73,30 +83,60 @@ function id(x: T) { return x; } -// equivalence in provable code +// unions of specs, to cleanly model functions parameters that are unions of types -type ProvableSpec = Spec & { provable: Provable }; -type MaybeProvableFromSpec = FromSpec & { - provable?: Provable; +type FromSpecUnion = { + _isUnion: true; + specs: Tuple>; + rng: Random<[number, T1]>; }; +type OrUnion = FromSpec | FromSpecUnion; + +type Union = T[keyof T & number]; + +function oneOf>>( + ...specs: In +): FromSpecUnion>, Union>> { + // the randomly generated value from a union keeps track of which spec it came from + let rng = Random.oneOf( + ...specs.map((spec, i) => + Random.map(spec.rng, (x) => [i, x] as [number, any]) + ) + ); + return { _isUnion: true, specs, rng }; +} + +function toUnion(spec: OrUnion): FromSpecUnion { + let specAny = spec as any; + return specAny._isUnion ? specAny : oneOf(specAny); +} + +// equivalence in provable code + function equivalentProvable< - In extends Tuple>, + In extends Tuple>, Out extends ToSpec ->({ from, to }: { from: In; to: Out }) { +>({ from: fromRaw, to }: { from: In; to: Out }) { + let fromUnions = fromRaw.map(toUnion); return function run( f1: (...args: Params1) => Result1, f2: (...args: Params2) => Result2, label = 'expect equal results' ) { - let generators = from.map((spec) => spec.rng); + let generators = fromUnions.map((spec) => spec.rng); let assertEqual = to.assertEqual ?? deepEqual; - test(...(generators as any[]), (...args) => { + test(...generators, (...args) => { args.pop(); - let inputs = args as any as Params1; - let inputs2 = inputs.map((x, i) => - from[i].there(x) - ) as any as Params2; + + // figure out which spec to use for each argument + let from = (args as [number, unknown][]).map( + ([j], i) => fromUnions[i].specs[j] + ); + let inputs = (args as [number, unknown][]).map( + ([, x]) => x + ) as Params1; + let inputs2 = inputs.map((x, i) => from[i].there(x)) as Params2; // outside provable code handleErrors( @@ -113,7 +153,7 @@ function equivalentProvable< return provable !== undefined ? Provable.witness(provable, () => x) : x; - }) as any as Params2; + }) as Params2; handleErrors( () => f1(...inputs), () => f2(...inputWitnesses), @@ -144,13 +184,8 @@ let fieldBigint: Spec = { // old equivalence testers function createEquivalenceTesters() { - function equivalent1( - f1: (x: Field) => Field, - f2: (x: bigint) => bigint, - rng: Random = Random.field - ) { - let field_ = { ...field, rng }; - equivalentProvable({ from: [field_], to: field_ })(f2, f1); + function equivalent1(f1: (x: Field) => Field, f2: (x: bigint) => bigint) { + equivalentProvable({ from: [field], to: field })(f2, f1); } function equivalent2( f1: (x: Field, y: Field | bigint) => Field, @@ -226,12 +261,28 @@ type Tuple = [] | [T, ...T[]]; // infer input types from specs -type Params1>> = { - [k in keyof Ins]: Ins[k] extends FromSpec ? In : never; +type Param1> = In extends { + there: (x: infer In) => any; +} + ? In + : In extends FromSpecUnion + ? T1 + : never; +type Param2> = In extends { + there: (x: any) => infer In; +} + ? In + : In extends FromSpecUnion + ? T2 + : never; + +type Params1>> = { + [k in keyof Ins]: Param1; }; -type Params2>> = { - [k in keyof Ins]: Ins[k] extends FromSpec ? In : never; +type Params2>> = { + [k in keyof Ins]: Param2; }; + type Result1> = Out extends ToSpec ? Out1 : never; From af2444d0f233b7de641fe371d8d5fd58d2adf95b Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 15:44:33 +0200 Subject: [PATCH 04/14] refactor field unit test to new style --- src/lib/field.unit-test.ts | 101 +++++++++++++++++++--------------- src/lib/testing/equivalent.ts | 56 +++++-------------- 2 files changed, 69 insertions(+), 88 deletions(-) diff --git a/src/lib/field.unit-test.ts b/src/lib/field.unit-test.ts index 67f007ab79..8f7d8843f5 100644 --- a/src/lib/field.unit-test.ts +++ b/src/lib/field.unit-test.ts @@ -7,7 +7,16 @@ import { Provable } from './provable.js'; import { Binable } from '../bindings/lib/binable.js'; import { ProvableExtended } from './circuit_value.js'; import { FieldType } from './field.js'; -import { createEquivalenceTesters, throwError } from './testing/equivalent.js'; +import { + equivalentProvable as equivalent, + oneOf, + field, + bigintField, + throwError, + unit, + bool, + Spec, +} from './testing/equivalent.js'; // types Field satisfies Provable; @@ -56,73 +65,75 @@ test(Random.field, Random.int(-5, 5), (x, k) => { deepEqual(Field(x + BigInt(k) * Field.ORDER), Field(x)); }); +// Field | bigint parameter +let fieldOrBigint = oneOf(field, bigintField); + // special generator let SmallField = Random.reject( Random.field, (x) => x.toString(2).length > Fp.sizeInBits - 2 ); - -let { equivalent1, equivalent2, equivalentVoid1, equivalentVoid2 } = - createEquivalenceTesters(); +let smallField: Spec = { ...field, rng: SmallField }; +let smallBigint: Spec = { ...bigintField, rng: SmallField }; +let smallFieldOrBigint = oneOf(smallField, smallBigint); // arithmetic, both in- and outside provable code -equivalent2((x, y) => x.add(y), Fp.add); -equivalent1((x) => x.neg(), Fp.negate); -equivalent2((x, y) => x.sub(y), Fp.sub); -equivalent2((x, y) => x.mul(y), Fp.mul); +let equivalent1 = equivalent({ from: [field], to: field }); +let equivalent2 = equivalent({ from: [field, fieldOrBigint], to: field }); + +equivalent2(Fp.add, (x, y) => x.add(y)); +equivalent1(Fp.negate, (x) => x.neg()); +equivalent2(Fp.sub, (x, y) => x.sub(y)); +equivalent2(Fp.mul, (x, y) => x.mul(y)); equivalent1( - (x) => x.inv(), - (x) => Fp.inverse(x) ?? throwError('division by 0') + (x) => Fp.inverse(x) ?? throwError('division by 0'), + (x) => x.inv() ); equivalent2( - (x, y) => x.div(y), - (x, y) => Fp.div(x, y) ?? throwError('division by 0') + (x, y) => Fp.div(x, y) ?? throwError('division by 0'), + (x, y) => x.div(y) ); -equivalent1((x) => x.square(), Fp.square); +equivalent1(Fp.square, (x) => x.square()); equivalent1( - (x) => x.sqrt(), - (x) => Fp.sqrt(x) ?? throwError('no sqrt') + (x) => Fp.sqrt(x) ?? throwError('no sqrt'), + (x) => x.sqrt() ); -equivalent2( - (x, y) => x.equals(y).toField(), - (x, y) => BigInt(x === y) +equivalent({ from: [field, fieldOrBigint], to: bool })( + (x, y) => x === y, + (x, y) => x.equals(y) ); -equivalent2( - (x, y) => x.lessThan(y).toField(), - (x, y) => BigInt(x < y), - SmallField + +equivalent({ from: [smallField, smallFieldOrBigint], to: bool })( + (x, y) => x < y, + (x, y) => x.lessThan(y) ); -equivalent2( - (x, y) => x.lessThanOrEqual(y).toField(), - (x, y) => BigInt(x <= y), - SmallField +equivalent({ from: [smallField, smallFieldOrBigint], to: bool })( + (x, y) => x <= y, + (x, y) => x.lessThanOrEqual(y) ); -equivalentVoid2( - (x, y) => x.assertEquals(y), - (x, y) => x === y || throwError('not equal') +equivalent({ from: [field, fieldOrBigint], to: unit })( + (x, y) => x === y || throwError('not equal'), + (x, y) => x.assertEquals(y) ); -equivalentVoid2( - (x, y) => x.assertNotEquals(y), - (x, y) => x !== y || throwError('equal') +equivalent({ from: [field, fieldOrBigint], to: unit })( + (x, y) => x !== y || throwError('equal'), + (x, y) => x.assertNotEquals(y) ); -equivalentVoid2( - (x, y) => x.assertLessThan(y), +equivalent({ from: [smallField, smallFieldOrBigint], to: unit })( (x, y) => x < y || throwError('not less than'), - SmallField + (x, y) => x.assertLessThan(y) ); -equivalentVoid2( - (x, y) => x.assertLessThanOrEqual(y), +equivalent({ from: [smallField, smallFieldOrBigint], to: unit })( (x, y) => x <= y || throwError('not less than or equal'), - SmallField + (x, y) => x.assertLessThanOrEqual(y) ); -equivalentVoid1( - (x) => x.assertBool(), - (x) => x === 0n || x === 1n || throwError('not boolean') +equivalent({ from: [field], to: unit })( + (x) => x === 0n || x === 1n || throwError('not boolean'), + (x) => x.assertBool() ); -equivalent1( - (x) => x.isEven().toField(), - (x) => BigInt((x & 1n) === 0n), - SmallField +equivalent({ from: [smallField], to: bool })( + (x) => (x & 1n) === 0n, + (x) => x.isEven() ); // non-constant field vars diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index 4557cb47c6..014437aa9a 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -4,20 +4,19 @@ import { test, Random } from '../testing/property.js'; import { Provable } from '../provable.js'; import { deepEqual } from 'node:assert/strict'; -import { Field } from '../core.js'; +import { Bool, Field } from '../core.js'; export { equivalent, equivalentProvable, oneOf, - createEquivalenceTesters, throwError, handleErrors, deepEqual as defaultAssertEqual, id, }; -export { field, fieldBigint }; -export { Spec, ToSpec, FromSpec, SpecFromFunctions }; +export { field, bigintField, bool, unit }; +export { Spec, ToSpec, FromSpec, SpecFromFunctions, ProvableSpec }; // a `Spec` tells us how to compare two functions @@ -83,7 +82,7 @@ function id(x: T) { return x; } -// unions of specs, to cleanly model functions parameters that are unions of types +// unions of specs, to cleanly model function parameters that are unions of types type FromSpecUnion = { _isUnion: true; @@ -170,54 +169,25 @@ let unit: ToSpec = { back: id, assertEqual() {} }; let field: ProvableSpec = { rng: Random.field, - there: (x) => new Field(x), + there: Field, back: (x) => x.toBigInt(), provable: Field, }; -let fieldBigint: Spec = { +let bigintField: Spec = { rng: Random.field, there: id, back: id, }; -// old equivalence testers - -function createEquivalenceTesters() { - function equivalent1(f1: (x: Field) => Field, f2: (x: bigint) => bigint) { - equivalentProvable({ from: [field], to: field })(f2, f1); - } - function equivalent2( - f1: (x: Field, y: Field | bigint) => Field, - f2: (x: bigint, y: bigint) => bigint, - rng: Random = Random.field - ) { - let field_ = { ...field, rng }; - let fieldBigint_ = { ...fieldBigint, rng }; - equivalentProvable({ from: [field_, field_], to: field_ })(f2, f1); - equivalentProvable({ from: [field_, fieldBigint_], to: field_ })(f2, f1); - } - function equivalentVoid1( - f1: (x: Field) => void, - f2: (x: bigint) => void, - rng: Random = Random.field - ) { - let field_ = { ...field, rng }; - equivalentProvable({ from: [field_], to: unit })(f2, f1); - } - function equivalentVoid2( - f1: (x: Field, y: Field | bigint) => void, - f2: (x: bigint, y: bigint) => void, - rng: Random = Random.field - ) { - let field_ = { ...field, rng }; - let fieldBigint_ = { ...fieldBigint, rng }; - equivalentProvable({ from: [field_, field_], to: unit })(f2, f1); - equivalentProvable({ from: [field_, fieldBigint_], to: unit })(f2, f1); - } +let bool: Spec = { + rng: Random.boolean, + there: Bool, + back: (x) => x.toBoolean(), + provable: Bool, +}; - return { equivalent1, equivalent2, equivalentVoid1, equivalentVoid2 }; -} +// helper to ensure two functions throw equivalent errors function handleErrors( op1: () => T, From 3bf09f966f6cb2e6310d5f8d86584256d5c730df Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 15:57:20 +0200 Subject: [PATCH 05/14] make non provable equivalence tester more similar --- src/lib/testing/equivalent.ts | 54 ++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index 014437aa9a..77cd08634f 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -57,27 +57,6 @@ type SpecFromFunctions< F2 extends AnyFunction > = FuncSpec, ReturnType, Parameters, ReturnType>; -function equivalent, Out1, In2 extends Tuple, Out2>( - { from, to }: FuncSpec, - f1: (...args: In1) => Out1, - f2: (...args: In2) => Out2, - label = 'expect equal results' -) { - let generators = from.map((spec) => spec.rng); - let assertEqual = to.assertEqual ?? deepEqual; - test(...(generators as any[]), (...args) => { - args.pop(); - let inputs = args as any as In1; - handleErrors( - () => f1(...inputs), - () => - to.back(f2(...(inputs.map((x, i) => from[i].there(x)) as any as In2))), - (x, y) => assertEqual(x, y, label), - label - ); - }); -} - function id(x: T) { return x; } @@ -111,7 +90,36 @@ function toUnion(spec: OrUnion): FromSpecUnion { return specAny._isUnion ? specAny : oneOf(specAny); } -// equivalence in provable code +// equivalence tester + +function equivalent< + In extends Tuple>, + Out extends ToSpec +>({ from, to }: { from: In; to: Out }) { + return function run( + f1: (...args: Params1) => Result1, + f2: (...args: Params2) => Result2, + label = 'expect equal results' + ) { + let generators = from.map((spec) => spec.rng); + let assertEqual = to.assertEqual ?? deepEqual; + test(...(generators as any[]), (...args) => { + args.pop(); + let inputs = args as Params1; + handleErrors( + () => f1(...inputs), + () => + to.back( + f2(...(inputs.map((x, i) => from[i].there(x)) as Params2)) + ), + (x, y) => assertEqual(x, y, label), + label + ); + }); + }; +} + +// equivalence tester for provable code function equivalentProvable< In extends Tuple>, @@ -180,7 +188,7 @@ let bigintField: Spec = { back: id, }; -let bool: Spec = { +let bool: ProvableSpec = { rng: Random.boolean, there: Bool, back: (x) => x.toBoolean(), From f0103e6dfacefa3c6ec02f28def13a14b2f2a901 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 16:40:07 +0200 Subject: [PATCH 06/14] equivalence testing for async functions --- src/lib/testing/equivalent.ts | 70 ++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index 77cd08634f..18a29f4a26 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -9,13 +9,14 @@ import { Bool, Field } from '../core.js'; export { equivalent, equivalentProvable, + equivalentAsync, oneOf, throwError, handleErrors, deepEqual as defaultAssertEqual, id, }; -export { field, bigintField, bool, unit }; +export { field, bigintField, bool, boolean, unit }; export { Spec, ToSpec, FromSpec, SpecFromFunctions, ProvableSpec }; // a `Spec` tells us how to compare two functions @@ -119,6 +120,38 @@ function equivalent< }; } +// async equivalence + +function equivalentAsync< + In extends Tuple>, + Out extends ToSpec +>({ from, to }: { from: In; to: Out }, { runs = 1 } = {}) { + return async function run( + f1: (...args: Params1) => Promise> | Result1, + f2: (...args: Params2) => Promise> | Result2, + label = 'expect equal results' + ) { + let generators = from.map((spec) => spec.rng); + let assertEqual = to.assertEqual ?? deepEqual; + + let nexts = generators.map((g) => g.create()); + + for (let i = 0; i < runs; i++) { + let args = nexts.map((next) => next()); + let inputs = args as Params1; + await handleErrorsAsync( + () => f1(...inputs), + async () => + to.back( + await f2(...(inputs.map((x, i) => from[i].there(x)) as Params2)) + ), + (x, y) => assertEqual(x, y, label), + label + ); + } + }; +} + // equivalence tester for provable code function equivalentProvable< @@ -194,6 +227,11 @@ let bool: ProvableSpec = { back: (x) => x.toBoolean(), provable: Bool, }; +let boolean: Spec = { + rng: Random.boolean, + there: id, + back: id, +}; // helper to ensure two functions throw equivalent errors @@ -227,6 +265,36 @@ function handleErrors( } } +async function handleErrorsAsync( + op1: () => T, + op2: () => S, + useResults?: (a: Awaited, b: Awaited) => R, + label?: string +): Promise { + let result1: Awaited, result2: Awaited; + let error1: Error | undefined; + let error2: Error | undefined; + try { + result1 = await op1(); + } catch (err) { + error1 = err as Error; + } + try { + result2 = await op2(); + } catch (err) { + error2 = err as Error; + } + if (!!error1 !== !!error2) { + error1 && console.log(error1); + error2 && console.log(error2); + } + let message = `${(label && `${label}: `) || ''}equivalent errors`; + deepEqual(!!error1, !!error2, message); + if (!(error1 || error2) && useResults !== undefined) { + return useResults(result1!, result2!); + } +} + function throwError(message?: string): any { throw Error(message); } From 7a3cdb21f556c8bfe33817e9faccd8770dfd5bd5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 16:40:43 +0200 Subject: [PATCH 07/14] add test for 64-bit range check gate --- src/lib/gadgets/gadgets.unit-test.ts | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/lib/gadgets/gadgets.unit-test.ts diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts new file mode 100644 index 0000000000..9963d53424 --- /dev/null +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -0,0 +1,42 @@ +import { Field } from '../field.js'; +import { ZkProgram } from '../proof_system.js'; +import { + Spec, + boolean, + equivalentAsync, + field, +} from '../testing/equivalent.js'; +import { Random } from '../testing/random.js'; +import { rangeCheck64 } from './range-check.js'; + +let RangeCheck64 = ZkProgram({ + methods: { + run: { + privateInputs: [Field], + method(x) { + rangeCheck64(x); + }, + }, + }, +}); + +await RangeCheck64.compile(); + +let maybeUint64: Spec = { + ...field, + rng: Random.oneOf(Random.uint64, Random.uint64.invalid), +}; + +// 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 })( + (x) => { + if (x >= 1n << 64n) throw Error('expected 64 bits'); + return true; + }, + async (x) => { + let proof = await RangeCheck64.run(x); + return await RangeCheck64.verify(proof); + } +); From 40ca430f62a425da6b43f94a732c862c532ce27f Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 16:46:24 +0200 Subject: [PATCH 08/14] export range check 64 gate under Gadgets namespace --- src/index.ts | 1 + src/lib/gadgets/gadgets.ts | 7 +++++++ src/lib/gadgets/gadgets.unit-test.ts | 6 ++++-- src/lib/gadgets/range-check.ts | 4 ++++ 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/lib/gadgets/gadgets.ts diff --git a/src/index.ts b/src/index.ts index 0813d3da4d..8a2f6ab2a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ export { export { Provable } from './lib/provable.js'; export { Circuit, Keypair, public_, circuitMain } from './lib/circuit.js'; export { UInt32, UInt64, Int64, Sign } from './lib/int.js'; +export { Gadgets } from './lib/gadgets/gadgets.js'; export { Types } from './bindings/mina-transaction/types.js'; export * as Mina from './lib/mina.js'; diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts new file mode 100644 index 0000000000..107ce568bf --- /dev/null +++ b/src/lib/gadgets/gadgets.ts @@ -0,0 +1,7 @@ +import { rangeCheck64 } from './range-check.js'; + +export { Gadgets }; + +const Gadgets = { + rangeCheck64, +}; diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts index 9963d53424..d34ed8231d 100644 --- a/src/lib/gadgets/gadgets.unit-test.ts +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -7,14 +7,16 @@ import { field, } from '../testing/equivalent.js'; import { Random } from '../testing/random.js'; -import { rangeCheck64 } from './range-check.js'; +import { Gadgets } from './gadgets.js'; + +// TODO: make a ZkFunction or something that doesn't go through Pickles let RangeCheck64 = ZkProgram({ methods: { run: { privateInputs: [Field], method(x) { - rangeCheck64(x); + Gadgets.rangeCheck64(x); }, }, }, diff --git a/src/lib/gadgets/range-check.ts b/src/lib/gadgets/range-check.ts index 12a914ccd8..d244b83ec4 100644 --- a/src/lib/gadgets/range-check.ts +++ b/src/lib/gadgets/range-check.ts @@ -3,6 +3,10 @@ import * as Gates from '../gates.js'; export { rangeCheck64 }; +/** + * Asserts that x is in the range [0, 2^64) + * @param x field element + */ function rangeCheck64(x: Field) { if (x.isConstant()) { if (x.toBigInt() >= 1n << 64n) { From 1951f571d4eab828111aab7193850c9e8e56c20e Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 16:46:29 +0200 Subject: [PATCH 09/14] bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 91e701cd02..fc7f6a97fd 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 91e701cd027d5a1213e750e5e9b80da80c9d211e +Subproject commit fc7f6a97fd5d0dd7941040f996441fe48a3b6c83 From 5e7467fe020b3ee73ffffe3c2ab5fd3f575c586d Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 16:51:35 +0200 Subject: [PATCH 10/14] changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b55c81885b..069787f8d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,10 +27,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - To recover existing verification keys and behavior, change the order of properties in your Struct definitions to be alphabetical - The `customObjectKeys` option is removed from `Struct` +### Added + +- `Gadgets.rangeCheck64()`, new provable method to do efficient 64-bit range checks using lookup tables https://github.com/o1-labs/o1js/pull/1181 + ### Changed - Improve prover performance by ~25% https://github.com/o1-labs/o1js/pull/1092 - Change internal representation of field elements to be JS bigint instead of Uint8Array +- Consolidate internal framework for testing equivalence of two implementations ## [0.13.0](https://github.com/o1-labs/o1js/compare/fbd4b2717...c2f392fe5) From a16c7e4265f0ea66a288e91e691c5d82e5a37d1a Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 18:11:01 +0200 Subject: [PATCH 11/14] fixup unit test --- src/examples/ex02_root.ts | 3 ++- src/examples/ex02_root_program.ts | 10 ++-------- src/lib/gadgets/gadgets.unit-test.ts | 7 +++++-- src/lib/testing/equivalent.ts | 25 ++++++++++++++++--------- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/examples/ex02_root.ts b/src/examples/ex02_root.ts index 31dd48e818..54e5f0841b 100644 --- a/src/examples/ex02_root.ts +++ b/src/examples/ex02_root.ts @@ -1,4 +1,4 @@ -import { Field, Circuit, circuitMain, public_, UInt64 } from 'o1js'; +import { Field, Circuit, circuitMain, public_, UInt64, Gadgets } from 'o1js'; /* Exercise 2: @@ -10,6 +10,7 @@ Prove: class Main extends Circuit { @circuitMain static main(@public_ y: Field, x: UInt64) { + Gadgets.rangeCheck64(x.value); let y3 = y.square().mul(y); y3.assertEquals(x.value); } diff --git a/src/examples/ex02_root_program.ts b/src/examples/ex02_root_program.ts index c4115dbd8d..b301a9f72f 100644 --- a/src/examples/ex02_root_program.ts +++ b/src/examples/ex02_root_program.ts @@ -1,11 +1,4 @@ -import { - Field, - Circuit, - circuitMain, - public_, - UInt64, - Experimental, -} from 'o1js'; +import { Field, UInt64, Experimental, Gadgets } from 'o1js'; let { ZkProgram } = Experimental; @@ -15,6 +8,7 @@ const Main = ZkProgram({ main: { privateInputs: [UInt64], method(y: Field, x: UInt64) { + Gadgets.rangeCheck64(x.value); let y3 = y.square().mul(y); y3.assertEquals(x.value); }, diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts index d34ed8231d..6d65c57d50 100644 --- a/src/lib/gadgets/gadgets.unit-test.ts +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -1,3 +1,4 @@ +import { mod } from '../../bindings/crypto/finite_field.js'; import { Field } from '../field.js'; import { ZkProgram } from '../proof_system.js'; import { @@ -26,13 +27,15 @@ await RangeCheck64.compile(); let maybeUint64: Spec = { ...field, - rng: Random.oneOf(Random.uint64, Random.uint64.invalid), + 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 :( -equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( +equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 20 })( (x) => { if (x >= 1n << 64n) throw Error('expected 64 bits'); return true; diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index 18a29f4a26..c19748624e 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -139,15 +139,22 @@ function equivalentAsync< for (let i = 0; i < runs; i++) { let args = nexts.map((next) => next()); let inputs = args as Params1; - await handleErrorsAsync( - () => f1(...inputs), - async () => - to.back( - await f2(...(inputs.map((x, i) => from[i].there(x)) as Params2)) - ), - (x, y) => assertEqual(x, y, label), - label - ); + try { + await handleErrorsAsync( + () => f1(...inputs), + async () => + to.back( + await f2( + ...(inputs.map((x, i) => from[i].there(x)) as Params2) + ) + ), + (x, y) => assertEqual(x, y, label), + label + ); + } catch (err) { + console.log(...inputs); + throw err; + } } }; } From ce21d6b9ba2f0e3bccd146b16c75e49a8838ecc5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 12 Oct 2023 18:16:52 +0200 Subject: [PATCH 12/14] fixup changelog --- CHANGELOG.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b6a31c849..1e64b6d357 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - 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 + ## [0.13.1](https://github.com/o1-labs/o1js/compare/c2f392fe5...045faa7) ### Breaking changes @@ -37,10 +39,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - To recover existing verification keys and behavior, change the order of properties in your Struct definitions to be alphabetical - The `customObjectKeys` option is removed from `Struct` -### Added - -- `Gadgets.rangeCheck64()`, new provable method to do efficient 64-bit range checks using lookup tables https://github.com/o1-labs/o1js/pull/1181 - ### Changed - Improve prover performance by ~25% https://github.com/o1-labs/o1js/pull/1092 From 6b776e2161dd88162304ff81d25624e86d21efb7 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 13 Oct 2023 10:05:47 +0200 Subject: [PATCH 13/14] address feedback: add doccomment --- src/lib/gadgets/gadgets.ts | 31 ++++++++++++++++++++++++++++++- src/lib/gadgets/range-check.ts | 3 +-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 107ce568bf..b4c799b2cb 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -1,7 +1,36 @@ +/** + * Wrapper file for various gadgets, with a namespace and doccomments. + */ import { rangeCheck64 } from './range-check.js'; +import { Field } from '../core.js'; export { Gadgets }; const Gadgets = { - rangeCheck64, + /** + * Asserts that the input value is in the range [0, 2^64). + * + * This function proves that the provided field element can be represented with 64 bits. + * If the field element exceeds 64 bits, an error is thrown. + * + * @param x - The value to be range-checked. + * + * @throws Throws an error if the input value exceeds 64 bits. + * + * @example + * ```ts + * const x = Provable.witness(Field, () => Field(12345678n)); + * rangeCheck64(x); // successfully proves 64-bit range + * + * const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n)); + * rangeCheck64(xLarge); // throws an error since input exceeds 64 bits + * ``` + * + * **Note**: Small "negative" field element inputs are interpreted as large integers close to the field size, + * and don't pass the 64-bit check. If you want to prove that a value lies in the int64 range [-2^63, 2^63), + * you could use `rangeCheck64(x.add(1n << 63n))`. + */ + rangeCheck64(x: Field) { + return rangeCheck64(x); + }, }; diff --git a/src/lib/gadgets/range-check.ts b/src/lib/gadgets/range-check.ts index d244b83ec4..d27d4807a4 100644 --- a/src/lib/gadgets/range-check.ts +++ b/src/lib/gadgets/range-check.ts @@ -4,8 +4,7 @@ import * as Gates from '../gates.js'; export { rangeCheck64 }; /** - * Asserts that x is in the range [0, 2^64) - * @param x field element + * Asserts that x is in the range [0, 2^64), handles constant case */ function rangeCheck64(x: Field) { if (x.isConstant()) { From d92b6fa610e257f8d0d40d4732bd45362d8ba32e Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 16 Oct 2023 21:00:20 +0200 Subject: [PATCH 14/14] revert excessive testing cost --- src/lib/gadgets/gadgets.unit-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/gadgets/gadgets.unit-test.ts b/src/lib/gadgets/gadgets.unit-test.ts index 6d65c57d50..13a44a059d 100644 --- a/src/lib/gadgets/gadgets.unit-test.ts +++ b/src/lib/gadgets/gadgets.unit-test.ts @@ -35,7 +35,7 @@ let maybeUint64: Spec = { // 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: 20 })( +equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( (x) => { if (x >= 1n << 64n) throw Error('expected 64 bits'); return true;