lnd.xprv/contractcourt/chain_watcher.go
Johan T. Halseth 854e73b8e0
contractcourt/chain_watcher: register for confirmed spend, mark closed when detected
This commit changes the ChainWatcher to only send a chain event in case
the various spends are _confirmed_ on-chain, not only seen on the
network.

A consequence of this is that we now give the ChainWatcher the
responsibility of marking the channel closed when the closing tx is
confirmed, instead of the ChannelArbitrator.
2018-04-25 09:37:24 +02:00

884 lines
29 KiB
Go

package contractcourt
import (
"fmt"
"sync"
"sync/atomic"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/roasbeef/btcd/chaincfg"
"github.com/roasbeef/btcd/chaincfg/chainhash"
"github.com/roasbeef/btcd/txscript"
"github.com/roasbeef/btcd/wire"
"github.com/roasbeef/btcutil"
)
// LocalUnilateralCloseInfo encapsulates all the informnation we need to act
// on a local force close that gets confirmed.
type LocalUnilateralCloseInfo struct {
*chainntnfs.SpendDetail
*lnwallet.LocalForceCloseSummary
}
// ChainEventSubscription is a struct that houses a subscription to be notified
// for any on-chain events related to a channel. There are three types of
// possible on-chain events: a cooperative channel closure, a unilateral
// channel closure, and a channel breach. The fourth type: a force close is
// locally initiated, so we don't provide any event stream for said event.
type ChainEventSubscription struct {
// ChanPoint is that channel that chain events will be dispatched for.
ChanPoint wire.OutPoint
// RemoteUnilateralClosure is a channel that will be sent upon in the
// event that the remote party's commitment transaction is confirmed.
RemoteUnilateralClosure chan *lnwallet.UnilateralCloseSummary
// LocalUnilateralClosure is a channel that will be sent upon in the
// event that our commitment transaction is confirmed.
LocalUnilateralClosure chan *LocalUnilateralCloseInfo
// CooperativeClosure is a signal that will be sent upon once a
// cooperative channel closure has been detected confirmed.
//
// TODO(roasbeef): or something else
CooperativeClosure chan struct{}
// ContractBreach is a channel that will be sent upon if we detect a
// contract breach. The struct sent across the channel contains all the
// material required to bring the cheating channel peer to justice.
ContractBreach chan *lnwallet.BreachRetribution
// ProcessACK is a channel that will be used by the chainWatcher to
// synchronize dispatch and processing of the notification with the act
// of updating the state of the channel on disk. This ensures that the
// event can be reliably handed off.
//
// NOTE: This channel will only be used if the syncDispatch arg passed
// into the constructor is true.
ProcessACK chan error
// Cancel cancels the subscription to the event stream for a particular
// channel. This method should be called once the caller no longer needs to
// be notified of any on-chain events for a particular channel.
Cancel func()
}
// chainWatcher is a system that's assigned to every active channel. The duty
// of this system is to watch the chain for spends of the channels chan point.
// If a spend is detected then with chain watcher will notify all subscribers
// that the channel has been closed, and also give them the materials necessary
// to sweep the funds of the channel on chain eventually.
type chainWatcher struct {
started int32
stopped int32
quit chan struct{}
wg sync.WaitGroup
// chanState is a snapshot of the persistent state of the channel that
// we're watching. In the event of an on-chain event, we'll query the
// database to ensure that we act using the most up to date state.
chanState *channeldb.OpenChannel
// stateHintObfuscator is a 48-bit state hint that's used to obfuscate
// the current state number on the commitment transactions.
stateHintObfuscator [lnwallet.StateHintSize]byte
// notifier is a reference to the channel notifier that we'll use to be
// notified of output spends and when transactions are confirmed.
notifier chainntnfs.ChainNotifier
// pCache is a reference to the shared preimage cache. We'll use this
// to see if we can settle any incoming HTLC's during a remote
// commitment close event.
pCache WitnessBeacon
// signer is the main signer instances that will be responsible for
// signing any HTLC and commitment transaction generated by the state
// machine.
signer lnwallet.Signer
// All the fields below are protected by this mutex.
sync.Mutex
// clientID is an ephemeral counter used to keep track of each
// individual client subscription.
clientID uint64
// clientSubscriptions is a map that keeps track of all the active
// client subscriptions for events related to this channel.
clientSubscriptions map[uint64]*ChainEventSubscription
// possibleCloses is a map from cooperative closing transaction txid to
// a close summary that describes the nature of the channel closure.
// We'll use this map to keep track of all possible channel closures to
// ensure out db state is correct in the end.
possibleCloses map[chainhash.Hash]*channeldb.ChannelCloseSummary
// markChanClosed is a method that will be called by the watcher if it
// detects that a cooperative closure transaction has successfully been
// confirmed.
markChanClosed func() error
// isOurAddr is a function that returns true if the passed address is
// known to us.
isOurAddr func(btcutil.Address) bool
}
// newChainWatcher returns a new instance of a chainWatcher for a channel given
// the chan point to watch, and also a notifier instance that will allow us to
// detect on chain events.
func newChainWatcher(chanState *channeldb.OpenChannel,
notifier chainntnfs.ChainNotifier, pCache WitnessBeacon,
signer lnwallet.Signer, isOurAddr func(btcutil.Address) bool,
markChanClosed func() error) (*chainWatcher, error) {
// In order to be able to detect the nature of a potential channel
// closure we'll need to reconstruct the state hint bytes used to
// obfuscate the commitment state number encoded in the lock time and
// sequence fields.
var stateHint [lnwallet.StateHintSize]byte
if chanState.IsInitiator {
stateHint = lnwallet.DeriveStateHintObfuscator(
chanState.LocalChanCfg.PaymentBasePoint.PubKey,
chanState.RemoteChanCfg.PaymentBasePoint.PubKey,
)
} else {
stateHint = lnwallet.DeriveStateHintObfuscator(
chanState.RemoteChanCfg.PaymentBasePoint.PubKey,
chanState.LocalChanCfg.PaymentBasePoint.PubKey,
)
}
return &chainWatcher{
chanState: chanState,
stateHintObfuscator: stateHint,
notifier: notifier,
pCache: pCache,
markChanClosed: markChanClosed,
signer: signer,
quit: make(chan struct{}),
clientSubscriptions: make(map[uint64]*ChainEventSubscription),
isOurAddr: isOurAddr,
possibleCloses: make(map[chainhash.Hash]*channeldb.ChannelCloseSummary),
}, nil
}
// Start starts all goroutines that the chainWatcher needs to perform its
// duties.
func (c *chainWatcher) Start() error {
if !atomic.CompareAndSwapInt32(&c.started, 0, 1) {
return nil
}
log.Debugf("Starting chain watcher for ChannelPoint(%v)",
c.chanState.FundingOutpoint)
// First, we'll register for a notification to be dispatched if the
// funding output is spent.
fundingOut := &c.chanState.FundingOutpoint
// As a height hint, we'll try to use the opening height, but if the
// channel isn't yet open, then we'll use the height it was broadcast
// at.
heightHint := c.chanState.ShortChanID.BlockHeight
if heightHint == 0 {
heightHint = c.chanState.FundingBroadcastHeight
}
spendNtfn, err := c.notifier.RegisterSpendNtfn(
fundingOut, heightHint, false,
)
if err != nil {
return err
}
// With the spend notification obtained, we'll now dispatch the
// closeObserver which will properly react to any changes.
c.wg.Add(1)
go c.closeObserver(spendNtfn)
return nil
}
// Stop signals the close observer to gracefully exit.
func (c *chainWatcher) Stop() error {
if !atomic.CompareAndSwapInt32(&c.stopped, 0, 1) {
return nil
}
close(c.quit)
c.wg.Wait()
return nil
}
// SubscribeChannelEvents returns an active subscription to the set of channel
// events for the channel watched by this chain watcher. Once clients no longer
// require the subscription, they should call the Cancel() method to allow the
// watcher to regain those committed resources. The syncDispatch bool indicates
// if the caller would like a synchronous dispatch of the notification. This
// means that the main chain watcher goroutine won't proceed with
// post-processing after the notification until the ProcessACK channel is sent
// upon.
func (c *chainWatcher) SubscribeChannelEvents(syncDispatch bool) *ChainEventSubscription {
c.Lock()
clientID := c.clientID
c.clientID++
c.Unlock()
log.Debugf("New ChainEventSubscription(id=%v) for ChannelPoint(%v)",
clientID, c.chanState.FundingOutpoint)
sub := &ChainEventSubscription{
ChanPoint: c.chanState.FundingOutpoint,
RemoteUnilateralClosure: make(chan *lnwallet.UnilateralCloseSummary, 1),
LocalUnilateralClosure: make(chan *LocalUnilateralCloseInfo, 1),
CooperativeClosure: make(chan struct{}, 1),
ContractBreach: make(chan *lnwallet.BreachRetribution, 1),
Cancel: func() {
c.Lock()
delete(c.clientSubscriptions, clientID)
c.Unlock()
return
},
}
if syncDispatch {
sub.ProcessACK = make(chan error, 1)
}
c.Lock()
c.clientSubscriptions[clientID] = sub
c.Unlock()
return sub
}
// closeObserver is a dedicated goroutine that will watch for any closes of the
// channel that it's watching on chain. In the event of an on-chain event, the
// close observer will assembled the proper materials required to claim the
// funds of the channel on-chain (if required), then dispatch these as
// notifications to all subscribers.
func (c *chainWatcher) closeObserver(spendNtfn *chainntnfs.SpendEvent) {
defer c.wg.Done()
log.Infof("Close observer for ChannelPoint(%v) active",
c.chanState.FundingOutpoint)
for {
select {
// We've detected a spend of the channel onchain! Depending on
// the type of spend, we'll act accordingly , so we'll examine
// the spending transaction to determine what we should do.
//
// TODO(Roasbeef): need to be able to ensure this only triggers
// on confirmation, to ensure if multiple txns are broadcast, we
// act on the one that's timestamped
case commitSpend, ok := <-spendNtfn.Spend:
// If the channel was closed, then this means that the
// notifier exited, so we will as well.
if !ok {
return
}
// Otherwise, the remote party might have broadcast a
// prior revoked state...!!!
commitTxBroadcast := commitSpend.SpendingTx
localCommit, remoteCommit, err := c.chanState.LatestCommitments()
if err != nil {
log.Errorf("Unable to fetch channel state for "+
"chan_point=%v", c.chanState.FundingOutpoint)
return
}
// We'll not retrieve the latest sate of the revocation
// store so we can populate the information within the
// channel state object that we have.
//
// TODO(roasbeef): mutation is bad mkay
_, err = c.chanState.RemoteRevocationStore()
if err != nil {
log.Errorf("Unable to fetch revocation state for "+
"chan_point=%v", c.chanState.FundingOutpoint)
return
}
// If this is our commitment transaction, then we can
// exit here as we don't have any further processing we
// need to do (we can't cheat ourselves :p).
commitmentHash := localCommit.CommitTx.TxHash()
isOurCommitment := commitSpend.SpenderTxHash.IsEqual(
&commitmentHash,
)
if isOurCommitment {
if err := c.dispatchLocalForceClose(
commitSpend, *localCommit,
); err != nil {
log.Errorf("unable to handle local"+
"close for chan_point=%v: %v",
c.chanState.FundingOutpoint, err)
}
return
}
// Next, we'll check to see if this is a cooperative
// channel closure or not. This is characterized by
// having an input sequence number that's finalized.
// This won't happen with regular commitment
// transactions due to the state hint encoding scheme.
if commitTxBroadcast.TxIn[0].Sequence == wire.MaxTxInSequenceNum {
err := c.dispatchCooperativeClose(commitSpend)
if err != nil {
log.Errorf("unable to handle co op close: %v", err)
}
return
}
log.Warnf("Unprompted commitment broadcast for "+
"ChannelPoint(%v) ", c.chanState.FundingOutpoint)
// Decode the state hint encoded within the commitment
// transaction to determine if this is a revoked state
// or not.
obfuscator := c.stateHintObfuscator
broadcastStateNum := lnwallet.GetStateNumHint(
commitTxBroadcast, obfuscator,
)
remoteStateNum := remoteCommit.CommitHeight
switch {
// If state number spending transaction matches the
// current latest state, then they've initiated a
// unilateral close. So we'll trigger the unilateral
// close signal so subscribers can clean up the state
// as necessary.
//
// We'll also handle the case of the remote party
// broadcasting their commitment transaction which is
// one height above ours. This case can arise when we
// initiate a state transition, but the remote party
// has a fail crash _after_ accepting the new state,
// but _before_ sending their signature to us.
case broadcastStateNum >= remoteStateNum:
if err := c.dispatchRemoteForceClose(
commitSpend, *remoteCommit,
); err != nil {
log.Errorf("unable to handle remote "+
"close for chan_point=%v: %v",
c.chanState.FundingOutpoint, err)
}
// If the state number broadcast is lower than the
// remote node's current un-revoked height, then
// THEY'RE ATTEMPTING TO VIOLATE THE CONTRACT LAID OUT
// WITHIN THE PAYMENT CHANNEL. Therefore we close the
// signal indicating a revoked broadcast to allow
// subscribers to
// swiftly dispatch justice!!!
case broadcastStateNum < remoteStateNum:
if err := c.dispatchContractBreach(
commitSpend, remoteCommit,
broadcastStateNum,
); err != nil {
log.Errorf("unable to handle channel "+
"breach for chan_point=%v: %v",
c.chanState.FundingOutpoint, err)
}
}
// Now that a spend has been detected, we've done our
// job, so we'll exit immediately.
return
// The chainWatcher has been signalled to exit, so we'll do so now.
case <-c.quit:
return
}
}
}
// toSelfAmount takes a transaction and returns the sum of all outputs that pay
// to a script that the wallet controls. If no outputs pay to us, then we
// return zero. This is possible as our output may have been trimmed due to
// being dust.
func (c *chainWatcher) toSelfAmount(tx *wire.MsgTx) btcutil.Amount {
var selfAmt btcutil.Amount
for _, txOut := range tx.TxOut {
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
// Doesn't matter what net we actually pass in.
txOut.PkScript, &chaincfg.TestNet3Params,
)
if err != nil {
continue
}
for _, addr := range addrs {
if c.isOurAddr(addr) {
selfAmt += btcutil.Amount(txOut.Value)
}
}
}
return selfAmt
}
// dispatchCooperativeClose processed a detect cooperative channel closure.
// We'll use the spending transaction to locate our output within the
// transaction, then clean up the database state. We'll also dispatch a
// notification to all subscribers that the channel has been closed in this
// manner.
func (c *chainWatcher) dispatchCooperativeClose(commitSpend *chainntnfs.SpendDetail) error {
broadcastTx := commitSpend.SpendingTx
log.Infof("Cooperative closure for ChannelPoint(%v): %v",
c.chanState.FundingOutpoint, spew.Sdump(broadcastTx))
// If the input *is* final, then we'll check to see which output is
// ours.
localAmt := c.toSelfAmount(broadcastTx)
// Once this is known, we'll mark the state as pending close in the
// database.
closeSummary := &channeldb.ChannelCloseSummary{
ChanPoint: c.chanState.FundingOutpoint,
ChainHash: c.chanState.ChainHash,
ClosingTXID: *commitSpend.SpenderTxHash,
RemotePub: c.chanState.IdentityPub,
Capacity: c.chanState.Capacity,
CloseHeight: uint32(commitSpend.SpendingHeight),
SettledBalance: localAmt,
CloseType: channeldb.CooperativeClose,
ShortChanID: c.chanState.ShortChanID,
IsPending: true,
}
err := c.chanState.CloseChannel(closeSummary)
if err != nil && err != channeldb.ErrNoActiveChannels &&
err != channeldb.ErrNoChanDBExists {
return fmt.Errorf("unable to close chan state: %v", err)
}
// Finally, we'll launch a goroutine to mark the channel as fully
// closed once the transaction confirmed.
go func() {
confNtfn, err := c.notifier.RegisterConfirmationsNtfn(
commitSpend.SpenderTxHash, 1,
uint32(commitSpend.SpendingHeight),
)
if err != nil {
log.Errorf("unable to register for conf: %v", err)
return
}
log.Infof("closeObserver: waiting for txid=%v to close "+
"ChannelPoint(%v) on chain", commitSpend.SpenderTxHash,
c.chanState.FundingOutpoint)
select {
case confInfo, ok := <-confNtfn.Confirmed:
if !ok {
log.Errorf("notifier exiting")
return
}
log.Infof("closeObserver: ChannelPoint(%v) is fully "+
"closed, at height: %v", c.chanState.FundingOutpoint,
confInfo.BlockHeight)
err := c.markChanClosed()
if err != nil {
log.Errorf("unable to mark chan fully "+
"closed: %v", err)
return
}
case <-c.quit:
return
}
}()
c.Lock()
for _, sub := range c.clientSubscriptions {
select {
case sub.CooperativeClosure <- struct{}{}:
case <-c.quit:
c.Unlock()
return fmt.Errorf("exiting")
}
}
c.Unlock()
return nil
}
// dispatchLocalForceClose processes a unilateral close by us being confirmed.
func (c *chainWatcher) dispatchLocalForceClose(
commitSpend *chainntnfs.SpendDetail,
localCommit channeldb.ChannelCommitment) error {
log.Infof("Local unilateral close of ChannelPoint(%v) "+
"detected", c.chanState.FundingOutpoint)
forceClose, err := lnwallet.NewLocalForceCloseSummary(
c.chanState, c.signer, c.pCache, commitSpend.SpendingTx,
localCommit,
)
if err != nil {
return err
}
// As we've detected that the channel has been closed, immediately
// delete the state from disk, creating a close summary for future
// usage by related sub-systems.
chanSnapshot := forceClose.ChanSnapshot
closeSummary := &channeldb.ChannelCloseSummary{
ChanPoint: chanSnapshot.ChannelPoint,
ChainHash: chanSnapshot.ChainHash,
ClosingTXID: forceClose.CloseTx.TxHash(),
RemotePub: &chanSnapshot.RemoteIdentity,
Capacity: chanSnapshot.Capacity,
CloseType: channeldb.LocalForceClose,
IsPending: true,
ShortChanID: c.chanState.ShortChanID,
CloseHeight: uint32(commitSpend.SpendingHeight),
}
// If our commitment output isn't dust or we have active HTLC's on the
// commitment transaction, then we'll populate the balances on the
// close channel summary.
if forceClose.CommitResolution != nil {
closeSummary.SettledBalance = chanSnapshot.LocalBalance.ToSatoshis()
closeSummary.TimeLockedBalance = chanSnapshot.LocalBalance.ToSatoshis()
}
for _, htlc := range forceClose.HtlcResolutions.OutgoingHTLCs {
htlcValue := btcutil.Amount(htlc.SweepSignDesc.Output.Value)
closeSummary.TimeLockedBalance += htlcValue
}
err = c.chanState.CloseChannel(closeSummary)
if err != nil {
return fmt.Errorf("unable to delete channel state: %v", err)
}
// With the event processed, we'll now notify all subscribers of the
// event.
closeInfo := &LocalUnilateralCloseInfo{commitSpend, forceClose}
c.Lock()
for _, sub := range c.clientSubscriptions {
select {
case sub.LocalUnilateralClosure <- closeInfo:
case <-c.quit:
c.Unlock()
return fmt.Errorf("exiting")
}
}
c.Unlock()
return nil
}
// dispatchRemoteForceClose processes a detected unilateral channel closure by the
// remote party. This function will prepare a UnilateralCloseSummary which will
// then be sent to any subscribers allowing them to resolve all our funds in
// the channel on chain. Once this close summary is prepared, all registered
// subscribers will receive a notification of this event.
func (c *chainWatcher) dispatchRemoteForceClose(commitSpend *chainntnfs.SpendDetail,
remoteCommit channeldb.ChannelCommitment) error {
log.Infof("Unilateral close of ChannelPoint(%v) "+
"detected", c.chanState.FundingOutpoint)
// First, we'll create a closure summary that contains all the
// materials required to let each subscriber sweep the funds in the
// channel on-chain.
uniClose, err := lnwallet.NewUnilateralCloseSummary(c.chanState,
c.signer, c.pCache, commitSpend, remoteCommit,
)
if err != nil {
return err
}
// As we've detected that the channel has been closed, immediately
// delete the state from disk, creating a close summary for future
// usage by related sub-systems.
err = c.chanState.CloseChannel(&uniClose.ChannelCloseSummary)
if err != nil {
return fmt.Errorf("unable to delete channel state: %v", err)
}
// With the event processed, we'll now notify all subscribers of the
// event.
c.Lock()
for _, sub := range c.clientSubscriptions {
// TODO(roasbeef): send msg before writing to disk
// * need to ensure proper fault tolerance in all cases
// * get ACK from the consumer of the ntfn before writing to disk?
// * no harm in repeated ntfns: at least once semantics
select {
case sub.RemoteUnilateralClosure <- uniClose:
case <-c.quit:
c.Unlock()
return fmt.Errorf("exiting")
}
}
c.Unlock()
return nil
}
// dispatchContractBreach processes a detected contract breached by the remote
// party. This method is to be called once we detect that the remote party has
// broadcast a prior revoked commitment state. This method well prepare all the
// materials required to bring the cheater to justice, then notify all
// registered subscribers of this event.
func (c *chainWatcher) dispatchContractBreach(spendEvent *chainntnfs.SpendDetail,
remoteCommit *channeldb.ChannelCommitment,
broadcastStateNum uint64) error {
log.Warnf("Remote peer has breached the channel contract for "+
"ChannelPoint(%v). Revoked state #%v was broadcast!!!",
c.chanState.FundingOutpoint, broadcastStateNum)
if err := c.chanState.MarkBorked(); err != nil {
return fmt.Errorf("unable to mark channel as borked: %v", err)
}
var (
commitTxBroadcast = spendEvent.SpendingTx
spendHeight = uint32(spendEvent.SpendingHeight)
)
// Create a new reach retribution struct which contains all the data
// needed to swiftly bring the cheating peer to justice.
//
// TODO(roasbeef): move to same package
retribution, err := lnwallet.NewBreachRetribution(
c.chanState, broadcastStateNum, commitTxBroadcast,
spendHeight,
)
if err != nil {
return fmt.Errorf("unable to create breach retribution: %v", err)
}
// Nil the curve before printing.
if retribution.RemoteOutputSignDesc != nil &&
retribution.RemoteOutputSignDesc.DoubleTweak != nil {
retribution.RemoteOutputSignDesc.DoubleTweak.Curve = nil
}
if retribution.LocalOutputSignDesc != nil &&
retribution.LocalOutputSignDesc.DoubleTweak != nil {
retribution.LocalOutputSignDesc.DoubleTweak.Curve = nil
}
log.Debugf("Punishment breach retribution created: %v",
newLogClosure(func() string {
return spew.Sdump(retribution)
}))
// With the event processed, we'll now notify all subscribers of the
// event.
c.Lock()
for _, sub := range c.clientSubscriptions {
select {
case sub.ContractBreach <- retribution:
case <-c.quit:
c.Unlock()
return fmt.Errorf("quitting")
}
// Wait for the breach arbiter to ACK the handoff before
// marking the channel as pending force closed in channeldb,
// but only if the client requested a sync dispatch.
if sub.ProcessACK != nil {
select {
case err := <-sub.ProcessACK:
// Bail if the handoff failed.
if err != nil {
c.Unlock()
return fmt.Errorf("unable to handoff "+
"retribution info: %v", err)
}
case <-c.quit:
c.Unlock()
return fmt.Errorf("quitting")
}
}
}
c.Unlock()
// At this point, we've successfully received an ack for the breach
// close. We now construct and persist the close summary, marking the
// channel as pending force closed.
//
// TODO(roasbeef): instead mark we got all the monies?
settledBalance := remoteCommit.LocalBalance.ToSatoshis()
closeSummary := channeldb.ChannelCloseSummary{
ChanPoint: c.chanState.FundingOutpoint,
ChainHash: c.chanState.ChainHash,
ClosingTXID: *spendEvent.SpenderTxHash,
CloseHeight: spendHeight,
RemotePub: c.chanState.IdentityPub,
Capacity: c.chanState.Capacity,
SettledBalance: settledBalance,
CloseType: channeldb.BreachClose,
IsPending: true,
ShortChanID: c.chanState.ShortChanID,
}
log.Infof("Breached channel=%v marked pending-closed",
c.chanState.FundingOutpoint)
return c.chanState.CloseChannel(&closeSummary)
}
// CooperativeCloseCtx is a transactional object that's used by external
// parties to initiate a cooperative closure negotiation. During the
// negotiation, we sign multiple versions of a closing transaction, either of
// which may be counter signed and broadcast by the remote party at any time.
// As a result, we'll need to watch the chain to see if any of these confirm,
// only afterwards will we mark the channel as fully closed.
type CooperativeCloseCtx struct {
// potentialCloses is a channel will be used by the party negotiating
// the cooperative closure to send possible closing states to the chain
// watcher to ensure we detect all on-chain spends.
potentialCloses chan *channeldb.ChannelCloseSummary
// activeCloses keeps track of all the txid's that we're currently
// watching for.
activeCloses map[chainhash.Hash]struct{}
// watchCancel will be closed once *one* of the txid's in the map above
// is confirmed. This will cause all the lingering goroutines to exit.
watchCancel chan struct{}
watcher *chainWatcher
sync.Mutex
}
// BeginCooperativeClose should be called by the party negotiating the
// cooperative closure before the first signature is sent to the remote party.
// This will return a context that should be used to communicate possible
// closing states so we can act on them.
func (c *chainWatcher) BeginCooperativeClose() *CooperativeCloseCtx {
// We'll simply return a new close context that will be used be the
// caller to notify us of potential closes.
return &CooperativeCloseCtx{
potentialCloses: make(chan *channeldb.ChannelCloseSummary),
watchCancel: make(chan struct{}),
activeCloses: make(map[chainhash.Hash]struct{}),
watcher: c,
}
}
// LogPotentialClose should be called by the party negotiating the cooperative
// closure once they signed a new state, but *before* they transmit it to the
// remote party. This will ensure that the chain watcher is able to log the new
// state it should watch the chain for.
func (c *CooperativeCloseCtx) LogPotentialClose(potentialClose *channeldb.ChannelCloseSummary) {
c.Lock()
defer c.Unlock()
// We'll check to see if we're already watching for a close of this
// channel, if so, then we'll exit early to avoid launching a duplicate
// goroutine.
if _, ok := c.activeCloses[potentialClose.ClosingTXID]; ok {
return
}
// Otherwise, we'll mark this txid as currently being watched.
c.activeCloses[potentialClose.ClosingTXID] = struct{}{}
// We'll take this potential close, and launch a goroutine which will
// wait until it's confirmed, then update the database state. When a
// potential close gets confirmed, we'll cancel out all other launched
// goroutines.
go func() {
confNtfn, err := c.watcher.notifier.RegisterConfirmationsNtfn(
&potentialClose.ClosingTXID, 1,
uint32(potentialClose.CloseHeight),
)
if err != nil {
log.Errorf("unable to register for conf: %v", err)
return
}
log.Infof("closeCtx: waiting for txid=%v to close "+
"ChannelPoint(%v) on chain", potentialClose.ClosingTXID,
c.watcher.chanState.FundingOutpoint)
select {
case confInfo, ok := <-confNtfn.Confirmed:
if !ok {
log.Errorf("notifier exiting")
return
}
log.Infof("closeCtx: ChannelPoint(%v) is fully closed, at "+
"height: %v", c.watcher.chanState.FundingOutpoint,
confInfo.BlockHeight)
close(c.watchCancel)
c.watcher.Lock()
for _, sub := range c.watcher.clientSubscriptions {
select {
case sub.CooperativeClosure <- struct{}{}:
case <-c.watcher.quit:
}
}
c.watcher.Unlock()
err := c.watcher.chanState.CloseChannel(potentialClose)
if err != nil {
log.Warnf("closeCtx: unable to update latest "+
"close for ChannelPoint(%v): %v",
c.watcher.chanState.FundingOutpoint, err)
}
err = c.watcher.markChanClosed()
if err != nil {
log.Errorf("closeCtx: unable to mark chan fully "+
"closed: %v", err)
return
}
case <-c.watchCancel:
log.Debugf("Exiting watch for close of txid=%v for "+
"ChannelPoint(%v)", potentialClose.ClosingTXID,
c.watcher.chanState.FundingOutpoint)
case <-c.watcher.quit:
return
}
}()
}
// Finalize should be called once both parties agree on a final transaction to
// close out the channel. This method will immediately mark the channel as
// pending closed in the database, then launch a goroutine to mark the channel
// fully closed upon confirmation.
func (c *CooperativeCloseCtx) Finalize(preferredClose *channeldb.ChannelCloseSummary) error {
chanPoint := c.watcher.chanState.FundingOutpoint
log.Infof("Finalizing chan close for ChannelPoint(%v)", chanPoint)
err := c.watcher.chanState.CloseChannel(preferredClose)
if err != nil {
log.Errorf("closeCtx: unable to close ChannelPoint(%v): %v",
chanPoint, err)
return err
}
go c.LogPotentialClose(preferredClose)
return nil
}