# base/node Administrator & Developer Guide

Operational reference for the [base/node](https://github.com/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

```text
+--------------------------------------------------------------------+
|                        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

```text
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](https://github.com/base/node/issues/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

```bash
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`)

```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

```bash
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:

```bash
# 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

```bash
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:
> ```yaml
> services:
>   execution:
>     stop_grace_period: 5m
>   node:
>     stop_grace_period: 5m
> ```

### 5.2 Data Backup

```bash
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

```bash
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

```bash
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:

```json
// /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:

```yaml
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

```bash
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

```bash
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):

```bash
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>`.

### 7.3 Rebinding To Localhost (Recommended Hardening)

```yaml
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:
> ```bash
> 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 ... | bash` — **no checksum** (improve) |
| 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](https://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

```text
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"]`.

```dockerfile
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:

```ini
[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

```bash
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

```bash
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

```bash
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`)

```text
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

```text
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:

```markdown
### 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:

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

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

#### 8.9.7 `createVersionsEnv` — JSON → Shell Regeneration

```go
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

```go
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

```bash
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` |

```bash
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](https://github.com/base/node/issues/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](https://github.com/base/node/issues/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](https://github.com/base/node/issues/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](https://github.com/base/node/issues/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](https://github.com/coinbase/code-of-conduct).
- Security issues → HackerOne, never GitHub issues.
- Support requests → Coinbase support, not GitHub issues.

Local development loop:

```bash
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:

```bash
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](https://github.com/base/base) | `v0.7.6` | reth Dockerfile |
| `NETHERMIND` | [NethermindEth/nethermind](https://github.com/NethermindEth/nethermind) | `1.36.2` | nethermind Dockerfile |
| `OP_GETH` | [ethereum-optimism/op-geth](https://github.com/ethereum-optimism/op-geth) | `v1.101702.0` | geth Dockerfile |
| `OP_NODE` | [ethereum-optimism/optimism](https://github.com/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`:

```text
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](https://docs.base.org/)
- Run-a-node docs: [docs.base.org/chain/run-a-base-node](https://docs.base.org/chain/run-a-base-node)
- Chain stats: [base.org/stats](https://base.org/stats)
- Snapshots: [basechaindata.vercel.app](https://basechaindata.vercel.app)
- Flashblocks docs: [docs.base.org/chain/flashblocks](https://docs.base.org/chain/flashblocks)
- OP Stack docs: [docs.optimism.io](https://docs.optimism.io/)
- Reth docs: [reth.rs](https://reth.rs/)
- Bug bounty: [hackerone.com/coinbase](https://hackerone.com/coinbase)
- Discord: [discord.gg/buildonbase](https://discord.gg/buildonbase)
- Source: [github.com/base/node](https://github.com/base/node)

---

_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. Unofficial; not affiliated with or reviewed by the project._
