Skip to main content
info

zkApp programmability is not yet available on the Mina Mainnet, but zkApps can now be deployed on Berkeley Testnet.

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 and actionState before applying actions
let initial = {
state: Bool(false),
actionState: Reducer.initialActionState,
};

let { state, actionState } = 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: S, action: A) => S, where S is the stateType and A 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). Reduce also returns the new actionState -- so you can store it to use when you reduce the next batch of actions. 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: AsFieldElements<A> });

this.reducer.dispatch(action: A): void;

this.reducer.reduce<S>(
actions: A[][],
stateType: AsFieldElements<S>,
reduce: (state: S, action: A) => S,
initial: { state: S, actionState: Field }
): { state: S, actionState: Field };

Reducer.initialActionState: Field;

In the near future, we want to add a function to retrieve actions from an archive node:

this.reducer.getActions({ fromActionState?: Field, endActionState?: Field }): A[][];

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.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.