世界に向けてサービスを提供する: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
認証済みエンドポイント(httpAuth、wsAuth)は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},
}...)
}
ethapi の GetAPIs() 関数が、コアとなる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_* メソッドはすべて EthereumAPI、BlockChainAPI、TransactionAPI という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つのコアメソッドファミリーがあります。
-
engine_forkchoiceUpdatedV*— CLがELに対して、どのブロックがヘッドであるか、どのブロックがセーフであるか、そしてどのブロックがファイナライズ済みであるかを通知します。新しいブロックの構築を開始するためのペイロードリクエストを含むこともあります。 -
engine_newPayloadV*— CLが検証対象の新しい実行ペイロード(ブロック)をELに送信します。Gethはそれを実行し、有効かどうかを返します。 -
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までを一通り見てきました。最終回では、プロジェクトのビルド、テスト、コントリビューションに関する実践的なガイダンスをお届けします。