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 on a chain with history behind it.
- execution-layer (
--syncmode=execution-layer, "snap") – op-node only drives the head; op-reth snap-syncs the state directly from a trusted reth peer over EL P2P. Fast, 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 | Initial sync | Public RPC |
|---|---|---|---|---|---|
full-snap-node |
Recommended default – fastest, simplest bootstrap | Pruned (--full) |
execution-layer (snap) | Fast | Yes |
full-node |
Public RPC node without snap sync | Pruned (--full) |
consensus-layer | Long – re-executes from genesis | Yes |
archive-node |
Full historical state, tracing on opt-in | Archive (full history) | consensus-layer | Longest – re-executes from genesis | 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. 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. Expect a long initial sync on a chain with history behind it. - archive-node – keeps the full historical state and raises the RPC limits. Use it for explorers, indexers, and historical
eth_call/debug_traceTransaction. Its RPC namespaces are the same read-only default as the other profiles (HTTP_API/WS_API=eth,net,web3,rpc);debug,trace,txpool,rethare an explicit opt-in through those variables – see Available RPC namespaces. Syncs by re-executing from L1 (archive cannot snap-sync), so its initial sync is the longest of the three. Needs the most disk and RAM.
- Docker with Compose v2
- Your own Ethereum L1 RPC endpoint
- Your own Ethereum L1 Beacon endpoint
- The published
genesis.jsonandrollup.jsonfor the chosen network, from whitechain-bootstrap, placed 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,jq(jqis used to cross-check the artifacts before start)
| 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 |
-
Get the network artifacts and place them under
artifacts/<network>/. They are not shipped in this repository – the authoritative copy is the whitechain-bootstrap repository, also described under Network artifacts in the Whitechain docs:git clone https://github.com/whitechain-labs/whitechain-bootstrap.git cd whitechain-bootstrap # verify the files against the SHA-256 hashes published in that # repository's README (and on the docs page) before using them shasum -a 256 testnet/genesis.json testnet/rollup.json cp testnet/genesis.json testnet/rollup.json <this-repo>/artifacts/testnet/
The result must be:
artifacts/testnet/genesis.json artifacts/testnet/rollup.jsonThe folder name must match
WHITECHAIN_NETWORKin.env(mainnetortestnet). Compare the hashes yourself – this repository deliberately keeps no copy of the artifacts and no checksum file of its own, so there is a single source of truth to check against.make upadditionally cross-checks the pair for consistency, see Artifact validation. -
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. -
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.env, cross-checks the artifacts (see Artifact validation), generateskeys/<profile>/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 |
|---|---|---|
HTTP_API |
eth,net,web3,rpc |
op-reth HTTP (8545) RPC namespaces, same variable for all three profiles. Comma-separated, no spaces. See Available RPC namespaces before adding debug, trace, txpool, reth |
WS_API |
eth,net,web3,rpc |
op-reth WebSocket (8546) RPC namespaces, same variable for all three profiles. Same rules as HTTP_API |
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 |
public.ecr.aws/l8q8a0h5/op-reth:v2.3.3 |
Pin the op-reth image |
OP_NODE_IMAGE |
public.ecr.aws/l8q8a0h5/op-node:v1.19.3 |
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 |
RPC_MAX_TRACING_REQUESTS caps how many tracing calls run at once; it does not cap the cost of a single call. It only matters once debug/trace are enabled through HTTP_API or WS_API.
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 – published on full-snap-node, not published on full-node / archive-node |
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 published only by thefull-snap-nodeprofile – the recommended default – on all interfaces (0.0.0.0), TCP and UDP. That profile bootstraps over EL P2P: op-reth snap-syncs the state from the trusted reth peer and uses reth discovery (UDP30303) and devp2p (TCP30303) for it, so the port is reachable for inbound EL peers as well. The upstream OP Stack compose example publishes the same port the same way. It carries devp2p traffic only – no RPC, noadminsurface – but if you do not want inbound EL peering, remap it withHOST_EL_P2P_PORTor block it at the firewall. - For
full-nodeandarchive-nodethe30303mapping is commented out indocker-compose.ymland the port is not published: both sync in consensus-layer mode, deriving the chain from L1, and need no EL peering. Uncomment it only if you want inbound EL peers on those profiles. - Of the RPC surfaces, 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/
keys/
full-snap-node/jwt.txt
full-node/jwt.txt
archive-node/jwt.txt
Each profile also owns its Engine API secret in keys/<profile>/jwt.txt and its own Compose network (public_rpc_full_snap, public_rpc_full, public_rpc_archive), so two profiles running at the same time share neither a credential nor a network path. make up generates the secret for the selected profile if it is missing.
make check-env (a prerequisite of make up) cross-checks artifacts/<network>/genesis.json against artifacts/<network>/rollup.json with jq and refuses to start on any of these:
| Check | Fails when |
|---|---|
genesis.config.chainId = rollup.l2_chain_id |
the two files describe different chains, e.g. a genesis.json from one network next to a rollup.json from another |
genesis.timestamp = rollup.genesis.l2_time |
the files come from different deployments of the same chain ID |
rollup.l1_chain_id matches the network |
the whole pair belongs to the other network – mainnet must settle on Ethereum mainnet (1), testnet on Sepolia (11155111) |
This is configuration consistency, not authenticity. All three checks still pass on a pair of files that was tampered with consistently. Authenticity comes from the source: the whitechain-bootstrap repository and the SHA-256 hashes published with it – compare them as shown in Quick Start step 1.
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/<profile>/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}'- op-reth HTTP (
8545), all three profiles:HTTP_APIfrom.env, defaulteth,net,web3,rpc. - op-reth WS (
8546), all three profiles:WS_APIfrom.env, defaulteth,net,web3,rpc. - Both variables drive every profile –
archive-nodeincluded – so an archive node serves the same read-only set as the others unless you widen it deliberately. The two ports are configured independently: wideningHTTP_APIdoes not change WS, and the reverse. debug,trace,txpool,rethare an explicit opt-in: add them toHTTP_API(and toWS_APIonly if you also need them over WebSocket), for exampleHTTP_API=eth,net,web3,rpc,debug,trace,txpool,reth. Only meaningful onarchive-node, which has the history these methods read. The stack ships no authentication, CORS or vhost restriction on8545/8546, andRPC_MAX_TRACING_REQUESTSbounds concurrency, not the cost of one call – a singletrace_blockordebug_traceTransactionon a heavy block costs seconds of CPU and gigabytes of RAM. Enable them only behind a reverse proxy that allowlists methods and rate-limits clients (Whitechain runsproxydin front of its own public RPC), never on a port open to untrusted clients.- 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, pull the new rollup.json (and genesis.json if it changed) from whitechain-bootstrap, check it against the SHA-256 hashes published there, copy it into artifacts/<network>/, then make reup. The bootstrap repository's hashes change when a hardfork changes the artifacts, so re-verify on every such update. 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 profile's own 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. - On
full-snap-nodethe EL P2P port30303is published on all interfaces (TCP and UDP) – op-reth needs it to snap-sync and to peer over devp2p. It carries no RPC and no administrative methods.full-nodeandarchive-nodedo not publish it. keys/<profile>/jwt.txtis generated locally and used only between the op-node and op-reth of that profile. Each profile has its own secret and its own compose network, so one profile's Engine API credential never grants access to another's. It does not need to match anything outside. If you upgraded from a version that used a singlekeys/jwt.txt, the nextmake upgenerates the per-profile secret and recreates both containers with it; the old file is unused and can be deleted.- To resync a profile from scratch:
make down PROFILE=<p>, thenrm -rf data/<p>/op-reth data/<p>/op-node, thenmake up PROFILE=<p>. Onfull-node/archive-nodethis means re-executing the chain from genesis, so do not do it on a node that is serving traffic without a maintenance window.