diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go index 98a4fccae..fe2adc8b1 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -192,6 +192,13 @@ func (c *anchorResolver) IsResolved() bool { return c.resolved } +// SupplementState allows the user of a ContractResolver to supplement it with +// state required for the proper resolution of a contract. +// +// NOTE: Part of the ContractResolver interface. +func (c *anchorResolver) SupplementState(_ *channeldb.OpenChannel) { +} + // report returns a report on the resolution state of the contract. func (c *anchorResolver) report() *ContractReport { c.reportLock.Lock() diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index aeeff69f9..182c3205d 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -363,6 +363,10 @@ func newActiveChannelArbitrator(channel *channeldb.OpenChannel, report, ) }, + FetchHistoricalChannel: func() (*channeldb.OpenChannel, error) { + chanStateDB := c.chanSource.ChannelStateDB() + return chanStateDB.FetchHistoricalChannel(&chanPoint) + }, } // The final component needed is an arbitrator log that the arbitrator @@ -577,6 +581,10 @@ func (c *ChainArbitrator) Start() error { tx, c.cfg.ChainHash, &chanPoint, report, ) }, + FetchHistoricalChannel: func() (*channeldb.OpenChannel, error) { + chanStateDB := c.chanSource.ChannelStateDB() + return chanStateDB.FetchHistoricalChannel(&chanPoint) + }, } chanLog, err := newBoltArbitratorLog( c.chanSource.Backend, arbCfg, c.cfg.ChainHash, chanPoint, diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index aabed591f..e85e74b34 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -158,6 +158,11 @@ type ChannelArbitratorConfig struct { PutResolverReport func(tx kvdb.RwTx, report *channeldb.ResolverReport) error + // FetchHistoricalChannel retrieves the historical state of a channel. + // This is mostly used to supplement the ContractResolvers with + // additional information required for proper contract resolution. + FetchHistoricalChannel func() (*channeldb.OpenChannel, error) + ChainArbitratorConfig } @@ -604,10 +609,20 @@ func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet, htlcMap[outpoint] = &htlc } + // We'll also fetch the historical state of this channel, as it should + // have been marked as closed by now, and supplement it to each resolver + // such that we can properly resolve our pending contracts. + chanState, err := c.cfg.FetchHistoricalChannel() + if err != nil { + return err + } + log.Infof("ChannelArbitrator(%v): relaunching %v contract "+ "resolvers", c.cfg.ChanPoint, len(unresolvedContracts)) for _, resolver := range unresolvedContracts { + resolver.SupplementState(chanState) + htlcResolver, ok := resolver.(htlcContractResolver) if !ok { continue @@ -1912,6 +1927,14 @@ func (c *ChannelArbitrator) prepContractResolutions( return nil, nil, err } + // We'll also fetch the historical state of this channel, as it should + // have been marked as closed by now, and supplement it to each resolver + // such that we can properly resolve our pending contracts. + chanState, err := c.cfg.FetchHistoricalChannel() + if err != nil { + return nil, nil, err + } + // There may be a class of HTLC's which we can fail back immediately, // for those we'll prepare a slice of packets to add to our outbox. Any // packets we need to send, will be cancels. @@ -2017,6 +2040,7 @@ func (c *ChannelArbitrator) prepContractResolutions( resolver := newTimeoutResolver( resolution, height, htlc, resolverCfg, ) + resolver.SupplementState(chanState) htlcResolvers = append(htlcResolvers, resolver) } @@ -2073,6 +2097,7 @@ func (c *ChannelArbitrator) prepContractResolutions( resolver := newOutgoingContestResolver( resolution, height, htlc, resolverCfg, ) + resolver.SupplementState(chanState) htlcResolvers = append(htlcResolvers, resolver) } } @@ -2083,9 +2108,10 @@ func (c *ChannelArbitrator) prepContractResolutions( // trimmed). if contractResolutions.CommitResolution != nil { resolver := newCommitSweepResolver( - *contractResolutions.CommitResolution, - height, c.cfg.ChanPoint, resolverCfg, + *contractResolutions.CommitResolution, height, + c.cfg.ChanPoint, resolverCfg, ) + resolver.SupplementState(chanState) htlcResolvers = append(htlcResolvers, resolver) } diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go index 595dbc2b3..46742a384 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -379,6 +379,9 @@ func createTestChannelArbitrator(t *testing.T, log ArbitratorLog, return nil }, + FetchHistoricalChannel: func() (*channeldb.OpenChannel, error) { + return &channeldb.OpenChannel{}, nil + }, } // Apply all custom options to the config struct. diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 1ce3f3229..6b365735f 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "fmt" "io" + "math" "sync" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -43,6 +44,19 @@ type commitSweepResolver struct { // chanPoint is the channel point of the original contract. chanPoint wire.OutPoint + // channelInitiator denotes whether the party responsible for resolving + // the contract initiated the channel. + channelInitiator bool + + // leaseExpiry denotes the additional waiting period the contract must + // hold until it can be resolved. This waiting period is known as the + // expiration of a script-enforced leased channel and only applies to + // the channel initiator. + // + // NOTE: This value should only be set when the contract belongs to a + // leased channel. + leaseExpiry uint32 + // currentReport stores the current state of the resolver for reporting // over the rpc interface. currentReport ContractReport @@ -55,8 +69,8 @@ type commitSweepResolver struct { // newCommitSweepResolver instantiates a new direct commit output resolver. func newCommitSweepResolver(res lnwallet.CommitOutputResolution, - broadcastHeight uint32, - chanPoint wire.OutPoint, resCfg ResolverConfig) *commitSweepResolver { + broadcastHeight uint32, chanPoint wire.OutPoint, + resCfg ResolverConfig) *commitSweepResolver { r := &commitSweepResolver{ contractResolverKit: *newContractResolverKit(resCfg), @@ -181,7 +195,14 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { return nil, err } + // Wait up until the CSV expires, unless we also have a CLTV that + // expires after. unlockHeight := confHeight + c.commitResolution.MaturityDelay + if c.hasCLTV() { + unlockHeight = uint32(math.Max( + float64(unlockHeight), float64(c.leaseExpiry), + )) + } c.log.Debugf("commit conf_height=%v, unlock_height=%v", confHeight, unlockHeight) @@ -191,14 +212,34 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { c.currentReport.MaturityHeight = unlockHeight c.reportLock.Unlock() - // If there is a csv delay, we'll wait for that. - if c.commitResolution.MaturityDelay > 0 { - c.log.Debugf("waiting for csv lock to expire at height %v", - unlockHeight) + // If there is a csv/cltv lock, we'll wait for that. + if c.commitResolution.MaturityDelay > 0 || c.hasCLTV() { + // Determine what height we should wait until for the locks to + // expire. + var waitHeight uint32 + switch { + // If we have both a csv and cltv lock, we'll need to look at + // both and see which expires later. + case c.commitResolution.MaturityDelay > 0 && c.hasCLTV(): + c.log.Debugf("waiting for CSV and CLTV lock to expire "+ + "at height %v", unlockHeight) + // If the CSV expires after the CLTV, or there is no + // CLTV, then we can broadcast a sweep a block before. + // Otherwise, we need to broadcast at our expected + // unlock height. + waitHeight = uint32(math.Max( + float64(unlockHeight-1), float64(c.leaseExpiry), + )) - // We only need to wait for the block before the block that - // unlocks the spend path. - err := waitForHeight(unlockHeight-1, c.Notifier, c.quit) + // If we only have a csv lock, wait for the height before the + // lock expires as the spend path should be unlocked by then. + case c.commitResolution.MaturityDelay > 0: + c.log.Debugf("waiting for CSV lock to expire at "+ + "height %v", unlockHeight) + waitHeight = unlockHeight - 1 + } + + err := waitForHeight(waitHeight, c.Notifier, c.quit) if err != nil { return nil, err } @@ -222,10 +263,20 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { var witnessType input.WitnessType switch { + // Delayed output to us on our local commitment for a channel lease in + // which we are the initiator. + case isLocalCommitTx && c.hasCLTV(): + witnessType = input.LeaseCommitmentTimeLock + // Delayed output to us on our local commitment. case isLocalCommitTx: witnessType = input.CommitmentTimeLock + // A confirmed output to us on the remote commitment for a channel lease + // in which we are the initiator. + case isDelayedOutput && c.hasCLTV(): + witnessType = input.LeaseCommitmentToRemoteConfirmed + // A confirmed output to us on the remote commitment. case isDelayedOutput: witnessType = input.CommitmentToRemoteConfirmed @@ -246,13 +297,21 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // We'll craft an input with all the information required for // the sweeper to create a fully valid sweeping transaction to // recover these coins. - inp := input.NewCsvInput( - &c.commitResolution.SelfOutPoint, - witnessType, - &c.commitResolution.SelfOutputSignDesc, - c.broadcastHeight, - c.commitResolution.MaturityDelay, - ) + var inp *input.BaseInput + if c.hasCLTV() { + inp = input.NewCsvInputWithCltv( + &c.commitResolution.SelfOutPoint, witnessType, + &c.commitResolution.SelfOutputSignDesc, + c.broadcastHeight, c.commitResolution.MaturityDelay, + c.leaseExpiry, + ) + } else { + inp = input.NewCsvInput( + &c.commitResolution.SelfOutPoint, witnessType, + &c.commitResolution.SelfOutputSignDesc, + c.broadcastHeight, c.commitResolution.MaturityDelay, + ) + } // With our input constructed, we'll now offer it to the // sweeper. @@ -337,6 +396,23 @@ func (c *commitSweepResolver) IsResolved() bool { return c.resolved } +// SupplementState allows the user of a ContractResolver to supplement it with +// state required for the proper resolution of a contract. +// +// NOTE: Part of the ContractResolver interface. +func (c *commitSweepResolver) SupplementState(state *channeldb.OpenChannel) { + if state.ChanType.HasLeaseExpiration() { + c.leaseExpiry = state.ThawHeight + } + c.channelInitiator = state.IsInitiator +} + +// hasCLTV denotes whether the resolver must wait for an additional CLTV to +// expire before resolving the contract. +func (c *commitSweepResolver) hasCLTV() bool { + return c.channelInitiator && c.leaseExpiry > 0 +} + // Encode writes an encoded version of the ContractResolver into the passed // Writer. // diff --git a/contractcourt/contract_resolvers.go b/contractcourt/contract_resolvers.go index ef391ff93..b12c4815c 100644 --- a/contractcourt/contract_resolvers.go +++ b/contractcourt/contract_resolvers.go @@ -48,6 +48,10 @@ type ContractResolver interface { // NOTE: This function MUST be run as a goroutine. Resolve() (ContractResolver, error) + // SupplementState allows the user of a ContractResolver to supplement + // it with state required for the proper resolution of a contract. + SupplementState(*channeldb.OpenChannel) + // IsResolved returns true if the stored state in the resolve is fully // resolved. In this case the target output can be forgotten. IsResolved() bool diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index 59f5d9fdb..ca4e05ddf 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -433,6 +433,13 @@ func (h *htlcIncomingContestResolver) Supplement(htlc channeldb.HTLC) { h.htlc = htlc } +// SupplementState allows the user of a ContractResolver to supplement it with +// state required for the proper resolution of a contract. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcIncomingContestResolver) SupplementState(_ *channeldb.OpenChannel) { +} + // decodePayload (re)decodes the hop payload of a received htlc. func (h *htlcIncomingContestResolver) decodePayload() (*hop.Payload, error) { diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index d2813c955..14398e1a7 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -190,6 +190,14 @@ func (h *htlcOutgoingContestResolver) IsResolved() bool { return h.resolved } +// SupplementState allows the user of a ContractResolver to supplement it with +// state required for the proper resolution of a contract. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcOutgoingContestResolver) SupplementState(state *channeldb.OpenChannel) { + h.htlcTimeoutResolver.SupplementState(state) +} + // Encode writes an encoded version of the ContractResolver into the passed // Writer. // diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 37b4d42b6..c81fffcc8 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -626,6 +626,13 @@ func (h *htlcSuccessResolver) Supplement(htlc channeldb.HTLC) { h.htlc = htlc } +// SupplementState allows the user of a ContractResolver to supplement it with +// state required for the proper resolution of a contract. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcSuccessResolver) SupplementState(_ *channeldb.OpenChannel) { +} + // HtlcPoint returns the htlc's outpoint on the commitment tx. // // NOTE: Part of the htlcContractResolver interface. diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 44be16433..b20f10ca9 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "fmt" "io" + "math" "sync" "github.com/btcsuite/btcd/wire" @@ -47,6 +48,19 @@ type htlcTimeoutResolver struct { // htlc contains information on the htlc that we are resolving on-chain. htlc channeldb.HTLC + // channelInitiator denotes whether the party responsible for resolving + // the contract initiated the channel. + channelInitiator bool + + // leaseExpiry denotes the additional waiting period the contract must + // hold until it can be resolved. This waiting period is known as the + // expiration of a script-enforced leased channel and only applies to + // the channel initiator. + // + // NOTE: This value should only be set when the contract belongs to a + // leased channel. + leaseExpiry uint32 + // currentReport stores the current state of the resolver for reporting // over the rpc interface. This should only be reported in case we have // a non-nil SignDetails on the htlcResolution, otherwise the nursery @@ -429,19 +443,30 @@ func (h *htlcTimeoutResolver) handleCommitSpend( switch { // If the sweeper is handling the second level transaction, wait for - // the CSV lock to expire, before sweeping the output on the - // second-level. + // the CSV and possible CLTV lock to expire, before sweeping the output + // on the second-level. case h.htlcResolution.SignDetails != nil: waitHeight := uint32(commitSpend.SpendingHeight) + h.htlcResolution.CsvDelay - 1 + if h.hasCLTV() { + waitHeight = uint32(math.Max( + float64(waitHeight), float64(h.leaseExpiry), + )) + } h.reportLock.Lock() h.currentReport.Stage = 2 h.currentReport.MaturityHeight = waitHeight h.reportLock.Unlock() - log.Infof("%T(%x): waiting for CSV lock to expire at height %v", - h, h.htlc.RHash[:], waitHeight) + if h.hasCLTV() { + log.Infof("%T(%x): waiting for CSV and CLTV lock to "+ + "expire at height %v", h, h.htlc.RHash[:], + waitHeight) + } else { + log.Infof("%T(%x): waiting for CSV lock to expire at "+ + "height %v", h, h.htlc.RHash[:], waitHeight) + } err := waitForHeight(waitHeight, h.Notifier, h.quit) if err != nil { @@ -459,16 +484,28 @@ func (h *htlcTimeoutResolver) handleCommitSpend( } // Let the sweeper sweep the second-level output now that the - // CSV delay has passed. - log.Infof("%T(%x): CSV lock expired, offering second-layer "+ - "output to sweeper: %v", h, h.htlc.RHash[:], op) - - inp := input.NewCsvInput( - op, input.HtlcOfferedTimeoutSecondLevel, - &h.htlcResolution.SweepSignDesc, - h.broadcastHeight, - h.htlcResolution.CsvDelay, - ) + // CSV/CLTV locks have expired. + var inp *input.BaseInput + if h.hasCLTV() { + log.Infof("%T(%x): CSV and CLTV locks expired, offering "+ + "second-layer output to sweeper: %v", h, + h.htlc.RHash[:], op) + inp = input.NewCsvInputWithCltv( + op, input.LeaseHtlcOfferedTimeoutSecondLevel, + &h.htlcResolution.SweepSignDesc, + h.broadcastHeight, h.htlcResolution.CsvDelay, + h.leaseExpiry, + ) + } else { + log.Infof("%T(%x): CSV lock expired, offering "+ + "second-layer output to sweeper: %v", h, + h.htlc.RHash[:], op) + inp = input.NewCsvInput( + op, input.HtlcOfferedTimeoutSecondLevel, + &h.htlcResolution.SweepSignDesc, + h.broadcastHeight, h.htlcResolution.CsvDelay, + ) + } _, err = h.Sweeper.SweepInput( inp, sweep.Params{ @@ -681,6 +718,23 @@ func (h *htlcTimeoutResolver) Supplement(htlc channeldb.HTLC) { h.htlc = htlc } +// SupplementState allows the user of a ContractResolver to supplement it with +// state required for the proper resolution of a contract. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcTimeoutResolver) SupplementState(state *channeldb.OpenChannel) { + if state.ChanType.HasLeaseExpiration() { + h.leaseExpiry = state.ThawHeight + } + h.channelInitiator = state.IsInitiator +} + +// hasCLTV denotes whether the resolver must wait for an additional CLTV to +// expire before resolving the contract. +func (h *htlcTimeoutResolver) hasCLTV() bool { + return h.channelInitiator && h.leaseExpiry > 0 +} + // HtlcPoint returns the htlc's outpoint on the commitment tx. // // NOTE: Part of the htlcContractResolver interface.