o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
321 lines (299 loc) • 10.9 kB
text/typescript
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))
)
);
},
};
}