# Tutorial 6: Off-Chain Storage

> Keep more state than a zkApp account can hold, using the offchain state API in o1js, and settle it with a proof.

Canonical URL: https://docs.minaprotocol.com/zkapps/tutorials/offchain-storage

In [Tutorial 5: Common Types and Functions](common-types-and-functions), you learned how to use Merkle trees to refer to large amounts of data stored off-chain.

A zkApp account holds 32 field elements of state. This tutorial shows how to keep much more than that, with only a commitment on chain, using the offchain state API that o1js provides.

The full example is in [examples/zkapps/06-offchain-storage](https://github.com/MinaProtocol/docs2/tree/main/examples/zkapps/06-offchain-storage). It runs on a local blockchain, so you need no network, no faucet and no funded accounts:

- [src/NumberStorageContract.ts](https://github.com/MinaProtocol/docs2/blob/main/examples/zkapps/06-offchain-storage/src/NumberStorageContract.ts) — the offchain state declaration and the contract
- [src/main.ts](https://github.com/MinaProtocol/docs2/blob/main/examples/zkapps/06-offchain-storage/src/main.ts) — a worked run: write, settle, read, update

## Why Off-Chain Storage?

When you build an application for testing and local use, you can build and store a Merkle root locally.

However, when you build a production-ready, distributed zkApp, you need more than this. All users that interact with your zkApp must be able to retrieve and modify the latest state.

Any data that modifies a zkApp must be available somewhere for other users to access.

### Off-Chain Storage and Decentralization

Solutions to storage span a large spectrum from inexpensive and more centralized to more expensive and more decentralized. The decentralized solutions are more expensive because they replicate and prove the stored data.

Your off-chain storage needs depend on the zkApp you are building and the guarantees you want that zkApp to have.

### Offchain state in o1js

The approach in this tutorial keeps the data in **actions**, which are dispatched to the zkApp account and therefore available to anyone who reads the chain. The contract holds only a commitment to the data.

Three parts make it work:

- **A declaration.** You say which fields and maps exist and what types they hold.
- **A commitment in on-chain state.** One state field of type `OffchainState.Commitments` holds a Merkle root and an action state.
- **A settlement proof.** Writes are actions first. A proof folds the pending actions into the commitment, and a `settle()` method puts the new commitment on chain.

A value is readable only after it is settled. That is the cost of the approach, and it is also what makes concurrent writes safe: an update names the value it expects to replace, and a settlement drops an update whose expected value no longer matches.

:::note Experimental
The offchain state API is exported under `Experimental`, so its shape can change between o1js releases. The example in this repository is built on every pull request, so it tracks the current release.
:::

## Prerequisites

- The latest version of the [zkApp CLI](https://www.npmjs.com/package/zkapp-cli)
- Node.js 22.19.5 or later, which is what o1js 3 requires

## Create the project

1. Create a project with the `zk project` command and select `none` when asked about a UI:

  ```sh
  $ zk project 06-offchain-storage
  ```

1. Change to the project directory and clear the scaffolding:

  ```sh
  $ cd 06-offchain-storage
  $ rm src/Add.ts src/Add.test.ts src/interact.ts
  $ zk file src/NumberStorageContract
  ```

## Declare the offchain state

Declare what the contract stores. A map from an index to a value takes the place of the Merkle tree of the earlier version of this tutorial, and a field counts the entries:

```ts title="src/NumberStorageContract.ts"
import {
  Experimental,
  Field,
  method,
  SmartContract,
  state,
  UInt64,
} from 'o1js';

const { OffchainState } = Experimental;

/**
 * The offchain state this contract keeps.
 *
 * `numbers` is a map from an index to a value, the same shape the earlier
 * version of this tutorial kept in a Merkle tree on a storage server. `total`
 * counts how many entries have been written.
 *
 * Only a commitment to all of this lives on chain. The data itself travels as
 * actions, and a settlement proof folds those actions into the commitment.
 */
export const offchainState = OffchainState(
  {
    numbers: OffchainState.Map(Field, Field),
    total: OffchainState.Field(UInt64),
  },
  {
    // 2^10 entries is plenty for a tutorial and keeps proving quick.
    logTotalCapacity: 10,
    maxActionsPerUpdate: 4,
  }
);

/** The proof that `settle()` consumes. */
export class StateProof extends offchainState.Proof {}
```

`logTotalCapacity` sizes the state: 10 gives 2<sup>10</sup> entries. A smaller capacity means fewer constraints and quicker proofs. `maxActionsPerUpdate` is how many writes one method may make.

## Write the contract

The contract holds one piece of on-chain state, the commitment, and binds the offchain state to itself:

```ts title="src/NumberStorageContract.ts"
export class NumberStorageContract extends SmartContract {
  /** The only onchain state: commitments to everything held offchain. */
  @state(OffchainState.Commitments) offchainStateCommitments =
    offchainState.emptyCommitments();

  offchainState = offchainState.init(this);
```

Add a method that writes a new entry. `update` takes the value it expects to find. Passing `undefined` as `from` requires that the entry is empty, so this method cannot overwrite an entry that already holds a value:

```ts title="src/NumberStorageContract.ts"
@method async setNumber(index: Field, value: Field) {
  this.offchainState.fields.numbers.update(index, {
    from: undefined,
    to: value,
  });

  const total = await this.offchainState.fields.total.get();
  this.offchainState.fields.total.update({
    from: total,
    to: total.orElse(0n).add(1),
  });
}
```

Add a method that replaces a value, where the caller names the value being replaced:

```ts title="src/NumberStorageContract.ts"
@method async updateNumber(index: Field, from: Field, to: Field) {
  this.offchainState.fields.numbers.update(index, { from, to });
}
```

Finally, add the method that settles. It takes the proof and hands it to the offchain state. Then close the class with `}`:

```ts title="src/NumberStorageContract.ts"
@method async settle(proof: StateProof) {
  await this.offchainState.settle(proof);
}
```

## Run it

`src/main.ts` runs the contract on a local blockchain, with the first test account as the deployer. Bind the state to the contract instance, then compile both the offchain state program and the contract:

```ts title="src/main.ts"
const contract = new NumberStorageContract(zkAppKey.toPublicKey());

// `init()` in the contract binds the offchain state to the contract class;
// the instance has to be named too, so the state knows which account to read
// actions from.
const state = contract.offchainState;
state.setContractInstance(contract);

console.log('compiling...');
await offchainState.compile();
await NumberStorageContract.compile();
```

After the deployment, write a value. At this point the write is an action, and reading the entry returns none:

```ts title="src/main.ts"
// A write is an action first. Nothing is readable until it is settled.
console.log('writing 42 at index 1...');
const writeTx = await Mina.transaction(deployer, async () => {
  await contract.setNumber(Field(1), Field(42));
});
await writeTx.prove();
await writeTx.sign([deployer.key]).send();

const beforeSettle = await state.fields.numbers.get(Field(1));
console.log(
  'before settling, index 1 is:',
  beforeSettle.isSome.toBoolean() ? beforeSettle.value.toString() : 'not set yet'
);
```

A settlement makes a proof over the pending actions and sends it to `settle()`. `main.ts` puts this in a function, because it settles more than once:

```ts title="src/main.ts"
async function settle() {
  const proof = await state.createSettlementProof();
  const tx = await Mina.transaction(deployer, async () => {
    await contract.settle(proof);
  });
  await tx.prove();
  await tx.sign([deployer.key]).send();
}
```

Settle, and the value becomes readable:

```ts title="src/main.ts"
console.log('settling...');
await settle();

const stored = await state.fields.numbers.get(Field(1));
console.log('after settling, index 1 is:', stored.value.toString());
console.log(
  'entries written:',
  (await state.fields.total.get()).value.toString()
);
```

Running `npm start` in the example prints the whole sequence:

```text
compiling...
deploying...
writing 42 at index 1...
before settling, index 1 is: not set yet
settling...
after settling, index 1 is: 42
entries written: 1
updating index 1 from 42 to 43...
index 1 is now: 43
```

## What happens to a stale write

`update` is the reason to prefer this API over keeping a tree yourself. Two users who read the same value and both write are not a lost update: the settlement applies the first and drops the second, because the second names a value that is no longer there.

The example's test suite asserts this. After index 1 has moved from 42 to 43, an update that still expects 42 changes nothing. `call()` sends a transaction from the deployer, and `numberAt()` reads an entry:

```ts title="src/NumberStorageContract.test.ts"
await call(async () => contract.updateNumber(Field(1), Field(42), Field(99)));
await settle();

assert.strictEqual(
  (await numberAt(1)).value.toString(),
  '43',
  'a stale update overwrote a newer value'
);
```

`overwrite` is the other option, and it does exactly what its name says: it ignores the previous value. Use it only where a lost update does not matter.

## Conclusion

You have stored more state than a zkApp account can hold, with one commitment on chain, and you have seen why a write becomes readable only after a settlement proof.

Check out [Tutorial 7: Oracles](oracle) to learn how to bring data from the outside world into a zkApp.
