diff --git a/src/examples/state_update_rollup.ts b/src/examples/state_update_rollup.ts index 3706c34eec..24d7394e89 100644 --- a/src/examples/state_update_rollup.ts +++ b/src/examples/state_update_rollup.ts @@ -10,6 +10,8 @@ import { Party, isReady, Permissions, + Circuit, + Ledger, } from 'snarkyjs'; await isReady; @@ -39,9 +41,12 @@ class CounterZkapp extends SmartContract { // 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, + this.reducer.getActions({ + fromActionHash: actionsHash, + }), // state type Field, // function that says how to apply an action @@ -60,12 +65,6 @@ class CounterZkapp extends SmartContract { 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); @@ -77,7 +76,6 @@ let zkappKey = PrivateKey.fromBase58( 'EKEQc95PPQZnMY9d9p1vq1MWLeDJKtvKj4V75UDG3rjnf32BerWD' ); let zkappAddress = zkappKey.toPublicKey(); - let zkapp = new CounterZkapp(zkappAddress); if (doProofs) { console.log('compile'); @@ -100,15 +98,16 @@ let tx = await Mina.transaction(feePayer, () => { }); 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(); -// update internal state -pendingActions.push([INCREMENT]); console.log('action 2'); tx = await Mina.transaction(feePayer, () => { @@ -117,21 +116,53 @@ tx = await Mina.transaction(feePayer, () => { }); 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('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()); + +console.log('applying more actions'); + +console.log('action 4'); +tx = await Mina.transaction(feePayer, () => { + zkapp.incrementCounter(); + if (!doProofs) zkapp.sign(zkappKey); +}); +tx.send(); + +console.log('action 5'); +tx = await Mina.transaction(feePayer, () => { + zkapp.incrementCounter(); + if (!doProofs) zkapp.sign(zkappKey); +}); +tx.send(); + +console.log('rolling up pending actions..'); + +console.log('state before: ' + zkapp.counter.get()); -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)); +console.log('state after rollup: ' + zkapp.counter.get()); diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 71d2c0b6df..21c3c2eaa7 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -41,6 +41,7 @@ export { accountCreationFee, sendTransaction, fetchEvents, + getActions, FeePayerSpec, }; interface TransactionId { @@ -197,6 +198,10 @@ interface Mina { accountCreationFee(): UInt64; sendTransaction(transaction: Transaction): TransactionId; fetchEvents: (publicKey: PublicKey, tokenId?: Field) => any; + getActions: ( + publicKey: PublicKey, + tokenId?: Field + ) => { hash: string; actions: string[][] }[]; } interface MockMina extends Mina { @@ -247,6 +252,7 @@ function LocalBlockchain({ } const events: Record = {}; + const actions: Record = {}; return { accountCreationFee: () => UInt64.from(accountCreationFee), @@ -318,8 +324,43 @@ function LocalBlockchain({ slot: networkState.globalSlotSinceHardFork.toString(), }); } - }); + // actions/sequencing events + + // gets the index of the most up to date sequence state from our sequence list + let n = actions[addr]?.[tokenId]?.length ?? 1; + + // most recent sequence state + let sequenceState = actions?.[addr]?.[tokenId]?.[n - 1]?.hash; + + // if there exists no hash, this means we initialize our latest hash with the empty state + let latestActionsHash = + sequenceState === undefined + ? Events.emptySequenceState() + : Ledger.fieldOfBase58(sequenceState); + + let actionList = p.body.sequenceEvents; + let eventsHash = Events.hash( + actionList.map((e) => e.map((f) => Field(f))) + ); + + if (actions[addr] === undefined) { + actions[addr] = {}; + } + if (p.body.sequenceEvents.length > 0) { + latestActionsHash = Events.updateSequenceState( + latestActionsHash, + eventsHash + ); + if (actions[addr][tokenId] === undefined) { + actions[addr][tokenId] = []; + } + actions[addr][tokenId].push({ + actions: actionList, + hash: Ledger.fieldToBase58(latestActionsHash), + }); + } + }); return { wait: async () => {} }; }, async transaction(sender: FeePayerSpec, f: () => void) { @@ -350,6 +391,14 @@ function LocalBlockchain({ ): Promise { return events?.[publicKey.toBase58()]?.[TokenId.toBase58(tokenId)] ?? []; }, + getActions( + publicKey: PublicKey, + tokenId: Field = TokenId.default + ): { hash: string; actions: string[][] }[] { + return ( + actions?.[publicKey.toBase58()]?.[Ledger.fieldToBase58(tokenId)] ?? [] + ); + }, addAccount, testAccounts, setTimestamp(ms: UInt64) { @@ -477,6 +526,11 @@ function RemoteBlockchain(graphqlEndpoint: string): Mina { 'fetchEvents() is not implemented yet for remote blockchains.' ); }, + getActions() { + throw Error( + 'fetchEvents() is not implemented yet for remote blockchains.' + ); + }, }; } @@ -543,6 +597,9 @@ let activeInstance: Mina = { fetchEvents() { throw Error('must call Mina.setActiveInstance first'); }, + getActions() { + throw Error('must call Mina.setActiveInstance first'); + }, }; /** @@ -630,6 +687,13 @@ async function fetchEvents(publicKey: PublicKey, tokenId: Field) { return await activeInstance.fetchEvents(publicKey, tokenId); } +/** + * @return A list of emitted sequencing actions associated to the given public key. + */ +function getActions(publicKey: PublicKey, tokenId: Field) { + return activeInstance.getActions(publicKey, tokenId); +} + function dummyAccount(pubkey?: PublicKey): Account { return { balance: UInt64.zero, diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index e3591f48a0..ed72038945 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -731,6 +731,13 @@ type ReducerReturn = { state: State; actionsHash: Field; }; + getActions({ + fromActionHash, + endActionHash, + }: { + fromActionHash?: Field; + endActionHash?: Field; + }): Action[][]; }; function getReducer(contract: SmartContract): ReducerReturn { @@ -826,6 +833,57 @@ Use the optional \`maxTransactionsWithActions\` argument to increase this number contract.account.sequenceState.assertEquals(actionsHash); return { state, actionsHash }; }, + getActions({ + fromActionHash, + endActionHash, + }: { + fromActionHash?: Field; + endActionHash?: Field; + }): A[][] { + let actionsForAccount: A[][] = []; + + Circuit.asProver(() => { + // if the fromActionHash is the empty state, we fetch all events + fromActionHash = fromActionHash + ?.equals(Events.emptySequenceState()) + .toBoolean() + ? undefined + : fromActionHash; + + // used to determine start and end values in string + let start: string | undefined = fromActionHash + ? Ledger.fieldToBase58(fromActionHash) + : undefined; + let end: string | undefined = endActionHash + ? Ledger.fieldToBase58(endActionHash) + : undefined; + + let actions = Mina.getActions(contract.address, contract.self.tokenId); + + // gets the start/end indices of our array slice + let startIndex = start + ? actions.findIndex((e) => e.hash === start) + 1 + : 0; + let endIndex = end + ? actions.findIndex((e) => e.hash === end) + 1 + : undefined; + + // slices the array so we only get the wanted range between fromActionHash and endActionHash + actionsForAccount = actions + .slice(startIndex, endIndex === 0 ? undefined : endIndex) + .map((event: { hash: string; actions: string[][] }) => + // putting our string-Fields back into the original action type + event.actions.map((action: string[]) => + reducer.actionType.ofFields( + action.map((fieldAsString: string) => + Field.fromString(fieldAsString) + ) + ) + ) + ); + }); + return actionsForAccount; + }, }; }