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.
When creating updates to unknown accounts in a reducer function, care should be taken to avoid introducing security vulnerabilities.
Please follow the guidelines here.
Actions & Reducer
Like events, actions are public arbitrary information that are passed along with a zkApp transaction. However, actions give you additional power: you can process previous actions in a smart contract! Under the hood, this is possible because a commitment is stored to the history of dispatched actions on every account -- the actionState. It allows you to prove that the actions you process are, in fact, the actions that were dispatched to the same smart contract.
Using actions and a "lagging state" pattern, you can write zkApps that can process concurrent state updates by multiple users. With this capability, you can imagine all kinds of use cases where actions act as a built-in, "append-only" off-chain storage layer.
To use actions, you first have to declare their type on the smart contract. The object to declare is called a reducer -- because it can take a list of actions and reduce them:
import { SmartContract, Reducer, Field } from 'o1js';
class MyContract extends SmartContract {
reducer = Reducer({ actionType: Field });
}
Contrary to events, actions have only one type per smart contract. This type doesn't have a name. The actionType
in this example is Field
.
On a reducer
, you have two functions: reducer.dispatch()
and reducer.reduce()
.
"Dispatch" is simple -- like emitting events, it pushes one additional action to your account's action history:
this.reducer.dispatch(Field(1000));
"Reduce" is more involved, but it gives you full power to process actions however it suits your application. It might be easiest to grasp from an example where you have a list of actions and want to find out if one of actions is equal to
1000
.
In JavaScript, a built-in function on Array
does this:
let has1000 = array.some((x) => x === 1000);
However, you can also implement this with Array.reduce
:
let has1000 = array.reduce((acc, x) => acc || x === 1000, false);
In fact, Array.reduce
is powerful enough to let you do pretty much all of the array processing you can think of.
With Reducer.reduce
, an in-SNARK operation is just as powerful:
// type for the "accumulated output" of reduce -- the `stateType`
let stateType = Bool;
// example actions data
let actions = [[Field(1000)], [Field(2)], [Field(100)]];
// state before applying actions
let initial = {
state: Bool(false),
};
let newState = this.reducer.reduce(
actions,
stateType,
(state: Bool, action: Field) => state.or(action.equals(1000)),
initial
);
The acc
shown earlier is now state
; you must pass in the state's type as a parameter and pass in an actionState
which refers to one particular point in the action's history.
Like Array.reduce
, Reducer.reduce
takes a callback that has the signature (state: State, action: Action) => State
, where State
is the stateType
and Action
is the actionType
. It returns the result of applying all the actions, in order, to the initial state
. In this example, the returned state
is Bool(true)
because one of the actions in the list is Field(1000)
. One last difference to JavaScript reduce
is that it takes a list of lists of actions, instead of a flat list. Each of the sublists are the actions that were dispatched in one account update (for example, while running one smart contract method).
As an astute reader, you might have noticed that this use of state
is eerily similar to a standard "Elm architecture" that scans over an implicit infinite stream of actions (though here they are aggregated in chunks). This problem is familiar to web developers through its instantiation by using the Redux library or by using the useReducer
hook in React.
There is one interesting nuance here when compared to traditional Elm Architecture/Redux/useReducer instantiations: Because multiple actions are handled concurrently in an undefined order, it is important that actions commute against any possible state to prevent race conditions in your zkApp. Given any two actions a1 and a2 applying to some state s, s * a1 * a2
means the same as s * a2 * a1
.
A zkApp can retrieve events and actions from one or more Mina archive nodes. If your smart contract needs to fetch events and actions from an archive node, see How to Fetch Events and Actions.
Reducer - API reference
reducer = Reducer({ actionType: FlexibleProvablePure<Action> });
this.reducer.dispatch(action: Action): void;
this.reducer.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;
The getActions
function retrieves a list of previously emitted actions:
let pendingActions = this.reducer.getActions({ fromActionState?: Field, endActionState?: Field }): MerkleList<MerkleList<Action>>;
The final action state can be accessed on pendingActions.hash
.
let endActionState = pendingActions.hash;
If the optional endActionState
parameter is provided, the list of actions will be fetched up to that state.
In that case, pendingActions.hash
is guaranteed to equal endActionState
.
Use getActions
for testing with a simulated LocalBlockchain
. See Testing zkApps Locally.
Actions for concurrent state updates
One of the most important use cases for actions is to enable concurrent state updates. This enablement is also why actions were originally added to the protocol.
You can see a full code example in reducer-composite.ts that demonstrates this pattern. Leveraging Reducer.reduce()
, it takes only about 30 lines of code to build a zkApp that handles concurrent state updates.