Base 节点架构:30 个文件如何编排一个以太坊 L2
前置知识
- ›Docker 和 Docker Compose 基础知识
- ›对以太坊 L1/L2 架构的基本理解(执行层与共识层)
- ›了解 OP Stack 是什么(Optimism 的 rollup 框架)
Base 节点架构:30 个文件如何编排一个以太坊 L2
大多数你克隆下来的代码库都是应用程序——有 src/ 目录、包管理器,以及能生成单个二进制文件的构建系统。base/node 仓库则完全不同。它是一个部署编排器——大约 30 个文件,负责从四个不同的仓库拉取、构建并串联上游二进制文件,横跨三种编程语言,最终产出一个可运行的 Base L2 以太坊节点。理解这一核心定位,是读懂整个仓库的关键所在。
本文是五篇深度解析系列的第一篇。我们将从整体上梳理整个代码库,讲解支撑一切的双服务架构,并展示一个 JSON 文件是如何驱动整条构建流水线的。
这个仓库到底是什么
如果你 git clone 这个仓库,期望找到区块链共识逻辑或 EVM 执行代码,你会大失所望。关键路径上没有 main()。base/node 本质上是一个有明确主张的组合层,它做了这几件事:
- 精确锁定版本:在单个 JSON 文件中固定四个上游依赖(Reth、Geth、Nethermind 和 op-node)的确切版本
- 从源码构建:在 Docker 多阶段构建中完成编译,并通过验证 commit hash 来防范供应链攻击
- 串联各组件:通过入口脚本处理服务发现、JWT 认证和功能开关切换
- 编排启动流程:借助 Docker Compose 确保执行客户端就绪后,共识客户端再建立连接
flowchart LR
subgraph "base/node repository"
DC[docker-compose.yml]
VJ[versions.json]
EP[Entrypoint Scripts]
DF[Dockerfiles]
end
subgraph "Upstream Repos"
R[base/base<br/>Reth + base-consensus]
G[ethereum-optimism/op-geth]
N[NethermindEth/nethermind]
O[ethereum-optimism/optimism<br/>op-node]
end
VJ -->|pins versions| DF
DF -->|clones & builds| R
DF -->|clones & builds| G
DF -->|clones & builds| N
DF -->|clones & builds| O
DC -->|orchestrates| EP
最终,无论你选择哪个执行客户端,一条 docker compose up 命令就能启动一个正在同步中的 Base 节点。
目录结构与文件职责
整个仓库只有约 30 个文件,不会有迷失在深层目录树中的烦恼。但每个文件都有明确的职责,分别对应构建与运行流水线的不同阶段。
| 路径 | 职责 | 说明 |
|---|---|---|
docker-compose.yml |
编排 | 定义两个服务及其连接关系 |
.env |
用户配置 | 默认客户端选择和数据目录 |
.env.mainnet / .env.sepolia |
网络配置 | 各网络的端点、bootnodes 和缓存设置 |
versions.json |
版本锁定 | 所有上游依赖版本的唯一真实来源 |
versions.env |
生成配置 | 可被 Dockerfile 直接 source 的 shell 导出变量 |
geth/Dockerfile |
构建流水线 | op-geth + op-node 的多阶段 Docker 构建 |
reth/Dockerfile |
构建流水线 | base-reth-node + base-consensus + op-node 的多阶段 Docker 构建 |
nethermind/Dockerfile |
构建流水线 | Nethermind + op-node 的多阶段 Docker 构建 |
geth/geth-entrypoint |
运行时 | Geth 启动脚本,含缓存调优 |
reth/reth-entrypoint |
运行时 | Reth 启动脚本,支持 Flashblocks 和历史证明 |
nethermind/nethermind-entrypoint |
运行时 | Nethermind 启动脚本 |
consensus-entrypoint |
运行时 | 分发器,路由至 op-node 或 base-consensus |
op-node-entrypoint |
运行时 | 旧版共识客户端启动脚本 |
base-consensus-entrypoint |
运行时 | 新版共识客户端启动脚本,支持 follow 模式 |
supervisord.conf |
进程管理 | 用于独立 Docker 容器运行时的回退方案 |
dependency_updater/ |
工具链 | 用于自动追踪版本更新的 Go CLI |
.github/workflows/ |
CI/CD | 构建、测试和发布自动化 |
提示: 扁平的目录结构是有意为之的。每个执行客户端都有自己的目录,其中恰好只有两个文件——一个
Dockerfile和一个入口脚本。其他所有内容都放在根目录下。这使得添加第四个客户端变得非常简单:新建一个目录,添加两个文件,更新versions.json即可。
双服务架构
base/node 的核心是 docker-compose.yml——一个极为精简的文件,定义了整个运行时拓扑。
flowchart TB
subgraph Docker["Docker Compose Network"]
subgraph EX["execution service"]
EC[Execution Client<br/>Reth / Geth / Nethermind]
end
subgraph ND["node service"]
CC[Consensus Client<br/>op-node / base-consensus]
end
end
CC -->|"Engine API<br/>port 8551<br/>JWT auth"| EC
EC -->|"depends_on"| ND
Internet -->|"P2P :30303"| EC
Internet -->|"P2P :9222"| CC
User -->|"RPC :8545"| EC
两个服务使用同一个 Dockerfile,在构建时通过 CLIENT 环境变量来选择:
# docker-compose.yml line 5
dockerfile: ${CLIENT:-geth}/Dockerfile
这意味着 docker compose build 会产出两个内容相同的镜像。差异化发生在运行时——通过 command 覆盖来实现:execution 服务运行 bash ./execution-entrypoint,而 node 服务运行 bash ./consensus-entrypoint。
第 26 行的 depends_on 指令确保 Docker 优先启动执行服务。但 Docker 的 depends_on 只等待容器启动,并不等待容器内的应用就绪。真正的就绪协调发生在共识入口脚本中——脚本会轮询 Engine API,这一模式将在第二篇中详细介绍。
supervisord.conf 存在但不会被调用的原因
你会注意到每个 Dockerfile 都以 CMD ["/usr/bin/supervisord"] 结尾,同时还有一个 supervisord.conf,它可以在单个容器中同时运行两个进程。这是在不使用 Docker Compose 的情况下运行镜像时的回退方案——由 supervisord 在单个容器中管理两个进程。
当你使用 docker compose up 时,docker-compose.yml 中的 command 指令会覆盖每个 Dockerfile 中的 CMD。执行容器只运行执行入口脚本,节点容器只运行共识入口脚本。supervisord 永远不会被调用。
flowchart TD
A{"How are you<br/>running it?"} -->|"docker compose up"| B["command overrides CMD<br/>Each service runs one process"]
A -->|"docker run"| C["CMD runs supervisord<br/>Both processes in one container"]
B --> D["execution-entrypoint"]
B --> E["consensus-entrypoint"]
C --> F["supervisord manages both<br/>execution + consensus"]
这种双模式设计是务实的选择。Docker Compose 提供了更好的日志输出、独立重启和资源隔离。但某些部署环境没有 Compose——此时带有 supervisord 的单容器方案同样好用。
三层配置体系
配置通过三个层次依次流动,后者优先级高于前者。理解这一层级关系,是排查"为什么我的节点连接到了错误的端点"此类问题的关键。
flowchart TD
A[".env<br/>CLIENT=geth<br/>USE_BASE_CONSENSUS=true<br/>HOST_DATA_DIR=./geth-data"] -->|"lowest priority"| D["Effective Config"]
B[".env.mainnet / .env.sepolia<br/>Network endpoints, bootnodes,<br/>cache settings, L1 config"] -->|"medium priority<br/>(loaded via env_file)"| D
C["Shell environment<br/>export CLIENT=reth"] -->|"highest priority"| D
第一层:.env — 默认配置只有三行:
CLIENT=${CLIENT:-geth}
HOST_DATA_DIR=./${CLIENT}-data
USE_BASE_CONSENSUS=true
这将执行客户端默认设置为 Geth,并将 base-consensus 启用为默认共识客户端。
第二层:.env.mainnet / .env.sepolia — 通过 docker-compose.yml 第 19 行的 env_file 指令加载。NETWORK_ENV 变量默认指向 .env.mainnet。要切换到 Sepolia 测试网,设置 NETWORK_ENV=.env.sepolia 即可。
第三层:Shell 环境变量 — 在运行 docker compose up 之前在 shell 中设置的任何变量,优先级都高于上述两个文件。
双命名空间模式
网络配置文件中让人困惑的地方之一,是重复的变量命名空间。看看 .env.mainnet:
OP_NODE_L2_ENGINE_RPC=http://execution:8551 # for op-node
BASE_NODE_L2_ENGINE_RPC=ws://execution:8551 # for base-consensus
同一个 Engine API 端点用两个不同的前缀定义了两次。这是因为 op-node 读取 OP_NODE_* 变量,而 base-consensus 读取 BASE_NODE_* 变量。两个客户端需要相同的信息,但使用了不同的环境变量命名约定。OP_NODE_* 命名空间是来自 Optimism 的旧版约定,BASE_NODE_* 则是 Base 专属的新版约定。
提示: 网络配置文件中硬编码的 JWT secret(
688f5d737bad920bdfb2fc2f488d6b6209eebda1dae949a8de91398d932c517a)看起来像是安全隐患,但这是有意为之的。Engine API 仅在 Docker 网络内部暴露,无法从宿主机访问。JWT 的存在是为了满足 Engine API 规范的要求,而非用于抵御外部攻击者。
版本锁定:从 versions.json 到 Docker 构建
versions.json 文件是所有上游依赖的唯一真实来源:
{
"base_reth_node": {
"tag": "v0.7.0",
"commit": "2b0d89d4267ae7b2893e1719d2ba026074e4a8b8",
"owner": "base",
"repo": "base",
"tracking": "release"
},
"op_geth": {
"tag": "v1.101702.0",
"commit": "d0734fd5f44234cde3b0a7c4beb1256fc6feedef",
"owner": "ethereum-optimism",
"repo": "op-geth",
"tracking": "release"
}
}
每个条目记录了 tag(要克隆的 Git tag)、commit(该 tag 对应的预期 SHA)以及 tracking 模式(控制自动更新器如何发现新版本)。我们将在第四篇中深入探讨更新器。
关键在于 Docker 构建期间发生的两步验证。看看 Geth Dockerfile 第 9-11 行的克隆步骤:
RUN . /tmp/versions.env && git clone $OP_GETH_REPO --branch $OP_GETH_TAG --single-branch . && \
git switch -c branch-$OP_GETH_TAG && \
bash -c '[ "$(git rev-parse HEAD)" = "$OP_GETH_COMMIT" ]'
首先按指定 tag 克隆,然后验证 HEAD 是否与预期的 commit hash 匹配。如果上游维护者将某个 tag 指向了另一个 commit(即"tag 篡改攻击"),构建会立即失败。这是在 Docker 层面实现的供应链安全保障。
flowchart LR
VJ[versions.json] -->|"dependency_updater"| VE[versions.env]
VE -->|"COPY into build"| DF[Dockerfile]
DF -->|"1. git clone at tag"| REPO[Upstream Repo]
DF -->|"2. verify commit hash"| CHECK{SHA matches?}
CHECK -->|Yes| BUILD[Build binary]
CHECK -->|No| FAIL[Build fails]
versions.env 是一个自动生成的文件——包含可直接 source 的 shell 导出变量,供 Dockerfile 在构建时使用。每个依赖项按统一的命名规则生成三个变量:
export OP_GETH_TAG=v1.101702.0
export OP_GETH_COMMIT=d0734fd5f44234cde3b0a7c4beb1256fc6feedef
export OP_GETH_REPO=https://github.com/ethereum-optimism/op-geth.git
规则是将依赖键名转为大写(op_geth → OP_GETH),再分别拼接 _TAG、_COMMIT、_REPO 后缀。这个文件已提交到仓库,并在 versions.json 发生变更时自动重新生成。
各组件如何协同
了解了各个独立组件后,让我们来看它们端到端是如何串联在一起的:
flowchart TD
USER["Operator runs:<br/>CLIENT=reth docker compose up"] --> DC[docker-compose.yml]
DC -->|"builds using"| RDF[reth/Dockerfile]
RDF -->|"reads versions from"| VE[versions.env]
VE -->|"generated from"| VJ[versions.json]
RDF -->|"clones & builds"| RETH[base-reth-node binary]
RDF -->|"clones & builds"| BC[base-consensus binary]
RDF -->|"clones & builds"| OPN[op-node binary]
DC -->|"starts execution service"| EX["reth-entrypoint<br/>→ base-reth-node"]
DC -->|"starts node service"| ND["consensus-entrypoint<br/>→ base-consensus"]
ND -->|"Engine API + JWT"| EX
整个流程是:用户选择客户端 → Docker Compose 触发对应的 Dockerfile → Dockerfile source versions.env 获取上游仓库和 commit 信息 → 从源码构建二进制文件 → 入口脚本处理运行时配置和服务间协调。
下一步
我们已经搭建好了架构骨架——两个服务、三层配置以及版本锁定的构建流水线。但骨架本身并不能告诉你容器实际启动时会发生什么。启动序列涉及轮询循环、公网 IP 探测、JWT secret 写入,而 Reth 的情况更为特殊,需要一个可能长达六小时的多阶段初始化流程。
第二篇将逐步追踪从 docker compose up 到节点完全同步的每一个环节,包括共识分发器模式以及 Reth 历史证明初始化的子流程。