diff --git a/CHANGELOG.md b/CHANGELOG.md index e83e6f2e1f..c099a5d3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `Group` operations now generate a different set of constraints. This breaks deployed contracts, because the circuit changed. https://github.com/o1-labs/snarkyjs/pull/967 +### Changed + +- Improve error message `Can't evaluate prover code outside an as_prover block` https://github.com/o1-labs/snarkyjs/pull/998 + ## [0.11.0](https://github.com/o1-labs/snarkyjs/compare/a632313a...3fbd9678e) ### Breaking changes diff --git a/src/bindings b/src/bindings index adaddf68fc..fe3b5e6992 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit adaddf68fc69b549bb0e5a08a01095d435998461 +Subproject commit fe3b5e699279d19a0676102d90026c50e060e7e5 diff --git a/src/lib/account_update.ts b/src/lib/account_update.ts index e73156c430..6c9bf9f7c4 100644 --- a/src/lib/account_update.ts +++ b/src/lib/account_update.ts @@ -3,11 +3,10 @@ import { FlexibleProvable, provable, provablePure, - Struct, } from './circuit_value.js'; import { memoizationContext, memoizeWitness, Provable } from './provable.js'; import { Field, Bool } from './core.js'; -import { Ledger, Pickles, Test } from '../snarky.js'; +import { Pickles, Test } from '../snarky.js'; import { jsLayout } from '../bindings/mina-transaction/gen/js-layout.js'; import { Types, @@ -30,7 +29,6 @@ import { hashWithPrefix, packToFields } from './hash.js'; import { prefixes } from '../bindings/crypto/constants.js'; import { Context } from './global-context.js'; import { assert } from './errors.js'; -import { Ml } from './ml/conversion.js'; import { MlArray } from './ml/base.js'; import { Signature, signFieldElement } from '../mina-signer/src/signature.js'; import { MlFieldConstArray } from './ml/fields.js'; diff --git a/src/lib/account_update.unit-test.ts b/src/lib/account_update.unit-test.ts index de3a6aa028..62f33962b5 100644 --- a/src/lib/account_update.unit-test.ts +++ b/src/lib/account_update.unit-test.ts @@ -1,5 +1,4 @@ import { - Ledger, AccountUpdate, PrivateKey, Field, diff --git a/src/lib/bool.ts b/src/lib/bool.ts index ad02a6c6ef..1c704aa30d 100644 --- a/src/lib/bool.ts +++ b/src/lib/bool.ts @@ -1,5 +1,11 @@ import { Snarky } from '../snarky.js'; -import { Field, FieldConst, FieldType, FieldVar } from './field.js'; +import { + Field, + FieldConst, + FieldType, + FieldVar, + readVarMessage, +} 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'; @@ -179,8 +185,10 @@ class Bool { let value: FieldConst; if (this.isConstant()) { value = this.value[1]; - } else { + } else if (Snarky.run.inProverBlock()) { value = Snarky.field.readVar(this.value); + } else { + throw Error(readVarMessage('toBoolean', 'b', 'Bool')); } return FieldConst.equal(value, FieldConst[1]); } @@ -368,7 +376,7 @@ function toBoolean(x: boolean | Bool): boolean { if (typeof x === 'boolean') { return x; } - return (x as Bool).toBoolean(); + return x.toBoolean(); } // TODO: This is duplicated diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index 0481c95d93..b5d6a88112 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -1,6 +1,6 @@ import 'reflect-metadata'; import { ProvablePure } from '../snarky.js'; -import { Field, Bool } from './core.js'; +import { Field, Bool, Scalar, Group } from './core.js'; import { provable, provablePure, @@ -451,21 +451,26 @@ function Struct< return Struct_ as any; } -let primitives = new Set(['Field', 'Bool', 'Scalar', 'Group']); +let primitives = new Set([Field, Bool, Scalar, Group]); +function isPrimitive(obj: any) { + for (let P of primitives) { + if (obj instanceof P) return true; + } + return false; +} -// FIXME: the logic in here to check for obj.constructor.name actually doesn't work -// something that works is Field(1).constructor === obj.constructor etc function cloneCircuitValue(obj: T): T { // primitive JS types and functions aren't cloned if (typeof obj !== 'object' || obj === null) return obj; // HACK: callbacks, account udpates if ( - ['GenericArgument', 'Callback'].includes((obj as any).constructor?.name) + obj.constructor?.name.includes('GenericArgument') || + obj.constructor?.name.includes('Callback') ) { return obj; } - if (['AccountUpdate'].includes((obj as any).constructor?.name)) { + if (obj.constructor?.name.includes('AccountUpdate')) { return (obj as any).constructor.clone(obj); } @@ -480,7 +485,9 @@ function cloneCircuitValue(obj: T): T { if (ArrayBuffer.isView(obj)) return new (obj.constructor as any)(obj); // snarkyjs primitives aren't cloned - if (primitives.has((obj as any).constructor.name)) return obj; + if (isPrimitive(obj)) { + return obj; + } // cloning strategy that works for plain objects AND classes whose constructor only assigns properties let propertyDescriptors: Record = {}; diff --git a/src/lib/field.ts b/src/lib/field.ts index 2ba4e84534..ee4b9dc66e 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -2,14 +2,24 @@ import { Snarky, Provable } from '../snarky.js'; import { Field as Fp } from '../provable/field-bigint.js'; import { defineBinable } from '../bindings/lib/binable.js'; import type { NonNegativeInteger } from '../bindings/crypto/non-negative.js'; -import { asProver } from './provable-context.js'; +import { asProver, inCheckedComputation } from './provable-context.js'; import { Bool } from './bool.js'; +import { assert } from './errors.js'; // external API export { Field }; // internal API -export { ConstantField, FieldType, FieldVar, FieldConst, isField, withMessage }; +export { + ConstantField, + FieldType, + FieldVar, + FieldConst, + isField, + withMessage, + readVarMessage, + toConstantField, +}; type FieldConst = Uint8Array; @@ -190,6 +200,10 @@ class Field { return this.value[0] === FieldType.Constant; } + #toConstant(name: string): ConstantField { + return toConstantField(this, name, 'x', 'field element'); + } + /** * Create a {@link Field} element equivalent to this {@link Field} element's value, * but is a constant. @@ -204,10 +218,7 @@ class Field { * @return A constant {@link Field} element equivalent to this {@link Field} element. */ toConstant(): ConstantField { - if (this.isConstant()) return this; - // TODO: fix OCaml error message, `Can't evaluate prover code outside an as_prover block` - let value = Snarky.field.readVar(this.value); - return new Field(value) as ConstantField; + return this.#toConstant('toConstant'); } /** @@ -224,7 +235,7 @@ class Field { * @return A bigint equivalent to the bigint representation of the Field. */ toBigInt() { - let x = this.toConstant(); + let x = this.#toConstant('toBigInt'); return FieldConst.toBigint(x.value[1]); } @@ -242,7 +253,7 @@ class Field { * @return A string equivalent to the string representation of the Field. */ toString() { - return this.toBigInt().toString(); + return this.#toConstant('toString').toBigInt().toString(); } /** @@ -1111,7 +1122,7 @@ class Field { * @return A string equivalent to the JSON representation of the {@link Field}. */ toJSON() { - return this.toString(); + return this.#toConstant('toJSON').toString(); } /** @@ -1215,17 +1226,12 @@ class Field { const FieldBinable = defineBinable({ toBytes(t: Field) { - return [...t.toConstant().value[1]]; + return [...toConstantField(t, 'toBytes').value[1]]; }, readBytes(bytes, offset) { let uint8array = new Uint8Array(32); uint8array.set(bytes.slice(offset, offset + 32)); - return [ - Object.assign(Object.create(new Field(1).constructor.prototype), { - value: [0, uint8array], - }) as Field, - offset + 32, - ]; + return [new Field(uint8array), offset + 32]; }, }); @@ -1256,3 +1262,48 @@ function withMessage(error: unknown, message?: string) { error.message = `${message}\n${error.message}`; return error; } + +function toConstantField( + x: Field, + methodName: string, + varName = 'x', + varDescription = 'field element' +): ConstantField { + // if this is a constant, return it + if (x.isConstant()) return x; + + // a non-constant can only appear inside a checked computation. everything else is a bug. + assert( + inCheckedComputation(), + 'variables only exist inside checked computations' + ); + + // if we are inside an asProver or witness block, read the variable's value and return it as constant + if (Snarky.run.inProverBlock()) { + let value = Snarky.field.readVar(x.value); + return new Field(value) as ConstantField; + } + + // otherwise, calling `toConstant()` is likely a mistake. throw a helpful error message. + throw Error(readVarMessage(methodName, varName, varDescription)); +} + +function readVarMessage( + methodName: string, + varName: string, + varDescription: string +) { + return `${varName}.${methodName}() was called on a variable ${varDescription} \`${varName}\` in provable code. +This is not supported, because variables represent an abstract computation, +which only carries actual values during proving, but not during compiling. + +Also, reading out JS values means that whatever you're doing with those values will no longer be +linked to the original variable in the proof, which makes this pattern prone to security holes. + +You can check whether your ${varDescription} is a variable or a constant by using ${varName}.isConstant(). + +To inspect values for debugging, use Provable.log(${varName}). For more advanced use cases, +there is \`Provable.asProver(() => { ... })\` which allows you to use ${varName}.${methodName}() inside the callback. +Warning: whatever happens inside asProver() will not be part of the zk proof. +`; +} diff --git a/src/lib/hash-input.unit-test.ts b/src/lib/hash-input.unit-test.ts index 6277ac13cc..247029c2c7 100644 --- a/src/lib/hash-input.unit-test.ts +++ b/src/lib/hash-input.unit-test.ts @@ -107,11 +107,13 @@ function testInput( // console.log('json', json); let input1 = MlHashInput.from(toInputOcaml(JSON.stringify(json))); let input2 = Module.toInput(value); - // console.log('snarkyjs', JSON.stringify(input2)); + let input1Json = JSON.stringify(input1); + let input2Json = JSON.stringify(input2); + // console.log('snarkyjs', input2Json); // console.log(); - // console.log('protocol', JSON.stringify(input1)); - let ok1 = JSON.stringify(input2) === JSON.stringify(input1); - expect(JSON.stringify(input2)).toEqual(JSON.stringify(input1)); + // console.log('protocol', input1Json); + let ok1 = input1Json === input2Json; + expect(input2Json).toEqual(input1Json); // console.log('ok?', ok1); let fields1 = MlFieldConstArray.from( hashInputFromJson.packInput(MlHashInput.to(input1)) diff --git a/src/lib/provable-context.ts b/src/lib/provable-context.ts index 62699fa78d..05310af119 100644 --- a/src/lib/provable-context.ts +++ b/src/lib/provable-context.ts @@ -58,7 +58,7 @@ function inCompileMode() { function asProver(f: () => void) { if (inCheckedComputation()) { - Snarky.asProver(f); + Snarky.run.asProver(f); } else { f(); } @@ -67,7 +67,7 @@ function asProver(f: () => void) { function runAndCheck(f: () => void) { let id = snarkContext.enter({ inCheckedComputation: true }); try { - Snarky.runAndCheck(f); + Snarky.run.runAndCheck(f); } catch (error) { throw prettifyStacktrace(error); } finally { @@ -78,7 +78,7 @@ function runAndCheck(f: () => void) { function runUnchecked(f: () => void) { let id = snarkContext.enter({ inCheckedComputation: true }); try { - Snarky.runUnchecked(f); + Snarky.run.runUnchecked(f); } catch (error) { throw prettifyStacktrace(error); } finally { @@ -90,7 +90,7 @@ function constraintSystem(f: () => T) { let id = snarkContext.enter({ inAnalyze: true, inCheckedComputation: true }); try { let result: T; - let { rows, digest, json } = Snarky.constraintSystem(() => { + let { rows, digest, json } = Snarky.run.constraintSystem(() => { result = f(); }); let { gates, publicInputSize } = gatesFromJson(json); diff --git a/src/lib/scalar.ts b/src/lib/scalar.ts index 89736a8fb6..dcef9e20d1 100644 --- a/src/lib/scalar.ts +++ b/src/lib/scalar.ts @@ -6,6 +6,9 @@ import { Bool } from './bool.js'; export { Scalar, ScalarConst, unshift, shift }; +// internal API +export { constantScalarToBigint }; + type BoolVar = FieldVar; type ScalarConst = Uint8Array; @@ -106,18 +109,8 @@ class Scalar { // operations on constant scalars - // TODO: this is a static method so that it works on ml scalars as well - static #assertConstantStatic(x: Scalar, name: string): Fq { - if (x.constantValue === undefined) - throw Error( - `Scalar.${name}() is not available in provable code. -That means it can't be called in a @method or similar environment, and there's no alternative implemented to achieve that.` - ); - return ScalarConst.toBigint(x.constantValue); - } - #assertConstant(name: string) { - return Scalar.#assertConstantStatic(this, name); + return constantScalarToBigint(this, `Scalar.${name}`); } /** @@ -138,7 +131,7 @@ That means it can't be called in a @method or similar environment, and there's n */ add(y: Scalar) { let x = this.#assertConstant('add'); - let y0 = Scalar.#assertConstantStatic(y, 'add'); + let y0 = y.#assertConstant('add'); let z = Fq.add(x, y0); return Scalar.from(z); } @@ -150,7 +143,7 @@ That means it can't be called in a @method or similar environment, and there's n */ sub(y: Scalar) { let x = this.#assertConstant('sub'); - let y0 = Scalar.#assertConstantStatic(y, 'sub'); + let y0 = y.#assertConstant('sub'); let z = Fq.sub(x, y0); return Scalar.from(z); } @@ -162,7 +155,7 @@ That means it can't be called in a @method or similar environment, and there's n */ mul(y: Scalar) { let x = this.#assertConstant('mul'); - let y0 = Scalar.#assertConstantStatic(y, 'mul'); + let y0 = y.#assertConstant('mul'); let z = Fq.mul(x, y0); return Scalar.from(z); } @@ -175,7 +168,7 @@ That means it can't be called in a @method or similar environment, and there's n */ div(y: Scalar) { let x = this.#assertConstant('div'); - let y0 = Scalar.#assertConstantStatic(y, 'div'); + let y0 = y.#assertConstant('div'); let z = Fq.div(x, y0); if (z === undefined) throw Error('Scalar.div(): Division by zero'); return Scalar.from(z); @@ -296,7 +289,7 @@ That means it can't be called in a @method or similar environment, and there's n * This operation does _not_ affect the circuit and can't be used to prove anything about the string representation of the Scalar. */ static toJSON(x: Scalar) { - let s = Scalar.#assertConstantStatic(x, 'toJSON'); + let s = x.#assertConstant('toJSON'); return s.toString(); } @@ -360,3 +353,12 @@ function constToBigint(x: ScalarConst): Fq { function constFromBigint(x: Fq) { return Uint8Array.from(Fq.toBytes(x)); } + +function constantScalarToBigint(s: Scalar, name: string) { + if (s.constantValue === undefined) + throw Error( + `${name}() is not available in provable code. +That means it can't be called in a @method or similar environment, and there's no alternative implemented to achieve that.` + ); + return ScalarConst.toBigint(s.constantValue); +} diff --git a/src/lib/signature.ts b/src/lib/signature.ts index 094590222d..e26e68ca2b 100644 --- a/src/lib/signature.ts +++ b/src/lib/signature.ts @@ -12,6 +12,8 @@ import { PublicKey as PublicKeyBigint, } from '../provable/curve-bigint.js'; import { prefixes } from '../bindings/crypto/constants.js'; +import { constantScalarToBigint } from './scalar.js'; +import { toConstantField } from './field.js'; // external API export { PrivateKey, PublicKey, Signature }; @@ -54,7 +56,7 @@ class PrivateKey extends CircuitValue { * Convert this {@link PrivateKey} to a bigint */ toBigInt() { - return this.s.toBigInt(); + return constantScalarToBigint(this.s, 'PrivateKey.toBigInt'); } /** @@ -100,7 +102,9 @@ class PrivateKey extends CircuitValue { * @returns a base58 encoded string */ static toBase58(privateKey: { s: Scalar }) { - return PrivateKeyBigint.toBase58(privateKey.s.toBigInt()); + return PrivateKeyBigint.toBase58( + constantScalarToBigint(privateKey.s, 'PrivateKey.toBase58') + ); } } @@ -196,6 +200,7 @@ class PublicKey extends CircuitValue { * @returns a base58 encoded {@link PublicKey} */ static toBase58({ x, isOdd }: PublicKey) { + x = toConstantField(x, 'toBase58', 'pk', 'public key'); return PublicKeyBigint.toBase58({ x: x.toBigInt(), isOdd: BoolBigint(isOdd.toBoolean()), diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index eef40b3c75..cd141ae8d0 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -1,4 +1,3 @@ -import { Types } from '../bindings/mina-transaction/types.js'; import { Gate, Pickles, ProvablePure } from '../snarky.js'; import { Field, Bool } from './core.js'; import { diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 37437a1ace..f630eb60ac 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -163,25 +163,35 @@ declare const Snarky: { * witness a single field element variable */ existsVar(compute: () => FieldConst): FieldVar; + /** - * Runs code as a prover. - */ - asProver(f: () => void): void; - /** - * Runs code and checks its correctness. - */ - runAndCheck(f: () => void): void; - /** - * Runs code in prover mode, without checking correctness. - */ - runUnchecked(f: () => void): void; - /** - * Returns information about the constraint system in the callback function. + * APIs that have to do with running provable code */ - constraintSystem(f: () => void): { - rows: number; - digest: string; - json: JsonConstraintSystem; + run: { + /** + * Runs code as a prover. + */ + asProver(f: () => void): void; + /** + * Check whether we are inside an asProver or exists block + */ + inProverBlock(): boolean; + /** + * Runs code and checks its correctness. + */ + runAndCheck(f: () => void): void; + /** + * Runs code in prover mode, without checking correctness. + */ + runUnchecked(f: () => void): void; + /** + * Returns information about the constraint system in the callback function. + */ + constraintSystem(f: () => void): { + rows: number; + digest: string; + json: JsonConstraintSystem; + }; }; /**