-
Notifications
You must be signed in to change notification settings - Fork 107
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
Sequencing events #274
Changes from 20 commits
06ca649
b6b4e22
56ed414
3a35ed6
61cbf2b
829c0d4
761f34b
a4a1691
1dc8eb2
b004963
3580c8a
ddacc84
a46489a
7c17b28
bd8d31b
39e4ec3
515e672
6561480
fc4dcf7
528fa94
8e725af
9075114
f9d34cc
d0ecb1d
3260855
60882d7
925a57f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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>(); | ||
|
||
// emits an update | ||
@method incrementCounter(increment: Field) { | ||
this.stateUpdate.emit(increment); | ||
} | ||
|
||
@method rollupStateUpdate() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); |
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', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -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 []; | ||
|
@@ -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>>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe this should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. switch is great -- analogous to |
||
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,6 +61,7 @@ type CurrentTransaction = | |
parties: Party[]; | ||
nextPartyIndex: number; | ||
fetchMode: FetchMode; | ||
isFinalRunOutsideCircuit: boolean; | ||
}; | ||
|
||
export let currentTransaction: CurrentTransaction = undefined; | ||
|
@@ -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'); | ||
|
@@ -98,6 +99,7 @@ function createTransaction( | |
parties: [], | ||
nextPartyIndex: 0, | ||
fetchMode, | ||
isFinalRunOutsideCircuit, | ||
}; | ||
|
||
try { | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I introduced |
||
// 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)); | ||
|
@@ -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, | ||
}); | ||
}, | ||
}; | ||
} | ||
|
There was a problem hiding this comment.
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
oractionsCommitment