Skip to content

Commit

Permalink
Merge pull request #296 from o1-labs/feature/composability-misc
Browse files Browse the repository at this point in the history
zkApp composability, pt 2: multiple proofs
  • Loading branch information
mitschabaude authored Jul 21, 2022
2 parents 8c7880b + 82460c1 commit f2c3360
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 22 deletions.
9 changes: 9 additions & 0 deletions src/examples/simple_zkapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ tx = await Mina.transaction(feePayer, () => {
if (doProofs) await tx.prove();
tx.send();

// pay more into the zkapp -- this doesn't need a proof
console.log('receive');
tx = await Mina.transaction(feePayer, () => {
let payerParty = Party.createSigned(feePayer);
payerParty.balance.subInPlace(8e9);
zkapp.balance.addInPlace(8e9);
});
tx.send();

console.log('payout');
tx = await Mina.transaction(feePayer, () => {
zkapp.payout(privilegedKey);
Expand Down
249 changes: 249 additions & 0 deletions src/examples/zkapps/simple_and_counter_zkapp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/**
* This is just two zkapps mixed together in one file, with their respective interactions bundled
* in the same transaction, to check that this actually works.
* -) "simple zkapp", testing state updates + events + account preconditions + child parties
* -) "counter rollup", testing state updates + sequence events / reducer
*/

import {
Field,
state,
State,
method,
UInt64,
PrivateKey,
SmartContract,
Mina,
Party,
isReady,
Permissions,
DeployArgs,
UInt32,
Bool,
PublicKey,
Circuit,
Experimental,
} from 'snarkyjs';

const doProofs = true;

await isReady;

const INCREMENT = Field.one;

let offchainStorage = {
pendingActions: [] as Field[][],
};

class CounterZkapp extends SmartContract {
// the "reducer" field describes a type of action that we can dispatch, and reduce later
reducer = Experimental.Reducer({ actionType: Field });

// on-chain version of our state. it will typically lag behind the
// version that's implicitly represented by the list of actions
@state(Field) counter = State<Field>();
// helper field to store the point in the action history that our on-chain state is at
@state(Field) actionsHash = State<Field>();

deploy(args: DeployArgs) {
super.deploy(args);
this.setPermissions({
...Permissions.default(),
editState: Permissions.proofOrSignature(),
editSequenceState: Permissions.proofOrSignature(),
});
this.actionsHash.set(Experimental.Reducer.initialActionsHash);
}

@method incrementCounter() {
this.reducer.dispatch(INCREMENT);
}

@method rollupIncrements() {
// get previous counter & actions hash, assert that they're the same as on-chain values
let counter = this.counter.get();
this.counter.assertEquals(counter);
let actionsHash = this.actionsHash.get();
this.actionsHash.assertEquals(actionsHash);

// compute the new counter and hash from pending actions
// remark: it's not feasible to pass in the pending actions as method arguments, because they have dynamic size
let { state: newCounter, actionsHash: newActionsHash } =
this.reducer.reduce(
offchainStorage.pendingActions,
// state type
Field,
// function that says how to apply an action
(state: Field, _action: Field) => {
return state.add(1);
},
{ state: counter, actionsHash }
);

// update on-chain state
this.counter.set(newCounter);
this.actionsHash.set(newActionsHash);
}
}

class SimpleZkapp extends SmartContract {
@state(Field) x = State<Field>();

events = {
update: Field,
payout: UInt64,
payoutReceiver: PublicKey,
};

deploy(args: DeployArgs) {
super.deploy(args);
this.setPermissions({
...Permissions.default(),
editState: Permissions.proofOrSignature(),
send: Permissions.proofOrSignature(),
});
this.balance.addInPlace(UInt64.fromNumber(initialBalance));
this.x.set(initialState);
}

@method update(y: Field) {
this.emitEvent('update', y);
let x = this.x.get();
this.x.assertEquals(x);
this.x.set(x.add(y));
}

/**
* This method allows a certain privileged account to claim half of the zkapp balance, but only once
* @param caller the privileged account
*/
@method payout(caller: PrivateKey) {
// check that caller is the privileged account
let callerAddress = caller.toPublicKey();
callerAddress.assertEquals(privilegedAddress);

// assert that the caller nonce is 0, and increment the nonce - this way, payout can only happen once
let callerParty = Experimental.createChildParty(this.self, callerAddress);
callerParty.account.nonce.assertEquals(UInt32.zero);
callerParty.body.incrementNonce = Bool(true);

// pay out half of the zkapp balance to the caller
let balance = this.account.balance.get();
this.account.balance.assertEquals(balance);
// FIXME UInt64.div() doesn't work on variables
let halfBalance = Circuit.witness(UInt64, () =>
balance.toConstant().div(2)
);
this.balance.subInPlace(halfBalance);
callerParty.balance.addInPlace(halfBalance);

// emit some events
this.emitEvent('payoutReceiver', callerAddress);
this.emitEvent('payout', halfBalance);
}
}

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();

// a special account that is allowed to pull out half of the zkapp balance, once
let privilegedKey = Local.testAccounts[1].privateKey;
let privilegedAddress = privilegedKey.toPublicKey();

let initialBalance = 10_000_000_000;
let initialState = Field(1);
let zkapp = new SimpleZkapp(zkappAddress);

let counterZkappKey = PrivateKey.random();
let counterZkappAddress = counterZkappKey.toPublicKey();
let counterZkapp = new CounterZkapp(counterZkappAddress);

if (doProofs) {
console.log('compile');
await SimpleZkapp.compile(zkappAddress);
await CounterZkapp.compile(counterZkappAddress);
}

console.log('deploy');
let tx = await Mina.transaction(feePayer, () => {
Party.fundNewAccount(feePayer, {
initialBalance: Mina.accountCreationFee().add(initialBalance),
});
zkapp.deploy({ zkappKey });
counterZkapp.deploy({ zkappKey: counterZkappKey });
});
tx.send();

console.log('initial state: ' + zkapp.x.get());
console.log(`initial balance: ${zkapp.account.balance.get().div(1e9)} MINA`);

console.log('update & dispatch increment');
tx = await Mina.transaction(feePayer, () => {
zkapp.update(Field(3));
counterZkapp.incrementCounter();
if (!doProofs) {
zkapp.sign(zkappKey);
counterZkapp.sign(counterZkappKey);
}
});
if (doProofs) await tx.prove();
tx.send();
offchainStorage.pendingActions.push([INCREMENT]);
console.log('state (on-chain): ' + counterZkapp.counter.get());
console.log('pending actions:', JSON.stringify(offchainStorage.pendingActions));

console.log('payout & rollup');
tx = await Mina.transaction(feePayer, () => {
zkapp.payout(privilegedKey);
counterZkapp.rollupIncrements();
if (!doProofs) {
zkapp.sign(zkappKey);
counterZkapp.sign(counterZkappKey);
}
});
if (doProofs) await tx.prove();
console.log(tx.toJSON());
tx.send();
offchainStorage.pendingActions = [];

console.log('final state: ' + zkapp.x.get());
console.log(`final balance: ${zkapp.account.balance.get().div(1e9)} MINA`);
console.log('state (on-chain): ' + counterZkapp.counter.get());
console.log('pending actions:', JSON.stringify(offchainStorage.pendingActions));

console.log('try to payout a second time..');
tx = await Mina.transaction(feePayer, () => {
zkapp.payout(privilegedKey);
if (!doProofs) zkapp.sign(zkappKey);
});
try {
if (doProofs) await tx.prove();
tx.send();
} catch (err: any) {
console.log('Transaction failed with error', err.message);
}

console.log('try to payout to a different account..');
try {
tx = await Mina.transaction(feePayer, () => {
zkapp.payout(Local.testAccounts[2].privateKey);
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();
} catch (err: any) {
console.log('Transaction failed with error', err.message);
}

console.log(
`should still be the same final balance: ${zkapp.account.balance
.get()
.div(1e9)} MINA`
);
8 changes: 5 additions & 3 deletions src/lib/circuit_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ abstract class CircuitValue {
Circuit.assertEqual(this, x);
}

isConstant(x: this) {
return x.toFields().every((x) => x.isConstant());
isConstant() {
return this.toFields().every((x) => x.isConstant());
}

static ofFields<T extends AnyConstructor>(
Expand Down Expand Up @@ -338,7 +338,7 @@ function circuitValue<T>(
options?: { customObjectKeys: string[] }
): AsFieldElements<T> {
let objectKeys =
typeof typeObj === 'object'
typeof typeObj === 'object' && typeObj !== null
? options?.customObjectKeys ?? Object.keys(typeObj).sort()
: [];

Expand Down Expand Up @@ -392,6 +392,8 @@ function circuitValue<T>(
};
}

// FIXME: the logic in here to check for obj.constructor.name actually doesn't work
// something that works is Field.one.constructor === obj.constructor etc
function cloneCircuitValue<T>(obj: T): T {
// primitive JS types and functions aren't cloned
if (typeof obj !== 'object' || obj === null) return obj;
Expand Down
21 changes: 13 additions & 8 deletions src/lib/party.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { UInt64, UInt32, Int64 } from './int';
import * as Mina from './mina';
import { SmartContract } from './zkapp';
import * as Precondition from './precondition';
import { Proof, snarkContext } from './proof_system';
import { inCheckedComputation, Proof, snarkContext } from './proof_system';
import { emptyHashWithPrefix, hashWithPrefix, prefixes } from './hash';

// external API
Expand Down Expand Up @@ -207,7 +207,7 @@ let Permissions = {
default: (): Permissions => ({
editState: Permission.proof(),
send: Permission.signature(),
receive: Permission.proof(),
receive: Permission.none(),
setDelegate: Permission.signature(),
setPermissions: Permission.signature(),
setVerificationKey: Permission.signature(),
Expand Down Expand Up @@ -304,8 +304,8 @@ interface Body extends PartyBody {
events: Events;
sequenceEvents: Events;
caller: Field;
callData: Field; //MerkleList<Array<Field>>;
callDepth: number; // TODO: this is an `int As_prover.t`
callData: Field;
callDepth: number;
preconditions: Preconditions;
useFullCommitment: Bool;
incrementNonce: Bool;
Expand Down Expand Up @@ -352,7 +352,7 @@ const Body = {
events: Events.empty(),
sequenceEvents: Events.empty(),
caller: getDefaultTokenId(),
callData: Field.zero, // TODO new MerkleList(),
callData: Field.zero,
callDepth: 0,
preconditions: Preconditions.ignoreAll(),
// the default assumption is that snarkyjs transactions don't include the fee payer
Expand Down Expand Up @@ -657,8 +657,14 @@ class Party {
}

hash() {
let fields = Types.Party.toFields(toPartyUnsafe(this));
return Ledger.hashPartyFromFields(fields);
// these two ways of hashing are (and have to be) consistent / produce the same hash
if (inCheckedComputation()) {
let fields = Types.Party.toFields(toPartyUnsafe(this));
return Ledger.hashPartyFromFields(fields);
} else {
let json = Types.Party.toJson(toPartyUnsafe(this));
return Ledger.hashPartyFromJson(JSON.stringify(json));
}
}

// TODO: this was only exposed to be used in a unit test
Expand Down Expand Up @@ -997,7 +1003,6 @@ function signJsonTransaction(
if (typeof privateKey === 'string')
privateKey = PrivateKey.fromBase58(privateKey);
let publicKey = privateKey.toPublicKey().toBase58();
// TODO: we really need types for the parties json
let parties: Types.Json.Parties = JSON.parse(transactionJson);
let feePayer = parties.feePayer;
if (feePayer.body.publicKey === publicKey) {
Expand Down
4 changes: 1 addition & 3 deletions src/snarky.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,9 +739,6 @@ declare class Ledger {

getAccount(publicKey: { g: Group }): Account | undefined;

static hashTransaction(partyHash: Field): Field;
static hashTransactionChecked(partyHash: Field): Field;

static transactionCommitments(txJson: string): {
commitment: Field;
fullCommitment: Field;
Expand Down Expand Up @@ -772,6 +769,7 @@ declare class Ledger {

static fieldsOfJson(json: string): Field[];
static hashPartyFromFields(fields: Field[]): Field;
static hashPartyFromJson(json: string): Field;
}

/**
Expand Down
Loading

0 comments on commit f2c3360

Please sign in to comment.