|
|
|
@ -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 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|