はじめに
!注意:執筆途中
lndで送金を行うチュートリアルについて以前記事を書いた。コマンドを叩けばlighnintgで送金をすることができた。しかし各コマンドが裏で何をしていることがのか、などはよくわからなかった。なので今回はlndのコードリーディングをしながら何が起きているのか
lndは読むにはBOLTを理解しているのがかなり大事みたいです。
BOLTとは
Lightning Network仕様の一つ。lndはBOLTに乗っ取って作られている。
全体の流れ
全体の流れはLND Developer SiteのDockerチュートリアルとほぼ同じ流れで進める。
チャンネル開設 -> invoice発行 -> 送金 -> チャンネルクローズ
登場人物:アリス、ボブ
これらの処理に関係するコードを掻い摘んで行く。
共通事項
lndの操作を行う時、lnd
もしくはlncli
コマンドを入力します。各コマンドはlnd/cmdの配下でurfave/cliというパッケージを使って実装されています。
[lnd/server.go]がlndのサーバーで、lnd
コマンドを入力した時に立ち上がるサーバーの機能を実装しています。
lncli
コマンドを入力すると、引数に従って各種gRPCメソッドが実行されます。lncli
で呼び出されるgRPCの処理はlnd/rpcserver.goで実装されています。
server.go
とrpcserver.go
の違いは、serverとしての機能と、RPCの処理を別ファイルに別々のファイルで実装するためです。
チャンネル開設に関係するソース
・lnd/fundingmanager.go
… チャンネル開設のための各種メソッドが実装されています。
https://github.com/lightningnetwork/lnd/blob/master/fundingmanager.go
・lnd/peer.go
… lndではpeersに設定されたnodeか仲介者nodeのどちらかに対してchannel開設を行えます。peer.go
ではメッセージの送受信をし、switchで条件判断しています。
・/lnd/chanacceptor
… open_channelメッセージを受け取る側の処理に関するコードがまとまったディレクトリ。
https://github.com/lightningnetwork/lnd/tree/master/chanacceptor
・/lnd/lnwire
… ノード間でやり取りされる各種メッセージのフォーマットを定義している。
・/lnd/channeldb
… オープン、クローズしたチャンネルや、invoice・paymentに関する記録をboltdbに記録するためのスクリプトがまとまったディレクトリ。
https://github.com/lightningnetwork/lnd/tree/master/channeldb
1.チャンネル開設
$ lncli openchannel --node_key=<BOB_PUBKEY> --local_amt=1000000
アリスがボブとチャンネルを開設する時に入力するコマンドです。
チャンネルを開設する時の処理はBOLT 02に書いてあります。
チャンネル開設時、アリスとボブの間では以下のように5種類の6つのメッセージがやり取りされます。

それぞれのメッセージについてlndのソースコードと共に解説する。
備考 : BOLTのメッセージのフォーマットはtypeとpayload(data)の二つにより構成されている。
・type … メッセージの種類を示す。
・payload … メッセージからtypeを取り除いた残りの部分。メッセージによって様々なdataに分けられる。
open_channelメッセージ
lndのopen_chacnnelメッセージのフォーマットを定義しているソースコード↓
https://github.com/lightningnetwork/lnd/blob/master/lnwire/open_channel.go
open_channelメッセージは以下のような構成になっています。
1.type: 32 (open_channel)
2.data:
・chain_hash(32byte)
オープンしたチャネルがあるブロックチェーンのジェネシスブロックのハッシュ値。
・temporary_channel_id(32)
ファンディングトランザクションが確立するまでにチャンネルを指定するID。
・funding_satoshis(8)
チャンネルをオープンした際チャンネルに置いた金額。
・push_msat(8)
・dust_limit_satoshis(8)
このノードのコミットメントまたはHTLCトランザクションに対して生成されるアウトプットが持つ金額下限値。例えば、この金額とHTLCトランザクション手数料の和よりも小さいHTLCはオンチェーンに入ることができない。
・max_htlc_value_in_flight_msat(8)
未払いHTLCの総和に対する上限値。未払いHTLCに拠出できる金額を制限している、
・channel_reserve_satoshis(8)
相手のノードが自身に必ずキープしておかなければいけない最小金額。
・htlc_minimum_msat(4)
このノードが受け取れる最も小さいHTLC金額
・feerate_per_kw(4)
初期手数料の金額
・to_self_delay(2)
相手ノード自身へのアウトプットをどれくらい遅延させておかないといけないかを指定するブロック数。相手のノードがダウンした時に自分の資産を回収するのに待たないといけないblock数。
・max_accepted_htlcs(2)
相手のノードが要求できる未払いHTLCの上限数
・funding_pubkey(33)
ファンディングトランザクションアウトプットの2-of-2マルチシグscriptにある公開鍵。
・revocation_basepoint(33)
取り消しpreimageと紐づくもので、このコミットメントトランザクションがこのコミットメントトランザクションに対する一意な取り消し鍵を生成する基準になる
・payment_basepoint(33)
同様にこのノードへの支払いに使う鍵の列を生成 するために使われる
・delayed_payment_basepoint(33)
同様にこのノードへの支払いに使う鍵の列を生成 するために使われる
・first_per_commitment_point(33)
accept_channelメッセージ
1.type: 33 (accept_channel)
2.data:
・temporary_channel_id(32)
・dust_limit_satoshis(8)
・max_htlc_value_in_flight_msat(8) …
・channel_reserve_satoshis(8) …
・minimum_depth(4) …
・htlc_minimum_msat(4) …
・to_self_delay(2)
・max_accepted_htlcs(2) …
・funding_pubkey(33) …
・revocation_basepoint(33) …
・payment_basepoint(33) …
・delayed_payment_basepoint(33) …
・first_per_commitment_point(33) …
funding_createメッセージ
1.type: 34 (funding_created)
2.data:
・temporary_channel_id(32)
・funding_txid(32)
・funding_output_index(2)
・signature(64)
funding_signedメッセージ
fundingに対して最初のcommitment txに必要な署名の情報を渡すためのもの。
1.type: 35 (funding_signed)
2.data:
・channel_id(32)
・signature(64)
funding_lockedメッセージ
Channel Open
TODO:関数遷移図
licli openchannel
コマンドを入力するとserver.go
のOpenChannel()が実行されます。
gRPCでは.proto
でinterfaceを定義し、コマンドを入力するとxxx.pb.goというinterfaceがライブラリとして自動で生成されます。server.goはそれらをimportし、各種interfaceをサーバー・クライアントで実装しています。
ちなみに
lnd/lnrpc/REDEME.mdを読むとlndで利用可能なRPCのinterfaceのリストを確認することができます。
server.goの3238行目にOpenChannel()
があります。
func (s *server) OpenChannel(
req *openChanReq) (chan *lnrpc.OpenStatusUpdate, chan error) {
// The updateChan will have a buffer of 2, since we expect a ChanPending
// + a ChanOpen update, and we want to make sure the funding process is
// not blocked if the caller is not reading the updates.
req.updates = make(chan *lnrpc.OpenStatusUpdate, 2)
req.err = make(chan error, 1)
// First attempt to locate the target peer to open a channel with, if
// we're unable to locate the peer then this request will fail.
pubKeyBytes := req.targetPubkey.SerializeCompressed()
s.mu.RLock()
peer, ok := s.peersByPub[string(pubKeyBytes)]
if !ok {
s.mu.RUnlock()
req.err <- fmt.Errorf("peer %x is not online", pubKeyBytes)
return req.updates, req.err
}
s.mu.RUnlock()
// We'll wait until the peer is active before beginning the channel
// opening process.
select {
case <-peer.activeSignal:
case <-peer.quit:
req.err <- fmt.Errorf("peer %x disconnected", pubKeyBytes)
return req.updates, req.err
case <-s.quit:
req.err <- ErrServerShuttingDown
return req.updates, req.err
}
// If the fee rate wasn't specified, then we'll use a default
// confirmation target.
if req.fundingFeePerKw == 0 {
estimator := s.cc.feeEstimator
feeRate, err := estimator.EstimateFeePerKW(6)
if err != nil {
req.err <- err
return req.updates, req.err
}
req.fundingFeePerKw = feeRate
}
// Spawn a goroutine to send the funding workflow request to the funding
// manager. This allows the server to continue handling queries instead
// of blocking on this request which is exported as a synchronous
// request to the outside world.
go s.fundingMgr.initFundingWorkflow(peer, req)
return req.updates, req.err
}
最後の方に go s.fundingMgr.initFundingWorkflow(peer, req)
とあります。ここでlnd/fundingmanager.go
の2776行目
にinitFundingWorkflow()
があります。
func (f *fundingManager) initFundingWorkflow(peer lnpeer.Peer, req *openChanReq) {
f.fundingRequests <- &initFundingMsg{
peer: peer,
openChanReq: req,
}
}
これはfundingメッセージの初期化を行なっています。
lndのサーバーが立ち上がった時、毎回lnd/server.go
のStart()
が呼び出されます。これはデータベースにあるchannelやpeerの確認をしたり、いらないデータの初期化をしたりするためなのですが、この時、fundingmanager.go
の469行目でstart()
が呼び出されます。これがサーバーがリスタートした時に、過去の待機中のfunding処理がデータベースに残っていないかを確認します。確認が終わったらreservationCoordinator()
を呼び出します。
fundingmanager.go
の735行目にreservationCoordinator()
があります
func (f *fundingManager) reservationCoordinator() {
defer f.wg.Done()
zombieSweepTicker := time.NewTicker(f.cfg.ZombieSweeperInterval)
defer zombieSweepTicker.Stop()
for {
select {
case msg := <-f.fundingMsgs:
switch fmsg := msg.(type) {
case *fundingOpenMsg:
f.handleFundingOpen(fmsg)
case *fundingAcceptMsg:
f.handleFundingAccept(fmsg)
case *fundingCreatedMsg:
f.handleFundingCreated(fmsg)
case *fundingSignedMsg:
f.handleFundingSigned(fmsg)
case *fundingLockedMsg:
f.wg.Add(1)
go f.handleFundingLocked(fmsg)
case *fundingErrorMsg:
f.handleErrorMsg(fmsg)
}
case req := <-f.fundingRequests:
f.handleInitFundingMsg(req)
case <-zombieSweepTicker.C:
f.pruneZombieReservations()
case req := <-f.queries:
switch msg := req.(type) {
case *pendingChansReq:
f.handlePendingChannels(msg)
}
case <-f.quit:
return
}
}
}
reservationCoordinator()とはgoの並行処理を用いて、メッセージの内容を判定し、正しいhandlerに受け渡します。
まず最初にf.handleFundingOpen(fmsg)
が実行されます。
handleFundingOpen()は1099行目にあります。
はいくつものif分が並び、walletがチャンネルを開設できる状況かを判断しています。
1187行目でついにlnd/chanacceptor/interface.go
のChannelAcceptRequest()を呼び出し、さらにその中でlnd/lnwire/open_channel.go
でメッセージのフォーマッティングを行っています。
chanReq := &chanacceptor.ChannelAcceptRequest{
Node: fmsg.peer.IdentityKey(),
OpenChanMsg: fmsg.msg,
}
type ChannelAcceptRequest struct {
// Node is the public key of the node requesting to open a channel.
Node *btcec.PublicKey
// OpenChanMsg is the actual OpenChannel protocol message that the peer
// sent to us.
OpenChanMsg *lnwire.OpenChannel
}
↓lnd/lnwire/open_channel.go
https://github.com/lightningnetwork/lnd/blob/master/lnwire/open_channel.go
1390行目にてlnd/peer.go
if err := fmsg.peer.SendMessage(false, fundingSigned); err != nil {
fndgLog.Errorf("unable to send FundingSigned message: %v", err)
f.failFundingFlow(fmsg.peer, pendingChanID, err)
deleteFromDatabase()
return
}
※go言語に詳しくないのですがif分の:=
部分が条件分岐の前処理で、そこでSendMessageをこなっています。
SendMessage()を辿っていくとnewMsgStream()
に行き着きます。
メッセージの送信側である**notifier(通知者)の側は確認できました。
では今度はaccepter(受信者)**側を見ていきます。
1002行目にreadHandler()
という関数があります。これはGoの並行処理で常に走っており、メッセージの受信を行ってます。
readHandler()内の1074行目からswitchが始ままります。これで受信したメッセージの仕分けを行います。
switch msg := nextMsg.(type) {
case *lnwire.Pong:
// When we receive a Pong message in response to our
// last ping message, we'll use the time in which we
// sent the ping message to measure a rough estimate of
// round trip time.
pingSendTime := atomic.LoadInt64(&p.pingLastSend)
delay := (time.Now().UnixNano() - pingSendTime) / 1000
atomic.StoreInt64(&p.pingTime, delay)
case *lnwire.Ping:
pongBytes := make([]byte, msg.NumPongBytes)
p.queueMsg(lnwire.NewPong(pongBytes), nil)
case *lnwire.OpenChannel:
p.server.fundingMgr.processFundingOpen(msg, p)
case *lnwire.AcceptChannel:
p.server.fundingMgr.processFundingAccept(msg, p)
case *lnwire.FundingCreated:
p.server.fundingMgr.processFundingCreated(msg, p)
case *lnwire.FundingSigned:
p.server.fundingMgr.processFundingSigned(msg, p)
case *lnwire.FundingLocked:
p.server.fundingMgr.processFundingLocked(msg, p)
case *lnwire.Shutdown:
select {
case p.chanCloseMsgs <- &closeMsg{msg.ChannelID, msg}:
case <-p.quit:
break out
}
case *lnwire.ClosingSigned:
select {
case p.chanCloseMsgs <- &closeMsg{msg.ChannelID, msg}:
case <-p.quit:
break out
}
case *lnwire.Error:
targetChan = msg.ChanID
isLinkUpdate = p.handleError(msg)
case *lnwire.ChannelReestablish:
targetChan = msg.ChanID
isLinkUpdate = p.isActiveChannel(targetChan)
// If we failed to find the link in question, and the
// message received was a channel sync message, then
// this might be a peer trying to resync closed channel.
// In this case we'll try to resend our last channel
// sync message, such that the peer can recover funds
// from the closed channel.
if !isLinkUpdate {
err := p.resendChanSyncMsg(targetChan)
if err != nil {
// TODO(halseth): send error to peer?
peerLog.Errorf("resend failed: %v",
err)
}
}
case LinkUpdater:
targetChan = msg.TargetChanID()
isLinkUpdate = p.isActiveChannel(targetChan)
case *lnwire.ChannelUpdate,
*lnwire.ChannelAnnouncement,
*lnwire.NodeAnnouncement,
*lnwire.AnnounceSignatures,
*lnwire.GossipTimestampRange,
*lnwire.QueryShortChanIDs,
*lnwire.QueryChannelRange,
*lnwire.ReplyChannelRange,
*lnwire.ReplyShortChanIDsEnd:
discStream.AddMsg(msg)
default:
peerLog.Errorf("unknown message %v received from peer "+
"%v", uint16(msg.MsgType()), p)
}
今受け取ったメッセージはOpenChannelメッセージなのでそのnextMsg(次のメッセージ)であるprocessFundingAccept()
を呼び出します。fundingmanager.goの1399行目に
func (f *fundingManager) processFundingAccept(msg *lnwire.AcceptChannel,
peer lnpeer.Peer) {
select {
case f.fundingMsgs <- &fundingAcceptMsg{msg, peer}:
case <-f.quit:
return
}
}
ここでlnwireのAcceptChannelを呼び出しメッセージをフォーマッティングし送信します。
通知者側はAcceptChannelのnextMsgであるprocessFundingCreated()
呼び出されます。
さらに並行処理で常に走っているreservationCoordinator()が受け取ったメッセージを判断し、handleFundingCreated()
を呼び出します。
handleFundingCreated()
はfundingmanager.goの735行目です。
func (f *fundingManager) handleFundingCreated(fmsg *fundingCreatedMsg) {
peerKey := fmsg.peer.IdentityKey()
pendingChanID := fmsg.msg.PendingChannelID
resCtx, err := f.getReservationCtx(peerKey, pendingChanID)
if err != nil {
fndgLog.Warnf("can't find reservation (peerID:%v, chanID:%x)",
peerKey, pendingChanID[:])
return
}
funding_signedメッセージは上にも書いた通り最初のコミットメントトランザクションに必要な署名を与えます。funding_signedメッセージは作成時、ファンディングノードが資金を取り戻せるようなトランザクションをブロードキャストする処理があります。
handleFundingSigned()
が呼び出されます。
終わりに
今回のコードリィーディングは開設からクローズに関連する部分だけを掻い摘んだのでいずれ精読してそれ以外の部分も把握したい。