From 21a0581682cf8953b651643a9cf275ac28f2de92 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Jul 2022 11:13:53 +0200 Subject: [PATCH 01/10] get rid of `otherContext` --- src/lib/global-context.ts | 1 - src/lib/zkapp.ts | 21 ++++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/lib/global-context.ts b/src/lib/global-context.ts index 35f915602..2981f48df 100644 --- a/src/lib/global-context.ts +++ b/src/lib/global-context.ts @@ -22,7 +22,6 @@ type MainContext = { inCompile?: boolean; inCheckedComputation?: boolean; inAnalyze?: boolean; - otherContext?: any; }; let mainContext = undefined as MainContext | undefined; diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index f66c9d436..188ad0bce 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -113,7 +113,7 @@ function wrapMethod( return function wrappedMethod(this: SmartContract, ...actualArgs: any[]) { cleanStatePrecondition(this); if (inCheckedComputation()) { - return withContext( + let [context, result] = withContext( { ...mainContext, // important to run this with a fresh party everytime, otherwise compile messes up our circuits @@ -137,7 +137,9 @@ function wrapMethod( assertStatePrecondition(this); return result; } - )[1]; + ); + if (mainContext !== undefined) mainContext.self = context.self; + return result; } else if (Mina.currentTransaction === undefined) { // outside a transaction, just call the method, but check precondition invariants let result = method.apply(this, actualArgs); @@ -395,14 +397,14 @@ class SmartContract { let { context, rows, digest } = analyzeMethod( ZkappPublicInput, methodIntf, - (...args) => (instance as any)[methodIntf.methodName](...args), - { - self: selfParty(address), - otherContext: { numberOfSequenceEvents: 0 }, - } + (...args) => (instance as any)[methodIntf.methodName](...args) + ); + console.log( + methodIntf.methodName, + context.self!.body.sequenceEvents.data.length ); ZkappClass._methodMetadata[methodIntf.methodName] = { - sequenceEvents: context?.otherContext?.numberOfSequenceEvents, + sequenceEvents: context.self!.body.sequenceEvents.data.length, rows, digest, }; @@ -456,9 +458,6 @@ class ${contract.constructor.name} extends SmartContract { party.body.sequenceEvents, eventFields ); - if (mainContext?.otherContext?.numberOfSequenceEvents !== undefined) { - mainContext.otherContext.numberOfSequenceEvents += 1; - } }, reduce( From af7f192c07455c586d9b8e136d306aa5131ea62f Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Jul 2022 14:32:43 +0200 Subject: [PATCH 02/10] split global context into multiple independent contexts, and fix another problem with analyzeMethods --- src/lib/circuit_value.ts | 30 ++++--- src/lib/global-context.ts | 170 ++++++++++++++++++-------------------- src/lib/hash.ts | 2 +- src/lib/mina.ts | 17 +++- src/lib/party.ts | 30 ++++--- src/lib/precondition.ts | 9 +- src/lib/proof_system.ts | 71 +++++++++++----- src/lib/state.ts | 19 +++-- src/lib/zkapp.ts | 52 ++++++------ src/snarky.d.ts | 6 +- 10 files changed, 227 insertions(+), 179 deletions(-) diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index 498264deb..94eb16ade 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -1,6 +1,6 @@ import 'reflect-metadata'; import { Circuit, Field, Bool, JSONValue, AsFieldElements } from '../snarky'; -import { withContext } from './global-context'; +import { snarkContext } from './proof_system'; // external API export { @@ -301,14 +301,17 @@ function circuitMain( } target.snarkyMain = (w: Array, pub: Array) => { - let [, result] = withContext({ inCheckedComputation: true }, () => { - let args = []; - for (let i = 0; i < numArgs; ++i) { - args.push((publicIndexSet.has(i) ? pub : w).shift()); - } + let [, result] = snarkContext.runWith( + { inCheckedComputation: true }, + () => { + let args = []; + for (let i = 0; i < numArgs; ++i) { + args.push((publicIndexSet.has(i) ? pub : w).shift()); + } - return target[propertyName].apply(target, args); - }); + return target[propertyName].apply(target, args); + } + ); return result; }; @@ -511,12 +514,15 @@ Circuit.switch = function >( return type.ofFields(fields); }; -Circuit.constraintSystem = function (f) { - let [, result] = withContext( +Circuit.constraintSystem = function (f: () => T) { + let [, result] = snarkContext.runWith( { inAnalyze: true, inCheckedComputation: true }, () => { - let { rows, digest, json } = (Circuit as any)._constraintSystem(f); - return { rows, digest }; + let result: T; + let { rows, digest, json } = (Circuit as any)._constraintSystem(() => { + result = f(); + }); + return { rows, digest, result: result! }; } ); return result; diff --git a/src/lib/global-context.ts b/src/lib/global-context.ts index 2981f48df..623366365 100644 --- a/src/lib/global-context.ts +++ b/src/lib/global-context.ts @@ -1,109 +1,97 @@ import { Party } from './party'; -export { - mainContext, - withContext, - withContextAsync, - getContext, - inProver, - inCompile, - inCheckedComputation, - inAnalyze, -}; +export { Context }; -// context for compiling / proving -// TODO reconcile mainContext with currentTransaction -type MainContext = { - witnesses?: unknown[]; - self?: Party; - expectedAccesses: number | undefined; - actualAccesses: number; - inProver?: boolean; - inCompile?: boolean; - inCheckedComputation?: boolean; - inAnalyze?: boolean; -}; +namespace Context { + export type id = number; -let mainContext = undefined as MainContext | undefined; -type PartialContext = Partial; + export type t = { + data: { context: Context; id: id }[]; + allowsNesting: boolean; -function withContext( - { - witnesses = undefined, - expectedAccesses = undefined, - actualAccesses = 0, - self, - ...other - }: PartialContext, - f: () => T -) { - let prevContext = mainContext; - mainContext = { witnesses, expectedAccesses, actualAccesses, self, ...other }; - let result: T; - let resultContext: MainContext; + get(): Context; + has(): boolean; + runWith(context: Context, func: () => Result): [Context, Result]; + runWithAsync( + context: Context, + func: () => Promise + ): Promise<[Context, Result]>; + }; +} +const Context = { create }; + +function create( + options = { + allowsNesting: true, + default: undefined, + } as { allowsNesting?: boolean; default?: C } +): Context.t { + let t: Context.t = { + data: [], + allowsNesting: options.allowsNesting ?? true, + get: () => get(t), + has: () => t.data.length !== 0, + runWith: (context, func) => runWith(t, context, func), + runWithAsync: (context, func) => runWithAsync(t, context, func), + }; + if (options.default !== undefined) enter(t, options.default); + return t; +} + +function enter(t: Context.t, context: C): Context.id { + if (t.data.length > 0 && !t.allowsNesting) { + throw Error(contextConflictMessage); + } + let id = Math.random(); + t.data.push({ context, id }); + return id; +} + +function leave(t: Context.t, id: Context.id): C { + let current = t.data.pop(); + if (current === undefined) throw Error(contextConflictMessage); + if (current.id !== id) throw Error(contextConflictMessage); + return current.context; +} + +function get(t: Context.t): C { + if (t.data.length === 0) throw Error(contextConflictMessage); + let current = t.data[t.data.length - 1]; + return current.context; +} + +function runWith( + t: Context.t, + context: C, + func: () => Result +): [C, Result] { + let id = enter(t, context); + let result: Result; + let resultContext: C; try { - result = f(); + result = func(); } finally { - resultContext = mainContext; - mainContext = prevContext; + resultContext = leave(t, id); } - return [resultContext, result] as [MainContext, T]; + return [resultContext, result]; } -// TODO: this is unsafe, the mainContext will be overridden if we invoke this function multiple times concurrently -// at the moment, we solve this by detecting unsafe use and throwing an error -async function withContextAsync( - { - witnesses = undefined, - expectedAccesses = 1, - actualAccesses = 0, - self, - ...other - }: PartialContext, - f: () => Promise -) { - let prevContext = mainContext; - mainContext = { witnesses, expectedAccesses, actualAccesses, self, ...other }; - let result: T; - let resultContext: MainContext; +async function runWithAsync( + t: Context.t, + context: C, + func: () => Promise +): Promise<[C, Result]> { + let id = enter(t, context); + let result: Result; + let resultContext: C; try { - result = await f(); - if (mainContext.actualAccesses !== mainContext.expectedAccesses) - throw Error(contextConflictMessage); + result = await func(); } finally { - resultContext = mainContext; - mainContext = prevContext; + resultContext = leave(t, id); } - return [resultContext, result] as [MainContext, T]; + return [resultContext, result]; } let contextConflictMessage = "It seems you're running multiple provers concurrently within" + ' the same JavaScript thread, which, at the moment, is not supported and would lead to bugs.'; -function getContext() { - if (mainContext === undefined) throw Error(contextConflictMessage); - mainContext.actualAccesses++; - if ( - mainContext.expectedAccesses !== undefined && - mainContext.actualAccesses > mainContext.expectedAccesses - ) - throw Error(contextConflictMessage); - return mainContext; -} - -function inProver() { - return !!mainContext?.inProver; -} -function inCompile() { - return !!mainContext?.inCompile; -} -function inCheckedComputation() { - return ( - !!mainContext?.inCompile || - !!mainContext?.inProver || - !!mainContext?.inCheckedComputation - ); -} -function inAnalyze() { - return !!mainContext?.inAnalyze; -} diff --git a/src/lib/hash.ts b/src/lib/hash.ts index 493eb09c0..7b4bc3010 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -1,5 +1,5 @@ import { Poseidon as Poseidon_, Field } from '../snarky'; -import { inCheckedComputation } from './global-context'; +import { inCheckedComputation } from './proof_system'; // external API export { Poseidon }; diff --git a/src/lib/mina.ts b/src/lib/mina.ts index ca826a8d5..2a5c06b2d 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -16,7 +16,7 @@ import { import * as Fetch from './fetch'; import { assertPreconditionInvariants, NetworkValue } from './precondition'; import { cloneCircuitValue } from './circuit_value'; -import { Proof } from './proof_system'; +import { Proof, snarkContext } from './proof_system'; export { createUnsignedTransaction, @@ -104,7 +104,20 @@ function createTransaction( try { // run circuit - Circuit.runAndCheck(f); + let err: any; + while (true) { + // this is such a ridiculous hack + if (err !== undefined) err.bootstrap(); + try { + snarkContext.runWith({ inRunAndCheck: true }, () => + Circuit.runAndCheck(f) + ); + break; + } catch (err_) { + if ((err_ as any)?.bootstrap) err = err_; + else throw err_; + } + } // check that on-chain values weren't used without setting a precondition for (let party of currentTransaction.parties) { diff --git a/src/lib/party.ts b/src/lib/party.ts index f82089512..6d0059352 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -4,9 +4,9 @@ import { PrivateKey, PublicKey } from './signature'; import { UInt64, UInt32, Int64 } from './int'; import * as Mina from './mina'; import { SmartContract } from './zkapp'; -import { withContextAsync } from './global-context'; +import { Context } from './global-context'; import * as Precondition from './precondition'; -import { Proof } from './proof_system'; +import { Proof, snarkContext } from './proof_system'; import { emptyHashWithPrefix, hashWithPrefix, prefixes } from './hash'; export { @@ -31,10 +31,17 @@ export { ZkappPublicInput, Events, partyToPublicInput, + partyContext, + PartyContext, }; const ZkappStateLength = 8; +// global self-party context +// TODO: merge with Mina.currentTransaction +type PartyContext = { self: Party }; +let partyContext = Context.create(); + type PartyBody = Types.Party['body']; type Update = PartyBody['update']; @@ -549,7 +556,7 @@ class Party { * @method onlyRunsWhenBalanceIsLow() { * let lower = UInt64.zero; * let upper = UInt64.fromNumber(20e9); - * Party.assertBetween(this.self.body.accountPrecondition.balance, lower, upper); + * Party.assertBetween(this.self.body.preconditions.account.balance, lower, upper); * // ... * } * ``` @@ -570,7 +577,7 @@ class Party { * * ```ts * @method onlyRunsWhenNonceIsZero() { - * Party.assertEquals(this.self.body.accountPrecondition.nonce, UInt32.zero); + * Party.assertEquals(this.self.body.preconditions.account.nonce, UInt32.zero); * // ... * } * ``` @@ -670,17 +677,12 @@ class Party { } static createUnsigned(publicKey: PublicKey) { - // TODO: This should be a witness block that uses the setVariable - // API to set the value of a variable after it's allocated - - const pk = publicKey; - const body: Body = Body.keepAll(pk); + const body: Body = Body.keepAll(publicKey); if (Mina.currentTransaction === undefined) { throw new Error( 'Party.createUnsigned: Cannot run outside of a transaction' ); } - const party = new Party(body); Mina.currentTransaction.nextPartyIndex++; Mina.currentTransaction.parties.push(party); @@ -933,12 +935,8 @@ async function addMissingProofs(parties: Parties): Promise<{ if (ZkappClass._methods === undefined) throw Error(methodError); let i = ZkappClass._methods.findIndex((m) => m.methodName === method.name); if (i === -1) throw Error(methodError); - let [, proof] = await withContextAsync( - { - self: Party.defaultParty(party.body.publicKey), - witnesses: args, - inProver: true, - }, + let [, proof] = await snarkContext.runWithAsync( + { inProver: true, witnesses: args }, () => provers[i](publicInputFields, previousProofs) ); party.authorization = { proof: Pickles.proofToBase64Transaction(proof) }; diff --git a/src/lib/precondition.ts b/src/lib/precondition.ts index 30c168015..538398ed4 100644 --- a/src/lib/precondition.ts +++ b/src/lib/precondition.ts @@ -10,9 +10,8 @@ import { circuitValueEquals } from './circuit_value'; import { PublicKey } from './signature'; import * as Mina from './mina'; import { Party, Preconditions } from './party'; -import * as GlobalContext from './global-context'; import { UInt32, UInt64 } from './int'; -import { emptyValue } from './proof_system'; +import { emptyValue, inAnalyze, inCompile, inProver } from './proof_system'; export { preconditions, @@ -162,14 +161,14 @@ function getVariable( fieldType: AsFieldElements ): U { // in compile, just return an empty variable - if (GlobalContext.inCompile()) { + if (inCompile()) { return Circuit.witness(fieldType, (): U => { throw Error( `This error is thrown because you are reading out the value of a variable, when that value is not known. To write a correct circuit, you must avoid any dependency on the concrete value of variables.` ); }); - } else if (GlobalContext.inAnalyze()) { + } else if (inAnalyze()) { return emptyValue(fieldType); } // if not in compile, get the variable's value first @@ -192,7 +191,7 @@ To write a correct circuit, you must avoid any dependency on the concrete value } // in prover, return a new variable which holds the value // outside, just return the value - if (GlobalContext.inProver()) { + if (inProver()) { return Circuit.witness(fieldType, () => value); } else { return value; diff --git a/src/lib/proof_system.ts b/src/lib/proof_system.ts index f86b23a0b..1476177fe 100644 --- a/src/lib/proof_system.ts +++ b/src/lib/proof_system.ts @@ -6,7 +6,7 @@ import { Circuit, Poseidon, } from '../snarky'; -import { getContext, withContext, withContextAsync } from './global-context'; +import { Context } from './global-context'; // public API export { Proof, SelfProof, ZkProgram, verify }; @@ -23,8 +23,24 @@ export { emptyValue, emptyWitness, synthesizeMethodArguments, + snarkContext, + inProver, + inCompile, + inAnalyze, + inCheckedComputation, }; +// global circuit-related context +type SnarkContext = { + witnesses?: unknown[]; + inProver?: boolean; + inCompile?: boolean; + inCheckedComputation?: boolean; + inAnalyze?: boolean; + inRunAndCheck?: boolean; +}; +let snarkContext = Context.create({ default: {} }); + class Proof { static publicInputType: AsFieldElements = undefined as any; static tag: () => { name: string } = () => { @@ -198,7 +214,7 @@ function ZkProgram< let publicInputFields = publicInputType.toFields(publicInput); let previousProofs = getPreviousProofsForProver(args, methodIntfs[i]); - let [, proof] = await withContextAsync( + let [, proof] = await snarkContext.runWithAsync( { witnesses: args, inProver: true }, () => picklesProver!(publicInputFields, previousProofs) ); @@ -350,38 +366,34 @@ function compileProgram( methodEntry ) ); - let [, { getVerificationKeyArtifact, provers, verify, tag }] = withContext( - { inCompile: true, ...additionalContext }, - () => Pickles.compile(rules, publicInputType.sizeInFields()) - ); + let [, { getVerificationKeyArtifact, provers, verify, tag }] = + snarkContext.runWith({ inCompile: true, ...additionalContext }, () => + Pickles.compile(rules, publicInputType.sizeInFields()) + ); CompiledTag.store(proofSystemTag, tag); return { getVerificationKeyArtifact, provers, verify, tag }; } -function analyzeMethod( +function analyzeMethod( publicInputType: AsFieldElements, methodIntf: MethodInterface, - method: (...args: any) => void, - additionalContext?: any + method: (...args: any) => T ) { - let [context, { rows, digest }] = withContext( - { - inAnalyze: true, - inCheckedComputation: true, - ...additionalContext, - }, + let [, { rows, digest, result }] = snarkContext.runWith( + { inAnalyze: true, inCheckedComputation: true }, () => { + let result: T; let { rows, digest }: ReturnType = ( Circuit as any )._constraintSystem(() => { let args = synthesizeMethodArguments(methodIntf, true); let publicInput = emptyWitness(publicInputType); - method(publicInput, ...args); + result = method(publicInput, ...args); }); - return { rows, digest }; + return { rows, digest, result: result! }; } ); - return { context, rows, digest }; + return { rows, digest, result: result as T }; } function picklesRuleFromFunction( @@ -394,7 +406,7 @@ function picklesRuleFromFunction( publicInput: Pickles.PublicInput, previousInputs: Pickles.PublicInput[] ) { - let { witnesses: argsWithoutPublicInput } = getContext(); + let { witnesses: argsWithoutPublicInput } = snarkContext.get(); let finalArgs = []; let proofs: Proof[] = []; for (let i = 0; i < allArgs.length; i++) { @@ -495,6 +507,27 @@ ZkProgram.Proof = function < }; }; +// helpers for circuit context + +function inProver() { + return !!snarkContext.get().inProver; +} +function inCompile() { + return !!snarkContext.get().inCompile; +} +function inAnalyze() { + return !!snarkContext.get().inAnalyze; +} +function inCheckedComputation() { + return ( + !!snarkContext.get().inCompile || + !!snarkContext.get().inProver || + !!snarkContext.get().inCheckedComputation + ); +} + +// helper types + type Tuple = [T, ...T[]] | []; // TODO: inference of AsFieldElements shouldn't just use InstanceType diff --git a/src/lib/state.ts b/src/lib/state.ts index eaac7f97a..fe3a9d98c 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -4,7 +4,12 @@ import { Party } from './party'; import { PublicKey } from './signature'; import * as Mina from './mina'; import { Account, fetchAccount } from './fetch'; -import * as GlobalContext from './global-context'; +import { + inAnalyze, + inCheckedComputation, + inCompile, + inProver, +} from './proof_system'; import { SmartContract } from './zkapp'; import { emptyValue } from './proof_system'; @@ -190,7 +195,7 @@ function createState(): InternalStateType { this._contract.cachedVariable !== undefined && // `inCheckedComputation() === true` here always implies being inside a wrapped smart contract method, // which will ensure that the cache is cleaned up before & after each method run. - GlobalContext.inCheckedComputation() + inCheckedComputation() ) { this._contract.wasRead = true; return this._contract.cachedVariable; @@ -198,15 +203,15 @@ function createState(): InternalStateType { let layout = getLayoutPosition(this._contract); let address: PublicKey = this._contract.instance.address; let stateAsFields: Field[]; - let inProver = GlobalContext.inProver(); + let inProver_ = inProver(); let stateFieldsType = circuitArray(Field, layout.length); - if (!GlobalContext.inCompile() && !GlobalContext.inAnalyze()) { + if (!inCompile() && !inAnalyze()) { let account: Account; try { account = Mina.getAccount(address); } catch (err) { // TODO: there should also be a reasonable error here - if (inProver) { + if (inProver_) { throw err; } throw Error( @@ -225,12 +230,12 @@ function createState(): InternalStateType { } // in prover, create a new witness with the state values // outside, just return the state values - stateAsFields = inProver + stateAsFields = inProver_ ? Circuit.witness(stateFieldsType, () => stateAsFields) : stateAsFields; } else { // in compile, we don't need the witness values - stateAsFields = GlobalContext.inCompile() + stateAsFields = inCompile() ? Circuit.witness(stateFieldsType, (): Field[] => { throw Error('this should never happen'); }) diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 188ad0bce..f8a2aa3ae 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -19,17 +19,12 @@ import { ZkappPublicInput, Events, partyToPublicInput, + partyContext, + PartyContext, } from './party'; import { PrivateKey, PublicKey } from './signature'; import * as Mina from './mina'; import { UInt32, UInt64 } from './int'; -import { - mainContext, - inCheckedComputation, - withContext, - inProver, - inAnalyze, -} from './global-context'; import { assertPreconditionInvariants, cleanPreconditionsCache, @@ -42,6 +37,10 @@ import { Proof, emptyValue, analyzeMethod, + inCheckedComputation, + snarkContext, + inProver, + inAnalyze, } from './proof_system'; import { assertStatePrecondition, cleanStatePrecondition } from './state'; @@ -113,13 +112,10 @@ function wrapMethod( return function wrappedMethod(this: SmartContract, ...actualArgs: any[]) { cleanStatePrecondition(this); if (inCheckedComputation()) { - let [context, result] = withContext( - { - ...mainContext, - // important to run this with a fresh party everytime, otherwise compile messes up our circuits - // because it runs this multiple times - self: selfParty(this.address), - }, + // important to run this with a fresh party everytime, otherwise compile messes up our circuits + // because it runs this multiple times + let [context, result] = partyContext.runWith( + { self: selfParty(this.address) }, () => { // inside prover / compile, the method is always called with the public input as first argument // -- so we can add assertions about it @@ -138,8 +134,7 @@ function wrapMethod( return result; } ); - if (mainContext !== undefined) mainContext.self = context.self; - return result; + return [context, result]; } else if (Mina.currentTransaction === undefined) { // outside a transaction, just call the method, but check precondition invariants let result = method.apply(this, actualArgs); @@ -285,12 +280,11 @@ class SmartContract { private executionState(): ExecutionState { // TODO reconcile mainContext with currentTransaction - if (mainContext !== undefined) { - if (mainContext.self === undefined) throw Error('bug'); + if (partyContext.has()) { return { transactionId: 0, partyIndex: 0, - party: mainContext.self, + party: partyContext.get().self, }; } if (Mina.currentTransaction === undefined) { @@ -394,17 +388,25 @@ class SmartContract { !inAnalyze() ) { for (let methodIntf of methodIntfs) { - let { context, rows, digest } = analyzeMethod( + if (snarkContext.get().inRunAndCheck) { + let err = new Error( + 'Can not analyze methods inside Circuit.runAndCheck, because this creates a circuit nested in another circuit' + ); + // EXCEPT if the code that calls this knows that it can first run `analyzeMethods` OUTSIDE runAndCheck and try again + (err as any).bootstrap = () => ZkappClass.analyzeMethods(address); + throw err; + } + let { + rows, + digest, + result: [context], + } = analyzeMethod<[PartyContext, any]>( ZkappPublicInput, methodIntf, (...args) => (instance as any)[methodIntf.methodName](...args) ); - console.log( - methodIntf.methodName, - context.self!.body.sequenceEvents.data.length - ); ZkappClass._methodMetadata[methodIntf.methodName] = { - sequenceEvents: context.self!.body.sequenceEvents.data.length, + sequenceEvents: context.self.body.sequenceEvents.data.length, rows, digest, }; diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 2bd6b25e0..1d66e583e 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -527,7 +527,11 @@ declare class Circuit { static runAndCheck(f: () => T): T; - static constraintSystem(f: () => void): { rows: number; digest: string }; + static constraintSystem(f: () => T): { + rows: number; + digest: string; + result: T; + }; static assertEqual(ctor: { toFields(x: T): Field[] }, x: T, y: T): void; From 94b06a2bb9ef3d68bf4bd423af3304fac66d3c91 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Jul 2022 17:31:31 +0200 Subject: [PATCH 03/10] refactor currentTransaction to Context.t --- src/lib/global-context.ts | 33 ++++++++++++++------ src/lib/mina.ts | 65 +++++++++++++++++---------------------- src/lib/party.ts | 16 +++++----- src/lib/zkapp.ts | 15 ++++----- 4 files changed, 69 insertions(+), 60 deletions(-) diff --git a/src/lib/global-context.ts b/src/lib/global-context.ts index 623366365..789769c50 100644 --- a/src/lib/global-context.ts +++ b/src/lib/global-context.ts @@ -5,7 +5,7 @@ export { Context }; namespace Context { export type id = number; - export type t = { + export type t = (() => Context | undefined) & { data: { context: Context; id: id }[]; allowsNesting: boolean; @@ -16,6 +16,9 @@ namespace Context { context: Context, func: () => Promise ): Promise<[Context, Result]>; + enter(context: Context): id; + leave(id: id): Context; + id: () => id; }; } const Context = { create }; @@ -26,14 +29,26 @@ function create( default: undefined, } as { allowsNesting?: boolean; default?: C } ): Context.t { - let t: Context.t = { - data: [], - allowsNesting: options.allowsNesting ?? true, - get: () => get(t), - has: () => t.data.length !== 0, - runWith: (context, func) => runWith(t, context, func), - runWithAsync: (context, func) => runWithAsync(t, context, func), - }; + let t: Context.t = Object.assign( + function (): C | undefined { + return t.data[t.data.length - 1].context; + }, + { + data: [], + allowsNesting: options.allowsNesting ?? true, + get: () => get(t), + has: () => t.data.length !== 0, + runWith: (context: C, func: () => R) => runWith(t, context, func), + runWithAsync: (context: C, func: () => Promise) => + runWithAsync(t, context, func), + enter: (context: C) => enter(t, context), + leave: (id: Context.id) => leave(t, id), + id: () => { + if (t.data.length === 0) throw Error(contextConflictMessage); + return t.data[t.data.length - 1].id; + }, + } + ); if (options.default !== undefined) enter(t, options.default); return t; } diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 2a5c06b2d..fb82f75bb 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -17,15 +17,15 @@ import * as Fetch from './fetch'; import { assertPreconditionInvariants, NetworkValue } from './precondition'; import { cloneCircuitValue } from './circuit_value'; import { Proof, snarkContext } from './proof_system'; +import { Context } from './global-context'; export { createUnsignedTransaction, createTransaction, BerkeleyQANet, LocalBlockchain, - nextTransactionId, + currentTransaction, CurrentTransaction, - setCurrentTransaction, setActiveInstance, transaction, currentSlot, @@ -51,23 +51,18 @@ interface Transaction { type Account = Fetch.Account; -let nextTransactionId: { value: number } = { value: 0 }; - type FetchMode = 'fetch' | 'cached' | 'test'; -type CurrentTransaction = - | undefined - | { - sender?: PrivateKey; - parties: Party[]; - nextPartyIndex: number; - fetchMode: FetchMode; - isFinalRunOutsideCircuit: boolean; - }; - -export let currentTransaction: CurrentTransaction = undefined; -function setCurrentTransaction(transaction: CurrentTransaction) { - currentTransaction = transaction; -} +type CurrentTransaction = { + sender?: PrivateKey; + parties: Party[]; + nextPartyIndex: number; + fetchMode: FetchMode; + isFinalRunOutsideCircuit: boolean; +}; + +let currentTransaction = Context.create({ + allowsNesting: false, +}); type SenderSpec = | PrivateKey @@ -86,7 +81,7 @@ function createTransaction( f: () => unknown, { fetchMode = 'cached' as FetchMode, isFinalRunOutsideCircuit = true } = {} ): Transaction { - if (currentTransaction !== undefined) { + if (currentTransaction.has()) { throw new Error('Cannot start new transaction within another transaction'); } let feePayerKey = @@ -94,13 +89,13 @@ function createTransaction( let fee = feePayer instanceof PrivateKey ? undefined : feePayer?.fee; let memo = feePayer instanceof PrivateKey ? '' : feePayer?.memo ?? ''; - currentTransaction = { + let transactionId = currentTransaction.enter({ sender: feePayerKey, parties: [], nextPartyIndex: 0, fetchMode, isFinalRunOutsideCircuit, - }; + }); try { // run circuit @@ -120,12 +115,11 @@ function createTransaction( } // check that on-chain values weren't used without setting a precondition - for (let party of currentTransaction.parties) { + for (let party of currentTransaction.get().parties) { assertPreconditionInvariants(party); } } catch (err) { - nextTransactionId.value += 1; - currentTransaction = undefined; + currentTransaction.leave(transactionId); // TODO would be nice if the error would be a bit more descriptive about what failed throw err; } @@ -150,13 +144,12 @@ function createTransaction( } let transaction: Parties = { - otherParties: currentTransaction.parties, + otherParties: currentTransaction.get().parties, feePayer: feePayerParty, memo, }; - nextTransactionId.value += 1; - currentTransaction = undefined; + currentTransaction.leave(transactionId); let self: Transaction = { transaction, @@ -304,14 +297,14 @@ function RemoteBlockchain(graphqlEndpoint: string): Mina { ); }, getAccount(publicKey: PublicKey) { - if (currentTransaction?.fetchMode === 'test') { + if (currentTransaction()?.fetchMode === 'test') { Fetch.markAccountToBeFetched(publicKey, graphqlEndpoint); let account = Fetch.getCachedAccount(publicKey, graphqlEndpoint); return account ?? dummyAccount(publicKey); } if ( - currentTransaction == undefined || - currentTransaction.fetchMode === 'cached' + !currentTransaction.has() || + currentTransaction.get().fetchMode === 'cached' ) { let account = Fetch.getCachedAccount(publicKey, graphqlEndpoint); if (account !== undefined) return account; @@ -321,14 +314,14 @@ function RemoteBlockchain(graphqlEndpoint: string): Mina { ); }, getNetworkState() { - if (currentTransaction?.fetchMode === 'test') { + if (currentTransaction()?.fetchMode === 'test') { Fetch.markNetworkToBeFetched(graphqlEndpoint); let network = Fetch.getCachedNetwork(graphqlEndpoint); return network ?? dummyNetworkState(); } if ( - currentTransaction == undefined || - currentTransaction.fetchMode === 'cached' + !currentTransaction.has() || + currentTransaction.get().fetchMode === 'cached' ) { let network = Fetch.getCachedNetwork(graphqlEndpoint); if (network !== undefined) return network; @@ -390,13 +383,13 @@ let activeInstance: Mina = { throw new Error('must call Mina.setActiveInstance first'); }, getAccount: (publicKey: PublicKey) => { - if (currentTransaction?.fetchMode === 'test') { + if (currentTransaction()?.fetchMode === 'test') { Fetch.markAccountToBeFetched(publicKey, Fetch.defaultGraphqlEndpoint); return dummyAccount(publicKey); } if ( - currentTransaction === undefined || - currentTransaction?.fetchMode === 'cached' + !currentTransaction.has() || + currentTransaction.get().fetchMode === 'cached' ) { let account = Fetch.getCachedAccount( publicKey, diff --git a/src/lib/party.ts b/src/lib/party.ts index 6d0059352..972606176 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -678,14 +678,14 @@ class Party { static createUnsigned(publicKey: PublicKey) { const body: Body = Body.keepAll(publicKey); - if (Mina.currentTransaction === undefined) { + if (!Mina.currentTransaction.has()) { throw new Error( 'Party.createUnsigned: Cannot run outside of a transaction' ); } const party = new Party(body); - Mina.currentTransaction.nextPartyIndex++; - Mina.currentTransaction.parties.push(party); + Mina.currentTransaction.get().nextPartyIndex++; + Mina.currentTransaction.get().parties.push(party); return party; } @@ -700,7 +700,7 @@ class Party { let isFeePayer = isSameAsFeePayer !== undefined ? Bool(isSameAsFeePayer) - : Mina.currentTransaction?.sender?.equals(signer) ?? Bool(false); + : Mina.currentTransaction()?.sender?.equals(signer) ?? Bool(false); // TODO: This should be a witness block that uses the setVariable // API to set the value of a variable after it's allocated @@ -714,7 +714,7 @@ class Party { nonce = account.nonce; } - if (Mina.currentTransaction === undefined) { + if (!Mina.currentTransaction.has()) { throw new Error( 'Party.createSigned: Cannot run outside of a transaction' ); @@ -727,7 +727,7 @@ class Party { UInt32.zero ); // now, we check how often this party already updated its nonce in this tx, and increase nonce from `getAccount` by that amount - for (let party of Mina.currentTransaction.parties) { + for (let party of Mina.currentTransaction.get().parties) { let shouldIncreaseNonce = party.publicKey .equals(publicKey) .and(party.body.incrementNonce); @@ -739,8 +739,8 @@ class Party { let party = new Party(body); party.authorization = { kind: 'lazy-signature', privateKey: signer }; - Mina.currentTransaction.nextPartyIndex++; - Mina.currentTransaction.parties.push(party); + Mina.currentTransaction.get().nextPartyIndex++; + Mina.currentTransaction.get().parties.push(party); return party; } diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index f8a2aa3ae..8118ef6db 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -135,7 +135,7 @@ function wrapMethod( } ); return [context, result]; - } else if (Mina.currentTransaction === undefined) { + } else if (!Mina.currentTransaction.has()) { // outside a transaction, just call the method, but check precondition invariants let result = method.apply(this, actualArgs); // check the self party right after calling the method @@ -287,7 +287,7 @@ class SmartContract { party: partyContext.get().self, }; } - if (Mina.currentTransaction === undefined) { + if (!Mina.currentTransaction.has()) { // throw new Error('Cannot execute outside of a Mina.transaction() block.'); // TODO: it's inefficient to return a fresh party everytime, would be better to return a constant "non-writable" party, // or even expose the .get() methods independently of any party (they don't need one) @@ -300,14 +300,15 @@ class SmartContract { let executionState = this._executionState; if ( executionState !== undefined && - executionState.transactionId === Mina.nextTransactionId.value + executionState.transactionId === Mina.currentTransaction.id() ) { return executionState; } - let id = Mina.nextTransactionId.value; - let index = Mina.currentTransaction.nextPartyIndex++; + let transaction = Mina.currentTransaction.get(); + let id = Mina.currentTransaction.id(); + let index = transaction.nextPartyIndex++; let party = selfParty(this.address); - Mina.currentTransaction.parties.push(party); + transaction.parties.push(party); executionState = { transactionId: id, partyIndex: index, @@ -371,7 +372,7 @@ class SmartContract { } static runOutsideCircuit(run: () => void) { - if (Mina.currentTransaction?.isFinalRunOutsideCircuit || inProver()) + if (Mina.currentTransaction()?.isFinalRunOutsideCircuit || inProver()) Circuit.asProver(run); } From f2ef62114b432f6366a745ff8d72f8b04edd5e94 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Jul 2022 17:47:26 +0200 Subject: [PATCH 04/10] clean up party.ts exports --- src/index.ts | 4 +--- src/lib/party.test.ts | 6 ++---- src/lib/party.ts | 11 ++++++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7e0313929..f65c3c821 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,14 +37,12 @@ export { } from './lib/zkapp'; export { state, State, declareState } from './lib/state'; export { Proof, SelfProof, ZkProgram, verify } from './lib/proof_system'; -export * from './lib/party'; +export { Party, Permissions, ZkappPublicInput } from './lib/party'; export { fetchAccount, fetchLastBlock, - parseFetchedAccount, addCachedAccount, setGraphqlEndpoint, - sendZkappQuery, sendZkapp, } from './lib/fetch'; export * as Encryption from './lib/encryption'; diff --git a/src/lib/party.test.ts b/src/lib/party.test.ts index 6fa849d44..23da903ef 100644 --- a/src/lib/party.test.ts +++ b/src/lib/party.test.ts @@ -4,8 +4,6 @@ import { Circuit, Party, PrivateKey, - toPartyUnsafe, - Types, shutdown, Field, PublicKey, @@ -25,10 +23,10 @@ describe('party', () => { it('can convert party to fields consistently', () => { // convert party to fields in OCaml, going via Party.of_json - let json = JSON.stringify(Types.Party.toJson(toPartyUnsafe(party)).body); + let json = JSON.stringify(party.toJSON().body); let fields1 = Ledger.fieldsOfJson(json); // convert party to fields in pure JS, leveraging generated code - let fields2 = Types.Party.toFields(toPartyUnsafe(party)); + let fields2 = party.toFields(); // this is useful console output in the case the test should fail if (fields1.length !== fields2.length) { diff --git a/src/lib/party.ts b/src/lib/party.ts index 972606176..3c5fba7db 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -9,13 +9,15 @@ import * as Precondition from './precondition'; import { Proof, snarkContext } from './proof_system'; import { emptyHashWithPrefix, hashWithPrefix, prefixes } from './hash'; +// external API +export { Permissions, Party, ZkappPublicInput }; + +// internal API export { SetOrKeep, Permission, - Permissions, Preconditions, Body, - Party, FeePayerUnsigned, Parties, LazyProof, @@ -28,7 +30,6 @@ export { addMissingProofs, signJsonTransaction, ZkappStateLength, - ZkappPublicInput, Events, partyToPublicInput, partyContext, @@ -652,6 +653,10 @@ class Party { return Types.Party.toFields(toPartyUnsafe(this)); } + toJSON() { + return Types.Party.toJson(toPartyUnsafe(this)); + } + hash() { let fields = Types.Party.toFields(toPartyUnsafe(this)); return Ledger.hashPartyFromFields(fields); From 19703536851269314c5bec0f09222ae14930fd37 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 11 Jul 2022 15:30:29 +0200 Subject: [PATCH 05/10] do proofs in simple_zkapps.ts --- src/examples/simple_zkapp.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/examples/simple_zkapp.ts b/src/examples/simple_zkapp.ts index eddcef92a..2609e0804 100644 --- a/src/examples/simple_zkapp.ts +++ b/src/examples/simple_zkapp.ts @@ -71,6 +71,8 @@ class SimpleZkapp extends SmartContract { } } +const doProofs = true; + let Local = Mina.LocalBlockchain(); Mina.setActiveInstance(Local); @@ -89,6 +91,11 @@ let initialBalance = 10_000_000_000; let initialState = Field(1); let zkapp = new SimpleZkapp(zkappAddress); +if (doProofs) { + console.log('compile'); + await SimpleZkapp.compile(zkappAddress); +} + console.log('deploy'); let tx = await Mina.transaction(feePayer, () => { Party.fundNewAccount(feePayer, { initialBalance }); @@ -102,26 +109,24 @@ console.log(`initial balance: ${zkapp.account.balance.get().div(1e9)} MINA`); console.log('update'); tx = await Mina.transaction(feePayer, () => { zkapp.update(Field(3)); - zkapp.sign(zkappKey); + if (!doProofs) zkapp.sign(zkappKey); }); tx.send(); console.log('payout'); tx = await Mina.transaction(feePayer, () => { zkapp.payout(privilegedKey); - zkapp.sign(zkappKey); + if (!doProofs) zkapp.sign(zkappKey); }); tx.send(); -console.log(tx.toJSON()); - console.log('final state: ' + zkapp.x.get()); console.log(`final balance: ${zkapp.account.balance.get().div(1e9)} MINA`); console.log('try to payout a second time..'); tx = await Mina.transaction(feePayer, () => { zkapp.payout(privilegedKey); - zkapp.sign(zkappKey); + if (!doProofs) zkapp.sign(zkappKey); }); try { tx.send(); @@ -133,7 +138,7 @@ console.log('try to payout to a different account..'); try { tx = await Mina.transaction(feePayer, () => { zkapp.payout(Local.testAccounts[2].privateKey); - zkapp.sign(zkappKey); + if (!doProofs) zkapp.sign(zkappKey); }); tx.send(); } catch (err: any) { From 499751bf46af9f4c503192d47728b17d626e4efb Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 14 Jul 2022 09:18:08 +0200 Subject: [PATCH 06/10] merge partyContext and currentTransaction --- src/examples/simple_zkapp.ts | 11 ++++++++++- src/lib/circuit_value.ts | 4 ++++ src/lib/party.ts | 21 +++++++++------------ src/lib/zkapp.ts | 31 ++++++++++++------------------- 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/examples/simple_zkapp.ts b/src/examples/simple_zkapp.ts index 2609e0804..08fbb2bd4 100644 --- a/src/examples/simple_zkapp.ts +++ b/src/examples/simple_zkapp.ts @@ -14,6 +14,7 @@ import { UInt32, Bool, PublicKey, + Circuit, } from 'snarkyjs'; await isReady; @@ -32,6 +33,7 @@ class SimpleZkapp extends SmartContract { this.setPermissions({ ...Permissions.default(), editState: Permissions.proofOrSignature(), + send: Permissions.proofOrSignature(), }); this.balance.addInPlace(UInt64.fromNumber(initialBalance)); this.x.set(initialState); @@ -61,7 +63,10 @@ class SimpleZkapp extends SmartContract { // pay out half of the zkapp balance to the caller let balance = this.account.balance.get(); this.account.balance.assertEquals(balance); - let halfBalance = balance.div(2); + // FIXME UInt64.div() doesn't work on variables + let halfBalance = Circuit.witness(UInt64, () => + balance.toConstant().div(2) + ); this.balance.subInPlace(halfBalance); callerParty.balance.addInPlace(halfBalance); @@ -111,6 +116,7 @@ tx = await Mina.transaction(feePayer, () => { zkapp.update(Field(3)); if (!doProofs) zkapp.sign(zkappKey); }); +if (doProofs) await tx.prove(); tx.send(); console.log('payout'); @@ -118,6 +124,7 @@ tx = await Mina.transaction(feePayer, () => { zkapp.payout(privilegedKey); if (!doProofs) zkapp.sign(zkappKey); }); +if (doProofs) await tx.prove(); tx.send(); console.log('final state: ' + zkapp.x.get()); @@ -129,6 +136,7 @@ tx = await Mina.transaction(feePayer, () => { if (!doProofs) zkapp.sign(zkappKey); }); try { + if (doProofs) await tx.prove(); tx.send(); } catch (err: any) { console.log('Transaction failed with error', err.message); @@ -140,6 +148,7 @@ try { zkapp.payout(Local.testAccounts[2].privateKey); if (!doProofs) zkapp.sign(zkappKey); }); + if (doProofs) await tx.prove(); tx.send(); } catch (err: any) { console.log('Transaction failed with error', err.message); diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index 94eb16ade..6d53324e2 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -69,6 +69,10 @@ abstract class CircuitValue { return (this.constructor as any).toJSON(this); } + toConstant(): this { + return (this.constructor as any).toConstant(this); + } + equals(x: this) { return Circuit.equal(this, x); } diff --git a/src/lib/party.ts b/src/lib/party.ts index 3c5fba7db..6a7b9cc45 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -32,17 +32,10 @@ export { ZkappStateLength, Events, partyToPublicInput, - partyContext, - PartyContext, }; const ZkappStateLength = 8; -// global self-party context -// TODO: merge with Mina.currentTransaction -type PartyContext = { self: Party }; -let partyContext = Context.create(); - type PartyBody = Types.Party['body']; type Update = PartyBody['update']; @@ -699,14 +692,17 @@ class Party { options?: { isSameAsFeePayer?: Bool | boolean; nonce?: UInt32 } ) { let { nonce, isSameAsFeePayer } = options ?? {}; - // if not specified, optimistically determine isSameAsFeePayer from the current transaction - // (gotcha: this makes the circuit depend on the fee payer parameter in the transaction. - // to avoid that, provide the argument explicitly) + // if not specified, determine isSameAsFeePayer from the current transaction + // (gotcha: this doesn't constrain `isSameAsFeePayer`, to avoid making the circuit depend on something that can't be + // inferred at compile time. to constrain it, provide the argument explicitly) let isFeePayer = isSameAsFeePayer !== undefined ? Bool(isSameAsFeePayer) - : Mina.currentTransaction()?.sender?.equals(signer) ?? Bool(false); - + : Circuit.witness( + Bool, + () => + Mina.currentTransaction()?.sender?.equals(signer) ?? Bool(false) + ); // TODO: This should be a witness block that uses the setVariable // API to set the value of a variable after it's allocated @@ -955,6 +951,7 @@ async function addMissingProofs(parties: Parties): Promise<{ proof: new ZkappProof({ publicInput, proof, maxProofsVerified }), }; } + let { feePayer, otherParties, memo } = parties; // compute proofs serially. in parallel would clash with our global variable hacks let otherPartiesProved: (Party & { diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 8118ef6db..24195121a 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -19,8 +19,6 @@ import { ZkappPublicInput, Events, partyToPublicInput, - partyContext, - PartyContext, } from './party'; import { PrivateKey, PublicKey } from './signature'; import * as Mina from './mina'; @@ -114,8 +112,14 @@ function wrapMethod( if (inCheckedComputation()) { // important to run this with a fresh party everytime, otherwise compile messes up our circuits // because it runs this multiple times - let [context, result] = partyContext.runWith( - { self: selfParty(this.address) }, + let [, result] = Mina.currentTransaction.runWith( + { + sender: undefined, + parties: [], + nextPartyIndex: 0, + fetchMode: inProver() ? 'cached' : 'test', + isFinalRunOutsideCircuit: false, + }, () => { // inside prover / compile, the method is always called with the public input as first argument // -- so we can add assertions about it @@ -134,7 +138,7 @@ function wrapMethod( return result; } ); - return [context, result]; + return result; } else if (!Mina.currentTransaction.has()) { // outside a transaction, just call the method, but check precondition invariants let result = method.apply(this, actualArgs); @@ -279,14 +283,6 @@ class SmartContract { } private executionState(): ExecutionState { - // TODO reconcile mainContext with currentTransaction - if (partyContext.has()) { - return { - transactionId: 0, - partyIndex: 0, - party: partyContext.get().self, - }; - } if (!Mina.currentTransaction.has()) { // throw new Error('Cannot execute outside of a Mina.transaction() block.'); // TODO: it's inefficient to return a fresh party everytime, would be better to return a constant "non-writable" party, @@ -397,17 +393,14 @@ class SmartContract { (err as any).bootstrap = () => ZkappClass.analyzeMethods(address); throw err; } - let { - rows, - digest, - result: [context], - } = analyzeMethod<[PartyContext, any]>( + let { rows, digest } = analyzeMethod( ZkappPublicInput, methodIntf, (...args) => (instance as any)[methodIntf.methodName](...args) ); + let party = instance._executionState?.party!; ZkappClass._methodMetadata[methodIntf.methodName] = { - sequenceEvents: context.self.body.sequenceEvents.data.length, + sequenceEvents: party.body.sequenceEvents.data.length, rows, digest, }; From 545da9713ff75b941d91e095cae584fcb80956be Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 14 Jul 2022 14:55:13 +0200 Subject: [PATCH 07/10] fix: allow nesting transaction context --- src/lib/mina.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/mina.ts b/src/lib/mina.ts index fb82f75bb..130384ebb 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -60,9 +60,7 @@ type CurrentTransaction = { isFinalRunOutsideCircuit: boolean; }; -let currentTransaction = Context.create({ - allowsNesting: false, -}); +let currentTransaction = Context.create(); type SenderSpec = | PrivateKey From c0b5757ca771a1b544766bc19d1334bf4119758a Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 18 Jul 2022 10:33:12 +0200 Subject: [PATCH 08/10] some clean up --- src/lib/proof_system.ts | 20 +++++--------------- src/lib/zkapp.ts | 16 ++++++++-------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/lib/proof_system.ts b/src/lib/proof_system.ts index 1476177fe..5393a605d 100644 --- a/src/lib/proof_system.ts +++ b/src/lib/proof_system.ts @@ -379,21 +379,11 @@ function analyzeMethod( methodIntf: MethodInterface, method: (...args: any) => T ) { - let [, { rows, digest, result }] = snarkContext.runWith( - { inAnalyze: true, inCheckedComputation: true }, - () => { - let result: T; - let { rows, digest }: ReturnType = ( - Circuit as any - )._constraintSystem(() => { - let args = synthesizeMethodArguments(methodIntf, true); - let publicInput = emptyWitness(publicInputType); - result = method(publicInput, ...args); - }); - return { rows, digest, result: result! }; - } - ); - return { rows, digest, result: result as T }; + return Circuit.constraintSystem(() => { + let args = synthesizeMethodArguments(methodIntf, true); + let publicInput = emptyWitness(publicInputType); + return method(publicInput, ...args); + }); } function picklesRuleFromFunction( diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 7d42736cc..948eb8802 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -384,15 +384,15 @@ class SmartContract { !methodIntfs.every((m) => m.methodName in ZkappClass._methodMetadata) && !inAnalyze() ) { + if (snarkContext.get().inRunAndCheck) { + let err = new Error( + 'Can not analyze methods inside Circuit.runAndCheck, because this creates a circuit nested in another circuit' + ); + // EXCEPT if the code that calls this knows that it can first run `analyzeMethods` OUTSIDE runAndCheck and try again + (err as any).bootstrap = () => ZkappClass.analyzeMethods(address); + throw err; + } for (let methodIntf of methodIntfs) { - if (snarkContext.get().inRunAndCheck) { - let err = new Error( - 'Can not analyze methods inside Circuit.runAndCheck, because this creates a circuit nested in another circuit' - ); - // EXCEPT if the code that calls this knows that it can first run `analyzeMethods` OUTSIDE runAndCheck and try again - (err as any).bootstrap = () => ZkappClass.analyzeMethods(address); - throw err; - } let { rows, digest } = analyzeMethod( ZkappPublicInput, methodIntf, From 41d61cc04072c872b81ef06584fc97362da1b969 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 20 Jul 2022 09:22:54 +0200 Subject: [PATCH 09/10] remove createSigned options, compute nonce out-of-snark --- src/lib/party.ts | 65 ++++++++++++++---------------------------------- src/lib/zkapp.ts | 12 +-------- 2 files changed, 20 insertions(+), 57 deletions(-) diff --git a/src/lib/party.ts b/src/lib/party.ts index 11c93a718..5d8594e28 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -687,54 +687,30 @@ class Party { return party; } - static createSigned( - signer: PrivateKey, - options?: { isSameAsFeePayer?: Bool | boolean; nonce?: UInt32 } - ) { - let { nonce, isSameAsFeePayer } = options ?? {}; - // if not specified, determine isSameAsFeePayer from the current transaction - // (gotcha: this doesn't constrain `isSameAsFeePayer`, to avoid making the circuit depend on something that can't be - // inferred at compile time. to constrain it, provide the argument explicitly) - let isFeePayer = - isSameAsFeePayer !== undefined - ? Bool(isSameAsFeePayer) - : Circuit.witness( - Bool, - () => - Mina.currentTransaction()?.sender?.equals(signer) ?? Bool(false) - ); - // TODO: This should be a witness block that uses the setVariable - // API to set the value of a variable after it's allocated - + static createSigned(signer: PrivateKey) { let publicKey = signer.toPublicKey(); let body = Body.keepAll(publicKey); - - // TODO: getAccount could always be used if we had a generic way to add account info prior to creating transactions - if (nonce === undefined) { - let account = Mina.getAccount(publicKey); - nonce = account.nonce; - } - if (!Mina.currentTransaction.has()) { throw new Error( 'Party.createSigned: Cannot run outside of a transaction' ); } - - // if the fee payer is the same party as this one, we have to start the nonce predicate at one higher bc the fee payer already increases its nonce - let nonceIncrement = Circuit.if( - isFeePayer, - new UInt32(Field.one), - UInt32.zero - ); - // now, we check how often this party already updated its nonce in this tx, and increase nonce from `getAccount` by that amount - for (let party of Mina.currentTransaction.get().parties) { - let shouldIncreaseNonce = party.publicKey - .equals(publicKey) - .and(party.body.incrementNonce); - nonceIncrement.add(new UInt32(shouldIncreaseNonce.toField())); - } - nonce = nonce.add(nonceIncrement); + // it's fine to compute the nonce outside the circuit, because we're constraining it with a precondition + let nonce = Circuit.witness(UInt32, () => { + let nonce = Number(Mina.getAccount(publicKey).nonce.toString()); + // if the fee payer is the same party as this one, we have to start the nonce predicate at one higher, + // bc the fee payer already increases its nonce + let isFeePayer = Mina.currentTransaction()?.sender?.equals(signer); + if (isFeePayer?.toBoolean()) nonce++; + // now, we check how often this party already updated its nonce in this tx, and increase nonce from `getAccount` by that amount + for (let party of Mina.currentTransaction.get().parties) { + let shouldIncreaseNonce = party.publicKey + .equals(publicKey) + .and(party.body.incrementNonce); + if (shouldIncreaseNonce.toBoolean()) nonce++; + } + return UInt32.from(nonce); + }); Party.assertEquals(body.preconditions.account.nonce, nonce); body.incrementNonce = Bool(true); @@ -758,12 +734,9 @@ class Party { */ static fundNewAccount( feePayerKey: PrivateKey, - { - initialBalance = UInt64.zero as number | string | UInt64, - isSameAsFeePayer = undefined as Bool | boolean | undefined, - } = {} + { initialBalance = UInt64.zero as number | string | UInt64 } = {} ) { - let party = Party.createSigned(feePayerKey, { isSameAsFeePayer }); + let party = Party.createSigned(feePayerKey); let amount = initialBalance instanceof UInt64 ? initialBalance diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 948eb8802..8ab75f5d1 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -560,7 +560,6 @@ async function deploy( shouldSignFeePayer, feePayerKey, transactionFee, - feePayerNonce, memo, }: { zkappKey: PrivateKey; @@ -569,7 +568,6 @@ async function deploy( feePayerKey?: PrivateKey; shouldSignFeePayer?: boolean; transactionFee?: string | number; - feePayerNonce?: string | number; memo?: string; } ) { @@ -584,15 +582,7 @@ async function deploy( let amount = UInt64.fromString(String(initialBalance)).add( Mina.accountCreationFee() ); - let nonce = - feePayerNonce !== undefined - ? UInt32.fromString(String(feePayerNonce)) - : undefined; - - let party = Party.createSigned(feePayerKey, { - isSameAsFeePayer: true, - nonce, - }); + let party = Party.createSigned(feePayerKey); party.balance.subInPlace(amount); } // main party: the zkapp account From c25233ebc9626f163e9fc6fabfb64055caf681bb Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 20 Jul 2022 09:23:22 +0200 Subject: [PATCH 10/10] better comment about retrying analyzeMethods --- src/lib/mina.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 130384ebb..f9db6b4cb 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -95,11 +95,15 @@ function createTransaction( isFinalRunOutsideCircuit, }); + // run circuit + // we have this while(true) loop because one of the smart contracts we're calling inside `f` might be calling + // SmartContract.analyzeMethods, which would be running its methods again inside `Circuit.constraintSystem`, which + // would throw an error when nested inside `Circuit.runAndCheck`. So if that happens, we have to run `analyzeMethods` first + // and retry `Circuit.runAndCheck(f)`. Since at this point in the function, we don't know which smart contracts are involved, + // we created that hack with a `bootstrap()` function that analyzeMethods sticks on the error, to call itself again. try { - // run circuit let err: any; while (true) { - // this is such a ridiculous hack if (err !== undefined) err.bootstrap(); try { snarkContext.runWith({ inRunAndCheck: true }, () =>