From 4992e414393fd243199023b2f654981686eb240a Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:03 +0100 Subject: [PATCH] contraccourt+input: create resolver for timeout second level In this commit we make the sweeper handle second level transactions for HTLC timeout resolvers for anchor channels. --- contractcourt/htlc_timeout_resolver.go | 210 ++++++++++++++++++++++--- input/input.go | 37 +++++ 2 files changed, 225 insertions(+), 22 deletions(-) diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 366e4fd5..44be1643 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "fmt" "io" + "sync" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" @@ -14,6 +15,7 @@ import ( "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/sweep" ) // htlcTimeoutResolver is a ContractResolver that's capable of resolving an @@ -45,6 +47,15 @@ type htlcTimeoutResolver struct { // htlc contains information on the htlc that we are resolving on-chain. htlc channeldb.HTLC + // 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 + // will produce reports. + currentReport ContractReport + + // reportLock prevents concurrent access to the resolver report. + reportLock sync.Mutex + contractResolverKit } @@ -53,12 +64,16 @@ func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution, broadcastHeight uint32, htlc channeldb.HTLC, resCfg ResolverConfig) *htlcTimeoutResolver { - return &htlcTimeoutResolver{ + h := &htlcTimeoutResolver{ contractResolverKit: *newContractResolverKit(resCfg), htlcResolution: res, broadcastHeight: broadcastHeight, htlc: htlc, } + + h.initReport() + + return h } // ResolverKey returns an identifier which should be globally unique for this @@ -288,7 +303,9 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { return nil, err } - return h.sweepSecondLevelTransaction(commitSpend) + // Depending on whether this was a local or remote commit, we must + // handle the spending transaction accordingly. + return h.handleCommitSpend(commitSpend) } // spendHtlcOutput handles the initial spend of an HTLC output via the timeout @@ -297,9 +314,37 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { // commitment, the output will be swept directly without the timeout // transaction. func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) { - // If we haven't already sent the output to the utxo nursery, then - // we'll do so now. - if !h.outputIncubating { + switch { + + // If we have non-nil SignDetails, this means that have a 2nd level + // HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY + // (the case for anchor type channels). In this case we can re-sign it + // and attach fees at will. We let the sweeper handle this job. + case h.htlcResolution.SignDetails != nil && !h.outputIncubating: + log.Infof("%T(%x): offering second-layer timeout tx to "+ + "sweeper: %v", h, h.htlc.RHash[:], + spew.Sdump(h.htlcResolution.SignedTimeoutTx)) + + inp := input.MakeHtlcSecondLevelTimeoutAnchorInput( + h.htlcResolution.SignedTimeoutTx, + h.htlcResolution.SignDetails, + h.broadcastHeight, + ) + _, err := h.Sweeper.SweepInput( + &inp, + sweep.Params{ + Fee: sweep.FeePreference{ + ConfTarget: secondLevelConfTarget, + }, + }, + ) + if err != nil { + return nil, err + } + + // If we have no SignDetails, and we haven't already sent the output to + // the utxo nursery, then we'll do so now. + case h.htlcResolution.SignDetails == nil && !h.outputIncubating: log.Tracef("%T(%v): incubating htlc output", h, h.htlcResolution.ClaimOutpoint) @@ -319,10 +364,10 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) } } - // Now that we've handed off the HTLC to the nursery, we'll watch for a - // spend of the output, and make our next move off of that. Depending - // on if this is our commitment, or the remote party's commitment, - // we'll be watching a different outpoint and script. + // Now that we've handed off the HTLC to the nursery or sweeper, we'll + // watch for a spend of the output, and make our next move off of that. + // Depending on if this is our commitment, or the remote party's + // commitment, we'll be watching a different outpoint and script. outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch() if err != nil { return nil, err @@ -342,16 +387,35 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) return nil, err } + // If this was the second level transaction published by the sweeper, + // we can checkpoint the resolver now that it's confirmed. + if h.htlcResolution.SignDetails != nil && !h.outputIncubating { + h.outputIncubating = true + if err := h.Checkpoint(h); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + return nil, err + } + } + return spend, err } -// sweepSecondLevelTransaction sweeps the output of the confirmed second-level -// timeout transaction into our wallet. The given SpendDetail should be the -// confirmed timeout tx spending the HTLC output on the commitment tx. -func (h *htlcTimeoutResolver) sweepSecondLevelTransaction( +// handleCommitSpend handles the spend of the HTLC output on the commitment +// transaction. If this was our local commitment, the spend will be he +// confirmed second-level timeout transaction, and we'll sweep that into our +// wallet. If the was a remote commitment, the resolver will resolve +// immetiately. +func (h *htlcTimeoutResolver) handleCommitSpend( commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) { var ( + // claimOutpoint will be the outpoint of the second level + // transaction, or on the remote commitment directly. It will + // start out as set in the resolution, but we'll update it if + // the second-level goes through the sweeper and changes its + // txid. + claimOutpoint = h.htlcResolution.ClaimOutpoint + // spendTxID will be the ultimate spend of the claimOutpoint. // We set it to the commit spend for now, as this is the // ultimate spend in case this is a remote commitment. If we go @@ -362,14 +426,74 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTransaction( reports []*channeldb.ResolverReport ) + 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. + case h.htlcResolution.SignDetails != nil: + waitHeight := uint32(commitSpend.SpendingHeight) + + h.htlcResolution.CsvDelay - 1 + + 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) + + err := waitForHeight(waitHeight, h.Notifier, h.quit) + if err != nil { + return nil, err + } + + // We'll use this input index to determine the second-level + // output index on the transaction, as the signatures requires + // the indexes to be the same. We don't look for the + // second-level output script directly, as there might be more + // than one HTLC output to the same pkScript. + op := &wire.OutPoint{ + Hash: *commitSpend.SpenderTxHash, + Index: commitSpend.SpenderInputIndex, + } + + // 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, + ) + _, err = h.Sweeper.SweepInput( + inp, + sweep.Params{ + Fee: sweep.FeePreference{ + ConfTarget: sweepConfTarget, + }, + }, + ) + if err != nil { + return nil, err + } + + // Update the claim outpoint to point to the second-level + // transaction created by the sweeper. + claimOutpoint = *op + fallthrough + // Finally, if this was an output on our commitment transaction, we'll // wait for the second-level HTLC output to be spent, and for that // transaction itself to confirm. - if h.htlcResolution.SignedTimeoutTx != nil { - log.Infof("%T(%v): waiting for nursery to spend CSV delayed "+ - "output", h, h.htlcResolution.ClaimOutpoint) + case h.htlcResolution.SignedTimeoutTx != nil: + log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+ + "delayed output", h, claimOutpoint) sweep, err := waitForSpend( - &h.htlcResolution.ClaimOutpoint, + &claimOutpoint, h.htlcResolution.SweepSignDesc.Output.PkScript, h.broadcastHeight, h.Notifier, h.quit, ) @@ -383,24 +507,30 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTransaction( // Once our sweep of the timeout tx has confirmed, we add a // resolution for our timeoutTx tx first stage transaction. timeoutTx := commitSpend.SpendingTx - spendHash := timeoutTx.TxHash() + index := commitSpend.SpenderInputIndex + spendHash := commitSpend.SpenderTxHash reports = append(reports, &channeldb.ResolverReport{ - OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, + OutPoint: timeoutTx.TxIn[index].PreviousOutPoint, Amount: h.htlc.Amt.ToSatoshis(), ResolverType: channeldb.ResolverTypeOutgoingHtlc, ResolverOutcome: channeldb.ResolverOutcomeFirstStage, - SpendTxID: &spendHash, + SpendTxID: spendHash, }) } // With the clean up message sent, we'll now mark the contract - // resolved, record the timeout and the sweep txid on disk, and wait. + // resolved, update the recovered balance, record the timeout and the + // sweep txid on disk, and wait. h.resolved = true + h.reportLock.Lock() + h.currentReport.RecoveredBalance = h.currentReport.LimboBalance + h.currentReport.LimboBalance = 0 + h.reportLock.Unlock() amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value) reports = append(reports, &channeldb.ResolverReport{ - OutPoint: h.htlcResolution.ClaimOutpoint, + OutPoint: claimOutpoint, Amount: amt, ResolverType: channeldb.ResolverTypeOutgoingHtlc, ResolverOutcome: channeldb.ResolverOutcomeTimeout, @@ -426,6 +556,40 @@ func (h *htlcTimeoutResolver) IsResolved() bool { return h.resolved } +// report returns a report on the resolution state of the contract. +func (h *htlcTimeoutResolver) report() *ContractReport { + // If the sign details are nil, the report will be created by handled + // by the nursery. + if h.htlcResolution.SignDetails == nil { + return nil + } + + h.reportLock.Lock() + defer h.reportLock.Unlock() + copy := h.currentReport + return © +} + +func (h *htlcTimeoutResolver) initReport() { + // We create the initial report. This will only be reported for + // resolvers not handled by the nursery. + finalAmt := h.htlc.Amt.ToSatoshis() + if h.htlcResolution.SignedTimeoutTx != nil { + finalAmt = btcutil.Amount( + h.htlcResolution.SignedTimeoutTx.TxOut[0].Value, + ) + } + + h.currentReport = ContractReport{ + Outpoint: h.htlcResolution.ClaimOutpoint, + Type: ReportOutputOutgoingHtlc, + Amount: finalAmt, + MaturityHeight: h.htlcResolution.Expiry, + LimboBalance: finalAmt, + Stage: 1, + } +} + // Encode writes an encoded version of the ContractResolver into the passed // Writer. // @@ -504,6 +668,8 @@ func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } + h.initReport() + return h, nil } diff --git a/input/input.go b/input/input.go index e5752e88..87a6339e 100644 --- a/input/input.go +++ b/input/input.go @@ -305,6 +305,43 @@ func (i *HtlcSecondLevelAnchorInput) CraftInputScript(signer Signer, }, nil } +// MakeHtlcSecondLevelTimeoutAnchorInput creates an input allowing the sweeper +// to spend the HTLC output on our commit using the second level timeout +// transaction. +func MakeHtlcSecondLevelTimeoutAnchorInput(signedTx *wire.MsgTx, + signDetails *SignDetails, heightHint uint32) HtlcSecondLevelAnchorInput { + + // Spend an HTLC output on our local commitment tx using the + // 2nd timeout transaction. + createWitness := func(signer Signer, txn *wire.MsgTx, + hashCache *txscript.TxSigHashes, + txinIdx int) (wire.TxWitness, error) { + + desc := signDetails.SignDesc + desc.SigHashes = txscript.NewTxSigHashes(txn) + desc.InputIndex = txinIdx + + return SenderHtlcSpendTimeout( + signDetails.PeerSig, signDetails.SigHashType, signer, + &desc, txn, + ) + } + + return HtlcSecondLevelAnchorInput{ + inputKit: inputKit{ + outpoint: signedTx.TxIn[0].PreviousOutPoint, + witnessType: HtlcOfferedTimeoutSecondLevelInputConfirmed, + signDesc: signDetails.SignDesc, + heightHint: heightHint, + + // CSV delay is always 1 for these inputs. + blockToMaturity: 1, + }, + SignedTx: signedTx, + createWitness: createWitness, + } +} + // MakeHtlcSecondLevelSuccessAnchorInput creates an input allowing the sweeper // to spend the HTLC output on our commit using the second level success // transaction.