contractcourt/chain_watcher: handleKnownRemoteState

Similar to what we did for the local state handling, we extract handling
all known remote states we can act on (breach, current, pending state)
into its own method.

Since we want to handle the case where we lost state (both in case of
local and remote close) last, we don't rely on the remote state number
to check which commit we are looking at, but match on TXIDs directly.
This commit is contained in:
Johan T. Halseth 2020-11-18 22:45:35 +01:00
parent acc45934f8
commit 743ea7be74
No known key found for this signature in database
GPG Key ID: 15BAADA29DA20D26
3 changed files with 131 additions and 77 deletions

@ -163,9 +163,9 @@ var (
// channel.
ErrChanBorked = fmt.Errorf("cannot mutate borked channel")
// errLogEntryNotFound is returned when we cannot find a log entry at
// ErrLogEntryNotFound is returned when we cannot find a log entry at
// the height requested in the revocation log.
errLogEntryNotFound = fmt.Errorf("log entry not found")
ErrLogEntryNotFound = fmt.Errorf("log entry not found")
// errHeightNotFound is returned when a query for channel balances at
// a height that we have not reached yet is made.
@ -3469,7 +3469,7 @@ func fetchChannelLogEntry(log kvdb.RBucket,
logEntrykey := makeLogKey(updateNum)
commitBytes := log.Get(logEntrykey[:])
if commitBytes == nil {
return ChannelCommitment{}, errLogEntryNotFound
return ChannelCommitment{}, ErrLogEntryNotFound
}
commitReader := bytes.NewReader(commitBytes)

@ -1485,7 +1485,7 @@ func TestBalanceAtHeight(t *testing.T) {
targetHeight: unknownHeight,
expectedLocalBalance: 0,
expectedRemoteBalance: 0,
expectedError: errLogEntryNotFound,
expectedError: ErrLogEntryNotFound,
},
{
name: "height not reached",

@ -546,6 +546,22 @@ func (c *chainWatcher) closeObserver(spendNtfn *chainntnfs.SpendEvent) {
return
}
// Now that we know it is neither a non-cooperative closure nor
// a local close with the latest state, we check if it is the
// remote that closed with any prior or current state.
ok, err = c.handleKnownRemoteState(
commitSpend, broadcastStateNum, chainSet,
)
if err != nil {
log.Errorf("Unable to handle known remote state: %v",
err)
return
}
if ok {
return
}
// Based on the output scripts within this commitment, we'll
// determine if this is our commitment transaction or not (a
// self force close).
@ -607,51 +623,6 @@ func (c *chainWatcher) closeObserver(spendNtfn *chainntnfs.SpendEvent) {
)
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.
case broadcastStateNum == chainSet.remoteStateNum &&
!isRecoveredChan:
log.Infof("Remote party broadcast base set, "+
"commit_num=%v", chainSet.remoteStateNum)
chainSet.commitSet.ConfCommitKey = &RemoteHtlcSet
err := c.dispatchRemoteForceClose(
commitSpend, chainSet.remoteCommit,
chainSet.commitSet,
c.cfg.chanState.RemoteCurrentRevocation,
)
if err != nil {
log.Errorf("unable to handle remote "+
"close for chan_point=%v: %v",
c.cfg.chanState.FundingOutpoint, err)
}
// 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 == chainSet.remoteStateNum+1 &&
chainSet.remotePendingCommit != nil && !isRecoveredChan:
log.Infof("Remote party broadcast pending set, "+
"commit_num=%v", chainSet.remoteStateNum+1)
chainSet.commitSet.ConfCommitKey = &RemotePendingHtlcSet
err := c.dispatchRemoteForceClose(
commitSpend, *chainSet.remotePendingCommit,
chainSet.commitSet,
c.cfg.chanState.RemoteNextRevocation,
)
if err != nil {
log.Errorf("unable to handle remote "+
"close for chan_point=%v: %v",
c.cfg.chanState.FundingOutpoint, err)
}
// If the remote party has broadcasted a state beyond our best
// known state for them, and they don't have a pending
// commitment (we write them to disk before sending out), then
@ -710,21 +681,6 @@ func (c *chainWatcher) closeObserver(spendNtfn *chainntnfs.SpendEvent) {
c.cfg.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 < chainSet.remoteStateNum:
err := c.dispatchContractBreach(
commitSpend, &chainSet.remoteCommit,
broadcastStateNum,
)
if err != nil {
log.Errorf("unable to handle channel "+
"breach for chan_point=%v: %v",
c.cfg.chanState.FundingOutpoint, err)
}
}
// Now that a spend has been detected, we've done our job, so
@ -769,6 +725,115 @@ func (c *chainWatcher) handleKnownLocalState(
return true, nil
}
// handleKnownRemoteState checks whether the passed spend is a remote state
// that is known to us (a revoked, current or pending state). If so we will act
// on this state using the passed chainSet. If this is not a known remote
// state, false is returned.
func (c *chainWatcher) handleKnownRemoteState(
commitSpend *chainntnfs.SpendDetail, broadcastStateNum uint64,
chainSet *chainSet) (bool, error) {
// If the channel is recovered, we won't have any remote commit to
// check against, so imemdiately return.
if c.cfg.chanState.HasChanStatus(channeldb.ChanStatusRestored) {
return false, nil
}
commitTxBroadcast := commitSpend.SpendingTx
commitHash := commitTxBroadcast.TxHash()
spendHeight := uint32(commitSpend.SpendingHeight)
switch {
// If the 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.
case chainSet.remoteCommit.CommitTx.TxHash() == commitHash:
log.Infof("Remote party broadcast base set, "+
"commit_num=%v", chainSet.remoteStateNum)
chainSet.commitSet.ConfCommitKey = &RemoteHtlcSet
err := c.dispatchRemoteForceClose(
commitSpend, chainSet.remoteCommit,
chainSet.commitSet,
c.cfg.chanState.RemoteCurrentRevocation,
)
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
// 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 chainSet.remotePendingCommit != nil &&
chainSet.remotePendingCommit.CommitTx.TxHash() == commitHash:
log.Infof("Remote party broadcast pending set, "+
"commit_num=%v", chainSet.remoteStateNum+1)
chainSet.commitSet.ConfCommitKey = &RemotePendingHtlcSet
err := c.dispatchRemoteForceClose(
commitSpend, *chainSet.remotePendingCommit,
chainSet.commitSet,
c.cfg.chanState.RemoteNextRevocation,
)
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
}
// We check if we have a revoked state at this state num that matches
// the spend transaction.
retribution, err := lnwallet.NewBreachRetribution(
c.cfg.chanState, broadcastStateNum, spendHeight,
)
switch {
// If we had no log entry at this height, this was not a revoked state.
case err == channeldb.ErrLogEntryNotFound:
return false, nil
case err == channeldb.ErrNoPastDeltas:
return false, nil
case err != nil:
return false, fmt.Errorf("unable to create breach "+
"retribution: %v", err)
}
// We found a revoked state at this height, but it could still be our
// own broadcasted state we are looking at. Therefore check that the
// commit matches before assuming it was a breach.
if retribution.BreachTransaction.TxHash() != commitHash {
return false, nil
}
// 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!!!
err = c.dispatchContractBreach(
commitSpend, &chainSet.remoteCommit,
broadcastStateNum, retribution,
)
if err != nil {
return false, fmt.Errorf("unable to handle channel "+
"breach 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
// 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
@ -993,8 +1058,8 @@ func (c *chainWatcher) dispatchRemoteForceClose(
// 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 {
remoteCommit *channeldb.ChannelCommitment, broadcastStateNum uint64,
retribution *lnwallet.BreachRetribution) error {
log.Warnf("Remote peer has breached the channel contract for "+
"ChannelPoint(%v). Revoked state #%v was broadcast!!!",
@ -1006,17 +1071,6 @@ func (c *chainWatcher) dispatchContractBreach(spendEvent *chainntnfs.SpendDetail
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.cfg.chanState, broadcastStateNum, 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 {