UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

321 lines (299 loc) 10.9 kB
import { Field } from '../../provable/wrapped.js'; import { Actions } from '../account-update.js'; import { FlexibleProvablePure, InferProvable, } from '../../provable/types/struct.js'; import { provable } from '../../provable/types/provable-derivers.js'; import { Provable } from '../../provable/provable.js'; import { ProvableHashable } from '../../provable/crypto/poseidon.js'; import * as Mina from '../mina.js'; import { ProvablePure } from '../../provable/types/provable-intf.js'; import { MerkleList } from '../../provable/merkle-list.js'; import type { SmartContract } from '../zkapp.js'; export { Reducer, getReducer }; const Reducer: (< T extends FlexibleProvablePure<any>, A extends InferProvable<T> = InferProvable<T> >(reducer: { actionType: T; }) => ReducerReturn<A>) & { initialActionState: Field; } = Object.defineProperty( function (reducer: any) { // we lie about the return value here, and instead overwrite this.reducer with // a getter, so we can get access to `this` inside functions on this.reducer (see constructor) return reducer; }, 'initialActionState', { get: Actions.emptyActionState } ) as any; type Reducer<Action> = { actionType: FlexibleProvablePure<Action>; }; type ReducerReturn<Action> = { /** * Dispatches an {@link Action}. Similar to normal {@link Event}s, * {@link Action}s can be stored by archive nodes and later reduced within a {@link SmartContract} method * to change the state of the contract accordingly * * ```ts * this.reducer.dispatch(Field(1)); // emits one action * ``` * * */ dispatch(action: Action): void; /** * Reduces a list of {@link Action}s, similar to `Array.reduce()`. * * ```ts * let pendingActions = this.reducer.getActions({ * fromActionState: actionState, * }); * * let newState = this.reducer.reduce( * pendingActions, * Field, // the state type * (state: Field, _action: Field) => { * return state.add(1); * }, * initialState // initial state * ); * ``` * * Warning: The reducer API in o1js is currently not safe to use in production applications. The `reduce()` * method breaks if more than the hard-coded number (default: 32) of actions are pending. Work is actively * in progress to mitigate this limitation. */ reduce<State>( actions: MerkleList<MerkleList<Action>>, stateType: Provable<State>, reduce: (state: State, action: Action) => State, initial: State, options?: { maxUpdatesWithActions?: number; maxActionsPerUpdate?: number; skipActionStatePrecondition?: boolean; } ): State; /** * Perform circuit logic for every {@link Action} in the list. * * This is a wrapper around {@link reduce} for when you don't need `state`. */ forEach( actions: MerkleList<MerkleList<Action>>, reduce: (action: Action) => void, options?: { maxUpdatesWithActions?: number; maxActionsPerUpdate?: number; skipActionStatePrecondition?: boolean; } ): void; /** * Fetches the list of previously emitted {@link Action}s by this {@link SmartContract}. * ```ts * let pendingActions = this.reducer.getActions({ * fromActionState: actionState, * }); * ``` * * The final action state can be accessed on `pendingActions.hash`. * ```ts * let endActionState = pendingActions.hash; * ``` * * If the optional `endActionState` is provided, the list of actions will be fetched up to that state. * In that case, `pendingActions.hash` is guaranteed to equal `endActionState`. */ getActions({ fromActionState, endActionState, }?: { fromActionState?: Field; endActionState?: Field; }): MerkleList<MerkleList<Action>>; /** * Fetches the list of previously emitted {@link Action}s by zkapp {@link SmartContract}. * ```ts * let pendingActions = await zkapp.reducer.fetchActions({ * fromActionState: actionState, * }); * ``` */ fetchActions({ fromActionState, endActionState, }?: { fromActionState?: Field; endActionState?: Field; }): Promise<Action[][]>; }; function getReducer<A>(contract: SmartContract): ReducerReturn<A> { let reducer: Reducer<A> = ((contract as any)._ ??= {}).reducer; if (reducer === undefined) throw Error( 'You are trying to use a reducer without having declared its type.\n' + `Make sure to add a property \`reducer\` on ${contract.constructor.name}, for example: class ${contract.constructor.name} extends SmartContract { reducer = Reducer({ actionType: Field }); }` ); return { dispatch(action: A) { let accountUpdate = contract.self; let eventFields = reducer.actionType.toFields(action); accountUpdate.body.actions = Actions.pushEvent( accountUpdate.body.actions, eventFields ); }, reduce<S>( actionLists: MerkleList<MerkleList<A>>, stateType: Provable<S>, reduce: (state: S, action: A) => S, state: S, { maxUpdatesWithActions = 32, maxActionsPerUpdate = 1, skipActionStatePrecondition = false, } = {} ): S { Provable.asProver(() => { if (actionLists.data.get().length > maxUpdatesWithActions) { throw Error( `reducer.reduce: Exceeded the maximum number of lists of actions, ${maxUpdatesWithActions}. Use the optional \`maxUpdatesWithActions\` argument to increase this number.` ); } }); if (!skipActionStatePrecondition) { // the actionList.hash is the hash of all actions in that list, appended to the previous hash (the previous list of historical actions) // this must equal one of the action states as preconditions to build a chain to that we only use actions that were dispatched between the current on chain action state and the initialActionState contract.account.actionState.requireEquals(actionLists.hash); } const listIter = actionLists.startIterating(); for (let i = 0; i < maxUpdatesWithActions; i++) { let { element: merkleActions, isDummy } = listIter.Unsafe.next(); let actionIter = merkleActions.startIterating(); let newState = state; if (maxActionsPerUpdate === 1) { // special case with less work, because the only action is a dummy iff merkleActions is a dummy let action = Provable.witness( reducer.actionType, () => actionIter.data.get()[0]?.element ?? actionIter.innerProvable.empty() ); let emptyHash = actionIter.Constructor.emptyHash; let finalHash = actionIter.nextHash(emptyHash, action); finalHash = Provable.if(isDummy, emptyHash, finalHash); // note: this asserts nothing in the isDummy case, because `actionIter.hash` is not well-defined // but it doesn't matter because we're also skipping all state and action state updates in that case actionIter.hash.assertEquals(finalHash); newState = reduce(newState, action); } else { for (let j = 0; j < maxActionsPerUpdate; j++) { let { element: action, isDummy } = actionIter.Unsafe.next(); newState = Provable.if( isDummy, stateType, newState, reduce(newState, action) ); } // note: this asserts nothing about the iterated actions if `MerkleActions` is a dummy // which doesn't matter because we're also skipping all state and action state updates in that case actionIter.assertAtEnd(); } state = Provable.if(isDummy, stateType, state, newState); } // important: we check that by iterating, we actually reached the claimed final action state listIter.assertAtEnd(); return state; }, forEach( actionLists: MerkleList<MerkleList<A>>, callback: (action: A) => void, config ) { const stateType = provable(null); this.reduce( actionLists, stateType, (_, action) => { callback(action); return null; }, null, config ); }, getActions(config?: { fromActionState?: Field; endActionState?: Field; }): MerkleList<MerkleList<A>> { const Action = reducer.actionType; const emptyHash = Actions.empty().hash; const nextHash = (hash: Field, action: A) => Actions.pushEvent({ hash, data: [] }, Action.toFields(action)).hash; class ActionList extends MerkleList.create( Action as unknown as ProvableHashable<A>, nextHash, emptyHash ) {} class MerkleActions extends MerkleList.create( ActionList.provable, (hash: Field, actions: ActionList) => Actions.updateSequenceState(hash, actions.hash), // if no "start" action hash was specified, this means we are fetching the entire history of actions, which started from the empty action state hash // otherwise we are only fetching a part of the history, which starts at `fromActionState` // TODO does this show that `emptyHash` should be part of the instance, not the class? that would make the provable representation bigger though config?.fromActionState ?? Actions.emptyActionState() ) {} let actions = Provable.witness(MerkleActions.provable, () => { let actionFields = Mina.getActions( contract.address, config, contract.tokenId ); // convert string-Fields back into the original action type let actions = actionFields.map((event) => event.actions.map((action) => (reducer.actionType as ProvablePure<A>).fromFields( action.map(Field) ) ) ); return MerkleActions.from( actions.map((a) => ActionList.fromReverse(a)) ); }); // note that we don't have to assert anything about the initial action state here, // because it is taken directly and not witnessed if (config?.endActionState !== undefined) { actions.hash.assertEquals(config.endActionState); } return actions; }, async fetchActions(config?: { fromActionState?: Field; endActionState?: Field; }): Promise<A[][]> { let result = await Mina.fetchActions( contract.address, config, contract.tokenId ); if ('error' in result) { throw Error(JSON.stringify(result)); } return result.map((event) => // putting our string-Fields back into the original action type event.actions.map((action) => (reducer.actionType as ProvablePure<A>).fromFields(action.map(Field)) ) ); }, }; }