contractcourt/chain_watcher: handleUnknownRemoteState

This commit extracts the data loss protect recovery procedure into its
own method.
This commit is contained in:
Johan T. Halseth 2020-11-18 22:45:35 +01:00
parent 743ea7be74
commit 93d917d82a
No known key found for this signature in database
GPG Key ID: 15BAADA29DA20D26

@ -613,78 +613,25 @@ func (c *chainWatcher) closeObserver(spendNtfn *chainntnfs.SpendEvent) {
log.Warnf("Unprompted commitment broadcast for "+ log.Warnf("Unprompted commitment broadcast for "+
"ChannelPoint(%v) ", c.cfg.chanState.FundingOutpoint) "ChannelPoint(%v) ", c.cfg.chanState.FundingOutpoint)
// If this channel has been recovered, then we'll modify our // Since it was neither a known remote state, nor a local state
// behavior as it isn't possible for us to close out the // that was published, it most likely mean we lost state and
// channel off-chain ourselves. It can only be the remote party // the remote node closed. In this case we must start the DLP
// force closing, or a cooperative closure we signed off on // protocol in hope of getting our money back.
// before losing data getting confirmed in the chain. ok, err = c.handleUnknownRemoteState(
isRecoveredChan := c.cfg.chanState.HasChanStatus( commitSpend, broadcastStateNum, chainSet,
channeldb.ChanStatusRestored,
) )
if err != nil {
switch { log.Errorf("Unable to handle unknown remote state: %v",
// If the remote party has broadcasted a state beyond our best err)
// known state for them, and they don't have a pending return
// commitment (we write them to disk before sending out), then
// this means that we've lost data. In this case, we'll enter
// the DLP protocol. Otherwise, if we've recovered our channel
// state from scratch, then we don't know what the precise
// current state is, so we assume either the remote party
// forced closed or we've been breached. In the latter case,
// our tower will take care of us.
case broadcastStateNum > chainSet.remoteStateNum || isRecoveredChan:
log.Warnf("Remote node broadcast state #%v, "+
"which is more than 1 beyond best known "+
"state #%v!!! Attempting recovery...",
broadcastStateNum, chainSet.remoteStateNum)
// If this isn't a tweakless commitment, then we'll
// need to wait for the remote party's latest unrevoked
// commitment point to be presented to us as we need
// this to sweep. Otherwise, we can dispatch the remote
// close and sweep immediately using a fake commitPoint
// as it isn't actually needed for recovery anymore.
commitPoint := c.cfg.chanState.RemoteCurrentRevocation
tweaklessCommit := c.cfg.chanState.ChanType.IsTweakless()
if !tweaklessCommit {
commitPoint = c.waitForCommitmentPoint()
if commitPoint == nil {
return
}
log.Infof("Recovered commit point(%x) for "+
"channel(%v)! Now attempting to use it to "+
"sweep our funds...",
commitPoint.SerializeCompressed(),
c.cfg.chanState.FundingOutpoint)
} else {
log.Infof("ChannelPoint(%v) is tweakless, "+
"moving to sweep directly on chain",
c.cfg.chanState.FundingOutpoint)
}
// Since we don't have the commitment stored for this
// state, we'll just pass an empty commitment within
// the commitment set. Note that this means we won't be
// able to recover any HTLC funds.
//
// TODO(halseth): can we try to recover some HTLCs?
chainSet.commitSet.ConfCommitKey = &RemoteHtlcSet
err = c.dispatchRemoteForceClose(
commitSpend, channeldb.ChannelCommitment{},
chainSet.commitSet, commitPoint,
)
if err != nil {
log.Errorf("unable to handle remote "+
"close for chan_point=%v: %v",
c.cfg.chanState.FundingOutpoint, err)
}
} }
// Now that a spend has been detected, we've done our job, so if ok {
// we'll exit immediately. return
}
log.Warnf("Unable to handle spending tx %v of channel point %v",
commitTxBroadcast.TxHash(), c.cfg.chanState.FundingOutpoint)
return return
// The chainWatcher has been signalled to exit, so we'll do so now. // The chainWatcher has been signalled to exit, so we'll do so now.
@ -834,6 +781,64 @@ func (c *chainWatcher) handleKnownRemoteState(
return true, nil return true, nil
} }
// handleUnknownRemoteState is the last attempt we make at reclaiming funds
// from the closed channel, by checkin whether the passed spend _could_ be a
// remote spend that is unknown to us (we lost state). We will try to initiate
// Data Loss Protection in order to restore our commit point and reclaim our
// funds from the channel. If we are not able to act on it, false is returned.
func (c *chainWatcher) handleUnknownRemoteState(
commitSpend *chainntnfs.SpendDetail, broadcastStateNum uint64,
chainSet *chainSet) (bool, error) {
log.Warnf("Remote node broadcast state #%v, "+
"which is more than 1 beyond best known "+
"state #%v!!! Attempting recovery...",
broadcastStateNum, chainSet.remoteStateNum)
// If this isn't a tweakless commitment, then we'll need to wait for
// the remote party's latest unrevoked commitment point to be presented
// to us as we need this to sweep. Otherwise, we can dispatch the
// remote close and sweep immediately using a fake commitPoint as it
// isn't actually needed for recovery anymore.
commitPoint := c.cfg.chanState.RemoteCurrentRevocation
tweaklessCommit := c.cfg.chanState.ChanType.IsTweakless()
if !tweaklessCommit {
commitPoint = c.waitForCommitmentPoint()
if commitPoint == nil {
return false, fmt.Errorf("unable to get commit point")
}
log.Infof("Recovered commit point(%x) for "+
"channel(%v)! Now attempting to use it to "+
"sweep our funds...",
commitPoint.SerializeCompressed(),
c.cfg.chanState.FundingOutpoint)
} else {
log.Infof("ChannelPoint(%v) is tweakless, "+
"moving to sweep directly on chain",
c.cfg.chanState.FundingOutpoint)
}
// Since we don't have the commitment stored for this state, we'll just
// pass an empty commitment within the commitment set. Note that this
// means we won't be able to recover any HTLC funds.
//
// TODO(halseth): can we try to recover some HTLCs?
chainSet.commitSet.ConfCommitKey = &RemoteHtlcSet
err := c.dispatchRemoteForceClose(
commitSpend, channeldb.ChannelCommitment{},
chainSet.commitSet, commitPoint,
)
if err != nil {
return false, fmt.Errorf("unable to handle remote "+
"close for chan_point=%v: %v",
c.cfg.chanState.FundingOutpoint, err)
}
return true, nil
}
// toSelfAmount takes a transaction and returns the sum of all outputs that pay // 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 // 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 // return zero. This is possible as our output may have been trimmed due to