Skip to content

Commit

Permalink
Merge pull request #272 from o1-labs/feature/events-2
Browse files Browse the repository at this point in the history
Events
  • Loading branch information
mitschabaude authored Jul 7, 2022
2 parents da44ad3 + 4aaf800 commit 100a1a1
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 49 deletions.
7 changes: 7 additions & 0 deletions src/examples/simple_zkapp.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@ class SimpleZkapp extends SmartContract {
super(address);
this.x = State();
}

events = {
update: Field,
};

deploy(args) {
super.deploy(args);
this.x.set(initialState);
}
update(y) {
this.emitEvent('update', y);
this.emitEvent('update', y);
this.account.balance.assertEquals(this.account.balance.get());
let x = this.x.get();
this.x.assertEquals(x);
Expand Down
26 changes: 21 additions & 5 deletions src/examples/simple_zkapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,20 @@ import {
DeployArgs,
UInt32,
Bool,
PublicKey,
} from 'snarkyjs';

await isReady;

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

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

deploy(args: DeployArgs) {
super.deploy(args);
this.setPermissions({
Expand All @@ -31,6 +38,7 @@ class SimpleZkapp extends SmartContract {
}

@method update(y: Field) {
this.emitEvent('update', y);
let x = this.x.get();
this.x.assertEquals(x);
this.x.set(x.add(y));
Expand All @@ -56,6 +64,10 @@ class SimpleZkapp extends SmartContract {
let halfBalance = balance.div(2);
this.balance.subInPlace(halfBalance);
callerParty.balance.addInPlace(halfBalance);

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

Expand All @@ -78,7 +90,7 @@ let initialState = Field(1);
let zkapp = new SimpleZkapp(zkappAddress);

console.log('deploy');
let tx = await Local.transaction(feePayer, () => {
let tx = await Mina.transaction(feePayer, () => {
Party.fundNewAccount(feePayer, { initialBalance });
zkapp.deploy({ zkappKey });
});
Expand All @@ -88,24 +100,28 @@ console.log('initial state: ' + zkapp.x.get());
console.log(`initial balance: ${zkapp.account.balance.get().div(1e9)} MINA`);

console.log('update');
tx = await Local.transaction(feePayer, () => {
tx = await Mina.transaction(feePayer, () => {
zkapp.update(Field(3));
zkapp.sign(zkappKey);
});
tx.send();

console.log(tx.toJSON());

console.log('payout');
tx = await Local.transaction(feePayer, () => {
tx = await Mina.transaction(feePayer, () => {
zkapp.payout(privilegedKey);
zkapp.sign(zkappKey);
});
tx.send();

console.log(tx.toJSON());

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

console.log('try to payout a second time..');
tx = await Local.transaction(feePayer, () => {
tx = await Mina.transaction(feePayer, () => {
zkapp.payout(privilegedKey);
zkapp.sign(zkappKey);
});
Expand All @@ -117,7 +133,7 @@ try {

console.log('try to payout to a different account..');
try {
tx = await Local.transaction(feePayer, () => {
tx = await Mina.transaction(feePayer, () => {
zkapp.payout(Local.testAccounts[2].privateKey);
zkapp.sign(zkappKey);
});
Expand Down
18 changes: 7 additions & 11 deletions src/lib/circuit_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Circuit, Field, Bool, JSONValue, AsFieldElements } from '../snarky';
import { withContext } from './global-context';

export {
asFieldElementsToConstant,
CircuitValue,
prop,
arrayProp,
Expand All @@ -18,11 +17,6 @@ export {

type Constructor<T> = { new (...args: any[]): T };

function asFieldElementsToConstant<T>(typ: AsFieldElements<T>, t: T): T {
const xs: Field[] = typ.toFields(t);
return typ.ofFields(xs);
}

// TODO: Synthesize the constructor if possible (bkase)
//
abstract class CircuitValue {
Expand Down Expand Up @@ -324,16 +318,18 @@ function circuitValue<T>(typeObj: any): AsFieldElements<T> {
let offset = 0;
for (let subObj of typeObj) {
let size = sizeInFields(subObj);
array.push(subObj.ofFields(fields.slice(offset, offset + size)));
array.push(ofFields(subObj, fields.slice(offset, offset + size)));
offset += size;
}
return array;
}
if ('ofFields' in typeObj) return typeObj.ofFields(fields);
let typeObjArray = Object.keys(typeObj)
.sort()
.map((k) => typeObj[k]);
return ofFields(typeObjArray, fields);
let keys = Object.keys(typeObj).sort();
let values = ofFields(
keys.map((k) => typeObj[k]),
fields
);
return Object.fromEntries(keys.map((k, i) => [k, values[i]]));
}
function check(typeObj: any, obj: any): void {
if (typeof typeObj !== 'object' || typeObj === null) return;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/global-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ function withContext<T>(
}: PartialContext,
f: () => T
) {
let prevContext = mainContext;
mainContext = { witnesses, expectedAccesses, actualAccesses, self, ...other };
let result: T;
try {
result = f();
} finally {
mainContext = undefined;
mainContext = prevContext;
}
return [self, result] as [Party, T];
}
Expand Down
17 changes: 15 additions & 2 deletions src/lib/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { inCheckedComputation } from './global-context';
export { Poseidon };

// internal API
export { salt, prefixes };
export { prefixes, emptyHashWithPrefix, hashWithPrefix };

class Sponge {
private sponge: unknown;
Expand Down Expand Up @@ -44,10 +44,23 @@ const Poseidon = {
Sponge,
};

function emptyHashWithPrefix(prefix: string) {
return salt(prefix)[0];
}

function hashWithPrefix(prefix: string, input: Field[]) {
let init = salt(prefix);
return Poseidon.update(init, input)[0];
}
const prefixes = Poseidon_.prefixes;

function salt(prefix: string) {
return Poseidon.update(Poseidon.initialState, [prefixToField(prefix)]);
return Poseidon_.update(
Poseidon.initialState,
[prefixToField(prefix)],
// salt is never suppoesed to run in checked mode
false
);
}

// same as Random_oracle.prefix_to_field in OCaml
Expand Down
40 changes: 23 additions & 17 deletions src/lib/party.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SmartContract } from './zkapp';
import { withContextAsync } from './global-context';
import * as Precondition from './precondition';
import { Proof } from './proof_system';
import { salt } from './hash';
import { emptyHashWithPrefix, hashWithPrefix, prefixes } from './hash';

export {
SetOrKeep,
Expand All @@ -29,6 +29,7 @@ export {
signJsonTransaction,
ZkappStateLength,
ZkappPublicInput,
Events,
};

const ZkappStateLength = 8;
Expand Down Expand Up @@ -231,24 +232,29 @@ let Permissions = {

const getDefaultTokenId = () => Field.one;

// TODO
class Events {
type Event = Field[];

type Events = {
hash: Field;
data: Field[][];
data: Event[];
};

static empty() {
let emptyHash = salt('MinaSnappEventsEmpty')[0];
return new Events(emptyHash, []);
}
static emptySequenceState() {
return salt('MinaSnappSequenceEmpty')[0];
}
const Events = {
empty(): Events {
let hash = emptyHashWithPrefix('MinaSnappEventsEmpty');
return { hash, data: [] };
},

constructor(hash: Field, events: Field[][]) {
this.hash = hash;
this.data = events;
}
}
pushEvent(events: Events, event: Event): Events {
let eventHash = hashWithPrefix(prefixes.event, event);
let hash = hashWithPrefix(prefixes.events, [events.hash, eventHash]);
return { hash, data: [...events.data, event] };
},

emptySequenceState() {
return emptyHashWithPrefix('MinaSnappSequenceEmpty');
},
};

// TODO: get docstrings from OCaml and delete this interface
/**
Expand Down Expand Up @@ -365,7 +371,7 @@ const FeePayerBody = {
};
type FeePayerUnsigned = {
body: FeePayerBody;
authorization: UnfinishedSignature | string;
authorization: UnfinishedSignature;
};

/**
Expand Down
84 changes: 71 additions & 13 deletions src/lib/zkapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ import {
Permissions,
SetOrKeep,
ZkappPublicInput,
Events,
} from './party';
import { PrivateKey, PublicKey } from './signature';
import * as Mina from './mina';
import { UInt32, UInt64 } from './int';
import { mainContext, inCheckedComputation } from './global-context';
import {
mainContext,
inCheckedComputation,
inCompile,
withContext,
inProver,
} from './global-context';
import {
assertPreconditionInvariants,
cleanPreconditionsCache,
Expand Down Expand Up @@ -85,18 +92,36 @@ function wrapMethod(
) {
return function wrappedMethod(this: SmartContract, ...actualArgs: any[]) {
cleanStatePrecondition(this);
if (inCheckedComputation() || Mina.currentTransaction === undefined) {
if (inCheckedComputation()) {
// 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);
// FIXME: figure out correct way to constrain public input https://github.com/o1-labs/snarkyjs/issues/98
let tail = Field.zero;
publicInput[0].assertEquals(publicInput[0]);
// checkPublicInput(publicInput, self, tail);
}

if (inCheckedComputation()) {
return withContext(
{
inCompile: inCompile(),
inProver: inProver(),
// important to run this with a fresh party everytime, otherwise we compile messes up our circuits
// because it runs this multiple times
self: selfParty(this.address),
},
() => {
// inside prover / compile, the method is always called with the public input as first argument
// -- so we can add assertions about it
let publicInput = actualArgs[0];
actualArgs = actualArgs.slice(1);
// FIXME: figure out correct way to constrain public input https://github.com/o1-labs/snarkyjs/issues/98
let tail = Field.zero;
publicInput[0].assertEquals(publicInput[0]);
// checkPublicInput(publicInput, self, tail);

// 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;
}
)[1];
} else if (Mina.currentTransaction === undefined) {
// 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
Expand Down Expand Up @@ -301,6 +326,39 @@ export class SmartContract {
return this.self.setNoncePrecondition();
}

events: { [key: string]: AsFieldElements<any> } = {};

// TODO: not able to type event such that it is inferred correctly so far
emitEvent<K extends keyof this['events']>(type: K, event: any) {
let party = this.self;
let eventTypes: (keyof this['events'])[] = Object.keys(this.events);
if (eventTypes.length === 0)
throw Error(
'emitEvent: You are trying to emit an event without having declared the types of your events.\n' +
`Make sure to add a property \`events\` on ${this.constructor.name}, for example: \n` +
`class ${this.constructor.name} extends SmartContract {\n` +
` events = { 'my-event': Field }\n` +
`}`
);
let eventNumber = eventTypes.sort().indexOf(type as string);
if (eventNumber === -1)
throw Error(
`emitEvent: Unknown event type "${
type as string
}". The declared event types are: ${eventTypes.join(', ')}.`
);
let eventType = (this.events as this['events'])[type];
let eventFields: Field[];
if (eventTypes.length === 1) {
// if there is just one event type, just store it directly as field elements
eventFields = eventType.toFields(event);
} else {
// if there is more than one event type, also store its index, like in an enum, to identify the type later
eventFields = [Field(eventNumber), ...eventType.toFields(event)];
}
party.body.events = Events.pushEvent(party.body.events, eventFields);
}

setValue<T>(maybeValue: SetOrKeep<T>, value: T) {
Party.setValue(maybeValue, value);
}
Expand Down
4 changes: 4 additions & 0 deletions src/snarky.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
shutdown,
Pickles,
JSONValue,
InferAsFieldElements,
};
export * as Types from './snarky/gen/parties';
export { jsLayout } from './snarky/gen/js-layout';
Expand Down Expand Up @@ -475,6 +476,9 @@ declare interface AsFieldElements<T> {
check(x: T): void;
}

type InferAsFieldElements<T extends AsFieldElements<any>> =
T extends AsFieldElements<infer U> ? U : never;

declare interface CircuitMain<W, P> {
snarkyWitnessTyp: AsFieldElements<W>;
snarkyPublicTyp: AsFieldElements<P>;
Expand Down

0 comments on commit 100a1a1

Please sign in to comment.