三种客户端,一个接口:Base 技术栈中的 Reth vs. Geth vs. Nethermind
前置知识
- ›第 1-2 篇:架构与启动流程
- ›具备区块链执行客户端的基本知识
- ›了解多阶段 Docker 构建
三种客户端,一个接口:Base 技术栈中的 Reth vs. Geth vs. Nethermind
客户端多样性是区块链网络的生存策略。一旦某个实现出现严重漏洞,运行其他客户端的节点仍能维持网络正常运转。Base 支持三种执行客户端——Reth(Rust)、Geth(Go)和 Nethermind(.NET)——base/node 仓库为每种客户端提供了遵循相同接口规范的 Dockerfile 和入口脚本。然而,统一的接口背后,三者在构建复杂度、功能范围,尤其是共识客户端兼容性上存在显著差异。
本文将全面对比三种客户端的 Dockerfile 与入口脚本,揭示其中影响性能、安全性和运维灵活性的设计决策。
构建流程对比
每个客户端的 Dockerfile 都采用多阶段构建模式,但各阶段的内容和工具链差异明显。以下是横向对比:
| 维度 | Reth | Geth | Nethermind |
|---|---|---|---|
| 语言 | Rust | Go | .NET (C#) |
| Dockerfile | reth/Dockerfile |
geth/Dockerfile |
nethermind/Dockerfile |
| 构建阶段数 | 4(Go + Rust 基础 + Reth 构建 + 运行时) | 3(Go op-node + Go geth + 运行时) | 3(Go op-node + .NET 构建 + 运行时) |
| 链接器 | mold(含 SHA256 校验) | 系统默认 | 系统默认 |
| 构建配置 | --profile maxperf |
默认(静态构建) | release 配置 |
| 输出二进制文件 | base-reth-node + base-consensus + op-node |
geth + op-node |
nethermind(目录)+ op-node |
| 运行时基础镜像 | ubuntu:24.04 |
ubuntu:24.04 |
mcr.microsoft.com/dotnet/aspnet:10.0-noble |
| 构建耗时 | 约 30-60 分钟(Rust 编译) | 约 5-10 分钟 | 约 5-10 分钟 |
Reth 的构建开销远高于另外两者。它需要安装带有逐架构 SHA256 校验的 mold 链接器,并使用 Cargo 的 maxperf profile 进行编译——该优化级别会开启激进的 LTO(链接时优化)及其他性能关键的编译器选项。这正是 Reth 的 Docker 构建需要 30-60 分钟,而 Geth 和 Nethermind 只需 5-10 分钟的原因。
flowchart TD
subgraph "Reth Build (4 stages)"
R1["Stage 1: golang:1.24<br/>Build op-node"] --> R4
R2["Stage 2: rust-builder-base<br/>Install mold linker + deps"] --> R3
R3["Stage 3: reth-base<br/>cargo build --profile maxperf<br/>→ base-reth-node + base-consensus"] --> R4
R4["Stage 4: ubuntu:24.04<br/>Runtime image"]
end
subgraph "Geth Build (3 stages)"
G1["Stage 1: golang:1.24<br/>Build op-node"] --> G3
G2["Stage 2: golang:1.24<br/>Build geth (static)"] --> G3
G3["Stage 3: ubuntu:24.04<br/>Runtime image"]
end
subgraph "Nethermind Build (3 stages)"
N1["Stage 1: golang:1.24<br/>Build op-node (via just)"] --> N3
N2["Stage 2: dotnet/sdk:10.0<br/>dotnet publish"] --> N3
N3["Stage 3: dotnet/aspnet:10.0<br/>Runtime image"]
end
提示: Reth 的构建代价较高。如果你只是在调整配置,请避免不必要地执行
docker compose build --no-cache,只有在确实变更了版本时才需要完整重建。仅修改入口脚本时,重建会很快,因为 Docker 层缓存会保留已编译的二进制文件。
Dockerfile 中的供应链安全验证
三个 Dockerfile 都实现了第 1 篇中介绍的相同供应链安全模式:锁定 tag 进行克隆,然后验证 commit hash。下面来看各客户端的具体实现。
Reth(第 51-53 行):
RUN . /tmp/versions.env && git clone $BASE_RETH_NODE_REPO . && \
git checkout tags/$BASE_RETH_NODE_TAG && \
bash -c '[ "$(git rev-parse HEAD)" = "$BASE_RETH_NODE_COMMIT" ]' || (echo "Commit hash verification failed" && exit 1)
Geth(第 22-24 行):
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" ]'
Nethermind(第 25-27 行):
RUN . /tmp/versions.env && git clone $NETHERMIND_REPO --branch $NETHERMIND_TAG --single-branch . && \
git switch -c $NETHERMIND_TAG && \
bash -c '[ "$(git rev-parse HEAD)" = "$NETHERMIND_COMMIT" ]'
flowchart LR
A[versions.env] --> B["git clone --branch TAG"]
B --> C["git rev-parse HEAD"]
C --> D{"== expected COMMIT?"}
D -->|Yes| E["Proceed to build"]
D -->|No| F["FAIL: tag-moving attack?"]
三者模式相同,但细节上有所差异。Reth 使用 git checkout tags/ 而非 --branch,且克隆完整的仓库历史(不加 --single-branch)。Geth 和 Nethermind 则通过 --single-branch 加快克隆速度。此外,Reth 在验证失败时会输出明确的错误信息("Commit hash verification failed"),而另外两者仅以非零退出码静默失败。
还有一个有趣的细节:三者都需要从 Optimism monorepo 构建 op-node。但 Geth 的 Dockerfile 使用 make 构建(第 14 行),Nethermind 则使用 just(第 14 行)。两者调用的是相同的构建目标——这处不一致很可能会在未来被统一。
Reth:功能最丰富的路径
Reth 的入口脚本位于 reth/reth-entrypoint,是三者中功能最强大的,提供了其他客户端所不具备的特性。
Flashblocks 支持(第 24-29 行)通过连接 Flashblocks WebSocket 端点,实现亚区块级延迟。设置 RETH_FB_WEBSOCKET_URL 后,节点可在区块最终确认前接收预确认数据:
if [[ -n "${RETH_FB_WEBSOCKET_URL:-}" ]]; then
ADDITIONAL_ARGS="$ADDITIONAL_ARGS --websocket-url=$RETH_FB_WEBSOCKET_URL"
echo "Enabling Flashblocks support with endpoint: $RETH_FB_WEBSOCKET_URL"
fi
裁剪配置(第 70-73 行)支持精细控制需要保留的历史数据范围。RETH_PRUNING_ARGS 变量可接受 --prune.receipts.distance=50000 等参数,让运维人员在磁盘空间与历史查询能力之间灵活权衡。
历史证明——即第 2 篇中介绍的多阶段初始化流程——是 Reth 独有的功能,用于为 L1 验证提供历史状态证明服务。
第 133-158 行的最终 exec 命令通过 HTTP 和 WebSocket 同时暴露了一组完整的 API,包括 web3、debug、eth、net、txpool 和 miner。
Geth:经过验证的默认选择
尽管 Geth 是默认客户端(.env 中 CLIENT=geth),其入口脚本 geth/geth-entrypoint 的设计重心在于内存管理调优,提供了一套不同的配置旋钮。
以下五个缓存参数值得重点关注:
| 变量 | 默认值 | 含义 |
|---|---|---|
GETH_CACHE |
20480(MB) | 缓存池总大小 |
GETH_CACHE_DATABASE |
20(%) | LevelDB 读写缓存 |
GETH_CACHE_GC |
12(%) | 垃圾回收缓存 |
GETH_CACHE_SNAPSHOT |
24(%) | 快照生成缓存 |
GETH_CACHE_TRIE |
44(%) | Trie 节点内存缓存 |
这些百分比加起来恰好等于缓存池的 100%。Trie 缓存占比最高(44%,默认设置下约为 9GB),反映了 L2 读密集型的状态访问模式——Trie 遍历是执行开销的主要来源。
Geth 还支持其他客户端所没有的若干条件特性:
- 非保护交易(通过
OP_GETH_ALLOW_UNPROTECTED_TXS)——适用于遗留交易格式 - 状态存储方案选择(通过
OP_GETH_STATE_SCHEME)——可在哈希型和路径型状态存储之间切换 - Rollup 停机保护(通过
--rollup.halt=major,见第 78 行)——在遇到主版本不兼容时自动停止节点
Nethermind:最简洁的路径
Nethermind 的入口脚本位于 nethermind/nethermind-entrypoint,是三者中最精简的。其优势来自 Nethermind 内置的配置体系:
exec ./nethermind \
--config="$OP_NODE_NETWORK" \
第 47 行的 --config 标志指向内置的网络配置(例如 base-mainnet 或 base-sepolia)。这一个参数就能替代 Geth 和 Reth 需要逐一传入的数十个链相关参数。
可选配置仅有 bootnode 和 ethstats 监控(第 33-43 行)。Nethermind 还默认启用健康检查(第 58 行:--HealthChecks.Enabled=true),这是其他两个客户端所没有的。
其 Dockerfile 使用 .NET 专属的运行时基础镜像(mcr.microsoft.com/dotnet/aspnet:10.0-noble),并通过架构名称映射处理交叉编译(第 29-32 行):
RUN TARGETARCH=${TARGETARCH#linux/} && \
arch=$([ "$TARGETARCH" = "amd64" ] && echo "x64" || echo "$TARGETARCH") && \
dotnet publish src/Nethermind/Nethermind.Runner -c $BUILD_CONFIG -a $arch -o /publish --sc false
这里将 Docker 的 TARGETARCH(amd64/arm64)映射为 .NET 的架构名称(x64/arm64),因为 .NET 使用 x64 而非 amd64。
base-consensus 的不对称性
这可能是整个代码库中最关键的发现,却也最容易被忽视。来看看每个 Dockerfile 在最终镜像中都复制了哪些文件:
| 文件 | Reth | Geth | Nethermind |
|---|---|---|---|
op-node 二进制文件 |
✅ | ✅ | ✅ |
base-consensus 二进制文件 |
✅ | ❌ | ❌ |
base-consensus-entrypoint |
✅ | ❌ | ❌ |
op-node-entrypoint |
✅ | ✅ | ✅ |
consensus-entrypoint |
✅ | ✅ | ✅ |
看看 Reth Dockerfile 的 COPY 指令(第 66-74 行):
COPY --from=op /app/op-node/bin/op-node ./
COPY --from=reth-base /app/target/maxperf/base-consensus ./
COPY --from=reth-base /app/target/maxperf/base-reth-node ./
...
COPY base-consensus-entrypoint .
再对比 Geth(第 37-42 行):
COPY --from=op /app/op-node/bin/op-node ./
COPY --from=geth /app/build/bin/geth ./
...
# 没有 base-consensus 二进制文件或入口脚本
只有 Reth 的 Dockerfile 会构建并打包 base-consensus 二进制文件。这与默认配置之间存在一个隐蔽的冲突:
flowchart TD
ENV[".env defaults"] --> C["CLIENT=geth"]
ENV --> U["USE_BASE_CONSENSUS=true"]
C --> GETH["Geth Dockerfile<br/>No base-consensus binary"]
U --> CE["consensus-entrypoint<br/>routes to base-consensus"]
CE --> FAIL["❌ Exit 1:<br/>Base client is not supported<br/>for this node type"]
style FAIL fill:#ff6666
.env 文件默认将 CLIENT 设为 geth,将 USE_BASE_CONSENSUS 设为 true。但 USE_BASE_CONSENSUS=true 必须配合 Reth 镜像使用。如果用户直接沿用默认配置,共识入口脚本的分发逻辑将会失败,因为 Geth 镜像中根本不存在 base-consensus-entrypoint。
实际上,这个问题由 docker-compose.yml 在第 17 行将 USE_BASE_CONSENSUS 默认为 false 来规避:
environment:
- USE_BASE_CONSENSUS=${USE_BASE_CONSENSUS:-false}
因此,除非显式设置,有效默认值为 false——但 .env 文件确实将其设置为了 true。最终以哪个为准,取决于你所用的 Docker Compose 版本的优先级规则。这类配置边界情况往往会让人调试数小时。
提示: 如果你想运行 base-consensus,请明确指定
CLIENT=reth。如果你想运行 Geth 或 Nethermind,请确保USE_BASE_CONSENSUS=false(或在.env中不设置该变量)。
功能特性总览
| 功能 | Reth | Geth | Nethermind |
|---|---|---|---|
| base-consensus 支持 | ✅ | ❌ | ❌ |
| Flashblocks | ✅ | ❌ | ❌ |
| 历史证明 | ✅ | ❌ | ❌ |
| 裁剪配置 | ✅(精细控制) | ✅(gcmode) | 内置 |
| 缓存调优 | 默认 | ✅(5 个参数) | 默认 |
| Snap sync | ❌ | ✅(实验性) | ✅(通过 bootnode) |
| Ethstats | ❌ | ✅ | ✅ |
| 内置网络配置 | ❌ | ❌ | ✅ |
| 健康检查 | ❌ | ❌ | ✅ |
| 日志级别转换 | ✅(名称 → -v 标志) | 直接(verbosity 整数) | 直接(名称) |
| 构建耗时 | 约 30-60 分钟 | 约 5-10 分钟 | 约 5-10 分钟 |
由此可以得出清晰的结论:Reth 是功能最丰富的路径,独家支持 Base 的最新能力(base-consensus、Flashblocks、历史证明);Geth 是久经考验的主力选择,提供精细的缓存调优能力;Nethermind 则是最省心的方案,依托内置配置即可轻松运行。
下一篇预告
我们已经了解了 versions.json 中的版本信息如何作为锁定依赖流入 Dockerfile。那么,谁来更新 versions.json?它如何防止版本降级、处理四种不同的版本格式,并自动生成 Pull Request?在第 4 篇中,我们将深入剖析 dependency_updater——一个 Go CLI 工具,专门解决在保障供应链安全的前提下,持续追踪四个上游依赖这一出乎意料的复杂问题。