diff --git a/CHANGELOG.md b/CHANGELOG.md index befb7417e6..fa06f42868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Improve number of constraints needed for Merkle tree hashing https://github.com/o1-labs/snarkyjs/pull/820 - This breaks deployed zkApps which use `MerkleWitness.calculateRoot()`, because the circuit is changed - You can make your existing contracts compatible again by switching to `MerkleWitness.calculateRootSlow()`, which has the old circuit +- Renamed Function Parameters: The `getAction` function now accepts a new object structure for its parameters. https://github.com/o1-labs/snarkyjs/pull/828 + - The previous object keys, `fromActionHash` and `endActionHash`, have been replaced by `fromActionState` and `endActionState`. ### Fixed @@ -30,6 +32,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - The internal event type now includes event data and transaction information as separate objects, allowing for more accurate information about each event and its associated transaction. - Removed multiple best tip blocks when fetching action data https://github.com/o1-labs/snarkyjs/pull/817 - Implemented a temporary fix that filters out multiple best tip blocks, if they exist, while fetching actions. This fix will be removed once the related issue in the Archive-Node-API repository (https://github.com/o1-labs/Archive-Node-API/issues/7) is resolved. +- New `fromActionState` and `endActionState` parameters for fetchActions function in SnarkyJS https://github.com/o1-labs/snarkyjs/pull/828 + - Allows fetching only necessary actions to compute the latest actions state + - Eliminates the need to retrieve the entire actions history of a zkApp + - Utilizes `actionStateTwo` field returned by Archive Node API as a safe starting point for deriving the most recent action hash ## [0.9.5](https://github.com/o1-labs/snarkyjs/compare/21de489...4573252d) diff --git a/src/examples/zkapps/reducer/reducer.ts b/src/examples/zkapps/reducer/reducer.ts index af2f9fa513..e80f44c023 100644 --- a/src/examples/zkapps/reducer/reducer.ts +++ b/src/examples/zkapps/reducer/reducer.ts @@ -39,7 +39,7 @@ class CounterZkapp extends SmartContract { // compute the new counter and hash from pending actions let pendingActions = this.reducer.getActions({ - fromActionHash: actionsHash, + fromActionState: actionsHash, }); let { state: newCounter, actionsHash: newActionsHash } = diff --git a/src/examples/zkapps/reducer/reducer_composite.ts b/src/examples/zkapps/reducer/reducer_composite.ts index d12e1bb8d2..8116d71371 100644 --- a/src/examples/zkapps/reducer/reducer_composite.ts +++ b/src/examples/zkapps/reducer/reducer_composite.ts @@ -50,7 +50,7 @@ class CounterZkapp extends SmartContract { // compute the new counter and hash from pending actions let pendingActions = this.reducer.getActions({ - fromActionHash: actionsHash, + fromActionState: actionsHash, }); let { state: newCounter, actionsHash: newActionsHash } = diff --git a/src/examples/zkapps/voting/membership.ts b/src/examples/zkapps/voting/membership.ts index 3407ff4df2..553cc71924 100644 --- a/src/examples/zkapps/voting/membership.ts +++ b/src/examples/zkapps/voting/membership.ts @@ -115,7 +115,7 @@ export class Membership_ extends SmartContract { // checking if the member already exists within the accumulator let { state: exists } = this.reducer.reduce( this.reducer.getActions({ - fromActionHash: accumulatedMembers, + fromActionState: accumulatedMembers, }), Bool, (state: Bool, action: Member) => { @@ -168,7 +168,7 @@ export class Membership_ extends SmartContract { this.committedMembers.assertEquals(committedMembers); let pendingActions = this.reducer.getActions({ - fromActionHash: accumulatedMembers, + fromActionState: accumulatedMembers, }); let { state: newCommittedMembers, actionsHash: newAccumulatedMembers } = diff --git a/src/examples/zkapps/voting/voting.ts b/src/examples/zkapps/voting/voting.ts index ebb12fd8ea..b66f54f0ad 100644 --- a/src/examples/zkapps/voting/voting.ts +++ b/src/examples/zkapps/voting/voting.ts @@ -273,7 +273,7 @@ export class Voting_ extends SmartContract { let { state: newCommittedVotes, actionsHash: newAccumulatedVotes } = this.reducer.reduce( - this.reducer.getActions({ fromActionHash: accumulatedVotes }), + this.reducer.getActions({ fromActionState: accumulatedVotes }), Field, (state: Field, action: Member) => { // apply one vote diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index babbf046d6..927d4065bc 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -5,6 +5,7 @@ import { SequenceEvents, TokenId } from './account_update.js'; import { PublicKey } from './signature.js'; import { NetworkValue } from './precondition.js'; import { Types } from '../provable/types.js'; +import { ActionStates } from './mina.js'; import * as Encoding from './encoding.js'; import { Account, @@ -139,6 +140,9 @@ type FetchError = { statusCode: number; statusText: string; }; +type ActionStatesStringified = { + [K in keyof ActionStates]: string; +}; // Specify 30s as the default timeout const defaultTimeout = 30000; @@ -173,7 +177,12 @@ let accountsToFetch = {} as Record< let networksToFetch = {} as Record; let actionsToFetch = {} as Record< string, - { publicKey: string; tokenId: string; graphqlEndpoint: string } + { + publicKey: string; + tokenId: string; + actionStates: ActionStatesStringified; + graphqlEndpoint: string; + } >; function markAccountToBeFetched( @@ -195,13 +204,26 @@ function markNetworkToBeFetched(graphqlEndpoint: string) { function markActionsToBeFetched( publicKey: PublicKey, tokenId: Field, - graphqlEndpoint: string + graphqlEndpoint: string, + actionStates: ActionStates = {} ) { let publicKeyBase58 = publicKey.toBase58(); let tokenBase58 = TokenId.toBase58(tokenId); + let { fromActionState, endActionState } = actionStates; + let fromActionStateBase58 = fromActionState + ? fromActionState.toString() + : undefined; + let endActionStateBase58 = endActionState + ? endActionState.toString() + : undefined; + actionsToFetch[`${publicKeyBase58};${tokenBase58};${graphqlEndpoint}`] = { publicKey: publicKeyBase58, tokenId: tokenBase58, + actionStates: { + fromActionState: fromActionStateBase58, + endActionState: endActionStateBase58, + }, graphqlEndpoint, }; } @@ -220,9 +242,9 @@ async function fetchMissingData( } ); let actionPromises = Object.entries(actionsToFetch).map( - async ([key, { publicKey, tokenId }]) => { + async ([key, { publicKey, actionStates, tokenId }]) => { let response = await fetchActions( - { publicKey, tokenId }, + { publicKey, actionStates, tokenId }, archiveEndpoint ); if (!('error' in response) || response.error === undefined) @@ -558,7 +580,10 @@ type FetchedActions = { blockInfo: { distanceFromMaxBlockHeight: number; }; - actionState: string; + actionState: { + actionStateOne: string; + actionStateTwo: string; + }; actionData: { accountUpdateId: string; data: string[]; @@ -606,23 +631,27 @@ const getEventsQuery = ( }; const getActionsQuery = ( publicKey: string, + actionStates: ActionStatesStringified, tokenId: string, - filterOptions?: EventActionFilterOptions + _filterOptions?: EventActionFilterOptions ) => { - const { to, from } = filterOptions ?? {}; + const { fromActionState, endActionState } = actionStates ?? {}; let input = `address: "${publicKey}", tokenId: "${tokenId}"`; - if (to !== undefined) { - input += `, to: ${to}`; + if (fromActionState !== undefined) { + input += `, fromActionState: "${fromActionState}"`; } - if (from !== undefined) { - input += `, from: ${from}`; + if (endActionState !== undefined) { + input += `, endActionState: "${endActionState}"`; } return `{ actions(input: { ${input} }) { blockInfo { distanceFromMaxBlockHeight } - actionState + actionState { + actionStateOne + actionStateTwo + } actionData { accountUpdateId data @@ -711,20 +740,23 @@ async function fetchEvents( } async function fetchActions( - accountInfo: { publicKey: string; tokenId?: string }, - graphqlEndpoint = archiveGraphqlEndpoint, - filterOptions: EventActionFilterOptions = {} + accountInfo: { + publicKey: string; + actionStates: ActionStatesStringified; + tokenId?: string; + }, + graphqlEndpoint = archiveGraphqlEndpoint ) { if (!graphqlEndpoint) throw new Error( 'fetchEvents: Specified GraphQL endpoint is undefined. Please specify a valid endpoint.' ); - const { publicKey, tokenId } = accountInfo; + const { publicKey, actionStates, tokenId } = accountInfo; let [response, error] = await makeGraphqlRequest( getActionsQuery( publicKey, - tokenId ?? TokenId.toBase58(TokenId.default), - filterOptions + actionStates, + tokenId ?? TokenId.toBase58(TokenId.default) ), graphqlEndpoint ); @@ -771,10 +803,12 @@ async function fetchActions( // 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 }[] = []; - let latestActionsHash = SequenceEvents.emptySequenceState(); fetchedActions.forEach((fetchedAction) => { - const { actionState, actionData } = fetchedAction; + const { actionData } = fetchedAction; + let latestActionsHash = Field(fetchedAction.actionState.actionStateTwo); + let actionState = Field(fetchedAction.actionState.actionStateOne); + if (actionData.length === 0) throw new Error( `No action data was found for the account ${publicKey} with the latest action state ${actionState}` diff --git a/src/lib/mina.ts b/src/lib/mina.ts index c244ce3f58..c490bd7e16 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -49,6 +49,7 @@ export { fetchEvents, getActions, FeePayerSpec, + ActionStates, faucet, waitForFunding, getProofsEnabled, @@ -148,6 +149,11 @@ type DeprecatedFeePayerSpec = }) | undefined; +type ActionStates = { + fromActionState?: Field; + endActionState?: Field; +}; + function reportGetAccountError(publicKey: string, tokenId: string) { if (tokenId === TokenId.toBase58(TokenId.default)) { return `getAccount: Could not find account for public key ${publicKey}`; @@ -340,6 +346,7 @@ interface Mina { ) => ReturnType; getActions: ( publicKey: PublicKey, + actionStates?: ActionStates, tokenId?: Field ) => { hash: string; actions: string[][] }[]; proofsEnabled: boolean; @@ -581,10 +588,39 @@ function LocalBlockchain({ }, getActions( publicKey: PublicKey, + actionStates?: ActionStates, tokenId: Field = TokenId.default ): { hash: string; actions: string[][] }[] { + let currentActions: { hash: string; actions: string[][] }[] = + actions?.[publicKey.toBase58()]?.[Ledger.fieldToBase58(tokenId)] ?? []; + let { fromActionState, endActionState } = actionStates ?? {}; + + fromActionState = fromActionState + ?.equals(SequenceEvents.emptySequenceState()) + .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 ( - actions?.[publicKey.toBase58()]?.[Ledger.fieldToBase58(tokenId)] ?? [] + currentActions?.slice( + startIndex, + endIndex === 0 ? undefined : endIndex + ) ?? [] ); }, addAccount, @@ -817,9 +853,18 @@ function Network(input: { mina: string; archive: string } | string): Mina { filterOptions ); }, - getActions(publicKey: PublicKey, tokenId: Field = TokenId.default) { + getActions( + publicKey: PublicKey, + actionStates?: ActionStates, + tokenId: Field = TokenId.default + ) { if (currentTransaction()?.fetchMode === 'test') { - Fetch.markActionsToBeFetched(publicKey, tokenId, archiveEndpoint); + Fetch.markActionsToBeFetched( + publicKey, + tokenId, + archiveEndpoint, + actionStates + ); let actions = Fetch.getCachedActions(publicKey, tokenId); return actions ?? []; } @@ -906,10 +951,14 @@ let activeInstance: Mina = { async transaction(sender: DeprecatedFeePayerSpec, f: () => void) { return createTransaction(sender, f, 0); }, - fetchEvents(publicKey: PublicKey, tokenId: Field = TokenId.default) { + fetchEvents(_publicKey: PublicKey, _tokenId: Field = TokenId.default) { throw Error('must call Mina.setActiveInstance first'); }, - getActions(publicKey: PublicKey, tokenId: Field = TokenId.default) { + getActions( + _publicKey: PublicKey, + _actionStates?: ActionStates, + _tokenId: Field = TokenId.default + ) { throw Error('must call Mina.setActiveInstance first'); }, proofsEnabled: true, @@ -1055,8 +1104,12 @@ async function fetchEvents( /** * @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 getActions( + publicKey: PublicKey, + actionStates: ActionStates, + tokenId?: Field +) { + return activeInstance.getActions(publicKey, actionStates, tokenId); } function getProofsEnabled() { diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 08b9862c27..dcf9abf43f 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -1262,7 +1262,7 @@ type ReducerReturn = { * * ```ts * let pendingActions = this.reducer.getActions({ - * fromActionHash: actionsHash, + * fromActionState: actionsHash, * }); * * let { state: newState, actionsHash: newActionsHash } = @@ -1288,16 +1288,16 @@ type ReducerReturn = { * Fetches the list of previously emitted {@link Action}s by this {@link SmartContract}. * ```ts * let pendingActions = this.reducer.getActions({ - * fromActionHash: actionsHash, + * fromActionState: actionsHash, * }); * ``` */ getActions({ - fromActionHash, - endActionHash, + fromActionState, + endActionState, }: { - fromActionHash?: Field; - endActionHash?: Field; + fromActionState?: Field; + endActionState?: Field; }): Action[][]; }; @@ -1391,50 +1391,32 @@ Use the optional \`maxTransactionsWithActions\` argument to increase this number return { state, actionsHash }; }, getActions({ - fromActionHash, - endActionHash, + fromActionState, + endActionState, }: { - fromActionHash?: Field; - endActionHash?: Field; + fromActionState?: Field; + endActionState?: Field; }): A[][] { let actionsForAccount: A[][] = []; Circuit.asProver(() => { - // if the fromActionHash is the empty state, we fetch all events - fromActionHash = fromActionHash - ?.equals(SequenceEvents.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[][] }) => + let actions = Mina.getActions( + contract.address, + { + 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)) ) ) - ); + ); }); return actionsForAccount;