Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Infrastructure & Operations12 min read

OP Stack Node Clients: op-node, op-geth, op-reth

Understand the two-process OP Stack node architecture and how one op-node binary serves every Superchain chain.

TL;DR

An OP Stack node is not a single binary but a pair of processes: a rollup consensus/derivation client (op-node) and an Ethereum execution client (op-geth or op-reth) that op-node drives over the Engine API. op-node reads L1 for sequencer batch data, derives the canonical L2 chain, and pushes payloads to the execution client via newPayload and forkchoiceUpdated. The execution client exposes the standard eth_* JSON-RPC surface, so dapps see the same RPC regardless of whether op-geth or op-reth is used. One op-node binary serves any Superchain chain—Optimism, Base, Unichain—by loading a chain-specific rollup config rather than requiring a different binary. This article explains the architecture, configuration, verification, and troubleshooting of the OP Stack node client pair.

The Two-Process OP Stack Node Model

Unlike a monolithic Ethereum client such as Geth or Nethermind, an OP Stack node is composed of two distinct processes that communicate over the Engine API. The first is a rollup consensus and derivation client, typically op-node, which reads the L1 chain (Ethereum) for sequencer batch data and derivation inputs. The second is an Ethereum execution client—either op-geth (a Geth fork) or op-reth (a Reth-based client)—which maintains L2 state, executes transactions, and exposes the standard eth_* JSON-RPC interface. This separation is documented in the Optimism node architecture and is fundamental to how OP Stack chains operate.

Because the two processes are separate, you must run both and wire them together correctly. op-node does not execute transactions or maintain state; it only derives the canonical L2 chain from L1 data and instructs the execution client on which blocks to import. The execution client does not read L1 directly; it relies entirely on op-node for new payloads and forkchoice updates. This division of labor means that an unhealthy L1 RPC endpoint will stall op-node, which in turn stalls the execution client, even if the execution client itself is perfectly healthy.

The Engine API connection between op-node and the execution client is authenticated with a JWT secret. This is the same Engine API used by Ethereum's consensus layer, but here op-node acts as the consensus client. The execution client must be started with the Engine API enabled and the same JWT secret that op-node uses. Without this authentication, op-node cannot push payloads, and the node will not sync.

  • op-node: rollup consensus/derivation client; reads L1, derives L2, drives execution client via Engine API.
  • op-geth or op-reth: execution client; maintains L2 state, exposes eth_* JSON-RPC.
  • Engine API: authenticated channel (JWT) for newPayload and forkchoiceUpdated calls.
  • Both processes must run; neither is optional for a full OP Stack node.

Role of op-node: Derivation and Engine API Interaction

op-node is the brain of the OP Stack node. It continuously reads the L1 chain for two types of data: sequencer batch transactions (which contain L2 transaction data) and derivation inputs such as L1 block attributes. Using these, it derives the canonical L2 chain according to the rollup protocol rules. The derivation process is specified in the Optimism specifications and involves a state machine that tracks the L1 origin and the L2 safe head.

Once op-node has derived a new L2 block, it pushes the block payload to the execution client via the Engine API's newPayload method. It then calls forkchoiceUpdated to set the new head and, when applicable, the safe and finalized block hashes. This is the same Engine API flow used by Ethereum's consensus layer, but with OP Stack-specific payload attributes. The execution client validates and imports the block, updating its state and making the new block available via eth_getBlockByNumber.

op-node also exposes its own JSON-RPC namespace, optimism_*, which includes methods like optimism_syncStatus and optimism_outputAtBlock. These are useful for monitoring derivation progress and verifying that the L2 head is tracking the L1 derivation head. For a deeper dive into the Engine API calls, see Engine API newPayload and forkchoice.

  • Reads L1 for batches and derivation inputs.
  • Derives canonical L2 chain per rollup rules.
  • Pushes payloads via Engine API newPayload and forkchoiceUpdated.
  • Exposes optimism_* RPC for sync status and output roots.

Execution Clients: op-geth and op-reth

The execution client is responsible for maintaining L2 state, executing transactions, and serving the standard Ethereum JSON-RPC API. Two primary implementations exist: op-geth, which is a fork of Go Ethereum, and op-reth, which is built on the Reth client architecture. Both implement the same execution and state transition logic, and both expose the same eth_* JSON-RPC surface. This means that from a dapp's perspective, the choice of execution client does not change the RPC interface—eth_chainId, eth_blockNumber, eth_getBlockByNumber, and all other standard methods behave identically.

The choice between op-geth and op-reth is an operational trade-off. op-geth benefits from the maturity and familiarity of the Geth codebase, with extensive tooling and documentation. op-reth, being newer, may offer performance advantages in certain workloads due to its Rust implementation and modular architecture. However, both are production-ready and used by node operators. The decision should be based on your team's expertise, monitoring stack, and performance requirements, not on RPC compatibility.

Both clients support the Engine API and can be driven by op-node. They also support the same database formats and snapshot mechanisms, though op-reth may have different storage requirements. When running a node, you must ensure that the execution client version is compatible with your op-node version, as the Engine API and OP Stack protocol evolve.

  • op-geth: Geth fork, mature, familiar tooling.
  • op-reth: Reth-based, Rust, potential performance benefits.
  • Both expose identical eth_* JSON-RPC surface.
  • Choice is operational, not functional for dapps.

One op-node Binary for Any Superchain Chain

A key design feature of the OP Stack is that a single op-node binary can serve any chain in the Superchain—Optimism, Base, Unichain, and others—by loading a chain-specific configuration. This configuration, often called the rollup config or chain config, includes parameters such as the L2 chain ID, the system config contract addresses, the L1 chain it derives from, and the sequencer feed URL. There is no separate binary for each chain; the same codebase runs everywhere.

This is the mechanism behind the 'one Superchain' narrative. When you start op-node, you provide it with the rollup config for your target chain. For example, to run a Base node, you use the Base rollup config; to run an Optimism node, you use the Optimism rollup config. The op-node then reads the appropriate L1 contracts and derives the correct L2 chain. This uniformity simplifies tooling and allows infrastructure providers to support multiple chains with the same operational procedures.

The rollup config is typically distributed as a JSON file or fetched from a chain registry. It includes the L1 chain ID, the L2 chain ID, the batch inbox address, the sequencer address, and other parameters. For a list of OP Stack chains and their configurations, see the Optimism documentation.

  • Same op-node binary for all Superchain chains.
  • Chain-specific parameters loaded via rollup config.
  • Config includes L2 chain ID, L1 chain ID, contract addresses, sequencer feed.
  • Enables uniform tooling and operations across chains.

Standing Up an OP Stack Node: Build and Configuration

To run an OP Stack node, you need to build or pull the op-node and execution client binaries, then configure them to work together. Start by obtaining the latest releases from the official repositories: op-node from the Optimism monorepo, and op-geth or op-reth from their respective repositories. You will also need a healthy L1 Ethereum RPC endpoint, as op-node reads L1 for batches. This L1 endpoint is a hard dependency; if it is unavailable, op-node cannot derive new L2 blocks.

Next, generate a JWT secret for the Engine API authentication. Both op-node and the execution client must use the same secret. Then, start the execution client with the Engine API enabled, pointing it to the JWT secret and a data directory. For op-geth, this typically involves flags like --http, --http.api, --authrpc.jwtsecret, and --rollup. For op-reth, similar flags exist. After the execution client is running, start op-node with the L1 RPC URL, the rollup config, and the Engine API endpoint of the execution client.

Finally, expose the execution client's eth_* endpoint for dapp reads. This is the endpoint that applications will use to query the L2 chain. You can also expose op-node's optimism_* endpoint for monitoring. Ensure that your firewall rules allow access to the desired ports and that you have adequate disk space for the L2 database, which can grow large depending on the chain's activity.

#!/bin/bash
# Example: Start op-geth and op-node for an OP Stack chain

# Generate JWT secret
openssl rand -hex 32 > jwt.hex

# Start op-geth (execution client)
./op-geth \
  --datadir ./datadir \
  --http \
  --http.addr 0.0.0.0 \
  --http.port 8545 \
  --http.api eth,net,web3,debug \
  --http.corsdomain '*' \
  --authrpc.addr 0.0.0.0 \
  --authrpc.port 8551 \
  --authrpc.jwtsecret ./jwt.hex \
  --rollup \
  --rollup.sequencerhttp https://mainnet-sequencer.optimism.io \
  --syncmode full

# Start op-node (rollup consensus client)
./op-node \
  --l1 https://ethereum-rpc.example.com \
  --l2 http://localhost:8551 \
  --rollup.config ./rollup.json \
  --rpc.addr 0.0.0.0 \
  --rpc.port 9545 \
  --p2p.listen.ip 0.0.0.0 \
  --p2p.listen.tcp 9003 \
  --p2p.listen.udp 9003 \
  --sequencer.enabled=false \
  --jwt-secret ./jwt.hex

Verifying Node Health and Derivation Progress

After starting the node, you must verify that it is syncing correctly and that the derivation process is honest. Begin by calling eth_chainId on the execution client's endpoint and confirming it matches the intended L2 chain ID. Then, call eth_blockNumber twice with a short delay to confirm the head is advancing. If the block number does not increase, the node is not syncing.

Next, check the safe and finalized block tags using eth_getBlockByNumber with 'safe' and 'finalized'. These should move forward as derivation catches up. The safe head represents blocks derived from L1 batches, while the finalized head represents blocks that are also finalized on L1. If the safe head is stuck behind the unsafe head (the sequencer head), it indicates that op-node is not deriving new blocks, possibly due to an unhealthy L1 RPC or a configuration issue.

Finally, query op-node's optimism_syncStatus to confirm that the L2 head tracks the L1 derivation head. This method returns information about the current L1 and L2 heads, as well as the safe and finalized L2 heads. If the L2 head is far behind the L1 head, derivation may be lagging. For a detailed guide on using the Engine API for sync status, see OP Stack node sync status via the Engine API.

// Node.js script to verify OP Stack node health
const { ethers } = require('ethers');

async function checkHealth() {
  const provider = new ethers.JsonRpcProvider('http://localhost:8545');
  const opNodeProvider = new ethers.JsonRpcProvider('http://localhost:9545');

  // 1. Check chain ID
  const chainId = await provider.getNetwork();
  console.log('Chain ID:', chainId.chainId);

  // 2. Check block number advancement
  const block1 = await provider.getBlockNumber();
  await new Promise(r => setTimeout(r, 5000));
  const block2 = await provider.getBlockNumber();
  console.log('Block number:', block1, '->', block2);
  if (block2 <= block1) console.warn('Head not advancing!');

  // 3. Check safe and finalized blocks
  const safe = await provider.getBlock('safe');
  const finalized = await provider.getBlock('finalized');
  console.log('Safe block:', safe?.number, 'Finalized block:', finalized?.number);

  // 4. Check op-node sync status
  const syncStatus = await opNodeProvider.send('optimism_syncStatus', []);
  console.log('Sync status:', JSON.stringify(syncStatus, null, 2));
}

checkHealth().catch(console.error);

Unsafe, Safe, and Finalized Heads: Confirmation Strategy

The OP Stack defines three distinct block tags: unsafe, safe, and finalized. The unsafe head is the latest block produced by the sequencer; it is not yet derived from L1 and could be reorganized if the sequencer misbehaves. The safe head is derived from L1 batches and is considered safe from sequencer reorgs, but it could still be reorganized if L1 itself reorgs. The finalized head is derived from L1 blocks that are finalized on Ethereum, making it the most secure but also the most delayed.

When building applications, you must choose a confirmation strategy deliberately. For high-value transactions, waiting for the finalized head provides the strongest guarantee. For lower-value or time-sensitive operations, the safe head may be sufficient. The unsafe head should only be used for real-time UI updates, not for settlement. The trade-off is between latency and security; the finalized head lags behind the unsafe head by the L1 finality time (typically 12-15 minutes on Ethereum).

You can query these heads using eth_getBlockByNumber with the tags 'safe' and 'finalized', or by using the optimism_syncStatus method on op-node. For more on L1 derivation timestamps, see OP Stack L1 derivation timestamp.

  • Unsafe: sequencer head, not derived, subject to sequencer reorgs.
  • Safe: derived from L1 batches, safe from sequencer reorgs, but not L1-finalized.
  • Finalized: derived from L1-finalized blocks, strongest guarantee, highest latency.
  • Choose confirmation tag based on transaction value and latency tolerance.

Self-Hosting vs Hosted Endpoints: Trade-offs and Selection

Running your own OP Stack node gives you full control, privacy, and independence from third-party providers. However, it requires significant resources: disk space, bandwidth, and ongoing maintenance. You must monitor the node, apply updates, and handle L1 RPC dependencies. For many teams, a hosted endpoint is more practical, especially for development or low-volume applications.

Hosted RPC providers, such as OnFinality, offer OP Stack chain endpoints that abstract away the node infrastructure. You get a reliable eth_* endpoint without running any software. The trade-off is that you rely on the provider for uptime and data accuracy. For production systems with high compliance requirements, self-hosting may be necessary. For a comparison of endpoint options, see Optimism RPC endpoints (RPC Assistant).

When choosing a hosted provider, consider factors like rate limits, supported methods, and geographic distribution. OnFinality provides API service with various plans; you can review RPC pricing to find a suitable tier. For a broader overview of networks, visit the OnFinality Learn hub.

  • Self-hosting: full control, privacy, but high operational overhead.
  • Hosted endpoints: low maintenance, but dependency on provider.
  • Consider rate limits, method support, and cost.
  • OnFinality offers OP Stack chain endpoints with flexible plans.

Limitations and Assumptions

This article assumes you are running a full OP Stack node, not a sequencer. Sequencer operation requires additional configuration and is not covered here. It also assumes you have a reliable L1 Ethereum RPC endpoint; without it, op-node cannot function. The performance and resource requirements vary by chain and by execution client; you should benchmark your setup under expected load.

The OP Stack is under active development. Client versions, configuration flags, and RPC methods may change. Always refer to the official documentation for the latest information. The verification methods described here are reproducible, but the exact results will depend on your node's sync state and network conditions. We do not provide specific latency or throughput numbers, as these are environment-dependent.

Finally, this article focuses on the client architecture and does not cover advanced topics like snapshots, pruning, or custom rollup configurations. For those, consult the Optimism specifications and client-specific documentation.

  • Assumes full node, not sequencer.
  • Requires healthy L1 RPC endpoint.
  • Client versions and flags may change; check official docs.
  • No specific performance numbers provided; measure your own.

Troubleshooting Playbook

If op-node fails to start or cannot connect to the execution client, check the Engine API authentication. Ensure both processes use the same JWT secret and that the execution client's authrpc port is accessible. Common errors include 'invalid jwt' or 'connection refused'. Verify the JWT file path and permissions.

If op-node is running but the L2 head is not advancing, check the L1 RPC endpoint. op-node reads L1 for batches; if the L1 RPC is unhealthy or rate-limited, derivation will stall. Test the L1 endpoint with a simple eth_blockNumber call. Also, verify that the rollup config matches the chain you intend to run; a chain ID mismatch will cause op-node to derive the wrong chain.

If the safe head is stuck behind the unsafe head, it means op-node is not deriving new safe blocks. This could be due to a lag in L1 batch submission or an issue with the batch inbox contract. Check the L1 batch inbox address in the rollup config and ensure it matches the chain's current contract. For more on reorg handling, see Bor and Erigon client reorg handling.

  • Engine API auth failures: check JWT secret and port.
  • op-node not advancing: verify L1 RPC health and rollup config.
  • Safe head stuck: check batch inbox contract and L1 batch submission.
  • Chain ID mismatch: confirm rollup config matches target chain.

Next Steps and Further Resources

Once your node is synced, you can connect your dapp to the execution client's eth_* endpoint. For production, consider setting up monitoring for block height, peer count, and derivation lag. You can use the optimism_syncStatus method to track derivation progress. If you prefer a managed solution, explore Optimism RPC endpoints (RPC Assistant) or the OnFinality networks page.

To deepen your understanding, review the Optimism specifications for derivation details and the Optimism node architecture for operational guidance. For related topics on OP Stack RPC, see our articles on Engine API newPayload and forkchoice and OP Stack node sync status via the Engine API.

Finally, stay updated with client releases. Both op-geth and op-reth are actively developed, and new versions may include performance improvements and bug fixes. Regularly check the official repositories and upgrade your node to benefit from the latest features.

  • Connect dapps to execution client's eth_* endpoint.
  • Monitor block height, peer count, and derivation lag.
  • Consider hosted endpoints for low-maintenance setups.
  • Keep clients updated and refer to official docs.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started