For the complete documentation index, see llms.txt. This page is also available as Markdown.

Run an RPC Node

To read Espresso settlement in your application, you run an RPC Node. To understand the role it plays, see Reading from Espresso.

What it does

The RPC Node (Espresso Rollup Node Proxy) is a Go service that sits between clients and a rollup's full node and enforces Espresso finality. It does not replace or modify the full node — it proxies JSON-RPC traffic in front of an unmodified node.

It has three parts:

  • Proxy — Intercepts every JSON-RPC request (including batch requests) before forwarding it to the full node. Any occurrence of the Espresso block tag (default "espresso") in the request params is replaced with the current Espresso-finalized L2 block number, then the request is forwarded transparently.

  • Verifier — A background loop that continuously compares data received from Espresso against the corresponding blocks on the full node. On a match it advances the finalized position and persists it; on a mismatch it retries at the next interval.

  • Store — Persists the Espresso-finalized L2 block number and the HotShot fallback height to a JSON file so state survives restarts. On restart the verifier resumes from both — the L2 block number is where verification left off, and the HotShot height is where to resume reading from Espresso.

The "espresso" tag always resolves to max(espresso finalized, eth finalized). Until the verifier confirms its first batch, the Espresso-finalized position has not advanced, so the tag returns that parent blockchain's finalized block; once batches start being confirmed, it reflects Espresso's faster finality whenever it is ahead. The Espresso-finalized block number is monotonically increasing — it never moves backwards across restarts, parent blockchain reorgs, or sequencer reorgs.

The proxy supports both OP Stack (--mode op) and Arbitrum Nitro / Orbit (--mode nitro) rollups. Clients integrate by pointing their RPC URL at the proxy and (optionally) using the "espresso" tag in their calls.

Prerequisites

The proxy runs in front of an existing, fully synced full node — it does not run one for you. Stand up the full node first, then point full_node_execution_rpc (and, for WebSockets, ws_full_node_execution_rpc) at it.

  • Arbitrum Nitro / Orbit: see Run a full node in the Arbitrum docs. Point nitro.feed_url at the full node's feed. You will need to enable and configure this on the full node.

  • OP Stack: see Node operators overview in the Optimism docs. The proxy sits in front of op-geth's JSON-RPC.

Architecture

RPC Node architecture

Build

Prebuilt container images are published to ghcr.io/espressosystems/espresso-rollup-node-proxy.

Network endpoints and contracts

Two of the required settings depend on which Espresso network you point at: the query service URL (query_service_url, both modes) and the light client contract address (op.light_client_address, OP mode). The current values are published on the Networks page:

Network

Query service URL (query_service_url)

Light Client address (op.light_client_address)

See the Networks page for the full, authoritative list of endpoints and contract addresses (including the Stake Table, ESP Token, Fee, and Reward Claim contracts).

Configuration

The proxy is configured via CLI flags or a JSON config file (or both — flags override file values). Pass --config <path> to load a file first, then apply any additional flags on top.

The examples below use placeholders such as <espresso-query-service-url> and <light-client-contract-address> — see Network endpoints and contracts above for the Mainnet and Decaf values to fill in. Replace <l2-chain-id> with your rollup's chain ID (see Finding the chain ID): namespace, eth_rpc, and query_service_url are now top-level settings required in both OP and Nitro modes. For <initial-hotshot-height>, see Initial HotShot height.

Example config file (OP Stack):

Example config file (Nitro):

Run with a config file:

Run with flags only (OP Stack):

Run with flags only (Nitro):

Example Config: ApeChain (Arbitrum Orbit)

ApeChain is a live Espresso-integrated Arbitrum Orbit chain (operated by Caldera), so it makes a useful concrete reference for filling in the Nitro placeholders above. The values below are public — the chain ID, Caldera's public RPC and sequencer feed, the Bridge contract on Arbitrum One, and the batch poster's signing key address:

A few notes for adapting this to your own chain:

  • namespace is the chain's own chain ID (ApeChain Mainnet is 33139).

  • full_node_execution_rpc is the full node's EVM JSON-RPC endpoint — the same endpoint clients would otherwise query directly (the proxy sits in front of it). ApeChain documents how to run your own node at docs.apechain.com/run-node.

  • eth_rpc must be your own Arbitrum One Mainnet RPC endpoint (ApeChain's parent chain) — point it at whichever provider or node you run.

  • feed_url is the message feed exposed by the Nitro full node you run in front of — point it at that node's feed, not at a separate service.

  • bridge_address is the chain's Bridge contract on Arbitrum One, available from the chain's public documentation. ApeChain publishes its contract addresses at docs.apechain.com.

  • valid_signing_key_addresses is either the chain's batch poster address or its sequencer signing key address, depending on the deployment. If you're unsure which applies to your chain, check with the Espresso team.

  • initial_hotshot_height is only used on the very first run, before a state file exists. The value shown is the height this deployment started streaming from — choose your own per Initial HotShot height.

  • eth_log_scan_block_range is optional and defaults to 10000. It caps how many Ethereum blocks the verifier scans per eth_getLogs query when fetching delayed messages — lower it if your Arbitrum One RPC provider rejects wide log ranges.

  • Optional settingslisten_addr, espresso_tag, store_file_path, and verification_interval are all optional; they appear above only to show concrete example values (verification_interval here is 50ms rather than the 10ms default). Anything you omit falls back to its default. This example also runs HTTP-only — the WebSocket proxy stays off unless you add both ws_listen_addr and ws_full_node_execution_rpc (see WebSockets). For every optional setting and its default, see the Optional reference table.

Docker

Run with a config file:

Mount a volume for --store-file-path so the persisted state survives container restarts.

Example docker-compose.yml (OP Stack):

Clients should point at the proxy (http://localhost:8080) rather than directly at the full node.

Configuration reference

The Required column shows ✅ for settings required in all modes, and ✅ OP / ✅ Nitro for settings required only in that mode. Everything else is optional and falls back to the listed default.

Required

Flag
JSON key
Required
Description

--mode

mode

Verifier mode: op or nitro

--namespace

namespace

Espresso namespace; must equal the chain's own chain ID

--full-node-execution-rpc

full_node_execution_rpc

Rollup execution layer RPC URL (the full node the proxy sits in front of)

--eth-rpc

eth_rpc

Ethereum / parent-chain RPC URL

--query-service-url

query_service_url

Espresso query service URL

--op.light-client-address

op.light_client_address

✅ OP

Espresso light client contract address on Ethereum

--op.batcher-address

op.batcher_address

✅ OP

OP batcher address

--op.batch-authenticator-address

op.batch_authenticator_address

✅ OP

Batch Authenticator contract address on Ethereum

--nitro.feed-url

nitro.feed_url

✅ Nitro

Nitro full node feed WebSocket URL

--nitro.bridge-address

nitro.bridge_address

✅ Nitro

Nitro Bridge contract address on Ethereum

--nitro.valid-signing-key-addresses

nitro.valid_signing_key_addresses

✅ Nitro

Valid signing key addresses (at least one)

Optional

Flag
JSON key
Default
Description

--listen-addr

listen_addr

:8080

Address the proxy listens on

--ws.listen-addr

ws_listen_addr

WebSocket listen address; set together with ws.full-node-execution-rpc to enable the WebSocket proxy

--ws.full-node-execution-rpc

ws_full_node_execution_rpc

Execution layer WebSocket RPC URL; required to enable the WebSocket proxy

--espresso-tag

espresso_tag

espresso

JSON-RPC block tag to intercept; set to finalized to back the standard finality tag with Espresso

--store-file-path

store_file_path

espresso_store.json

Path to the state persistence file

--verification-interval

verification_interval

10ms

How often the verifier polls for new confirmed batches

--finality-poll-interval

finality_poll_interval

1s

How often the finality poller queries the full node for the latest finalized block

--initial-hotshot-height

initial_hotshot_height

0

HotShot height to start streaming from; used only on first run, when no state file exists. Must be non-zero on a fresh start — the proxy exits otherwise (see Initial HotShot height)

--max-batch-size

max_batch_size

1000

Maximum requests in a JSON-RPC batch (0 = unlimited)

--max-request-body-size

max_request_body_size

5242880

Maximum request body size in bytes (0 = unlimited)

--log-level

log_level

info

Log level (debug, info, warn, error)

--log-format

log_format

json

Log output format (text or json)

--track-batch-latency

track_batch_latency

false

Log per-batch and average latency from HotShot finalization to verification

--nitro.wait-for-eth-finality

nitro.wait_for_eth_finality

false

Wait for Ethereum block finalization before fetching delayed messages

--nitro.eth-log-scan-block-range

nitro.eth_log_scan_block_range

10000

Max Ethereum blocks scanned per eth_getLogs query when fetching delayed messages; lower it for RPC providers that cap log ranges (0 uses the default)

Finding the chain ID

The namespace setting must equal the chains own chain Id. If you don't already know it, query the full node's EVM JSON-RPC endpoint (the same URL you set as full_node_execution_rpc) with eth_chainId:

The result is hex-encoded, so convert it to decimal for the namespace value — for example "0x8173" (33139) is ApeChain Mainnet.

If you have Foundry's cast installed, it returns the decimal chain ID directly:

Initial HotShot height

initial_hotshot_height (--initial-hotshot-height) sets the HotShot block height the verifier starts streaming from. It is only used on the first run, when no state file exists. Once the proxy has written its state file (store_file_path), every restart resumes from the persisted position and this setting is ignored — changing it later has no effect unless you delete the state file.

On a fresh start (no state file) you must set a non-zero value — the proxy refuses to start with initial_hotshot_height at its 0 default, because streaming the entire Espresso blockchain from genesis is not intended and would take a very long time.

Set it to a HotShot height whose block timestamp is before the current Ethereum-finalized L2 block's timestamp. Starting from before that point ensures the verifier streams forward without leaving a gap between the Espresso-confirmed data it reads and what the full node already treats as finalized. Until the verifier catches up, the "espresso" tag safely falls back to Ethereum-finalized state through the max(espresso finalized, eth finalized) resolution, so erring earlier is harmless.

To find the current HotShot block height, query the status/block-height endpoint of your query_service_url:

(Swap in the Mainnet query service URL from Network endpoints and contracts as needed.)

WebSockets

To enable the WebSocket proxy, two optional settings must both be present — the WebSocket listening address and the execution WebSocket RPC URL:

Flag
JSON key

--ws.listen-addr

ws_listen_addr

--ws.full-node-execution-rpc

ws_full_node_execution_rpc

If one or both are missing, the WebSocket port is not enabled. The startup logs indicate whether the WebSocket proxy is active or why it was skipped.

Verify the proxy is running

The proxy forwards traffic transparently to the full node from the moment it starts. The "espresso" tag resolves to max(espresso finalized, eth finalized), so until the verifier confirms its first batch it returns the chain's Ethereum-finalized block; once batches start being confirmed, it reflects Espresso's faster finality whenever it is ahead. The startup logs report the listening addresses, verifier progress, and whether the WebSocket proxy is active.

Point the examples below at your proxy by setting its URL once — use the proxy's listen address, which defaults to http://localhost:8080:

To confirm the proxy is up and reachable, query it the same way you would the underlying full node — for example, fetch the latest block:

Once the verifier has confirmed its first batch, the "espresso" tag begins tracking the Espresso-finalized L2 block (before that it returns the Ethereum-finalized block). The value is monotonically increasing — re-running this query over time should show it advance and never move backwards:

If the "espresso" tag returns an error or the same block indefinitely, check that the verifier is making progress in the logs and that --query-service-url and the mode-specific settings (--op.* / --nitro.*) are correct.

Using the proxy from a client

Clients integrate by pointing their RPC URL at the proxy (its listen address, e.g. http://localhost:8080) instead of directly at the full node. No other code changes are required — the proxy speaks the standard JSON-RPC API and forwards everything transparently.

To opt into Espresso finality, use the "espresso" block tag anywhere a client would normally pass a block tag such as "latest" or "finalized":

The proxy resolves the tag to max(espresso finalized, eth finalized), so clients always get Espresso's faster finality when it is ahead while safely falling back to Ethereum finality.

If you want existing clients to benefit without any code changes, run the proxy with --espresso-tag finalized. The proxy then backs the standard "finalized" tag with Espresso finality, so anything already requesting "finalized" transparently gets faster, Espresso-backed finality.

Troubleshooting

Common symptoms and what to check. For how the proxy behaves under outages, verification mismatches, reorgs, and force inclusion, see Edge cases and failure behavior.

  • Proxy exits at startup with "no state file found and --initial-hotshot-height is 0". On a fresh start you must set a non-zero initial_hotshot_height — see Initial HotShot height. It is ignored once a state file exists.

  • Proxy exits at startup with "invalid configuration". A required setting is missing or malformed; each offending field is logged on its own line. Check the Required settings for your mode.

  • Proxy exits at startup in OP mode while setting up the light client. Confirm eth_rpc is reachable and op.light_client_address is correct for your network (see Network endpoints and contracts).

  • The "espresso" tag returns an error or never advances. The verifier isn't making progress. At the default info log level a healthy verifier emits an advance line each time it moves the position forward — Successfully verified and advanced OP batches (OP) or successfully verified and advanced nitro messages (Nitro), each including the new hotshot_height. If you don't see these recurring, check the logs for verifier errors and confirm query_service_url and the mode-specific settings (op.* / nitro.*) are correct.

  • The "espresso" tag is stuck at the Ethereum-finalized block. Expected before the verifier confirms its first batch, or while Espresso is unreachable — the tag falls back to Ethereum finality and resumes once the verifier catches up. On startup you should see Starting OP Verifier / Starting Nitro Verifier (logging the position it resumed from), followed by the advance line above appearing repeatedly as it catches up.

  • The WebSocket endpoint isn't reachable. Both ws_listen_addr and ws_full_node_execution_rpc must be set; otherwise the WebSocket proxy is skipped. The startup logs say whether it is active or why it was skipped (see WebSockets). The WebSocket endpoint must also be enabled on the full node itself — ws_full_node_execution_rpc has to point at a node that actually serves WebSockets.

  • Nitro: errors fetching delayed messages, or the Eth RPC endpoint rejects wide eth_getLogs ranges. Lower nitro.eth_log_scan_block_range below your RPC provider's log-range cap.

Restart and crash recovery

All verifier progress lives in store_file_path (the Espresso-finalized block number and the HotShot fallback position). On restart the proxy loads this file and resumes from the persisted position, so a crash or restart never replays already-verified work and never moves the tag backwards. Remember that initial_hotshot_height is honored only on the very first run, when no state file exists — after that it is ignored (see Initial HotShot height).

Last updated