diff --git a/src/bindings b/src/bindings index c9ca9e5579..5f1cd56ff4 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit c9ca9e557919f5f6a3eaaee6035611ba0c3227bc +Subproject commit 5f1cd56ff49108d7c224abfd5a7cdb0de82be569 diff --git a/src/examples/vk_regression.ts b/src/examples/vk_regression.ts index 6f0d2ddf3b..6dbcd31373 100644 --- a/src/examples/vk_regression.ts +++ b/src/examples/vk_regression.ts @@ -5,6 +5,9 @@ import { HelloWorld } from './zkapps/hello_world/hello_world.js'; import { TokenContract, createDex } from './zkapps/dex/dex.js'; import { GroupCS } from './primitive_constraint_system.js'; +// toggle this for quick iteration when debugging vk regressions +const skipVerificationKeys = false; + // usage ./run ./src/examples/vk_regression.ts --bundle --dump ./src/examples/regression_test.json let dump = process.argv[4] === '--dump'; let jsonPath = process.argv[dump ? 5 : 4]; @@ -132,7 +135,10 @@ async function dumpVk(contracts: typeof ConstraintSystems) { for await (const c of contracts) { let data = c.analyzeMethods(); let digest = c.digest(); - let { verificationKey } = await c.compile(); + let verificationKey: + | { data: string; hash: { toString(): string } } + | undefined; + if (!skipVerificationKeys) ({ verificationKey } = await c.compile()); newEntries[c.name] = { digest, methods: Object.fromEntries( @@ -142,8 +148,8 @@ async function dumpVk(contracts: typeof ConstraintSystems) { ]) ), verificationKey: { - data: verificationKey.data, - hash: verificationKey.hash.toString(), + data: verificationKey?.data ?? '', + hash: verificationKey?.hash.toString() ?? '0', }, }; } diff --git a/src/lib/bool.ts b/src/lib/bool.ts new file mode 100644 index 0000000000..fddc2ba0dd --- /dev/null +++ b/src/lib/bool.ts @@ -0,0 +1,389 @@ +import { Snarky, SnarkyBool } from '../snarky.js'; +import { Field, FieldConst, FieldType, FieldVar } from './field.js'; +import { Bool as B } from '../provable/field-bigint.js'; +import { defineBinable } from '../bindings/lib/binable.js'; +import { NonNegativeInteger } from 'src/bindings/crypto/non-negative.js'; + +export { BoolVar, Bool, isBool }; + +// same representation, but use a different name to communicate intent / constraints +type BoolVar = FieldVar; + +type ConstantBoolVar = [FieldType.Constant, FieldConst]; +type ConstantBool = Bool & { value: ConstantBoolVar }; + +const SnarkyBoolConstructor = SnarkyBool(true).constructor; + +/** + * A boolean value. You can use it like this: + * + * ``` + * const x = new Bool(true); + * ``` + * + * You can also combine multiple booleans via [[`not`]], [[`and`]], [[`or`]]. + * + * Use [[assertEquals]] to enforce the value of a Bool. + */ +class Bool { + value: BoolVar; + + constructor(x: boolean | Bool | BoolVar) { + if (Bool.#isBool(x)) { + this.value = x.value; + return; + } + if (Array.isArray(x)) { + this.value = x; + return; + } + this.value = FieldVar.constant(B(x)); + } + + isConstant(): this is { value: ConstantBoolVar } { + return this.value[0] === FieldType.Constant; + } + + /** + * Converts a {@link Bool} to a {@link Field}. `false` becomes 0 and `true` becomes 1. + */ + toField(): Field { + return Bool.toField(this); + } + + /** + * @returns a new {@link Bool} that is the negation of this {@link Bool}. + */ + not(): Bool { + if (this.isConstant()) { + return new Bool(!this.toBoolean()); + } + return new Bool(Snarky.bool.not(this.value)); + } + + /** + * @param y A {@link Bool} to AND with this {@link Bool}. + * @returns a new {@link Bool} that is set to true only if + * this {@link Bool} and `y` are also true. + */ + and(y: Bool | boolean): Bool { + if (this.isConstant() && isConstant(y)) { + return new Bool(this.toBoolean() && toBoolean(y)); + } + return new Bool(Snarky.bool.and(this.value, Bool.#toVar(y))); + } + + /** + * @param y a {@link Bool} to OR with this {@link Bool}. + * @returns a new {@link Bool} that is set to true if either + * this {@link Bool} or `y` is true. + */ + or(y: Bool | boolean): Bool { + if (this.isConstant() && isConstant(y)) { + return new Bool(this.toBoolean() || toBoolean(y)); + } + return new Bool(Snarky.bool.or(this.value, Bool.#toVar(y))); + } + + /** + * Proves that this {@link Bool} is equal to `y`. + * @param y a {@link Bool}. + */ + assertEquals(y: Bool | boolean, message?: string): void { + try { + if (this.isConstant() && isConstant(y)) { + if (this.toBoolean() !== toBoolean(y)) { + throw Error(`Bool.assertEquals(): ${this} != ${y}`); + } + return; + } + Snarky.bool.assertEqual(this.value, Bool.#toVar(y)); + } catch (err) { + throw withMessage(err, message); + } + } + + /** + * Proves that this {@link Bool} is `true`. + */ + assertTrue(message?: string): void { + try { + if (this.isConstant() && !this.toBoolean()) { + throw Error(`Bool.assertTrue(): ${this} != ${true}`); + } + this.assertEquals(true); + } catch (err) { + throw withMessage(err, message); + } + } + + /** + * Proves that this {@link Bool} is `false`. + */ + assertFalse(message?: string): void { + try { + if (this.isConstant() && this.toBoolean()) { + throw Error(`Bool.assertFalse(): ${this} != ${false}`); + } + this.assertEquals(false); + } catch (err) { + throw withMessage(err, message); + } + } + + /** + * Returns true if this {@link Bool} is equal to `y`. + * @param y a {@link Bool}. + */ + equals(y: Bool | boolean): Bool { + if (this.isConstant() && isConstant(y)) { + return new Bool(this.toBoolean() === toBoolean(y)); + } + return new Bool(Snarky.bool.equals(this.value, Bool.#toVar(y))); + } + + /** + * Returns the size of this type. + */ + sizeInFields(): number { + return 1; + } + + /** + * Serializes this {@link Bool} into {@link Field} elements. + */ + toFields(): Field[] { + return Bool.toFields(this); + } + + /** + * Serialize the {@link Bool} to a string, e.g. for printing. + * This operation does _not_ affect the circuit and can't be used to prove anything about the string representation of the Field. + */ + toString(): string { + return this.toBoolean().toString(); + } + + /** + * Serialize the {@link Bool} to a JSON string. + * This operation does _not_ affect the circuit and can't be used to prove anything about the string representation of the Field. + */ + toJSON(): boolean { + return this.toBoolean(); + } + + /** + * This converts the {@link Bool} to a javascript [[boolean]]. + * This can only be called on non-witness values. + */ + toBoolean(): boolean { + let value: FieldConst; + if (this.isConstant()) { + value = this.value[1]; + } else { + value = Snarky.field.readVar(this.value); + } + return FieldConst.equal(value, FieldConst[1]); + } + + static toField(x: Bool | boolean): Field { + return new Field(Bool.#toVar(x)); + } + + /** + * Boolean negation. + */ + static not(x: Bool | boolean): Bool { + if (Bool.#isBool(x)) { + return x.not(); + } + return new Bool(!x); + } + + /** + * Boolean AND operation. + */ + static and(x: Bool | boolean, y: Bool | boolean): Bool { + if (Bool.#isBool(x)) { + return x.and(y); + } + return new Bool(x).and(y); + } + + /** + * Boolean OR operation. + */ + static or(x: Bool | boolean, y: Bool | boolean): Bool { + if (Bool.#isBool(x)) { + return x.or(y); + } + return new Bool(x).or(y); + } + + /** + * Asserts if both {@link Bool} are equal. + */ + static assertEqual(x: Bool, y: Bool | boolean): void { + if (Bool.#isBool(x)) { + x.assertEquals(y); + return; + } + new Bool(x).assertEquals(y); + } + + /** + * Checks two {@link Bool} for equality. + */ + static equal(x: Bool | boolean, y: Bool | boolean): Bool { + if (Bool.#isBool(x)) { + return x.equals(y); + } + return new Bool(x).equals(y); + } + + /** + * Static method to serialize a {@link Bool} into an array of {@link Field} elements. + */ + static toFields(x: Bool): Field[] { + return [Bool.toField(x)]; + } + + /** + * Static method to serialize a {@link Bool} into its auxiliary data. + */ + static toAuxiliary(_?: Bool): [] { + return []; + } + + /** + * Creates a data structure from an array of serialized {@link Field} elements. + */ + static fromFields(fields: Field[]): Bool { + if (fields.length !== 1) { + throw Error(`Bool.fromFields(): expected 1 field, got ${fields.length}`); + } + return new Bool(fields[0].value); + } + + /** + * Serialize a {@link Bool} to a JSON string. + * This operation does _not_ affect the circuit and can't be used to prove anything about the string representation of the Field. + */ + static toJSON(x: Bool): boolean { + return x.toBoolean(); + } + + /** + * Deserialize a JSON structure into a {@link Bool}. + * This operation does _not_ affect the circuit and can't be used to prove anything about the string representation of the Field. + */ + static fromJSON(b: boolean): Bool { + return new Bool(b); + } + + /** + * Returns the size of this type. + */ + static sizeInFields() { + return 1; + } + + static toInput(x: Bool): { packed: [Field, number][] } { + return { packed: [[x.toField(), 1] as [Field, number]] }; + } + + static toBytes(b: Bool): number[] { + return BoolBinable.toBytes(b); + } + + static fromBytes(bytes: number[]): Bool { + return BoolBinable.fromBytes(bytes); + } + + static readBytes( + bytes: number[], + offset: NonNegativeInteger + ): [value: Bool, offset: number] { + return BoolBinable.readBytes(bytes, offset); + } + + static sizeInBytes() { + return 1; + } + + // TODO + static count(x: Bool | boolean[]): Field { + return new Field(0); + } + + static check(x: Bool): void { + Snarky.field.assertBoolean(x.value); + } + + static Unsafe = { + /** + * Converts a {@link Field} into a {@link Bool}. This is a **dangerous** operation + * as it assumes that the field element is either 1 or 0 + * (which might not be true). + * @param x a {@link Field} + */ + ofField(x: Field | number | string | boolean): Bool { + if (typeof x === 'number') { + return new Bool(x === 1); + } else if (typeof x === 'string') { + return new Bool(x === '1'); + } else if (typeof x === 'boolean') { + return new Bool(x); + } else { + return new Bool(x.value); + } + }, + }; + + static #isBool(x: boolean | Bool | BoolVar): x is Bool { + return x instanceof Bool || (x as any) instanceof SnarkyBoolConstructor; + } + + static #toVar(x: boolean | Bool): BoolVar { + if (Bool.#isBool(x)) return x.value; + return FieldVar.constant(B(x)); + } +} + +const BoolBinable = defineBinable({ + toBytes(b: Bool) { + return [Number(b.toBoolean())]; + }, + readBytes(bytes, offset) { + return [new Bool(!!bytes[offset]), offset + 1]; + }, +}); + +function isConstant(x: boolean | Bool): x is boolean | ConstantBool { + if (typeof x === 'boolean') { + return true; + } + // TODO: remove when we get rid of old Bool + if (x instanceof SnarkyBoolConstructor) { + return x.toField().isConstant(); + } + return x.isConstant(); +} + +function isBool(x: unknown) { + return x instanceof Bool || (x as any) instanceof SnarkyBoolConstructor; +} + +function toBoolean(x: boolean | Bool): boolean { + if (typeof x === 'boolean') { + return x; + } + return (x as Bool).toBoolean(); +} + +// TODO: This is duplicated +function withMessage(error: unknown, message?: string) { + if (message === undefined || !(error instanceof Error)) return error; + error.message = `${message}\n${error.message}`; + return error; +} diff --git a/src/lib/core.ts b/src/lib/core.ts index 9efbb8f21f..b321deba39 100644 --- a/src/lib/core.ts +++ b/src/lib/core.ts @@ -1,7 +1,7 @@ import { defineBinable } from '../bindings/lib/binable.js'; import { sizeInBits } from '../provable/field-bigint.js'; -import { Bool } from '../snarky.js'; import { Field as InternalField } from './field.js'; +import { Bool as InternalBool } from './bool.js'; import { Group as InternalGroup } from './group.js'; import { Scalar } from './scalar.js'; @@ -43,6 +43,20 @@ export { Field, Bool, Scalar, Group }; const Field = toFunctionConstructor(InternalField); type Field = InternalField; +/** + * A boolean value. You can use it like this: + * + * ``` + * const x = new Bool(true); + * ``` + * + * You can also combine multiple booleans via [[`not`]], [[`and`]], [[`or`]]. + * + * Use [[assertEquals]] to enforce the value of a Bool. + */ +const Bool = toFunctionConstructor(InternalBool); +type Bool = InternalBool; + /** * An element of a Group. */ @@ -63,25 +77,3 @@ type InferArgs = T extends new (...args: infer Args) => any ? Args : never; type InferReturn = T extends new (...args: any) => infer Return ? Return : never; - -// patching ocaml classes - -Bool.toAuxiliary = () => []; - -Bool.toInput = function (x) { - return { packed: [[x.toField(), 1] as [Field, number]] }; -}; - -// binable -const BoolBinable = defineBinable({ - toBytes(b: Bool) { - return [Number(b.toBoolean())]; - }, - readBytes(bytes, offset) { - return [Bool(!!bytes[offset]), offset + 1]; - }, -}); -Bool.toBytes = BoolBinable.toBytes; -Bool.fromBytes = BoolBinable.fromBytes; -Bool.readBytes = BoolBinable.readBytes; -Bool.sizeInBytes = () => 1; diff --git a/src/lib/field.ts b/src/lib/field.ts index 8706e4c5cf..b14e7a431b 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -1,10 +1,9 @@ import { Snarky, SnarkyField, Provable } from '../snarky.js'; import { Field as Fp } from '../provable/field-bigint.js'; -import { Bool } from '../snarky.js'; import { defineBinable } from '../bindings/lib/binable.js'; import type { NonNegativeInteger } from '../bindings/crypto/non-negative.js'; import { asProver } from './provable-context.js'; -import { MlArray } from './ml/base.js'; +import { Bool } from './bool.js'; // external API export { Field }; @@ -544,7 +543,7 @@ class Field { */ isZero() { if (this.isConstant()) { - return Bool(this.toBigInt() === 0n); + return new Bool(this.toBigInt() === 0n); } // create witnesses z = 1/x, or z=0 if x=0, // and b = 1 - zx @@ -647,7 +646,7 @@ class Field { */ lessThan(y: Field | bigint | number | string): Bool { if (this.isConstant() && isConstant(y)) { - return Bool(this.toBigInt() < toFp(y)); + return new Bool(this.toBigInt() < toFp(y)); } return this.#compare(Field.#toVar(y)).less; } @@ -677,7 +676,7 @@ class Field { */ lessThanOrEqual(y: Field | bigint | number | string): Bool { if (this.isConstant() && isConstant(y)) { - return Bool(this.toBigInt() <= toFp(y)); + return new Bool(this.toBigInt() <= toFp(y)); } return this.#compare(Field.#toVar(y)).lessOrEqual; } @@ -910,9 +909,9 @@ class Field { if (length !== undefined) { if (bits.slice(length).some((bit) => bit)) throw Error(`Field.toBits(): ${this} does not fit in ${length} bits`); - return bits.slice(0, length).map(Bool); + return bits.slice(0, length).map((b) => new Bool(b)); } - return bits.map(Bool); + return bits.map((b) => new Bool(b)); } let [, ...bits] = Snarky.field.toBits(length ?? Fp.sizeInBits, this.value); return bits.map((b) => Bool.Unsafe.ofField(new Field(b))); @@ -987,7 +986,9 @@ class Field { * @return A {@link Field} element that is equal to the result of AST that was previously on this {@link Field} element. */ seal() { - if (this.isConstant()) return this; + // TODO: this is just commented for constraint equivalence with the old version + // uncomment to sometimes save constraints + // if (this.isConstant()) return this; let x = Snarky.field.seal(this.value); return new Field(x); } diff --git a/src/lib/group.ts b/src/lib/group.ts index 9953c44f36..3dc98e2193 100644 --- a/src/lib/group.ts +++ b/src/lib/group.ts @@ -1,8 +1,9 @@ import { Field, FieldVar, isField } from './field.js'; import { Scalar } from './scalar.js'; -import { Bool, Snarky } from '../snarky.js'; +import { Snarky } from '../snarky.js'; import { Field as Fp } from '../provable/field-bigint.js'; import { Pallas } from '../bindings/crypto/elliptic_curve.js'; +import { Bool } from './bool.js'; export { Group }; @@ -166,7 +167,7 @@ class Group { let { x: x1, y: y1 } = this; let { x: x2, y: y2 } = g; - return Bool(x1.equals(x2).and(y1.equals(y2))); + return x1.equals(x2).and(y1.equals(y2)); } else { let z = Snarky.group.equals(this.#toTuple(), g.#toTuple()); return Bool.Unsafe.ofField(new Field(z)); diff --git a/src/lib/proof_system.ts b/src/lib/proof_system.ts index 5e79852e36..779aecb507 100644 --- a/src/lib/proof_system.ts +++ b/src/lib/proof_system.ts @@ -4,8 +4,8 @@ import { EmptyVoid, } from '../bindings/lib/generic.js'; import { withThreadPool } from '../bindings/js/wrapper.js'; -import { Bool, ProvablePure, Pickles } from '../snarky.js'; -import { Field } from './core.js'; +import { ProvablePure, Pickles } from '../snarky.js'; +import { Field, Bool } from './core.js'; import { FlexibleProvable, FlexibleProvablePure, diff --git a/src/lib/provable.ts b/src/lib/provable.ts index 970f212917..4bc079b5ce 100644 --- a/src/lib/provable.ts +++ b/src/lib/provable.ts @@ -23,6 +23,7 @@ import { runUnchecked, constraintSystem, } from './provable-context.js'; +import { isBool } from './bool.js'; // external API export { Provable }; @@ -333,7 +334,11 @@ function ifImplicit(condition: Bool, x: T, y: T): T { ); // TODO remove second condition once we have consolidated field class back into one // if (type !== y.constructor) { - if (type !== y.constructor && !(isField(x) && isField(y))) { + if ( + type !== y.constructor && + !(isField(x) && isField(y)) && + !(isBool(x) && isBool(y)) + ) { throw Error( 'Provable.if: Mismatched argument types. Try using an explicit type argument:\n' + `Provable.if(bool, MyType, x, y)` diff --git a/src/lib/scalar.ts b/src/lib/scalar.ts index b048d93ef6..89736a8fb6 100644 --- a/src/lib/scalar.ts +++ b/src/lib/scalar.ts @@ -1,7 +1,8 @@ -import { Snarky, Provable, Bool } from '../snarky.js'; +import { Snarky, Provable } from '../snarky.js'; import { Scalar as Fq } from '../provable/curve-bigint.js'; import { Field, FieldConst, FieldVar } from './field.js'; import { MlArray } from './ml/base.js'; +import { Bool } from './bool.js'; export { Scalar, ScalarConst, unshift, shift }; @@ -204,7 +205,7 @@ That means it can't be called in a @method or similar environment, and there's n let lowBitMask = (1n << lowBitSize) - 1n; return { field: new Field(s & lowBitMask), - highBit: Bool(s >> lowBitSize === 1n), + highBit: new Bool(s >> lowBitSize === 1n), }; } diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index e638d9ec94..b3e106dcf2 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -1,12 +1,11 @@ import { Types } from '../bindings/mina-transaction/types.js'; import { - Bool, Gate, Pickles, Poseidon as Poseidon_, ProvablePure, } from '../snarky.js'; -import { Field } from './core.js'; +import { Field, Bool } from './core.js'; import { AccountUpdate, AccountUpdatesLayout, diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 7527d4fce3..d71de67b76 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -1,5 +1,6 @@ import type { Account as JsonAccount } from './bindings/mina-transaction/gen/transaction-json.js'; import type { Field, FieldConst, FieldVar } from './lib/field.js'; +import type { BoolVar, Bool } from './lib/bool.js'; import type { ScalarConst } from './lib/scalar.js'; import type { MlArray, @@ -10,9 +11,8 @@ import type { } from './lib/ml/base.js'; import type { MlHashInput } from './lib/ml/conversion.js'; -export { SnarkyField }; +export { SnarkyField, SnarkyBool }; export { - Bool, ProvablePure, Provable, Poseidon, @@ -24,7 +24,7 @@ export { }; // internal -export { Snarky, Test, JsonGate, MlPublicKey, MlPublicKeyVar }; +export { Snarky, Test, JsonGate, MlArray, MlPublicKey, MlPublicKeyVar }; /** * `Provable` is the general circuit type interface. Provable interface describes how a type `T` is made up of field elements and auxiliary (non-field element) data. @@ -60,8 +60,6 @@ declare namespace Snarky { type VerificationKey = unknown; type Proof = unknown; } -// same representation, but use a different name to communicate intent / constraints -type BoolVar = FieldVar; /** * Internal interface to snarky-ml @@ -179,6 +177,18 @@ declare const Snarky: { ): MlTuple, MlList>>; }; + bool: { + not(x: BoolVar): BoolVar; + + and(x: BoolVar, y: BoolVar): BoolVar; + + or(x: BoolVar, y: BoolVar): BoolVar; + + equals(x: BoolVar, y: BoolVar): BoolVar; + + assertEqual(x: BoolVar, y: BoolVar): void; + }; + group: { /** * Addition of two group elements, handles only variables. @@ -1072,8 +1082,8 @@ declare class SnarkyField { * * Use [[assertEquals]] to enforce the value of a Bool. */ -declare function Bool(x: Bool | boolean): Bool; -declare class Bool { +declare function SnarkyBool(x: Bool | boolean): Bool; +declare class SnarkyBool { constructor(x: Bool | boolean); /** diff --git a/src/snarky.js b/src/snarky.js index 882f74480b..bdc419f38c 100644 --- a/src/snarky.js +++ b/src/snarky.js @@ -4,7 +4,7 @@ import { proxyClasses } from './bindings/js/proxy.js'; export { Field as SnarkyField, - Bool, + Bool as SnarkyBool, Snarky, Poseidon, Ledger,