Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zkApp composability #297

Merged
merged 22 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a20146b
add account.isNew
mitschabaude Jul 15, 2022
c7a0e56
WIP manual composability
mitschabaude Jul 20, 2022
9fc8d3a
fix public key from fields, remove debug logs
mitschabaude Jul 20, 2022
ec7ac47
use method name for lazy proof, skip party.check
mitschabaude Jul 20, 2022
89a55d2
get manual composability example working
mitschabaude Jul 20, 2022
c56d0c3
support memoizing witnesses in methods for prover
mitschabaude Jul 21, 2022
f669f6c
composability mvp
mitschabaude Jul 21, 2022
4ea5c05
include return value in callData hash
mitschabaude Jul 21, 2022
504f663
add random value into the callData hash for hiding
mitschabaude Jul 21, 2022
f8536e6
start handling calls inside calls
mitschabaude Jul 21, 2022
48b9a7a
change composability example to do nested calls
mitschabaude Jul 21, 2022
bcfab0f
fix nested composability by witnessing calls hash
mitschabaude Jul 22, 2022
8664529
fix example: deploy all contracts
mitschabaude Jul 22, 2022
e974cb7
clean up comments
mitschabaude Jul 22, 2022
4806e3a
delete manual composability example
mitschabaude Jul 22, 2022
4ba97c1
minor change to party.test.ts
mitschabaude Jul 22, 2022
2b30db8
add method index + name to callData hash
mitschabaude Jul 25, 2022
7b999e3
minor
mitschabaude Jul 26, 2022
4a93ee8
Merge branch 'feature/composability' into tmp
mitschabaude Jul 26, 2022
e9964a8
Merge branch 'feature/is-new' into feature/composability-rebased
mitschabaude Jul 27, 2022
56a1283
Merge branch 'main' into feature/composability-rebased
mitschabaude Aug 1, 2022
58331ea
compute callData s.t. it fixes method signature
mitschabaude Aug 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
118 changes: 118 additions & 0 deletions src/examples/zkapps/composability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the example that demonstrates composability

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great intuitive example to demonstrate composability.

* zkApps composability
*/
import {
Field,
isReady,
method,
Mina,
Party,
Permissions,
PrivateKey,
SmartContract,
state,
State,
} from 'snarkyjs';

const doProofs = true;

await isReady;

// contract which can add 1 to a number
class Incrementer extends SmartContract {
@method increment(x: Field): Field {
return x.add(1);
}
}

// 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<Field>();
events = { sum: Field };

@method callAddAndEmit(x: Field, y: Field) {
let adder = new Adder(adderAddress);
let sum = adder.addPlus1(x, y);
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 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 adderZkapp = new Adder(adderAddress);
let incrementerZkapp = new Incrementer(incrementerAddress);

if (doProofs) {
console.log('compile (incrementer)');
await Incrementer.compile(incrementerAddress);
console.log('compile (adder)');
await Adder.compile(adderAddress);
console.log('compile (caller)');
await Caller.compile(zkappAddress);
}

console.log('deploy');
let tx = await Mina.transaction(feePayer, () => {
// TODO: enable funding multiple accounts properly
Party.fundNewAccount(feePayer, {
initialBalance: Mina.accountCreationFee().add(Mina.accountCreationFee()),
});
zkapp.deploy({ zkappKey });
if (!doProofs) {
zkapp.setPermissions({
...Permissions.default(),
editState: Permissions.proofOrSignature(),
});
}
adderZkapp.deploy({ zkappKey: adderKey });
incrementerZkapp.deploy({ zkappKey: incrementerKey });
});
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 (3 proofs.. can take a bit!)');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How long are the 3 proofs taking on your machine @mitschabaude?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

takes 42 sec for me

await tx.prove();
}

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());
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
50 changes: 49 additions & 1 deletion src/lib/circuit_value.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,7 +16,15 @@ export {
};

// internal API
export { cloneCircuitValue, circuitValueEquals, circuitArray };
export {
cloneCircuitValue,
circuitValueEquals,
circuitArray,
memoizationContext,
memoizeWitness,
getBlindingValue,
toConstant,
};

type AnyConstructor = new (...args: any) => any;

Expand Down Expand Up @@ -485,6 +494,10 @@ function circuitValueEquals<T>(a: T, b: T): boolean {
);
}

function toConstant<T>(type: AsFieldElements<T>, 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 <T, A extends AsFieldElements<T>>(
mask: Bool[],
Expand Down Expand Up @@ -533,3 +546,38 @@ Circuit.constraintSystem = function <T>(f: () => T) {
);
return result;
};

let memoizationContext = Context.create<{
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new global context that is used to remember prover values which are created during createTransaction, to be reused in the prover for the same method. In snarkyjs, only used for the blindingValue right now, but I figured it would be nice to also expose that capability to the outside world -- that's what the memoizeWitness function below is for.

memoized: Field[][];
currentIndex: number;
blindingValue: Field;
}>();

/**
* 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<T>(type: AsFieldElements<T>, 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);
});
}

function getBlindingValue() {
if (!memoizationContext.has()) return Field.random();
let context = memoizationContext.get();
if (context.blindingValue === undefined) {
context.blindingValue = Field.random();
}
return context.blindingValue;
}
14 changes: 3 additions & 11 deletions src/lib/party.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
shutdown,
Field,
PublicKey,
CircuitValue,
Mina,
Experimental,
} from '../../dist/server';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
57 changes: 41 additions & 16 deletions src/lib/party.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { circuitArray, circuitValue, cloneCircuitValue } from './circuit_value';
import {
circuitArray,
circuitValue,
cloneCircuitValue,
memoizationContext,
} from './circuit_value';
import {
Field,
Bool,
Expand Down Expand Up @@ -489,10 +494,12 @@ 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;
memoized: Field[][];
blindingValue: Field;
};

class Party implements Types.Party {
Expand All @@ -501,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;
Expand Down Expand Up @@ -768,7 +775,8 @@ class Party implements Types.Party {

static witness<T>(
type: AsFieldElements<T>,
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());
Expand All @@ -790,7 +798,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);
Expand All @@ -810,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;
},
Expand All @@ -824,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]);
}
Expand All @@ -837,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;
}

Expand Down Expand Up @@ -980,24 +994,35 @@ 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,
memoized,
blindingValue,
} = 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(
let [, [, proof]] = await snarkContext.runWithAsync(
{ inProver: true, witnesses: args },
() => provers[i](publicInputFields, previousProofs)
() =>
memoizationContext.runWithAsync(
{ memoized, currentIndex: 0, blindingValue },
() => provers[i](publicInputFields, previousProofs)
)
);
Authorization.setProof(party, Pickles.proofToBase64Transaction(proof));
let maxProofsVerified = ZkappClass._maxProofsVerified!;
Expand Down