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_updaterCI 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:
executionruns./execution-entrypoint.noderuns./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 bundlesbase-reth-nodeandbase-consensustogether), not fromparadigmxyz/rethdirectly.
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
.envsetsUSE_BASE_CONSENSUS=truewhich only works withCLIENT=reth. Always setCLIENTexplicitly 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) andBASE_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.mainnetand.env.sepolia. Every deployment using the shipped value uses an identical JWT. This is acceptable only becauseauthrpc.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 withopenssl rand -hex 32for 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-cachewhen you've changedversions.envor 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.localtemplate. - 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/9222ports — 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.ymlso 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.mainnetor.env.sepolia - [ ]
CLIENTchosen andHOST_DATA_DIRset accordingly - [ ]
USE_BASE_CONSENSUSconsistent withCLIENT(true only with reth) - [ ] Firewall allows
30303/tcp+udpand9222/tcp+udpinbound - [ ] Host has free disk for
2 × chain size + snapshot + 20 % - [ ] Snapshot downloaded and extracted (recommended)
- [ ] Logs rotate via Docker daemon config
- [ ] Monitoring scrapes
:7301/metricsand: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 restartof the affected service. Reach fordocker compose down && up --buildonly 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 betweencompose buildandcompose 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 use1_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:
- Launch
base-reth-node node ...with minimal RPC config. - Poll
eth_getBlockByNumberuntil the returned block number is non-zero. Timeout: 6 hours (MAX_WAIT=$((60 * 60 * 6))). kill "$PID"and wait for the process to exit.- Run
base-reth-node proofs init --proofs-history.storage-path=$PATH(idempotent). - Proceed to the main
execwith--proofs-historyarguments 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.
- RPC exposure — public JSON-RPC reveals chain data and accepts state-mutating calls; some methods (
debug_*,admin_*) are abusable. - 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.
- Peer authentication — Base uses libp2p with discv5; peer auth is cryptographic but the bootnode list is hardcoded.
- Validator/sequencer slashing — base/node is not a sequencer or validator. There is no slashing risk from operating a follower node.
- 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 indocker-compose.ymlto127.0.0.1:<port>:<port>.
7.3 Rebinding To Localhost (Recommended Hardening)
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-jwtinside each container at every boot (ephemeral; not on a host volume). - Source: plaintext hex in
BASE_NODE_L2_ENGINE_AUTH_RAWinside.env.mainnet/.env.sepolia. - Risk: shipped values are public and identical across deployments. Acceptable only because port
8551is 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
USERdirective 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
USERto each Dockerfile's final stage;chown/dataaccordingly. - Add
read_only: trueand a tmpfs for/tmpin compose. - Use
cap_drop: [ALL]and add only what's required (NET_BIND_SERVICEfor 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
justinstall step uses an unchecksummed pipe-to-bash. A compromise ofjust.systemswould inject code into the build. Replace with a versioned download + SHA verification (mirror themoldpattern).
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.mainnetand.env.sepoliaare committed. They contain the sample JWT and bootnode ENRs..envis 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),.gitignoreit, and reference it viaNETWORK_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_CONSENSUSfrom the shell into the container viaenvironment:. - The
executionservice mounts${HOST_DATA_DIR}:/datafor chain state. - The
nodeservice overridescommand: ./consensus-entrypoint(vs. the Dockerfile's default). - The
nodeservice depends on theexecutionservice viadepends_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:
opstage (golang:1.24): installjust, copyversions.env, clone$OP_NODE_REPOat$OP_NODE_TAG, verifyHEAD == $OP_NODE_COMMIT, buildop-node.- Client builder stage: clone the client repo at its pinned tag and commit, verify, build the binary.
- Final stage: copy
op-node, the client binary,supervisord.conf, and entrypoint scripts. SetCMD ["/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
justto build op-node. Geth and reth usemake, despite both installingjustin the op stage. This is a cosmetic inconsistency, not a bug — bothMakefileandjustfileexist upstream inethereum-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-entrypointfor the execution container,consensus-entrypointfor 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
- Validate
$OP_NODE_NETWORKor$OP_NODE_ROLLUP_CONFIG. - Poll
${OP_NODE_L2_ENGINE_RPC}until it returns HTTP 401 (execution ready). get_public_ip()— try four IP providers in sequence; fail with exit code 8 if all fail.export OP_NODE_P2P_ADVERTISE_IP=$PUBLIC_IP.- Write
$BASE_NODE_L2_ENGINE_AUTH_RAWto$BASE_NODE_L2_ENGINE_AUTH(default/tmp/engine-auth-jwt). export OP_NODE_L2_ENGINE_AUTH=$BASE_NODE_L2_ENGINE_AUTH.exec ./op-node.
8.7 base-consensus-entrypoint Flow
- Validate
$BASE_NODE_NETWORK. - Same engine-readiness poll.
- Same public-IP detection →
BASE_NODE_P2P_ADVERTISE_IP. - Write JWT.
- Conditional dispatch:
$BASE_NODE_SOURCE_L2_RPCset →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
trackingmode (release,tag, orbranch). - Pick the highest semver among valid candidates.
- Rewrite
versions.json. - Regenerate
versions.env(JSON → shell exports). - Optionally create a git commit and write
TITLE/DESCto$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:
- Tag prefix stripping — e.g.
op-node/v1.16.2→v1.16.2whentagPrefix=op-node. - RC format normalization —
-rc1,-rc-1,-RC1→-rc.1(regex(?i)-rc[.-]?(\d+)inrcPattern). - 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
currentTagis empty (first-time setup), any parseablenewTagis valid. - If
currentTagis unparseable, validation ignores the current version and only checks thatnewTagis 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.jsonuse snake_case (op_node,base_reth_node). AfterToUpper, they becomeOP_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):
- Edit
versions.jsondirectly. - Run
./updater --repo .(no--commit) to regenerateversions.env. - Commit both files.
docker compose build --no-cacheto 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:
- CI runs
dependency_updateragainst each upstream repo (releasetracking by default). - The updater opens a PR that bumps
versions.json+versions.env. - Operators
git pullanddocker compose build --no-cache. - Each Dockerfile re-clones the upstream repos at the new pinned commits and rebuilds the binaries.
- 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:
- Allow
OP_NODE_P2P_ADVERTISE_IPoverride.get_public_ip()is currently authoritative; addif [[ -n "$OP_NODE_P2P_ADVERTISE_IP_OVERRIDE" ]]; then ...for CGNAT or air-gapped operators. - Run containers as non-root. Add a
USERdirective in each Dockerfile's final stage. - Rebind pprof to localhost. Drop
6060:6060from compose or comment it out by default. - Drop deprecated
docker-composereferences in repo docs. - Ship
base-consensusin every client image (or remove the dispatcher branch from geth/nethermind). Current state is an avoidable runtime failure mode. - Add
versions.jsonschema validation in CI (reject malformed entries — missingcommit, badtracking). - Tests for
dependency_updater.go. Onlyversion.gois tested; the main updater flow (getAndUpdateDependency,createCommitMessage,createVersionsEnv) is untested. - Checksum the
justinstaller. Replacecurl … | bashwith a versioned download + SHA check (mirror themoldpattern). - Healthchecks in
docker-compose.yml. Declare per-service healthchecks; compose will auto-restart unhealthy containers. - Remove vestigial root
go.mod. It declares moduledependency_updaterwith no purpose. - Sensible default for
HOST_IPin geth-entrypoint. CurrentlyHOST_IP=""placeholder with a comment; promote to a real env override. - Remove unused
op-executionprogram fromsupervisord.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})— fortracking="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) |
9.9 Useful Links
- Base docs: docs.base.org
- Run-a-node docs: docs.base.org/chain/run-a-base-node
- Chain stats: base.org/stats
- Snapshots: basechaindata.vercel.app
- Flashblocks docs: docs.base.org/chain/flashblocks
- OP Stack docs: docs.optimism.io
- Reth docs: reth.rs
- Bug bounty: hackerone.com/coinbase
- Discord: discord.gg/buildonbase
- Source: github.com/base/node