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 25 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.switch is a generalization of Circuit.if, for when you need to distinguish between multiple cases.
let x = Circuit.switch([Bool(false), Bool(true), Bool(false)], Int64, [
Int64.from(1),
Int64.from(2),
Int64.from(3),
]);
x.assertEquals(Int64.from(2));

/* # Signature
*/

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

await isReady;

const INCREMENT = Field.one;

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

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

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

// this is a data structure where we internally keep track of the pending actions
// TODO: get these from a Mina node / the local blockchain
// note: each entry in pendingActions is itself an array -- the list of actions dispatched by one method
// this is the structure we need to do the hashing correctly
let pendingActions: 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 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('action 1');
tx = await Mina.transaction(feePayer, () => {
zkapp.incrementCounter();
if (!doProofs) zkapp.sign(zkappKey);
});
if (doProofs) await tx.prove();
tx.send();
// update internal state
pendingActions.push([INCREMENT]);
Copy link
Member

Choose a reason for hiding this comment

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

We can do this later when we revisit this API to move it out of experimental, but shouldn't we have some way of reaching into the reducer inside the zkapp to pull the pendingActions out for testing? It's brittle to need to remember to push the actions.

I really like how this library handles testing reducers https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/teststore

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah I agree. I think this would be achieved if reducer.getActions -- which will fetch actions in the real network interaction -- gets them from memory in the LocalBlockchain case


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

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

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

console.log('state (on-chain): ' + zkapp.counter.get());
console.log('pending actions:', JSON.stringify(pendingActions));
22 changes: 19 additions & 3 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,11 +13,28 @@ 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';
export * from './lib/zkapp';
export {
SmartContract,
Experimental,
method,
deploy,
DeployArgs,
signFeePayer,
declareMethods,
} from './lib/zkapp';
export { state, State, declareState } from './lib/state';
export { Proof, SelfProof, ZkProgram, verify } from './lib/proof_system';
export * from './lib/party';
Expand Down
25 changes: 25 additions & 0 deletions src/lib/circuit_value.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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.switch picks the right value', () => {
const x = Circuit.switch([Bool(false), Bool(true), Bool(false)], Int64, [
Int64.from(-1),
Int64.from(-2),
Int64.from(-3),
]);
expect(x.toString()).toBe('-2');
});

it('Circuit.switch throws when mask has >1 nonzero elements', () => {
expect(() =>
Circuit.switch([Bool(true), Bool(true), Bool(false)], Int64, [
Int64.from(-1),
Int64.from(-2),
Int64.from(-3),
])
).toThrow(/`mask` must have 0 or 1 true element, found 2/);
});
});
55 changes: 48 additions & 7 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 { cloneCircuitValue, circuitValueEquals, circuitArray };

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

abstract class CircuitValue {
Expand Down Expand Up @@ -67,14 +69,18 @@ abstract class CircuitValue {
return (this.constructor as any).toJSON(this);
}

equals(x: this): Bool {
equals(x: this) {
return Circuit.equal(this, x);
}

assertEquals(x: this): void {
assertEquals(x: this) {
Circuit.assertEqual(this, x);
}

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

static ofFields<T extends AnyConstructor>(
this: T,
xs: Field[]
Expand Down Expand Up @@ -324,11 +330,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 +473,38 @@ function circuitValueEquals<T>(a: T, b: T): boolean {
([key, value]) => key in b && circuitValueEquals((b as any)[key], value)
);
}

// TODO: move `Circuit` to JS entirely, this patching harms code discoverability
Circuit.switch = function <T, A extends AsFieldElements<T>>(
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(
`Circuit.switch: \`values\` and \`mask\` have different lengths (${values.length} vs. ${mask.length}), which is not allowed.`
);
let checkMask = () => {
let nTrue = mask.filter((b) => b.toBoolean()).length;
if (nTrue > 1) {
throw Error(
`Circuit.switch: \`mask\` must have 0 or 1 true element, found ${nTrue}.`
);
}
};
if (mask.every((b) => b.toField().isConstant())) checkMask();
else Circuit.asProver(checkMask);
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);
};
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;
}
Loading