c-lightning 0.7から任意の言語でPluginを書けるようになった↓
blockstream.com
現在、C、Python、Golang、JavaでPluginを書くためのライブラリが提供されている。
c-lightningのPlugin
Pluginを利用することでc-lightningが提供する機能を拡張することができ、そのPluginは任意の言語で書くことができる。これはc-lightningが標準入力と標準出力を介してPluginと連携しているためで、Pluginは標準入力でc-lightningからイベントの通知や、メソッドの要求を待ち受け、Pluginはその応答を標準出力を介して送信する。この標準入力と標準出力で連携する際のデータフォーマットはJSON-RPCv2。
Pluginを作ることで以下のようなことが可能になる。
- コマンドラインオプションパススルー
lightningdを介して公開される独自のコマンドラインオプションを登録できる。
- JSON-RPCコマンドパススルー
独自のコマンドをJSON-RPCインターフェースに追加することができる。
- イベント通知
lightningdからイベントのプッシュ通知を受け取れる。
- フック
lightningdの内部イベントの通知を受け取り、その動作を変更したり、カスタム動作を追加したりすることができる。
Pluginの登録方法
Pluginはlightningd
起動時に--plugin=
オプションで登録できる。複数のPluginを登録したい場合は、Pluginの数分--plugin=
オプションを指定する。
$ lightningd --plugin=<Pluginファイルのパス>
なお、Pluginのファイルは実行可能ファイル(実行権限のあるファイル)である必要がある。実行権限が付いてないと、lightningd
起動時にPermission deniedで怒られる。
lightningd
はJSON-RPCリクエストをプラグインの標準入力に書き込み、標準出力からの返信を読み取る。Pluginを初期化する際は以下の2つのRPCメソッドが必要になるため、Pluginを作る際は必ず以下のgetmanifest
とinit
のRPCメソッドを作る必要がある。
実際、Pluginを指定してlightningdを起動すると、Pluginの標準入力に以下のようなgetmanifest
を要求するリクエストが飛んでくる(↓ログ形式で表記してるけど、データの実態はINFO -- : 以降)。
INFO -- : {
INFO -- : "jsonrpc": "2.0",
INFO -- : "id": 1,
INFO -- : "method": "getmanifest",
INFO -- : "params": {
INFO -- : }
INFO -- : }
INFO -- :
Pluginは、改行コードが2つ続いたら、それまでのデータをJSONとしてパースして処理すればいい。なお、標準出力はlightningd
への応答になるので、Plugin内でログを出力したい場合は、標準出力以外(ファイルなど)に出力する必要がある。
getmanifest
全てのPluginに必要なメソッドで起動時にパラメータ無しで呼び出される。getmanifest
メソッドは以下のような内容を返す。
{
"options": [
{
"name": "greeting",
"type": "string",
"default": "World",
"description": "What name should I call you?"
}
],
"rpcmethods": [
{
"name": "hello",
"usage": "[name]",
"description": "Returns a personalized greeting for {greeting} (set via options)."
},
{
"name": "gettime",
"usage": "",
"description": "Returns the current time in {timezone}",
"long_description": "Returns the current time in the timezone that is given as the only parameter.\nThis description may be quite long and is allowed to span multiple lines."
}
],
"subscriptions": [
"connect",
"disconnect"
]
}
option
はlightningd
が受け入れるコマンドラインオプションのリストに追加される。上記の例では、デフォルト値がWorld
で指定された定義内容の--greeting
オプションが追加される。現在サポートしているのは文字列オプションのみ。
rpcmethods
はlightningd
の組み込みコマンドのようにlightningd
のJSON-RPC over Unix-Socketインターフェースを介して公開されるメソッドになる。JSON-RPC呼び出しに与えられた任意の引数は、すべて逐次渡される。name、usage、descriptionフィールドは必須だが、long_descriptionは省略可能。usageでは、[]でオプションのパラメータ名を囲む必要がある。
Pluginはrpcmethodの名称が以前に登録されているのと同じでない限り任意の名称を自由に登録できる。これにはgetinfo
やhelp
などの組み込みメソッドも含まれる。名称が競合する場合lightningd
はエラーを出力して終了する。
init
getmanifest
のレスポンスを返すと、続いてlightningd
が受け入れるコマンドラインオプションのリストと、稼働しているlightningd
のホームディレクトリとRPCのソケットファイルの情報を含むJSONオブジェクトが送られて来て、lightningd
がJSON-RPCコマンドを受け取る準備ができたことをPluginに通知するのにinit
メソッドが要求される。
{
"jsonrpc"=>"2.0",
"id"=>3,
"method"=>"init",
"params"=>{
"options"=>{},
"configuration"=>{
"lightning-dir"=>"/home/azuchi/.lightning",
"rpc-file"=>"lightning-rpc"
}
}
}
Pluginはinit
呼び出しに応じなければならないが、応答するかどうかは任意で、応答しても現在はlightningd
によって破棄される。
イベント通知
上記のRPCメソッドに加えて、Pluginはlightningd
のイベントを購読することができる。↑のgetmanifest
のレスポンスのsubscriptions
で、購読するイベントを指定する。指定したイベントがlightningd
で発生すると、lightningd
はJSON-RPCを使ってPluginに通知をプッシュする。この時、上記のRPCと違って、JSONにidパラメータは含まれない(通知であり、レスポンスを受け取る必要がないので)。
ただ、現状のlightningd
で通知されるイベントは以下の2つのみっぽい。
- connect
ピアへの新しい接続が確立された際に通知されるイベントで、接続先ピアのid
とaddress
が通知される。
{
"id": "02f6725f9c1c40333b67faea92fd211c183050f28df32cac3f9d69685fe9665432",
"address": "1.2.3.4"
}
- disconnect
ピアへの接続が切断された際に通知するイベントで、切断したピアのid
が通知される。
{
"id": "02f6725f9c1c40333b67faea92fd211c183050f28df32cac3f9d69685fe9665432"
}
もうちょっと、通知されるイベント増えないもんかねー。
Hook
Hookを使うと、c-lightningのコードを変更することなく、Pluginでlightningd
の動作をカスタマイズできる。このHookとイベントの通知は似ているが、以下の点が異なる。
- 通知は非同期で、
lightningd
は通知を送信するが、Pluginがその通知を処理するのを待つことはない。一方Hookは動機的で、Pluginからの応答があるまでlightningd
はイベントの処理を完了できない。
- 通知を受け取るPluginは何個でも登録できるけど、Hookに登録できるPluginは1つだけ(複数のPluginからHookのコールバックで矛盾する結果が返ってくると処理できないため)。
Hookはlightningd
の動作を変更できるので、有効なレスポンスをlightningd
に返すよう注意して実装する必要がある。
Hookの種類
現在対応しているHookは以下のとおり。
peer_connected
ピアが接続しハンドシェイクが正常に完了した際に呼び出されるHook。接続したピアと既にチャネルを開いている場合は、以下の情報が返ってくる。
{
"peer": {
"id": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f",
"addr": "34.239.230.56:9735",
"globalfeatures": "",
"localfeatures": ""
}
}
Pluginからlightningd
への応答は、result
メンバーとしてdisconnect
かcontinue
を返さなければならない。disconnect
でerror_message
メンバーがある場合、その情報は切断前にピアに送信される。
db_write
変更がデータベースにコミットされる直前に呼び出されるHook。このHookの利用にあたっては、以下のような厳しめの制約がある。
- このHookに登録するPluginは応答としてDB操作を引き起こす可能性があるもの(ロギング以外の)を実行してはならない。
- このHookを登録しているPluginや他のHookやコマンドに登録してはならない。それらが混在していると↑のルールを破る可能性があるため。
- HookはPluginが初期化される前に呼ばれる。
lightningd
から以下のようなデータが通知される。
{
"writes": [ "PRAGMA foreign_keys = ON" ]
}
応答はtrue
で、それ以外の場合、データベースへのコミットはされずlightningd
はエラーになる。
invoice_payment
まだ支払いがされていないinvoiceに対する有効な支払いが届いた際に呼び出されるHook。
{
"payment": {
"label": "unique-label-for-invoice",
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
"msat": "10000msat"
}
}
応答は、BOLT 4で定義されているゼロでないfailure_code
を返すか、支払いを受け入れる場合は、空のオブジェクトを返す。
openchannel
リモートピアからチャネルオープンの要求が来て、基本的なチェックをパスした際に呼ばれるHookで、lightningd
からは以下の情報が送られてくる。
{
"openchannel": {
"id": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f",
"funding_satoshis": "100000000msat",
"push_msat": "0msat",
"dust_limit_satoshis": "546000msat",
"max_htlc_value_in_flight_msat": "18446744073709551615msat",
"channel_reserve_satoshis": "1000000msat",
"htlc_minimum_msat": "0msat",
"feerate_per_kw": 7500,
"to_self_delay": 5,
"max_accepted_htlcs": 483,
"channel_flags": 1
}
}
他にもフィールドがある場合があり、各フィールドはBOLT 2のopen_channelメッセージに定義されている。
応答には、result
メンバーに、reject
もしくはcontinue
文字列を含める必要がある。reject
でerror_message
メンバーがある場合、その情報は切断前にピアに送信される。
PluginをRubyで簡単に書く
↑のPluginをRubyで簡単に書けるようライブラリc-lightningrb
を作ってみた↓
github.com
このライブラリを使うと以下のようにDSLでRPCやイベント通知、Hookのハンドラをlambdaで記述できる。lambdaで記述したロジックは、クラスのインスタンスメソッドとして定義されるので、インスタンスの各フィールド、メソッドにアクセス可能。
#!/usr/bin/env ruby
require 'lightning'
class HelloPlugin < Lightning::Plugin
desc '[name]', 'Returns a personalized greeting for {greeting} (set via options).'
define_rpc :hello, -> (name) do
log.info "log = #{log}"
"hello #{name}"
end
subscribe :connect, ->(id, address) do
log.info "received connect notification. id = #{id}, address = #{address}"
end
hook :peer_connected, ->(peer) do
log.info "peer_connected. peer = #{peer}"
{result: 'continue'}
end
end
p = HelloPlugin.new
p.run
pluginオプションでlightningd
にpluginを指定する。この時ファイルに実行権限を付けておくこと。
$ lightningd --network=testnet --plugin=<Pluginファイルのパス>
help実行すると、追加したRPCメソッドの情報もでてくる。
$ lightning-cli help
...
hello [name]
Returns a personalized greeting for {greeting} (set via options)
...
実行すると、ちゃんとPluginが実行されてるのが分かる。
$ lightning-cli hello lightning
"hello lightning"