> For the complete documentation index, see [llms.txt](https://docs.espressosys.com/network/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.espressosys.com/network/developer/operators/run-a-node.md).

# Run a Validator Node

Register, configure, and operate an Espresso validator node on Mainnet or Decaf Testnet.

Espresso is secured by a permissionless, delegated Proof of Stake validator set. To participate, you register your node in the stake table contract on Ethereum, have ESP delegated to it, and run the Espresso node software.

This page covers both networks. Every command and environment variable is the same on Mainnet and Decaf; only the contract addresses, endpoints, and genesis file differ. Network-specific values are collected in [Network values](#network-values) and referenced by name throughout.

{% hint style="info" %}
**Running a Decaf node.** New Decaf operators are welcome. Decaf ESP is not publicly distributed, so once your node is registered and running, ask the Espresso team for a delegation in the [#decaf-node-ops](https://discord.com/channels/854451048012709889/1417966013861072936) channel of the [Espresso Discord](https://discord.gg/GJa4gznGfU). Your node joins consensus once that delegation is active.
{% endhint %}

## Network values

|                      | **Mainnet**                                                       | **Decaf Testnet**                                                 |
| -------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- |
| Purpose              | Production. Real ESP staking and rewards.                         | Testing integrations and node setups before Mainnet.              |
| L1                   | Ethereum                                                          | Ethereum Sepolia                                                  |
| Container image      | `ghcr.io/espressosystems/espresso-network/espresso-node:20260910` | `ghcr.io/espressosystems/espresso-network/espresso-node:20260910` |
| Genesis file         | `/genesis/mainnet.toml`                                           | `/genesis/decaf.toml`                                             |
| Stake table contract | `0xCeF474D372B5b09dEfe2aF187bf17338Dc704451`                      | `0x40304fbe94d5e7d1492dd90c53a2d63e8506a037`                      |
| Query service        | `https://query.main.net.espresso.network`                         | `https://query.decaf.testnet.espresso.network`                    |
| Config cache         | `https://cache.main.net.espresso.network`                         | `https://cache.decaf.testnet.espresso.network`                    |
| State relay          | `https://state-relay.main.net.espresso.network`                   | `https://state-relay.decaf.testnet.espresso.network`              |
| Staking UI           | <https://stake.espresso.network/>                                 | <https://stake.decaf.espresso.network/>                           |

The genesis files are built into the official container images. Operators building their own images should build from [release 20260910](https://github.com/EspressoSystems/espresso-network/releases/tag/20260910) and include the file for their network ([mainnet.toml](https://github.com/EspressoSystems/espresso-network/blob/20260910/data/genesis/mainnet.toml), [decaf.toml](https://github.com/EspressoSystems/espresso-network/blob/20260910/data/genesis/decaf.toml)), pointing the node at it.

For ESP token, light client, fee, and reward claim contract addresses, and for block explorers, see [Networks](/network/network/networks.md).

{% hint style="info" %}
Environment variables use the prefix `ESPRESSO_NODE_` for node settings and `ESPRESSO_L1_` for L1 client settings, except `ESPRESSO_STATE_RELAY_SERVER_URL`. The binary is named `espresso-node`.
{% endhint %}

## Generate keys

Each validator needs three keypairs:

* **Staking key** (BLS): signs consensus messages. Supports the aggregation consensus performance depends on.
* **State key** (Schnorr): signs finalized consensus states, which drive onchain Espresso state updates.
* **x25519 key**: encrypts and authenticates connections to other validators.

Generate all three with the `keygen` utility shipped in the node image. `$IMAGE` is the container image for your network from [Network values](#network-values):

```bash
docker run -v ./keys:/keys $IMAGE keygen -o /keys
```

This writes `./keys/0.env` on the host, containing all three keypairs:

```
# Mnemonic: ...
# Index: 0
ESPRESSO_NODE_PUBLIC_STAKING_KEY=BLS_VER_KEY~...
ESPRESSO_NODE_PRIVATE_STAKING_KEY=BLS_SIGNING_KEY~...
ESPRESSO_NODE_PUBLIC_STATE_KEY=SCHNORR_VER_KEY~...
ESPRESSO_NODE_PRIVATE_STATE_KEY=SCHNORR_SIGNING_KEY~...
ESPRESSO_NODE_PUBLIC_X25519_KEY=X25519_PK~...
ESPRESSO_NODE_PRIVATE_X25519_KEY=X25519_SK~...
```

Generating the keys inside the container keeps them off the host unless you mount a volume, as above.

Without a persistent `ESPRESSO_NODE_PRIVATE_X25519_KEY` the node generates a random ephemeral key on every start, which will not match the public key registered onchain.

`keygen --help` lists further options. `--mnemonic <MNEMONIC>` derives all three keys deterministically from a BIP-39 phrase instead of a randomly generated one, and `-n <N>` writes `0.env` through `<N-1>.env`. `keygen` ignores `--index`, deriving each file's keys from its own file index. Without a mnemonic it generates a fresh one from OS entropy and, when `-o` is given, records it and the index as comments at the top of each `.env` file.

Use `keygen --scheme x25519` only to rotate the x25519 key on its own. It derives from a new random mnemonic, so replace both the public and private x25519 lines in the key file when you do, and re-register the public key.

The node can also be configured with `ESPRESSO_NODE_KEY_MNEMONIC` (plus `ESPRESSO_NODE_KEY_INDEX`) instead of a key file, deriving all three keys at startup. The mnemonic conflicts with `ESPRESSO_NODE_KEY_FILE`; use one or the other. To print the public keys of a node configured this way, use the `pub-key` utility from the same image:

```bash
docker run -e ESPRESSO_NODE_KEY_MNEMONIC $IMAGE pub-key --scheme x25519 --index $KEY_INDEX
```

`--scheme` also accepts `bls`, `schnorr`, and `all`.

## Register the validator

Registration associates the node's consensus keys with an Ethereum address in the stake table contract. That address receives commission and does not exist on the node itself.

Before registering, decide the P2P address other validators will dial. The stake table requires both the x25519 public key and the P2P address at registration, and a validator without them cannot participate in consensus.

The P2P address must:

* Use the node's public IP address or DNS name, not a private or loopback address.
* Be reachable over **TCP from the public internet** and route to the node's P2P bind port (`ESPRESSO_NODE_CLIQUENET_BIND_ADDRESS`, default `9977`). Behind NAT, a firewall, or a load balancer, register the public address and forward that port to the node.
* If it is an IP address, match the source IP of the node's **outgoing** connections. The registered address is not only a listen address: peers check the source IP of every incoming connection against it.

{% hint style="warning" %}
Registering an IP address while the node's outbound traffic leaves through a different IP breaks connectivity in one direction. This happens when ingress and egress use separate paths, for example an inbound load balancer or proxy with a separate NAT gateway for egress, or a pool of egress IPs. Peers reject those connections and log `party has invalid ip addr`.

Register a **DNS name** instead when the egress IP differs from the ingress IP or is not stable. Registering a DNS name disables the source IP check, so any source IP is accepted for the node.
{% endhint %}

```bash
docker run -e L1_PROVIDER -e STAKE_TABLE_ADDRESS -e MNEMONIC -e ACCOUNT_INDEX \
    -e CONSENSUS_PRIVATE_KEY -e STATE_PRIVATE_KEY \
    -e X25519_KEY -e P2P_ADDR \
    ghcr.io/espressosystems/espresso-network/staking-cli:main \
    staking-cli register-validator \
    --commission $COMMISSION \
    --metadata-uri $METADATA_URI
```

| Environment variable    | Meaning                                                                                                                                                                                           |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `L1_PROVIDER`           | RPC provider URL for the network's L1. Ethereum for Mainnet, Ethereum Sepolia for Decaf.                                                                                                          |
| `STAKE_TABLE_ADDRESS`   | The stake table address for your network. See [Network values](#network-values).                                                                                                                  |
| `MNEMONIC`              | Ethereum mnemonic, combined with `ACCOUNT_INDEX` to derive the wallet that signs the transaction.                                                                                                 |
| `ACCOUNT_INDEX`         | Ethereum account index, combined with `MNEMONIC`.                                                                                                                                                 |
| `CONSENSUS_PRIVATE_KEY` | The node's staking key, of the form `BLS_SIGNING_KEY~...`.                                                                                                                                        |
| `STATE_PRIVATE_KEY`     | The node's state key, of the form `SCHNORR_SIGNING_KEY~...`.                                                                                                                                      |
| `X25519_KEY`            | The node's x25519 public key, of the form `X25519_PK~...`.                                                                                                                                        |
| `P2P_ADDR`              | The `host:port` address other validators dial, for example `validator.example.com:9977`.                                                                                                          |
| `COMMISSION`            | Share of rewards the validator keeps, in percentage points with up to 2 decimal places, from `0.00` to `100.00`. `12.34` means 12.34%. The remainder is distributed proportionally to delegators. |
| `METADATA_URI`          | URL where validator metadata is hosted, in JSON or OpenMetrics format.                                                                                                                            |

The Ethereum wallet that signs this transaction is separate from the node key mnemonic used in [Generate keys](#generate-keys). The example above signs with a mnemonic (`MNEMONIC` + `ACCOUNT_INDEX`); `staking-cli` also accepts a raw private key (`PRIVATE_KEY`) or a Ledger device (`--ledger`, with `--account-index`). See [Choose your type of wallet](https://github.com/EspressoSystems/espresso-network/blob/main/staking-cli/README.md#choose-your-type-of-wallet-mnemonic-private-key-or-ledger) in the staking-cli README.

Additional requirements:

* Each Ethereum account (derived from `MNEMONIC` + `ACCOUNT_INDEX`) needs enough gas on the L1 to call the registration method.
* Each BLS key can be registered only once.
* Each Ethereum account can register only one validator. Running multiple validators requires different account indices or mnemonics.
* The P2P address must be publicly routable. `staking-cli` rejects addresses that are not, unless `--skip-reachability-check` is passed.

A successful registration looks like this:

{% code overflow="wrap" %}

```
Success! transaction hash: 0xa632dfea882d80855d2cc5e6713d9fc839cdd5df5aa29f6e9ce8d5a5ec8da615
event: ValidatorRegisteredV3 { account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, commission: 1234, metadataUri: https://validator.example.com/metadata.json, x25519Key: 0x..., p2pAddr: validator.example.com:9977 }
```

{% endcode %}

The event prints commission in basis points, so `1234` is the 12.34% passed on the command line.

Values registered onchain take 2 to 3 epochs to become active in consensus.

### Rotate keys and registered values

Everything registered onchain can be changed later. Each command is signed by the same Ethereum wallet that registered the validator, and takes `-e MNEMONIC -e ACCOUNT_INDEX -e L1_PROVIDER -e STAKE_TABLE_ADDRESS`.

| Value                      | Command                                                                  |
| -------------------------- | ------------------------------------------------------------------------ |
| Staking key and state key  | `update-consensus-keys`                                                  |
| x25519 key and P2P address | `update-network-config`                                                  |
| x25519 key only            | `update-x25519-key`                                                      |
| P2P address only           | `update-p2p-addr`                                                        |
| Commission                 | `update-commission`, see [Update the commission](#update-the-commission) |
| Metadata URI               | `update-metadata-uri`                                                    |

#### Rotate the consensus keys

Generate a new key set, then submit both new private keys. `staking-cli` uses them to sign a proof of ownership; they are not stored onchain.

```bash
docker run -e MNEMONIC -e ACCOUNT_INDEX -e L1_PROVIDER -e STAKE_TABLE_ADDRESS \
    -e CONSENSUS_PRIVATE_KEY -e STATE_PRIVATE_KEY \
    ghcr.io/espressosystems/espresso-network/staking-cli:main \
    staking-cli update-consensus-keys
```

The new keys become active in the third epoch after the command runs, so update the node's key file at that point, not immediately. If the signing keys are held offline, sign in advance and pass the result with `--node-signatures signatures.json`.

#### Rotate the x25519 key or P2P address

```bash
docker run -e MNEMONIC -e ACCOUNT_INDEX -e L1_PROVIDER -e STAKE_TABLE_ADDRESS \
    ghcr.io/espressosystems/espresso-network/staking-cli:main \
    staking-cli update-network-config \
        --x25519-key $ESPRESSO_NODE_PUBLIC_X25519_KEY \
        --p2p-addr $PUBLIC_HOST:9977
```

`update-x25519-key` and `update-p2p-addr` change one value at a time. The node's `ESPRESSO_NODE_PRIVATE_X25519_KEY` must match the registered public key, and `ESPRESSO_NODE_CLIQUENET_BIND_ADDRESS` must use the same port as the registered P2P address.

#### Update the metadata URI

```bash
docker run -e MNEMONIC -e ACCOUNT_INDEX -e L1_PROVIDER -e STAKE_TABLE_ADDRESS \
    -e CONSENSUS_PUBLIC_KEY \
    ghcr.io/espressosystems/espresso-network/staking-cli:main \
    staking-cli update-metadata-uri --metadata-uri $METADATA_URI
```

`CONSENSUS_PUBLIC_KEY` is the validator's BLS public key, used to validate the metadata at the new URL. Pass `--skip-metadata-validation` to submit without that check.

### Update the commission

Commission can be changed after registration, subject to limits enforced by the stake table contract (these are adjustable by the contract admin):

* One commission **increase** per `minCommissionIncreaseInterval`, 7 days at deployment.
* Increases capped at `maxCommissionIncrease`, 500 basis points (5 percentage points) at deployment, for example 10% to 15%.
* Decreases are neither rate-limited nor capped, and do not reset the increase timer.
* The new value must differ from the current one, or the call reverts with `CommissionUnchanged`.

```bash
docker run -e L1_PROVIDER -e STAKE_TABLE_ADDRESS -e MNEMONIC -e ACCOUNT_INDEX \
    ghcr.io/espressosystems/espresso-network/staking-cli:main \
    staking-cli update-commission \
    --new-commission 2.54
```

### Deregister

Deregistration is a two-step process: `deregister-validator` exits the active set and unbonds all delegators, then after the exit escrow period (about 7 days) `claim-validator-exit` returns the validator's principal to the operator address.

{% hint style="info" %}
Accumulated staking rewards must be claimed separately with `claim-rewards`. Deregistration does not claim them for you. See [Claim rewards](#claim-rewards).
{% endhint %}

Step 1 removes the node from the active validator set immediately and starts the escrow:

```bash
docker run -e MNEMONIC -e ACCOUNT_INDEX -e L1_PROVIDER -e STAKE_TABLE_ADDRESS \
    ghcr.io/espressosystems/espresso-network/staking-cli:main \
        staking-cli deregister-validator
```

Step 2, once the escrow elapses, withdraws the principal to the operator wallet:

```bash
docker run -e MNEMONIC -e ACCOUNT_INDEX -e L1_PROVIDER -e STAKE_TABLE_ADDRESS \
    ghcr.io/espressosystems/espresso-network/staking-cli:main \
        staking-cli claim-validator-exit --validator-address $VALIDATOR_ADDRESS
```

`VALIDATOR_ADDRESS` is the Ethereum address associated with the validator at registration, derived from `MNEMONIC` + `ACCOUNT_INDEX`.

## Stake and rewards

A registered node participates only once ESP is delegated to it. On Mainnet, delegations can come from any ESP holder, and operators can bootstrap by delegating to their own node. On Decaf, ask the Espresso team for a delegation in [#decaf-node-ops](https://discord.com/channels/854451048012709889/1417966013861072936) on the [Espresso Discord](https://discord.gg/GJa4gznGfU).

{% hint style="info" %}
Participation is limited to a dynamic, permissionless set of 100 nodes. In each epoch (roughly 24 hours) the 100 nodes with the most delegated stake form the active set.
{% endhint %}

### Delegate and undelegate

Delegate, undelegate, and claim withdrawals through the staking UI, using a wallet such as MetaMask or Ledger:

* **Mainnet:** <https://stake.espresso.network/>
* **Decaf:** <https://stake.decaf.espresso.network/>

See [Delegate $ESP](/network/network/delegation-ui.md) for a walkthrough.

Delegations become active 2 epochs after the L1 transaction is finalized, and the minimum delegation is 1 ESP. Undelegating starts an exit escrow of about 7 days, after which the withdrawal can be claimed; effective stake decreases 2 epochs after L1 finalization. Only one undelegation can be pending per validator at a time, so claim each withdrawal before starting another from the same validator.

### Claim rewards

Validators and delegators accrue rewards every block their validator proposes. Rewards do not auto-compound: claim them manually and re-delegate if desired.

Rewards can be checked and claimed from the staking UI linked above. The `staking-cli` equivalents are useful for scripting and multisig flows:

```bash
docker run -e MNEMONIC -e ACCOUNT_INDEX -e L1_PROVIDER -e STAKE_TABLE_ADDRESS \
    -e ESPRESSO_URL=https://query.main.net.espresso.network \
    ghcr.io/espressosystems/espresso-network/staking-cli:main \
        staking-cli unclaimed-rewards

docker run -e MNEMONIC -e ACCOUNT_INDEX -e L1_PROVIDER -e STAKE_TABLE_ADDRESS \
    -e ESPRESSO_URL=https://query.main.net.espresso.network \
    ghcr.io/espressosystems/espresso-network/staking-cli:main \
        staking-cli claim-rewards
```

On Decaf, use `https://query.decaf.testnet.espresso.network`. The ESP token address is read from the stake table contract.

The claim transaction interacts with the Reward Claim contract on the L1; see [Networks](/network/network/networks.md) for the address. For how rewards are calculated and distributed, and for using `staking-cli` with a Ledger, see the [staking-cli README](https://github.com/EspressoSystems/espresso-network/blob/main/staking-cli/README.md).

## Run the node

### Command

The recommended configuration serves the query and light client APIs on SQL storage. With the embedded SQLite database, the container entrypoint supplies the storage module itself, so the command must not repeat it:

```bash
# ESPRESSO_NODE_EMBEDDED_DB=true
espresso-node -- http -- query -- light-client
```

With an external Postgres server, pass the storage module explicitly:

```bash
# ESPRESSO_NODE_EMBEDDED_DB=false (default)
espresso-node -- storage-sql -- http -- query -- light-client
```

{% hint style="warning" %}
With `ESPRESSO_NODE_EMBEDDED_DB=true`, passing `storage-sql` again fails with `optional module storage-sql can only be started once`, and passing `storage-fs` silently overrides SQLite.
{% endhint %}

With the query module enabled, the catchup, status, and state signature routes are served without being listed.

Please enable `light-client`. It serves leaves, headers, and stake tables that a client can verify against the light client contract on Ethereum, which is how other nodes catch up without having to trust the node they fetch from. The more operators serve it, the less that catchup traffic depends on any single provider.

[espresso-for-dummies](https://github.com/EspressoSystems/espresso-for-dummies) holds worked Docker Compose files for both networks and both storage backends, covering the services, volumes, and graceful shutdown. Take the file layout from there and the image tag, modules, and environment from this page.

A validator that does not serve the query API adds `-- status -- catchup` and drops `-- query -- light-client`. It still needs SQL storage for its consensus state. Without `status` such a node exposes no consensus metrics at all, so none of the alerts under [Monitoring](#monitoring) work. This configuration is not recommended: see [Storage and retention](#storage-and-retention).

### Environment

The same on both networks:

```
ESPRESSO_NODE_EMBEDDED_DB=true
RUST_LOG="warn"
RUST_LOG_FORMAT="json"
```

Network-specific, taken from [Network values](#network-values):

```
# /genesis/mainnet.toml or /genesis/decaf.toml
ESPRESSO_NODE_GENESIS_FILE

# The network's state relay server
ESPRESSO_STATE_RELAY_SERVER_URL

# Peers used to catch up consensus state over HTTP. At least one is required.
# Add further endpoints, comma-separated, to avoid depending on a single peer.
ESPRESSO_NODE_STATE_PEERS

# Peers used by the query module to fetch missing availability data over HTTP.
ESPRESSO_NODE_API_PEERS
```

Chosen by the operator:

```
# An HTTP JSON-RPC endpoint for the network's L1. Required.
ESPRESSO_L1_PROVIDER # e.g. https://mainnet.infura.io/v3/<API-KEY>

# A `ws://` or `wss://` endpoint for the network's L1. Optional but recommended,
# since it decreases the load on your provider.
ESPRESSO_L1_WS_PROVIDER # e.g. wss://mainnet.infura.io/v3/<API-KEY>

# Port on which to serve metrics, healthchecks, and the query API. Default 8080.
ESPRESSO_NODE_API_PORT

# Path in the container for persistent data. With SQLite the database is created
# in a `sqlite` subdirectory of this path.
ESPRESSO_NODE_STORAGE_PATH # e.g. /mount/espresso/store/

# Path in the container to the key file.
ESPRESSO_NODE_KEY_FILE # e.g. /mount/espresso/keys/0.env

# The address cliquenet binds for the validator mesh, in host:port form. The port
# must match the P2P address registered in the stake table and be reachable over
# TCP from the public internet. Default 0.0.0.0:9977.
ESPRESSO_NODE_CLIQUENET_BIND_ADDRESS

# Connection to the Postgres server. Required when ESPRESSO_NODE_EMBEDDED_DB=false.
ESPRESSO_NODE_POSTGRES_HOST
ESPRESSO_NODE_POSTGRES_PORT
ESPRESSO_NODE_POSTGRES_DATABASE
ESPRESSO_NODE_POSTGRES_USER
ESPRESSO_NODE_POSTGRES_PASSWORD
ESPRESSO_NODE_POSTGRES_USE_TLS
```

Instead of `ESPRESSO_NODE_KEY_FILE`, the keys can be set individually:

* `ESPRESSO_NODE_PRIVATE_STAKING_KEY` = `BLS_SIGNING_KEY~...`
* `ESPRESSO_NODE_PRIVATE_STATE_KEY` = `SCHNORR_SIGNING_KEY~...`
* `ESPRESSO_NODE_PRIVATE_X25519_KEY` = `X25519_SK~...`

Mount `ESPRESSO_NODE_STORAGE_PATH` and `ESPRESSO_NODE_KEY_FILE` as volumes. With Postgres storage the node keeps no local state, so only the key file needs a volume.

{% hint style="info" %}
The advertised P2P address is **not** an environment variable. Peers learn each other's address and x25519 key from the stake table contract, so the address is set with `staking-cli`.
{% endhint %}

### First start

On its very first start a node has no network configuration saved locally, and must fetch it from a running peer. Set `ESPRESSO_NODE_CONFIG_PEERS` to the network's config cache, for example `https://cache.main.net.espresso.network`.

The node stores the configuration once it joins, so this variable is not needed on subsequent restarts. It is needed again after any reset or migration that clears the node's storage.

### Networking summary

| Port                     | Protocol | Direction       | Purpose                                                                      |
| ------------------------ | -------- | --------------- | ---------------------------------------------------------------------------- |
| `9977` (default)         | TCP      | Inbound, public | Validator consensus mesh (cliquenet). Must match the registered P2P address. |
| `ESPRESSO_NODE_API_PORT` | TCP      | Inbound         | Metrics, healthchecks, query API.                                            |

The node serves plain HTTP. For production deployments, put the API port behind a reverse proxy that terminates TLS.

## Storage and retention

Two independent storage systems have separate lifecycles. Operators need to configure only the second.

### Consensus storage

Every validator reconstructs the full block payload for every view and writes it to consensus storage. Block data is the responsibility of the whole validator set, which is why the recommended configuration below is the same for every operator.

Consensus storage is a bounded working set, not accumulating history. As each view decides, the node emits its events and deletes the corresponding rows. Garbage collection is automatic and view-based, and operators should not need to change it. It targets about 1 GB of database, with a default retention of roughly one week of views and a floor of roughly three days.

This applies to every validator, including one running without the query module. Size such a node for write throughput, one full-payload write and delete per view, rather than for accumulated history.

### Query database

Enabling the query module adds the historical database that serves the availability, node, and state APIs. This is the recommended configuration for all operators.

Pruning is off by default, so an unconfigured query node retains everything. For a non-archival node, enable pruning with a two-week retention window:

```
ESPRESSO_NODE_DATABASE_PRUNE=true
ESPRESSO_NODE_PRUNER_TARGET_RETENTION=14d
ESPRESSO_NODE_PRUNER_MINIMUM_RETENTION=14d
ESPRESSO_NODE_PRUNER_STATE_TARGET_RETENTION=14d
ESPRESSO_NODE_PRUNER_STATE_MINIMUM_RETENTION=14d
ESPRESSO_NODE_PRUNER_PRUNING_THRESHOLD=<your provisioned disk>
```

Target retention is what the pruner deletes past on every run. Minimum retention is the floor it will not cross even under disk pressure, so setting both to `14d` makes two weeks a hard window rather than a target. The `STATE_` variables cover the Merklized state tables; without them, that state is still pruned at the 7-day default.

Durations take one integer and one unit from `ns`, `mc`, `ms`, `s`, `m`, `h`, `d`, `w`, or a colon form (`hh:mm` or `hh:mm:ss`). A bare number of seconds is rejected. `14d` and `2w` are equivalent.

Defaults for the remaining pruner settings, which most operators can leave alone:

| Variable                                 | Default | Meaning                                                                                            |
| ---------------------------------------- | ------- | -------------------------------------------------------------------------------------------------- |
| `ESPRESSO_NODE_PRUNER_PRUNING_THRESHOLD` | `3TB`   | Database size above which the pruner deletes past target retention, down toward minimum retention. |
| `ESPRESSO_NODE_PRUNER_MAX_USAGE`         | `8000`  | Basis points of the threshold to prune back to, so 80%.                                            |
| `ESPRESSO_NODE_PRUNER_INTERVAL`          | `5400s` | Time between pruner runs. The pruner sleeps one interval before its first run.                     |
| `ESPRESSO_NODE_PRUNER_BATCH_SIZE`        | `1000`  | Block heights deleted per transaction.                                                             |

{% hint style="warning" %}
Setting any retention variable without `ESPRESSO_NODE_DATABASE_PRUNE=true` has no effect. The pruner configuration is only built when pruning is enabled.
{% endhint %}

{% hint style="warning" %}
Pruning does not shrink the database to the retention window. Two tables that dominate long-run growth, the hash table and the aggregate table, are never deleted by the pruner, and Merkle state pruning keeps the newest node at every path indefinitely. On Postgres the pruner also skips `VACUUM`, so deleted rows return space to Postgres for reuse but not to the filesystem. Provision disk for peak usage, not for the size of the retention window.
{% endhint %}

### Archival nodes

An archival node retains all historical data and can serve queries for arbitrary historical state. Run the query module without any pruning variables. `ESPRESSO_NODE_ARCHIVE=true` additionally clears the pruning watermark so the node backfills missing data from peers, which is what turns a pruned database into a full one. It conflicts with `ESPRESSO_NODE_DATABASE_PRUNE`.

### Storage backends

Both SQL backends support the full API; no endpoint is unavailable under SQLite. The differences are operational.

|                         | SQLite (`ESPRESSO_NODE_EMBEDDED_DB=true`)           | Postgres                                                                      |
| ----------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------- |
| Deployment              | Single container, no separate database server       | Requires operating a Postgres server                                          |
| Reclaiming pruned space | Returns it to the filesystem via incremental vacuum | Returns it to Postgres for reuse, not to the filesystem                       |
| Query concurrency       | Query load shares the node's connection pool        | Separate, tunable query pool (`ESPRESSO_NODE_DATABASE_QUERY_MAX_CONNECTIONS`) |
| Suited to               | Most validators                                     | Nodes serving heavy external query load                                       |

`ESPRESSO_NODE_PRUNER_INCREMENTAL_VACUUM_PAGES` (default `8000`) sets how many pages each SQLite vacuum reclaims. It has no effect on Postgres.

{% hint style="warning" %}
Filesystem storage (`storage-fs`) does not support pruning and cannot serve state catchup. Use SQL storage for any node running the query module.
{% endhint %}

## Hardware requirements

These figures are observed from running nodes rather than a guaranteed minimum, and Espresso block data grows over time. Provision headroom and the ability to scale disk on demand.

|         | Validator with query API, pruned               | Archival node                                  |
| ------- | ---------------------------------------------- | ---------------------------------------------- |
| CPU     | 4 cores, plus 2 for a separate Postgres server | 4 cores, plus 2 for a separate Postgres server |
| Memory  | 8 GB, plus 4 GB for a separate Postgres server | 8 GB, plus 4 GB for a separate Postgres server |
| Storage | 500 GB SSD                                     | 2.5 TB SSD                                     |

A Mainnet archival node measured about 1.7 TB in September 2026 and grows roughly 215 GB per month, of which most is Merkle tree and hash data that pruning does not bound. The pruned figure above is provisioning guidance, not a steady state: see the caveat under [Query database](#query-database).

Disk throughput matters more than capacity headroom for a query node. Database migrations on version upgrades are IO bound, and provisioning more IOPS temporarily makes them substantially faster.

## TCP tuning

Consensus traffic runs over TCP. The node sets `TCP_NODELAY` and its own keepalive policy but does not size socket buffers, so kernel autotuning bounds are the operator's to set. These settings are optional OS-level tuning outside the node; apply them if your validator is connecting across long-haul links.

{% code title="/etc/sysctl.d/espresso-opts.conf" %}

```
net.ipv4.tcp_congestion_control=bbr
net.ipv4.tcp_rmem=8192 262144 67108864
net.ipv4.tcp_wmem=4096 16384 536870912
net.ipv4.tcp_adv_win_scale=0
net.ipv4.tcp_notsent_lowat=131072
net.ipv4.tcp_slow_start_after_idle=0
```

{% endcode %}

Apply without rebooting:

```bash
sudo sysctl -p /etc/sysctl.d/espresso-opts.conf
```

This applies to *all* TCP connections on the machine. To scope the settings to the node's container instead, add them to the Docker Compose service:

```yaml
sysctls:
    net.ipv4.tcp_congestion_control: "bbr"
    net.ipv4.tcp_rmem: "8192 262144 67108864"
    net.ipv4.tcp_wmem: "4096 16384 536870912"
    net.ipv4.tcp_adv_win_scale: "0"
    net.ipv4.tcp_notsent_lowat: "131072"
    net.ipv4.tcp_slow_start_after_idle: "0"
```

Or as `--sysctl` flags on `docker run`.

## Monitoring

The node exposes Prometheus metrics at `/status/metrics` on `ESPRESSO_NODE_API_PORT`. See the [Status API](/network/developer/espresso-api/status-api.md) for the full endpoint reference.

{% hint style="warning" %}
`/healthcheck` is a static liveness probe for the HTTP server. It does not reflect consensus health. Alert on the metrics below instead.
{% endhint %}

**Consensus liveness**

* `consensus_current_view` should advance continuously at the network's view rate. Static for more than a minute means the node has fallen out of sync or the network has lost liveness.
* `consensus_last_decided_view` should track `consensus_current_view` closely. Current view advancing while last decided view stalls points at a network-wide consensus problem.
* `/status/time-since-last-decide` returns seconds since the last decide and is the single best liveness alert.

**Proposal health**

* `consensus_number_of_timeouts_as_leader` counts views where this node was leader and failed to propose. Every increment is a missed proposal and warrants investigation. There is no separate missed-proposal or missed-vote metric.
* Participation is visible from another query node, for example the Espresso-hosted service:

  ```bash
  curl <query service URL>/node/participation/proposal/current
  curl <query service URL>/node/participation/vote/current
  ```

  The response maps BLS public keys to participation scores. A healthy node scores close to `1.0`. Below `0.95` warrants investigation. A dip lasting up to one epoch is expected after a restart or outage. To read a past epoch, replace `current` with the epoch number; that segment must be a number, so there is no `previous` shorthand.

**Peer connectivity**

* `consensus_cliquenet_*` metrics carry a `peer` label holding the peer's x25519 public key, base58-encoded rather than in the `X25519_PK~...` form used at registration.
* `consensus_cliquenet_connect_attempts` climbing for a peer while the connection never establishes is the signature of a misregistered P2P address or x25519 key. Fix this by re-registering the correct values with `staking-cli`, not by changing node environment variables. See [Rotate the x25519 key or P2P address](#rotate-the-x25519-key-or-p2p-address).

**Node resources**

* `process_resident_memory_bytes` and `process_open_fds` against the file descriptor limit.
* Database size is not exposed as a metric. Watch it at the operating system or Postgres level.
* `consensus_version{desc=...}` reports the running image tag. Verify it matches the tag in [Network values](#network-values).

## Related references

* [Debug P2P Connectivity](/network/developer/operators/run-a-node/p2p-troubleshooting.md): checks for validators that cannot connect to peers.
* [Networks](/network/network/networks.md): endpoints, contract addresses, and block explorers.
* [Delegate $ESP](/network/network/delegation-ui.md): staking UI walkthrough for delegators.
* [Status API](/network/developer/espresso-api/status-api.md): the full metrics endpoint reference.
* [staking-cli README](https://github.com/EspressoSystems/espresso-network/blob/main/staking-cli/README.md): every `staking-cli` subcommand, including Ledger support.
* [espresso-for-dummies](https://github.com/EspressoSystems/espresso-for-dummies): Docker Compose examples for both networks.
