Standalone Docker Compose stack for running an external Whitechain RPC node. The node follows the canonical Whitechain L2 chain over L1 derivation plus P2P, and forwards user transactions to the Whitechain network.
Each node is a pair of services:
op-reth– execution client, exposes JSON-RPC and WebSocketop-node– consensus client, derives the chain from L1 and peers over libp2p
Two independent choices define a node. The profiles below are fixed combinations of them.
Storage – how much state op-reth keeps:
- Pruned (
--full) – keeps only recent state, prunes history. Smallest disk. Serves a complete public RPC for current data. - Archive (no
--full) – keeps the full historical state. Largest disk. Required for historical tracing andeth_callat old blocks.
Sync method – how op-reth obtains state:
- consensus-layer (op-node default) – op-node derives the chain from L1 and feeds blocks to op-reth one by one; op-reth re-executes every transaction from genesis. No EL P2P peer needed, but the initial sync is long unless you restore a DB snapshot.
- execution-layer (
--syncmode=execution-layer, "snap") – op-node only drives the head; op-reth snap-syncs the state snapshot directly from a trusted reth peer over EL P2P. Fast, no snapshot restore needed, but requires a reachable seed peer (WHITECHAIN_RETH_TRUSTED_PEERS). Snap sync cannot build an archive – it only produces pruned state.
| Storage | consensus-layer (re-execute) | execution-layer (snap) |
|---|---|---|
Pruned (--full) |
full-node |
full-snap-node |
| Archive | archive-node |
not supported (snap can't build history) |
The stack ships three profiles. You pick one with the PROFILE variable (or a per-profile make target):
| Profile | Purpose | Storage | Sync method | DB snapshot | Public RPC |
|---|---|---|---|---|---|
full-snap-node |
Recommended default – fastest, simplest bootstrap | Pruned (--full) |
execution-layer (snap) | Not needed | Yes |
full-node |
Public RPC node without snap sync | Pruned (--full) |
consensus-layer | Recommended | Yes |
archive-node |
Full historical state + tracing | Archive (full history) | consensus-layer | Recommended | Yes |
- full-snap-node – the recommended default and fastest, simplest way to bring up a node. Pruned execution state, basic RPC namespaces (
eth,net,web3,rpc). Bootstraps by snap-syncing from a trusted reth peer (WHITECHAIN_RETH_TRUSTED_PEERS) instead of re-executing, so it needs no DB snapshot. Use it when you have a reachable seed reth enode (your own fleet, or one the Whitechain team provides). - full-node – same pruned state and namespaces as
full-snap-node, but syncs in consensus-layer mode by re-executing the chain from L1. Use it when you have no trusted reth peer to snap-sync from. Restore a DB snapshot to avoid a long initial sync. - archive-node – keeps the full historical state and enables
debug,trace,txpool,rethnamespaces plus higher RPC limits. Use it for explorers, indexers, and historicaleth_call/debug_traceTransaction. Syncs by re-executing from L1 (archive cannot snap-sync), so restore a DB snapshot to avoid a very long initial sync. Needs the most disk and RAM.
This applies to the non-snap profiles (full-node and archive-node). They sync in consensus-layer mode, which means op-node derives the chain from L1 and op-reth re-executes every transaction from genesis. On a long-running chain that takes many hours. Restoring a published op-reth DB snapshot lets you start near a recent block and only sync the gap since the snapshot was taken.
full-snap-nodedoes not need a snapshot – it snap-syncs the state directly fromWHITECHAIN_RETH_TRUSTED_PEERS.
The workflow:
-
Stop the node if it is running:
make down PROFILE=full-node
-
Download the published snapshot archive for your network and profile (URL provided by the Whitechain team), then extract it into the matching data directory. The archive contains the
op-rethdb,static_files, and related folders:# example layout – adjust the URL to the one the team gives you mkdir -p data/full-node/op-reth curl -L "<WHITECHAIN_SNAPSHOT_URL>/op-reth-mainnet-full.tar.zst" \ | zstd -d \ | tar -x -C data/full-node/op-reth
The result must be
data/full-node/op-reth/db,data/full-node/op-reth/static_files, etc. (see Data layout). -
Start the node. op-node derives the remaining blocks from L1 and catches up the unsafe head over P2P:
make up PROFILE=full-node make logs PROFILE=full-node
Notes:
- Match the snapshot to the same profile. A pruned snapshot cannot serve archive queries; restore an archive snapshot into
data/archive-node/op-rethfor an archive node. - Restore only the
op-rethdata. Theop-nodedirectory (peerstore,discovery,safedb) is rebuilt automatically and does not need to be restored. - The snapshot is only a starting point. The node still needs a working L1 RPC + Beacon to derive everything after the snapshot block.
- Without a snapshot,
full-nodeandarchive-nodesync from genesis by re-executing every transaction – expect a long initial sync.
- Docker with Compose v2
- Your own Ethereum L1 RPC endpoint
- Your own Ethereum L1 Beacon endpoint
- The published
genesis.jsonandrollup.jsonfor the chosen network, underartifacts/<network>/ - A reachable public IP for the node (
PUBLIC_IP), used for P2P advertisement - For
full-snap-node: a trusted reth enode to snap-sync from (WHITECHAIN_RETH_TRUSTED_PEERS) make,git,openssl,curl;zstdandtarif you restore from a snapshot
| Whitechain network | L1 chain |
|---|---|
| Whitechain mainnet | Ethereum mainnet |
| Whitechain testnet | Ethereum Sepolia |
| Component | full-snap-node / full-node | archive-node |
|---|---|---|
| CPU | 4+ cores | 8+ cores |
| RAM | 16 GB | 32 GB |
| Storage | NVMe SSD, 500 GB min / 1 TB recommended (≥ 2× current chain size + 20%) | NVMe SSD, sized for full history (≥ 1 TB) |
| Network | 100 Mbps+ | 1 Gbps |
-
Place the published artifacts under
artifacts/<network>/:artifacts/mainnet/genesis.json artifacts/mainnet/rollup.jsonThe folder name must match
WHITECHAIN_NETWORKin.env. For testnet useartifacts/testnet/. -
Create your
.env:cp .env.mainnet.example .env # or .env.testnet.exampleFill in at least
PUBLIC_IP,WHITECHAIN_PUBLIC_RPC,L1_RPC_URL,L1_BEACON_URL(see Configuration). Forfull-snap-nodealso setWHITECHAIN_RETH_TRUSTED_PEERS. -
(Recommended for
full-node/archive-node) Restore anop-rethsnapshot to skip the long initial sync – see Database snapshots. -
Start the node profile you want:
make up-full-snap-node # recommended default: pruned node, snap-syncs from a trusted peer # make up-full-node # pruned node, consensus-layer sync (re-executes from L1) # make up-archive-node # archive node make logs-full-snap-node
make upvalidates.envand the artifacts, generateskeys/jwt.txtif missing, then runsdocker compose --profile <profile> up -d. -
Confirm the node responds (use the profile's HTTP port):
curl -s -X POST http://127.0.0.1:8545 \ -H 'Content-Type: application/json' \ --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
Required .env variables:
| Variable | Description |
|---|---|
WHITECHAIN_NETWORK |
Subdirectory under artifacts/, e.g. mainnet or testnet |
PUBLIC_IP |
Public IP of this host, advertised for op-reth and op-node P2P |
WHITECHAIN_PUBLIC_RPC |
Public Whitechain RPC, used as --rollup.sequencer-http for op-reth (transaction forwarding) |
L1_RPC_URL |
Operator-provided Ethereum L1 RPC endpoint |
L1_BEACON_URL |
Operator-provided Ethereum L1 Beacon endpoint |
full-snap-node profile additionally requires:
| Variable | Description |
|---|---|
WHITECHAIN_RETH_TRUSTED_PEERS |
Trusted reth enode to snap-sync from, in the form enode://<pubkey>@<ip>:30303 |
Optional variables (with defaults):
| Variable | Default | Description |
|---|---|---|
L1_RPC_KIND |
basic |
One of alchemy, quicknode, infura, parity, nethermind, debug_geth, erigon, standard, any if your provider supports extra receipt methods |
WHITECHAIN_PUBLIC_OP_NODE_P2P |
empty | Static op-node peer multiaddr /dns4/<host>/tcp/9222/p2p/<peerID> |
OP_NODE_ONLY_REQ_TO_STATIC |
false |
Restrict unsafe-block requests to the static peer only |
OP_RETH_IMAGE |
op-reth:v2.0.0 |
Pin the op-reth image |
OP_NODE_IMAGE |
op-node:v1.19.0 |
Pin the op-node image |
Archive-only RPC limits (optional):
| Variable | Default |
|---|---|
RPC_MAX_CONNECTIONS |
1000 |
RPC_MAX_LOGS_PER_RESPONSE |
20000 |
RPC_MAX_BLOCKS_PER_FILTER |
100000 |
RPC_MAX_TRACING_REQUESTS |
8 |
Host port overrides – see Ports.
Only one profile runs at a time, so all profiles share the same host ports. Each is overridable through the env var in parentheses.
| Port | Default | Env var |
|---|---|---|
| HTTP RPC | 8545 |
HOST_HTTP_PORT |
| WebSocket RPC | 8546 |
HOST_WS_PORT |
| op-node RPC | 9545 (loopback 127.0.0.1 only) |
HOST_OP_NODE_RPC_PORT |
| op-node P2P (TCP+UDP) | 9222 |
HOST_OP_NODE_P2P_PORT |
| EL P2P (TCP+UDP) | 30303 (disabled by default) |
HOST_EL_P2P_PORT |
- The Engine API (
8551) stays inside the compose network and is not published to the host. - op-node RPC (
9545) is bound to loopback (127.0.0.1) only – reachable for local monitoring on the host, never from the network. Theadminnamespace is not enabled, so it serves only the read-onlyoptimism,opp2p, andsuperrootnamespaces. - The EL P2P port (
30303) is not published by default – snap sync only needs outbound connectivity to the trusted peer. Its host mapping is commented out indocker-compose.yml; uncomment it only if you want inbound EL peering. - Only the public JSON-RPC (
8545) and WebSocket (8546) ports are network-facing, and they expose only read-only namespaces. Still, put them behind a firewall, reverse proxy, or rate limiter before serving untrusted clients. - Run only one profile at a time – they all bind the same host ports. To run two side by side on one host, override one profile's ports in
.env.
Each profile keeps its data in its own subtree, so profiles never clash:
data/
full-snap-node/
op-reth/ # execution db, static_files, blobstore, ...
op-node/ # peerstore, discovery, safedb
full-node/
op-reth/
op-node/
archive-node/
op-reth/
op-node/
Snapshots are restored into data/<profile>/op-reth/.
The generic targets take PROFILE=full-snap-node|full-node|archive-node (default full-snap-node):
make up PROFILE=archive-node # start
make down PROFILE=archive-node # stop
make reup PROFILE=archive-node # down + up
make ps PROFILE=archive-node # status
make logs PROFILE=archive-node # tail logs
make config PROFILE=archive-node # render merged compose configPer-profile shortcuts:
make up-full-snap-node make down-full-snap-node make reup-full-snap-node make ps-full-snap-node make logs-full-snap-node
make up-full-node make down-full-node make reup-full-node make ps-full-node make logs-full-node
make up-archive-node make down-archive-node make reup-archive-node make ps-archive-node make logs-archive-node
make ensure-jwt # generate keys/jwt.txt if missing
make help # list all targetsExecution client (use the profile's HTTP port):
curl -s -X POST http://127.0.0.1:8545 \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false]}'op-node sync status (op-node RPC on loopback 9545, run on the host):
curl -s -X POST http://127.0.0.1:9545 \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"optimism_syncStatus","params":[],"id":1}'Applications submit transactions to the local op-reth HTTP port. The node forwards them to WHITECHAIN_PUBLIC_RPC, which routes them to the closed sequencer. You need no direct access to the sequencer.
curl -s -X POST http://127.0.0.1:8545 \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x..."],"id":1}'full-snap-node/full-nodeop-reth HTTP/WS:eth,net,web3,rpcarchive-nodeop-reth HTTP:eth,net,web3,rpc,debug,trace,txpool,reth; WS:eth,net,web3,rpc- op-reth exposes no
adminnamespace on any profile – all exposed namespaces are read-only. - op-node RPC (
9545, loopback-only):optimism,opp2p,superroot(theadminnamespace is not enabled).superrootis a read-only OP Stack interop API (superroot_getSuperRootAtTimestamp) that op-node registers unconditionally; it cannot be disabled and is unused in this single-chain deployment.
Image versions are pinned in docker-compose.yml. To upgrade:
git pull
docker compose pull
make reup PROFILE=full-snap-nodeIf the upgrade includes a new hardfork, replace artifacts/<network>/rollup.json (and genesis.json if it changed) with the published version before make reup. Apply hardfork artifacts before the activation timestamp to avoid a chain-divergence stall.
- This stack holds no project-side private keys. The sequencer, batcher, proposer, and challenger keys stay on the Whitechain side. You operate a follow-only node.
- The Engine API on
8551is bound only to the internal compose network. Do not publish it. - op-node RPC (
9545) is bound to loopback only and does not enable theadminnamespace. op-reth exposes noadminnamespace. The node therefore exposes no administrative or state-mutating control surface to the network. keys/jwt.txtis generated locally and used only between op-node and op-reth in this stack. It does not need to match anything outside.- To resync a profile from scratch:
make down PROFILE=<p>, thenrm -rf data/<p>/op-reth data/<p>/op-node, thenmake up PROFILE=<p>. Forfull-node/archive-nodeprefer restoring a snapshot over a full genesis resync.