Read OSS

世界に向けてサービスを提供する:JSON-RPC、Engine API、APIアーキテクチャ

中級

前提知識

  • 第1〜2回:アーキテクチャとブートプロセス
  • JSON-RPCの基礎知識
  • Ethereumの実行レイヤー/コンセンサスレイヤー分離の理解

世界に向けてサービスを提供する:JSON-RPC、Engine API、APIアーキテクチャ

dApp、ウォレット、ブロックエクスプローラー、開発ツールのいずれも、JSON-RPCインターフェースを通じてGethとやりとりします。GethはHTTP、WebSocket、IPCの各トランスポートを介して複数のネームスペースにまたがる数千ものRPCメソッドを提供し、さらにコンセンサスレイヤー専用の認証済みエンドポイントも備えています。本記事では、RPCアーキテクチャの全体像を解説します。リフレクションベースの登録フレームワーク、トランスポートレイヤー、APIハンドラを内部実装から切り離すBackend抽象化、ネームスペース登録、そしてEngine APIまでを網羅します。

RPCフレームワーク:リフレクションベースの登録

rpc/ パッケージに実装されたGethのRPCフレームワークは、Goのリフレクションを活用して構造体のメソッドをJSON-RPCエンドポイントに自動マッピングします。登録の中核となる仕組みは service.go に定義されています。

type serviceRegistry struct {
    mu       sync.Mutex
    services map[string]service
}

type service struct {
    name          string
    callbacks     map[string]*callback
    subscriptions map[string]*callback
}

type callback struct {
    fn          reflect.Value
    rcvr        reflect.Value
    argTypes    []reflect.Type
    hasCtx      bool
    errPos      int
    isSubscribe bool
}

server.RegisterName("eth", myService) で構造体を登録すると、フレームワークはリフレクションを使ってエクスポートされたすべてのメソッドを検出し、パラメータの型を特定して、呼び出し可能なラッパーを生成します。命名規則は自動で適用されます。たとえば "eth" ネームスペースに登録されたサービスの GetBalance メソッドは、先頭を小文字にしたキャメルケースの eth_getBalance として公開されます。

flowchart LR
    REG["RegisterName('eth', EthereumAPI{})"] --> REFLECT["Reflect on EthereumAPI"]
    REFLECT --> DISCOVER["Find exported methods"]
    DISCOVER --> MAP["GetBalance → eth_getBalance<br/>BlockNumber → eth_blockNumber<br/>Subscribe → eth_subscribe"]
    MAP --> STORE["Store in serviceRegistry"]

    REQ["JSON-RPC Request<br/>{method: 'eth_getBalance'}"] --> LOOKUP["Lookup service + method"]
    LOOKUP --> INVOKE["Reflect.Call(args...)"]
    INVOKE --> RESP["JSON-RPC Response"]

最初のパラメータとして context.Context を受け取るメソッドには、リクエストコンテキストが自動的に注入されます。(Subscription, error) を返すメソッドはサブスクリプションエンドポイントとして扱われます。JSONのマーシャリングやエラーのラッピングはすべてフレームワークが担当します。

Tip: 新しいRPCメソッドを追加したいときは、対象のAPI構造体にエクスポートされたメソッドを追加するだけです。フレームワークが自動的に検出してくれるため、登録のボイラープレートもコード生成も必要ありません。構造体名がネームスペースのプレフィックスになり、メソッド名がRPCメソッドのサフィックスになります。

トランスポートレイヤー:HTTP、WebSocket、IPC

第2回で説明したとおり、Nodeは構築時に複数のトランスポートエンドポイントを作成・管理します。RPCフレームワークは4種類のトランスポートをサポートしています。

flowchart TD
    subgraph "Unauthenticated"
        HTTP["HTTP Server<br/>:8545<br/>Stateless requests"]
        WS["WebSocket Server<br/>:8546<br/>Subscriptions"]
        IPC["IPC Server<br/>Unix socket<br/>Local access only"]
    end
    subgraph "Authenticated (JWT)"
        HTTP_AUTH["Auth HTTP<br/>:8551<br/>Engine API"]
        WS_AUTH["Auth WebSocket<br/>:8551<br/>Engine API subscriptions"]
    end
    subgraph "In-Process"
        INPROC["InProc Handler<br/>Direct function calls<br/>No serialization"]
    end
    HTTP --> RPC_SERVER["rpc.Server<br/>serviceRegistry"]
    WS --> RPC_SERVER
    IPC --> RPC_SERVER
    HTTP_AUTH --> RPC_SERVER
    WS_AUTH --> RPC_SERVER
    INPROC --> RPC_SERVER

認証済みエンドポイント(httpAuthwsAuth)はEngine API専用です。コンセンサスレイヤーと実行レイヤーが共有するシークレットを使ったJWT認証を採用しており、すべてのリクエストに有効なJWTトークンが必要です。Nodeの startRPC() メソッドがすべてのトランスポートをセットアップし、認証が必要なAPIメソッドが存在する場合にのみ認証済みエンドポイントが作成されます。

インプロセスハンドラ(inprocHandler)も重要な役割を担っています。node.Attach() がこれを使ってRPCクライアントを生成し、ネットワークシリアライゼーションを一切介さずにメソッドを直接呼び出せるようにします。geth console コマンドがインプロセスクライアント経由で自分自身のノードに接続する仕組みも、これによって実現されています。

BackendインターフェースとEthAPIBackend

ethapi.Backend インターフェースは、すべてのRPCハンドラをブロックチェーンの内部実装から切り離す巨大な抽象化レイヤーです。チェーンアクセス、トランザクションプール操作、設定、ログフィルタリングなど、40以上のメソッドを備えています。

type Backend interface {
    // General Ethereum API
    SyncProgress(ctx context.Context) ethereum.SyncProgress
    SuggestGasTipCap(ctx context.Context) (*big.Int, error)
    ChainDb() ethdb.Database
    AccountManager() *accounts.Manager
    RPCGasCap() uint64
    RPCEVMTimeout() time.Duration

    // Blockchain API
    HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
    BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)
    StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error)

    // Transaction pool API
    SendTx(ctx context.Context, signedTx *types.Transaction) error
    GetPoolTransaction(txHash common.Hash) *types.Transaction
    // ... many more
}
classDiagram
    class Backend {
        <<interface>>
        +SyncProgress() SyncProgress
        +HeaderByNumber(number) Header
        +BlockByNumber(number) Block
        +StateAndHeaderByNumber(number) StateDB, Header
        +SendTx(tx) error
        +GetPoolTransaction(hash) Transaction
        +ChainConfig() ChainConfig
        +Engine() Engine
    }
    class EthAPIBackend {
        -eth *Ethereum
        -gpo *OracleBackend
        +HeaderByNumber() Header
        +SendTx() error
    }
    class EthereumAPI {
        -b Backend
        +GetBalance(addr, block)
        +GetTransactionByHash(hash)
    }
    EthAPIBackend ..|> Backend
    EthereumAPI --> Backend : uses

eth/api_backend.go に実装された EthAPIBackend は、各メソッドを Ethereum 構造体の適切なコンポーネントに委譲するだけのシンプルな構造です。チェーンのクエリは eth.blockchain へ、トランザクションプール操作は eth.txPool へ、コンセンサスエンジンへのアクセスは eth.engine へとそれぞれ処理が渡されます。

この間接参照は意図的な設計です。RPCハンドラがブロックチェーンやトランザクションプールに直接触れることなく、常にBackendインターフェースを経由するようになっています。これにより、モックBackendを使ったハンドラのテストが容易になり、理論上はフルノードやライトノードなど異なるノードタイプで同じハンドラコードを共有することも可能です。

APIネームスペースと登録

Ethereumサービスは APIs() メソッドを通じてAPIネームスペースを登録します。

func (s *Ethereum) APIs() []rpc.API {
    apis := ethapi.GetAPIs(s.APIBackend)  // eth, txpool, debug namespaces
    return append(apis, []rpc.API{
        {Namespace: "miner",  Service: NewMinerAPI(s)},
        {Namespace: "eth",    Service: downloader.NewDownloaderAPI(...)},
        {Namespace: "admin",  Service: NewAdminAPI(s)},
        {Namespace: "debug",  Service: NewDebugAPI(s)},
        {Namespace: "net",    Service: s.netRPCService},
    }...)
}

ethapiGetAPIs() 関数が、コアとなるAPI構造体を生成します。

func GetAPIs(apiBackend Backend) []rpc.API {
    return []rpc.API{
        {Namespace: "eth",    Service: NewEthereumAPI(apiBackend)},
        {Namespace: "eth",    Service: NewBlockChainAPI(apiBackend)},
        {Namespace: "eth",    Service: NewTransactionAPI(apiBackend, nonceLock)},
        {Namespace: "txpool", Service: NewTxPoolAPI(apiBackend)},
        // ...
    }
}

複数のサービスが同じネームスペースに登録できる点も重要です。eth_* メソッドはすべて EthereumAPIBlockChainAPITransactionAPI という3つの異なる構造体から提供されていますが、呼び出し側からは eth ネームスペース配下にまとめて見えます。

flowchart TD
    subgraph "eth namespace"
        EA["EthereumAPI<br/>eth_chainId, eth_syncing,<br/>eth_gasPrice"]
        BCA["BlockChainAPI<br/>eth_getBalance, eth_getCode,<br/>eth_call, eth_estimateGas"]
        TXA["TransactionAPI<br/>eth_sendRawTransaction,<br/>eth_getTransactionByHash"]
    end
    subgraph "Other namespaces"
        MINER["MinerAPI<br/>miner_setGasPrice,<br/>miner_setExtra"]
        ADMIN["AdminAPI<br/>admin_peers,<br/>admin_nodeInfo"]
        DEBUG["DebugAPI<br/>debug_traceTransaction,<br/>debug_dumpBlock"]
        NET["NetAPI<br/>net_version,<br/>net_peerCount"]
    end

Engine API:コンセンサスレイヤーが実行を制御する

マージ後のEthereumにおいて最も重要なRPCインターフェースは、公開されている eth_* APIではなく Engine API です。この認証済みJSON-RPCインターフェースを通じて、コンセンサスレイヤークライアントはGethに対してさまざまな指示を送ります。

Engine APIには3つのコアメソッドファミリーがあります。

  1. engine_forkchoiceUpdatedV* — CLがELに対して、どのブロックがヘッドであるか、どのブロックがセーフであるか、そしてどのブロックがファイナライズ済みであるかを通知します。新しいブロックの構築を開始するためのペイロードリクエストを含むこともあります。

  2. engine_newPayloadV* — CLが検証対象の新しい実行ペイロード(ブロック)をELに送信します。Gethはそれを実行し、有効かどうかを返します。

  3. engine_getPayloadV* — CLがELに対して、以前リクエストして構築中のペイロードを取得します。

sequenceDiagram
    participant CL as Consensus Layer
    participant Engine as Engine API (authenticated)
    participant Miner as Miner
    participant BC as BlockChain

    CL->>Engine: engine_forkchoiceUpdatedV3<br/>(headHash, safeHash, finalHash, payloadAttrs)
    Engine->>BC: Update fork choice head
    Engine->>Miner: BuildPayload(payloadAttrs)
    Miner-->>Engine: payloadId

    Note over Miner: Miner builds block in background...

    CL->>Engine: engine_getPayloadV4(payloadId)
    Engine->>Miner: GetPayload(payloadId)
    Miner-->>Engine: ExecutionPayload + BlobsBundle
    Engine-->>CL: Return payload

    Note over CL: CL proposes block to network

    CL->>Engine: engine_newPayloadV4(payload)
    Engine->>BC: InsertBlockWithoutSealVerification
    BC-->>Engine: valid/invalid/syncing
    Engine-->>CL: PayloadStatus

Miner 構造体は、プルーフオブワーク時代の名残でその名が残っていますが、現在はペイロードビルダーとしての役割を担っています。CLが forkchoiceUpdated を通じてペイロードをリクエストすると、Minerはブロックの組み立てを開始します。トランザクションプールからトランザクションを選択し、実行し、状態ルートを計算して実行ペイロードを準備します。CLはその後 getPayload で取得できます。

type Miner struct {
    confMu      sync.RWMutex
    config      *Config
    chainConfig *params.ChainConfig
    engine      consensus.Engine
    txpool      *txpool.TxPool
    prio        []common.Address  // Prioritized senders
    chain       *core.BlockChain
    pending     *pending
}

Engine APIのエンドポイントは認証済みAPIとして登録されており、JWT保護されたトランスポートにのみ公開されます。Nodeの getAPIs() メソッドが認証済みAPIと非認証APIを分離することで、Engine APIメソッドが公開HTTP/WSエンドポイントから呼び出されることはありません。

Tip: Engine APIの実際のメソッド実装は catalyst パッケージ(eth/catalyst/)に含まれています。CL-ELの連携の詳細を理解したいなら、まず catalyst/api.go を読みましょう。ForkchoiceUpdatedV*NewPayloadV*GetPayloadV* の実装がすべてここにあります。

RPC層とAPI層の全体像を把握したことで、Gethの主要なサブシステムをすべて辿ったことになります。ブートから始まり、ブロック実行、ストレージ、ネットワーキング、そして外部APIまでを一通り見てきました。最終回では、プロジェクトのビルド、テスト、コントリビューションに関する実践的なガイダンスをお届けします。