Read OSS

実践入門:Gethのビルド・テスト・コントリビューション

中級

前提知識

  • シリーズの全過去記事
  • Go開発環境のセットアップ
  • Gitの基礎知識

実践入門:Gethのビルド・テスト・コントリビューション

これまでの6本の記事を通じて、go-ethereumの主要なサブシステムをひとつひとつ追ってきました。CLIの起動シーケンスに始まり、ブロック実行、ステートの保存、トランザクションプール、P2Pネットワーク、RPCレイヤーまで全体像を把握できたことと思います。最終回となるこの記事では、より実践的な内容に踏み込みます。プロジェクトのビルド方法、多層構造のテストスイートの実行、コード生成のパターン、新しいハードフォークの実装方法を解説します。バグ修正や機能追加、特定の挙動を深く理解したい場合など、このコードベースを効率よく扱うための道具が揃います。

ビルドシステム:Makefileとbuild/ci.go

Part 1で軽く触れたとおり、Gethは二段構えのビルドシステムを採用しています。開発者が直接操作するのはMakefileです。

make geth        # Build just the geth binary
make evm         # Build the standalone EVM tool
make all         # Build all packages and executables
make test        # Run tests (builds first)
make lint        # Run linters
make fmt         # Format all Go code
make devtools    # Install code generation tools

すべてのmakeターゲットは、最終的にgo run build/ci.go <command>を呼び出します。build/ci.go//go:build noneを持つGoプログラムで、モジュールの一部としてコンパイルされることはなく、スクリプトとして実行されます。主な機能は次のとおりです。

  • install — アーキテクチャとコンパイラを選択したクロスプラットフォームコンパイル
  • test — カバレッジ計測付きのテスト実行
  • lint — 事前設定されたリンター構成
  • check_generate — 生成コードが最新であることを検証
  • check_baddeps — 禁止された依存関係の混入を防止
  • archive / debsrc / nsis — 配布用パッケージの作成
flowchart TD
    DEV["Developer"] -->|make geth| MAKE["Makefile"]
    MAKE --> CI["go run build/ci.go install ./cmd/geth"]
    CI --> BUILD["go build -o build/bin/geth ./cmd/geth"]
    BUILD --> BIN["build/bin/geth"]

    DEV -->|make test| MAKE
    MAKE --> CI2["go run build/ci.go test"]
    CI2 --> TEST["go test ./..."]

    DEV -->|make devtools| MAKE
    MAKE --> TOOLS["Install stringer, gencodec,<br/>protoc-gen-go, abigen"]

Tip: 日常の開発ではmake gethだけで十分です。バイナリは./build/bin/gethに生成されます。CIやリリースビルドでは、build/ci.goスクリプトがクロスコンパイル、署名、パッケージングまで一手に引き受けます。

テスト戦略:ユニット・インテグレーション・リファレンステスト

Gethは多層的なテスト戦略を採用しています。AGENTS.mdファイルには、推奨されるワークフローが明文化されています。

flowchart TD
    subgraph "During Development"
        SHORT["go run ./build/ci.go test -short<br/>Fast feedback, skips slow tests"]
    end
    subgraph "Before Commit"
        FULL["go run ./build/ci.go test<br/>Full suite including reference tests"]
        LINT["go run ./build/ci.go lint<br/>Style checks"]
        GEN["go run ./build/ci.go check_generate<br/>Generated code up-to-date"]
        DEPS["go run ./build/ci.go check_baddeps<br/>Dependency hygiene"]
    end
    SHORT -->|iterate| SHORT
    SHORT -->|ready to commit| FULL
    FULL --> LINT
    LINT --> GEN
    GEN --> DEPS

テストの各層は次のとおりです。

  1. ユニットテスト — テスト対象のコードと同じ場所に置かれた標準的なGo _test.goファイルです。ほとんどのパッケージには充実したユニットテストが揃っています。特定のパッケージだけをテストするにはgo test ./core/vm/...を使いましょう。

  2. インテグレーションテスト — 複数のパッケージを組み合わせて動作確認するテストで、インメモリデータベースバックエンドを使うことが多いです。eth/パッケージには、シミュレーションされたピアと組み合わせてハンドラー全体を検証するテストが含まれています。

  3. Ethereumリファレンステストtests/ディレクトリには、Ethereum実行仕様の公式テストスイートが含まれています。GethのEVMがすべてのフォークにわたってリファレンス仕様と完全に一致した結果を返すかを検証します。ステート遷移、ブロック処理、トランザクション検証、RLPエンコーディングなどを網羅しています。

  4. cmd/evmツール — フルノードを起動せずに、ステートテストの実行、トランザクションのトレース、opcodeのベンチマークを単独で行える独立したEVMです。EVMに関する問題のデバッグに欠かせないツールです。

core/vm/runtime/パッケージは、EVMを隔離して実行するためのテスト用ランタイムを提供しています。合成ステートでEVMを生成し、任意のバイトコードを実行して結果を確認できます。多くの内部テストがこのパターンを採用しています。

// Example pattern from core/vm tests
result, _, err := runtime.Execute(code, input, &runtime.Config{
    GasLimit: 1000000,
    // ... configuration
})

コード生成のパターン

Gethは主に3つの目的でgo:generateディレクティブを使用しています。

  1. gencodec — 型安全なJSONマーシャリングコードを生成します。core/types/の多くの型はgencodecを使ってgen_*.goファイルを生成し、ランタイムリフレクションを使わずにJSONエンコーディングを処理します。ethconfig/config.goにある次のディレクティブが典型的な例です。
//go:generate go run github.com/fjl/gencodec -type Config -formats toml -out gen_config.go
  1. stringer — enum型にString()メソッドを生成します。make devtoolsターゲットでこのツールをインストールできます。

  2. protoc-gen-go — proto定義された型のProtocol Bufferコードを生成します。

ビルドシステムのcheck_generateコマンドは、すべての生成ファイルが最新であることを確認します。go:generateディレクティブを持つ型を変更した場合は、次の手順が必要です。

make devtools          # Install generators (first time only)
go generate ./...      # Regenerate all files
flowchart LR
    SOURCE["Source type<br/>(e.g., Config struct)"] -->|go:generate directive| GENCODEC["gencodec"]
    GENCODEC --> GENERATED["gen_config.go<br/>Type-safe marshaling"]
    SOURCE2["Enum type<br/>(e.g., SyncMode)"] -->|go:generate directive| STRINGER["stringer"]
    STRINGER --> GENERATED2["syncmode_string.go<br/>String() method"]

補助ツールと実行ファイル

geth本体に加えて、cmd/ディレクトリにはさまざまな便利なツールが含まれています。

ツール 用途
cmd/evm 独立したEVM — ステートテストの実行、トランザクションのトレース、ベンチマーク
cmd/devp2p P2Pプロトコルテスト — ENR操作、Discoveryクローリング、プロトコルテスト
cmd/clef 外部署名者 — Gethプロセスの外でキーを管理
cmd/abigen ABIバインディング生成 — コントラクトの型安全なGoラッパーを生成
cmd/rlpdump RLPインスペクター — RLPエンコードされたデータをデコードして表示
cmd/era Era1アーカイブツール — era1アーカイブファイルの操作
cmd/blsync Beaconライトクライアント同期 — 軽量なCL同期

devp2pツールはネットワークのデバッグに特に役立ちます。Discoveryネットワークのクローリング、プロトコルハンドシェイクのテスト、ENRレコードの検証が行えます。P2Pのコードを扱うなら、このツールは欠かせない相棒になるでしょう。

ハードフォークの実装方法

新しいEthereumハードフォークを実装するための確立されたパターンは、Gethのアーキテクチャを理解するうえで最も参考になる例のひとつです。これまで取り上げてきたほぼすべてのサブシステムに関わります。実装の手順を見ていきましょう。

flowchart TD
    A["1. Add activation field to ChainConfig<br/>(params/config.go)"] --> B["2. Add Rules flag<br/>(params/config.go → Rules struct)"]
    B --> C["3. Create new instruction set<br/>(core/vm/jump_table.go)"]
    C --> D["4. Implement EIP enable functions<br/>(core/vm/eips.go)"]
    D --> E["5. Add fork-specific logic<br/>(core/state_processor.go,<br/>consensus/, etc.)"]
    E --> F["6. Update jump table selection<br/>(core/vm/evm.go → NewEVM)"]
    F --> G["7. Update reference tests<br/>(tests/)"]
    G --> H["8. Add override flag<br/>(cmd/geth/main.go)"]

Step 1: ChainConfigに時刻ベースのアクティベーションフィールドを追加します。Merge以降のフォークは*uint64のタイムスタンプを使用します(例:OsakaTime *uint64AmsterdamTime *uint64)。Merge以前のフォークはブロック番号を使っていました。

Step 2: Rules構造体にbooleanフラグを追加します(例:IsOsaka boolIsAmsterdam bool)。Rulesは特定のブロック番号とタイムスタンプにおいてChainConfigから算出されます。

Step 3: jump_table.goに新しい命令セットのコンストラクターを作成します。前のフォークのテーブルをコピーし、新しいopcodeを追加します。

func newAmsterdamInstructionSet() JumpTable {
    instructionSet := newOsakaInstructionSet()
    enable7843(&instructionSet) // SLOTNUM opcode
    enable8024(&instructionSet) // SWAPN, DUPN, EXCHANGE
    return validate(instructionSet)
}

Step 4: ジャンプテーブルの特定エントリーを変更するenable*関数を実装します。新規または変更されたopcodeの実行関数、ガスコスト、スタックパラメーターを設定します。

Step 5: EVM以外のフォークロジックを追加します。新しいシステムコントラクト、変更されたステート遷移ルール、コンセンサスの変更、新しいトランザクションタイプなどが該当します。

Step 6: NewEVM()switch文に新しいフォークを追加します。最新のフォークが常に最初にチェックされます。

Step 7: 新しいフォークの期待動作をリファレンステストスイートに追加します。

Step 8: メインネットのアクティベーション前にフォークをテストするための--override.<forkname> CLIフラグを追加します。

Tip: リポジトリルートにあるAGENTS.mdファイルには、コントリビューターガイドラインがまとめられています。コミットメッセージのフォーマット(<package>: description)、コミット前のチェックリスト、プルリクエストのタイトル規則などが記載されています。初めてPRを出す前に必ず目を通しておきましょう。

さらに深く探求するためのヒント

7本の記事を通じて、Gethの全体像をしっかりと把握できたはずです。最後に、コードベースを探索するうえで役立つヒントをまとめておきます。

  1. インターフェースを追いましょう。 インターフェース上のメソッド呼び出しに出会ったら、それを満たす構造体を探して具体的な実装を確認しましょう。Goの暗黙的なインターフェース実装により、IDEの「実装を探す」機能よりもgrepの方が効果的なことがよくあります。

  2. eth/backend.goから始めましょう。 Ethereum構造体とNew()コンストラクターはロゼッタストーンです。すべての主要なサブシステムがここで生成・接続されます。2つのコンポーネントがどのように連携しているか迷ったときは、eth.New()を確認しましょう。

  3. rawdbパッケージをデータベースの辞書として使いましょう。 データベースに何が保存されているか、キーがどう構造化されているかを知りたいときは、core/rawdb/を参照しましょう。

  4. paramsパッケージがプロトコル定数の信頼できる情報源です。 ガスコスト、フォークのアクティベーションロジック、チェーンID、プリコンパイルアドレスなど、すべてがparams/に集約されています。

  5. テストはドキュメントでもあります。 コードのコメントで挙動が説明されていない場合、テストが答えを持っていることがよくあります。同じパッケージ内の_test.goファイルを確認してみましょう。

  6. コミットメッセージは変更履歴でもあります。 Gethのコミット履歴は厳格な<package>: descriptionフォーマットに従っています。git log --oneline -- core/vm/を使えば、任意のサブシステムの変遷を追えます。

go-ethereumのコードベースは、丁寧に読めば読むほど多くのことを教えてくれます。規模と歴史を考えると驚くほど整然と構造化されており、インターフェース駆動の設計によって各サブシステムを独立して理解できます。地図は手に入りました。あとは探索あるのみです。