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"
|
"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 ©
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
Loading…
Reference in New Issue
Block a user