Skip to main content
info

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

experimental

Offchain storage is currently an experimental feature and is subject to change in the future.

Offchain Storage

One of Mina's unique features is its succinctness, both in computation and storage. To prevent state bloat and maintain Mina's efficiency and verifiability, we use offchain storage solutions for handling large volumes of data.

In a previous section, we introduced the concept of on-chain Values. Since Mina currently only supports a total of 8 on-chain Field elements, we need to leverage offchain storage to extend that capacity. This approach maintains a provably secure connection between the on-chain smart contract and the off-chain data, such as that stored in an archive node.

Design

Presently, Offchain storage offers support for two types of state: OffchainState.Field, representing a single field element, and OffchainState.Map, akin to the key-value maps found in JavaScript, like let myMap = new Map<string, number>();.

All offchain state resides within a single Merkle Map, which is essentially a wrapper around a Merkle Tree. Practically speaking, there are no constraints on the number of state fields and maps that a developer can store in a smart contract using Offchain storage.

Under this framework, Actions and Reducer are utilized to manage state changes, with actions dispatching state updates and reducers settling them. Additionally, a Merkle Tree is employed to maintain a provably secure commitment to the data, with the root stored on-chain.

Prior to users accessing published state, it must first undergo settlement. Thanks to the design of Offchain storage, all state is recoverable from actions alone, eliminating the need for additional events or external data storage.

Utilizing Offchain Storage

Prerequisites

The OffchainState API is accessible within the Experimental namespace. To use OffchainState, import Experimental from o1js version 1.2.0 or higher.

import { Experimental } from 'o1js';

const { OffchainState, OffchainStateCommitments } = Experimental;

Setting up Offchain Storage

To integrate Offchain storage, developers must initially define an Offchain state configuration and a state proof type, then prepare the smart contract. The OffchainState configuration allows specification of the desired Offchain state type, including key-value pairs in a map and any additional required state.

The StateProof type will subsequently be used to finalize published state changes using a recursive reducer.

const offchainState = OffchainState({
players: OffchainState.Map(PublicKey, UInt64),
totalScore: OffchainState.Field(UInt64),
});

class StateProof extends offchainState.Proof {}

Developers also need to set the smart contract instance and assign it to the offchain storage. This also compiles the recursive Offchain zkProgram in the background and assigns the Offchain state to a smart contract instance.

let contract = new MyContract(contractAddress);
offchainState.setContractInstance(contract);

// compile Offchain state program
await offchainState.compile();
// compile smart contract
await ExampleContract.compile();

When the offchain state requires settlement, an Offchain storage proof must be generated and provided to the smart contract's settle() method. This method automatically retrieves all pending actions (state changes) and resolves them using a recursive reducer. Finally, the proof is passed to the settle() method.

let proof = await offchainState.createSettlementProof();

await Mina.transaction(sender, () => {
// settle all outstanding state changes
contract.settle(proof);
})
.sign([sender.key])
.prove()
.send();

Configuring Your Smart Contract

The smart contract requires a field indicating the previously defined commitment to the offchain state. This field allows accessing offchain state similar to any other on-chain value.

class MyContract extends SmartContract {
@state(OffchainStateCommitments) offchainState = State(
OffchainStateCommitments.empty()
);
}

The contract also need a settle() method to resolve all pending state updates. This method verifies a recursive proof to finalize all pending state changes, with the proof being generated before invoking the settle() method.

class MyContract extends SmartContract {
// ...
@method
async settle(proof: StateProof) {
await offchainState.settle(proof);
}
}
note

State is only available after it was settled via settle()!

Utilizing Offchain Storage

Now developers can utilize Offchain storage in any of their smart contract methods, as demonstrated below:

class MyContract extends SmartContract {
// ...
@method
async useOffchainStorage(playerA: PublicKey) {
let totalScoreOption = await offchainState.fields.totalScore.get(); // retrieve totalScore
let totalScore = totalScoreOption.orElse(0n); // unwrap the Option and return a default value if the entry if empty
offchainState.fields.totalScore.update({
from: totalScoreOption,
to: totalScore.add(1),
}); // increment totalScore, set a precondition on the state, if `from` is undefined apply the update unconditionally

let playerOption = await offchainState.fields.players.get(playerA); // retrieve an entry from the map, returning an Option
let score = playerOption.orElse(0n); // unwrap the player's score Option and return a default value if the entry is empty
offchainState.fields.players.update(playerA, {
from: fromOption,
to: score.add(1),
});
}
}

Currently, Offchain storage of type Field support field.get() and field.overwrite(newValue), while maps support map.get(key) and map.overwrite(key, newValue). The .overwrite() method sets the value without taking into account the previous value. If the value is modified by multiple zkkApps concurrently, interactions that are applied later will simply be overwritten!

All Offchain storage types also provide an .update() method which is a safe version of .overwrite(). The .update() method lets you define preconditions on the state that you want to update. If the precondition of the previous value does not match, the update will not be applied.

Additional Resources

This feature remains experimental, indicating that it is currently under active development. For further insight into its implementation, please refer to the following pull requests and examples on GitHub: