go-ethereum を読み解く:アーキテクチャ概観とディレクトリマップ
前提知識
- ›Go 言語の基礎知識(インターフェース、パッケージ、struct の埋め込み)
- ›Ethereum の基本概念(ブロック、トランザクション、アカウント、EVM)
go-ethereum を読み解く:アーキテクチャ概観とディレクトリマップ
Ethereum の実行レイヤーには、Go で書かれたリファレンス実装があります。go-ethereum リポジトリ — 通称「Geth」— は誕生から10年以上が経過し、コード量はおよそ100万行に達する、最も広く使われている Ethereum クライアントです。Ethereum が実装レベルでどのように動作しているかを理解したいなら、このコードベースを読むのが一番の近道です。とはいえ、地図なしで100万行のプロジェクトへ飛び込むのは、迷子になるようなものです。この記事では、そのための地図を提供します。
Geth とは何か・何でないかから始まり、ディレクトリ構造がどのように関心事を整理しているか、ライブラリとアプリケーションコードの分離について順を追って解説します。大規模なコードベースを扱いやすく保つインターフェース駆動の設計思想も取り上げます。記事を読み終える頃には、どのサブシステムを調べたいときにどこを見ればいいかが、はっきりとわかるようになっているはずです。
Geth とは何か、どこに位置づけられるのか
Geth は、Ethereum の実行レイヤープロトコルの公式 Go 実装です。The Merge(2022年9月)以降、Ethereum は2クライアント構成で動作しています。Prysm・Lighthouse・Teku といったコンセンサスレイヤークライアントがプルーフ・オブ・ステークのコンセンサスを担い、Geth のような実行レイヤークライアントがトランザクション実行、状態管理、EVM を担当します。
flowchart TD
CL["Consensus Layer Client<br/>(Prysm, Lighthouse, etc.)"]
EL["Execution Layer Client<br/>(Geth)"]
CL -->|Engine API| EL
EL -->|State, Blocks, Receipts| DB[(LevelDB / Pebble)]
EL <-->|devp2p| PEERS["Peer Nodes"]
CL <-->|libp2p| CL_PEERS["CL Peers"]
「どのブロックをいつ生成するか」はコンセンサスレイヤーが決め、「どのように生成するか」を Geth が処理します。この通信は Engine API — 認証付きの JSON-RPC エンドポイント群 — を通じて行われます。詳細はパート6で扱います。
重要なのは、go-ethereum が実行可能なクライアント(geth バイナリ)であると同時に、再利用可能な Go ライブラリでもある点です。外部プロジェクトが github.com/ethereum/go-ethereum をインポートして、型定義・RLP エンコーディング・暗号処理を利用したり、ブロックチェーン機能をまるごと組み込んだりするケースは珍しくありません。go.mod のモジュール宣言を見ると、これが Go 1.24 をターゲットとした単一の Go モジュールであることがわかります。
ディレクトリ構造:パッケージ完全マップ
リポジトリは Go の慣習に忠実に従っており、すべてのディレクトリがパッケージとなっています。依存関係は上位の アプリケーションコードから下位のプリミティブへと、一方向に流れます。完全なマップは以下のとおりです。
| ディレクトリ | ドメイン | 説明 |
|---|---|---|
cmd/ |
Application | CLI エントリーポイント:geth、clef、evm、devp2p、abigen、rlpdump など |
node/ |
Infrastructure | プロトコル非依存のサービスコンテナ — P2P・RPC・データベース・ライフサイクルを管理 |
eth/ |
Protocol | Ethereum プロトコルサービス — ブロックチェーン・tx pool・ハンドラ・マイナー・API を束ねる |
core/ |
Blockchain | ブロック処理、状態遷移、ジェネシス、tx pool、型定義 |
core/vm/ |
Execution | EVM 実装 — インタープリタ、ジャンプテーブル、オペコード、プリコンパイル |
core/state/ |
State | StateDB — ジャーナルベースのスナップショットを持つインメモリのワールドステートキャッシュ |
core/types/ |
Data | 標準型定義:Block、Transaction、Receipt、Header、Log |
core/txpool/ |
Mempool | SubPool インターフェースによるトランザクションプールアグリゲータ |
consensus/ |
Consensus | プラガブルなコンセンサスエンジン(beacon、clique、ethash) |
p2p/ |
Networking | devp2p スタック — 暗号化接続、ピア管理、ディスカバリ |
trie/ |
Data Structure | Merkle Patricia Trie 実装 |
triedb/ |
Trie Storage | ハッシュベースおよびパスベースのバックエンドを持つ Trie データベース |
ethdb/ |
Storage | データベースインターフェース抽象化 — LevelDB または Pebble がバックエンド |
rpc/ |
API Framework | リフレクションベースのメソッド登録を持つ JSON-RPC サーバー |
internal/ethapi/ |
API Handlers | eth_*・debug_*・txpool_* RPC メソッドの実装 |
accounts/ |
Key Management | アカウント管理、キーストア、ハードウェアウォレットサポート |
params/ |
Configuration | チェーン設定、フォークスケジュール、ガスコスト、ネットワーク定義 |
miner/ |
Block Building | Engine API 向けの Post-Merge ペイロードビルダー |
crypto/ |
Cryptography | secp256k1、SHA3、BLS、KZG サポート |
rlp/ |
Serialization | Recursive Length Prefix のエンコード/デコード |
common/ |
Utilities | 共通型(Hash、Address)、数値ヘルパー、キャッシング |
log/ |
Logging | 構造化ロギングフレームワーク |
metrics/ |
Observability | メトリクス収集とレポーティング |
event/ |
Pub/Sub | 内部イベントサブスクリプションシステム |
flowchart TD
CMD["cmd/geth"] --> ETH["eth/"]
CMD --> NODE["node/"]
ETH --> CORE["core/"]
ETH --> CONSENSUS["consensus/"]
ETH --> MINER["miner/"]
CORE --> VM["core/vm/"]
CORE --> STATE["core/state/"]
CORE --> TYPES["core/types/"]
CORE --> TXPOOL["core/txpool/"]
STATE --> TRIE["trie/"]
TRIE --> TRIEDB["triedb/"]
TRIEDB --> ETHDB["ethdb/"]
NODE --> P2P["p2p/"]
NODE --> RPC["rpc/"]
ETH --> ETHAPI["internal/ethapi/"]
ヒント: 未知のサブシステムを探索するときは、
cmd/レイヤーを起点に下へたどっていきましょう。依存関係の流れはcmd/ → eth/ → core/ → trie/ → ethdb/と一方向に決まっており、下位パッケージが上位パッケージをインポートすることはありません。
ライブラリとアプリケーションの分離:cmd/ の役割
Geth のアーキテクチャ上の重要な決断のひとつが、cmd/ に置かれたアプリケーションコードと、それ以外すべてのライブラリコードの明確な分離です。Makefile を見ると、生成される実行ファイルの全貌が把握できます。
flowchart LR
subgraph "cmd/ — Application Layer"
GETH["geth<br/>Full node client"]
EVM["evm<br/>Standalone EVM"]
DEVP2P["devp2p<br/>Protocol testing"]
CLEF["clef<br/>External signer"]
ABIGEN["abigen<br/>ABI bindings"]
RLPDUMP["rlpdump<br/>RLP inspector"]
end
subgraph "Library Packages"
LIB["eth/, core/, p2p/, trie/,<br/>rpc/, consensus/, ..."]
end
GETH --> LIB
EVM --> LIB
DEVP2P --> LIB
この分離があるおかげで、サードパーティの Go プロジェクトは CLI まわりのコードを引き込まずに import "github.com/ethereum/go-ethereum" してライブラリパッケージを利用できます。たとえば ethclient パッケージは、ルートレベルのインターフェースを実装した型付きの Go クライアントを提供しますが、これはライブラリの境界が厳格に守られているからこそ成り立っています。
Geth のメインバイナリは cmd/geth/main.go で定義されており、main() はわずか5行で app.Run(os.Args) を呼び出すだけです。実際の処理はすべてライブラリパッケージの中にあります。
インターフェース駆動の設計思想
100万行のコードベースが管理不能なモノリスに陥らないのはなぜか — その答えはインターフェースにあります。go-ethereum は一貫したパターンを採用しています。パッケージの境界に狭いインターフェースを定義し、具体的な型でそれを実装し、パッケージをまたいで具体的な型に依存しない、というパターンです。
主要な抽象化の境界を以下に示します。
classDiagram
class Lifecycle {
<<interface>>
+Start() error
+Stop() error
}
class Engine {
<<interface>>
+Author(header) address
+VerifyHeader(chain, header) error
+VerifyHeaders(chain, headers) chan
+Prepare(chain, header) error
+Finalize(chain, header, state, body)
+Seal(chain, block, results, stop) error
}
class Database {
<<interface>>
KeyValueStore
AncientStore
}
class SubPool {
<<interface>>
+Filter(tx) bool
+Init(gasTip, head, reserver) error
+Add(txs, sync) errors
+Pending(filter) map
}
class Backend {
<<interface>>
+HeaderByNumber(ctx, number)
+StateAndHeaderByNumber(ctx, number)
+SendTx(ctx, tx) error
+ChainConfig() ChainConfig
}
Lifecycle インターフェースはその中でも特に洗練されています — Start() と Stop() のわずか2メソッドだけです。起動・停止の管理が必要なサービスはこの2つのメソッドを実装して Node コンテナに登録するだけでよく、Ethereum サービスやローカルトランザクショントラッカーなど、さまざまなコンポーネントがこの最小限のコントラクトを共有しています。
consensus.Engine インターフェースにより、同一のコア実行パイプラインがプルーフ・オブ・ステーク(beacon)、プルーフ・オブ・オーソリティ(clique)、レガシーなプルーフ・オブ・ワーク(ethash)のいずれでも動作します。ただし、現在の Geth はすべてのネットワークが The Merge を通過済みであることを前提としています。
ethdb.Database インターフェースは KeyValueStore と AncientStore を合成しており、LevelDB・Pebble・インメモリバックエンドをシームレスに切り替えられます。これはテスト時に特に重要です。
ルートレベルの公開 API
リポジトリのルートには interfaces.go があり、外部利用者向けの安定した公開 Go API を定義しています。これが ethereum パッケージ — ethclient が実装するパッケージ — です。
ここで定義されているインターフェースには以下のものがあります。
ChainReader— ハッシュまたはブロック番号でブロックやヘッダーにアクセスするTransactionReader— 過去のトランザクションとレシートを取得するChainStateReader— 残高・ストレージ・コード・nonce を照会するContractCaller— 読み取り専用のコントラクト呼び出しを実行するLogFilterer— イベントログを照会・購読するTransactionSender— 署名済みトランザクションを送信するGasPricer/GasPricer1559— ガス価格の推奨値を提供するSubscription— イベントサブスクリプションの共通コントラクト
これらのインターフェースは意図的に狭く、安定した状態に保たれています。go-ethereum をインポートする外部 Go プロジェクトに対して Geth が維持する公開 API コントラクトそのものです。ここに破壊的変更が入れば、下流のすべてのプロジェクトに影響が及びます。
ヒント: Ethereum とやり取りする Go アプリケーションを構築するときは、Geth の具体的な型ではなく
interfaces.goのインターフェースに対してプログラミングしましょう。こうすることで、たとえばテスト時にethclientをモックに差し替えるといった柔軟な対応が可能になります。
ビルドシステムとコードベースの読み方
Geth のビルドシステムは2層構成です。Makefile が開発者向けのインターフェース(make geth・make all・make test)を提供し、その裏では多くのターゲットが build/ci.go に処理を委譲しています。build/ci.go はクロスコンパイル・テスト・パッケージング・CI タスクを担う Go 製のビルドオーケストレータです。
flowchart LR
DEV["Developer"] -->|make geth| MK["Makefile"]
MK -->|go run build/ci.go install| CI["build/ci.go"]
CI -->|go build| BIN["build/bin/geth"]
DEV -->|make test| MK
MK -->|go run build/ci.go test| CI
CI -->|go test| TESTS["Test Suite"]
Go プログラムをビルドオーケストレータとして使うこのパターンにより、シェルスクリプトの方言に依存することなく、Linux・macOS・Windows で一貫した動作が保証されます。
日々のコードリーディングでは、次のヒューリスティクスを参考にしてください。
- 型定義は
core/types/— Block、Transaction、Receipt、Header、Log - 設定は
params/— フォークスケジュール、ガスコスト、チェーン ID - RPC ハンドラは
internal/ethapi/— すべてのeth_*メソッドがここの Go メソッドに対応している - EVM は
core/vm/— オペコード、ガステーブル、インタープリタループ - 状態管理は
core/state/→trie/→triedb/→ethdb/— 4層の階層構造
地図が手に入ったところで、次の記事では main() から動作中のノードに至るまでの道のりをたどります。CLI パース・Node 構築・サービス初期化を経たブートシーケンスを、順を追って追いかけていきましょう。