Jesse A. Green / notebook

base/node Administrator & Developer Guide

Generated April 2026 from base/node at d4a32b2

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

Operational reference for the base/node repository. Audience: node operators, platform engineers, SRE, and contributors. Covers architecture, system requirements, the full configuration surface, deployment, day-to-day operations, performance tuning, security posture, developer internals, and integration points.


1. Architecture Overview

base/node is not a node implementation. It is a thin orchestration layer — a few Dockerfiles, entrypoint shell scripts, pinned-version manifests, and a small Go dependency-bumping utility — that composes upstream binaries (op-node, base-consensus, geth/op-geth, reth/base-reth-node, nethermind) into a reproducible Base-network node deployment.

1.1 What This Repo Does (And Does Not)

Aspect Value
Total source files 30
Total source chars 24,475
Complexity tier small
Languages Go (dependency_updater), shell (entrypoints), Dockerfile
Upstream binaries managed 4 (op-node, op-geth, base-reth-node, nethermind)
Supported clients reth, geth, nethermind
Networks Base mainnet, Base Sepolia testnet
Distribution Source-only — no registry images published from this repo
License See LICENSE in repo
Security reporting HackerOne (Coinbase program)

What it does NOT do:

  • Does not publish pre-built container images. Every deployment builds the upstream binaries from source.
  • Does not include a Base node binary of its own (besides the dependency_updater CI tool).
  • Does not provide key management, remote signing, or wallet functionality.
  • Does not implement the Base consensus or execution protocol — those live in upstream repos.

1.2 Topology

+--------------------------------------------------------------------+
|                        Your Base Node Host                         |
|                                                                    |
|  +-----------------------------+  +-----------------------------+  |
|  | Container: execution        |  | Container: node             |  |
|  | (built from <CLIENT>/       |  | (same image, different cmd) |  |
|  |  Dockerfile)                |  |                             |  |
|  |                             |  |                             |  |
|  |  supervisord                |  |  supervisord                |  |
|  |   `-- execution-entrypoint  |  |   `-- consensus-entrypoint  |  |
|  |       (geth | reth |        |  |       |- USE_BASE_CONSENSUS |  |
|  |        nethermind)          |  |       |    -> base-consensus|  |
|  |                             |  |       `- else               |  |
|  |                             |  |            -> op-node       |  |
|  |  Ports: 8545, 8546,         |  |  Ports: 8545->7545, 9222,   |  |
|  |         6060->7301,         |  |         7300, 6060          |  |
|  |         30303 (TCP+UDP)     |  |                             |  |
|  +----+------------------------+  +----+------------------------+  |
|       | Engine API (port 8551, JWT)    |                           |
|       |   ws:// or http://             |                           |
|       |    (network-dependent)         |                           |
|       +--------------------------------+                           |
|                                                                    |
|  Shared volume: ${HOST_DATA_DIR} -> /data (execution only)         |
+--------------------------------------------------------------------+
                              ^                 ^
                              |                 |
                              | L1 RPC          | P2P (Base peers)
                              | L1 Beacon       |
                              | L1 Beacon       |
                              |   Archiver      |
                              v                 v
              +----------------------+   +-------------------+
              |  Ethereum L1         |   |  Base network     |
              |  (your provider)     |   |  peers            |
              +----------------------+   +-------------------+

1.3 Component Inventory

Component Path Role
Compose stack docker-compose.yml Two-service orchestration
Host-side defaults .env CLIENT, HOST_DATA_DIR, USE_BASE_CONSENSUS
Mainnet env .env.mainnet Loaded into both containers
Sepolia env .env.sepolia Loaded into both containers when NETWORK_ENV=.env.sepolia
Pinned versions versions.env (shell exports) + versions.json (canonical) Build-time pins
Process supervisor supervisord.conf PID 1 inside containers
Consensus dispatcher consensus-entrypoint Picks op-node vs base-consensus
op-node launcher op-node-entrypoint Validates env, polls engine, writes JWT, execs op-node
base-consensus launcher base-consensus-entrypoint Same flow, execs base-consensus (reth image only)
Geth build + runtime geth/Dockerfile + geth/geth-entrypoint Multi-stage build, exec geth
Nethermind build + runtime nethermind/Dockerfile + nethermind/nethermind-entrypoint Multi-stage build, exec nethermind
Reth build + runtime reth/Dockerfile + reth/reth-entrypoint Multi-stage build with mold linker
Dependency updater dependency_updater/ Go CLI that bumps upstream pins (CI utility)

1.4 Container Duality

The execution and node services are built from the same Dockerfile — both contain the full set of binaries (geth/reth/nethermind, op-node, and optionally base-consensus). They differ only in the command: field:

  • execution runs ./execution-entrypoint.
  • node runs ./consensus-entrypoint.

Note

Because both containers carry all binaries, memory overhead is slightly higher than a minimal split, but image management is simpler — a single client build produces the complete stack.

1.5 Engine API Handshake

consensus-entrypoint (or base-consensus-entrypoint)
    |
    | 1. Poll ${OP_NODE_L2_ENGINE_RPC} (ws/http) until HTTP 401 is returned.
    |    The 401 proves the execution client is up and rejecting unauthed calls.
    |
    | 2. get_public_ip() tries four HTTP IP-echo services in sequence
    |    (ifconfig.me -> api.ipify.org -> ipecho.net -> v4.ident.me).
    |
    | 3. Write BASE_NODE_L2_ENGINE_AUTH_RAW hex to BASE_NODE_L2_ENGINE_AUTH
    |    (default: /tmp/engine-auth-jwt).
    |
    | 4. exec ./op-node (or base-consensus node | base-consensus follow).
    v
op-node / base-consensus
    |
    | Opens Engine API connection to the execution container.
    |    (Reads JWT from the same path; both clients share /tmp inside their container.)
    v
execution-entrypoint (geth/reth/nethermind)
    |
    | Reads BASE_NODE_L2_ENGINE_AUTH_RAW and writes the same file
    |    so --authrpc.jwtsecret matches.

1.6 Tech Stack

Layer Technology
Container runtime Docker Engine 20.10+, Compose v2 plugin
Process supervisor supervisord
Build tools Go 1.24 (op-node, geth), Rust 1.93 + mold (reth, base-consensus), .NET SDK 10 (nethermind)
op-node build make (geth/reth Dockerfile) or just (nethermind Dockerfile)
Dependency-updater language Go 1.24.3
Updater dependencies github.com/ethereum-optimism/optimism v1.13.3 (retry helper), github.com/google/go-github/v72 v72.0.0, github.com/urfave/cli/v3 v3.3.8, github.com/Masterminds/semver/v3 v3.4.0

Note

Coinbase's chosen execution client in production is reth (archive). Reth is built from base/base (the team's hard fork that bundles base-reth-node and base-consensus together), not from paradigmxyz/reth directly.


2. System Requirements

2.1 Hardware

Resource Minimum Recommended Coinbase Production
CPU Modern multicore 8+ cores AWS i7i.12xlarge (48 vCPU)
RAM 32 GB 64 GB 384 GB
Disk NVMe SSD NVMe SSD (RAID 0) RAID 0 over all local NVMe (/dev/nvme*)
Filesystem ext4 ext4 ext4
Storage size 2 × current chain size + snapshot + 20 % same same

2.2 Software

Item Version
Docker Engine 20.10+ with Compose v2 plugin
Linux kernel 5.x+ recommended
Go (only for dependency_updater) 1.24.3

2.3 External Dependencies

The node operator must supply:

Dependency Used by Variable
Ethereum L1 execution RPC op-node, base-consensus OP_NODE_L1_ETH_RPC, BASE_NODE_L1_ETH_RPC
Ethereum L1 beacon (CL) op-node, base-consensus OP_NODE_L1_BEACON, BASE_NODE_L1_BEACON
Ethereum L1 beacon archiver (blob sidecars) op-node OP_NODE_L1_BEACON_ARCHIVER

2.4 Network Requirements

Inbound (must be reachable from the public internet):

Port Protocol Service
30303 TCP + UDP Execution P2P
9222 TCP + UDP Consensus P2P

Outbound:

  • Sequencer HTTPS — https://mainnet-sequencer.base.org, https://sepolia-sequencer.base.org (port 443).
  • L1 provider (yours).
  • L1 beacon and archiver (yours).
  • Public-IP echo services on port 80: ifconfig.me, api.ipify.org, ipecho.net, v4.ident.me. The entrypoints require at least one of these to respond.

3. Configuration Reference

3.1 Host-Side .env

Consumed by docker compose itself, not the containers.

Variable Default Required? Purpose
CLIENT geth (compose-fallback) No Dockerfile selection (${CLIENT}/Dockerfile)
HOST_DATA_DIR ./${CLIENT}-data No Bind-mount source for execution container /data
USE_BASE_CONSENSUS true No Passed into containers via compose environment:
NETWORK_ENV .env.mainnet No (host shell) Which per-network env file to load

Important

The shipped .env sets USE_BASE_CONSENSUS=true which only works with CLIENT=reth. Always set CLIENT explicitly in production. The README claims reth is the default, but the compose file falls back to geth via ${CLIENT:-geth} — see issue #1010.

3.2 Per-Network Env Files

.env.mainnet and .env.sepolia are injected into both containers via env_file:. Differences between them:

Variable Mainnet Sepolia
RETH_CHAIN base base-sepolia
OP_NODE_NETWORK base-mainnet base-sepolia
BASE_NODE_NETWORK base base-sepolia
OP_GETH_OP_NETWORK base-mainnet base-sepolia
RETH_SEQUENCER_HTTP https://mainnet-sequencer.base.org https://sepolia-sequencer.base.org
OP_SEQUENCER_HTTP https://mainnet-sequencer.base.org https://sepolia-sequencer.base.org
OP_GETH_SEQUENCER_HTTP https://mainnet-sequencer.base.org https://sepolia-sequencer.base.org
OP_RETH_SEQUENCER_HTTP https://mainnet-sequencer.base.org https://sepolia-sequencer.base.org
OP_NODE_L2_ENGINE_RPC http://execution:8551 ws://execution:8551
BASE_NODE_L2_ENGINE_RPC ws://execution:8551 http://execution:8551
OP_RETH_OP_NETWORK base base-sepolia
RETH_FB_WEBSOCKET_URL (commented) wss://mainnet.flashblocks.base.org/ws wss://sepolia.flashblocks.base.org/ws

Note

The mainnet vs. Sepolia transport swap on OP_NODE_L2_ENGINE_RPC (http ↔ ws) and BASE_NODE_L2_ENGINE_RPC (inverse) is intentional — it exercises both engine transports across networks.

3.3 Mandatory User-Provided Values

Variable Purpose
OP_NODE_L1_ETH_RPC L1 execution RPC for op-node
OP_NODE_L1_BEACON L1 beacon endpoint for op-node
OP_NODE_L1_BEACON_ARCHIVER L1 blob archiver for op-node
BASE_NODE_L1_ETH_RPC Same, for base-consensus (reth + USE_BASE_CONSENSUS=true)
BASE_NODE_L1_BEACON Same, for base-consensus

3.4 Variable Namespaces

Prefix Consumed by
OP_NODE_* op-node binary
BASE_NODE_* base-consensus binary
OP_GETH_* geth-entrypoint
OP_NETHERMIND_* nethermind-entrypoint
RETH_*, OP_RETH_* reth-entrypoint (reth flags + reth env passthrough)
GETH_* geth-entrypoint (cache, verbosity, data dir)
NETHERMIND_* nethermind-entrypoint

3.5 Optional Feature Toggles

Feature Variable(s) Notes
EthStats (geth) OP_GETH_ETH_STATS nodename:secret@host:port
EthStats (nethermind) OP_NETHERMIND_ETHSTATS_ENABLED, _NODE_NAME, _ENDPOINT Three separate env vars
Trust L1 RPC OP_NODE_L1_TRUST_RPC=true Disables op-node L1 validation
Snap sync (geth) OP_GETH_SYNCMODE=snap, OP_GETH_BOOTNODES Experimental
Snap sync (nethermind) OP_NETHERMIND_BOOTNODES —
Flashblocks (reth) RETH_FB_WEBSOCKET_URL Sub-block streaming
Pruning (reth) RETH_PRUNING_ARGS Distance must exceed 10064
Historical proofs (reth) RETH_HISTORICAL_PROOFS, RETH_HISTORICAL_PROOFS_STORAGE_PATH Adds start-wait-stop-init preflight
Geth state scheme OP_GETH_STATE_SCHEME Passed as --state.scheme
Geth unprotected txs OP_GETH_ALLOW_UNPROTECTED_TXS Passed as --rpc.allow-unprotected-txs

3.6 JWT Engine Auth

BASE_NODE_L2_ENGINE_AUTH=/tmp/engine-auth-jwt
BASE_NODE_L2_ENGINE_AUTH_RAW=688f5d737bad920bdfb2fc2f488d6b6209eebda1dae949a8de91398d932c517a

Caution

The shipped hex (688f5d73...517a) is a public sample in both .env.mainnet and .env.sepolia. Every deployment using the shipped value uses an identical JWT. This is acceptable only because authrpc.port (8551) is not published by the compose file. If a misconfigured firewall ever exposes 8551, that is an immediate elevation path. Generate your own with openssl rand -hex 32 for any internet-facing node.

3.7 Reference — Shipped Variables (Pre-Filled, Change Only If You Know Why)

Variable Default Purpose
RETH_CHAIN base / base-sepolia Reth chain ID
OP_NODE_NETWORK base-mainnet / base-sepolia op-node network name
OP_GETH_OP_NETWORK base-mainnet / base-sepolia op-geth network
RETH_SEQUENCER_HTTP https://mainnet-sequencer.base.org Sequencer URL
OP_NODE_L1_RPC_KIND debug_geth L1 provider hint
OP_NODE_L1_BEACON_FETCH_ALL_SIDECARS true Fetch all blob sidecars
OP_NODE_L1_TRUST_RPC false Validate L1 responses
OP_NODE_L2_ENGINE_KIND reth Engine kind hint
OP_NODE_L2_ENGINE_RPC http://execution:8551 (mainnet) / ws://execution:8551 (Sepolia) Engine RPC
BASE_NODE_L2_ENGINE_RPC ws://execution:8551 (mainnet) / http://execution:8551 (Sepolia) base-consensus engine RPC
BASE_NODE_L2_ENGINE_AUTH /tmp/engine-auth-jwt JWT secret path
BASE_NODE_L2_ENGINE_AUTH_RAW hex-encoded JWT Raw JWT secret
OP_NODE_P2P_AGENT base P2P agent string
OP_NODE_P2P_LISTEN_IP 0.0.0.0 P2P listen IP
OP_NODE_P2P_LISTEN_TCP_PORT 9222 P2P TCP
OP_NODE_P2P_LISTEN_UDP_PORT 9222 P2P UDP
OP_NODE_INTERNAL_IP true Enable internal-IP handling
OP_NODE_P2P_BOOTNODES (ENR list) Bootnodes
OP_RETH_DISABLE_DISCOVERY false Reth discovery
OP_RETH_DISABLE_TX_POOL_GOSSIP true Reth txpool gossip
OP_RETH_OP_NETWORK base / base-sepolia Reth network
OP_NODE_RPC_ADDR 0.0.0.0 op-node RPC bind
OP_NODE_RPC_PORT 8545 op-node RPC port
GETH_CACHE 20480 Geth cache (MB)
GETH_CACHE_DATABASE 20 % to DB cache
GETH_CACHE_GC 12 % to GC cache
GETH_CACHE_SNAPSHOT 24 % to snapshot cache
GETH_CACHE_TRIE 44 % to trie cache
OP_GETH_NET_RESTRICT 10.0.0.0/8 Geth admin restrict CIDR
OP_NODE_LOG_LEVEL info op-node log level
OP_NODE_LOG_FORMAT json op-node log format
OP_NODE_SNAPSHOT_LOG /tmp/op-node-snapshot-log Snapshot log path
OP_NODE_METRICS_ENABLED true op-node metrics
OP_NODE_METRICS_ADDR 0.0.0.0 op-node metrics bind
OP_NODE_METRICS_PORT 7300 op-node metrics port
BASE_NODE_LOG_VERBOSITY 3 base-consensus log verbosity
BASE_NODE_LOG_FORMAT json base-consensus log format
BASE_NODE_METRICS_ENABLED true base-consensus metrics
BASE_NODE_METRICS_ADDR 0.0.0.0 base-consensus metrics bind
BASE_NODE_METRICS_PORT 7300 base-consensus metrics port
STATSD_ADDRESS 172.17.0.1 StatsD address (base-consensus only, best effort)

3.8 Pinned Upstream Versions (versions.json)

{
  "base_reth_node": {
    "tag": "v0.7.6",
    "commit": "5759d44b9384fd2ecde6c3fea6372e7c096e6267",
    "owner": "base",
    "repo": "base",
    "tracking": "release"
  },
  "nethermind": {
    "tag": "1.36.2",
    "commit": "f5507dec1c9c7f5e31dadae445c08622be166054",
    "owner": "NethermindEth",
    "repo": "nethermind",
    "tracking": "release"
  },
  "op_geth": {
    "tag": "v1.101702.0",
    "commit": "d0734fd5f44234cde3b0a7c4beb1256fc6feedef",
    "owner": "ethereum-optimism",
    "repo": "op-geth",
    "tracking": "release"
  },
  "op_node": {
    "tag": "op-node/v1.16.11",
    "commit": "cba7aba0c98aae22720b21c3a023990a486cb6e0",
    "tagPrefix": "op-node",
    "owner": "ethereum-optimism",
    "repo": "optimism",
    "tracking": "release"
  }
}

versions.env is regenerated from this JSON by dependency_updater and consumed by every Dockerfile to clone the upstream repos at exactly these commits. See §8 for the updater internals.


4. Deployment & Scaling

4.1 Single-Host Deployment

git clone https://github.com/base/node.git
cd node

# 1. Pick a client and an env file
echo 'CLIENT=reth'                  >  .env.local
echo 'HOST_DATA_DIR=./reth-data'    >> .env.local
echo 'USE_BASE_CONSENSUS=true'      >> .env.local

# 2. Edit .env.mainnet — set L1 endpoints

# 3. Build + start
CLIENT=reth docker compose up --build -d

# 4. Watch logs
docker compose logs -f

4.2 Multi-Node (Mainnet + Sepolia) On One Host

Use distinct project names to keep container names separate, and distinct host ports + data directories to keep state apart:

# Mainnet
docker compose -p base-mainnet up --build -d

# Sepolia (in a sibling clone or with a different docker-compose port-remap)
NETWORK_ENV=.env.sepolia HOST_DATA_DIR=./reth-data-sepolia \
  docker compose -p base-sepolia -f docker-compose.sepolia.yml up --build -d

4.3 Build Times

Client Cold build (AWS c6i.4xlarge) Notable cost
geth 5–10 min Go link step
nethermind 10–20 min dotnet NuGet restore
reth 30–60+ min Rust maxperf profile, two binaries

Note

Use docker compose build (no --no-cache) to benefit from layer caching across rebuilds. Only fall back to --no-cache when you've changed versions.env or want to re-verify upstream.

4.4 Scaling

This stack is designed for one node per host. To run a fleet:

  • One VM per node, all with the same .env.local template.
  • Configure your DNS / load balancer to round-robin RPC across host ports 8545.
  • For peer reachability, every node needs its own routable public IP and open 30303/9222 ports — they cannot share an IP without aggressive port-remapping.

Tip

A single-node cluster is the typical Base operator deployment. If you need a multi-node RPC pool, prefer running each instance on its own host and load-balancing the JSON-RPC layer rather than trying to share state across containers.


5. Operations

5.1 Graceful Shutdown

docker compose down

Compose sends SIGTERM and then SIGKILL after stop_grace_period (default 10 s). The entrypoints pass signals through because they use exec. However, supervisord.conf declares stopwaitsecs=300 — supervisord itself waits up to 5 min for child shutdown — but compose's 10 s grace period preempts it.

Important

For production, override the grace period in docker-compose.yml so the client can flush its database cleanly:

services:
  execution:
    stop_grace_period: 5m
  node:
    stop_grace_period: 5m

5.2 Data Backup

docker compose down
tar --use-compress-program='zstd -T0' -cf base-data.tar.zst ${HOST_DATA_DIR}
docker compose up -d

Warning

Back up only with the node stopped. Hot-backing-up an open MDBX (reth), LevelDB (geth), or Nethermind store yields an inconsistent and unusable snapshot.

5.3 Upgrade To A New Release

git -C /path/to/node pull
docker compose build --no-cache
docker compose up -d

The Dockerfile's git rev-parse HEAD == $<COMPONENT>_COMMIT assertion guarantees the new binaries match the upstream commit recorded in versions.env.

5.4 Switch Clients

docker compose down
mv ./geth-data /some/archive/path        # or rm -rf, if you don't want it
HOST_DATA_DIR=./reth-data CLIENT=reth docker compose up --build -d

You will need to re-sync from a snapshot (much faster than from genesis).

5.5 Log Rotation

Docker captures all stdout/stderr. Configure the daemon to rotate:

// /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "10"
  }
}

Restart the Docker daemon.

5.6 Health Checks

The compose file does not declare healthchecks. For production, add:

services:
  execution:
    healthcheck:
      test: ["CMD", "curl", "-f", "-s", "-X", "POST",
             "-H", "Content-Type: application/json",
             "-d", '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}',
             "http://localhost:8545"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 300s

For nethermind, the binary exposes its own health endpoint (HealthChecks.Enabled=true is set by the nethermind entrypoint). Query at http://<host>:8545/health.

5.7 Startup Checklist

  • [ ] L1 endpoints configured in .env.mainnet or .env.sepolia
  • [ ] CLIENT chosen and HOST_DATA_DIR set accordingly
  • [ ] USE_BASE_CONSENSUS consistent with CLIENT (true only with reth)
  • [ ] Firewall allows 30303/tcp+udp and 9222/tcp+udp inbound
  • [ ] Host has free disk for 2 × chain size + snapshot + 20 %
  • [ ] Snapshot downloaded and extracted (recommended)
  • [ ] Logs rotate via Docker daemon config
  • [ ] Monitoring scrapes :7301/metrics and :7300/metrics
  • [ ] Backup strategy defined for ${HOST_DATA_DIR}

5.8 Routine Operations

Task Command
Start docker compose up --build -d
Stop docker compose down
Tail logs docker compose logs -f --tail=100
Restart one service docker compose restart execution
Inspect container docker compose exec execution bash
Check disk du -sh ./reth-data && df -h .
Quick RPC check curl -s -X POST http://localhost:8545 -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'

Tip

Most operational glitches resolve with docker compose restart of the affected service. Reach for docker compose down && up --build only when you suspect a corrupt image or a Dockerfile/env change wasn't picked up.

Note

The two containers share a single versions.env — both client and consensus images are rebuilt from the same pinned commit set. There is no scenario where they can diverge between compose build and compose up.


6. Performance

6.1 Geth Cache Configuration

Defaults assume a 64 GB+ host:

Variable Default Unit Derived
GETH_CACHE 20480 MB 20 GB total cache
GETH_CACHE_DATABASE 20 % 20 % of total (~4 GB)
GETH_CACHE_GC 12 % 12 % (~2.5 GB)
GETH_CACHE_SNAPSHOT 24 % 24 % (~5 GB)
GETH_CACHE_TRIE 44 % 44 % (~9 GB)

Scale GETH_CACHE proportionally to host RAM. The percentage-based sub-caches scale with it.

6.2 Geth Network Restriction

OP_GETH_NET_RESTRICT="10.0.0.0/8"

Restricts Geth's admin namespace to the declared CIDR. Production deployments often run nodes inside a VPC on 10.0.0.0/8. If your bastion or peer network is on a different CIDR, update this value.

6.3 Geth Sync And GC Modes

OP_GETH_SYNCMODE=full      # default, safest
OP_GETH_SYNCMODE=snap      # experimental, faster first sync
OP_GETH_GCMODE=full        # archive mode (default)
OP_GETH_GCMODE=archive     # full history retained

6.4 Reth maxperf Build Profile

The reth Dockerfile uses cargo build --profile maxperf, enabling LTO and aggressive optimization. Build time is significantly longer; runtime performance is significantly better than debug or default release.

6.5 Peer Limits

Client Limit Source
Geth --maxpeers=100 geth-entrypoint (hard-coded)
Reth --max-outbound-peers=100 reth-entrypoint (hard-coded)
Nethermind (binary default) no peer flag set

6.6 Disk I/O

  • Use NVMe. SATA SSD cannot keep up with Base's block rate for long.
  • RAID 0 across all local NVMe drives is what Coinbase uses (/dev/nvme*).
  • ext4 is preferred over btrfs/zfs — copy-on-write filesystems impose unnecessary overhead on client databases.

6.7 Reth Pruning

RETH_PRUNING_ARGS (commented-out in shipped env, default value provided):

RETH_PRUNING_ARGS="--prune.senderrecovery.distance=50000 --prune.transactionlookup.distance=50000 --prune.receipts.distance=50000 --prune.accounthistory.distance=50000 --prune.storagehistory.distance=50000 --prune.bodies.distance=50000"

Caution

Pruning distance must exceed 10064. Published pruned snapshots use 1_339_200 (~31 days). A distance below 10064 can break consensus participation. Node type cannot be changed after initial sync — archive ↔ pruned conversions are unsupported by reth.

6.8 Reth Historical Proofs Preflight

When RETH_HISTORICAL_PROOFS=true and RETH_HISTORICAL_PROOFS_STORAGE_PATH is set, the reth entrypoint does a start-wait-stop-init dance before the main boot:

  1. Launch base-reth-node node ... with minimal RPC config.
  2. Poll eth_getBlockByNumber until the returned block number is non-zero. Timeout: 6 hours (MAX_WAIT=$((60 * 60 * 6))).
  3. kill "$PID" and wait for the process to exit.
  4. Run base-reth-node proofs init --proofs-history.storage-path=$PATH (idempotent).
  5. Proceed to the main exec with --proofs-history arguments appended.

6.9 Build-Time mold Linker (Reth Only)

The reth Dockerfile installs mold v2.40.4 with SHA-256-verified downloads:

Arch SHA-256
x86_64 4c999e19ffa31afa5aa429c679b665d5e2ca5a6b6832ad4b79668e8dcf3d8ec1
aarch64 / arm64 c799b9ccae8728793da2186718fbe53b76400a9da396184fac0c64aa3298ec37
armv7l / armv6l d82792748a81202423ddd2496fc8719404fe694493abdef691cc080392ee44bf

Other architectures abort the build with unsupported architecture: <uname -m>.


7. Security

7.1 Threat Model — Chain Operator's View

A Base node operator faces five categories of risk; each maps to a hardening checklist below.

  1. RPC exposure — public JSON-RPC reveals chain data and accepts state-mutating calls; some methods (debug_*, admin_*) are abusable.
  2. Engine API exposure — Engine API on port 8551 carries the JWT and drives the execution client. Exposing it externally is a direct compromise path because the shipped JWT is a public sample.
  3. Peer authentication — Base uses libp2p with discv5; peer auth is cryptographic but the bootnode list is hardcoded.
  4. Validator/sequencer slashing — base/node is not a sequencer or validator. There is no slashing risk from operating a follower node.
  5. Supply chain — every build clones four upstream repos. A force-pushed tag, a tampered binary, or a compromised CI dependency could ship malicious code.

7.2 Authentication & Exposure

Surface AuthN Default exposure
Execution JSON-RPC (8545/8546) None Published to host; bound 0.0.0.0 inside container
Consensus JSON-RPC (host 7545) None Published to host
Engine API (8551) JWT Not published; container-to-container only
Metrics (7301, 7300) None Published to host
pprof (6060, consensus) None Published to host

Caution

All published RPC and metrics ports bind 0.0.0.0. On a host with a public IP and no firewall, your RPC is open to the internet. Restrict with iptables/ufw/security-group, or rebind in docker-compose.yml to 127.0.0.1:<port>:<port>.

services:
  execution:
    ports:
      - "127.0.0.1:8545:8545"
      - "127.0.0.1:8546:8546"
      - "127.0.0.1:7301:6060"
      - "30303:30303"
      - "30303:30303/udp"
  node:
    ports:
      - "127.0.0.1:7545:8545"
      - "127.0.0.1:7300:7300"
      - "127.0.0.1:6060:6060"
      - "9222:9222"
      - "9222:9222/udp"

This keeps RPC and metrics local while still allowing P2P inbound.

7.4 JWT Secret Handling

  • Storage: written to /tmp/engine-auth-jwt inside each container at every boot (ephemeral; not on a host volume).
  • Source: plaintext hex in BASE_NODE_L2_ENGINE_AUTH_RAW inside .env.mainnet / .env.sepolia.
  • Risk: shipped values are public and identical across deployments. Acceptable only because port 8551 is unpublished.

Warning

Generate a per-deployment JWT before exposing port 8551 anywhere — even on an internal LAN:

openssl rand -hex 32 | tr -d '\n' > /tmp/my-jwt
# Set BASE_NODE_L2_ENGINE_AUTH_RAW in .env.* to the generated hex

7.5 pprof Disclosure Surface

Port 6060 (consensus pprof) exposes Go heap, goroutine, and CPU dumps without authentication — useful for diagnostics, dangerous in production. Rebind to 127.0.0.1:6060:6060 or remove the port mapping entirely.

7.6 Privilege Model

  • Containers run as root (no USER directive in any Dockerfile).
  • Filesystem is read-write everywhere except where Docker bind-mounts (/data).
  • No Linux capabilities explicitly dropped.
  • No read-only root filesystem.

Recommended hardening:

  • Add USER to each Dockerfile's final stage; chown /data accordingly.
  • Add read_only: true and a tmpfs for /tmp in compose.
  • Use cap_drop: [ALL] and add only what's required (NET_BIND_SERVICE for ports < 1024, not currently needed since all listening ports are >= 6060).

7.7 Supply Chain

Source Verification
Public Docker images (golang, rust, ubuntu, dotnet/aspnet) Docker registry signing
mold binary (GitHub releases) SHA-256 hash check (good)
just installer (just.systems/install.sh) `curl ...
Upstream Go/Rust/.NET source (op-node, op-geth, base-reth-node, nethermind) git rev-parse HEAD == $<COMPONENT>_COMMIT (good)
get_public_ip IP-echo None — could be spoofed; impact is bad peer reachability, not compromise

Caution

The just install step uses an unchecksummed pipe-to-bash. A compromise of just.systems would inject code into the build. Replace with a versioned download + SHA verification (mirror the mold pattern).

7.8 Bootnode ENRs

.env.mainnet and .env.sepolia embed Coinbase-operated Base bootnode ENRs in OP_NODE_P2P_BOOTNODES and BASE_NODE_P2P_BOOTNODES. If all bootnodes become unreachable, peer discovery breaks until the env files are updated. Not an attack surface — a single point of failure for discovery only.

7.9 Secrets In Version Control

  • .env.mainnet and .env.sepolia are committed. They contain the sample JWT and bootnode ENRs.
  • .env is committed. It has no secrets.
  • L1 RPC URLs (often containing API keys) are not committed — operators fill them in locally.

Important

If you customize an env file with secrets, copy to .env.mainnet.local (or similar), .gitignore it, and reference it via NETWORK_ENV. Never commit your provider API keys.

7.10 Vulnerability Reporting

SECURITY.md: all vulnerabilities go to HackerOne against the Coinbase program (hackerone.com/coinbase). The program covers Base network, bridge contracts, and infrastructure. Do not file security issues on GitHub.


8. Developer Internals

8.1 Repository Layout

node/
|-- README.md                       # User-facing quick start
|-- CONTRIBUTING.md                 # Contribution guidelines
|-- SECURITY.md                     # HackerOne reporting
|-- LICENSE
|-- logo.webp
|-- docker-compose.yml              # Two-service orchestration
|-- .env                            # Host-side defaults (CLIENT, HOST_DATA_DIR)
|-- .env.mainnet                    # Mainnet env file
|-- .env.sepolia                    # Sepolia testnet env file
|-- .dockerignore                   # Excludes *-data directories
|-- .gitignore
|-- versions.env                    # Pinned upstream commits/tags (shell exports)
|-- versions.json                   # Same data in JSON (canonical; updater rewrites)
|-- supervisord.conf                # Supervisor program definitions
|-- consensus-entrypoint            # Dispatcher: base-consensus vs op-node
|-- base-consensus-entrypoint       # Base Rust consensus launcher
|-- op-node-entrypoint              # OP Stack Go consensus launcher
|-- go.mod                          # Root-level (empty module, vestigial)
|
|-- geth/
|   |-- Dockerfile                  # Multi-stage geth build
|   `-- geth-entrypoint             # Geth launch script
|
|-- nethermind/
|   |-- Dockerfile                  # Multi-stage nethermind build
|   `-- nethermind-entrypoint       # Nethermind launch script
|
|-- reth/
|   |-- Dockerfile                  # Multi-stage reth build with mold linker
|   |-- README.md                   # Reth-specific notes (Flashblocks)
|   `-- reth-entrypoint             # Reth launch script
|
`-- dependency_updater/             # Go CI utility (not runtime)
    |-- go.mod
    |-- go.sum
    |-- dependency_updater.go       # Main CLI, updater(), GitHub tag listing
    |-- version.go                  # Semver parsing, RC handling, upgrade validation
    `-- version_test.go             # Unit tests for version.go

8.2 The Compose Stack — Wiring

docker-compose.yml declares two services built from the same per-client Dockerfile. In short:

  • Both services have build: { context: ., dockerfile: ${CLIENT:-geth}/Dockerfile }.
  • Both services share env_file: [${NETWORK_ENV:-.env.mainnet}].
  • Both services pass USE_BASE_CONSENSUS from the shell into the container via environment:.
  • The execution service mounts ${HOST_DATA_DIR}:/data for chain state.
  • The node service overrides command: ./consensus-entrypoint (vs. the Dockerfile's default).
  • The node service depends on the execution service via depends_on: (so the Engine API poll doesn't run before the execution container starts).

8.3 Multi-Stage Build Pattern

Every client Dockerfile follows a consistent recipe:

  1. op stage (golang:1.24): install just, copy versions.env, clone $OP_NODE_REPO at $OP_NODE_TAG, verify HEAD == $OP_NODE_COMMIT, build op-node.
  2. Client builder stage: clone the client repo at its pinned tag and commit, verify, build the binary.
  3. Final stage: copy op-node, the client binary, supervisord.conf, and entrypoint scripts. Set CMD ["/usr/bin/supervisord"].
COPY versions.env /tmp/versions.env

RUN . /tmp/versions.env && \
    git clone $<COMPONENT>_REPO --branch $<COMPONENT>_TAG --single-branch . && \
    git switch -c branch-$<COMPONENT>_TAG && \
    bash -c '[ "$(git rev-parse HEAD)" = "$<COMPONENT>_COMMIT" ]'

The final assertion protects against:

  • Tag force-push by upstream maintainers.
  • Tag re-pointing to a different commit.
  • Man-in-the-middle on git clone (HTTPS + git's certificate validation).

8.4 Build Tool Per Client

Client Build invocation
geth go run build/ci.go install -static ./cmd/geth
nethermind dotnet publish src/Nethermind/Nethermind.Runner -c $BUILD_CONFIG -a $arch -o /publish --sc false
reth cargo build --bin base-reth-node --bin base-consensus --profile maxperf
op-node (geth+reth) cd op-node && make VERSION=$OP_NODE_TAG op-node
op-node (nethermind) cd op-node && just VERSION=$OP_NODE_TAG op-node

Note

Only the nethermind Dockerfile uses just to build op-node. Geth and reth use make, despite both installing just in the op stage. This is a cosmetic inconsistency, not a bug — both Makefile and justfile exist upstream in ethereum-optimism/optimism.

8.5 supervisord And The Entrypoints

Every client image uses supervisord as PID 1:

[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0

[program:base-consensus]
command=/app/consensus-entrypoint
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
stopwaitsecs=300

[program:op-execution]
command=/app/execution-entrypoint
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
stopwaitsecs=300

Note

Both programs are declared, but each container invokes only one: execution-entrypoint for the execution container, consensus-entrypoint for the node container. The dual declaration is a remnant of an earlier single-container model — visually confusing, but harmless.

stopwaitsecs=300 gives each child up to 5 minutes to flush; compose's stop_grace_period overrides this externally (default 10 s — hence the recommendation in §5.1 to override it to 5m).

8.6 op-node-entrypoint Flow

  1. Validate $OP_NODE_NETWORK or $OP_NODE_ROLLUP_CONFIG.
  2. Poll ${OP_NODE_L2_ENGINE_RPC} until it returns HTTP 401 (execution ready).
  3. get_public_ip() — try four IP providers in sequence; fail with exit code 8 if all fail.
  4. export OP_NODE_P2P_ADVERTISE_IP=$PUBLIC_IP.
  5. Write $BASE_NODE_L2_ENGINE_AUTH_RAW to $BASE_NODE_L2_ENGINE_AUTH (default /tmp/engine-auth-jwt).
  6. export OP_NODE_L2_ENGINE_AUTH=$BASE_NODE_L2_ENGINE_AUTH.
  7. exec ./op-node.

8.7 base-consensus-entrypoint Flow

  1. Validate $BASE_NODE_NETWORK.
  2. Same engine-readiness poll.
  3. Same public-IP detection → BASE_NODE_P2P_ADVERTISE_IP.
  4. Write JWT.
  5. Conditional dispatch:
    • $BASE_NODE_SOURCE_L2_RPC set → exec ./base-consensus follow (follow-mode).
    • Otherwise → exec ./base-consensus node.

8.8 Execution Entrypoints

Geth

exec ./geth \
    --datadir="$GETH_DATA_DIR" \
    --verbosity="$VERBOSITY" \
    --http --http.addr=0.0.0.0 --http.port="$RPC_PORT" \
    --http.api=web3,debug,eth,net,engine \
    --authrpc.addr=0.0.0.0 --authrpc.port="$AUTHRPC_PORT" \
    --authrpc.jwtsecret="$BASE_NODE_L2_ENGINE_AUTH" \
    --ws --ws.addr=0.0.0.0 --ws.port="$WS_PORT" \
    --ws.api=debug,eth,net,engine \
    --metrics --metrics.addr=0.0.0.0 --metrics.port="$METRICS_PORT" \
    --syncmode="$OP_GETH_SYNCMODE" --gcmode="$OP_GETH_GCMODE" \
    --maxpeers=100 \
    --rollup.sequencerhttp="$OP_GETH_SEQUENCER_HTTP" \
    --rollup.halt=major --op-network="$OP_NODE_NETWORK" \
    --discovery.port="$DISCOVERY_PORT" --port="$P2P_PORT" \
    --rollup.disabletxpoolgossip=true \
    --cache="$GETH_CACHE" \
    --cache.database="$GETH_CACHE_DATABASE" \
    --cache.gc="$GETH_CACHE_GC" \
    --cache.snapshot="$GETH_CACHE_SNAPSHOT" \
    --cache.trie="$GETH_CACHE_TRIE" \
    $ADDITIONAL_ARGS

$ADDITIONAL_ARGS is intentionally unquoted so shell word-splitting applies — this is how optional flags (--ethstats, --bootnodes, --state.scheme, --rpc.allow-unprotected-txs, --nat=extip:) get appended.

Reth

exec ./base-reth-node node \
  -$LOG_LEVEL \
  --datadir="$RETH_DATA_DIR" \
  --log.stdout.format json \
  --ws --ws.origins="*" --ws.addr=0.0.0.0 --ws.port="$WS_PORT" \
  --ws.api=web3,debug,eth,net,txpool \
  --http --http.corsdomain="*" --http.addr=0.0.0.0 --http.port="$RPC_PORT" \
  --http.api=web3,debug,eth,net,txpool,miner \
  --ipcpath="$IPC_PATH" \
  --authrpc.addr=0.0.0.0 --authrpc.port="$AUTHRPC_PORT" \
  --authrpc.jwtsecret="$BASE_NODE_L2_ENGINE_AUTH" \
  --metrics=0.0.0.0:"$METRICS_PORT" \
  --max-outbound-peers=100 \
  --chain "$RETH_CHAIN" \
  --rollup.sequencer-http="$RETH_SEQUENCER_HTTP" \
  --rollup.disable-tx-pool-gossip \
  --discovery.port="$DISCOVERY_PORT" --port="$P2P_PORT" \
  $ADDITIONAL_ARGS

Reth-specific extensions:

  • IPC path: $IPC_PATH (default /data/reth.ipc).
  • Flashblocks: --websocket-url=$RETH_FB_WEBSOCKET_URL (when set).
  • Historical proofs: --proofs-history --proofs-history.storage-path=$RETH_HISTORICAL_PROOFS_STORAGE_PATH (when enabled).
  • Log level mapping: error/warn/info/debug/trace → -v/-vv/-vvv/-vvvv/-vvvvv.

Nethermind

exec ./nethermind \
    --config="$OP_NODE_NETWORK" \
    --datadir="$NETHERMIND_DATA_DIR" \
    --Optimism.SequencerUrl=$OP_SEQUENCER_HTTP \
    --log="$NETHERMIND_LOG_LEVEL" \
    --JsonRpc.Enabled=true --JsonRpc.Host=0.0.0.0 \
    --JsonRpc.WebSocketsPort="$WS_PORT" --JsonRpc.Port="$RPC_PORT" \
    --JsonRpc.JwtSecretFile="$BASE_NODE_L2_ENGINE_AUTH" \
    --JsonRpc.EngineHost=0.0.0.0 --JsonRpc.EnginePort="$AUTHRPC_PORT" \
    --HealthChecks.Enabled=true \
    --Metrics.Enabled=true --Metrics.ExposePort="$METRICS_PORT" \
    --Network.P2PPort="$P2P_PORT" --Network.DiscoveryPort="$DISCOVERY_PORT" \
    $ADDITIONAL_ARGS

8.9 The dependency_updater Go CLI

The dependency_updater/ Go package is a CI-only utility that bumps the pinned upstream versions automatically. It does not run at node startup.

8.9.1 Purpose

  • Query GitHub for the latest valid tag on each upstream repo (op-node, op-geth, nethermind, base-reth-node).
  • Filter candidate tags by a tracking mode (release, tag, or branch).
  • Pick the highest semver among valid candidates.
  • Rewrite versions.json.
  • Regenerate versions.env (JSON → shell exports).
  • Optionally create a git commit and write TITLE/DESC to $GITHUB_OUTPUT.

8.9.2 CLI Surface (urfave/cli/v3)

updater --token <GITHUB_TOKEN> --repo <path> [--commit] [--github-action]
Flag Required Source Purpose
--token Yes env GITHUB_TOKEN PAT or GITHUB_TOKEN
--repo Yes CLI arg Path to the node repo root
--commit No CLI arg Stage changes and create a git commit locally
--github-action No CLI arg Write TITLE/DESC to $GITHUB_OUTPUT instead of committing

Implementation: dependency_updater/dependency_updater.go:42-79 — urfave/cli/v3 command construction. The Action calls updater(token, repo, commit, githubAction) and wraps any error as failed to run updater: %s.

8.9.3 Tracking Modes (declared per-dependency in versions.json)

Mode Filter Use case
release Stable tags only (no prerelease suffix) Production pins (op_node, op_geth, base_reth_node, nethermind)
tag Stable tags or RC tags (excludes -alpha, -beta, -synctest) Early adoption of RCs
branch Latest commit on a named branch Bleeding edge

8.9.4 Version Semantics (version.go)

The updater parses tags into semver via Masterminds/semver/v3, with three preprocessing steps:

  1. Tag prefix stripping — e.g. op-node/v1.16.2 → v1.16.2 when tagPrefix=op-node.
  2. RC format normalization — -rc1, -rc-1, -RC1 → -rc.1 (regex (?i)-rc[.-]?(\d+) in rcPattern).
  3. Parse with semver.

Predicate functions (all in version.go):

Function True when
IsReleaseVersion(tag, prefix) Prerelease is empty
IsRCVersion(tag, prefix) Prerelease matches /^-rc[.-]?\d+$/i (regex rcOnlyPattern)
IsReleaseOrRCVersion Either of the above

ValidateVersionUpgrade(currentTag, newTag, tagPrefix) rejects downgrades:

  • If currentTag is empty (first-time setup), any parseable newTag is valid.
  • If currentTag is unparseable, validation ignores the current version and only checks that newTag is parseable.
  • Stable (v0.3.0) → RC (v0.3.0-rc2) is a downgrade (semver-correct: RCs sort before releases).

8.9.5 Upgrade Algorithm Per Dependency

ListTags(owner, repo, page=1..N):
  for each tag:
    strip tagPrefix if set
    filter by tracking mode (release | tag)
    try ValidateVersionUpgrade(current, tag) -> skip on error
    accumulate valid candidates
  pick max via CompareVersions()
  if max != current:
    fetch commit SHA
    diffUrl = github.com/owner/repo/compare/<current>...<selected>
    rewrite versions.json

For tracking="branch": no tag filtering; call ListCommits(SHA=branch) and take the first result.

8.9.6 Commit Construction

createCommitMessage(updatedDependencies, repoPath, githubAction) produces:

### Dependency Updates
**<repo>** - <newTag>:  [diff](<diffUrl>)
**<repo>** - <newTag>:  [diff](<diffUrl>)
...

Title: chore: updated <repo>, <repo>, ....

If --github-action is set: writes to $GITHUB_OUTPUT with this schema:

TITLE=<title>
DESC<<EOF
<description>
EOF

If --commit is set: runs git commit -am <title> -m <description>.

8.9.7 createVersionsEnv — JSON → Shell Regeneration

for dependency in dependencies:
    dependencyPrefix = strings.ToUpper(dependency)   // "op_node" -> "OP_NODE"
    if tracking == "branch": tag = branch            // branch name acts as "tag"
    emit: export <PREFIX>_TAG=<tag>
          export <PREFIX>_COMMIT=<commit>
          export <PREFIX>_REPO=https://github.com/<owner>/<repo>.git
sort lines
write to versions.env

Note

Keys in versions.json use snake_case (op_node, base_reth_node). After ToUpper, they become OP_NODE, BASE_RETH_NODE — exactly the prefixes the Dockerfiles expect.

8.9.8 Retry Logic

retry.Do0(ctx, 3, retry.Fixed(1*time.Second), func() error { ... })

Each dependency update is wrapped in 3 attempts with 1-second fixed delay, using ethereum-optimism/optimism/op-service/retry.

8.9.9 Running Locally

cd dependency_updater
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
go build -o updater .
./updater --repo /path/to/node --commit

Without --commit / --github-action, the tool writes updated versions.json + versions.env but doesn't create a commit.

8.9.10 Manual Version Bump

If you need to pin to a version the updater wouldn't pick (e.g. an -rc tag with tracking=release):

  1. Edit versions.json directly.
  2. Run ./updater --repo . (no --commit) to regenerate versions.env.
  3. Commit both files.
  4. docker compose build --no-cache to rebuild.

8.10 Where State Lives

Item Persistence
Chain DB (reth/geth/nethermind) Host bind mount ${HOST_DATA_DIR} → /data
op-node snapshot log /tmp/op-node-snapshot-log (ephemeral, in-container)
JWT secret /tmp/engine-auth-jwt (ephemeral, in-container)
Reth IPC /data/reth.ipc (in the bind mount)
Pinned upstream commits versions.env + versions.json (in repo)
Logs Docker daemon (rotation per daemon.json)

The bind mount is the only persistent state. Loss of ${HOST_DATA_DIR} triggers a fresh sync.

8.11 Upgrade Story

The contract between the platform and operators:

  1. CI runs dependency_updater against each upstream repo (release tracking by default).
  2. The updater opens a PR that bumps versions.json + versions.env.
  3. Operators git pull and docker compose build --no-cache.
  4. Each Dockerfile re-clones the upstream repos at the new pinned commits and rebuilds the binaries.
  5. The commit-hash assertion ensures the new binaries are byte-for-byte what the updater pinned.

There is no in-place upgrade — the model is "rebuild from source at the pinned commit". This trades cold-start time for trust.

8.12 Testing Strategy

8.12.1 Unit Tests

Only the dependency_updater package has tests. Location: dependency_updater/version_test.go.

Test Coverage
TestNormalizeRCFormat RC format regex normalization (8 cases)
TestParseVersion Tag → semver parsing including prefix stripping (12 cases)
TestValidateVersionUpgrade Upgrade validation with downgrade detection (20+ cases)
TestCompareVersions Version comparison (7 cases)
TestIsReleaseVersion Stable-release predicate (12 cases)
TestIsRCVersion RC predicate (15 cases)
TestIsReleaseOrRCVersion Combined predicate (10 cases)
TestRCVersionOrdering End-to-end ordering v0.2.2 → v0.3.0-rc.1 → v0.3.0-rc.2 → v0.3.0 → v0.3.1
cd dependency_updater
go test -v ./...

8.12.2 No Integration Tests

The repository ships no integration tests for the Docker stack. Verification is implicit:

  • Build-time commit hash verification.
  • Entrypoint engine-readiness polling at runtime.
  • Manual checks of peer count, sync status, and RPC responses.

8.13 Known Issues & Behaviors

Issue Detail Reference
CLIENT default mismatch README says reth is default; compose falls back to geth via ${CLIENT:-geth} issue #1010
base-consensus only in reth image base-consensus-entrypoint is shipped only by reth/Dockerfile; geth/nethermind image fails fast with Base client is not supported for this node type issue #1016 (closed)
USE_BASE_CONSENSUS not in per-network env Only top-level .env defines it; passed via environment: not env_file: — operators who delete .env get default false issue #1014 (closed)
OP_NODE_SYNCMODE=execution-layer deprecated Comment in env files says so; safe until upstream removes support —
Public IP detect failure → exit 8 Four-fallback chain; no manual override —
Historical-proofs preflight up to 6 hours MAX_WAIT=$((60 * 60 * 6)) in reth-entrypoint; fresh DB only —
Two go.mod files Root go.mod is module dependency_updater (no requires); dependency_updater/go.mod is github.com/base/node/dependency_updater (with requires). Root file is vestigial. —
op-node build tool inconsistent geth+reth use make, nethermind uses just —
supervisord.conf declares both programs Each container only invokes one; visually confusing, harmless —
Sample JWT shipped Public sample value — fine while 8551 is unpublished, dangerous if exposed §7.4
Deprecated docker-compose (v1) hyphenated form mentioned in reth/README.md Use docker compose two-words issue #1012 (closed)

8.14 Potential Improvements

Source-derived improvement candidates:

  1. Allow OP_NODE_P2P_ADVERTISE_IP override. get_public_ip() is currently authoritative; add if [[ -n "$OP_NODE_P2P_ADVERTISE_IP_OVERRIDE" ]]; then ... for CGNAT or air-gapped operators.
  2. Run containers as non-root. Add a USER directive in each Dockerfile's final stage.
  3. Rebind pprof to localhost. Drop 6060:6060 from compose or comment it out by default.
  4. Drop deprecated docker-compose references in repo docs.
  5. Ship base-consensus in every client image (or remove the dispatcher branch from geth/nethermind). Current state is an avoidable runtime failure mode.
  6. Add versions.json schema validation in CI (reject malformed entries — missing commit, bad tracking).
  7. Tests for dependency_updater.go. Only version.go is tested; the main updater flow (getAndUpdateDependency, createCommitMessage, createVersionsEnv) is untested.
  8. Checksum the just installer. Replace curl … | bash with a versioned download + SHA check (mirror the mold pattern).
  9. Healthchecks in docker-compose.yml. Declare per-service healthchecks; compose will auto-restart unhealthy containers.
  10. Remove vestigial root go.mod. It declares module dependency_updater with no purpose.
  11. Sensible default for HOST_IP in geth-entrypoint. Currently HOST_IP="" placeholder with a comment; promote to a real env override.
  12. Remove unused op-execution program from supervisord.conf (or split into two configs to match the current two-container layout).

8.15 Contributing

See CONTRIBUTING.md. Highlights:

  • Open an issue first for non-trivial changes.
  • Every PR touching dependency_updater/ should add tests.
  • Follow the Coinbase code of conduct.
  • Security issues → HackerOne, never GitHub issues.
  • Support requests → Coinbase support, not GitHub issues.

Local development loop:

git switch -c my-fix
CLIENT=reth docker compose build --no-cache execution node
NETWORK_ENV=.env.sepolia CLIENT=reth docker compose up

dependency_updater development loop:

cd dependency_updater
go test -v ./...
go build .
./dependency_updater --token $GITHUB_TOKEN --repo /path/to/another-node-checkout

9. Integration Points

9.1 Upstream Source Repositories

Pin Upstream Tag Used by
BASE_RETH_NODE base/base v0.7.6 reth Dockerfile
NETHERMIND NethermindEth/nethermind 1.36.2 nethermind Dockerfile
OP_GETH ethereum-optimism/op-geth v1.101702.0 geth Dockerfile
OP_NODE ethereum-optimism/optimism (subdir op-node, prefix op-node) op-node/v1.16.11 all Dockerfiles' op stage

9.2 Network Endpoints The Stack Calls

Endpoint Purpose
https://mainnet-sequencer.base.org / https://sepolia-sequencer.base.org Sequencer URL (--rollup.sequencerhttp)
User-supplied OP_NODE_L1_ETH_RPC L1 execution RPC
User-supplied OP_NODE_L1_BEACON L1 beacon CL
User-supplied OP_NODE_L1_BEACON_ARCHIVER L1 blob archiver
wss://mainnet.flashblocks.base.org/ws (commented) Flashblocks websocket (reth-only)
wss://sepolia.flashblocks.base.org/ws (commented) Sepolia Flashblocks
https://ifconfig.me, https://api.ipify.org, https://ipecho.net/plain, https://v4.ident.me Public IP detection (one must respond)

9.3 GitHub API (Used by dependency_updater Only)

github.com/google/go-github/v72:

  • client.Repositories.ListTags(ctx, owner, repo, options) — paginated tag listing.
  • client.Repositories.ListCommits(ctx, owner, repo, &CommitsListOptions{SHA: branch}) — for tracking="branch".

Authentication via GITHUB_TOKEN env var (or --token flag).

9.4 GitHub Actions Output Contract

When the updater runs with --github-action, it writes to $GITHUB_OUTPUT:

TITLE=chore: updated <repo>, <repo>, ...
DESC<<EOF
### Dependency Updates
**<repo>** - <newTag>:  [diff](<diffUrl>)
...
EOF

Downstream workflow steps consume ${{ steps.<id>.outputs.TITLE }} and ${{ steps.<id>.outputs.DESC }} to open a PR.

9.5 Prometheus Scrape Targets

Target Endpoint
Execution metrics http://<host>:7301/metrics
Consensus metrics http://<host>:7300/metrics

9.6 Engine API (Internal)

Container-to-container only. The execution client exposes --authrpc on 8551 (HTTP and/or WS depending on network); the consensus client polls until HTTP 401 then connects with the JWT.

9.7 StatsD (base-consensus only)

STATSD_ADDRESS="172.17.0.1" assumes a StatsD receiver on the Docker host's bridge IP. Best-effort — base-consensus does not require StatsD.

9.8 Public Ports → Purpose Quick Reference

Host port Purpose
8545 Execution JSON-RPC
8546 Execution WebSocket
7301 Execution metrics
30303 Execution P2P (TCP+UDP)
7545 Consensus JSON-RPC
7300 Consensus metrics
9222 Consensus P2P (TCP+UDP)
6060 Consensus pprof
8551 (internal) Engine API (JWT-authenticated, not published)