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

Sequencing events #274

Merged
merged 27 commits into from
Jul 15, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
06ca649
emit state update
mitschabaude Jul 5, 2022
b6b4e22
fix circuitValue
mitschabaude Jul 5, 2022
56ed414
fix sequenceState precondition
mitschabaude Jul 5, 2022
3a35ed6
Merge branch 'feature/circuit-value-tweaks' into feature/sequence-events
mitschabaude Jul 6, 2022
61cbf2b
add Circuit.pickOne
mitschabaude Jul 6, 2022
829c0d4
implement sequence state hashing
mitschabaude Jul 6, 2022
761f34b
implement stateUpdate.applyUpdates
mitschabaude Jul 6, 2022
a4a1691
export Circuit.pickOne separately as well
mitschabaude Jul 6, 2022
1dc8eb2
SmartContract.runOutsideCircuit, runs just once
mitschabaude Jul 6, 2022
b004963
example for rolling up state updates
mitschabaude Jul 6, 2022
3580c8a
handle more rolled-up txs by default
mitschabaude Jul 6, 2022
ddacc84
run methods ahead of time to count sequence events
mitschabaude Jul 7, 2022
a46489a
add comment
mitschabaude Jul 7, 2022
7c17b28
tweak types & make example clearer
mitschabaude Jul 7, 2022
bd8d31b
use proofs by default
mitschabaude Jul 7, 2022
39e4ec3
Merge branch 'main' into feature/sequence-events
mitschabaude Jul 7, 2022
515e672
revert changes to simple_zkapp
mitschabaude Jul 7, 2022
6561480
fix {state,precondition}.get() in analyze mode
mitschabaude Jul 7, 2022
fc4dcf7
add missing preconditions to example & simplify
mitschabaude Jul 7, 2022
528fa94
remove junk
mitschabaude Jul 7, 2022
8e725af
rename to Circuit.switch, throw on invalid mask
mitschabaude Jul 14, 2022
9075114
use reducer terminology
mitschabaude Jul 14, 2022
f9d34cc
make stuff not extend AsFieldElements<unknown>
mitschabaude Jul 14, 2022
d0ecb1d
make reducer API flexible w.r.t. state & reduce
mitschabaude Jul 14, 2022
3260855
mark reducer API as experimental
mitschabaude Jul 14, 2022
60882d7
address pr feedback
mitschabaude Jul 15, 2022
925a57f
update bindings
mitschabaude Jul 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/examples/api_exploration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PrivateKey,
PublicKey,
Signature,
Int64,
} from 'snarkyjs';

/* This file demonstrates the classes and functions available in snarky.js */
Expand Down Expand Up @@ -115,6 +116,14 @@ const c = Circuit.if(

console.assert(c.bar.someFieldElt.equals(x1).toBoolean());

// Circuit.pickOne is a generalization for when you need to distinguish between multiple cases.
let x = Circuit.pickOne([Bool(false), Bool(true), Bool(false)], Int64, [
Int64.from(1),
Int64.from(2),
Int64.from(3),
]);
x.assertEquals(Int64.from(2));

/* # Signature
*/

Expand Down
140 changes: 140 additions & 0 deletions src/examples/state_update_rollup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {
Field,
state,
State,
method,
PrivateKey,
SmartContract,
Mina,
Party,
isReady,
Permissions,
circuitValue,
} from 'snarkyjs';

await isReady;

// version of the "simple zkapp" which accepts concurrent updates
class StateUpdateZkapp extends SmartContract {
// the "stateUpdate" field describes a state and how it can be updated
// (the state doesn't have to be related to on-chain state. here it is, though)
stateUpdate = SmartContract.StateUpdate({
// type for the state
state: circuitValue<{ counter: Field }>({ counter: Field }),
// type for the update
update: Field,
// function that says how to apply an update
apply(state: { counter: Field }, update: Field) {
state.counter = state.counter.add(update);
return state;
},
});

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

Choose a reason for hiding this comment

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

let's change this to whatever you pick on the RFC-side like actionsHash or actionsCommitment


// emits an update
@method incrementCounter(increment: Field) {
this.stateUpdate.emit(increment);
}

@method rollupStateUpdate() {
Copy link
Member

Choose a reason for hiding this comment

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

-> reduce

// get previous state and assert that it's the same as on-chain state
let counter = this.counter.get();
let oldStateHash = this.stateHash.get();
this.counter.assertEquals(counter);
this.stateHash.assertEquals(oldStateHash);
// compute the new state and hash from pending updates
// remark: it's not feasible to pass in the pending updates as method arguments, because they have dynamic size
let { state, stateHash } = this.stateUpdate.applyUpdates({
state: { counter },
stateHash: oldStateHash,
updates: pendingUpdates,
});
// update on-chain state
this.counter.set(state.counter);
this.stateHash.set(stateHash);
}
}

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

// this is a data structure where we internally keep track of the pending updates
// TODO: get these from a Mina node / the local blockchain
// note: each entry in pendingUpdates is itself an array -- the list of updates emitted by one method
// this is the structure we need to do the hashing correctly
let pendingUpdates: Field[][] = [];

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 StateUpdateZkapp(zkappAddress);
if (doProofs) {
console.log('compile');
await StateUpdateZkapp.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.stateHash.set(SmartContract.StateUpdate.initialStateHash);
});
tx.send();

console.log('update 1');
let increment = Field(3);
tx = await Mina.transaction(feePayer, () => {
zkapp.incrementCounter(increment);
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();
// update internal state
pendingUpdates.push([increment]);

console.log('update 2');
increment = Field(2);
tx = await Mina.transaction(feePayer, () => {
zkapp.incrementCounter(increment);
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();
// update internal state
pendingUpdates.push([increment]);

console.log('state (on-chain): ' + zkapp.counter.get());
console.log('pending updates:', JSON.stringify(pendingUpdates));

console.log('rollup transaction');
tx = await Mina.transaction(feePayer, () => {
zkapp.rollupStateUpdate();
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();
// reset pending updates
pendingUpdates = [];

console.log('state (on-chain): ' + zkapp.counter.get());
console.log('pending updates:', JSON.stringify(pendingUpdates));
12 changes: 10 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export {
Group,
Scalar,
AsFieldElements,
Circuit,
Ledger,
isReady,
shutdown,
Expand All @@ -14,7 +13,16 @@ export type { VerificationKey, Keypair } from './snarky';
export * from './snarky/addons';
export { Poseidon } from './lib/hash';
export * from './lib/signature';
export * from './lib/circuit_value';
export {
Circuit,
CircuitValue,
prop,
arrayProp,
matrixProp,
public_,
circuitMain,
circuitValue,
} from './lib/circuit_value';

export * from './lib/int';
export * as Mina from './lib/mina';
Expand Down
15 changes: 15 additions & 0 deletions src/lib/circuit_value.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Bool, Circuit, isReady, shutdown, Int64 } from '../../dist/server';

describe('circuit', () => {
Copy link
Member

Choose a reason for hiding this comment

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

can you add tests expecting failures if there are more than one true and zero true?

Copy link
Member Author

Choose a reason for hiding this comment

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

done. for some reason returning 0 for zero true seemed natural to me. but I can also throw in that case if you think that's better

Copy link
Member

Choose a reason for hiding this comment

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

good point yeah 0 is fine, but can you add a test for the zero case succeeding too?

beforeAll(() => isReady);
afterAll(() => setTimeout(shutdown, 0));

it('Circuit.pickOne picks the right value', () => {
const x = Circuit.pickOne([Bool(false), Bool(true), Bool(false)], Int64, [
Int64.from(-1),
Int64.from(-2),
Int64.from(-3),
]);
expect(x.toString()).toBe('-2');
});
});
48 changes: 43 additions & 5 deletions src/lib/circuit_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ import 'reflect-metadata';
import { Circuit, Field, Bool, JSONValue, AsFieldElements } from '../snarky';
import { withContext } from './global-context';

// external API
export {
Circuit,
CircuitValue,
prop,
arrayProp,
matrixProp,
public_,
circuitMain,
cloneCircuitValue,
circuitValueEquals,
circuitArray,
circuitValue,
};

// internal API
export { pickOne, cloneCircuitValue, circuitValueEquals, circuitArray };

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

abstract class CircuitValue {
Expand Down Expand Up @@ -324,11 +326,11 @@ function circuitValue<T>(typeObj: any): AsFieldElements<T> {
function sizeInFields(typeObj: any): number {
if (!complexTypes.has(typeof typeObj) || typeObj === null) return 0;
if (Array.isArray(typeObj))
return typeObj.map(sizeInFields).reduce((a, b) => a + b);
return typeObj.map(sizeInFields).reduce((a, b) => a + b, 0);
if ('sizeInFields' in typeObj) return typeObj.sizeInFields();
return Object.values(typeObj)
.map(sizeInFields)
.reduce((a, b) => a + b);
.reduce((a, b) => a + b, 0);
}
function toFields(typeObj: any, obj: any): Field[] {
if (!complexTypes.has(typeof typeObj) || typeObj === null) return [];
Expand Down Expand Up @@ -467,3 +469,39 @@ function circuitValueEquals<T>(a: T, b: T): boolean {
([key, value]) => key in b && circuitValueEquals((b as any)[key], value)
);
}

function pickOne<T, A extends AsFieldElements<T>>(
Copy link
Member Author

Choose a reason for hiding this comment

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

handy function that I needed for the dynamic length stuff, and exported as Circuit.pickOne. A less general version was previously in string.ts

Copy link
Member

Choose a reason for hiding this comment

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

maybe this should be select or switch or case? pickOne doesn't feel right to me

Copy link
Member Author

@mitschabaude mitschabaude Jul 14, 2022

Choose a reason for hiding this comment

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

switch is great -- analogous to Circuit.if

mask: Bool[],
type: A,
values: T[]
): T {
// picks the value at the index where mask is true
let nValues = values.length;
if (mask.length !== nValues)
throw Error(
`pickOne: \`values\` and \`mask\` have different lengths (${values.length} vs. ${mask.length}), which is not allowed.`
);
// TODO: we'd like to do a sanity check on the input values here -- but Circuit.asProver is only available in checked computations
// would be nice to have a generalization which works in all environments
// Circuit.asProver(() => {
// let nTrue = mask.filter((b) => b.toBoolean()).length;
// if (nTrue > 1) {
// throw Error(
// `pickOne: \`mask\` must have 0 or 1 true element, found ${nTrue}.`
// );
// }
// });
let size = type.sizeInFields();
let fields = Array(size).fill(Field.zero);
for (let i = 0; i < nValues; i++) {
let valueFields = type.toFields(values[i]);
let maskField = mask[i].toField();
for (let j = 0; j < size; j++) {
let maybeField = valueFields[j].mul(maskField);
fields[j] = fields[j].add(maybeField);
}
}
return type.ofFields(fields);
}

Circuit.pickOne = pickOne;
8 changes: 8 additions & 0 deletions src/lib/global-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export {
inProver,
inCompile,
inCheckedComputation,
inAnalyze,
};

// context for compiling / proving
Expand All @@ -21,6 +22,8 @@ let mainContext = undefined as
inProver?: boolean;
inCompile?: boolean;
inCheckedComputation?: boolean;
inAnalyze?: boolean;
otherContext?: any;
}
| undefined;
type PartialContext = {
Expand All @@ -31,6 +34,8 @@ type PartialContext = {
inProver?: boolean;
inCompile?: boolean;
inCheckedComputation?: boolean;
inAnalyze?: boolean;
otherContext?: any;
};

function withContext<T>(
Expand Down Expand Up @@ -105,3 +110,6 @@ function inCheckedComputation() {
!!mainContext?.inCheckedComputation
);
}
function inAnalyze() {
return !!mainContext?.inAnalyze;
}
35 changes: 31 additions & 4 deletions src/lib/mina.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type CurrentTransaction =
parties: Party[];
nextPartyIndex: number;
fetchMode: FetchMode;
isFinalRunOutsideCircuit: boolean;
};

export let currentTransaction: CurrentTransaction = undefined;
Expand All @@ -83,7 +84,7 @@ function createUnsignedTransaction(
function createTransaction(
feePayer: SenderSpec,
f: () => unknown,
{ fetchMode = 'cached' as FetchMode } = {}
{ fetchMode = 'cached' as FetchMode, isFinalRunOutsideCircuit = true } = {}
): Transaction {
if (currentTransaction !== undefined) {
throw new Error('Cannot start new transaction within another transaction');
Expand All @@ -98,6 +99,7 @@ function createTransaction(
parties: [],
nextPartyIndex: 0,
fetchMode,
isFinalRunOutsideCircuit,
};

try {
Expand Down Expand Up @@ -254,7 +256,21 @@ function LocalBlockchain({
return { wait: async () => {} };
},
async transaction(sender: SenderSpec, f: () => void) {
return createTransaction(sender, f);
// bad hack: run transaction just to see whether it creates proofs
// if it doesn't, this is the last chance to run SmartContract.runOutsideCircuit, which is supposed to run only once
Copy link
Member Author

Choose a reason for hiding this comment

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

I introduced SmartContract.runOutsideCircuit for an earlier version of the state rollup example, where I needed to take values from the @method run and store them outside. It was pretty hacky to get that working -- e.g. it's important that this runs only once (in the "user expectation" sense of "once"). Left it in the API since it may be useful another time and could get much less hacky with the some planned changes (parties-returning and async-method-running)

// TODO: this has obvious holes if multiple zkapps are involved, but not relevant currently because we can't prove with multiple parties
// and hopefully with upcoming work by Matt we can just run everything in the prover, and nowhere else
let tx = createTransaction(sender, f, {
isFinalRunOutsideCircuit: false,
});
let hasProofs = tx.transaction.otherParties.some(
(party) =>
'kind' in party.authorization &&
party.authorization.kind === 'lazy-proof'
);
return createTransaction(sender, f, {
isFinalRunOutsideCircuit: !hasProofs,
});
},
applyJsonTransaction(json: string) {
return ledger.applyJsonTransaction(json, String(accountCreationFee));
Expand Down Expand Up @@ -333,9 +349,20 @@ function RemoteBlockchain(graphqlEndpoint: string): Mina {
};
},
async transaction(sender: SenderSpec, f: () => void) {
createTransaction(sender, f, { fetchMode: 'test' });
let tx = createTransaction(sender, f, {
fetchMode: 'test',
isFinalRunOutsideCircuit: false,
});
await Fetch.fetchMissingData(graphqlEndpoint);
return createTransaction(sender, f, { fetchMode: 'cached' });
let hasProofs = tx.transaction.otherParties.some(
(party) =>
'kind' in party.authorization &&
party.authorization.kind === 'lazy-proof'
);
return createTransaction(sender, f, {
fetchMode: 'cached',
isFinalRunOutsideCircuit: !hasProofs,
});
},
};
}
Expand Down
Loading