> For the complete documentation index, see [llms.txt](https://docs.ethernity.cloud/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ethernity.cloud/developer-guide/the-guide/managing-dapp-state-esr.md).

# Managing dApp State (ESR)

By default a task is stateless: it receives an input, computes, returns a result, and forgets everything. The **Enclave State Registry (ESR)** gives your dApp durable state that persists between tasks — encrypted inside the enclave, stored on IPFS, and pointed to on-chain.

The enclave is the only thing that can read it. That is the point: your state is not visible to the node running your task, to other dApps, or to anyone reading the blockchain.

### How it works

```
your backend            enclave (SGX)              chain
------------            -------------              -----
state.commit(...)  -->  encrypt state
                        compute the CID
                        publish to storage   -->   commit(key, cid, version)
```

Only a **pointer** goes on-chain — the content stays encrypted. Each key has a version that increments on every commit.

State is available network-wide: your next task can run anywhere and still read what the last task committed.

The core guarantee: a task that returns SUCCESS has its state already confirmed on-chain. The attested trustedzone refuses to sign a result until every state commit the task made is verified on the registry — so a successful result and persisted state are the same event, never two things to reconcile. If the node fails to persist your commits, the task comes back as ESR\_RELAY\_TIMEOUT (code 37) and the order is refunded automatically: you resubmit, you never lose state silently.

### Enabling it

Run `ecld-init` and answer **yes** at the Enclave State Registry step, or add this to `.config.json`:

```json
"ESR": {
  "enabled": true,
  "contract_address": ""
}
```

Leave `contract_address` empty. `ecld-build` fills in the correct registry for your network automatically, and refuses to build for a network where no registry is deployed.

### Using it in your backend

```python
from ecld_state import StateRegistry

def get_profile(user_id):
    return StateRegistry().get(f"profile-{user_id}")

def add_score(user_id, points):
    state = StateRegistry()
    return state.commit(
        f"profile-{user_id}",
        lambda s: {**s, "score": s.get("score", 0) + points},
    )
```

For Nodenithy (JavaScript) the API is the same:

```javascript
const { StateRegistry } = require('../ecld_state');

async function addScore(userId, points) {
  const state = new StateRegistry();
  return state.commit(`profile-${userId}`, (s) => ({
    ...s,
    score: (s.score || 0) + points,
  }));
}
```

#### The API

| Call                  | What it does                                                                   |
| --------------------- | ------------------------------------------------------------------------------ |
| `get(key)`            | Returns the decrypted state, or `{}` if nothing was ever stored                |
| `commit(key, mutate)` | Read-modify-write: `mutate` receives the current state and returns the new one |
| `get_version(key)`    | The current version number; `0` means never written                            |
| `enclave_address`     | The enclave's on-chain identity address — the namespace your state lives under |

commit uses optimistic concurrency. If another task commits between your read and your write, it re-reads and retries automatically, so parallel tasks cannot silently overwrite each other.

One run can make at most 256 commits. Past that, commit fails with task code 38 (ESR\_COMMIT\_LIMIT\_EXCEEDED) and the over-limit commit is not applied — the earlier ones stand. In practice you should never get close: one commit can update an entire object, so batch your writes instead of committing per field, or split very large jobs across tasks.

Two things in that table deserve a plain-language definition:

* **`mutate` is a function you supply.** It receives the current state (a dict / object — `{}` on first write) and returns the new state to store. You transform what is there instead of overwriting blindly, which is what makes concurrent commits safe.
* **`enclave_address` is the enclave's address — think of it as the&#x20;*****wallet address owned by the enclave*****.** It is the enclave's own on-chain identity, derived inside the enclave; it is not your wallet and not the node's. Every state key you write lives under this address, and it is the address you pass to `ecld-info esr state --enclave <A>` to inspect your commits.

### Returning results: `ecld_result`

`ecld_result(...)` is the way to return a task's result. It replaces the older `___etny_result___` (which still works, unchanged) and adds two things:

* **Automatic encoding.** Return anything — a `dict`/`list`/number rides as JSON, a string as text, `bytes` as base64. You never encode by hand, and a result never fails on encoding.
* **State attached by default.** The result carries the ESR state of every key the task touched, so the caller gets the fresh state in the same result — no separate read task needed.

```python
def add_score(user_id, points):
    state = StateRegistry()
    new = state.commit(f"profile-{user_id}",
                       lambda s: {**s, "score": s.get("score", 0) + points})
    return ecld_result(new)               # value + attached state

def render_chart(match_id):
    png = draw(match_id)                   # bytes
    return ecld_result(png)                # auto base64

def cheap_ping():
    return ecld_result("ok", state=False)  # suppress the state attachment
```

```javascript
return ecldResult(next);                       // value + attached state
return ecldResult(buffer, { state: false });   // binary, no state
```

`state=True` (default) attaches full state for every key touched; `state="meta"` attaches only `{key, version, cid}`; `state=False` attaches nothing.

### The task caller

Inside the enclave, `task_caller()` returns the wallet that submitted the task (the DO-request owner). Use it to scope state per user without trusting anything the client sends:

```python
def my_profile():
    return StateRegistry().get(f"profile-{task_caller()}")
```

### State ownership and permissions

The first task to commit a key becomes its **owner** (the task caller at that moment). Only the owner can grant or revoke access; by default nobody else can read or write it.

```python
esr_grant(key, address, "read")      # let address read this key
esr_grant(key, address, "write")     # let address write this key
esr_revoke(key, address, "read")     # take it back
esr_set_public_read(key, True)       # anyone may read (writes still restricted)
esr_transfer(key, new_owner)         # hand ownership to another address
esr_owner(key)                       # the owner address, or None if unowned
esr_acl(key)                         # owner-only: the full permission list
```

```javascript
await esrGrant(key, address, 'write');
await esrSetPublicRead(key, true);
await esrTransfer(key, newOwner);
```

`get`/`commit` enforce these automatically: a caller with no permission gets a permission error, never the state. A granted **writer** can commit a key it does not own; a granted **reader** can read but not write.

### Exactly-once commits — the nonce

Every commit advances the key's nonce by exactly 1 — a public, on-chain counter (a free eth\_call reads it; no task, no gas). If you don't pass a nonce, the registry assigns the next value automatically. If the same task might be submitted twice (client retries, impatient users), pin the nonce yourself: read it and pass exactly the last accepted value + 1. A duplicate, stale, or skipped-ahead nonce fails the commit and the state is untouched.

```python
n = esr_nonce("orders")                     # last accepted nonce (0 if none)
state.commit("orders", apply_order, nonce=n + 1)
```

```javascript
const n = await esrNonce('orders');
await state.commit('orders', applyOrder, { nonce: n + 1 });
```

A duplicate returns task code 36 (ESR\_NONCE\_VIOLATION) — meaning "already applied", not a failure: the first submission did the work. Passing nonce=0 explicitly is refused by the SDK: 0 is only a wire-level sentinel for "no nonce given" — it never binds the commit to the value 0; the registry auto-assigns the next number instead, and pinned nonces start at 1. You can also pre-plan a batch: submit tasks with n+1, n+2, n+3 and they apply strictly in that order. Inspect the sequence any time with ecld-info esr nonce --enclave ADDRESS --key KEY.

### Reading state without paying for a task

Getting an enclave's current state normally means submitting a read task — a full order. The runner avoids that automatically: `esr_read` serves unchanged state straight from a cache after a **free** on-chain check, and places an order only when the state has actually changed. **The cache is on by default** — you don't enable anything.

```python
r = runner.esr_read(enclave="my_enclave", key=f"profile-{user}", node=NODE)
# r.state, r.version, r.from_cache
```

```javascript
const r = await runner.esrRead({ key: `profile-${user}`, registryAddress });
// r.state, r.version, r.fromCache
```

The cache refreshes automatically from every task result (results carry attached state), so right after a write the next read is a free cache hit. On a miss the runner runs one read task and re-caches. The free on-chain check means it never serves stale state.

Controls, if you need them: `enable_state_cache(file=...)` to persist across restarts (default is in-memory), and `disable_state_cache()` to turn it off. For an authenticated read (e.g. a per-user membership check), point it at your own read function with `read_code=` instead of the built-in fetch.

Because a SUCCESS result already means the commits are confirmed on-chain, a client that ran the task itself does not need to wait for anything. Polling is for the other case — a web3 observer that didn't run the task and wants to react when someone else's task changes state. The runner polls the on-chain version for free (eth\_calls, no orders) instead of you guessing at a delay:

```python
v = runner.esr_version("dashboard", enclave_wallet=W)["version"]
# ... submit the state-writing task ...
runner.esr_wait_for_version("dashboard", v, enclave_wallet=W)   # returns when version > v
```

```javascript
const { version } = await runner.esrVersion({ key: 'dashboard', registryAddress, enclaveAddress });
// ... submit the state-writing task ...
await runner.esrWaitForVersion({ key: 'dashboard', sinceVersion: version, registryAddress, enclaveAddress });
```

### Designing your keys

A key is any string — keccak256 of it identifies the slot on-chain. Use one key per independent thing so unrelated writes never conflict:

* `profile-<user_id>` — one slot per user
* `game-<match_id>` — one slot per match
* `config` — a single global slot

Everything under one key is read and written together, so avoid putting unrelated data in the same key: two tasks touching different parts of it will contend over the same version.

Keys are also the unit of ordering. Commits to the same key are strictly serialized network-wide — if two tasks write one key at the same time, exactly one lands and the other fails visibly (retry it); commits to different keys run fully in parallel, even on different nodes. Sharded keys like profile-\<user\_id> therefore aren't just tidy — they are what lets your dApp's writes scale concurrently.

### Testing it locally — free

`ecld-test` emulates the full ESR locally, **on by default**: `StateRegistry` get/commit, ownership and permissions, and `task_caller()` all behave exactly as on-chain, against an in-memory registry — zero orders, zero gas. State persists between runs in `.ecld-esr-local.*.json`, so a counter advances across invocations just like real state. Use `--caller` to test the ACL from another identity. See [Running your dApp](https://docs.ethernity.cloud/developer-guide/the-guide/running-your-dapp).

To confirm a commit actually landed on-chain (the CID and version advanced), use `ecld-info esr state` — free, no task. See [Inspecting your dApp](https://docs.ethernity.cloud/developer-guide/the-guide/inspecting-your-dapp).

### Testnet warning

On testnet there is no enclave attestation, which means the enclave identity — and therefore its wallet and its state encryption key — can be reproduced by anyone who runs your published image.

Treat testnet state as functional testing only:

* do not fund testnet enclave wallets with real value
* do not store real user data in testnet state

### Troubleshooting

| What you see                                                  | What it means                                                                                                                                                                                      |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ESR is enabled but no ESR registry is deployed on <network>` | That network has no registry. Build for a supported network.                                                                                                                                       |
| Task returns `CONFIG_ERROR` (32)                              | The enclave was built without a required value. Re-run `ecld-build`.                                                                                                                               |
| Task returns `ESR_GAS_LIMIT_EXCEEDED` (34)                    | The task's commits exceeded its gas budget. Commit less often or batch state changes under fewer keys.                                                                                             |
| Task returns `SECURITY_VIOLATION` (35)                        | A state commit ran under a caller that was not the task submitter — the ownership check was bypassed. Do not forge the caller; commit as the real submitter.                                       |
| Task fails with a stack trace as the result                   | Your backend raised during execution. The full traceback is returned as the task result — read it; the enclave is fine and the order completed.                                                    |
| `holds a pointer that is not a CID`                           | A previous version of your code wrote an invalid pointer.                                                                                                                                          |
| Task returns ESR\_RELAY\_TIMEOUT (37)                         | The node failed to persist your state commits on-chain. Not your bug: the order is refunded automatically — resubmit the task.                                                                     |
| Task returns ESR\_COMMIT\_LIMIT\_EXCEEDED (38)                | The run tried more than 256 commits (the per-run cap). Batch writes — one commit can update a whole object — or split the work across tasks. Earlier commits stand; no refund (this is dApp-side). |

### A complete example

The SDK ships a working end-to-end example at `examples/esr-counter`. Its `esr_selftest()` function needs no gas and no chain access, so it is the fastest way to confirm your enclave is built correctly before you start debugging.
