CometBFT: ABCI
概要
ABCI (application blockchain interface) は CometBFT コアの各コンポーネント (コンセンサス層) とブロックチェーンアプリケーションの実装 (アプリケーション層) が通信するためのインターフェース仕様である。ブロックチェーンアプリケーション開発者が CometBFT と連携するための標準的なプロトコルを定義している。
ABCI は Fig 1 のようにコンセンサスアルゴリズムやブロック生成に関する責任を CometBFT コアに分離できるため、アプリケーション実装者はブロックチェーン機能、状態管理、スマートコントラクト実装などのアプリケーション固有の機能の実装に注力することができる。ただし、現実的には CometBFT が何の目的で各機能を呼び出しているかの詳細を知っておく必要があり、ABCI の方向性もブロックチェーンを効率化するためにインターフェースの詳細化と密結合化が進んでいる。
アプリケーション要件によっては、実際にアプリケーション固有のブロックチェーンを構築するために ABCI 経由でアプリケーション層を実装するだけでは不十分な可能性がある。この場合、CometBFT のソースを修正する必要がある。
アプリケーション層のみを開発する。これは CometBFT のバイナリをそのまま利用し、socket または grpc でアプリケーション実装と通信する方法である。
アプリケーション層に加えて CometBFT の起動部分を実装する。つまり 1. に加えて main() から開始する処理を実装し CometBFT をライブラリとして利用する実装する方針である。
- CometBFT とアプリケーションを同一のバイナリまたは同一のプロセスで動作させたい。
- 認証や監視などの外部システムと統合したい。
アプリケーション層に加えて CometBFT をフォークして任意の部分を実装する。CometBFT に対する要件が 2. では行えなかったケースだが、CometBFT のバージョンアップを追従する必要がありメンテナンスコストが高い。
本格的に独自のブロックチェーンを開発するのであれば IBC (inter-blockchain) の利用できる Cosmos SDK を用いる方が良いだろう。Cosmos SDK はコンセンサスエンジンに CometBFT を使用しアプリケーション実装とは ABCI で通信するため開発知識を共有することができる。
Table of Contents
構成
ABCI の実体は RPC に基づく通信プロトコルの定義である。アプリケーション実装と CometBFT コアをプロセスまたはノードで分離できることから、アプリケーションのプロセスをより制約の強い環境や隔離されたコンテナ内で実行したり、ノード自体を DMZ 内で動作させるような構成ができる。
CometBFT コアから呼び出されるアプリケーション実装までのスタックを Fig 2 に示す。
呼び出し経路
標準の ABCI 実装では Fig 2 の socket, grpc, local に対応する 3 つのアプリケーション呼び出し経路の構成を選択できる。
local 以外の socket や grpc 構成では、サーバ側 (アプリケーション側) のプロセスを CometBFT とは別に起動する必要がある。クライアント側では、設定の abci に grpc
を指定すると grpcClient が選択され socket
を指定すると socketClient が選択される。また設定の proxy_app に接続先アドレスの代わりに kvstore や noop といった標準ビルトイン実装のアプリケーションを指定すると localClient が選択される。デフォルトの動作は tcp://127.0.0.1:26658 への socket 接続である。標準実装では DefaultNewNode または RunReplayFile から呼び出す DefaultClientCreator で決定している。
これらの 3 つの ABCI 呼び出し経路には次のような特徴がある:
- socket でのメッセージ送受信
-
ProtocolBuffers を用いてシリアライズされたリクエスト/レスポンスメッセージを単一の TCP ソケットまたは Unix ドメインソケット上で送受信する通信実装。現在の実装では、メッセージの送信はクライアント側でキューによって直列化され、またサーバ側でも受信順に逐次処理するため、呼び出し側が並行化されていたとしても ABCI アプリケーション実装が並行で実行されることはない。
- grpc でのメッセージ送受信
-
gRPC を使用して ABCIApplicationClient の実装を使用する通信実装。ただし will have significant performance overhead と言われており CometBFT では標準的に socket が使用されている。
gRPC 仕様の通信であるため Go 以外の言語で開発されたサーバ実装に対して開発が容易な点は選択肢の一つとなる。現在の実装では呼び出し側が並行化されていると ABCI アプリケーション実装も並行実行される。したがって、アプリケーションの特定の処理が非常に遅いようなケースでは、socket では後続の処理を巻き込んで遅延するが、grpc であれば遅い処理と並行して後続の処理を実行できるため有効に機能する可能性がある。
- local でのメッセージ送受信
-
同一のプロセス内に存在する ABCI アプリケーション実装を通常のメソッド呼び出しと同じ機構で呼び出す実装。現在の実装ではメッセージの送受信 (つまりメソッドの呼び出し) はすべて Mutex によって直列化されているため、呼び出し側が並行化されていてもアプリケーション実装が並行で実行されることはない。
アプリケーションを local で起動するような main() を作成することで CometBFT コアとアプリケーションを同一のバイナリとして同一のプロセス内で動作させることができる。
選択したクライアント実装によって並行実行する ABCI の挙動が異なることに注意。アプリケーションを並行して呼び出した場合、socketClient と localClient では逐次的にアプリケーション実装が呼び出されるのに対して grpcClient では並行して呼び出しが行われる。具体的には、例えばアプリケーション実装で Info() に 1 秒かかると仮定し、異なる 5 つのスレッドで Info() を呼び出したとすると、socket と local ではすべてのスレッドが終了するまでに 5 秒程度かかるのに対して、grpcClient は 1 秒程度で終了する。このような挙動の違いは次のテストで transport
を書き換えて実行すると確認できる。
torao@lazurite$ cat abci-parcall_test.go
package test import ( "fmt" "sync" "testing" "time" abcicli "github.com/cometbft/cometbft/abci/client" "github.com/cometbft/cometbft/abci/server" "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/libs/service" "github.com/cometbft/cometbft/proxy" ) type SlowApp struct { types.BaseApplication } func (*SlowApp) Info(types.RequestInfo) types.ResponseInfo { fmt.Printf("[%s] begin Info()\n", time.Now().Format(time.RFC3339Nano)) time.Sleep(1 * time.Second) fmt.Printf("[%s] end Info()\n", time.Now().Format(time.RFC3339Nano)) return types.ResponseInfo{} } var _ types.Application = (*SlowApp)(nil) func TestABCIParallelCall(t *testing.T) { addr := "tcp://127.0.0.1:8899" transport := "socket" app := &SlowApp{} var err error var cli abcicli.Client var svr service.Service if transport == "socket" || transport == "grpc" { svr, err = server.NewServer(addr, transport, app) if err != nil { t.Fatal(err) } svr.Start() creator := proxy.DefaultClientCreator(addr, transport, "./app/db") cli, err = creator.NewABCIClient() if err != nil { t.Fatal(err) } cli.Start() } else { cli = abcicli.NewLocalClient(nil, &SlowApp{}) } defer func() { defer cli.Stop() if svr != nil { svr.Stop() } }() wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func() { cli.InfoSync(types.RequestInfo{}) wg.Done() }() } wg.Wait() }
このような挙動の違いは socket, local で正しく動作するアプリケーション実装が grpc では動作しないという互換性の問題を引き起こす可能性がある。
ABCI アプリケーション
ABCI アプリケーション実装の主要な目的は CometBFT によって決定されたブロックを実行することである。これは、CometBFT が分散システムにおけるステートマシンレプリケーションとして機能するときのステートマシン (state machine) に相当する。
Application インターフェースに定義されている 14 個のメソッドはそれぞれ ABCI クライアントのメソッドに対応している。各メソッドは次に示す状況で呼び出される。
メソッド | アプリケーションのメソッドが呼び出される状況 |
---|---|
Info |
|
Query |
|
CheckTx |
|
InitChain |
|
PrepareProposal |
|
ProcessProposal |
|
BeginBlock |
|
DeliverTx |
|
EndBlock |
|
Commit |
|
ListSnapshots |
|
OfferSnapshot |
|
LoadSnapshotChunk |
|
ApplySnapshotChunk |
|
アプリケーションの実装フェーズではこれらすべてのメソッドを注意深く実装する必要がある。ただし、動作確認用のアプリケーション実装であれば BaseApplication を匿名フィールドに定義して必要なメソッドのみを実装することもできる。
アプリケーション実装の Query メソッドは RPC から呼び出すこともできるため、例えば通貨や NFT のようにブロックチェーンアプリケーション固有の機能を問い合わせる用途に利用できる。実際、Table 2 に示すように CometBFT からも (ABCI の定義を追加するまでもないような) 問い合わせで使用されている。
Path | 意味 |
---|---|
/p2p/filter/addr/$REMOTE_ADDR |
指定された $REMOTE_ADDR からの接続を受け付けたときに呼び出される。アプリケーションが CodeTypeOK 以外の応答をするとその接続は拒否される。 |
/p2p/filter/id/$REMOTE_ID |
指定された $REMOTE_ID からの接続を受け付けたときに呼び出される。アプリケーションが CodeTypeOK 以外の応答をするとそのピアは拒否される。 |
ABCI クライアント
Client インターフェースは CometBFT コアからアプリケーション実装の機能を呼び出すためのメソッドを定義している。追加の Echo と Flush は ABCI 通信のみに作用しアプリケーション実装までは到達せずに SocketServer や GRPCApplication で折り返している。Echo は単純に与えられたメッセージで応答するだけで ABCI CLI から疎通確認のために利用できる。Flush は Commit や Recheck 時に呼び出され、socket クライアント/サーバに対してストリームをフラッシュを指示しバッファリングされているすべてのリクエスト/レスポンスを確実に相手に送信する (grpc と local では無視される)。
Client インターフェースの接尾辞 Sync/Async はそれぞれ該当するアプリケーションメソッドの同期/非同期版であることを示している。ただし本質的に Async が非同期実行となっているのは socket 実装のみで、grpc と local は同期実行の結果をコールバックで通知しているだけである (したがって grpc と local での Async 呼び出しはアプリケーション実装の処理が終わるまでブロックされる)。またすべての標準クライアント実装の Sync 版は単に内部で Async を呼び出して応答があるまで待機しているだけである。
Async 版メソッドの返値である ReqRes は他の言語の Future/Promise パターンを模して CometBFT で独自に実装されたものである。しかし ReqRes では成功応答しか通知できないため、いくつかのレスポンスでは Code フィールドを使ってアプリケーション側の失敗を通知している。いずれのクライアント実装も通信エラーやサーバ側のエラーは回復不能な状況と見なされてクライアント処理が終了してメッセージ送受信ルーチンを終える。
Client インターフェースは AppConnConsensus, AppConnMempool, AppConnQuery, AppConnSnapshot の 4 つのインターフェースを継承しており、CometBFT コアからはこれらの目的に限定されたインターフェースを介してアプリケーション実装の機能を利用する (Client インターフェースでは各メソッドに対して同期/非同期の対称性を強制しているため無駄なメソッド実装が発生している)。
ABCI サーバ
ABCI サーバは ABCI クライアントからの接続を受け付け、アプリケーション実装の機能を呼び出して応答する。標準実装では接続方法の local 以外の socket と grpc に対応するサーバが用意されている。
Cosmos のエコシステムでは ABCI アプリケーションをどのように開発し実行するかは Cosmos SDK の関心であることから、CometBFT では ABCI サーバを起動するための NewServer を用意しているだけである。CometBFT のみで ABCI アプリケーションを実装する場合、abci-cli のkvstore や e2e テストのコードを参考に CometBFT をライブラリとして参照し、main() から ABCI サーバを起動するコードを作成する必要がある。
参照
- ABCI++ (v0.37.2)