diff --git a/CHANGELOG.md b/CHANGELOG.md index dadf1323e4..f61df445a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - **SHA256 hash function** exposed via `Hash.SHA2_256` or `Gadgets.SHA256`. https://github.com/o1-labs/o1js/pull/1285 +### Changed + +- Massively improved error reporting when detecting mismatches in the constraints generated in `compile()` vs `prove()` https://github.com/o1-labs/o1js/pull/1363 + - Made possible by new internal tooling to collect constraints, along with their stack trace, when they are added by snarky . + ### Fixed - Fix approving of complex account update layouts https://github.com/o1-labs/o1js/pull/1364 diff --git a/src/bindings b/src/bindings index a884dc593d..9830a95564 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit a884dc593dbab69e55ab9602b998ec12dfc3a288 +Subproject commit 9830a955642adf97a09719570c13272d264b2eac diff --git a/src/lib/account_update.ts b/src/lib/account_update.ts index b24b32354b..3e95bd4177 100644 --- a/src/lib/account_update.ts +++ b/src/lib/account_update.ts @@ -4,7 +4,7 @@ import { provable, provablePure, } from './circuit_value.js'; -import { memoizationContext, memoizeWitness, Provable } from './provable.js'; +import { memoizeWitness, Provable } from './provable.js'; import { Field, Bool } from './core.js'; import { Pickles, Test } from '../snarky.js'; import { jsLayout } from '../bindings/mina-transaction/gen/js-layout.js'; @@ -18,7 +18,7 @@ import { UInt64, UInt32, Int64, Sign } from './int.js'; import * as Mina from './mina.js'; import { SmartContract } from './zkapp.js'; import * as Precondition from './precondition.js'; -import { dummyBase64Proof, Empty, Proof, Prover } from './proof_system.js'; +import { Proof, Prover } from './proof_system.js'; import { Memo } from '../mina-signer/src/memo.js'; import { Events, @@ -29,9 +29,7 @@ import { hashWithPrefix, packToFields } from './hash.js'; import { mocks, prefixes } from '../bindings/crypto/constants.js'; import { Context } from './global-context.js'; import { assert } from './errors.js'; -import { MlArray } from './ml/base.js'; import { Signature, signFieldElement } from '../mina-signer/src/signature.js'; -import { MlFieldConstArray } from './ml/fields.js'; import { transactionCommitments } from '../mina-signer/src/sign-zkapp-command.js'; // external API @@ -47,7 +45,6 @@ export { FeePayerUnsigned, ZkappCommand, addMissingSignatures, - addMissingProofs, ZkappStateLength, Events, Actions, @@ -57,8 +54,11 @@ export { createChildAccountUpdate, AccountUpdatesLayout, zkAppProver, + ZkappProverData, SmartContractContext, dummySignature, + LazySignature, + LazyProof, }; const ZkappStateLength = 8; @@ -96,8 +96,8 @@ type Preconditions = AccountUpdateBody['preconditions']; */ type SetOrKeep = { isSome: Bool; value: T }; -const True = () => Bool(true); -const False = () => Bool(false); +const True = Bool(true); +const False = Bool(false); /** * One specific permission value. @@ -114,45 +114,45 @@ let Permission = { * Modification is impossible. */ impossible: (): Permission => ({ - constant: True(), - signatureNecessary: True(), - signatureSufficient: False(), + constant: True, + signatureNecessary: True, + signatureSufficient: False, }), /** * Modification is always permitted */ none: (): Permission => ({ - constant: True(), - signatureNecessary: False(), - signatureSufficient: True(), + constant: True, + signatureNecessary: False, + signatureSufficient: True, }), /** * Modification is permitted by zkapp proofs only */ proof: (): Permission => ({ - constant: False(), - signatureNecessary: False(), - signatureSufficient: False(), + constant: False, + signatureNecessary: False, + signatureSufficient: False, }), /** * Modification is permitted by signatures only, using the private key of the zkapp account */ signature: (): Permission => ({ - constant: False(), - signatureNecessary: True(), - signatureSufficient: True(), + constant: False, + signatureNecessary: True, + signatureSufficient: True, }), /** * Modification is permitted by zkapp proofs or signatures */ proofOrSignature: (): Permission => ({ - constant: False(), - signatureNecessary: False(), - signatureSufficient: True(), + constant: False, + signatureNecessary: False, + signatureSufficient: True, }), }; @@ -1775,11 +1775,6 @@ type ZkappCommandSigned = { accountUpdates: (AccountUpdate & { lazyAuthorization?: LazyProof })[]; memo: string; }; -type ZkappCommandProved = { - feePayer: FeePayerUnsigned; - accountUpdates: (AccountUpdate & { lazyAuthorization?: LazySignature })[]; - memo: string; -}; const ZkappCommand = { toPretty(transaction: ZkappCommand) { @@ -2003,127 +1998,3 @@ type ZkappPublicInput = { calls: Field; }; let ZkappPublicInput = provablePure({ accountUpdate: Field, calls: Field }); - -async function addMissingProofs( - zkappCommand: ZkappCommand, - { proofsEnabled = true } -): Promise<{ - zkappCommand: ZkappCommandProved; - proofs: (Proof | undefined)[]; -}> { - let { feePayer, accountUpdates, memo } = zkappCommand; - // compute proofs serially. in parallel would clash with our global variable - // hacks - let accountUpdatesProved: AccountUpdateProved[] = []; - let proofs: (Proof | undefined)[] = []; - for (let i = 0; i < accountUpdates.length; i++) { - let { accountUpdateProved, proof } = await addProof( - zkappCommand, - i, - proofsEnabled - ); - accountUpdatesProved.push(accountUpdateProved); - proofs.push(proof); - } - return { - zkappCommand: { feePayer, accountUpdates: accountUpdatesProved, memo }, - proofs, - }; -} - -async function addProof( - transaction: ZkappCommand, - index: number, - proofsEnabled: boolean -) { - let accountUpdate = transaction.accountUpdates[index]; - accountUpdate = AccountUpdate.clone(accountUpdate); - - if (accountUpdate.lazyAuthorization?.kind !== 'lazy-proof') { - return { - accountUpdateProved: accountUpdate as AccountUpdateProved, - proof: undefined, - }; - } - if (!proofsEnabled) { - Authorization.setProof(accountUpdate, await dummyBase64Proof()); - return { - accountUpdateProved: accountUpdate as AccountUpdateProved, - proof: undefined, - }; - } - - let lazyProof: LazyProof = accountUpdate.lazyAuthorization; - let prover = getZkappProver(lazyProof); - let proverData = { transaction, accountUpdate, index }; - let proof = await createZkappProof(prover, lazyProof, proverData); - - let accountUpdateProved = Authorization.setProof( - accountUpdate, - Pickles.proofToBase64Transaction(proof.proof) - ); - return { accountUpdateProved, proof }; -} - -async function createZkappProof( - prover: Pickles.Prover, - { - methodName, - args, - previousProofs, - ZkappClass, - memoized, - blindingValue, - }: LazyProof, - { transaction, accountUpdate, index }: ZkappProverData -): Promise> { - let publicInput = accountUpdate.toPublicInput(); - let publicInputFields = MlFieldConstArray.to( - ZkappPublicInput.toFields(publicInput) - ); - - let [, , proof] = await zkAppProver.run( - [accountUpdate.publicKey, accountUpdate.tokenId, ...args], - { transaction, accountUpdate, index }, - async () => { - let id = memoizationContext.enter({ - memoized, - currentIndex: 0, - blindingValue, - }); - try { - return await prover(publicInputFields, MlArray.to(previousProofs)); - } catch (err) { - console.error(`Error when proving ${ZkappClass.name}.${methodName}()`); - throw err; - } finally { - memoizationContext.leave(id); - } - } - ); - - let maxProofsVerified = ZkappClass._maxProofsVerified!; - const Proof = ZkappClass.Proof(); - return new Proof({ - publicInput, - publicOutput: undefined, - proof, - maxProofsVerified, - }); -} - -function getZkappProver({ methodName, ZkappClass }: LazyProof) { - if (ZkappClass._provers === undefined) - throw Error( - `Cannot prove execution of ${methodName}(), no prover found. ` + - `Try calling \`await ${ZkappClass.name}.compile()\` first, this will cache provers in the background.` - ); - let provers = ZkappClass._provers; - let methodError = - `Error when computing proofs: Method ${methodName} not found. ` + - `Make sure your environment supports decorators, and annotate with \`@method ${methodName}\`.`; - if (ZkappClass._methods === undefined) throw Error(methodError); - let i = ZkappClass._methods.findIndex((m) => m.methodName === methodName); - if (i === -1) throw Error(methodError); - return provers[i]; -} diff --git a/src/lib/gates.ts b/src/lib/gates.ts index a8900cfe49..35154986a5 100644 --- a/src/lib/gates.ts +++ b/src/lib/gates.ts @@ -15,6 +15,8 @@ export { foreignFieldAdd, foreignFieldMul, KimchiGateType, + KimchiGateTypeString, + gateTypeToString, }; const Gates = { @@ -265,3 +267,31 @@ enum KimchiGateType { Xor16, Rot64, } + +function gateTypeToString(gate: KimchiGateType) { + return KimchiGateTypeToString[gate]; +} + +type KimchiGateTypeString = + (typeof KimchiGateTypeToString)[keyof typeof KimchiGateTypeToString]; + +const KimchiGateTypeToString = { + [KimchiGateType.Zero]: 'Zero', + [KimchiGateType.Generic]: 'Generic', + [KimchiGateType.Poseidon]: 'Poseidon', + [KimchiGateType.CompleteAdd]: 'CompleteAdd', + [KimchiGateType.VarBaseMul]: 'VarBaseMul', + [KimchiGateType.EndoMul]: 'EndoMul', + [KimchiGateType.EndoMulScalar]: 'EndoMulScalar', + [KimchiGateType.Lookup]: 'Lookup', + [KimchiGateType.CairoClaim]: 'CairoClaim', + [KimchiGateType.CairoInstruction]: 'CairoInstruction', + [KimchiGateType.CairoFlags]: 'CairoFlags', + [KimchiGateType.CairoTransition]: 'CairoTransition', + [KimchiGateType.RangeCheck0]: 'RangeCheck0', + [KimchiGateType.RangeCheck1]: 'RangeCheck1', + [KimchiGateType.ForeignFieldAdd]: 'ForeignFieldAdd', + [KimchiGateType.ForeignFieldMul]: 'ForeignFieldMul', + [KimchiGateType.Xor16]: 'Xor16', + [KimchiGateType.Rot64]: 'Rot64', +} as const; diff --git a/src/lib/mina.ts b/src/lib/mina.ts index e3b5823a9c..07c38282e6 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -3,7 +3,6 @@ import { Field } from './core.js'; import { UInt32, UInt64 } from './int.js'; import { PrivateKey, PublicKey } from './signature.js'; import { - addMissingProofs, addMissingSignatures, FeePayerUnsigned, ZkappCommand, @@ -33,6 +32,7 @@ import { transactionCommitments, verifyAccountUpdateSignature, } from '../mina-signer/src/sign-zkapp-command.js'; +import { addMissingProofs } from './mina/zkapp-proof.js'; export { createTransaction, diff --git a/src/lib/mina/zkapp-proof.ts b/src/lib/mina/zkapp-proof.ts new file mode 100644 index 0000000000..3c9f57d19a --- /dev/null +++ b/src/lib/mina/zkapp-proof.ts @@ -0,0 +1,245 @@ +import { Pickles } from '../../snarky.js'; +import { + AccountUpdate, + Authorization, + FeePayerUnsigned, + LazyProof, + LazySignature, + ZkappCommand, + ZkappProverData, + ZkappPublicInput, + zkAppProver, +} from '../account_update.js'; +import { assert } from '../gadgets/common.js'; +import { MlArray } from '../ml/base.js'; +import { MlFieldConstArray } from '../ml/fields.js'; +import { + Empty, + Proof, + dummyBase64Proof, + emptyWitness, + methodArgumentsToVars, + synthesizeMethodArguments, +} from '../proof_system.js'; +import { runCircuit } from '../provable-context-debug.js'; +import { Provable, memoizationContext } from '../provable.js'; + +export { addMissingProofs }; + +type AccountUpdateProved = AccountUpdate & { + lazyAuthorization?: LazySignature; +}; + +type ZkappCommandProved = { + feePayer: FeePayerUnsigned; + accountUpdates: AccountUpdateProved[]; + memo: string; +}; + +async function addMissingProofs( + zkappCommand: ZkappCommand, + { proofsEnabled = true } +): Promise<{ + zkappCommand: ZkappCommandProved; + proofs: (Proof | undefined)[]; +}> { + let { feePayer, accountUpdates, memo } = zkappCommand; + // compute proofs serially. in parallel would clash with our global variable + // hacks + let accountUpdatesProved: AccountUpdateProved[] = []; + let proofs: (Proof | undefined)[] = []; + for (let i = 0; i < accountUpdates.length; i++) { + let { accountUpdateProved, proof } = await addProof( + zkappCommand, + i, + proofsEnabled + ); + accountUpdatesProved.push(accountUpdateProved); + proofs.push(proof); + } + return { + zkappCommand: { feePayer, accountUpdates: accountUpdatesProved, memo }, + proofs, + }; +} + +async function addProof( + transaction: ZkappCommand, + index: number, + proofsEnabled: boolean +) { + let accountUpdate = transaction.accountUpdates[index]; + accountUpdate = AccountUpdate.clone(accountUpdate); + + if (accountUpdate.lazyAuthorization?.kind !== 'lazy-proof') { + return { + accountUpdateProved: accountUpdate as AccountUpdateProved, + proof: undefined, + }; + } + if (!proofsEnabled) { + Authorization.setProof(accountUpdate, await dummyBase64Proof()); + return { + accountUpdateProved: accountUpdate as AccountUpdateProved, + proof: undefined, + }; + } + + let lazyProof: LazyProof = accountUpdate.lazyAuthorization; + let prover = getZkappProver(lazyProof); + let proverData = { transaction, accountUpdate, index }; + let proof = await createZkappProof(prover, lazyProof, proverData); + + let accountUpdateProved = Authorization.setProof( + accountUpdate, + Pickles.proofToBase64Transaction(proof.proof) + ); + return { accountUpdateProved, proof }; +} + +async function createZkappProof( + prover: Pickles.Prover, + { + methodName, + args, + previousProofs, + ZkappClass, + memoized, + blindingValue, + }: LazyProof, + { transaction, accountUpdate, index }: ZkappProverData +): Promise> { + let publicInput = accountUpdate.toPublicInput(); + let publicInputFields = MlFieldConstArray.to( + ZkappPublicInput.toFields(publicInput) + ); + + let [, , proof] = await zkAppProver.run( + [accountUpdate.publicKey, accountUpdate.tokenId, ...args], + { transaction, accountUpdate, index }, + async () => { + let id = memoizationContext.enter({ + memoized, + currentIndex: 0, + blindingValue, + }); + try { + return await prover(publicInputFields, MlArray.to(previousProofs)); + } catch (err) { + console.error( + `\n\nError when proving ${ZkappClass.name}.${methodName}()` + ); + if ( + err instanceof Error && + (err.message.includes('FieldVector.get(): Index out of bounds') || + err.message.includes('rest of division by vanishing polynomial') || + err.message.includes('Constraint unsatisfied')) + ) { + debugInconsistentConstraint(transaction, index); + } + throw err; + } finally { + memoizationContext.leave(id); + } + } + ); + + let maxProofsVerified = ZkappClass._maxProofsVerified!; + const Proof = ZkappClass.Proof(); + return new Proof({ + publicInput, + publicOutput: undefined, + proof, + maxProofsVerified, + }); +} + +function getZkappProver({ methodName, ZkappClass }: LazyProof) { + if (ZkappClass._provers === undefined) + throw Error( + `Cannot prove execution of ${methodName}(), no prover found. ` + + `Try calling \`await ${ZkappClass.name}.compile()\` first, this will cache provers in the background.` + ); + let provers = ZkappClass._provers; + let methodError = + `Error when computing proofs: Method ${methodName} not found. ` + + `Make sure your environment supports decorators, and annotate with \`@method ${methodName}\`.`; + if (ZkappClass._methods === undefined) throw Error(methodError); + let i = ZkappClass._methods.findIndex((m) => m.methodName === methodName); + if (i === -1) throw Error(methodError); + return provers[i]; +} + +// for debugging prove/compile discrepancies + +function debugInconsistentConstraint(transaction: ZkappCommand, index: number) { + let accountUpdate = transaction.accountUpdates[index]; + accountUpdate = AccountUpdate.clone(accountUpdate); + + assert( + accountUpdate.lazyAuthorization?.kind === 'lazy-proof', + 'Account update is not associated with a provable method call' + ); + + let { methodName, ZkappClass, args, memoized, blindingValue } = + accountUpdate.lazyAuthorization; + let methodIntf = ZkappClass._methods?.find( + (m) => m.methodName === methodName + ); + + // run circuit in compile mode to get expected constraints + let { constraints: expectedConstraints } = runCircuit( + () => { + let [pk, tid, ...otherArgs] = synthesizeMethodArguments( + methodIntf!, + true + ) as any[]; + let publicInput = emptyWitness(ZkappPublicInput); + + let instance = new ZkappClass(pk, tid); + (instance as any)[methodName](publicInput, ...otherArgs); + }, + { + withWitness: false, + snarkContext: { inAnalyze: true }, + createDebugTraces: true, + } + ); + + let publicInput = accountUpdate.toPublicInput(); + let proverData = { transaction, accountUpdate, index }; + + // and a second time in prove mode to get actual constraints + runCircuit( + () => { + let id = memoizationContext.enter({ + memoized, + currentIndex: 0, + blindingValue, + }); + try { + let [pk, tid, ...otherArgs] = methodArgumentsToVars( + [accountUpdate.publicKey, accountUpdate.tokenId, ...args], + methodIntf! + ).args; + let publicInput_ = Provable.witness( + ZkappPublicInput, + () => publicInput + ); + + let instance = new ZkappClass(pk, tid); + (instance as any)[methodName](publicInput_, ...otherArgs); + } finally { + memoizationContext.leave(id); + } + }, + { + withWitness: true, + snarkContext: { proverData, inProver: true }, + expectedConstraints, + unexpectedConstraintMessage: + 'Constraint generated during prove() was different than the constraint generated at this location in compile().\n' + + 'See the stack traces below for where this constraint originated.', + } + ); +} diff --git a/src/lib/proof_system.ts b/src/lib/proof_system.ts index 59ebf57ec4..e123818c4c 100644 --- a/src/lib/proof_system.ts +++ b/src/lib/proof_system.ts @@ -64,6 +64,7 @@ export { analyzeMethod, emptyValue, emptyWitness, + methodArgumentsToVars, synthesizeMethodArguments, methodArgumentsToConstant, methodArgumentTypesAndValues, @@ -832,6 +833,46 @@ function picklesRuleFromFunction( }; } +// TODO share logic with picklesRuleFromFunction + +function methodArgumentsToVars( + argsWithoutPublicInput: any[], + { allArgs, proofArgs, witnessArgs }: MethodInterface +) { + let finalArgs = []; + let proofs: Proof[] = []; + let previousStatements: Pickles.Statement[] = []; + for (let i = 0; i < allArgs.length; i++) { + let arg = allArgs[i]; + if (arg.type === 'witness') { + let type = witnessArgs[arg.index]; + finalArgs[i] = Provable.witness(type, () => { + return argsWithoutPublicInput?.[i] ?? emptyValue(type); + }); + } else if (arg.type === 'proof') { + let Proof = proofArgs[arg.index]; + let type = getStatementType(Proof); + let proof_ = (argsWithoutPublicInput?.[i] as Proof) ?? { + proof: undefined, + publicInput: emptyValue(type.input), + publicOutput: emptyValue(type.output), + }; + let { proof, publicInput, publicOutput } = proof_; + publicInput = Provable.witness(type.input, () => publicInput); + publicOutput = Provable.witness(type.output, () => publicOutput); + let proofInstance = new Proof({ publicInput, publicOutput, proof }); + finalArgs[i] = proofInstance; + proofs.push(proofInstance); + let input = toFieldVars(type.input, publicInput); + let output = toFieldVars(type.output, publicOutput); + previousStatements.push(MlPair(input, output)); + } else if (arg.type === 'generic') { + finalArgs[i] = argsWithoutPublicInput?.[i] ?? emptyGeneric(); + } + } + return { args: finalArgs, proofs, previousStatements }; +} + function synthesizeMethodArguments( { allArgs, proofArgs, witnessArgs }: MethodInterface, asVariables = false diff --git a/src/lib/provable-context-debug.ts b/src/lib/provable-context-debug.ts new file mode 100644 index 0000000000..b560cf9baa --- /dev/null +++ b/src/lib/provable-context-debug.ts @@ -0,0 +1,229 @@ +/** + * This contains a runner for provable code which is has advanced powers + * compared to the provable code runners found in `./provable-context.ts`, + * and can be used for debugging expected vs received constraints. + * + * To achieve this, it uses low-level hooks into snarky's internal state and + * contains code to interpret constraints in the format that snarky logs them in. + */ +import { Snarky } from '../snarky.js'; +import { prettifyStacktrace } from './errors.js'; +import { FieldConst, FieldVar } from './field.js'; +import { assert } from './gadgets/common.js'; +import { + KimchiGateType, + KimchiGateTypeString, + gateTypeToString, +} from './gates.js'; +import { MlArray, MlBool, MlOption, MlString, MlTuple } from './ml/base.js'; +import { SnarkContext, snarkContext } from './provable-context.js'; +import { deepEqual, stringify } from './util/nested.js'; + +export { runCircuit, SnarkyConstraint, ConstraintLog, MlConstraintSystem }; + +/** + * Run a circuit `main()` with various advanced options: + * + * - `withWitness`: whether to run witness blocks and collect the witness + * - `evalConstraints`: whether to throw on unsatisfied constraints + * - `expectedConstraints`: a list of constraints from a precious `runCircuit()` call to compare against + * - `unexpectedConstraintMessage`: a message to throw if the constraints don't match + * - `snarkContext`: extra inputs to the {@link SnarkContext} this runs in + * - `createDebugTraces`: whether to create a stack trace for each constraint, that can be be printed + * in a future run to show where an expected constraint originated + */ +function runCircuit( + main: () => void, + { + withWitness, + evalConstraints = true, + expectedConstraints, + unexpectedConstraintMessage, + snarkContext: ctx = {}, + createDebugTraces = false, + }: { + withWitness: boolean; + evalConstraints?: boolean; + expectedConstraints?: ConstraintLog[]; + unexpectedConstraintMessage?: string; + snarkContext?: SnarkContext; + createDebugTraces?: boolean; + } +) { + const snarkyState = Snarky.lowLevel.state; + let numInputs = 0; + let constraints: ConstraintLog[] = []; + + let [, state, input, aux, system] = Snarky.lowLevel.createState( + numInputs, + MlBool(evalConstraints), + MlBool(withWitness), + MlOption(function collectConstraints(_label, maybeConstraint) { + let mlConstraint = MlOption.from(maybeConstraint); + if (mlConstraint === undefined) return; + let constraint = getGateTypeAndData(mlConstraint); + let debug = createDebugTraces ? new Error(constraint.type) : undefined; + constraints.push({ constraint, debug }); + + if (expectedConstraints !== undefined) { + let expected = expectedConstraints[constraints.length - 1]; + let ok = deepEqual(constraint, expected.constraint); + if (ok) return; + + let message = + unexpectedConstraintMessage ?? + 'Generated constraint generated did not match expected constraint..\n' + + 'See the stack traces below for where this constraint originated.'; + + let expectedStackTrace = + expected.debug?.stack !== undefined + ? `\nStack trace for the expected constraint:\n\n${expected.debug.stack}\n` + : ''; + + let fullMessage = `${message}\n\nDeep equality failed:\n +actual: ${stringify(constraint)} +expected: ${stringify(expected.constraint)} +${expectedStackTrace} +Stack trace for the actual constraint: +`; + throw Error(fullMessage); + } + }) + ); + + let id = snarkContext.enter({ inCheckedComputation: true, ...ctx }); + let [, oldState] = snarkyState; + snarkyState[1] = state; + let counters = Snarky.lowLevel.pushActiveCounter(); + try { + main(); + } catch (error) { + throw prettifyStacktrace(error); + } finally { + Snarky.lowLevel.resetActiveCounter(counters); + snarkyState[1] = oldState; + snarkContext.leave(id); + } + + let publicInput = MlArray.mapFrom(input, FieldConst.toBigint); + let witness = MlArray.mapFrom(aux, FieldConst.toBigint); + + return { constraints, publicInput, witness, system }; +} + +class MlConstraintSystem { + // opaque +} + +type ConstraintLog = { + constraint: { type: ConstraintType; data: any }; + debug?: Error; +}; + +type ConstraintType = + | KimchiGateTypeString + | 'Boolean' + | 'Equal' + | 'Square' + | 'R1CS'; + +function getGateTypeAndData( + constraint: SnarkyConstraint +): ConstraintLog['constraint'] { + let [, basic] = constraint; + switch (basic[1][1].c) { + case SnarkyConstraintType.Boolean: + return { type: 'Boolean', data: basic.slice(2) }; + case SnarkyConstraintType.Equal: + return { type: 'Equal', data: basic.slice(2) }; + case SnarkyConstraintType.Square: + return { type: 'Square', data: basic.slice(2) }; + case SnarkyConstraintType.R1CS: + return { type: 'R1CS', data: basic.slice(2) }; + case SnarkyConstraintType.Added: + // why can't TS narrow this? + let [plonkConstraint, ...data] = basic[2] as [PlonkConstraint, ...any]; + let kimchiGateType = plonkConstraintToKimchiGateType[plonkConstraint]; + assert(kimchiGateType !== undefined, 'unimplemented'); + return { type: gateTypeToString(kimchiGateType), data }; + default: + assert(false); + } +} + +type SnarkyConstraint = [ + _: 0, + basic: SnarkyConstraintBasic, + annotation: MlOption +]; + +// types that are defined by `type t = ..`, `type t += `, etc +type MlIncrementalTypeEnum = [ + 248, + { t: number; c: T; l: number }, + number +]; + +// matches Snarky_backendless.Constraint.basic +type SnarkyConstraintBasic = + | [0, MlIncrementalTypeEnum, FieldVar] + | [0, MlIncrementalTypeEnum, MlTuple] + | [ + 0, + MlIncrementalTypeEnum, + MlTuple + ] + | [0, MlIncrementalTypeEnum, MlTuple] + | [ + 0, + MlIncrementalTypeEnum<'Snarky_backendless__Constraint.Add_kind(C).T'>, + [PlonkConstraint, ...any] + ]; + +enum SnarkyConstraintType { + Boolean = 'Snarky_backendless__Constraint.Boolean', + Equal = 'Snarky_backendless__Constraint.Equal', + Square = 'Snarky_backendless__Constraint.Square', + R1CS = 'Snarky_backendless__Constraint.R1CS', + Added = 'Snarky_backendless__Constraint.Add_kind(C).T', +} + +// matches Plonk_constraint_system.Plonk_constraint.t +enum PlonkConstraint { + Basic, + Poseidon, + EC_add_complete, + EC_scale, + EC_endoscale, + EC_endoscalar, + Lookup, + RangeCheck0, + RangeCheck1, + Xor, + ForeignFieldAdd, + ForeignFieldMul, + Rot64, + AddFixedLookupTable, + AddRuntimeTableCfg, + Raw, +} + +const plonkConstraintToKimchiGateType = { + [PlonkConstraint.Basic]: KimchiGateType.Generic, + [PlonkConstraint.Poseidon]: KimchiGateType.Poseidon, + [PlonkConstraint.EC_add_complete]: KimchiGateType.CompleteAdd, + [PlonkConstraint.EC_scale]: KimchiGateType.VarBaseMul, + [PlonkConstraint.EC_endoscale]: KimchiGateType.EndoMul, + [PlonkConstraint.EC_endoscalar]: KimchiGateType.EndoMulScalar, + [PlonkConstraint.Lookup]: KimchiGateType.Lookup, + [PlonkConstraint.RangeCheck0]: KimchiGateType.RangeCheck0, + [PlonkConstraint.RangeCheck1]: KimchiGateType.RangeCheck1, + [PlonkConstraint.Xor]: KimchiGateType.Xor16, + [PlonkConstraint.ForeignFieldAdd]: KimchiGateType.ForeignFieldAdd, + [PlonkConstraint.ForeignFieldMul]: KimchiGateType.ForeignFieldMul, + [PlonkConstraint.Rot64]: KimchiGateType.Rot64, + [PlonkConstraint.AddFixedLookupTable]: undefined, + [PlonkConstraint.AddRuntimeTableCfg]: undefined, + // isn't used for other stuff + [PlonkConstraint.Raw]: KimchiGateType.Zero, +} as const; diff --git a/src/lib/provable-context-debug.unit-test.ts b/src/lib/provable-context-debug.unit-test.ts new file mode 100644 index 0000000000..1ffbbd3c77 --- /dev/null +++ b/src/lib/provable-context-debug.unit-test.ts @@ -0,0 +1,16 @@ +import { Field } from './core.js'; +import { Gadgets } from './gadgets/gadgets.js'; +import { runCircuit } from './provable-context-debug.js'; +import { Provable } from './provable.js'; + +let cs = runCircuit( + () => { + let x = Provable.witness(Field, () => Field(5)); + let y = x.mul(x); + Gadgets.rangeCheck16(y); + Gadgets.rangeCheck64(y); + }, + { withWitness: false } +); + +console.log(cs); diff --git a/src/lib/provable-context.ts b/src/lib/provable-context.ts index c285ee81f8..c8fdf2f236 100644 --- a/src/lib/provable-context.ts +++ b/src/lib/provable-context.ts @@ -1,5 +1,18 @@ +/** + * This contains functions to manage the global context for provable code: + * + * - A global `snarkContext` which tells us whether we're in a provable context, + * and contains a small amount of global state on top of snarky's internal state. + * - Various "runners" which execute provable code in different modes. + */ import { Context } from './global-context.js'; -import { Gate, GateType, JsonGate, Snarky } from '../snarky.js'; +import { + Gate, + GateType, + JsonConstraintSystem, + JsonGate, + Snarky, +} from '../snarky.js'; import { parseHexString32 } from '../bindings/crypto/bigint-helpers.js'; import { prettifyStacktrace } from './errors.js'; import { Fp } from '../bindings/crypto/finite_field.js'; @@ -19,6 +32,7 @@ export { inCompileMode, gatesFromJson, printGates, + constraintSystemFromJson, }; // global circuit-related context @@ -88,32 +102,14 @@ function runUnchecked(f: () => void) { } } -function constraintSystem(f: () => T) { +function constraintSystem(f: () => void) { let id = snarkContext.enter({ inAnalyze: true, inCheckedComputation: true }); try { - let result: T; - let { rows, digest, json } = Snarky.run.constraintSystem(() => { - result = f(); - }); - let { gates, publicInputSize } = gatesFromJson(json); + let { rows, digest, json } = Snarky.run.constraintSystem(f); return { rows, digest, - result: result! as T, - gates, - publicInputSize, - print() { - printGates(gates); - }, - summary() { - let gateTypes: Partial> = {}; - gateTypes['Total rows'] = rows; - for (let gate of gates) { - gateTypes[gate.type] ??= 0; - gateTypes[gate.type]!++; - } - return gateTypes; - }, + ...constraintSystemFromJson(json), }; } catch (error) { throw prettifyStacktrace(error); @@ -122,6 +118,26 @@ function constraintSystem(f: () => T) { } } +function constraintSystemFromJson(json: JsonConstraintSystem) { + let { gates, publicInputSize } = gatesFromJson(json); + return { + gates, + publicInputSize, + print() { + printGates(gates); + }, + summary() { + let gateTypes: Partial> = {}; + gateTypes['Total rows'] = gates.length; + for (let gate of gates) { + gateTypes[gate.type] ??= 0; + gateTypes[gate.type]!++; + } + return gateTypes; + }, + }; +} + // helpers function gatesFromJson(cs: { gates: JsonGate[]; public_input_size: number }) { diff --git a/src/lib/util/nested.ts b/src/lib/util/nested.ts new file mode 100644 index 0000000000..85e03555cd --- /dev/null +++ b/src/lib/util/nested.ts @@ -0,0 +1,69 @@ +export { assertDeepEqual, deepEqual, stringify }; + +type Nested = + | number + | bigint + | string + | boolean + | null + | undefined + | Nested[] + | { [key: string]: Nested }; + +function assertDeepEqual(actual: Nested, expected: Nested, message?: string) { + if (!deepEqual(actual, expected)) { + let fullMessage = `${message ? `${message}\n\n` : ''}Deep equality failed: + +actual: ${stringify(actual)} +expected: ${stringify(expected)} +`; + throw Error(fullMessage); + } +} + +function deepEqual(a: Nested, b: Nested): boolean { + if (a === b) return true; + if (typeof a !== typeof b) return false; + if (typeof a !== 'object' || typeof b !== 'object') return false; + if (a === null || b === null) return false; + if (Array.isArray(a)) { + if (!Array.isArray(b)) return false; + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (!deepEqual(a[i], b[i])) return false; + } + return true; + } + if (Array.isArray(b)) return false; + if (Object.keys(a).length !== Object.keys(b).length) return false; + for (const key in a) { + if (!deepEqual(a[key], b[key])) return false; + } + return true; +} + +function stringify(x: Nested): string { + if (typeof x === 'object') { + if (x === null) return 'null'; + if (Array.isArray(x)) return `[${x.map(stringify).join(', ')}]`; + let result = '{ '; + for (let key in x) { + if (result.length > 2) result += ', '; + result += `${key}: ${stringify(x[key])}`; + } + return result + ' }'; + } + switch (typeof x) { + case 'string': + return `"${x}"`; + case 'bigint': + return `${x.toString()}n`; + case 'number': + case 'boolean': + return x.toString(); + case 'undefined': + return 'undefined'; + default: + throw Error(`Unexpected type ${typeof x}`); + } +} diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 9346973f72..d37cc7d96c 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -1186,7 +1186,8 @@ super.init(); try { for (let methodIntf of methodIntfs) { let accountUpdate: AccountUpdate; - let { rows, digest, result, gates } = analyzeMethod( + let hasReturn = false; + let { rows, digest, gates } = analyzeMethod( ZkappPublicInput, methodIntf, (publicInput, publicKey, tokenId, ...args) => { @@ -1195,15 +1196,15 @@ super.init(); publicInput, ...args ); + hasReturn = result !== undefined; accountUpdate = instance.#executionState!.accountUpdate; - return result; } ); methodMetadata[methodIntf.methodName] = { actions: accountUpdate!.body.actions.data.length, rows, digest, - hasReturn: result !== undefined, + hasReturn, gates, }; } diff --git a/src/mina b/src/mina index 2a968c8347..f867de6429 160000 --- a/src/mina +++ b/src/mina @@ -1 +1 @@ -Subproject commit 2a968c83477ed9f9e3b30a02cc357e541b76dcac +Subproject commit f867de6429dd433b483b7e5b8c668166a7e275c8 diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 2c47b9dc63..8076b94a58 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -26,6 +26,11 @@ import type { WasmFqSrs, } from './bindings/compiled/node_bindings/plonk_wasm.cjs'; import type { KimchiGateType } from './lib/gates.ts'; +import type { + SnarkyConstraint, + MlConstraintSystem, +} from './lib/provable-context-debug.js'; +import type { FieldVector } from './bindings/crypto/bindings/vector.ts'; export { ProvablePure, Provable, Ledger, Pickles, Gate, GateType, getWasm }; @@ -38,6 +43,8 @@ export { MlPublicKeyVar, FeatureFlags, MlFeatureFlags, + SnarkyState, + JsonConstraintSystem, }; /** @@ -540,8 +547,59 @@ declare const Snarky: { squeeze(sponge: unknown): FieldVar; }; }; + + lowLevel: { + state: MlRef; + createState( + numInputs: number, + evalConstraints: MlBool, + withWitness: MlBool, + logConstraint: MlOption< + ( + atLabelBoundary: MlOption, + constraint: MlOption + ) => void + > + ): [ + _: 0, + state: SnarkyState, + input: FieldVector, + aux: FieldVector, + system: MlConstraintSystem + ]; + + pushActiveCounter(): MlList; + resetActiveCounter(counters: MlList): void; + + constraintSystem: { + getRows(system: MlConstraintSystem): number; + digest(system: MlConstraintSystem): string; + toJson(system: MlConstraintSystem): JsonConstraintSystem; + }; + }; }; +type MlRef = [_: 0, contents: T]; + +type SnarkyVector = [0, [unknown, number, FieldVector]]; +type ConstraintSystem = unknown; + +type SnarkyState = [ + _: 0, + system: MlOption, + input: SnarkyVector, + aux: SnarkyVector, + eval_constraints: MlBool, + num_inputs: number, + next_auxiliary: MlRef, + has_witness: MlBool, + stack: MlList, + handler: unknown, + is_running: MlBool, + as_prover: MlRef, + log_constraint: unknown +]; + type GateType = | 'Zero' | 'Generic'