Mina Action & Reducers Guide: Off-chain storage

ZkNoid
ZkNoid
Published in
4 min readDec 26, 2024

In our previous articles [1][2][3], we explored Mina’s actions and reducers — what they are, how they’re structured, and ways to use them. This article and the next will focus on off-chain storage structures provided by the o1js library, which are based on reducers. Currently, both off-chain storage and the batch reducer are still under development, so their functionality may change over time. Here, we’ll cover the problems off-chain storage aims to solve and how to use it in zkApps. The focus of this article is off-chain storage.

The Problem

In Mina, each smart contract has only 8 fields for storing information. This limited space makes it challenging to store complex data mappings directly on-chain. However, these fields can be used to store a commitment to off-chain data, which can then be verified and used on-chain. Another challenge in Mina is concurrent updates to Merkle trees; if two transactions simultaneously try to update the same tree, the first transaction will invalidate the proof of the second.

Off-chain storage addresses both of these problems:

  1. It allows the contract to store a single field as a commitment to all off-chain data.
  2. By using actions and reducers internally, off-chain storage coordinates multiple transactions so they can update concurrently. However, while this allows parallel updates to different branches of the tree, simultaneous updates to the same fields remain a limitation.

Using Off-Chain State

To illustrate, let’s refer to the o1js code that demonstrates the use of off-chain storage: ExampleContract.ts.

  1. Define the Structure: First, you need to define an off-chain storage structure, which can include any number of Field and Map fields. Field is used for single values (like totalSupply), while Map is used for mappings (in our case, mapping a user to their address). You’ll also define meta parameters like logTotalCapacity (setting the height of the Merkle tree, thus the maximum number of elements it can hold) and maxActionsPerProof (the maximum number of updates per proof. In case you need more number of updates - decrease maxActionsPerProof parameter, so circuit size remains small).
const offchainState = OffchainState(
{
accounts: OffchainState.Map(PublicKey, UInt64),
totalSupply: OffchainState.Field(UInt64),
},
{ logTotalCapacity: 10, maxActionsPerProof: 5 }
);

With this structure, you can create a state proof that allows modifications to be applied on-chain.

class StateProof extends offchainState.Proof {}

2. Set Up the Contract: Construct the contract similarly to a regular o1js smart contract, but with a few added fields:

  • Allocate one field to hold the commitment to off-chain storage (offchainStateCommitments) and initialize it with the empty commitment from off-chain storage (offchainState.emptyCommitments()).
  • Create the off-chain storage instance in the contract’s internal variables and bind it to the contract. o1js will remember the off-chain storage state for this contract.
class ExampleContract extends SmartContract {
@state(OffchainState.Commitments) offchainStateCommitments =
offchainState.emptyCommitments();
offchainState = offchainState.init(this);
}

3. Apply Changes: To ensure off-chain storage works correctly, create a function to apply changes:

@method
async settle(proof: StateProof) {
await this.offchainState.settle(proof);
}

Since off-chain storage is based on the action reducer pattern, user interactions only add changes to a queue, requiring an additional step to apply them.

4. Access Fields: You can access fields with offchainState.fields.<your_field>.get. For example, the getBalance function uses this to retrieve the balance of a specified address:

@method.returns(UInt64)
async getBalance(address: PublicKey) {
return (await this.offchainState.fields.accounts.get(address)).orElse(0n);
}

5. Update Fields: For updating, there are two main functions, override and update:

  • override directly overwrites the variable’s value regardless of the previous state.
  • update checks the previous value and cancels the change if it detects any conflicts.

Using update in a transfer function is safer as it respects existing data:

let fromOption = await this.offchainState.fields.accounts.get(from);
let toOption = await this.offchainState.fields.accounts.get(to);

this.offchainState.fields.accounts.update(from, {
from: fromOption,
to: fromBalance.sub(amount),
});

this.offchainState.fields.accounts.update(to, {
from: toOption,
to: toBalance.add(amount),
});

This setup allows for smooth, native data interaction, while o1js handles proof and reducer logic.

Testing

Tests can be found here. They resemble regular zkApp tests but with some differences:

  1. Initialize Off-Chain Storage: When creating the contract, initialize off-chain storage:
const contract = new ExampleContract(contractAccount);
contract.offchainState.setContractInstance(contract);

2. Compile the State Proof:

await exampleOffchainState.compile();
await ExampleContract.compile();

3. Two-Stage State Update:

  • First, the user calls a function (e.g., createAccount, transfer).
  • Then, the settle function is invoked.

4. Concurrency Issue: If two transactions attempt to change the same state, only the first will succeed, with the second being skipped. The proof will remain valid, and the zkApp will continue functioning.

Conclusion

This article explored off-chain storage — a structure that enables easy use of the action reducer pattern and solves off-chain data proof challenges in smart contracts. It also partially addresses concurrent data modification issues.

In the next article, we’ll dive into batch reducers and see how they help manage the limited number of actions in standard reducers.

Previous articles in series:

  1. Mina Action & Reducers Guide: Why We Need Them
  2. Mina Action & Reducers Guide: Let’s take a closer look
  3. Mina Action & Reducers Guide: Writing our own reducers

--

--

ZkNoid
ZkNoid

Published in ZkNoid

ZkNoid is the home for provable gaming. On the platform you can try yourself the cutting edge games utilizing Zero-Knowledge proofs or build one using the provided infrastructure. https://www.zknoid.io/

ZkNoid
ZkNoid

Written by ZkNoid

Platform for games with provable game process based on Mina protocol and o1js. Docs – docs.zknoid.io. Github – github.com/ZkNoid. Twitter – https://x.com/ZkNoid

No responses yet