Skip to content

Gateway Diagnosis & Troubleshooting

Prerequisites: Quick Start and Configuration.

This guide covers the operational issues you can hit running the Optimum Gateway binary: config, network, CL pairing, and telemetry. Start with the first-line diagnosis below — it resolves most issues.

What you control vs what comes from your credential

Provided by Optimum

  • Gateway Docker image / binary
  • A credential — either an API key (ogw_live_...), which binds gateway_id, chain, operator, and validator scope, or an org join key (ojk_live_...) the gateway enrolls with, which binds chain, type, and cluster scope. See Gateway Self-Enrollment
  • An assigned gateway_cluster_id (e.g. optimum_ethereum_hoodi_v0_1 for Hoodi; Mainnet ID provided during onboarding)

You configure (operational only)

FieldNotes
api_key (env OPT_API_KEY)Wrong/revoked key -> gateway crashes at startup. Set via env, not YAML
join_key (env OPT_JOIN_KEY)Fleet alternative to api_key; the two are mutually exclusive. Set via env, not YAML
gateway_id (env OPT_GATEWAY_ID)Join-key path only: the enrollment label, unique per host. Ignored on the API-key path
enroll_cred_dirJoin-key path only; persist as a volume — losing it re-enrolls and consumes a join-key use
gateway_cluster_idMust match onboarding (Hoodi vs Mainnet)
identity_libp2p_dir / identity_mump2p_dirPersist as volumes — without them, peer ID changes every restart
agent_lib_p2p_portDefault 33212; CL connects here
agent_mump2p_portDefault 33213; Optimum network peers connect here (inbound)
telemetry_enable / telemetry_portDefault port 48123
direct_cl_peersOptional, strongly recommended for Lighthouse / Nimbus
remote_push_enableOptional; pushes logs/metrics to Optimum for support visibility
log_leveldebug / info

Derived from your credential — you do NOT configure these

  • gateway_id — from the JWT sub claim
  • chain — from the JWT chain_id claim; not a YAML field
  • Validator list — from the auth mint, refreshed periodically
  • Gossip topics (beacon_block + 64 attestation subnets) — baked into the binary

If chain looks wrong (e.g. "I want mainnet but it's hoodi"), the credential is wrong — chain cannot be changed via YAML. Get the matching API key or join key from Optimum.

First-line diagnosis (run this first)

bash
curl -s http://localhost:48123/health | jq
curl -s http://localhost:48123/api/v1/self_info | jq
docker logs optimum-gateway --tail=50
EndpointWhat it gives you
GET /healthPass/fail checks, failing[] list, HTTP 200 (healthy) or 503 (degraded)
GET /Lightweight liveness — {"status":"ok"} when the process and HTTP server are up
GET /api/v1/self_infoIdentity, peer IDs, per-topic peer counts, fork digest, chain, cluster, propagation flags, multiaddrs
docker logsStartup fatals, auth mint errors, bootstrap retries, subscribe logs

Healthy target: cl_peers ≥ 1, mump2p_peers ≥ 1, subscribed_topics ≈ 65, last_block_age_sec < 60.

On a stream_only gateway the target is mump2p_peers ≥ 1, mump2p_health ok, last_block_age_sec < 60; only the three CL checks read skipped, so a stale mesh still returns 503.

/health checks -> meaning -> fix

GET /health returns 200 (healthy) or 503 (degraded). Each check covers a different part of the pipeline.

A check is ok, fail, or skipped. skipped means the check does not apply to this node's mode: a stream_only gateway never starts the CL host, so cl_peers, cl_health and subscribed_topics are reported as skipped and are left out of failing and of the 200/503 roll-up.

Failing checkWhat it meansFix
cl_peersNo CL client connected on :33212Configure CL peering; add direct_cl_peers; open 33212 inbound; use a reachable IP in the multiaddr; restart CL after a gateway restart
mump2p_peersNot connected to the Optimum meshCheck 33213 inbound + HTTPS to bootstrap; verify api_key + gateway_cluster_id; wait 2-5 min after start
subscribed_topicsTopic subscription failed (expect ~65)Usually a chain/cluster mismatch (wrong API key for the network); check logs for subscribe errors
last_block_age_secNo beacon block in ~60sCL is connected but silent — CL not synced, EL stuck, or CL OOM/restart; fix the CL first
cl_healthNo CL gossip traffic in the last 30sSame as silent CL — peer count alone can be misleading
mump2p_healthNo mesh traffic in the last 30sMesh/auth/bootstrap issue; can be briefly normal right after restart

cl_peers can be OK while last_block_age_sec fails: the CL is connected but not delivering blocks (silent CL, EL not synced, Prysm stuck). Don't stop at peer count.

Example healthy response

json
{
  "status": "healthy",
  "gateway_id": "optimum-eu-hoodi-01",
  "version": "v1.3.2",
  "uptime_seconds": 1639,
  "checks": {
    "cl_peers": {"status": "ok", "value": 1},
    "mump2p_peers": {"status": "ok", "value": 13},
    "subscribed_topics": {"status": "ok", "value": 65},
    "last_block_age_sec": {"status": "ok", "value": 1},
    "cl_health": {"status": "ok"},
    "mump2p_health": {"status": "ok"}
  }
}

Example degraded response

Read the failing[] list first — it names which checks to fix next.

json
{
  "status": "degraded",
  "gateway_id": "optimum-eu-hoodi-01",
  "checks": {
    "cl_peers": {"status": "ok", "value": 1},
    "last_block_age_sec": {"status": "fail", "value": 120},
    "cl_health": {"status": "fail"}
  },
  "failing": ["last_block_age_sec", "cl_health"]
}

Example stream_only response

json
{
  "status": "healthy",
  "gateway_id": "optimum-eu-mainnet-stream-01",
  "checks": {
    "cl_peers": {"status": "skipped"},
    "cl_health": {"status": "skipped"},
    "subscribed_topics": {"status": "skipped"},
    "mump2p_peers": {"status": "ok", "value": 25},
    "mump2p_health": {"status": "ok"},
    "last_block_age_sec": {"status": "ok", "value": 11}
  }
}

/api/v1/self_info fields worth reading

FieldTells you
chain / fork_digestNetwork match (hoodi c6ecb76c, mainnet 8c9f62fe)
gateway_cluster_idYou set this; must match the assignment
gateway_idFrom the API key; use it in dashboards and support tickets
libp2p.multiaddrs + peer_idGive these to the CL for --peer / --direct-peer
libp2p.total_peers / direct_peersCL connectivity detail
mump2p.total_peersMesh connectivity
libp2p.peers_per_topicWhich topics have CL peers when the CL is healthy
propagation_enabledOptimum dynamic config; false can explain "not propagating". Same state as mump2p_gateway_propagation_state=0
paired_withGateway type from the API key: partner, hermes, or relay — only partner forwards attestations

For support tickets, attach the full output of curl -s http://localhost:48123/api/v1/self_info | jq.

Gateway won't start / crashloops

IssueLogs (if applicable)Fix
Container exits immediatelyunable to load configCheck config path and YAML validity
Auth fatal at startupunable to initialize auth_token manager / initial JWT mint failed / enroll gatewayBad/missing/revoked credential, enroll failure, or no outbound HTTPS to auth.getoptimum.io. Verify API key or join key in the console; check firewall/DNS
Both credentials setapi_key and join_key are mutually exclusiveUse exactly one: OPT_API_KEY or OPT_JOIN_KEY
Port bind errorbind: address already in uselsof -i :33212 (and 48123); stop the conflicting process or change ports consistently in docker run -p
Config not loadedunable to load configMount config correctly: -v $(pwd)/config:/app/config and -config=/app/config/app_conf.yml
Invalid YAMLfailed to validate configFix the YAML syntax
Identity dir not writablefailed to create identity directorymkdir -p data/libp2p data/mump2p and mount both volumes
remote_push_enable without telemetryremote_push_enable requires telemetry_enable=trueSet telemetry_enable: true or disable remote push
Missing cluster IDOPT_GATEWAY_CLUSTER_ID is requiredSet the assigned gateway_cluster_id
Gateway ID conflictgateway with same ID already existsOne gateway per API key. Preserve identity volumes on redeploy, or wait ~1h for the old bootstrap registration to expire
API key revoked / suspendedapi key revoked (403) / api key suspended (403)Generate a new key in the console
Auth refresh stoppedapi key terminal failure — refresh loop exitingKey permanently invalid — fix the key and restart
Clock skewJWT verify failures with no obvious key issueHost clock must be within ~30s of UTC (timedatectl, NTP)

API key / network mismatch

The chain comes from the API key, not YAML. A Hoodi key on a Mainnet cluster (or vice versa) breaks things quietly.

IssueWhat you seeFix
self_info.chain is hoodi but cluster is mainnetchain: hoodi, fork_digest: c6ecb76c, but gateway_cluster_id: optimum_ethereum_mainnet_*Generate a Mainnet key in the console — the network picker calls it Ethereum — and set the matching Mainnet gateway_cluster_id; restart
Wrong fork digest in topicsmump2p topics use /eth2/c6ecb76c/... on mainnetFix key + cluster, then restart; confirm self_info.chain and fork_digest
Gateway not visible on bootstrap / no mesh peersmump2p_peers: 0Registered on the wrong chain or never registered. Fix the mismatch; confirm outbound HTTPS
Attestations never forwardedvalidator list emptyValidators come from the auth mint; wait for sync. If persistent, verify the key's validator assignment with Optimum

Verify after restart:

bash
curl -s http://localhost:48123/api/v1/self_info | jq '.chain, .fork_digest'
# mainnet -> "mainnet"  "8c9f62fe"

CL client connection (most common issue)

IssueFix
cl_peers: 0Point the CL at the self_info multiaddr + peer_id; open 33212 inbound
Disconnected after a gateway restartLighthouse/Nimbus don't re-dial. Add the CL to direct_cl_peers so the gateway keeps retrying
Wrong peer ID in CL configIdentity dirs not persisted -> new peer ID each run. Mount volumes; refresh CL config from current self_info.peer_id
Lighthouse v8.x drops gatewayPeerDAS change. Use a fixed gateway version; on Lighthouse add --semi-supernode, --target-peers=500, --trusted-peers=<gateway_peer_id>
Teku disconnects the gatewayUpgrade Teku to v26.4.0+; use --p2p-direct-peers (not just --p2p-static-peers)
Nimbus won't stay connectedUse a stable --netkey-file (not random); add Nimbus to direct_cl_peers with its public IP:port; open Nimbus P2P port
Prysm shows connected but no blocksSilent CL / EL not synced. Prysm can follow in optimistic sync while Geth catches up — cl_peers OK but last_block_age_sec / cl_health fail. Check EL logs; wait for EL, then Prysm /healthz goes 200
Docker NAT / wrong IP in multiaddrCL on host + gateway in bridge network -> use the host IP or --network host

Recommended: Prysm v7.1.8; Lighthouse v8.2.1+; Teku v26.6.0+ (minimum v26.4.0); Nimbus v26.7.0+.

Getting the gateway's peer info for the CL

bash
curl -s http://localhost:48123/api/v1/self_info | jq -r '.peer_id'
curl -s http://localhost:48123/api/v1/self_info | jq -r '.libp2p.multiaddrs'

Lighthouse and Nimbus do not reliably re-dial the gateway after a gateway restart. Add the CL node so the gateway runs a background goroutine that keeps retrying:

yaml
direct_cl_peers:
  - /ip4/YOUR_CL_IP/tcp/9000/p2p/YOUR_CL_PEER_ID

PeerDAS / custody metadata

The gateway advertises a custody group count of 8 (Fulu VALIDATOR_CUSTODY_REQUIREMENT) in its libp2p metadata so PeerDAS-aware clients — Lighthouse in particular — keep the gateway as a useful peer. See the Fulu p2p metadata spec.

Lighthouse v8.x PeerDAS Configuration (Important)

Use Lighthouse v8.2.1+ with:

yaml
command:
  - lighthouse
  - beacon_node
  - --network=hoodi
  - --semi-supernode
  - --target-peers=500
  - --boot-nodes=/ip4/YOUR_GATEWAY_IP/tcp/33212/p2p/YOUR_GATEWAY_PEER_ID
  - --trusted-peers=YOUR_GATEWAY_PEER_ID

Why: PeerDAS changes peer requirements; --semi-supernode and --target-peers=500 prevent pruning.

Lighthouse v8.2.0 Release Notes

Teku PeerDAS Configuration (Important)

Minimum version: Teku v26.4.0 or later. Older versions have a known bug (NoSuchElementException at MetadataDasPeerCustodyTracker.onPeerMetadataUpdate) that causes recurring errors when exchanging metadata with peers.

Use --p2p-direct-peers (not just --p2p-static-peers): static peers can be pruned during peer management; direct peers maintain a persistent, protected connection.

sh
teku \
  --network=hoodi \
  --p2p-direct-peers=/ip4/YOUR_GATEWAY_IP/tcp/33212/p2p/YOUR_GATEWAY_PEER_ID \
  --p2p-static-peers=/ip4/YOUR_GATEWAY_IP/tcp/33212/p2p/YOUR_GATEWAY_PEER_ID \
  --p2p-subscribe-all-subnets-enabled \
  --p2p-subscribe-all-custody-subnets-enabled \
  ...
SymptomCauseFix
NoSuchElementException at MetadataDasPeerCustodyTrackerTeku version too oldUpgrade to v26.4.0+
Gateway peer keeps disconnectingUsing --p2p-static-peers onlyAdd --p2p-direct-peers
Payload marked as invalid by Execution ClientEL still syncingWait for the EL to finish

Nimbus

Connect Nimbus to the gateway and add Nimbus to the gateway's direct_cl_peers:

sh
nimbus_beacon_node \
  --direct-peer=/ip4/YOUR_GATEWAY_IP/tcp/33212/p2p/YOUR_GATEWAY_PEER_ID \
  --netkey-file=/data/netkey \
  ...

Verify Nimbus sees the gateway:

sh
curl -s http://localhost:9596/eth/v1/node/peers/YOUR_GATEWAY_PEER_ID | jq '.data | {state, agent}'

Expected: "state": "connected" with an agent containing optimum-gateway.

Nimbus behavior — what is normal (not a gateway bug):

  1. Warmup is normal. Nimbus may take a few hours after start to settle. Gossip/scoring can look off early but stabilizes on its own — don't treat early instability as a failure.
  2. Peer drop + reconnect is expected. Nimbus drops peers when a connection closes or peer score drops low, then reconnects with fresh scores. Transient cl_peers flapping is normal.
  3. The connection itself is reliable. On a gateway restart you immediately see mump2p_gateway_cl_peers 1; Nimbus reconnects fast and stays connected (held over a full weekend in testing).
  4. Behavior differs from Prysm/Lighthouse. Prysm doesn't show this drop/reconnect pattern and Lighthouse syncs in parallel — don't benchmark Nimbus's warmup/scoring against them.
  5. Ignore the stale issue tracker. Nimbus has many old open issues (2020-2022), but its commit history is active; those open issues don't indicate a broken integration.
SymptomCauseFix
Adding privileged direct peer in logs but no connectionFirewall or wrong IP/portOpen 33212 (Nimbus -> gateway) and your Nimbus P2P port (gateway -> Nimbus); use a public IP in direct_cl_peers
Gateway peer always disconnected in RESTMissing direct_cl_peers or unstable netkeyAdd direct_cl_peers; use a stable --netkey-file

Lodestar

Lodestar is supported. Add the gateway as a trusted/direct peer in Lodestar, and add the Lodestar node to the gateway's direct_cl_peers so the gateway re-dials after restarts. Verify with curl -s http://localhost:48123/health | jq '.checks.cl_peers'.

Network / firewall / Docker

PortDirectionAction
33212InboundCL clients connect here
33213InboundOptimum network (mump2p) peers connect here
48123Localhost/health, /metrics, /api/v1/self_info — see Network Requirements
443Outboundauth, bootstrap, Loki, Mimir
bash
sudo lsof -i :33212 -i :33213 -i :48123
sudo ufw allow 33212/tcp
sudo ufw allow 33213/tcp

33213 must be reachable from the internet for the mesh. 33212 is for your CL (usually private). Docker tip: --network host exposes all ports on the host network interface.

Identity / persistence

IssueWhat you seeFix
CL config "suddenly wrong" after redeploypeer_id changed in self_infoNew libp2p identity each run. Persist identity_libp2p_dir and identity_mump2p_dir as volumes
Auth/mesh issues after a wipeNew mump2p peer_id in the mint payload. Persist the identity dir; Optimum may need to re-link the key
Two gateways with the same identityCopied volume. One identity per gateway instance
bash
docker exec optimum-gateway ls -la /tmp/libp2p /tmp/mump2p

Local dashboard (Prometheus + Grafana)

Partners scrape the gateway /metrics locally with their own Prometheus + Grafana. See Metrics & Grafana for the full stack setup.

IssueFix
Grafana empty / "no data"telemetry_enable: false. Set it to true and restart
Prometheus can't scrapeWrong target. macOS/Windows: host.docker.internal:48123; Linux: 172.17.0.1:48123
Gateway not in the dropdownNo successful scrape yet. Confirm curl localhost:48123/metrics | grep gateway_id
Block/attestation panels emptyGateway isn't receiving blocks/attestations. Fix CL + mesh first via /health
"Accelerated slots" always 0No block race data yet. Needs a healthy CL + mesh + time on the network
Consumer stream quiet, connection still openA quiet chain and a stalled feed look the same without liveness frames. Alert on missing heartbeats and send client keepalives. See Consumer Block Stream
bash
curl -s http://localhost:48123/metrics | grep mump2p_gateway

Message flow / performance (gateway runs but "isn't helping")

SymptomCauseFix
Blocks not faster than baselineCL or mesh unhealthyFix /health first
Attestations not on the meshValidators not yet in the JWT listWait for sync. Only partner gateways forward attestations (self_info.paired_with). Check mump2p_gateway_known_validators_total — 0 means no validators synced from auth
Attestations dropped (expected)Non-partner validators on the same CLNormal — only your own validators are forwarded
propagation_enabled: false in self_infoOptimum dynamic configNot a partner YAML knob. Metrics: mump2p_gateway_propagation_state=0

Gateway self-enrollment

Applies when OPT_JOIN_KEY is set. See Gateway Self-Enrollment.

SymptomLikely causeFix
Startup fails with enroll gateway / 401Join key unknown, expired, exhausted, or revoked; or host clock >~2 min slowThe 401 is deliberately the same for all four, so check the key under Manage GatewaysEnrollment keys: expired and exhausted show as a badge, revoked keys are removed from the list. Sync NTP. Generate a new key if needed
Startup fails with label_conflict / 409Enrollment label already live in the orgSet a unique OPT_GATEWAY_ID per host, or revoke the orphan under Manage GatewaysGateway tab, finding it by that label
Startup fails with gateway_key_limit / 409Org at the 1000-gateway capRevoke unused credentials or contact Optimum
Startup fails: peer ID mismatchmumP2P identity changed under an existing credential. Raised by the gateway, not by authRestore the original identity_mump2p_dir volume, or revoke the enrolled credential and enroll fresh
Corrupt credential on diskenrollment.json unreadable, or the thumbprint recorded in it does not match the private key it carries. Raised by the gateway, not by authDo not delete and re-enroll blindly — that burns a join-key use. Restore enrollment.json from backup, or revoke the credential in the console first
auth_enrollment_total{result="success"} on every restartCredential directory not persistingMount identity_mump2p_dir (or enroll_cred_dir) as a volume. Repeated success across a fleet means enrollments are not being reused
Log line enrolling without a label at startupOPT_GATEWAY_ID left at default (dev-gateway), which sends an empty labelSet a unique OPT_GATEWAY_ID per host before first enroll. An empty label is exempt from the conflict check, so the host silently re-enrolls and burns a join-key use every time it loses its credential dir
Mesh peers stay at 0 after enrollJoin key minted without matching cluster_idsMint a join key whose cluster scope includes your gateway_cluster_id
Revoked join key, gateway still runsRevoking a join key stops new enrollments onlyRevoke the enrolled gateway credential separately if you need to cut access

Runtime gateway_id in /health and metrics is the enrolled client_id (JWT sub), not the OPT_GATEWAY_ID enrollment label.

Bootstrap / mesh (partner-visible symptoms only)

SymptomAction
Startup logs: failed to connect to bootstrap node... i/o timeoutIgnore if transient (first few minutes)
Persistent mump2p_peers: 0Check credential (OPT_API_KEY or OPT_JOIN_KEY), gateway_cluster_id, outbound network
Handshake errors in logsUsually peer churn; persistent -> contact Optimum
Gateway missing from the bootstrap listUsually chain/cluster/key mismatch or no heartbeat (TTL ~1h)

CL client-specific quick reference

ClientRecurring issueFix
LighthouseNo reconnect after gateway restartdirect_cl_peers + --boot-nodes / trusted peer
Lighthouse v8+Goodbye / custody metadata mismatchUpdated gateway + PeerDAS flags (--semi-supernode, --target-peers=500)
PrysmConnected but no gossipEL sync; restart beacon node. Use v7.1.8+
TekuNoSuchElementException metadataUpgrade to v26.4.0+
TekuGateway peer prunedUse --p2p-direct-peers (not just --p2p-static-peers)
NimbusPrivileged peer failsStable --netkey-file + public IP in direct_cl_peers

Operational mistakes

IssueFix
Wrong Docker image tagUse getoptimum/gateway:v1.3.2
Config edited but container not restarteddocker restart optimum-gateway
Running hoodi + mainnet on the same portsThe second instance needs different ports + its own config + its own credential
Duplicate gateway (same API key, two hosts)One key -> one gateway; generate a second key
Checking health on the wrong host/portConfirm telemetry_port and Docker port mapping — see Network Requirements
Exposed /metrics to the internetUse -p 127.0.0.1:48123:48123, not -p 48123:48123

Normal Log Noise (Safe to Ignore)

text
failed to connect to bootstrap node... i/o timeout
failed to send handshake for peer... connection closed
failed to load topics data... file does not exist
  • Expected during the first minutes of startup or on first run.
  • A brief 503 on /health right after restart (before CL/mesh settle) is also normal — allow 30-60s after boot.
  • stale_data, skipping publish of beacon_block while the CL is still syncing — expected, not a gateway bug.

Highest-frequency real-world issues (prioritize)

  1. CL not connected — missing direct_cl_peers, firewall, or wrong multiaddr
  2. CL connected, no blocks — silent CL / EL sync (last_block_age_sec)
  3. Wrong API key for the network — Hoodi key on a Mainnet cluster (or vice versa)
  4. Identity not persisted — peer ID drift breaks CL config
  5. Local Grafana empty — telemetry off or Prometheus target wrong
  6. Lighthouse v8 PeerDAS — custody/metadata disconnect