From a20146bfcb077f01506bce037c0853c65d08c084 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 15 Jul 2022 23:19:02 +0200 Subject: [PATCH 01/19] add account.isNew --- src/lib/party.ts | 1 + src/lib/precondition.test.ts | 1 + src/lib/precondition.ts | 1 + src/snarky/gen/js-layout.ts | 18 ++++++++++++++++++ src/snarky/gen/parties-json.ts | 2 ++ src/snarky/gen/parties.ts | 2 ++ 6 files changed, 25 insertions(+) diff --git a/src/lib/party.ts b/src/lib/party.ts index 11c93a718..fb65ad0b9 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -460,6 +460,7 @@ const AccountPrecondition = { state: appState, sequenceState: Events.emptySequenceState(), provedState: ignore(Bool(false)), + isNew: ignore(Bool(false)), }; }, nonce(nonce: UInt32): AccountPrecondition { diff --git a/src/lib/precondition.test.ts b/src/lib/precondition.test.ts index 363044bb6..5ec10f3f7 100644 --- a/src/lib/precondition.test.ts +++ b/src/lib/precondition.test.ts @@ -195,6 +195,7 @@ let implementedWithRange = [ ]; let unimplemented = [ () => zkapp.account.provedState, + () => zkapp.account.isNew, () => zkapp.account.delegate, () => zkapp.account.receiptChainHash, () => zkapp.network.timestamp, diff --git a/src/lib/precondition.ts b/src/lib/precondition.ts index 3e912ed1c..858a98eb2 100644 --- a/src/lib/precondition.ts +++ b/src/lib/precondition.ts @@ -63,6 +63,7 @@ let unimplementedPreconditions: LongKey[] = [ 'network.stakingEpochData.lockCheckpoint', // this is unimplemented because the field is missing on the account endpoint 'account.provedState', + 'account.isNew', // this is partially unimplemented because the field is not returned by the local blockchain 'account.delegate', // this is unimplemented because setting this precondition made the integration test fail diff --git a/src/snarky/gen/js-layout.ts b/src/snarky/gen/js-layout.ts index a4b7d40ca..351bc0e1d 100644 --- a/src/snarky/gen/js-layout.ts +++ b/src/snarky/gen/js-layout.ts @@ -888,6 +888,15 @@ let jsLayout = { }, docs: null, }, + { + key: 'isNew', + value: { + type: 'option', + optionType: 'flaggedOption', + inner: { type: 'Bool' }, + }, + docs: null, + }, ], }, docs: null, @@ -1751,6 +1760,15 @@ let jsLayout = { }, docs: null, }, + { + key: 'isNew', + value: { + type: 'option', + optionType: 'flaggedOption', + inner: { type: 'Bool' }, + }, + docs: null, + }, ], }, docs: null, diff --git a/src/snarky/gen/parties-json.ts b/src/snarky/gen/parties-json.ts index 20663c193..7588cf9d0 100644 --- a/src/snarky/gen/parties-json.ts +++ b/src/snarky/gen/parties-json.ts @@ -142,6 +142,7 @@ type Parties = { state: (Field | null)[]; sequenceState: Field | null; provedState: Bool | null; + isNew: Bool | null; }; }; useFullCommitment: Bool; @@ -273,6 +274,7 @@ type Party = { state: (Field | null)[]; sequenceState: Field | null; provedState: Bool | null; + isNew: Bool | null; }; }; useFullCommitment: Bool; diff --git a/src/snarky/gen/parties.ts b/src/snarky/gen/parties.ts index 2987df772..4b1e953c1 100644 --- a/src/snarky/gen/parties.ts +++ b/src/snarky/gen/parties.ts @@ -201,6 +201,7 @@ type Parties = { state: { isSome: Bool; value: Field }[]; sequenceState: Field; provedState: { isSome: Bool; value: Bool }; + isNew: { isSome: Bool; value: Bool }; }; }; useFullCommitment: Bool; @@ -368,6 +369,7 @@ type Party = { state: { isSome: Bool; value: Field }[]; sequenceState: Field; provedState: { isSome: Bool; value: Bool }; + isNew: { isSome: Bool; value: Bool }; }; }; useFullCommitment: Bool; From c7a0e5657b30bc3895aed3369bf20bb01838d352 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 20 Jul 2022 11:28:57 +0200 Subject: [PATCH 02/19] WIP manual composability --- src/examples/zkapps/composability_manual.ts | 151 ++++++++++++++++++++ src/snarky/parties-leaves.ts | 5 +- 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/examples/zkapps/composability_manual.ts diff --git a/src/examples/zkapps/composability_manual.ts b/src/examples/zkapps/composability_manual.ts new file mode 100644 index 000000000..88e573cbe --- /dev/null +++ b/src/examples/zkapps/composability_manual.ts @@ -0,0 +1,151 @@ +/** + * how to do composability "by hand" + */ +import { + Circuit, + Experimental, + Field, + isReady, + method, + Mina, + Party, + Permissions, + Poseidon, + PrivateKey, + SmartContract, + state, + State, +} from 'snarkyjs'; + +const doProofs = true; + +await isReady; + +// contract which can add two numbers and return the result +class CallableAdd extends SmartContract { + @method add(x: Field, y: Field, blindingValue: Field) { + // compute result + let result = x.add(y); + + // store inputs + result in callData + // the blindingValue is necessary because otherwise, putting this on the transaction would leak information about the private inputs + this.self.body.callData = Poseidon.hash([blindingValue, x, y, result]); + return result; + } +} + +let callableKey = PrivateKey.random(); +let callableAddress = callableKey.toPublicKey(); +console.log('callable address', callableAddress.toBase58()); +let callableTokenId = Field.one; + +class Caller extends SmartContract { + @state(Field) sum = State(); + events = { sum: Field }; + + @method callAddAndEmit(x: Field, y: Field) { + let input: [Field, Field] = [x, y]; + + // we have to call this.self to create our party first! + let thisParty = this.self; + + // witness the result of calling `add` + let blindingValue = Circuit.witness(Field, () => Field.random()); + let { party, result } = Party.witness(Field, () => { + // here we will call the other method + // let adder = new CallableAdd(callableAddress); + // let result = adder.add(...input, blindingValue); + let party = Experimental.createChildParty(thisParty, callableAddress); + let [x0, y0] = [x.toConstant(), y.toConstant()]; + let result = x0.add(y0); + // store inputs + result in callData + // the blindingValue is necessary because otherwise, putting this on the transaction would leak information about the private inputs + party.body.callData = Poseidon.hash([blindingValue, x0, y0, result]); + + party.lazyAuthorization = { + kind: 'lazy-proof', + method: CallableAdd.prototype.add, + args: [x0, y0], + previousProofs: [], + ZkappClass: CallableAdd, + }; + + console.log('address base58', party.body.publicKey.toBase58()); + console.log('address x', party.body.publicKey.g.x + ''); + + return { party, result }; + }); + + // assert that we really called the right zkapp + party.body.publicKey.assertEquals(callableAddress); + party.body.tokenId.assertEquals(callableTokenId); + + // assert that the inputs & outputs we have match what the callee put on its callData + let callData = Poseidon.hash([blindingValue, ...input, result]); + party.body.callData.assertEquals(callData); + + // finally, we proved that we called that other zkapp, with the inputs `x`, `y` and the output `result` + // now we can do anything with the result + this.emitEvent('sum', result); + this.sum.set(result); + } +} + +function makeChildParty(parent: Party, child: Party) { + let otherParties = Mina.currentTransaction()?.parties; + if (otherParties?.includes(child)) { + let i = otherParties.indexOf(child); + otherParties.splice(i, 1); + } + child.body.callDepth = parent.body.callDepth + 1; + child.parent = parent; + parent.children.push(child); + return child; +} + +// script to deploy zkapps and do interactions + +let Local = Mina.LocalBlockchain(); +Mina.setActiveInstance(Local); + +// a test account that pays all the fees, and puts additional funds into the zkapp +let feePayer = Local.testAccounts[0].privateKey; + +// the zkapp account +let zkappKey = PrivateKey.random(); +let zkappAddress = zkappKey.toPublicKey(); + +let zkapp = new Caller(zkappAddress); +let callableZkapp = new CallableAdd(callableAddress); + +if (doProofs) { + console.log('compile'); + await CallableAdd.compile(callableAddress); + await Caller.compile(zkappAddress); +} + +console.log('deploy'); +let tx = await Mina.transaction(feePayer, () => { + Party.fundNewAccount(feePayer, { initialBalance: Mina.accountCreationFee() }); + zkapp.deploy({ zkappKey }); + zkapp.setPermissions({ + ...Permissions.default(), + editState: Permissions.proofOrSignature(), + }); + callableZkapp.deploy({ zkappKey: callableKey }); +}); +tx.send(); + +console.log('call interaction'); +tx = await Mina.transaction(feePayer, () => { + zkapp.callAddAndEmit(Field(5), Field(6)); + if (!doProofs) zkapp.sign(zkappKey); +}); +console.log('proving'); +if (doProofs) await tx.prove(); + +console.log(tx.toJSON()); + +tx.send(); + +console.log('state: ' + zkapp.sum.get()); diff --git a/src/snarky/parties-leaves.ts b/src/snarky/parties-leaves.ts index 8b4776c30..eb3c6216f 100644 --- a/src/snarky/parties-leaves.ts +++ b/src/snarky/parties-leaves.ts @@ -1,4 +1,4 @@ -import { Field, Bool, Group, Ledger } from '../snarky'; +import { Field, Bool, Group, Ledger, Circuit } from '../snarky'; import * as Json from './gen/parties-json'; import { UInt32, UInt64, Sign } from '../lib/int'; import { PublicKey } from '../lib/signature'; @@ -234,6 +234,9 @@ let FromFields: FromFields = { PublicKey(fields: Field[]) { let x = fields.pop()!; let isOdd = fields.pop()!; + Circuit.asProver(() => { + console.log('public key', x + ''); + }); // compute y from elliptic curve equation y^2 = x^3 + 5 // TODO: this is used in-snark, so we should improve constraint efficiency let someY = x.mul(x).mul(x).add(5).sqrt(); From 9fc8d3af38063a73174900797c4af3e6198d543f Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 20 Jul 2022 16:49:29 +0200 Subject: [PATCH 03/19] fix public key from fields, remove debug logs --- src/snarky/parties-leaves.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/snarky/parties-leaves.ts b/src/snarky/parties-leaves.ts index eb3c6216f..8d5357978 100644 --- a/src/snarky/parties-leaves.ts +++ b/src/snarky/parties-leaves.ts @@ -234,12 +234,16 @@ let FromFields: FromFields = { PublicKey(fields: Field[]) { let x = fields.pop()!; let isOdd = fields.pop()!; - Circuit.asProver(() => { - console.log('public key', x + ''); - }); // compute y from elliptic curve equation y^2 = x^3 + 5 // TODO: this is used in-snark, so we should improve constraint efficiency - let someY = x.mul(x).mul(x).add(5).sqrt(); + let ySquared = x.mul(x).mul(x).add(5); + let someY: Field; + if (ySquared.isConstant()) { + someY = ySquared.sqrt(); + } else { + someY = Circuit.witness(Field, () => ySquared.toConstant().sqrt()); + someY.square().equals(ySquared).or(x.equals(Field.zero)).assertTrue(); + } let isTheRightY = isOdd.equals(someY.toBits()[0].toField()); let y = isTheRightY .toField() From ec7ac4743930ca211351d5fe5a35cd82d2de47f7 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 20 Jul 2022 16:51:35 +0200 Subject: [PATCH 04/19] use method name for lazy proof, skip party.check --- src/lib/party.ts | 21 +++++++++++++-------- src/lib/zkapp.ts | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/lib/party.ts b/src/lib/party.ts index a1a816feb..271eff4de 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -489,7 +489,7 @@ type Control = Types.Party['authorization']; type LazySignature = { kind: 'lazy-signature'; privateKey?: PrivateKey }; type LazyProof = { kind: 'lazy-proof'; - method: Function; + methodName: string; args: any[]; previousProofs: { publicInput: Field[]; proof: Pickles.Proof }[]; ZkappClass: typeof SmartContract; @@ -768,7 +768,8 @@ class Party implements Types.Party { static witness( type: AsFieldElements, - compute: () => { party: Party; result: T } + compute: () => { party: Party; result: T }, + skipCheck = false ) { // construct the circuit type for a party + other result let partyType = circuitArray(Field, Types.Party.sizeInFields()); @@ -790,7 +791,10 @@ class Party implements Types.Party { // get back a Types.Party from the fields + aux (where aux is just the default in compile) let aux = Types.Party.toAuxiliary(proverParty); let rawParty = Types.Party.fromFields(fieldsAndResult.party, aux); - Types.Party.check(rawParty); + // usually when we introduce witnesses, we add checks for their type-specific properties (e.g., booleanness). + // a party, however, might already be forced to be valid by the on-chain transaction logic, + // allowing us to skip expensive checks in user proofs. + if (!skipCheck) Types.Party.check(rawParty); // construct the full Party instance from the raw party + (maybe) the prover party let party = new Party(rawParty.body, rawParty.authorization); @@ -980,20 +984,21 @@ async function addMissingProofs(parties: Parties): Promise<{ if (party.lazyAuthorization?.kind !== 'lazy-proof') { return { partyProved: party as PartyProved, proof: undefined }; } - let { method, args, previousProofs, ZkappClass } = party.lazyAuthorization; + let { methodName, args, previousProofs, ZkappClass } = + party.lazyAuthorization; let publicInput = partyToPublicInput(party); let publicInputFields = ZkappPublicInput.toFields(publicInput); if (ZkappClass._provers === undefined) throw Error( - `Cannot prove execution of ${method.name}(), no prover found. ` + + `Cannot prove execution of ${methodName}(), no prover found. ` + `Try calling \`await ${ZkappClass.name}.compile(address)\` first, this will cache provers in the background.` ); let provers = ZkappClass._provers; let methodError = - `Error when computing proofs: Method ${method.name} not found. ` + - `Make sure your environment supports decorators, and annotate with \`@method ${method.name}\`.`; + `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 === method.name); + let i = ZkappClass._methods.findIndex((m) => m.methodName === methodName); if (i === -1) throw Error(methodError); let [, proof] = await snarkContext.runWithAsync( { inProver: true, witnesses: args }, diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 100ec0943..7ef6f48ef 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -162,7 +162,7 @@ function wrapMethod( let party = this.self; if (!Authorization.hasAny(party)) { Authorization.setLazyProof(party, { - method, + methodName: methodIntf.methodName, args: clonedArgs, // proofs actually don't have to be cloned previousProofs: getPreviousProofsForProver(actualArgs, methodIntf), From 89a55d23a39d2fd122af1b7aac318dacebb209b9 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 20 Jul 2022 16:55:47 +0200 Subject: [PATCH 05/19] get manual composability example working --- src/examples/zkapps/composability_manual.ts | 73 ++++++++++----------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/src/examples/zkapps/composability_manual.ts b/src/examples/zkapps/composability_manual.ts index 88e573cbe..3d9ff3a8d 100644 --- a/src/examples/zkapps/composability_manual.ts +++ b/src/examples/zkapps/composability_manual.ts @@ -36,8 +36,10 @@ class CallableAdd extends SmartContract { let callableKey = PrivateKey.random(); let callableAddress = callableKey.toPublicKey(); -console.log('callable address', callableAddress.toBase58()); let callableTokenId = Field.one; +// TODO: we need a way to declare witnesses that should be the same when proving, +// just like we do with method arguments +let blindingValue0 = Field.random(); class Caller extends SmartContract { @state(Field) sum = State(); @@ -47,34 +49,39 @@ class Caller extends SmartContract { let input: [Field, Field] = [x, y]; // we have to call this.self to create our party first! - let thisParty = this.self; + let selfParty = this.self; // witness the result of calling `add` - let blindingValue = Circuit.witness(Field, () => Field.random()); - let { party, result } = Party.witness(Field, () => { - // here we will call the other method - // let adder = new CallableAdd(callableAddress); - // let result = adder.add(...input, blindingValue); - let party = Experimental.createChildParty(thisParty, callableAddress); - let [x0, y0] = [x.toConstant(), y.toConstant()]; - let result = x0.add(y0); - // store inputs + result in callData - // the blindingValue is necessary because otherwise, putting this on the transaction would leak information about the private inputs - party.body.callData = Poseidon.hash([blindingValue, x0, y0, result]); - - party.lazyAuthorization = { - kind: 'lazy-proof', - method: CallableAdd.prototype.add, - args: [x0, y0], - previousProofs: [], - ZkappClass: CallableAdd, - }; - - console.log('address base58', party.body.publicKey.toBase58()); - console.log('address x', party.body.publicKey.g.x + ''); - - return { party, result }; - }); + let blindingValue = Circuit.witness(Field, () => blindingValue0); + let { party, result } = Party.witness( + Field, + () => { + // here we will call the other method + // let adder = new CallableAdd(callableAddress); + // let result = adder.add(...input, blindingValue); + let party = Party.defaultParty(callableAddress); + let [x0, y0] = [x.toConstant(), y.toConstant()]; + let result = x0.add(y0); + // store inputs + result in callData + // the blindingValue is necessary because otherwise, putting this on the transaction would leak information about the private inputs + + party.body.callData = Poseidon.hash([blindingValue, x0, y0, result]); + + party.lazyAuthorization = { + kind: 'lazy-proof', + methodName: 'add', + args: [x0, y0, blindingValue], + previousProofs: [], + ZkappClass: CallableAdd, + }; + return { party, result }; + }, + true + ); + // connect party to our own. outside Circuit.witness so compile knows about it + party.body.callDepth = selfParty.body.callDepth + 1; + party.parent = selfParty; + selfParty.children.push(party); // assert that we really called the right zkapp party.body.publicKey.assertEquals(callableAddress); @@ -91,18 +98,6 @@ class Caller extends SmartContract { } } -function makeChildParty(parent: Party, child: Party) { - let otherParties = Mina.currentTransaction()?.parties; - if (otherParties?.includes(child)) { - let i = otherParties.indexOf(child); - otherParties.splice(i, 1); - } - child.body.callDepth = parent.body.callDepth + 1; - child.parent = parent; - parent.children.push(child); - return child; -} - // script to deploy zkapps and do interactions let Local = Mina.LocalBlockchain(); From c56d0c3f9fc75046c625d2832142339a39a7555f Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 21 Jul 2022 10:54:28 +0200 Subject: [PATCH 06/19] support memoizing witnesses in methods for prover --- src/examples/zkapps/composability_manual.ts | 10 +++---- src/index.ts | 3 +- src/lib/circuit_value.ts | 32 ++++++++++++++++++++- src/lib/party.ts | 17 ++++++++--- src/lib/zkapp.ts | 27 +++++++++++++---- 5 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/examples/zkapps/composability_manual.ts b/src/examples/zkapps/composability_manual.ts index 3d9ff3a8d..4d12702cd 100644 --- a/src/examples/zkapps/composability_manual.ts +++ b/src/examples/zkapps/composability_manual.ts @@ -37,9 +37,6 @@ class CallableAdd extends SmartContract { let callableKey = PrivateKey.random(); let callableAddress = callableKey.toPublicKey(); let callableTokenId = Field.one; -// TODO: we need a way to declare witnesses that should be the same when proving, -// just like we do with method arguments -let blindingValue0 = Field.random(); class Caller extends SmartContract { @state(Field) sum = State(); @@ -52,7 +49,9 @@ class Caller extends SmartContract { let selfParty = this.self; // witness the result of calling `add` - let blindingValue = Circuit.witness(Field, () => blindingValue0); + let blindingValue = Experimental.memoizeWitness(Field, () => + Field.random() + ); let { party, result } = Party.witness( Field, () => { @@ -73,6 +72,7 @@ class Caller extends SmartContract { args: [x0, y0, blindingValue], previousProofs: [], ZkappClass: CallableAdd, + memoized: [], }; return { party, result }; }, @@ -139,7 +139,7 @@ tx = await Mina.transaction(feePayer, () => { console.log('proving'); if (doProofs) await tx.prove(); -console.log(tx.toJSON()); +console.dir(JSON.parse(tx.toJSON()), { depth: 5 }); tx.send(); diff --git a/src/index.ts b/src/index.ts index 8129cb614..db19b65c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,10 +51,11 @@ export { Character, CircuitString } from './lib/string'; // experimental APIs import { Reducer } from './lib/zkapp'; import { createChildParty } from './lib/party'; +import { memoizeWitness } from './lib/circuit_value'; export { Experimental }; /** * This module exposes APIs that are unstable, in the sense that the API surface is expected to change. * (Not unstable in the sense that they are less functional or tested than other parts.) */ -const Experimental = { Reducer, createChildParty }; +const Experimental = { Reducer, createChildParty, memoizeWitness }; diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index 25dd15b1b..f6f2d458f 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -1,5 +1,6 @@ import 'reflect-metadata'; import { Circuit, Field, Bool, JSONValue, AsFieldElements } from '../snarky'; +import { Context } from './global-context'; import { snarkContext } from './proof_system'; // external API @@ -15,7 +16,13 @@ export { }; // internal API -export { cloneCircuitValue, circuitValueEquals, circuitArray }; +export { + cloneCircuitValue, + circuitValueEquals, + circuitArray, + memoizationContext, + memoizeWitness, +}; type AnyConstructor = new (...args: any) => any; @@ -533,3 +540,26 @@ Circuit.constraintSystem = function (f: () => T) { ); return result; }; + +let memoizationContext = + Context.create<{ memoized: Field[][]; currentIndex: number }>(); + +/** + * Like Circuit.witness, but memoizes the witness during transaction construction + * for reuse by the prover. This is needed to witness non-deterministic values. + */ +function memoizeWitness(type: AsFieldElements, compute: () => T) { + return Circuit.witness(type, () => { + if (!memoizationContext.has()) return compute(); + let context = memoizationContext.get(); + let { memoized, currentIndex } = context; + let currentValue = memoized[currentIndex]; + if (currentValue === undefined) { + let value = compute(); + currentValue = type.toFields(value).map((x) => x.toConstant()); + memoized[currentIndex] = currentValue; + } + context.currentIndex += 1; + return type.ofFields(currentValue); + }); +} diff --git a/src/lib/party.ts b/src/lib/party.ts index 271eff4de..4f963e119 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -1,4 +1,9 @@ -import { circuitArray, circuitValue, cloneCircuitValue } from './circuit_value'; +import { + circuitArray, + circuitValue, + cloneCircuitValue, + memoizationContext, +} from './circuit_value'; import { Field, Bool, @@ -493,6 +498,7 @@ type LazyProof = { args: any[]; previousProofs: { publicInput: Field[]; proof: Pickles.Proof }[]; ZkappClass: typeof SmartContract; + memoized: Field[][]; }; class Party implements Types.Party { @@ -984,7 +990,7 @@ async function addMissingProofs(parties: Parties): Promise<{ if (party.lazyAuthorization?.kind !== 'lazy-proof') { return { partyProved: party as PartyProved, proof: undefined }; } - let { methodName, args, previousProofs, ZkappClass } = + let { methodName, args, previousProofs, ZkappClass, memoized } = party.lazyAuthorization; let publicInput = partyToPublicInput(party); let publicInputFields = ZkappPublicInput.toFields(publicInput); @@ -1000,9 +1006,12 @@ async function addMissingProofs(parties: Parties): Promise<{ if (ZkappClass._methods === undefined) throw Error(methodError); let i = ZkappClass._methods.findIndex((m) => m.methodName === methodName); if (i === -1) throw Error(methodError); - let [, proof] = await snarkContext.runWithAsync( + let [, [, proof]] = await snarkContext.runWithAsync( { inProver: true, witnesses: args }, - () => provers[i](publicInputFields, previousProofs) + () => + memoizationContext.runWithAsync({ memoized, currentIndex: 0 }, () => + provers[i](publicInputFields, previousProofs) + ) ); Authorization.setProof(party, Pickles.proofToBase64Transaction(proof)); let maxProofsVerified = ZkappClass._maxProofsVerified!; diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 7ef6f48ef..5de0b591a 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -7,7 +7,12 @@ import { InferAsFieldElements, Poseidon, } from '../snarky'; -import { Circuit, circuitArray, cloneCircuitValue } from './circuit_value'; +import { + Circuit, + circuitArray, + cloneCircuitValue, + memoizationContext, +} from './circuit_value'; import { Body, Party, @@ -127,15 +132,16 @@ function wrapMethod( // -- so we can add assertions about it let publicInput = actualArgs[0]; actualArgs = actualArgs.slice(1); + let party = this.self; // outside a transaction, just call the method, but check precondition invariants let result = method.apply(this, actualArgs); - checkPublicInput(publicInput, this.self); + checkPublicInput(publicInput, party); // check the self party right after calling the method // TODO: this needs to be done in a unified way for all parties that are created - assertPreconditionInvariants(this.self); - cleanPreconditionsCache(this.self); + assertPreconditionInvariants(party); + cleanPreconditionsCache(party); assertStatePrecondition(this); return result; } @@ -157,9 +163,17 @@ function wrapMethod( // first, clone to protect against the method modifying arguments! // TODO: double-check that this works on all possible inputs, e.g. CircuitValue, snarkyjs primitives let clonedArgs = cloneCircuitValue(actualArgs); - let result = method.apply(this, actualArgs); - assertStatePrecondition(this); let party = this.self; + + // we run this in a "memoization context" so that we can remember witnesses for reuse when proving + let [{ memoized }, result] = memoizationContext.runWith( + { memoized: [], currentIndex: 0 }, + () => { + let result = method.apply(this, actualArgs); + assertStatePrecondition(this); + return result; + } + ); if (!Authorization.hasAny(party)) { Authorization.setLazyProof(party, { methodName: methodIntf.methodName, @@ -167,6 +181,7 @@ function wrapMethod( // proofs actually don't have to be cloned previousProofs: getPreviousProofsForProver(actualArgs, methodIntf), ZkappClass, + memoized, }); } return result; From f669f6cf64109b2e1ecb44fa7451d03e1a50d9ac Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 21 Jul 2022 15:49:41 +0200 Subject: [PATCH 07/19] composability mvp --- src/examples/zkapps/composability.ts | 96 +++++++++++ src/lib/circuit_value.ts | 5 + src/lib/proof_system.ts | 40 +++++ src/lib/zkapp.ts | 243 +++++++++++++++++++-------- 4 files changed, 311 insertions(+), 73 deletions(-) create mode 100644 src/examples/zkapps/composability.ts diff --git a/src/examples/zkapps/composability.ts b/src/examples/zkapps/composability.ts new file mode 100644 index 000000000..b13e23166 --- /dev/null +++ b/src/examples/zkapps/composability.ts @@ -0,0 +1,96 @@ +/** + * zkApps composability + */ +import { + Experimental, + Field, + isReady, + method, + Mina, + Party, + Permissions, + PrivateKey, + SmartContract, + state, + State, +} from 'snarkyjs'; + +const doProofs = true; + +await isReady; + +// contract which can add two numbers and return the result +class CallableAdd extends SmartContract { + @method add(x: Field, y: Field, _blindingValue: Field) { + // compute result + return x.add(y); + } +} + +let callableKey = PrivateKey.random(); +let callableAddress = callableKey.toPublicKey(); + +class Caller extends SmartContract { + @state(Field) sum = State(); + events = { sum: Field }; + + @method callAddAndEmit(x: Field, y: Field) { + let blindingValue = Experimental.memoizeWitness(Field, () => + Field.random() + ); + let adder = new CallableAdd(callableAddress); + let sum = adder.add(x, y, blindingValue); + this.emitEvent('sum', sum); + this.sum.set(sum); + } +} + +// script to deploy zkapps and do interactions + +let Local = Mina.LocalBlockchain(); +Mina.setActiveInstance(Local); + +// a test account that pays all the fees, and puts additional funds into the zkapp +let feePayer = Local.testAccounts[0].privateKey; + +// the zkapp account +let zkappKey = PrivateKey.random(); +let zkappAddress = zkappKey.toPublicKey(); + +let zkapp = new Caller(zkappAddress); +let callableZkapp = new CallableAdd(callableAddress); + +if (doProofs) { + console.log('compile (caller)'); + await CallableAdd.compile(callableAddress); + console.log('compile (callee)'); + await Caller.compile(zkappAddress); +} + +console.log('deploy'); +let tx = await Mina.transaction(feePayer, () => { + Party.fundNewAccount(feePayer, { initialBalance: Mina.accountCreationFee() }); + zkapp.deploy({ zkappKey }); + zkapp.setPermissions({ + ...Permissions.default(), + editState: Permissions.proofOrSignature(), + }); + callableZkapp.deploy({ zkappKey: callableKey }); +}); +tx.send(); + +console.log('call interaction'); +tx = await Mina.transaction(feePayer, () => { + zkapp.callAddAndEmit(Field(5), Field(6)); + if (!doProofs) zkapp.sign(zkappKey); +}); +if (doProofs) { + console.log('proving (2 proofs: caller + callee)'); + await tx.prove(); +} + +console.dir(JSON.parse(tx.toJSON()), { depth: 5 }); + +tx.send(); + +console.log('state: ' + zkapp.sum.get()); diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index f6f2d458f..302f1afc6 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -22,6 +22,7 @@ export { circuitArray, memoizationContext, memoizeWitness, + toConstant, }; type AnyConstructor = new (...args: any) => any; @@ -492,6 +493,10 @@ function circuitValueEquals(a: T, b: T): boolean { ); } +function toConstant(type: AsFieldElements, value: T): T { + return type.ofFields(type.toFields(value).map((x) => x.toConstant())); +} + // TODO: move `Circuit` to JS entirely, this patching harms code discoverability Circuit.switch = function >( mask: Bool[], diff --git a/src/lib/proof_system.ts b/src/lib/proof_system.ts index 5393a605d..9643e7039 100644 --- a/src/lib/proof_system.ts +++ b/src/lib/proof_system.ts @@ -6,6 +6,7 @@ import { Circuit, Poseidon, } from '../snarky'; +import { toConstant } from './circuit_value'; import { Context } from './global-context'; // public API @@ -23,6 +24,8 @@ export { emptyValue, emptyWitness, synthesizeMethodArguments, + methodArgumentsToConstant, + methodArgumentsToFields, snarkContext, inProver, inCompile, @@ -467,6 +470,43 @@ function synthesizeMethodArguments( return args; } +function methodArgumentsToConstant( + { allArgs, proofArgs, witnessArgs }: MethodInterface, + args: any[] +) { + let constArgs = []; + for (let i = 0; i < allArgs.length; i++) { + let arg = args[i]; + let { type, index } = allArgs[i]; + if (type === 'witness') { + constArgs.push(toConstant(witnessArgs[index], arg)); + } else { + let Proof = proofArgs[index]; + let publicInput = toConstant(getPublicInputType(Proof), arg.publicInput); + constArgs.push(new Proof({ publicInput, proof: arg.proof })); + } + } + return constArgs; +} +function methodArgumentsToFields( + { allArgs, proofArgs, witnessArgs }: MethodInterface, + args: any[] +) { + let fields: Field[] = []; + for (let i = 0; i < allArgs.length; i++) { + let arg = args[i]; + let { type, index } = allArgs[i]; + if (type === 'witness') { + fields.push(...witnessArgs[index].toFields(arg)); + } else { + let Proof = proofArgs[index]; + let publicInput = getPublicInputType(Proof).toFields(arg.publicInput); + fields.push(...publicInput); + } + } + return fields; +} + function emptyValue(type: AsFieldElements) { return type.ofFields(Array(type.sizeInFields()).fill(Field.zero)); } diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 5de0b591a..835029161 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -5,7 +5,7 @@ import { Ledger, Pickles, InferAsFieldElements, - Poseidon, + Poseidon as Poseidon_, } from '../snarky'; import { Circuit, @@ -44,9 +44,13 @@ import { snarkContext, inProver, inAnalyze, + methodArgumentsToConstant, + methodArgumentsToFields, } from './proof_system'; import { assertStatePrecondition, cleanStatePrecondition } from './state'; import { Types } from '../snarky/types'; +import { Context } from './global-context'; +import { Poseidon } from './hash'; // external API export { @@ -88,6 +92,9 @@ function method( ); } let paramTypes = Reflect.getMetadata('design:paramtypes', target, methodName); + // this doesn't work :/ + // let returnType = Reflect.getMetadata('design:returntype', target, methodName); + class SelfProof extends Proof { static publicInputType = ZkappPublicInput; static tag = () => ZkappClass; @@ -109,6 +116,8 @@ function method( descriptor.value = wrapMethod(func, ZkappClass, methodEntry); } +let smartContractContext = Context.create<{ this: SmartContract }>(); + // do different things when calling a method, depending on the circumstance function wrapMethod( method: Function, @@ -116,76 +125,174 @@ function wrapMethod( methodIntf: MethodInterface ) { return function wrappedMethod(this: SmartContract, ...actualArgs: any[]) { + // console.log('wrapped method', methodIntf.methodName); cleanStatePrecondition(this); - if (inCheckedComputation()) { - // important to run this with a fresh party everytime, otherwise compile messes up our circuits - // because it runs this multiple times - let [, result] = Mina.currentTransaction.runWith( - { - sender: undefined, - parties: [], - 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 - let publicInput = actualArgs[0]; - actualArgs = actualArgs.slice(1); - let party = this.self; - + if (!smartContractContext.has()) { + return smartContractContext.runWith({ this: this }, () => { + if (inCheckedComputation()) { + // important to run this with a fresh party everytime, otherwise compile messes up our circuits + // because it runs this multiple times + let [, result] = Mina.currentTransaction.runWith( + { + sender: undefined, + parties: [], + 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 + let publicInput = actualArgs[0]; + actualArgs = actualArgs.slice(1); + let party = this.self; + + let result = method.apply(this, actualArgs); + + // connects our input + result with callData, so this method can be called + // TODO include result + blinding value + let argsFields = methodArgumentsToFields(methodIntf, actualArgs); + party.body.callData = Poseidon.hash(argsFields); + // Circuit.asProver(() => { + // console.log('callData (checked) ' + party.body.callData); + // }); + + // connect the public input to the party & child parties we created + // Circuit.asProver(() => { + // console.log('party (prover)'); + // console.dir(party.toJSON(), { depth: 5 }); + // }); + checkPublicInput(publicInput, party); + + // check the self party right after calling the method + // TODO: this needs to be done in a unified way for all parties that are created + assertPreconditionInvariants(party); + cleanPreconditionsCache(party); + assertStatePrecondition(this); + return 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); - checkPublicInput(publicInput, party); - // check the self party right after calling the method // TODO: this needs to be done in a unified way for all parties that are created - assertPreconditionInvariants(party); - cleanPreconditionsCache(party); + assertPreconditionInvariants(this.self); + cleanPreconditionsCache(this.self); assertStatePrecondition(this); return 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); - // check the self party right after calling the method - // TODO: this needs to be done in a unified way for all parties that are created - assertPreconditionInvariants(this.self); - cleanPreconditionsCache(this.self); - assertStatePrecondition(this); - return result; - } else { - // in a transaction, also add a lazy proof to the self party - // (if there's no other authorization set) - - // first, clone to protect against the method modifying arguments! - // TODO: double-check that this works on all possible inputs, e.g. CircuitValue, snarkyjs primitives - let clonedArgs = cloneCircuitValue(actualArgs); - let party = this.self; - - // we run this in a "memoization context" so that we can remember witnesses for reuse when proving - let [{ memoized }, result] = memoizationContext.runWith( - { memoized: [], currentIndex: 0 }, - () => { - let result = method.apply(this, actualArgs); + } else { + // in a transaction, also add a lazy proof to the self party + // (if there's no other authorization set) + + // first, clone to protect against the method modifying arguments! + // TODO: double-check that this works on all possible inputs, e.g. CircuitValue, snarkyjs primitives + let clonedArgs = cloneCircuitValue(actualArgs); + let party = this.self; + + // we run this in a "memoization context" so that we can remember witnesses for reuse when proving + let [{ memoized }, result] = memoizationContext.runWith( + { memoized: [], currentIndex: 0 }, + () => method.apply(this, actualArgs) + ); assertStatePrecondition(this); + + // connects our input + result with callData, so this method can be called + // TODO include result + blinding value + let argsFields = methodArgumentsToFields(methodIntf, actualArgs); + party.body.callData = Poseidon.hash(argsFields); + // console.log('callData (transaction) ' + party.body.callData);s + + if (!Authorization.hasAny(party)) { + Authorization.setLazyProof(party, { + methodName: methodIntf.methodName, + args: clonedArgs, + // proofs actually don't have to be cloned + previousProofs: getPreviousProofsForProver( + actualArgs, + methodIntf + ), + ZkappClass, + memoized, + }); + } return result; } - ); - if (!Authorization.hasAny(party)) { - Authorization.setLazyProof(party, { - methodName: methodIntf.methodName, - args: clonedArgs, - // proofs actually don't have to be cloned - previousProofs: getPreviousProofsForProver(actualArgs, methodIntf), - ZkappClass, - memoized, - }); - } - return result; + })[1]; } + // if we're here, this method was called inside _another_ smart contract method + // this means we have to run this method inside a witness block, to not affect the caller's circuit + let parentParty = smartContractContext.get().this.self; + + // TODO create blinding value automatically, with custom annotation? or everytime, without annotation? + // the blindingValue is necessary because otherwise, putting this on the transaction would leak information about the private inputs + // let blindingValue = memoizeWitness(Field, () => Field.random()); + + // witness the result of calling `add` + // let result: any; + let { party, result } = Party.witness( + // circuitValue(null), + Field, + () => { + let constantArgs = methodArgumentsToConstant(methodIntf, actualArgs); + let party = this.self; + // the line above adds the callee's self party into the wrong place in the transaction structure + // so we remove it again + // TODO: since we wrap all method calls now anyway, should remove that hidden logic in this.self + // and add parties to transactions more explicitly + let transaction = Mina.currentTransaction(); + if (transaction !== undefined) transaction.parties.pop(); + + let [{ memoized }, result] = memoizationContext.runWith( + { memoized: [], currentIndex: 0 }, + () => method.apply(this, constantArgs) + ); + // result = result0; + assertStatePrecondition(this); + + // store inputs + result in callData + // TODO include result + blinding value + // TODO: need annotation for result type, include result type in this hash + let argsFields = methodArgumentsToFields(methodIntf, constantArgs); + party.body.callData = Poseidon_.hash(argsFields, false); + // console.log('callData (callee) ' + party.body.callData); + + if (!Authorization.hasAny(party)) { + Authorization.setLazyProof(party, { + methodName: methodIntf.methodName, + args: constantArgs, + previousProofs: getPreviousProofsForProver( + constantArgs, + methodIntf + ), + ZkappClass, + memoized, + }); + } + return { party, result }; + }, + true + ); + // we're in the _caller's_ circuit now, where we assert stuff about the method call + + // connect party to our own. outside Circuit.witness so compile knows about it + party.body.callDepth = parentParty.body.callDepth + 1; + party.parent = parentParty; + parentParty.children.push(party); + + // assert that we really called the right zkapp + party.body.publicKey.assertEquals(this.address); + party.body.tokenId.assertEquals(this.self.body.tokenId); + + // assert that the inputs & outputs we have match what the callee put on its callData + let callData = Poseidon.hash( + methodArgumentsToFields(methodIntf, actualArgs) + ); + // Circuit.asProver(() => { + // console.log('callData (caller) ' + callData); + // }); + party.body.callData.assertEquals(callData); + return result; }; } @@ -270,7 +377,7 @@ class SmartContract { static digest(address: PublicKey) { let methodData = this.analyzeMethods(address); - let hash = Poseidon.hash( + let hash = Poseidon_.hash( Object.values(methodData).map((d) => Field(BigInt('0x' + d.digest))), false ); @@ -300,12 +407,10 @@ class SmartContract { private executionState(): ExecutionState { 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) return { transactionId: NaN, - partyIndex: NaN, party: selfParty(this.address), }; } @@ -320,11 +425,7 @@ class SmartContract { let id = Mina.currentTransaction.id(); let party = selfParty(this.address); transaction.parties.push(party); - executionState = { - transactionId: id, - partyIndex: transaction.parties.length, - party, - }; + executionState = { transactionId: id, party }; this._executionState = executionState; return executionState; } @@ -553,11 +654,7 @@ function selfParty(address: PublicKey) { } // per-smart-contract context for transaction construction -type ExecutionState = { - transactionId: number; - partyIndex: number; - party: Party; -}; +type ExecutionState = { transactionId: number; party: Party }; type DeployArgs = { verificationKey?: { data: string; hash: string | Field }; From 4ea5c059a197d1c28ab7da2e82c77cd501cee0d4 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 21 Jul 2022 17:10:13 +0200 Subject: [PATCH 08/19] include return value in callData hash --- src/examples/zkapps/composability.ts | 2 +- src/lib/proof_system.ts | 2 + src/lib/zkapp.ts | 64 ++++++++++++++++++++++------ 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/examples/zkapps/composability.ts b/src/examples/zkapps/composability.ts index b13e23166..94bb3248a 100644 --- a/src/examples/zkapps/composability.ts +++ b/src/examples/zkapps/composability.ts @@ -21,7 +21,7 @@ await isReady; // contract which can add two numbers and return the result class CallableAdd extends SmartContract { - @method add(x: Field, y: Field, _blindingValue: Field) { + @method add(x: Field, y: Field, _blindingValue: Field): Field { // compute result return x.add(y); } diff --git a/src/lib/proof_system.ts b/src/lib/proof_system.ts index 9643e7039..9af2471c1 100644 --- a/src/lib/proof_system.ts +++ b/src/lib/proof_system.ts @@ -26,6 +26,7 @@ export { synthesizeMethodArguments, methodArgumentsToConstant, methodArgumentsToFields, + isAsFields, snarkContext, inProver, inCompile, @@ -352,6 +353,7 @@ type MethodInterface = { witnessArgs: AsFieldElements[]; proofArgs: Subclass[]; allArgs: { type: 'witness' | 'proof'; index: number }[]; + returnType?: AsFieldElements; }; function compileProgram( diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 835029161..a307a1442 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -10,8 +10,10 @@ import { import { Circuit, circuitArray, + circuitValue, cloneCircuitValue, memoizationContext, + toConstant, } from './circuit_value'; import { Body, @@ -46,6 +48,7 @@ import { inAnalyze, methodArgumentsToConstant, methodArgumentsToFields, + isAsFields, } from './proof_system'; import { assertStatePrecondition, cleanStatePrecondition } from './state'; import { Types } from '../snarky/types'; @@ -92,8 +95,7 @@ function method( ); } let paramTypes = Reflect.getMetadata('design:paramtypes', target, methodName); - // this doesn't work :/ - // let returnType = Reflect.getMetadata('design:returntype', target, methodName); + let returnType = Reflect.getMetadata('design:returntype', target, methodName); class SelfProof extends Proof { static publicInputType = ZkappPublicInput; @@ -105,6 +107,7 @@ function method( paramTypes, SelfProof ); + if (isAsFields(returnType)) methodEntry.returnType = returnType; ZkappClass._methods ??= []; ZkappClass._methods.push(methodEntry); ZkappClass._maxProofsVerified ??= 0; @@ -151,6 +154,9 @@ function wrapMethod( // connects our input + result with callData, so this method can be called // TODO include result + blinding value let argsFields = methodArgumentsToFields(methodIntf, actualArgs); + if (methodIntf.returnType) { + argsFields.push(...methodIntf.returnType.toFields(result)); + } party.body.callData = Poseidon.hash(argsFields); // Circuit.asProver(() => { // console.log('callData (checked) ' + party.body.callData); @@ -200,6 +206,9 @@ function wrapMethod( // connects our input + result with callData, so this method can be called // TODO include result + blinding value let argsFields = methodArgumentsToFields(methodIntf, actualArgs); + if (methodIntf.returnType) { + argsFields.push(...methodIntf.returnType.toFields(result)); + } party.body.callData = Poseidon.hash(argsFields); // console.log('callData (transaction) ' + party.body.callData);s @@ -229,10 +238,28 @@ function wrapMethod( // let blindingValue = memoizeWitness(Field, () => Field.random()); // witness the result of calling `add` + let { returnType } = methodIntf; + + // if the result is not undefined but there's no known returnType, the returnType was probably not annotated properly, + // so we have to explain to the user how to do that + let noReturnTypeError = + `To return a result from ${methodIntf.methodName}() inside another zkApp, you need to declare the return type.\n` + + `This can be done by annotating the type at the end of the function signature. For example:\n\n` + + `@method ${methodIntf.methodName}(): Field {\n` + + ` // ...\n` + + `}\n\n` + + `Note: Only types built out of \`Field\` are valid return types. This includes snarkyjs primitive types and custom CircuitValues.`; + // if we're lucky, analyzeMethods was already run on the callee smart contract, and we can catch this error early + if ( + (ZkappClass as any)._methodMetadata[methodIntf.methodName]?.hasReturn && + returnType === undefined + ) { + throw Error(noReturnTypeError); + } + // let result: any; let { party, result } = Party.witness( - // circuitValue(null), - Field, + returnType ?? circuitValue(null), () => { let constantArgs = methodArgumentsToConstant(methodIntf, actualArgs); let party = this.self; @@ -247,14 +274,26 @@ function wrapMethod( { memoized: [], currentIndex: 0 }, () => method.apply(this, constantArgs) ); - // result = result0; assertStatePrecondition(this); + let resultFields: Field[] = []; + if (result !== undefined) { + if (returnType === undefined) { + throw Error(noReturnTypeError); + } else { + result = toConstant(returnType, result); + resultFields = returnType.toFields(result); + } + } + // store inputs + result in callData // TODO include result + blinding value // TODO: need annotation for result type, include result type in this hash let argsFields = methodArgumentsToFields(methodIntf, constantArgs); - party.body.callData = Poseidon_.hash(argsFields, false); + party.body.callData = Poseidon_.hash( + argsFields.concat(resultFields), + false + ); // console.log('callData (callee) ' + party.body.callData); if (!Authorization.hasAny(party)) { @@ -269,7 +308,7 @@ function wrapMethod( memoized, }); } - return { party, result }; + return { party, result: result ?? null }; }, true ); @@ -285,9 +324,9 @@ function wrapMethod( party.body.tokenId.assertEquals(this.self.body.tokenId); // assert that the inputs & outputs we have match what the callee put on its callData - let callData = Poseidon.hash( - methodArgumentsToFields(methodIntf, actualArgs) - ); + let argsFields = methodArgumentsToFields(methodIntf, actualArgs); + if (returnType) argsFields.push(...returnType.toFields(result)); + let callData = Poseidon.hash(argsFields); // Circuit.asProver(() => { // console.log('callData (caller) ' + callData); // }); @@ -319,7 +358,7 @@ class SmartContract { static _methods?: MethodInterface[]; private static _methodMetadata: Record< string, - { sequenceEvents: number; rows: number; digest: string } + { sequenceEvents: number; rows: number; digest: string; hasReturn: boolean } > = {}; // keyed by method name static _provers?: Pickles.Prover[]; static _maxProofsVerified?: 0 | 1 | 2; @@ -509,7 +548,7 @@ class SmartContract { throw err; } for (let methodIntf of methodIntfs) { - let { rows, digest } = analyzeMethod( + let { rows, digest, result } = analyzeMethod( ZkappPublicInput, methodIntf, (...args) => (instance as any)[methodIntf.methodName](...args) @@ -519,6 +558,7 @@ class SmartContract { sequenceEvents: party.body.sequenceEvents.data.length, rows, digest, + hasReturn: result !== undefined, }; } } From 504f663b30fa6c6ab6c64f2cbaa30dd1c6d2205f Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 21 Jul 2022 20:55:15 +0200 Subject: [PATCH 09/19] add random value into the callData hash for hiding --- src/examples/zkapps/composability.ts | 11 ++--- src/lib/circuit_value.ts | 17 +++++++- src/lib/party.ts | 16 ++++++-- src/lib/zkapp.ts | 61 ++++++++++++++++++++++------ 4 files changed, 79 insertions(+), 26 deletions(-) diff --git a/src/examples/zkapps/composability.ts b/src/examples/zkapps/composability.ts index 94bb3248a..1debe45ca 100644 --- a/src/examples/zkapps/composability.ts +++ b/src/examples/zkapps/composability.ts @@ -21,7 +21,7 @@ await isReady; // contract which can add two numbers and return the result class CallableAdd extends SmartContract { - @method add(x: Field, y: Field, _blindingValue: Field): Field { + @method add(x: Field, y: Field): Field { // compute result return x.add(y); } @@ -35,11 +35,8 @@ class Caller extends SmartContract { events = { sum: Field }; @method callAddAndEmit(x: Field, y: Field) { - let blindingValue = Experimental.memoizeWitness(Field, () => - Field.random() - ); let adder = new CallableAdd(callableAddress); - let sum = adder.add(x, y, blindingValue); + let sum = adder.add(x, y); this.emitEvent('sum', sum); this.sum.set(sum); } @@ -61,9 +58,9 @@ let zkapp = new Caller(zkappAddress); let callableZkapp = new CallableAdd(callableAddress); if (doProofs) { - console.log('compile (caller)'); - await CallableAdd.compile(callableAddress); console.log('compile (callee)'); + await CallableAdd.compile(callableAddress); + console.log('compile (caller)'); await Caller.compile(zkappAddress); } diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index 302f1afc6..e5976d51b 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -22,6 +22,7 @@ export { circuitArray, memoizationContext, memoizeWitness, + getBlindingValue, toConstant, }; @@ -546,8 +547,11 @@ Circuit.constraintSystem = function (f: () => T) { return result; }; -let memoizationContext = - Context.create<{ memoized: Field[][]; currentIndex: number }>(); +let memoizationContext = Context.create<{ + memoized: Field[][]; + currentIndex: number; + blindingValue: Field; +}>(); /** * Like Circuit.witness, but memoizes the witness during transaction construction @@ -568,3 +572,12 @@ function memoizeWitness(type: AsFieldElements, compute: () => T) { return type.ofFields(currentValue); }); } + +function getBlindingValue() { + if (!memoizationContext.has()) return Field.random(); + let context = memoizationContext.get(); + if (context?.blindingValue === undefined) { + context.blindingValue = Field.random(); + } + return context.blindingValue; +} diff --git a/src/lib/party.ts b/src/lib/party.ts index 4f963e119..51cce8f53 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -499,6 +499,7 @@ type LazyProof = { previousProofs: { publicInput: Field[]; proof: Pickles.Proof }[]; ZkappClass: typeof SmartContract; memoized: Field[][]; + blindingValue: Field; }; class Party implements Types.Party { @@ -990,8 +991,14 @@ async function addMissingProofs(parties: Parties): Promise<{ if (party.lazyAuthorization?.kind !== 'lazy-proof') { return { partyProved: party as PartyProved, proof: undefined }; } - let { methodName, args, previousProofs, ZkappClass, memoized } = - party.lazyAuthorization; + let { + methodName, + args, + previousProofs, + ZkappClass, + memoized, + blindingValue, + } = party.lazyAuthorization; let publicInput = partyToPublicInput(party); let publicInputFields = ZkappPublicInput.toFields(publicInput); if (ZkappClass._provers === undefined) @@ -1009,8 +1016,9 @@ async function addMissingProofs(parties: Parties): Promise<{ let [, [, proof]] = await snarkContext.runWithAsync( { inProver: true, witnesses: args }, () => - memoizationContext.runWithAsync({ memoized, currentIndex: 0 }, () => - provers[i](publicInputFields, previousProofs) + memoizationContext.runWithAsync( + { memoized, currentIndex: 0, blindingValue }, + () => provers[i](publicInputFields, previousProofs) ) ); Authorization.setProof(party, Pickles.proofToBase64Transaction(proof)); diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index a307a1442..449cac183 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -12,6 +12,7 @@ import { circuitArray, circuitValue, cloneCircuitValue, + getBlindingValue, memoizationContext, toConstant, } from './circuit_value'; @@ -128,9 +129,9 @@ function wrapMethod( methodIntf: MethodInterface ) { return function wrappedMethod(this: SmartContract, ...actualArgs: any[]) { - // console.log('wrapped method', methodIntf.methodName); cleanStatePrecondition(this); if (!smartContractContext.has()) { + // console.log('wrapped method', methodIntf.methodName); return smartContractContext.runWith({ this: this }, () => { if (inCheckedComputation()) { // important to run this with a fresh party everytime, otherwise compile messes up our circuits @@ -149,14 +150,26 @@ function wrapMethod( actualArgs = actualArgs.slice(1); let party = this.self; - let result = method.apply(this, actualArgs); + // the blinding value is important because otherwise, putting callData on the transaction would leak information about the private inputs + let blindingValue = Circuit.witness(Field, getBlindingValue); + // it's also good if we prove that we use the same blinding value across the method + // that's why we pass the variable_ (not the constant) into a new context + // console.log('blinding value (circuit)', blindingValue); + let context = memoizationContext() ?? { + memoized: [], + currentIndex: 0, + }; + let [, result] = memoizationContext.runWith( + { ...context, blindingValue }, + () => method.apply(this, actualArgs) + ); // connects our input + result with callData, so this method can be called - // TODO include result + blinding value let argsFields = methodArgumentsToFields(methodIntf, actualArgs); if (methodIntf.returnType) { argsFields.push(...methodIntf.returnType.toFields(result)); } + argsFields.push(blindingValue); party.body.callData = Poseidon.hash(argsFields); // Circuit.asProver(() => { // console.log('callData (checked) ' + party.body.callData); @@ -197,9 +210,17 @@ function wrapMethod( let party = this.self; // we run this in a "memoization context" so that we can remember witnesses for reuse when proving + let blindingValue = getBlindingValue(); + // console.log('blinding value (tx)', blindingValue); let [{ memoized }, result] = memoizationContext.runWith( - { memoized: [], currentIndex: 0 }, - () => method.apply(this, actualArgs) + { + memoized: [], + currentIndex: 0, + blindingValue, + }, + () => { + method.apply(this, actualArgs); + } ); assertStatePrecondition(this); @@ -209,6 +230,7 @@ function wrapMethod( if (methodIntf.returnType) { argsFields.push(...methodIntf.returnType.toFields(result)); } + argsFields.push(blindingValue); party.body.callData = Poseidon.hash(argsFields); // console.log('callData (transaction) ' + party.body.callData);s @@ -223,19 +245,25 @@ function wrapMethod( ), ZkappClass, memoized, + blindingValue, }); } return result; } })[1]; } + // console.log( + // 'wrapped method', + // methodIntf.methodName, + // '(called by another one)' + // ); // if we're here, this method was called inside _another_ smart contract method // this means we have to run this method inside a witness block, to not affect the caller's circuit let parentParty = smartContractContext.get().this.self; - // TODO create blinding value automatically, with custom annotation? or everytime, without annotation? - // the blindingValue is necessary because otherwise, putting this on the transaction would leak information about the private inputs - // let blindingValue = memoizeWitness(Field, () => Field.random()); + // we just reuse the blinding value of the caller for the callee + let blindingValue = getBlindingValue(); + // console.log('blinding value (caller)', blindingValue); // witness the result of calling `add` let { returnType } = methodIntf; @@ -262,6 +290,8 @@ function wrapMethod( returnType ?? circuitValue(null), () => { let constantArgs = methodArgumentsToConstant(methodIntf, actualArgs); + let constantBlindingValue = blindingValue.toConstant(); + // console.log('blinding value (callee)', constantBlindingValue); let party = this.self; // the line above adds the callee's self party into the wrong place in the transaction structure // so we remove it again @@ -271,7 +301,11 @@ function wrapMethod( if (transaction !== undefined) transaction.parties.pop(); let [{ memoized }, result] = memoizationContext.runWith( - { memoized: [], currentIndex: 0 }, + { + memoized: [], + currentIndex: 0, + blindingValue: constantBlindingValue, + }, () => method.apply(this, constantArgs) ); assertStatePrecondition(this); @@ -290,10 +324,9 @@ function wrapMethod( // TODO include result + blinding value // TODO: need annotation for result type, include result type in this hash let argsFields = methodArgumentsToFields(methodIntf, constantArgs); - party.body.callData = Poseidon_.hash( - argsFields.concat(resultFields), - false - ); + argsFields.push(...resultFields); + argsFields.push(constantBlindingValue); + party.body.callData = Poseidon_.hash(argsFields, false); // console.log('callData (callee) ' + party.body.callData); if (!Authorization.hasAny(party)) { @@ -306,6 +339,7 @@ function wrapMethod( ), ZkappClass, memoized, + blindingValue: constantBlindingValue, }); } return { party, result: result ?? null }; @@ -326,6 +360,7 @@ function wrapMethod( // assert that the inputs & outputs we have match what the callee put on its callData let argsFields = methodArgumentsToFields(methodIntf, actualArgs); if (returnType) argsFields.push(...returnType.toFields(result)); + argsFields.push(blindingValue); let callData = Poseidon.hash(argsFields); // Circuit.asProver(() => { // console.log('callData (caller) ' + callData); From f8536e6527bcf9a6a5ef56142e1b74a3fb99fd77 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 21 Jul 2022 21:29:50 +0200 Subject: [PATCH 10/19] start handling calls inside calls --- src/lib/zkapp.ts | 434 +++++++++++++++++++++++++---------------------- 1 file changed, 227 insertions(+), 207 deletions(-) diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 449cac183..9ba9fe87d 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -120,7 +120,8 @@ function method( descriptor.value = wrapMethod(func, ZkappClass, methodEntry); } -let smartContractContext = Context.create<{ this: SmartContract }>(); +let smartContractContext = + Context.create<{ this: SmartContract; methodCallDepth: number }>(); // do different things when calling a method, depending on the circumstance function wrapMethod( @@ -132,240 +133,259 @@ function wrapMethod( cleanStatePrecondition(this); if (!smartContractContext.has()) { // console.log('wrapped method', methodIntf.methodName); - return smartContractContext.runWith({ this: this }, () => { - if (inCheckedComputation()) { - // important to run this with a fresh party everytime, otherwise compile messes up our circuits - // because it runs this multiple times - let [, result] = Mina.currentTransaction.runWith( - { - sender: undefined, - parties: [], - 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 - let publicInput = actualArgs[0]; - actualArgs = actualArgs.slice(1); - let party = this.self; - - // the blinding value is important because otherwise, putting callData on the transaction would leak information about the private inputs - let blindingValue = Circuit.witness(Field, getBlindingValue); - // it's also good if we prove that we use the same blinding value across the method - // that's why we pass the variable_ (not the constant) into a new context - // console.log('blinding value (circuit)', blindingValue); - let context = memoizationContext() ?? { + return smartContractContext.runWith( + { this: this, methodCallDepth: 0 }, + () => { + if (inCheckedComputation()) { + // important to run this with a fresh party everytime, otherwise compile messes up our circuits + // because it runs this multiple times + let [, result] = Mina.currentTransaction.runWith( + { + sender: undefined, + parties: [], + 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 + let publicInput = actualArgs[0]; + actualArgs = actualArgs.slice(1); + let party = this.self; + + // the blinding value is important because otherwise, putting callData on the transaction would leak information about the private inputs + let blindingValue = Circuit.witness(Field, getBlindingValue); + // it's also good if we prove that we use the same blinding value across the method + // that's why we pass the variable_ (not the constant) into a new context + // console.log('blinding value (circuit)', blindingValue); + let context = memoizationContext() ?? { + memoized: [], + currentIndex: 0, + }; + let [, result] = memoizationContext.runWith( + { ...context, blindingValue }, + () => method.apply(this, actualArgs) + ); + + // connects our input + result with callData, so this method can be called + let argsFields = methodArgumentsToFields( + methodIntf, + actualArgs + ); + if (methodIntf.returnType) { + argsFields.push(...methodIntf.returnType.toFields(result)); + } + argsFields.push(blindingValue); + party.body.callData = Poseidon.hash(argsFields); + // Circuit.asProver(() => { + // console.log('callData (checked) ' + party.body.callData); + // }); + + // connect the public input to the party & child parties we created + // Circuit.asProver(() => { + // console.log('party (prover)'); + // console.dir(party.toJSON(), { depth: 5 }); + // }); + checkPublicInput(publicInput, party); + + // check the self party right after calling the method + // TODO: this needs to be done in a unified way for all parties that are created + assertPreconditionInvariants(party); + cleanPreconditionsCache(party); + assertStatePrecondition(this); + return 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); + // check the self party right after calling the method + // TODO: this needs to be done in a unified way for all parties that are created + assertPreconditionInvariants(this.self); + cleanPreconditionsCache(this.self); + assertStatePrecondition(this); + return result; + } else { + // in a transaction, also add a lazy proof to the self party + // (if there's no other authorization set) + + // first, clone to protect against the method modifying arguments! + // TODO: double-check that this works on all possible inputs, e.g. CircuitValue, snarkyjs primitives + let clonedArgs = cloneCircuitValue(actualArgs); + let party = this.self; + + // we run this in a "memoization context" so that we can remember witnesses for reuse when proving + let blindingValue = getBlindingValue(); + // console.log('blinding value (tx)', blindingValue); + let [{ memoized }, result] = memoizationContext.runWith( + { memoized: [], currentIndex: 0, - }; - let [, result] = memoizationContext.runWith( - { ...context, blindingValue }, - () => method.apply(this, actualArgs) - ); - - // connects our input + result with callData, so this method can be called - let argsFields = methodArgumentsToFields(methodIntf, actualArgs); - if (methodIntf.returnType) { - argsFields.push(...methodIntf.returnType.toFields(result)); + blindingValue, + }, + () => { + method.apply(this, actualArgs); } - argsFields.push(blindingValue); - party.body.callData = Poseidon.hash(argsFields); - // Circuit.asProver(() => { - // console.log('callData (checked) ' + party.body.callData); - // }); - - // connect the public input to the party & child parties we created - // Circuit.asProver(() => { - // console.log('party (prover)'); - // console.dir(party.toJSON(), { depth: 5 }); - // }); - checkPublicInput(publicInput, party); - - // check the self party right after calling the method - // TODO: this needs to be done in a unified way for all parties that are created - assertPreconditionInvariants(party); - cleanPreconditionsCache(party); - assertStatePrecondition(this); - return result; + ); + assertStatePrecondition(this); + + // connects our input + result with callData, so this method can be called + // TODO include result + blinding value + let argsFields = methodArgumentsToFields(methodIntf, actualArgs); + if (methodIntf.returnType) { + argsFields.push(...methodIntf.returnType.toFields(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); - // check the self party right after calling the method - // TODO: this needs to be done in a unified way for all parties that are created - assertPreconditionInvariants(this.self); - cleanPreconditionsCache(this.self); - assertStatePrecondition(this); - return result; - } else { - // in a transaction, also add a lazy proof to the self party - // (if there's no other authorization set) - - // first, clone to protect against the method modifying arguments! - // TODO: double-check that this works on all possible inputs, e.g. CircuitValue, snarkyjs primitives - let clonedArgs = cloneCircuitValue(actualArgs); + argsFields.push(blindingValue); + party.body.callData = Poseidon.hash(argsFields); + // console.log('callData (transaction) ' + party.body.callData);s + + if (!Authorization.hasAny(party)) { + Authorization.setLazyProof(party, { + methodName: methodIntf.methodName, + args: clonedArgs, + // proofs actually don't have to be cloned + previousProofs: getPreviousProofsForProver( + actualArgs, + methodIntf + ), + ZkappClass, + memoized, + blindingValue, + }); + } + return result; + } + } + )[1]; + } + // console.log( + // 'wrapped method', + // methodIntf.methodName, + // '(called by another one)' + // ); + // if we're here, this method was called inside _another_ smart contract method + let parentParty = smartContractContext.get().this.self; + let methodCallDepth = smartContractContext.get().methodCallDepth; + console.log({ methodCallDepth }); + let [, result] = smartContractContext.runWith( + { this: this, methodCallDepth: methodCallDepth + 1 }, + () => { + // we just reuse the blinding value of the caller for the callee + let blindingValue = getBlindingValue(); + // console.log('blinding value (caller)', blindingValue); + + // witness the result of calling `add` + let { returnType } = methodIntf; + + // if the result is not undefined but there's no known returnType, the returnType was probably not annotated properly, + // so we have to explain to the user how to do that + let noReturnTypeError = + `To return a result from ${methodIntf.methodName}() inside another zkApp, you need to declare the return type.\n` + + `This can be done by annotating the type at the end of the function signature. For example:\n\n` + + `@method ${methodIntf.methodName}(): Field {\n` + + ` // ...\n` + + `}\n\n` + + `Note: Only types built out of \`Field\` are valid return types. This includes snarkyjs primitive types and custom CircuitValues.`; + // if we're lucky, analyzeMethods was already run on the callee smart contract, and we can catch this error early + if ( + (ZkappClass as any)._methodMetadata[methodIntf.methodName] + ?.hasReturn && + returnType === undefined + ) { + throw Error(noReturnTypeError); + } + + let runCalledContract = () => { + let constantArgs = methodArgumentsToConstant(methodIntf, actualArgs); + let constantBlindingValue = blindingValue.toConstant(); + // console.log('blinding value (callee)', constantBlindingValue); let party = this.self; + // the line above adds the callee's self party into the wrong place in the transaction structure + // so we remove it again + // TODO: since we wrap all method calls now anyway, should remove that hidden logic in this.self + // and add parties to transactions more explicitly + let transaction = Mina.currentTransaction(); + if (transaction !== undefined) transaction.parties.pop(); - // we run this in a "memoization context" so that we can remember witnesses for reuse when proving - let blindingValue = getBlindingValue(); - // console.log('blinding value (tx)', blindingValue); let [{ memoized }, result] = memoizationContext.runWith( { memoized: [], currentIndex: 0, - blindingValue, + blindingValue: constantBlindingValue, }, - () => { - method.apply(this, actualArgs); - } + () => method.apply(this, constantArgs) ); assertStatePrecondition(this); - // connects our input + result with callData, so this method can be called - // TODO include result + blinding value - let argsFields = methodArgumentsToFields(methodIntf, actualArgs); - if (methodIntf.returnType) { - argsFields.push(...methodIntf.returnType.toFields(result)); + let resultFields: Field[] = []; + if (result !== undefined) { + if (returnType === undefined) { + throw Error(noReturnTypeError); + } else { + result = toConstant(returnType, result); + resultFields = returnType.toFields(result); + } } - argsFields.push(blindingValue); - party.body.callData = Poseidon.hash(argsFields); - // console.log('callData (transaction) ' + party.body.callData);s + + // store inputs + result in callData + let argsFields = methodArgumentsToFields(methodIntf, constantArgs); + argsFields.push(...resultFields); + argsFields.push(constantBlindingValue); + party.body.callData = Poseidon_.hash(argsFields, false); + // console.log('callData (callee) ' + party.body.callData); if (!Authorization.hasAny(party)) { Authorization.setLazyProof(party, { methodName: methodIntf.methodName, - args: clonedArgs, - // proofs actually don't have to be cloned + args: constantArgs, previousProofs: getPreviousProofsForProver( - actualArgs, + constantArgs, methodIntf ), ZkappClass, memoized, - blindingValue, + blindingValue: constantBlindingValue, }); } - return result; - } - })[1]; - } - // console.log( - // 'wrapped method', - // methodIntf.methodName, - // '(called by another one)' - // ); - // if we're here, this method was called inside _another_ smart contract method - // this means we have to run this method inside a witness block, to not affect the caller's circuit - let parentParty = smartContractContext.get().this.self; - - // we just reuse the blinding value of the caller for the callee - let blindingValue = getBlindingValue(); - // console.log('blinding value (caller)', blindingValue); - - // witness the result of calling `add` - let { returnType } = methodIntf; - - // if the result is not undefined but there's no known returnType, the returnType was probably not annotated properly, - // so we have to explain to the user how to do that - let noReturnTypeError = - `To return a result from ${methodIntf.methodName}() inside another zkApp, you need to declare the return type.\n` + - `This can be done by annotating the type at the end of the function signature. For example:\n\n` + - `@method ${methodIntf.methodName}(): Field {\n` + - ` // ...\n` + - `}\n\n` + - `Note: Only types built out of \`Field\` are valid return types. This includes snarkyjs primitive types and custom CircuitValues.`; - // if we're lucky, analyzeMethods was already run on the callee smart contract, and we can catch this error early - if ( - (ZkappClass as any)._methodMetadata[methodIntf.methodName]?.hasReturn && - returnType === undefined - ) { - throw Error(noReturnTypeError); - } - - // let result: any; - let { party, result } = Party.witness( - returnType ?? circuitValue(null), - () => { - let constantArgs = methodArgumentsToConstant(methodIntf, actualArgs); - let constantBlindingValue = blindingValue.toConstant(); - // console.log('blinding value (callee)', constantBlindingValue); - let party = this.self; - // the line above adds the callee's self party into the wrong place in the transaction structure - // so we remove it again - // TODO: since we wrap all method calls now anyway, should remove that hidden logic in this.self - // and add parties to transactions more explicitly - let transaction = Mina.currentTransaction(); - if (transaction !== undefined) transaction.parties.pop(); - - let [{ memoized }, result] = memoizationContext.runWith( - { - memoized: [], - currentIndex: 0, - blindingValue: constantBlindingValue, - }, - () => method.apply(this, constantArgs) - ); - assertStatePrecondition(this); - - let resultFields: Field[] = []; - if (result !== undefined) { - if (returnType === undefined) { - throw Error(noReturnTypeError); - } else { - result = toConstant(returnType, result); - resultFields = returnType.toFields(result); - } - } + return { party, result: result ?? null }; + }; - // store inputs + result in callData - // TODO include result + blinding value - // TODO: need annotation for result type, include result type in this hash - let argsFields = methodArgumentsToFields(methodIntf, constantArgs); - argsFields.push(...resultFields); - argsFields.push(constantBlindingValue); - party.body.callData = Poseidon_.hash(argsFields, false); - // console.log('callData (callee) ' + party.body.callData); - - if (!Authorization.hasAny(party)) { - Authorization.setLazyProof(party, { - methodName: methodIntf.methodName, - args: constantArgs, - previousProofs: getPreviousProofsForProver( - constantArgs, - methodIntf - ), - ZkappClass, - memoized, - blindingValue: constantBlindingValue, - }); - } - return { party, result: result ?? null }; - }, - true + // we have to run the called contract inside a witness block, to not affect the caller's circuit + // however, if this is another contract called by a called contract, then we're already in a witness block, + // and shouldn't open another one + let { party, result } = + methodCallDepth === 0 + ? Party.witness( + returnType ?? circuitValue(null), + runCalledContract, + true + ) + : runCalledContract(); + + // we're back in the _caller's_ circuit now, where we assert stuff about the method call + + // connect party to our own. outside Circuit.witness so compile knows about it + party.body.callDepth = parentParty.body.callDepth + 1; + party.parent = parentParty; + parentParty.children.push(party); + + // assert that we really called the right zkapp + party.body.publicKey.assertEquals(this.address); + party.body.tokenId.assertEquals(this.self.body.tokenId); + + // assert that the inputs & outputs we have match what the callee put on its callData + let argsFields = methodArgumentsToFields(methodIntf, actualArgs); + if (returnType) argsFields.push(...returnType.toFields(result)); + argsFields.push(blindingValue); + let callData = Poseidon.hash(argsFields); + // Circuit.asProver(() => { + // console.log('callData (caller) ' + callData); + // }); + party.body.callData.assertEquals(callData); + return result; + } ); - // we're in the _caller's_ circuit now, where we assert stuff about the method call - - // connect party to our own. outside Circuit.witness so compile knows about it - party.body.callDepth = parentParty.body.callDepth + 1; - party.parent = parentParty; - parentParty.children.push(party); - - // assert that we really called the right zkapp - party.body.publicKey.assertEquals(this.address); - party.body.tokenId.assertEquals(this.self.body.tokenId); - - // assert that the inputs & outputs we have match what the callee put on its callData - let argsFields = methodArgumentsToFields(methodIntf, actualArgs); - if (returnType) argsFields.push(...returnType.toFields(result)); - argsFields.push(blindingValue); - let callData = Poseidon.hash(argsFields); - // Circuit.asProver(() => { - // console.log('callData (caller) ' + callData); - // }); - party.body.callData.assertEquals(callData); return result; }; } From 48b9a7a8aad8397c313f11d5e254d9b122a25b14 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 21 Jul 2022 21:30:08 +0200 Subject: [PATCH 11/19] change composability example to do nested calls --- src/examples/zkapps/composability.ts | 50 +++++++++++++++++++--------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/src/examples/zkapps/composability.ts b/src/examples/zkapps/composability.ts index 1debe45ca..fd834f2de 100644 --- a/src/examples/zkapps/composability.ts +++ b/src/examples/zkapps/composability.ts @@ -2,7 +2,6 @@ * zkApps composability */ import { - Experimental, Field, isReady, method, @@ -19,24 +18,33 @@ const doProofs = true; await isReady; -// contract which can add two numbers and return the result -class CallableAdd extends SmartContract { - @method add(x: Field, y: Field): Field { - // compute result - return x.add(y); +// contract which can add 1 to a number +class Incrementer extends SmartContract { + @method increment(x: Field): Field { + return x.add(1); } } -let callableKey = PrivateKey.random(); -let callableAddress = callableKey.toPublicKey(); +// contract which can add two numbers, plus 1, and return the result +// incrementing by one is outsourced to another contract (it's cleaner that way, we want to stick to the single responsibility principle) +class Adder extends SmartContract { + @method addPlus1(x: Field, y: Field): Field { + // compute result + let sum = x.add(y); + // call the other contract to increment + let incrementer = new Incrementer(incrementerAddress); + return incrementer.increment(sum); + } +} +// contract which calls the Adder, stores the result on chain & emits an event class Caller extends SmartContract { @state(Field) sum = State(); events = { sum: Field }; @method callAddAndEmit(x: Field, y: Field) { - let adder = new CallableAdd(callableAddress); - let sum = adder.add(x, y); + let adder = new Adder(adderAddress); + let sum = adder.addPlus1(x, y); this.emitEvent('sum', sum); this.sum.set(sum); } @@ -50,16 +58,24 @@ Mina.setActiveInstance(Local); // a test account that pays all the fees, and puts additional funds into the zkapp let feePayer = Local.testAccounts[0].privateKey; -// the zkapp account +// the first contract's address +let incrementerKey = PrivateKey.random(); +let incrementerAddress = incrementerKey.toPublicKey(); +// the second contract's address +let adderKey = PrivateKey.random(); +let adderAddress = adderKey.toPublicKey(); +// the third contract's address let zkappKey = PrivateKey.random(); let zkappAddress = zkappKey.toPublicKey(); let zkapp = new Caller(zkappAddress); -let callableZkapp = new CallableAdd(callableAddress); +let callableZkapp = new Adder(adderAddress); if (doProofs) { - console.log('compile (callee)'); - await CallableAdd.compile(callableAddress); + console.log('compile (incrementer)'); + await Incrementer.compile(incrementerAddress); + console.log('compile (adder)'); + await Adder.compile(adderAddress); console.log('compile (caller)'); await Caller.compile(zkappAddress); } @@ -72,17 +88,18 @@ let tx = await Mina.transaction(feePayer, () => { ...Permissions.default(), editState: Permissions.proofOrSignature(), }); - callableZkapp.deploy({ zkappKey: callableKey }); + callableZkapp.deploy({ zkappKey: adderKey }); }); tx.send(); console.log('call interaction'); tx = await Mina.transaction(feePayer, () => { + // we just call one contract here, nothing special to do zkapp.callAddAndEmit(Field(5), Field(6)); if (!doProofs) zkapp.sign(zkappKey); }); if (doProofs) { - console.log('proving (2 proofs: caller + callee)'); + console.log('proving (3 proofs.. can take a bit!)'); await tx.prove(); } @@ -90,4 +107,5 @@ console.dir(JSON.parse(tx.toJSON()), { depth: 5 }); tx.send(); +// should hopefully be 12 since we added 5 + 6 + 1 console.log('state: ' + zkapp.sum.get()); From bcfab0fe8d069640890433573f70e256f9503e45 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 22 Jul 2022 11:28:20 +0200 Subject: [PATCH 12/19] fix nested composability by witnessing calls hash --- src/lib/party.ts | 13 ++++++++----- src/lib/zkapp.ts | 9 +++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/lib/party.ts b/src/lib/party.ts index 51cce8f53..361c35150 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -508,7 +508,7 @@ class Party implements Types.Party { lazyAuthorization: LazySignature | LazyProof | undefined = undefined; account: Precondition.Account; network: Precondition.Network; - children: Party[] = []; + children: { party: Party; calls?: Field }[] = []; parent: Party | undefined = undefined; private isSelf: boolean; @@ -821,7 +821,8 @@ const CallForest = { let parties = []; for (let party of forest) { party.body.callDepth = depth; - parties.push(party, ...CallForest.toFlatList(party.children, depth + 1)); + let children = party.children.map((c) => c.party); + parties.push(party, ...CallForest.toFlatList(children, depth + 1)); } return parties; }, @@ -835,8 +836,10 @@ const CallForest = { // hashes a party's children (and their children, and ...) to compute the `calls` field of ZkappPublicInput hashChildren(parent: Party) { let stackHash = CallForest.emptyHash(); - for (let party of parent.children.reverse()) { - let calls = CallForest.hashChildren(party); + for (let { party, calls } of parent.children.reverse()) { + // only compute calls if it's not there yet -- + // this gives us the flexibility to witness only direct children of a zkApp + calls ??= CallForest.hashChildren(party); let nodeHash = hashWithPrefix(prefixes.partyNode, [party.hash(), calls]); stackHash = hashWithPrefix(prefixes.partyCons, [nodeHash, stackHash]); } @@ -848,7 +851,7 @@ function createChildParty(parent: Party, childAddress: PublicKey) { let child = Party.defaultParty(childAddress); child.body.callDepth = parent.body.callDepth + 1; child.parent = parent; - parent.children.push(child); + parent.children.push({ party: child, calls: undefined }); return child; } diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 9ba9fe87d..006514eb2 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -27,6 +27,7 @@ import { Events, partyToPublicInput, Authorization, + CallForest, } from './party'; import { PrivateKey, PublicKey } from './signature'; import * as Mina from './mina'; @@ -267,7 +268,6 @@ function wrapMethod( // if we're here, this method was called inside _another_ smart contract method let parentParty = smartContractContext.get().this.self; let methodCallDepth = smartContractContext.get().methodCallDepth; - console.log({ methodCallDepth }); let [, result] = smartContractContext.runWith( { this: this, methodCallDepth: methodCallDepth + 1 }, () => { @@ -368,7 +368,12 @@ function wrapMethod( // connect party to our own. outside Circuit.witness so compile knows about it party.body.callDepth = parentParty.body.callDepth + 1; party.parent = parentParty; - parentParty.children.push(party); + // beware: we don't include the callee's children in the caller circuit + // nothing is asserted about them -- it's the callee's task to check their children + let calls = Circuit.witness(Field, () => + CallForest.hashChildren(party) + ); + parentParty.children.push({ party, calls }); // assert that we really called the right zkapp party.body.publicKey.assertEquals(this.address); From 8664529f95b15cf6ed9885d88a1e8e17ab44523c Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 22 Jul 2022 11:28:51 +0200 Subject: [PATCH 13/19] fix example: deploy all contracts --- src/examples/zkapps/composability.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/examples/zkapps/composability.ts b/src/examples/zkapps/composability.ts index fd834f2de..025004991 100644 --- a/src/examples/zkapps/composability.ts +++ b/src/examples/zkapps/composability.ts @@ -69,7 +69,8 @@ let zkappKey = PrivateKey.random(); let zkappAddress = zkappKey.toPublicKey(); let zkapp = new Caller(zkappAddress); -let callableZkapp = new Adder(adderAddress); +let adderZkapp = new Adder(adderAddress); +let incrementerZkapp = new Incrementer(incrementerAddress); if (doProofs) { console.log('compile (incrementer)'); @@ -82,13 +83,17 @@ if (doProofs) { console.log('deploy'); let tx = await Mina.transaction(feePayer, () => { - Party.fundNewAccount(feePayer, { initialBalance: Mina.accountCreationFee() }); + // TODO: enable funding multiple accounts properly + Party.fundNewAccount(feePayer, { + initialBalance: Mina.accountCreationFee().add(Mina.accountCreationFee()), + }); zkapp.deploy({ zkappKey }); zkapp.setPermissions({ ...Permissions.default(), editState: Permissions.proofOrSignature(), }); - callableZkapp.deploy({ zkappKey: adderKey }); + adderZkapp.deploy({ zkappKey: adderKey }); + incrementerZkapp.deploy({ zkappKey: incrementerKey }); }); tx.send(); From e974cb72907d8e850dd28c5b63a9cda039d9be2c Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 22 Jul 2022 11:53:09 +0200 Subject: [PATCH 14/19] clean up comments --- src/examples/zkapps/composability.ts | 10 ++++--- src/lib/zkapp.ts | 45 +++++----------------------- 2 files changed, 14 insertions(+), 41 deletions(-) diff --git a/src/examples/zkapps/composability.ts b/src/examples/zkapps/composability.ts index 025004991..cdd9c1aa1 100644 --- a/src/examples/zkapps/composability.ts +++ b/src/examples/zkapps/composability.ts @@ -88,10 +88,12 @@ let tx = await Mina.transaction(feePayer, () => { initialBalance: Mina.accountCreationFee().add(Mina.accountCreationFee()), }); zkapp.deploy({ zkappKey }); - zkapp.setPermissions({ - ...Permissions.default(), - editState: Permissions.proofOrSignature(), - }); + if (!doProofs) { + zkapp.setPermissions({ + ...Permissions.default(), + editState: Permissions.proofOrSignature(), + }); + } adderZkapp.deploy({ zkappKey: adderKey }); incrementerZkapp.deploy({ zkappKey: incrementerKey }); }); diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 006514eb2..c48e696ad 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -133,7 +133,6 @@ function wrapMethod( return function wrappedMethod(this: SmartContract, ...actualArgs: any[]) { cleanStatePrecondition(this); if (!smartContractContext.has()) { - // console.log('wrapped method', methodIntf.methodName); return smartContractContext.runWith( { this: this, methodCallDepth: 0 }, () => { @@ -158,7 +157,6 @@ function wrapMethod( let blindingValue = Circuit.witness(Field, getBlindingValue); // it's also good if we prove that we use the same blinding value across the method // that's why we pass the variable_ (not the constant) into a new context - // console.log('blinding value (circuit)', blindingValue); let context = memoizationContext() ?? { memoized: [], currentIndex: 0, @@ -178,15 +176,8 @@ function wrapMethod( } argsFields.push(blindingValue); party.body.callData = Poseidon.hash(argsFields); - // Circuit.asProver(() => { - // console.log('callData (checked) ' + party.body.callData); - // }); // connect the public input to the party & child parties we created - // Circuit.asProver(() => { - // console.log('party (prover)'); - // console.dir(party.toJSON(), { depth: 5 }); - // }); checkPublicInput(publicInput, party); // check the self party right after calling the method @@ -218,7 +209,6 @@ function wrapMethod( // we run this in a "memoization context" so that we can remember witnesses for reuse when proving let blindingValue = getBlindingValue(); - // console.log('blinding value (tx)', blindingValue); let [{ memoized }, result] = memoizationContext.runWith( { memoized: [], @@ -231,15 +221,13 @@ function wrapMethod( ); assertStatePrecondition(this); - // connects our input + result with callData, so this method can be called - // TODO include result + blinding value + // connect our input + result with callData, so this method can be called let argsFields = methodArgumentsToFields(methodIntf, actualArgs); if (methodIntf.returnType) { argsFields.push(...methodIntf.returnType.toFields(result)); } argsFields.push(blindingValue); party.body.callData = Poseidon.hash(argsFields); - // console.log('callData (transaction) ' + party.body.callData);s if (!Authorization.hasAny(party)) { Authorization.setLazyProof(party, { @@ -260,26 +248,15 @@ function wrapMethod( } )[1]; } - // console.log( - // 'wrapped method', - // methodIntf.methodName, - // '(called by another one)' - // ); // if we're here, this method was called inside _another_ smart contract method let parentParty = smartContractContext.get().this.self; let methodCallDepth = smartContractContext.get().methodCallDepth; let [, result] = smartContractContext.runWith( { this: this, methodCallDepth: methodCallDepth + 1 }, () => { - // we just reuse the blinding value of the caller for the callee - let blindingValue = getBlindingValue(); - // console.log('blinding value (caller)', blindingValue); - - // witness the result of calling `add` - let { returnType } = methodIntf; - - // if the result is not undefined but there's no known returnType, the returnType was probably not annotated properly, + // if the call result is not undefined but there's no known returnType, the returnType was probably not annotated properly, // so we have to explain to the user how to do that + let { returnType } = methodIntf; let noReturnTypeError = `To return a result from ${methodIntf.methodName}() inside another zkApp, you need to declare the return type.\n` + `This can be done by annotating the type at the end of the function signature. For example:\n\n` + @@ -295,11 +272,12 @@ function wrapMethod( ) { throw Error(noReturnTypeError); } + // we just reuse the blinding value of the caller for the callee + let blindingValue = getBlindingValue(); let runCalledContract = () => { let constantArgs = methodArgumentsToConstant(methodIntf, actualArgs); let constantBlindingValue = blindingValue.toConstant(); - // console.log('blinding value (callee)', constantBlindingValue); let party = this.self; // the line above adds the callee's self party into the wrong place in the transaction structure // so we remove it again @@ -333,7 +311,6 @@ function wrapMethod( argsFields.push(...resultFields); argsFields.push(constantBlindingValue); party.body.callData = Poseidon_.hash(argsFields, false); - // console.log('callData (callee) ' + party.body.callData); if (!Authorization.hasAny(party)) { Authorization.setLazyProof(party, { @@ -352,8 +329,8 @@ function wrapMethod( }; // we have to run the called contract inside a witness block, to not affect the caller's circuit - // however, if this is another contract called by a called contract, then we're already in a witness block, - // and shouldn't open another one + // however, if this is a nested call -- the caller is already called by another contract --, + // then we're already in a witness block, and shouldn't open another one let { party, result } = methodCallDepth === 0 ? Party.witness( @@ -365,7 +342,7 @@ function wrapMethod( // we're back in the _caller's_ circuit now, where we assert stuff about the method call - // connect party to our own. outside Circuit.witness so compile knows about it + // connect party to our own. outside Circuit.witness so compile knows the right structure when hashing children party.body.callDepth = parentParty.body.callDepth + 1; party.parent = parentParty; // beware: we don't include the callee's children in the caller circuit @@ -384,9 +361,6 @@ function wrapMethod( if (returnType) argsFields.push(...returnType.toFields(result)); argsFields.push(blindingValue); let callData = Poseidon.hash(argsFields); - // Circuit.asProver(() => { - // console.log('callData (caller) ' + callData); - // }); party.body.callData.assertEquals(callData); return result; } @@ -589,8 +563,6 @@ class SmartContract { // run all methods to collect metadata like how many sequence events they use -- if we don't have this information yet // TODO: this could also be used to quickly perform any invariant checks on parties construction - // TODO: it would be even better to run the methods in "compile mode" here -- i.e. with variables which can't be read --, - // to be able to give quick errors when people try to depend on variables for constructing their circuit (https://github.com/o1-labs/snarkyjs/issues/224) static analyzeMethods(address: PublicKey) { let ZkappClass = this as typeof SmartContract; let instance = new ZkappClass(address); @@ -818,7 +790,6 @@ async function deploy( transactionFee, }); } - // TODO modifying the json after calling to ocaml would avoid extra vk serialization.. but need to compute vk hash return tx.sign().toJSON(); } From 4806e3a6f278aaccf38850f9eff9806b6e6ce4c2 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 22 Jul 2022 11:59:46 +0200 Subject: [PATCH 15/19] delete manual composability example --- src/examples/zkapps/composability_manual.ts | 146 -------------------- 1 file changed, 146 deletions(-) delete mode 100644 src/examples/zkapps/composability_manual.ts diff --git a/src/examples/zkapps/composability_manual.ts b/src/examples/zkapps/composability_manual.ts deleted file mode 100644 index 4d12702cd..000000000 --- a/src/examples/zkapps/composability_manual.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * how to do composability "by hand" - */ -import { - Circuit, - Experimental, - Field, - isReady, - method, - Mina, - Party, - Permissions, - Poseidon, - PrivateKey, - SmartContract, - state, - State, -} from 'snarkyjs'; - -const doProofs = true; - -await isReady; - -// contract which can add two numbers and return the result -class CallableAdd extends SmartContract { - @method add(x: Field, y: Field, blindingValue: Field) { - // compute result - let result = x.add(y); - - // store inputs + result in callData - // the blindingValue is necessary because otherwise, putting this on the transaction would leak information about the private inputs - this.self.body.callData = Poseidon.hash([blindingValue, x, y, result]); - return result; - } -} - -let callableKey = PrivateKey.random(); -let callableAddress = callableKey.toPublicKey(); -let callableTokenId = Field.one; - -class Caller extends SmartContract { - @state(Field) sum = State(); - events = { sum: Field }; - - @method callAddAndEmit(x: Field, y: Field) { - let input: [Field, Field] = [x, y]; - - // we have to call this.self to create our party first! - let selfParty = this.self; - - // witness the result of calling `add` - let blindingValue = Experimental.memoizeWitness(Field, () => - Field.random() - ); - let { party, result } = Party.witness( - Field, - () => { - // here we will call the other method - // let adder = new CallableAdd(callableAddress); - // let result = adder.add(...input, blindingValue); - let party = Party.defaultParty(callableAddress); - let [x0, y0] = [x.toConstant(), y.toConstant()]; - let result = x0.add(y0); - // store inputs + result in callData - // the blindingValue is necessary because otherwise, putting this on the transaction would leak information about the private inputs - - party.body.callData = Poseidon.hash([blindingValue, x0, y0, result]); - - party.lazyAuthorization = { - kind: 'lazy-proof', - methodName: 'add', - args: [x0, y0, blindingValue], - previousProofs: [], - ZkappClass: CallableAdd, - memoized: [], - }; - return { party, result }; - }, - true - ); - // connect party to our own. outside Circuit.witness so compile knows about it - party.body.callDepth = selfParty.body.callDepth + 1; - party.parent = selfParty; - selfParty.children.push(party); - - // assert that we really called the right zkapp - party.body.publicKey.assertEquals(callableAddress); - party.body.tokenId.assertEquals(callableTokenId); - - // assert that the inputs & outputs we have match what the callee put on its callData - let callData = Poseidon.hash([blindingValue, ...input, result]); - party.body.callData.assertEquals(callData); - - // finally, we proved that we called that other zkapp, with the inputs `x`, `y` and the output `result` - // now we can do anything with the result - this.emitEvent('sum', result); - this.sum.set(result); - } -} - -// script to deploy zkapps and do interactions - -let Local = Mina.LocalBlockchain(); -Mina.setActiveInstance(Local); - -// a test account that pays all the fees, and puts additional funds into the zkapp -let feePayer = Local.testAccounts[0].privateKey; - -// the zkapp account -let zkappKey = PrivateKey.random(); -let zkappAddress = zkappKey.toPublicKey(); - -let zkapp = new Caller(zkappAddress); -let callableZkapp = new CallableAdd(callableAddress); - -if (doProofs) { - console.log('compile'); - await CallableAdd.compile(callableAddress); - await Caller.compile(zkappAddress); -} - -console.log('deploy'); -let tx = await Mina.transaction(feePayer, () => { - Party.fundNewAccount(feePayer, { initialBalance: Mina.accountCreationFee() }); - zkapp.deploy({ zkappKey }); - zkapp.setPermissions({ - ...Permissions.default(), - editState: Permissions.proofOrSignature(), - }); - callableZkapp.deploy({ zkappKey: callableKey }); -}); -tx.send(); - -console.log('call interaction'); -tx = await Mina.transaction(feePayer, () => { - zkapp.callAddAndEmit(Field(5), Field(6)); - if (!doProofs) zkapp.sign(zkappKey); -}); -console.log('proving'); -if (doProofs) await tx.prove(); - -console.dir(JSON.parse(tx.toJSON()), { depth: 5 }); - -tx.send(); - -console.log('state: ' + zkapp.sum.get()); From 4ba97c142f526420ae34c2a3965d64d6c0283943 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 22 Jul 2022 14:06:52 +0200 Subject: [PATCH 16/19] minor change to party.test.ts --- src/lib/party.test.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/lib/party.test.ts b/src/lib/party.test.ts index be65726be..97fd0b31c 100644 --- a/src/lib/party.test.ts +++ b/src/lib/party.test.ts @@ -7,7 +7,6 @@ import { shutdown, Field, PublicKey, - CircuitValue, Mina, Experimental, } from '../../dist/server'; @@ -52,7 +51,7 @@ describe('party', () => { // TODO remove restriction "This function can't be run outside of a checked computation." Circuit.runAndCheck(() => { let hash = party.hash(); - expect(isLikeField(hash)).toBeTruthy(); + expect(isField(hash)).toBeTruthy(); // if we clone the party, hash should be the same let party2 = Party.clone(party); @@ -95,13 +94,6 @@ describe('party', () => { // to check that we got something that looks like a Field // note: `instanceof Field` doesn't work -function isLikeField(x: any) { - return ( - !(x instanceof CircuitValue) && - 'equals' in x && - 'toFields' in x && - 'add' in x && - 'mul' in x && - Array.isArray((x as any).value) - ); +function isField(x: any): x is Field { + return x?.constructor === Field.one.constructor; } From 2b30db84f90ce7b1e0f13983ebf8353634213619 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 25 Jul 2022 18:40:34 +0200 Subject: [PATCH 17/19] add method index + name to callData hash --- src/lib/zkapp.ts | 55 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index c48e696ad..48048f63c 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -56,6 +56,7 @@ import { assertStatePrecondition, cleanStatePrecondition } from './state'; import { Types } from '../snarky/types'; import { Context } from './global-context'; import { Poseidon } from './hash'; +import * as Encoding from './encoding'; // external API export { @@ -111,6 +112,7 @@ function method( ); if (isAsFields(returnType)) methodEntry.returnType = returnType; ZkappClass._methods ??= []; + let methodIndex = ZkappClass._methods.length; ZkappClass._methods.push(methodEntry); ZkappClass._maxProofsVerified ??= 0; ZkappClass._maxProofsVerified = Math.max( @@ -118,7 +120,7 @@ function method( methodEntry.proofArgs.length ); let func = descriptor.value; - descriptor.value = wrapMethod(func, ZkappClass, methodEntry); + descriptor.value = wrapMethod(func, ZkappClass, methodEntry, methodIndex); } let smartContractContext = @@ -128,7 +130,8 @@ let smartContractContext = function wrapMethod( method: Function, ZkappClass: typeof SmartContract, - methodIntf: MethodInterface + methodIntf: MethodInterface, + methodIndex: number ) { return function wrappedMethod(this: SmartContract, ...actualArgs: any[]) { cleanStatePrecondition(this); @@ -167,15 +170,18 @@ function wrapMethod( ); // connects our input + result with callData, so this method can be called - let argsFields = methodArgumentsToFields( + let callDataFields = methodArgumentsToFields( methodIntf, actualArgs ); if (methodIntf.returnType) { - argsFields.push(...methodIntf.returnType.toFields(result)); + callDataFields.push( + ...methodIntf.returnType.toFields(result) + ); } - argsFields.push(blindingValue); - party.body.callData = Poseidon.hash(argsFields); + callDataFields.push(getMethodId(methodIntf, methodIndex)); + callDataFields.push(blindingValue); + party.body.callData = Poseidon.hash(callDataFields); // connect the public input to the party & child parties we created checkPublicInput(publicInput, party); @@ -222,12 +228,16 @@ function wrapMethod( assertStatePrecondition(this); // connect our input + result with callData, so this method can be called - let argsFields = methodArgumentsToFields(methodIntf, actualArgs); + let callDataFields = methodArgumentsToFields( + methodIntf, + actualArgs + ); if (methodIntf.returnType) { - argsFields.push(...methodIntf.returnType.toFields(result)); + callDataFields.push(...methodIntf.returnType.toFields(result)); } - argsFields.push(blindingValue); - party.body.callData = Poseidon.hash(argsFields); + callDataFields.push(getMethodId(methodIntf, methodIndex)); + callDataFields.push(blindingValue); + party.body.callData = Poseidon.hash(callDataFields); if (!Authorization.hasAny(party)) { Authorization.setLazyProof(party, { @@ -307,10 +317,14 @@ function wrapMethod( } // store inputs + result in callData - let argsFields = methodArgumentsToFields(methodIntf, constantArgs); - argsFields.push(...resultFields); - argsFields.push(constantBlindingValue); - party.body.callData = Poseidon_.hash(argsFields, false); + let callDataFields = methodArgumentsToFields( + methodIntf, + constantArgs + ); + callDataFields.push(...resultFields); + callDataFields.push(getMethodId(methodIntf, methodIndex)); + callDataFields.push(constantBlindingValue); + party.body.callData = Poseidon_.hash(callDataFields, false); if (!Authorization.hasAny(party)) { Authorization.setLazyProof(party, { @@ -357,10 +371,11 @@ function wrapMethod( party.body.tokenId.assertEquals(this.self.body.tokenId); // assert that the inputs & outputs we have match what the callee put on its callData - let argsFields = methodArgumentsToFields(methodIntf, actualArgs); - if (returnType) argsFields.push(...returnType.toFields(result)); - argsFields.push(blindingValue); - let callData = Poseidon.hash(argsFields); + let callDataFields = methodArgumentsToFields(methodIntf, actualArgs); + if (returnType) callDataFields.push(...returnType.toFields(result)); + callDataFields.push(getMethodId(methodIntf, methodIndex)); + callDataFields.push(blindingValue); + let callData = Poseidon.hash(callDataFields); party.body.callData.assertEquals(callData); return result; } @@ -375,6 +390,10 @@ function checkPublicInput({ party, calls }: ZkappPublicInput, self: Party) { calls.assertEquals(otherInput.calls); } +function getMethodId({ methodName }: MethodInterface, methodIndex: number) { + return Encoding.stringToFields(`${methodIndex};${methodName}`)[0]; +} + /** * The main zkapp class. To write a zkapp, extend this class as such: * From 7b999e3f3e7d563d2ba5fc9c112382b410eb2b96 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 26 Jul 2022 09:44:28 +0200 Subject: [PATCH 18/19] minor --- src/lib/circuit_value.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index e5976d51b..0f766bd6f 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -576,7 +576,7 @@ function memoizeWitness(type: AsFieldElements, compute: () => T) { function getBlindingValue() { if (!memoizationContext.has()) return Field.random(); let context = memoizationContext.get(); - if (context?.blindingValue === undefined) { + if (context.blindingValue === undefined) { context.blindingValue = Field.random(); } return context.blindingValue; From 58331ea5b9f259abca91c9d4cb7ab3aae0091ade Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 1 Aug 2022 11:07:03 +0200 Subject: [PATCH 19/19] compute callData s.t. it fixes method signature --- src/lib/proof_system.ts | 21 ++++++---- src/lib/zkapp.ts | 91 ++++++++++++++++++++++++++--------------- 2 files changed, 70 insertions(+), 42 deletions(-) diff --git a/src/lib/proof_system.ts b/src/lib/proof_system.ts index 9af2471c1..e2f676bbc 100644 --- a/src/lib/proof_system.ts +++ b/src/lib/proof_system.ts @@ -25,7 +25,7 @@ export { emptyWitness, synthesizeMethodArguments, methodArgumentsToConstant, - methodArgumentsToFields, + methodArgumentTypesAndValues, isAsFields, snarkContext, inProver, @@ -490,23 +490,28 @@ function methodArgumentsToConstant( } return constArgs; } -function methodArgumentsToFields( + +type TypeAndValue = { type: AsFieldElements; value: T }; + +function methodArgumentTypesAndValues( { allArgs, proofArgs, witnessArgs }: MethodInterface, - args: any[] + args: unknown[] ) { - let fields: Field[] = []; + let typesAndValues: TypeAndValue[] = []; for (let i = 0; i < allArgs.length; i++) { let arg = args[i]; let { type, index } = allArgs[i]; if (type === 'witness') { - fields.push(...witnessArgs[index].toFields(arg)); + typesAndValues.push({ type: witnessArgs[index], value: arg }); } else { let Proof = proofArgs[index]; - let publicInput = getPublicInputType(Proof).toFields(arg.publicInput); - fields.push(...publicInput); + typesAndValues.push({ + type: getPublicInputType(Proof), + value: (arg as Proof).publicInput, + }); } } - return fields; + return typesAndValues; } function emptyValue(type: AsFieldElements) { diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 07c54d9bc..8c479819d 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -50,8 +50,8 @@ import { inProver, inAnalyze, methodArgumentsToConstant, - methodArgumentsToFields, isAsFields, + methodArgumentTypesAndValues, } from './proof_system'; import { assertStatePrecondition, cleanStatePrecondition } from './state'; import { Types } from '../snarky/types'; @@ -113,7 +113,6 @@ function method( ); if (isAsFields(returnType)) methodEntry.returnType = returnType; ZkappClass._methods ??= []; - let methodIndex = ZkappClass._methods.length; ZkappClass._methods.push(methodEntry); ZkappClass._maxProofsVerified ??= 0; ZkappClass._maxProofsVerified = Math.max( @@ -121,7 +120,7 @@ function method( methodEntry.proofArgs.length ); let func = descriptor.value; - descriptor.value = wrapMethod(func, ZkappClass, methodEntry, methodIndex); + descriptor.value = wrapMethod(func, ZkappClass, methodEntry); } let smartContractContext = @@ -131,8 +130,7 @@ let smartContractContext = function wrapMethod( method: Function, ZkappClass: typeof SmartContract, - methodIntf: MethodInterface, - methodIndex: number + methodIntf: MethodInterface ) { return function wrappedMethod(this: SmartContract, ...actualArgs: any[]) { cleanStatePrecondition(this); @@ -171,17 +169,12 @@ function wrapMethod( ); // connects our input + result with callData, so this method can be called - let callDataFields = methodArgumentsToFields( + let callDataFields = computeCallData( methodIntf, - actualArgs + actualArgs, + result, + blindingValue ); - if (methodIntf.returnType) { - callDataFields.push( - ...methodIntf.returnType.toFields(result) - ); - } - callDataFields.push(getMethodId(methodIntf, methodIndex)); - callDataFields.push(blindingValue); party.body.callData = Poseidon.hash(callDataFields); // connect the public input to the party & child parties we created @@ -229,15 +222,12 @@ function wrapMethod( assertStatePrecondition(this); // connect our input + result with callData, so this method can be called - let callDataFields = methodArgumentsToFields( + let callDataFields = computeCallData( methodIntf, - actualArgs + actualArgs, + result, + blindingValue ); - if (methodIntf.returnType) { - callDataFields.push(...methodIntf.returnType.toFields(result)); - } - callDataFields.push(getMethodId(methodIntf, methodIndex)); - callDataFields.push(blindingValue); party.body.callData = Poseidon.hash(callDataFields); if (!Authorization.hasAny(party)) { @@ -307,24 +297,21 @@ function wrapMethod( ); assertStatePrecondition(this); - let resultFields: Field[] = []; if (result !== undefined) { if (returnType === undefined) { throw Error(noReturnTypeError); } else { result = toConstant(returnType, result); - resultFields = returnType.toFields(result); } } // store inputs + result in callData - let callDataFields = methodArgumentsToFields( + let callDataFields = computeCallData( methodIntf, - constantArgs + constantArgs, + result, + constantBlindingValue ); - callDataFields.push(...resultFields); - callDataFields.push(getMethodId(methodIntf, methodIndex)); - callDataFields.push(constantBlindingValue); party.body.callData = Poseidon_.hash(callDataFields, false); if (!Authorization.hasAny(party)) { @@ -372,10 +359,12 @@ function wrapMethod( party.body.tokenId.assertEquals(this.self.body.tokenId); // assert that the inputs & outputs we have match what the callee put on its callData - let callDataFields = methodArgumentsToFields(methodIntf, actualArgs); - if (returnType) callDataFields.push(...returnType.toFields(result)); - callDataFields.push(getMethodId(methodIntf, methodIndex)); - callDataFields.push(blindingValue); + let callDataFields = computeCallData( + methodIntf, + actualArgs, + result, + blindingValue + ); let callData = Poseidon.hash(callDataFields); party.body.callData.assertEquals(callData); return result; @@ -391,8 +380,42 @@ function checkPublicInput({ party, calls }: ZkappPublicInput, self: Party) { calls.assertEquals(otherInput.calls); } -function getMethodId({ methodName }: MethodInterface, methodIndex: number) { - return Encoding.stringToFields(`${methodIndex};${methodName}`)[0]; +/** + * compute fields to be hashed as callData, in a way that the hash & circuit changes whenever + * the method signature changes, i.e., the argument / return types represented as lists of field elements and the methodName. + * see https://github.com/o1-labs/snarkyjs/issues/303#issuecomment-1196441140 + */ +function computeCallData( + methodIntf: MethodInterface, + argumentValues: any[], + returnValue: any, + blindingValue: Field +) { + let { returnType, methodName } = methodIntf; + let args = methodArgumentTypesAndValues(methodIntf, argumentValues); + let argSizesAndFields: Field[][] = args.map(({ type, value }) => [ + Field(type.sizeInFields()), + ...type.toFields(value), + ]); + let totalArgSize = Field( + args.map(({ type }) => type.sizeInFields()).reduce((s, t) => s + t) + ); + let totalArgFields = argSizesAndFields.flat(); + let returnSize = Field(returnType?.sizeInFields() ?? 0); + let returnFields = returnType?.toFields(returnValue) ?? []; + let methodNameFields = Encoding.stringToFields(methodName); + return [ + // we have to encode the sizes of arguments / return value, so that fields can't accidentally shift + // from one argument to another, or from arguments to the return value, or from the return value to the method name + totalArgSize, + ...totalArgFields, + returnSize, + ...returnFields, + // we don't have to encode the method name size because the blinding value is fixed to one field element, + // so method name fields can't accidentally become the blinding value and vice versa + ...methodNameFields, + blindingValue, + ]; } /**