Skip to content

Commit

Permalink
Merge pull request #365 from o1-labs/fix/composite-actions
Browse files Browse the repository at this point in the history
Fix actions hashing
  • Loading branch information
mitschabaude committed Aug 29, 2022
2 parents 0155044 + 6cdc97b commit 4dfb7bf
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 68 deletions.
32 changes: 20 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/o1-labs/snarkyjs/compare/e0192f7...HEAD)
<!--
Possible subsections:
_Added_ for new features.
_Changed_ for changes in existing functionality.
_Deprecated_ for soon-to-be removed features.
_Removed_ for now removed features.
_Fixed_ for any bug fixes.
_Security_ in case of vulnerabilities.
-->

## [Unreleased](https://github.com/o1-labs/snarkyjs/compare/55c8ea0...HEAD)

### Added

- `reducer.getActions` partially implemented for local testing https://github.com/o1-labs/snarkyjs/pull/327

(no unreleased changes yet)
## [0.5.1](https://github.com/o1-labs/snarkyjs/compare/e0192f7...55c8ea0)

### Fixed

- `fetchAccount` https://github.com/o1-labs/snarkyjs/pull/350

## [0.5.0](https://github.com/o1-labs/snarkyjs/compare/2375f08...e0192f7)

Expand Down Expand Up @@ -77,13 +95,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- zkApp proving on web https://github.com/o1-labs/snarkyjs/issues/226

<!--
Possible subsections:
Added for new features.
Changed for changes in existing functionality.
Deprecated for soon-to-be removed features.
Removed for now removed features.
Fixed for any bug fixes.
Security in case of vulnerabilities.
-->
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"format": "prettier --write --ignore-unknown **/*",
"test": "node --experimental-wasm-modules --experimental-modules --experimental-wasm-threads --experimental-vm-modules ./node_modules/jest/bin/jest.js",
"clean": "rimraf ./dist",
"test:integration": "./run src/examples/zkapps/hello_world/run.ts && ./run src/examples/zkapps/voting/run.ts"
"test:integration": "./run-integration-tests.sh"
},
"author": "O(1) Labs",
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions run-integration-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
./run src/examples/zkapps/hello_world/run.ts
./run src/examples/zkapps/voting/run.ts
./run src/examples/zkapps/reducer/reducer_composite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
Party,
isReady,
Permissions,
Circuit,
Ledger,
} from 'snarkyjs';

await isReady;
Expand Down Expand Up @@ -40,13 +38,13 @@ class CounterZkapp extends SmartContract {
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 pendingActions = this.reducer.getActions({
fromActionHash: actionsHash,
});

let { state: newCounter, actionsHash: newActionsHash } =
this.reducer.reduce(
this.reducer.getActions({
fromActionHash: actionsHash,
}),
pendingActions,
// state type
Field,
// function that says how to apply an action
Expand All @@ -62,7 +60,7 @@ class CounterZkapp extends SmartContract {
}
}

const doProofs = false;
const doProofs = true;
const initialCounter = Field.zero;

let Local = Mina.LocalBlockchain();
Expand Down Expand Up @@ -145,13 +143,15 @@ tx = await Mina.transaction(feePayer, () => {
zkapp.incrementCounter();
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();

console.log('action 5');
tx = await Mina.transaction(feePayer, () => {
zkapp.incrementCounter();
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();

console.log('rolling up pending actions..');
Expand Down
182 changes: 182 additions & 0 deletions src/examples/zkapps/reducer/reducer_composite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import {
Field,
state,
State,
method,
PrivateKey,
SmartContract,
Experimental,
Mina,
Party,
isReady,
Permissions,
circuitValue,
Bool,
Circuit,
} from 'snarkyjs';
import assert from 'node:assert/strict';

await isReady;

type MaybeIncrement = { isIncrement: Bool; otherData: Field };
const MaybeIncrement = circuitValue<MaybeIncrement>({
isIncrement: Bool,
otherData: Field,
});
const INCREMENT = { isIncrement: Bool(true), otherData: Field.zero };

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

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

@method incrementCounter() {
this.reducer.dispatch(INCREMENT);
}
@method dispatchData(data: Field) {
this.reducer.dispatch({ isIncrement: Bool(false), otherData: data });
}

@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
let pendingActions = this.reducer.getActions({
fromActionHash: actionsHash,
});

let { state: newCounter, actionsHash: newActionsHash } =
this.reducer.reduce(
pendingActions,
// state type
Field,
// function that says how to apply an action
(state: Field, action: MaybeIncrement) => {
return Circuit.if(action.isIncrement, state.add(1), state);
},
{ state: counter, actionsHash }
);

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

const doProofs = true;
const initialCounter = Field.zero;

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.fromBase58(
'EKEQc95PPQZnMY9d9p1vq1MWLeDJKtvKj4V75UDG3rjnf32BerWD'
);
let zkappAddress = zkappKey.toPublicKey();
let zkapp = new CounterZkapp(zkappAddress);
if (doProofs) {
console.log('compile');
await CounterZkapp.compile(zkappAddress);
}

console.log('deploy');
let tx = await Mina.transaction(feePayer, () => {
Party.fundNewAccount(feePayer);
zkapp.deploy({ zkappKey });
if (!doProofs) {
zkapp.setPermissions({
...Permissions.default(),
editState: Permissions.proofOrSignature(),
editSequenceState: Permissions.proofOrSignature(),
});
}
zkapp.counter.set(initialCounter);
zkapp.actionsHash.set(Experimental.Reducer.initialActionsHash);
});
tx.send();

console.log('applying actions..');

console.log('action 1');

tx = await Mina.transaction(feePayer, () => {
zkapp.incrementCounter();
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();

console.log('action 2');
tx = await Mina.transaction(feePayer, () => {
zkapp.incrementCounter();
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();

console.log('action 3');
tx = await Mina.transaction(feePayer, () => {
zkapp.incrementCounter();
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();

console.log('rolling up pending actions..');

console.log('state before: ' + zkapp.counter.get());

tx = await Mina.transaction(feePayer, () => {
zkapp.rollupIncrements();
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();

console.log('state after rollup: ' + zkapp.counter.get());
assert.deepEqual(zkapp.counter.get().toString(), '3');

console.log('applying more actions');

console.log('action 4 (no increment)');
tx = await Mina.transaction(feePayer, () => {
zkapp.dispatchData(Field.random());
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();

console.log('action 5');
tx = await Mina.transaction(feePayer, () => {
zkapp.incrementCounter();
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();

console.log('rolling up pending actions..');

console.log('state before: ' + zkapp.counter.get());

tx = await Mina.transaction(feePayer, () => {
zkapp.rollupIncrements();
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();

console.log('state after rollup: ' + zkapp.counter.get());
assert.equal(zkapp.counter.get().toString(), '4');
2 changes: 1 addition & 1 deletion src/lib/mina.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ function LocalBlockchain({
: Ledger.fieldOfBase58(sequenceState);

let actionList = p.body.sequenceEvents;
let eventsHash = Events.hash(
let eventsHash = SequenceEvents.hash(
actionList.map((e) => e.map((f) => Field(f)))
);

Expand Down
8 changes: 2 additions & 6 deletions src/lib/party.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,14 @@ describe('party', () => {
expect(publicInputOcaml).toEqual(publicInput);
});

it('creates the right empty events', () => {
expect(party.body.events.hash.toString()).toEqual(
'23641812384071365026036270005604392899711718400522999453895455265440046333209'
);
});
it('creates the right empty sequence state', () => {
expect(
party.body.preconditions.account.sequenceState.value.toString()
).toEqual(
'19777675955122618431670853529822242067051263606115426372178827525373304476695'
'12935064460869035604753254773225484359407575580289870070671311469994328713165'
);
});

it('encodes token ids correctly', () => {
let x = Field.random();
let defaultTokenId = 'wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf';
Expand Down
23 changes: 10 additions & 13 deletions src/lib/party.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,42 +265,39 @@ const Events = {
let hash = emptyHashWithPrefix('MinaZkappEventsEmpty');
return { hash, data: [] };
},

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] };
},

hash(events: Event[]) {
return events.reduce(Events.pushEvent, Events.empty()).hash;
},
};

const emptySequenceStateElementSalt = 'MinaZkappSequenceStateEmptyElt';

const SequenceEvents = {
empty(): Events {
// same as events but w/ different hash prefixes
empty(): Events {
let hash = emptyHashWithPrefix('MinaZkappSequenceEmpty');
return { hash, data: [] };
},

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

hash(events: Event[]) {
return events.reduce(SequenceEvents.pushEvent, SequenceEvents.empty()).hash;
},

// different than events
emptySequenceState() {
return emptyHashWithPrefix(emptySequenceStateElementSalt);
return emptyHashWithPrefix('MinaZkappSequenceStateEmptyElt');
},

updateSequenceState(state: Field, sequenceEventsHash: Field) {
return hashWithPrefix(emptySequenceStateElementSalt, [state, sequenceEventsHash]);
return hashWithPrefix(prefixes.sequenceEvents, [state, sequenceEventsHash]);
},
};

Expand Down
4 changes: 2 additions & 2 deletions src/lib/zkapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ class ${contract.constructor.name} extends SmartContract {
dispatch(action: A) {
let party = contract.self;
let eventFields = reducer.actionType.toFields(action);
party.body.sequenceEvents = Events.pushEvent(
party.body.sequenceEvents = SequenceEvents.pushEvent(
party.body.sequenceEvents,
eventFields
);
Expand Down Expand Up @@ -803,7 +803,7 @@ Use the optional \`maxTransactionsWithActions\` argument to increase this number
// for each action length, compute the events hash and then pick the actual one
let eventsHashes = actionss.map((actions) => {
let events = actions.map((u) => reducer.actionType.toFields(u));
return Events.hash(events);
return SequenceEvents.hash(events);
});
let eventsHash = Circuit.switch(lengths, Field, eventsHashes);
let newActionsHash = SequenceEvents.updateSequenceState(
Expand Down
Loading

0 comments on commit 4dfb7bf

Please sign in to comment.