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:
parent
0c3b64a3cd
commit
4992e41439
@ -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
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user