Read OSS

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 エントリーポイント:gethclefevmdevp2pabigenrlpdump など
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 インターフェースは KeyValueStoreAncientStore を合成しており、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 gethmake allmake 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 で一貫した動作が保証されます。

日々のコードリーディングでは、次のヒューリスティクスを参考にしてください。

  1. 型定義は core/types/ — Block、Transaction、Receipt、Header、Log
  2. 設定は params/ — フォークスケジュール、ガスコスト、チェーン ID
  3. RPC ハンドラは internal/ethapi/ — すべての eth_* メソッドがここの Go メソッドに対応している
  4. EVM は core/vm/ — オペコード、ガステーブル、インタープリタループ
  5. 状態管理は core/state/trie/triedb/ethdb/ — 4層の階層構造

地図が手に入ったところで、次の記事では main() から動作中のノードに至るまでの道のりをたどります。CLI パース・Node 構築・サービス初期化を経たブートシーケンスを、順を追って追いかけていきましょう。