This is the final article in our “Reducers in Mina” series. Here, we’ll explore another method of leveraging reducers in your zkApps: batch reducers. To illustrate, we’ll apply it to a counter, previously discussed in Article 3, where we explained reducers.
The Problem
As discussed in Article 3, the basic reducer provided by o1js has a limitation on the maximum number of actions it can handle. This makes it challenging to use in real applications, as a large number of actions could easily block the contract. Another problem is the limitation of account updates amount. Writing a custom reducer to handle this can be time-consuming. To address this, the o1js developers introduced batch reducers, which allow developers to handle an unlimited number of actions efficiently. Batch reducers operate similarly to the snapshot reducer (also discussed in Article 3) by expanding the action list, breaking it into chunks manageable in a single batch, and processing each chunk sequentially.
Using batch reducers
First, define the structure of the batch reducer. Key parameters are the action type (here, Field
) and the number of actions per batch (we’ll use 2 for demonstration of batching).
export const batchReducer = new BatchReducer({
actionType: Field,
batchSize: 2,
});
class Batch extends batchReducer.Batch {}
class BatchProof extends batchReducer.BatchProof {}
To use the batch reducer, add two fields to your contract to store information about the processed and pending actions:
export class Add extends SmartContract {
...
@state(Field)
actionState = State(BatchReducer.initialActionState);
@state(Field)
actionStack = State(BatchReducer.initialActionStack);
...
}
Dispatching actions is nearly the same as with a regular reducer, but you’ll use the batchReducer
you declared:
@method async add(value: Field) {
value.assertGreaterThan(Field(0));
batchReducer.dispatch(value);
}
The reduce method accepts a batch and proof, types we defined earlier. The main difference here is that empty elements require additional handling:
batchReducer.processBatch({ batch, proof }, (number, isDummy) => {
curTotal = Provable.if(isDummy, curTotal, curTotal.add(number));
});
That’s it. The entire code remains compact and readable.
Full code here.
Testing
We’ll reuse the test case from Article 3[link]. The main differences are as follows:
- You’ll need to set up the contract specifically for the batch reducer:
zkApp = new Add(zkAppAddress);
batchReducer.setContractInstance(zkApp);
2. The reduce function has been updated. Batches and proofs are prepared using the prepareBatches function. For each batch, call the batchReduce function:
const batchReduce = async () => {
const batches = await batchReducer.prepareBatches();
for (let i = 0; i < batches.length; i++) {
console.log('Processing batch', i);
const batch = batches[i];
let tx = await Mina.transaction(senderAccount, async () => {
await zkApp.batchReduce(batch.batch, batch.proof);
});
await tx.prove();
await tx.sign([senderAccount.key]).send();
}
};
Conclusion
Today, we explored the functionality of batch reducers. Although still experimental and subject to change, batch reducers provide zkApp developers with a straightforward and robust way to use reducers effectively. This article concludes our series on reducers, where we examined the issues developers face in zkApp creation and how reducers help address them. We also looked at how to implement custom reducers, as well as reducer-based tools that simplify zkApp development.