From 36b069d40fd47f35a34a0d7b58d7b52481495caa Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 12 Apr 2023 11:58:18 +0200 Subject: [PATCH 1/7] expose setting the archive node endpoint --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index a0063e7c9..5cada122e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,6 +51,7 @@ export { TransactionStatus, addCachedAccount, setGraphqlEndpoint, + setArchiveGraphqlEndpoint, sendZkapp, } from './lib/fetch.js'; export * as Encryption from './lib/encryption.js'; From 7f819169b17d3f03ad4a7fe6fc5449f1bbfb2f03 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 12 Apr 2023 12:00:11 +0200 Subject: [PATCH 2/7] store action state as decimal string (local) also, through an error instead of silently accepting if fromActionState or endActionState aren't found --- src/lib/mina.ts | 114 ++++++++++++++++++++---------------------------- 1 file changed, 48 insertions(+), 66 deletions(-) diff --git a/src/lib/mina.ts b/src/lib/mina.ts index e7b0be8e7..e608ea6e6 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -393,7 +393,10 @@ function LocalBlockchain({ } const events: Record = {}; - const actions: Record = {}; + const actions: Record< + string, + Record + > = {}; return { proofsEnabled, @@ -476,17 +479,14 @@ function LocalBlockchain({ // fetches all events from the transaction and stores them // events are identified and associated with a publicKey and tokenId - zkappCommandJson.accountUpdates.forEach((p) => { - let addr = p.body.publicKey; - let tokenId = p.body.tokenId; - if (events[addr] === undefined) { - events[addr] = {}; - } - if (p.body.events.length > 0) { - if (events[addr][tokenId] === undefined) { - events[addr][tokenId] = []; - } - let updatedEvents = p.body.events.map((data) => { + txn.transaction.accountUpdates.forEach((p, i) => { + let pJson = zkappCommandJson.accountUpdates[i]; + let addr = pJson.body.publicKey; + let tokenId = pJson.body.tokenId; + events[addr] ??= {}; + if (p.body.events.data.length > 0) { + events[addr][tokenId] ??= []; + let updatedEvents = p.body.events.data.map((data) => { return { data, transactionInfo: { @@ -510,37 +510,26 @@ function LocalBlockchain({ // 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 actionState = actions?.[addr]?.[tokenId]?.[n - 1]?.hash; - + // most recent action state + let storedActions = actions[addr]?.[tokenId]; + let latestActionState_ = + storedActions?.[storedActions.length - 1]?.hash; // if there exists no hash, this means we initialize our latest hash with the empty state - let latestActionsHash = - actionState === undefined - ? Actions.emptyActionState() - : Ledger.fieldOfBase58(actionState); - - let actionList = p.body.actions; - let eventsHash = Actions.hash( - actionList.map((e) => e.map((f) => Field(f))) - ); - - if (actions[addr] === undefined) { - actions[addr] = {}; - } - if (p.body.actions.length > 0) { - latestActionsHash = Actions.updateSequenceState( - latestActionsHash, - eventsHash + let latestActionState = + latestActionState_ !== undefined + ? Field(latestActionState_) + : Actions.emptyActionState(); + + actions[addr] ??= {}; + if (p.body.actions.data.length > 0) { + let newActionState = Actions.updateSequenceState( + latestActionState, + p.body.actions.hash ); - if (actions[addr][tokenId] === undefined) { - actions[addr][tokenId] = []; - } + actions[addr][tokenId] ??= []; actions[addr][tokenId].push({ - actions: actionList, - hash: Ledger.fieldToBase58(latestActionsHash), + actions: pJson.body.actions, + hash: newActionState.toString(), }); } }); @@ -602,37 +591,30 @@ function LocalBlockchain({ actionStates?: ActionStates, tokenId: Field = TokenId.default ): { hash: string; actions: string[][] }[] { - let currentActions: { hash: string; actions: string[][] }[] = - actions?.[publicKey.toBase58()]?.[Ledger.fieldToBase58(tokenId)] ?? []; + let currentActions = + actions?.[publicKey.toBase58()]?.[TokenId.toBase58(tokenId)] ?? []; let { fromActionState, endActionState } = actionStates ?? {}; - fromActionState = fromActionState + let start = fromActionState ?.equals(Actions.emptyActionState()) .toBoolean() ? undefined - : fromActionState; - - // used to determine start and end values in string - let start: string | undefined = fromActionState - ? Ledger.fieldToBase58(fromActionState) - : undefined; - let end: string | undefined = endActionState - ? Ledger.fieldToBase58(endActionState) - : undefined; - - let startIndex = start - ? currentActions.findIndex((e) => e.hash === start) + 1 - : 0; - let endIndex = end - ? currentActions.findIndex((e) => e.hash === end) + 1 - : undefined; - - return ( - currentActions?.slice( - startIndex, - endIndex === 0 ? undefined : endIndex - ) ?? [] - ); + : fromActionState?.toString(); + let end = endActionState?.toString(); + + let startIndex = 0; + if (start) { + let i = currentActions.findIndex((e) => e.hash === start); + if (i === -1) throw Error(`getActions: fromActionState not found.`); + startIndex = i + 1; + } + let endIndex: number | undefined; + if (end) { + let i = currentActions.findIndex((e) => e.hash === end); + if (i === -1) throw Error(`getActions: endActionState not found.`); + endIndex = i + 1; + } + return currentActions.slice(startIndex, endIndex); }, addAccount, /** From 0cd1a7082193a793574e8599a42aa0886c3911f9 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 12 Apr 2023 12:40:28 +0200 Subject: [PATCH 3/7] reverse actions before applying in reduce --- src/lib/fetch.ts | 27 ++++++++--------------- src/lib/zkapp.ts | 56 +++++++++++++++++------------------------------- 2 files changed, 29 insertions(+), 54 deletions(-) diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index e51f29b59..024008e95 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -789,17 +789,6 @@ async function fetchActions( break; } } - - const processActionData = ( - currentActionList: string[][], - latestActionsHash: Field - ) => { - const actionsHash = Actions.hash( - currentActionList.map((e) => e.map((f) => Field(f))) - ); - return Actions.updateSequenceState(latestActionsHash, actionsHash); - }; - // Archive Node API returns actions in the latest order, so we reverse the array to get the actions in chronological order. fetchedActions.reverse(); let actionsList: { actions: string[][]; hash: string }[] = []; @@ -813,13 +802,10 @@ async function fetchActions( throw new Error( `No action data was found for the account ${publicKey} with the latest action state ${actionState}` ); - - actionData.reverse(); + let { accountUpdateId: currentAccountUpdateId } = actionData[0]; let currentActionList: string[][] = []; - - actionData.forEach((action, i) => { const { accountUpdateId, data } = action; const isLastAction = i === actionData.length - 1; @@ -831,7 +817,7 @@ async function fetchActions( } else if (isSameAccountUpdate && isLastAction) { currentActionList.push(data); } else if (!isSameAccountUpdate && isLastAction) { - latestActionsHash = processActionData( + latestActionsHash = updateActionState( currentActionList, latestActionsHash ); @@ -839,11 +825,11 @@ async function fetchActions( actions: currentActionList, hash: Ledger.fieldToBase58(Field(latestActionsHash)), }); - + currentActionList = [data]; } - latestActionsHash = processActionData( + latestActionsHash = updateActionState( currentActionList, latestActionsHash ); @@ -879,6 +865,11 @@ async function fetchActions( return actionsList; } +function updateActionState(actions: string[][], actionState: Field) { + let actionsHash = Actions.fromJSON(actions).hash; + return Actions.updateSequenceState(actionState, actionsHash); +} + // removes the quotes on JSON keys function removeJsonQuotes(json: string) { let cleaned = JSON.stringify(JSON.parse(json), null, 2); diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 50fdda122..795fd71c1 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -1375,7 +1375,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)); + let events = actions.map((a) => reducer.actionType.toFields(a)); return Actions.hash(events); }); let eventsHash = Circuit.switch(lengths, Field, eventsHashes); @@ -1386,13 +1386,13 @@ Use the optional \`maxTransactionsWithActions\` argument to increase this number let isEmpty = lengths[0]; // update state hash, if this is not an empty action actionsHash = Circuit.if(isEmpty, actionsHash, newActionsHash); - // also, for each action length, compute the new state and then pick the - // actual one + // also, for each action length, compute the new state and then pick the actual one let newStates = actionss.map((actions) => { // we generate a new witness for the state so that this doesn't break if `apply` modifies the state let newState = Circuit.witness(stateType, () => state); Circuit.assertEqual(stateType, newState, state); - actions.forEach((action) => { + // apply actions in reverse order since that's how they were stored at dispatch + [...actions].reverse().forEach((action) => { newState = reduce(newState, action); }); return newState; @@ -1414,24 +1414,18 @@ Use the optional \`maxTransactionsWithActions\` argument to increase this number Circuit.asProver(() => { let actions = Mina.getActions( contract.address, - { - fromActionState, - endActionState, - }, + { fromActionState, endActionState }, contract.self.tokenId ); - - actionsForAccount = actions.map( - (event: { hash: string; actions: string[][] }) => - // putting our string-Fields back into the original action type - event.actions.map((action: string[]) => - (reducer.actionType as ProvablePure).fromFields( - action.map((fieldAsString: string) => Field(fieldAsString)) - ) + actionsForAccount = actions.map((event) => + // putting our string-Fields back into the original action type + event.actions.map((action) => + (reducer.actionType as ProvablePure).fromFields( + action.map(Field) ) + ) ); }); - return actionsForAccount; }, async fetchActions({ @@ -1441,30 +1435,20 @@ Use the optional \`maxTransactionsWithActions\` argument to increase this number fromActionState?: Field; endActionState?: Field; }): Promise { - let actionsForAccount: A[][] = []; - let res = await Mina.fetchActions( + let result = await Mina.fetchActions( contract.address, - { - fromActionState, - endActionState, - }, + { fromActionState, endActionState }, contract.self.tokenId ); - if(res.hasOwnProperty('error')) { - throw Error(JSON.stringify(res)); + if ('error' in result) { + throw Error(JSON.stringify(result)); } - - actionsForAccount = (res as { hash: string; actions: string[][] }[]).map( - (event: { hash: string; actions: string[][] }) => - // putting our string-Fields back into the original action type - event.actions.map((action: string[]) => - (reducer.actionType as ProvablePure).fromFields( - action.map((fieldAsString: string) => Field(fieldAsString)) - ) - ) + return result.map((event) => + // putting our string-Fields back into the original action type + event.actions.map((action) => + (reducer.actionType as ProvablePure).fromFields(action.map(Field)) + ) ); - - return actionsForAccount; }, }; } From 57ebd26c072b3bc06869eb9931612362721a8c18 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 12 Apr 2023 12:54:38 +0200 Subject: [PATCH 4/7] store action state as decimal string (network) --- src/lib/fetch.ts | 75 +++++++++++++++++------------------------------- 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index 024008e95..e1d1480f2 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -1,5 +1,5 @@ import 'isomorphic-fetch'; -import { Field, Ledger } from '../snarky.js'; +import { Field } from '../snarky.js'; import { UInt32, UInt64 } from './int.js'; import { Actions, TokenId } from './account_update.js'; import { PublicKey } from './signature.js'; @@ -311,14 +311,12 @@ function addCachedAccountInternal(account: Account, graphqlEndpoint: string) { }; } -function addCachedActionsInternal( - accountInfo: { publicKey: PublicKey; tokenId: Field }, +function addCachedActions( + { publicKey, tokenId }: { publicKey: string; tokenId: string }, actions: { hash: string; actions: string[][] }[], graphqlEndpoint: string ) { - actionsCache[ - accountCacheKey(accountInfo.publicKey, accountInfo.tokenId, graphqlEndpoint) - ] = { + actionsCache[`${publicKey};${tokenId};${graphqlEndpoint}`] = { actions, graphqlEndpoint, timestamp: Date.now(), @@ -751,13 +749,13 @@ async function fetchActions( throw new Error( 'fetchActions: Specified GraphQL endpoint is undefined. Please specify a valid endpoint.' ); - const { publicKey, actionStates, tokenId } = accountInfo; + const { + publicKey, + actionStates, + tokenId = TokenId.toBase58(TokenId.default), + } = accountInfo; let [response, error] = await makeGraphqlRequest( - getActionsQuery( - publicKey, - actionStates, - tokenId ?? TokenId.toBase58(TokenId.default) - ), + getActionsQuery(publicKey, actionStates, tokenId), graphqlEndpoint ); if (error) throw Error(error.statusText); @@ -795,8 +793,8 @@ async function fetchActions( fetchedActions.forEach((fetchedAction) => { let { actionData } = fetchedAction; - let latestActionsHash = Field(fetchedAction.actionState.actionStateTwo); - let actionState = Field(fetchedAction.actionState.actionStateOne); + let latestActionState = Field(fetchedAction.actionState.actionStateTwo); + let actionState = fetchedAction.actionState.actionStateOne; if (actionData.length === 0) throw new Error( @@ -804,7 +802,7 @@ async function fetchActions( ); let { accountUpdateId: currentAccountUpdateId } = actionData[0]; - let currentActionList: string[][] = []; + let actions: string[][] = []; actionData.forEach((action, i) => { const { accountUpdateId, data } = action; @@ -812,56 +810,37 @@ async function fetchActions( const isSameAccountUpdate = accountUpdateId === currentAccountUpdateId; if (isSameAccountUpdate && !isLastAction) { - currentActionList.push(data); + actions.push(data); return; } else if (isSameAccountUpdate && isLastAction) { - currentActionList.push(data); + actions.push(data); } else if (!isSameAccountUpdate && isLastAction) { - latestActionsHash = updateActionState( - currentActionList, - latestActionsHash - ); - actionsList.push({ - actions: currentActionList, - hash: Ledger.fieldToBase58(Field(latestActionsHash)), - }); - - currentActionList = [data]; + latestActionState = updateActionState(actions, latestActionState); + actionsList.push({ actions, hash: latestActionState.toString() }); + + actions = [data]; } - latestActionsHash = updateActionState( - currentActionList, - latestActionsHash - ); - actionsList.push({ - actions: currentActionList, - hash: Ledger.fieldToBase58(Field(latestActionsHash)), - }); + latestActionState = updateActionState(actions, latestActionState); + actionsList.push({ actions, hash: latestActionState.toString() }); currentAccountUpdateId = accountUpdateId; - currentActionList = [data]; + actions = [data]; }); - const currentActionHash = Ledger.fieldToBase58(Field(latestActionsHash)); - const expectedActionHash = Ledger.fieldToBase58(Field(actionState)); + const finalActionState = latestActionState.toString(); + const expectedActionState = actionState; - if (currentActionHash !== expectedActionHash) { + if (finalActionState !== expectedActionState) { throw new Error( `Failed to derive correct actions hash for ${publicKey}. - Derived hash: ${currentActionHash}, expected hash: ${expectedActionHash}). + Derived hash: ${finalActionState}, expected hash: ${expectedActionState}). All action hashes derived: ${JSON.stringify(actionsList, null, 2)} Please try a different Archive Node API endpoint. ` ); } }); - addCachedActionsInternal( - { - publicKey: PublicKey.fromBase58(publicKey), - tokenId: TokenId.fromBase58(tokenId ?? TokenId.toBase58(TokenId.default)), - }, - actionsList, - graphqlEndpoint - ); + addCachedActions({ publicKey, tokenId }, actionsList, graphqlEndpoint); return actionsList; } From e97a0ffc460c18cb05b48fafaaafe3a2b0dbe0ad Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 12 Apr 2023 14:05:01 +0200 Subject: [PATCH 5/7] make action processing morev readable --- src/lib/fetch.ts | 44 +++++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index e1d1480f2..0580aac72 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -791,41 +791,35 @@ async function fetchActions( fetchedActions.reverse(); let actionsList: { actions: string[][]; hash: string }[] = []; - fetchedActions.forEach((fetchedAction) => { - let { actionData } = fetchedAction; - let latestActionState = Field(fetchedAction.actionState.actionStateTwo); - let actionState = fetchedAction.actionState.actionStateOne; + fetchedActions.forEach((actionBlock) => { + let { actionData } = actionBlock; + let latestActionState = Field(actionBlock.actionState.actionStateTwo); + let actionState = actionBlock.actionState.actionStateOne; if (actionData.length === 0) throw new Error( `No action data was found for the account ${publicKey} with the latest action state ${actionState}` ); - let { accountUpdateId: currentAccountUpdateId } = actionData[0]; - let actions: string[][] = []; - - actionData.forEach((action, i) => { - const { accountUpdateId, data } = action; - const isLastAction = i === actionData.length - 1; - const isSameAccountUpdate = accountUpdateId === currentAccountUpdateId; - - if (isSameAccountUpdate && !isLastAction) { - actions.push(data); - return; - } else if (isSameAccountUpdate && isLastAction) { - actions.push(data); - } else if (!isSameAccountUpdate && isLastAction) { - latestActionState = updateActionState(actions, latestActionState); - actionsList.push({ actions, hash: latestActionState.toString() }); - - actions = [data]; + // split actions by account update + let actionsByAccountUpdate: string[][][] = []; + let currentAccountUpdateId = 'none'; + let currentActions: string[][]; + actionData.forEach(({ accountUpdateId, data }) => { + if (accountUpdateId === currentAccountUpdateId) { + currentActions.push(data); + } else { + currentAccountUpdateId = accountUpdateId; + currentActions = [data]; + actionsByAccountUpdate.push(currentActions); } + }); + // re-hash actions + for (let actions of actionsByAccountUpdate) { latestActionState = updateActionState(actions, latestActionState); actionsList.push({ actions, hash: latestActionState.toString() }); - currentAccountUpdateId = accountUpdateId; - actions = [data]; - }); + } const finalActionState = latestActionState.toString(); const expectedActionState = actionState; From 9737153c2bd78b42f469b91663e4a2a35de8f4a7 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 12 Apr 2023 14:21:39 +0200 Subject: [PATCH 6/7] fix bug where archive node includes the block which ended in fromActionsHash --- src/lib/fetch.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index 0580aac72..75e1f2f59 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -791,13 +791,22 @@ async function fetchActions( fetchedActions.reverse(); let actionsList: { actions: string[][]; hash: string }[] = []; + // correct for archive node sending one block too many + if ( + fetchedActions.length !== 0 && + fetchedActions[0].actionState.actionStateOne === + actionStates.fromActionState + ) { + fetchedActions = fetchedActions.slice(1); + } + fetchedActions.forEach((actionBlock) => { let { actionData } = actionBlock; let latestActionState = Field(actionBlock.actionState.actionStateTwo); let actionState = actionBlock.actionState.actionStateOne; if (actionData.length === 0) - throw new Error( + throw Error( `No action data was found for the account ${publicKey} with the latest action state ${actionState}` ); From abc6605c20a4a5f9faa25e211779fef7567d3ec0 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 12 Apr 2023 16:02:45 +0200 Subject: [PATCH 7/7] change of error message --- src/examples/zkapps/voting/test.ts | 55 +++++++++--------------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/src/examples/zkapps/voting/test.ts b/src/examples/zkapps/voting/test.ts index c26ab658f..33b5f35e8 100644 --- a/src/examples/zkapps/voting/test.ts +++ b/src/examples/zkapps/voting/test.ts @@ -79,16 +79,12 @@ export async function testSet( ); console.log('checking that the tx is valid using default verification key'); + let m = Member.from(PrivateKey.random().toPublicKey(), UInt64.from(15)); + verificationKeySet.Local.addAccount(m.publicKey, m.balance.toString()); + await assertValidTx( true, () => { - let m = Member.from( - PrivateKey.random().toPublicKey(), - - UInt64.from(15) - ); - verificationKeySet.Local.addAccount(m.publicKey, m.balance.toString()); - verificationKeySet.voting.voterRegistration(m); }, verificationKeySet.feePayer @@ -109,16 +105,12 @@ export async function testSet( verificationKeySet.feePayer ); + m = Member.from(PrivateKey.random().toPublicKey(), UInt64.from(15)); + verificationKeySet.Local.addAccount(m.publicKey, m.balance.toString()); + await assertValidTx( false, () => { - let m = Member.from( - PrivateKey.random().toPublicKey(), - - UInt64.from(15) - ); - verificationKeySet.Local.addAccount(m.publicKey, m.balance.toString()); - verificationKeySet.voting.voterRegistration(m); }, verificationKeySet.feePayer, @@ -159,16 +151,12 @@ export async function testSet( ); console.log('checking that the tx is valid using default permissions'); + let m = Member.from(PrivateKey.random().toPublicKey(), UInt64.from(15)); + permissionedSet.Local.addAccount(m.publicKey, m.balance.toString()); + await assertValidTx( true, () => { - let m = Member.from( - PrivateKey.random().toPublicKey(), - - UInt64.from(15) - ); - permissionedSet.Local.addAccount(m.publicKey, m.balance.toString()); - permissionedSet.voting.voterRegistration(m); }, permissionedSet.feePayer @@ -192,16 +180,12 @@ export async function testSet( console.log('trying to invoke method with invalid permissions...'); + m = Member.from(PrivateKey.random().toPublicKey(), UInt64.from(15)); + permissionedSet.Local.addAccount(m.publicKey, m.balance.toString()); + await assertValidTx( false, () => { - let m = Member.from( - PrivateKey.random().toPublicKey(), - - UInt64.from(15) - ); - permissionedSet.Local.addAccount(m.publicKey, m.balance.toString()); - permissionedSet.voting.voterRegistration(m); }, permissionedSet.feePayer, @@ -240,24 +224,19 @@ export async function testSet( console.log('trying to invoke invalid contract method...'); + let m = Member.from(PrivateKey.random().toPublicKey(), UInt64.from(15)); + invalidSet.Local.addAccount(m.publicKey, m.balance.toString()); + try { let tx = await Mina.transaction(invalidSet.feePayer.toPublicKey(), () => { - let m = Member.from( - PrivateKey.random().toPublicKey(), - - UInt64.from(15) - ); - invalidSet.Local.addAccount(m.publicKey, m.balance.toString()); - invalidSet.voting.voterRegistration(m); }); - await tx.prove(); await tx.sign([invalidSet.feePayer]).send(); } catch (err: any) { - if (!err.toString().includes('precondition_unsatisfied')) { + if (!err.toString().includes('fromActionState not found')) { throw Error( - `Transaction should have failed but went through! Error: ${err}` + `Transaction should have failed, but failed with an unexpected error! ${err}` ); } }