From b7407882ac1c57b2580d1f8d94821c5b1cad6adc Mon Sep 17 00:00:00 2001 From: eugene Date: Thu, 13 Jan 2022 14:55:55 -0500 Subject: [PATCH] contractcourt: create breachResolver if BreachResolution present Also transitions to the proper state based on if this is a legacy breach in the channel arbitrator or a modern breach with a resolver. --- contractcourt/briefcase.go | 14 +-- contractcourt/channel_arbitrator.go | 136 +++++++++++++++++++++++----- 2 files changed, 122 insertions(+), 28 deletions(-) diff --git a/contractcourt/briefcase.go b/contractcourt/briefcase.go index 61f2d2482..e6d5e4cf0 100644 --- a/contractcourt/briefcase.go +++ b/contractcourt/briefcase.go @@ -147,7 +147,7 @@ const ( // | | | // | | |-> StateCommitmentBroadcasted: chain/user trigger // | | | - // | | |-> StateContractClosed: local/remote close trigger + // | | |-> StateContractClosed: local/remote/breach close trigger // | | | | // | | | |-> StateWaitingFullResolution: contract resolutions not empty // | | | | | @@ -157,9 +157,9 @@ const ( // | | | | // | | | |-> StateFullyResolved: contract resolutions empty // | | | - // | | |-> StateFullyResolved: coop/breach close trigger + // | | |-> StateFullyResolved: coop/breach(legacy) close trigger // | | - // | |-> StateContractClosed: local/remote close trigger + // | |-> StateContractClosed: local/remote/breach close trigger // | | | // | | |-> StateWaitingFullResolution: contract resolutions not empty // | | | | @@ -169,11 +169,11 @@ const ( // | | | // | | |-> StateFullyResolved: contract resolutions empty // | | - // | |-> StateFullyResolved: coop/breach close trigger + // | |-> StateFullyResolved: coop/breach(legacy) close trigger // | - // |-> StateContractClosed: local/remote close trigger + // |-> StateContractClosed: local/remote/breach close trigger // | | - // | |-> StateWaitingFullResolution: contract resolutions empty + // | |-> StateWaitingFullResolution: contract resolutions not empty // | | | // | | |-> StateWaitingFullResolution: contract resolutions not empty // | | | @@ -181,7 +181,7 @@ const ( // | | // | |-> StateFullyResolved: contract resolutions empty // | - // |-> StateFullyResolved: coop/breach close trigger + // |-> StateFullyResolved: coop/breach(legacy) close trigger // StateDefault is the default state. In this state, no major actions // need to be executed. diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 944f4b736..4c02b346c 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -747,8 +747,8 @@ const ( coopCloseTrigger // breachCloseTrigger is a transition trigger driven by a remote breach - // being confirmed. In this case the channel arbitrator won't have to - // do anything, so we'll just clean up and exit gracefully. + // being confirmed. In this case the channel arbitrator will wait for + // the breacharbiter to finish and then clean up gracefully. breachCloseTrigger ) @@ -852,9 +852,8 @@ func (c *ChannelArbitrator) stateStep( // If the trigger is a cooperative close being confirmed, then // we can go straight to StateFullyResolved, as there won't be - // any contracts to resolve. The same is true in the case of a - // breach. - case coopCloseTrigger, breachCloseTrigger: + // any contracts to resolve. + case coopCloseTrigger: nextState = StateFullyResolved // Otherwise, if this state advance was triggered by a @@ -868,6 +867,14 @@ func (c *ChannelArbitrator) stateStep( fallthrough case remoteCloseTrigger: nextState = StateContractClosed + + case breachCloseTrigger: + nextContractState, err := c.checkLegacyBreach() + if nextContractState == StateError { + return nextContractState, nil, err + } + + nextState = nextContractState } // If we're in this state, then we've decided to broadcast the @@ -890,7 +897,23 @@ func (c *ChannelArbitrator) stateStep( c.cfg.ChanPoint, trigger, StateContractClosed) return StateContractClosed, closeTx, nil - case coopCloseTrigger, breachCloseTrigger: + case breachCloseTrigger: + nextContractState, err := c.checkLegacyBreach() + if nextContractState == StateError { + log.Infof("ChannelArbitrator(%v): unable to "+ + "advance breach close resolution: %v", + c.cfg.ChanPoint, nextContractState) + return StateError, closeTx, err + } + + log.Infof("ChannelArbitrator(%v): detected %s close "+ + "after closing channel, fast-forwarding to %s"+ + " to resolve contract", c.cfg.ChanPoint, + trigger, nextContractState) + + return nextContractState, closeTx, nil + + case coopCloseTrigger: log.Infof("ChannelArbitrator(%v): detected %s "+ "close after closing channel, fast-forwarding "+ "to %s to resolve contract", @@ -994,10 +1017,18 @@ func (c *ChannelArbitrator) stateStep( case localCloseTrigger, remoteCloseTrigger: nextState = StateContractClosed - // If a coop close or breach was confirmed, jump straight to - // the fully resolved state. - case coopCloseTrigger, breachCloseTrigger: + // If a coop close was confirmed, jump straight to the fully + // resolved state. + case coopCloseTrigger: nextState = StateFullyResolved + + case breachCloseTrigger: + nextContractState, err := c.checkLegacyBreach() + if nextContractState == StateError { + return nextContractState, closeTx, err + } + + nextState = nextContractState } log.Infof("ChannelArbitrator(%v): trigger %v moving from "+ @@ -1996,10 +2027,62 @@ func (c *ChannelArbitrator) prepContractResolutions( commitHash := contractResolutions.CommitHash failureMsg := &lnwire.FailPermanentChannelFailure{} + var htlcResolvers []ContractResolver + + // We instantiate an anchor resolver if the commitment tx has an + // anchor. + if contractResolutions.AnchorResolution != nil { + anchorResolver := newAnchorResolver( + contractResolutions.AnchorResolution.AnchorSignDescriptor, + contractResolutions.AnchorResolution.CommitAnchor, + height, c.cfg.ChanPoint, resolverCfg, + ) + htlcResolvers = append(htlcResolvers, anchorResolver) + } + + // If this is a breach close, we'll create a breach resolver, determine + // the htlc's to fail back, and exit. This is done because the other + // steps taken for non-breach-closes do not matter for breach-closes. + if contractResolutions.BreachResolution != nil { + breachResolver := newBreachResolver(resolverCfg) + htlcResolvers = append(htlcResolvers, breachResolver) + + // We'll use the CommitSet, we'll fail back all outgoing HTLC's + // that exist on either of the remote commitments. The map is + // used to deduplicate any shared htlc's. + remoteOutgoing := make(map[uint64]channeldb.HTLC) + for htlcSetKey, htlcs := range confCommitSet.HtlcSets { + if !htlcSetKey.IsRemote { + continue + } + + for _, htlc := range htlcs { + if htlc.Incoming { + continue + } + + remoteOutgoing[htlc.HtlcIndex] = htlc + } + } + + // Now we'll loop over the map and create ResolutionMsgs for + // each of them. + for _, htlc := range remoteOutgoing { + failMsg := ResolutionMsg{ + SourceChan: c.cfg.ShortChanID, + HtlcIndex: htlc.HtlcIndex, + Failure: failureMsg, + } + + msgsToSend = append(msgsToSend, failMsg) + } + + return htlcResolvers, msgsToSend, nil + } + // For each HTLC, we'll either act immediately, meaning we'll instantly // fail the HTLC, or we'll act only once the transaction has been // confirmed, in which case we'll need an HTLC resolver. - var htlcResolvers []ContractResolver for htlcAction, htlcs := range htlcActions { switch htlcAction { @@ -2145,17 +2228,6 @@ func (c *ChannelArbitrator) prepContractResolutions( htlcResolvers = append(htlcResolvers, resolver) } - // We instantiate an anchor resolver if the commitmentment tx has an - // anchor. - if contractResolutions.AnchorResolution != nil { - anchorResolver := newAnchorResolver( - contractResolutions.AnchorResolution.AnchorSignDescriptor, - contractResolutions.AnchorResolution.CommitAnchor, - height, c.cfg.ChanPoint, resolverCfg, - ) - htlcResolvers = append(htlcResolvers, anchorResolver) - } - return htlcResolvers, msgsToSend, nil } @@ -2661,3 +2733,25 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) { } } } + +// checkLegacyBreach returns StateFullyResolved if the channel was closed with +// a breach transaction before the channel arbitrator launched its own breach +// resolver. StateContractClosed is returned if this is a modern breach close +// with a breach resolver. StateError is returned if the log lookup failed. +func (c *ChannelArbitrator) checkLegacyBreach() (ArbitratorState, error) { + // A previous version of the channel arbitrator would make the breach + // close skip to StateFullyResolved. If there are no contract + // resolutions in the bolt arbitrator log, then this is an older breach + // close. Otherwise, if there are resolutions, the state should advance + // to StateContractClosed. + _, err := c.log.FetchContractResolutions() + if err == errNoResolutions { + // This is an older breach close still in the database. + return StateFullyResolved, nil + } else if err != nil { + return StateError, err + } + + // This is a modern breach close with resolvers. + return StateContractClosed, nil +}