3つのクライアント、1つのインターフェース:Base スタックにおける Reth vs. Geth vs. Nethermind
前提知識
- ›第1〜2回:アーキテクチャとスタートアップシーケンス
- ›ブロックチェーン実行クライアントの基本的な知識
- ›マルチステージ Docker ビルドの理解
3つのクライアント、1つのインターフェース:Base スタックにおける Reth vs. Geth vs. Nethermind
クライアントの多様性は、ブロックチェーンネットワークにとって生存戦略のひとつです。ある実装に深刻なバグが発生しても、別のクライアントを動かしているノードがネットワークを支え続けられます。Base は Reth(Rust)、Geth(Go)、Nethermind(.NET)という3つの実行クライアントをサポートしており、base/node リポジトリでは各クライアントに対して同じインターフェースに準拠した Dockerfile とエントリーポイントスクリプトが用意されています。しかし、その均一なインターフェースの裏では、ビルドの複雑さ・機能の幅・そして何より重要なコンセンサスクライアントとの互換性において、3つのクライアントは大きく異なります。
この記事では、各クライアントの 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 のビルドは他の2つと比べてかなり重くなっています。アーキテクチャごとに SHA256 を検証しながら mold リンカーをインストールし、Cargo の maxperf プロファイルでビルドします。このプロファイルは積極的な LTO(Link Time Optimization)をはじめとするパフォーマンス重視のコンパイラフラグを有効にします。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 におけるサプライチェーンの検証
3つの Dockerfile はすべて、第1回で紹介したのと同じサプライチェーンセキュリティのパターンを実装しています。ピン留めされたタグでクローンし、コミットハッシュを検証するというパターンです。各クライアントの実装を詳しく比べてみましょう。
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 は --branch ではなく git checkout tags/ を使い、--single-branch なしでリポジトリの全履歴をクローンします。一方、Geth と Nethermind は --single-branch を使ってクローンを高速化しています。また Reth は検証失敗時に明示的なエラーメッセージ("Commit hash verification failed")を出力しますが、他の2つはゼロ以外の終了コードで黙って失敗します。
もうひとつ興味深い点として、3つすべてが Optimism モノリポから op-node もビルドしています。ただし Geth の Dockerfile は make(14行目)を使うのに対し、Nethermind は just(14行目)を使っています。どちらも同じビルドターゲットを呼び出しており、これは時間をかけて統一されていく可能性が高い不整合です。
Reth:機能豊富なパス
reth/reth-entrypoint の Reth エントリーポイントは3つの中でもっとも機能が充実しており、他のクライアントにはない機能を複数備えています。
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 のような引数を渡すことで、ディスク容量と過去データへのクエリ能力のトレードオフを調整できます。
Historical proofs(第2回で取り上げたマルチステージ初期化)は Reth 独自の機能で、L1 検証のための過去の状態証明を提供できるようにします。
133〜158行目の最終的な exec コマンドでは、web3、debug、eth、net、txpool、miner を含む幅広い API が HTTP と WebSocket の両方で公開されます。
Geth:実績あるデフォルト
.env でのデフォルトクライアント(CLIENT=geth)でありながら、geth/geth-entrypoint のエントリーポイントはメモリ管理に焦点を当てた独自のチューニングパラメータを提供しています。
5つのキャッシュパラメータを詳しく見てみましょう。
| 変数 | デフォルト値 | 意味 |
|---|---|---|
GETH_CACHE |
20480(MB) | キャッシュプール全体のサイズ |
GETH_CACHE_DATABASE |
20(%) | LevelDB の読み書きキャッシュ |
GETH_CACHE_GC |
12(%) | ガベージコレクション用キャッシュ |
GETH_CACHE_SNAPSHOT |
24(%) | スナップショット生成キャッシュ |
GETH_CACHE_TRIE |
44(%) | トライノードのインメモリキャッシュ |
これらのパーセンテージはキャッシュプール全体の100%になるように配分されています。トライへの大きな割り当て(44% = デフォルト設定で約9GB)は、L2 の読み取り中心のステートアクセスパターンを反映しています。実行コストの大部分をトライのトラバーサルが占めるためです。
Geth には他のクライアントにはない条件付き機能もあります。
- 非保護トランザクション(
OP_GETH_ALLOW_UNPROTECTED_TXS)— レガシーなトランザクション形式への対応に有用 - ステートスキームの選択(
OP_GETH_STATE_SCHEME)— ハッシュベースとパスベースのステートストレージを切り替え可能 - ロールアップの停止(
--rollup.halt=major、78行目)— メジャーバージョンの非互換が発生した際にノードを自動停止
Nethermind:シンプルなパス
nethermind/nethermind-entrypoint の Nethermind エントリーポイントは3つの中でもっともシンプルです。その強みは Nethermind に組み込まれた設定システムにあります。
exec ./nethermind \
--config="$OP_NODE_NETWORK" \
47行目の --config フラグは組み込みのネットワークプロファイル(例:base-mainnet や base-sepolia)を指定します。このフラグ1つで、Geth や Reth では個別に渡す必要がある数十ものチェーン固有パラメータをまとめて設定できます。
オプションとして追加できるのは、bootnodes の設定と ethstats によるモニタリング(33〜43行目)のみです。また Nethermind はデフォルトでヘルスチェック(58行目の --HealthChecks.Enabled=true)を有効にする点も特徴的です。
Dockerfile では、プレーンな Ubuntu ではなく .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
.NET は amd64 ではなく x64 というアーキテクチャ名を使うため、Docker の TARGETARCH(amd64/arm64)を .NET のアーキテクチャ名(x64/arm64)に変換しています。
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 ./
...
# No base-consensus binary or entrypoint
base-consensus バイナリをビルドしてバンドルするのは Reth の Dockerfile だけです。これがデフォルト設定と微妙な矛盾を生み出します。
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 イメージを必要とします。デフォルトのままで使うと、base-consensus-entrypoint が Geth イメージに存在しないため、コンセンサスエントリーポイントのディスパッチャが失敗します。
実際には、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 | ✅ | ❌ | ❌ |
| Historical proofs | ✅ | ❌ | ❌ |
| プルーニング設定 | ✅(細粒度) | ✅(gcmode) | 組み込み |
| キャッシュチューニング | デフォルト | ✅(5パラメータ) | デフォルト |
| Snap sync | ❌ | ✅(実験的) | ✅(bootnodes 経由) |
| Ethstats | ❌ | ✅ | ✅ |
| 組み込みネットワーク設定 | ❌ | ❌ | ✅ |
| ヘルスチェック | ❌ | ❌ | ✅ |
| ログレベルの変換 | ✅(名前 → -v フラグ) | 直接指定(verbosity の整数値) | 直接指定(名前) |
| ビルド時間 | 約30〜60分 | 約5〜10分 | 約5〜10分 |
全体像は明確です。Reth は Base の最新機能(base-consensus、Flashblocks、historical proofs)に排他的にアクセスできる、機能豊富なパスです。Geth はきめ細かいキャッシュチューニングを備えた実績あるワーカーホースです。Nethermind は組み込み設定に頼ることで、メンテナンスコストを最小限に抑えた選択肢です。
次回予告
ここまで、versions.json のバージョン情報が Dockerfile にピン留めされた依存関係としてどう流れ込むかを見てきました。では versions.json を更新するのは誰で、どのようにダウングレードを防ぎ、4つの異なるバージョンフォーマットを扱い、プルリクエストを自動生成しているのでしょうか。第4回では dependency_updater を解剖します。これは4つのアップストリーム依存関係を最新に保ちながらサプライチェーンセキュリティを強制するという、見かけよりもずっと複雑な問題を解く Go 製 CLI ツールです。