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.
This commit is contained in:
Johan T. Halseth 2020-12-09 12:24:03 +01:00
parent 0c3b64a3cd
commit 4992e41439
No known key found for this signature in database
GPG Key ID: 15BAADA29DA20D26
2 changed files with 225 additions and 22 deletions

@ -4,6 +4,7 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io" "io"
"sync"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
@ -14,6 +15,7 @@ import (
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/sweep"
) )
// htlcTimeoutResolver is a ContractResolver that's capable of resolving an // 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 contains information on the htlc that we are resolving on-chain.
htlc channeldb.HTLC 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 contractResolverKit
} }
@ -53,12 +64,16 @@ func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution,
broadcastHeight uint32, htlc channeldb.HTLC, broadcastHeight uint32, htlc channeldb.HTLC,
resCfg ResolverConfig) *htlcTimeoutResolver { resCfg ResolverConfig) *htlcTimeoutResolver {
return &htlcTimeoutResolver{ h := &htlcTimeoutResolver{
contractResolverKit: *newContractResolverKit(resCfg), contractResolverKit: *newContractResolverKit(resCfg),
htlcResolution: res, htlcResolution: res,
broadcastHeight: broadcastHeight, broadcastHeight: broadcastHeight,
htlc: htlc, htlc: htlc,
} }
h.initReport()
return h
} }
// ResolverKey returns an identifier which should be globally unique for this // 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 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 // 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 // commitment, the output will be swept directly without the timeout
// transaction. // transaction.
func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) { func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) {
// If we haven't already sent the output to the utxo nursery, then switch {
// we'll do so now.
if !h.outputIncubating { // 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, log.Tracef("%T(%v): incubating htlc output", h,
h.htlcResolution.ClaimOutpoint) 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 // Now that we've handed off the HTLC to the nursery or sweeper, we'll
// spend of the output, and make our next move off of that. Depending // watch for a spend of the output, and make our next move off of that.
// on if this is our commitment, or the remote party's commitment, // Depending on if this is our commitment, or the remote party's
// we'll be watching a different outpoint and script. // commitment, we'll be watching a different outpoint and script.
outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch() outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
if err != nil { if err != nil {
return nil, err return nil, err
@ -342,16 +387,35 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error)
return nil, err 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 return spend, err
} }
// sweepSecondLevelTransaction sweeps the output of the confirmed second-level // handleCommitSpend handles the spend of the HTLC output on the commitment
// timeout transaction into our wallet. The given SpendDetail should be the // transaction. If this was our local commitment, the spend will be he
// confirmed timeout tx spending the HTLC output on the commitment tx. // confirmed second-level timeout transaction, and we'll sweep that into our
func (h *htlcTimeoutResolver) sweepSecondLevelTransaction( // wallet. If the was a remote commitment, the resolver will resolve
// immetiately.
func (h *htlcTimeoutResolver) handleCommitSpend(
commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) { commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) {
var ( 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. // spendTxID will be the ultimate spend of the claimOutpoint.
// We set it to the commit spend for now, as this is the // 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 // ultimate spend in case this is a remote commitment. If we go
@ -362,14 +426,74 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTransaction(
reports []*channeldb.ResolverReport 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 // 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 // wait for the second-level HTLC output to be spent, and for that
// transaction itself to confirm. // transaction itself to confirm.
if h.htlcResolution.SignedTimeoutTx != nil { case h.htlcResolution.SignedTimeoutTx != nil:
log.Infof("%T(%v): waiting for nursery to spend CSV delayed "+ log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
"output", h, h.htlcResolution.ClaimOutpoint) "delayed output", h, claimOutpoint)
sweep, err := waitForSpend( sweep, err := waitForSpend(
&h.htlcResolution.ClaimOutpoint, &claimOutpoint,
h.htlcResolution.SweepSignDesc.Output.PkScript, h.htlcResolution.SweepSignDesc.Output.PkScript,
h.broadcastHeight, h.Notifier, h.quit, 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 // Once our sweep of the timeout tx has confirmed, we add a
// resolution for our timeoutTx tx first stage transaction. // resolution for our timeoutTx tx first stage transaction.
timeoutTx := commitSpend.SpendingTx timeoutTx := commitSpend.SpendingTx
spendHash := timeoutTx.TxHash() index := commitSpend.SpenderInputIndex
spendHash := commitSpend.SpenderTxHash
reports = append(reports, &channeldb.ResolverReport{ reports = append(reports, &channeldb.ResolverReport{
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, OutPoint: timeoutTx.TxIn[index].PreviousOutPoint,
Amount: h.htlc.Amt.ToSatoshis(), Amount: h.htlc.Amt.ToSatoshis(),
ResolverType: channeldb.ResolverTypeOutgoingHtlc, ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeFirstStage, ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
SpendTxID: &spendHash, SpendTxID: spendHash,
}) })
} }
// With the clean up message sent, we'll now mark the contract // 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.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) amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value)
reports = append(reports, &channeldb.ResolverReport{ reports = append(reports, &channeldb.ResolverReport{
OutPoint: h.htlcResolution.ClaimOutpoint, OutPoint: claimOutpoint,
Amount: amt, Amount: amt,
ResolverType: channeldb.ResolverTypeOutgoingHtlc, ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeTimeout, ResolverOutcome: channeldb.ResolverOutcomeTimeout,
@ -426,6 +556,40 @@ func (h *htlcTimeoutResolver) IsResolved() bool {
return h.resolved 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 &copy
}
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 // Encode writes an encoded version of the ContractResolver into the passed
// Writer. // Writer.
// //
@ -504,6 +668,8 @@ func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) (
return nil, err return nil, err
} }
h.initReport()
return h, nil return h, nil
} }

@ -305,6 +305,43 @@ func (i *HtlcSecondLevelAnchorInput) CraftInputScript(signer Signer,
}, nil }, 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 // MakeHtlcSecondLevelSuccessAnchorInput creates an input allowing the sweeper
// to spend the HTLC output on our commit using the second level success // to spend the HTLC output on our commit using the second level success
// transaction. // transaction.