mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-23 22:46:40 +01:00
This commit breaks the `Resolve` into two parts - the first part is moved into a `Launch` method that handles sending sweep requests, and the second part remains in `Resolve` which handles waiting for the spend. Since we are using both utxo nursery and sweeper at the same time, to make sure this change doesn't break the existing behavior, we implement the `Launch` as following, - zero-fee htlc - handled by the sweeper - direct output from the remote commit - handled by the sweeper - legacy htlc - handled by the utxo nursery
1322 lines
43 KiB
Go
1322 lines
43 KiB
Go
package contractcourt
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/fn/v2"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnutils"
|
|
"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
|
|
// outgoing HTLC. The HTLC may be on our commitment transaction, or on the
|
|
// commitment transaction of the remote party. An output on our commitment
|
|
// transaction is considered fully resolved once the second-level transaction
|
|
// has been confirmed (and reached a sufficient depth). An output on the
|
|
// commitment transaction of the remote party is resolved once we detect a
|
|
// spend of the direct HTLC output using the timeout clause.
|
|
type htlcTimeoutResolver struct {
|
|
// htlcResolution contains all the information required to properly
|
|
// resolve this outgoing HTLC.
|
|
htlcResolution lnwallet.OutgoingHtlcResolution
|
|
|
|
// outputIncubating returns true if we've sent the output to the output
|
|
// incubator (utxo nursery).
|
|
outputIncubating bool
|
|
|
|
// resolved reflects if the contract has been fully resolved or not.
|
|
resolved bool
|
|
|
|
// broadcastHeight is the height that the original contract was
|
|
// broadcast to the main-chain at. We'll use this value to bound any
|
|
// historical queries to the chain for spends/confirmations.
|
|
//
|
|
// TODO(roasbeef): wrap above into definite resolution embedding?
|
|
broadcastHeight uint32
|
|
|
|
// 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
|
|
|
|
htlcLeaseResolver
|
|
|
|
// incomingHTLCExpiryHeight is the absolute block height at which the
|
|
// incoming HTLC will expire. This is used as the deadline height as
|
|
// the outgoing HTLC must be swept before its incoming HTLC expires.
|
|
incomingHTLCExpiryHeight fn.Option[int32]
|
|
}
|
|
|
|
// newTimeoutResolver instantiates a new timeout htlc resolver.
|
|
func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution,
|
|
broadcastHeight uint32, htlc channeldb.HTLC,
|
|
resCfg ResolverConfig) *htlcTimeoutResolver {
|
|
|
|
h := &htlcTimeoutResolver{
|
|
contractResolverKit: *newContractResolverKit(resCfg),
|
|
htlcResolution: res,
|
|
broadcastHeight: broadcastHeight,
|
|
htlc: htlc,
|
|
}
|
|
|
|
h.initReport()
|
|
h.initLogger(fmt.Sprintf("%T(%v)", h, h.outpoint()))
|
|
|
|
return h
|
|
}
|
|
|
|
// isTaproot returns true if the htlc output is a taproot output.
|
|
func (h *htlcTimeoutResolver) isTaproot() bool {
|
|
return txscript.IsPayToTaproot(
|
|
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
|
)
|
|
}
|
|
|
|
// outpoint returns the outpoint of the HTLC output we're attempting to sweep.
|
|
func (h *htlcTimeoutResolver) outpoint() wire.OutPoint {
|
|
// The primary key for this resolver will be the outpoint of the HTLC
|
|
// on the commitment transaction itself. If this is our commitment,
|
|
// then the output can be found within the signed timeout tx,
|
|
// otherwise, it's just the ClaimOutpoint.
|
|
if h.htlcResolution.SignedTimeoutTx != nil {
|
|
return h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint
|
|
}
|
|
|
|
return h.htlcResolution.ClaimOutpoint
|
|
}
|
|
|
|
// ResolverKey returns an identifier which should be globally unique for this
|
|
// particular resolver within the chain the original contract resides within.
|
|
//
|
|
// NOTE: Part of the ContractResolver interface.
|
|
func (h *htlcTimeoutResolver) ResolverKey() []byte {
|
|
key := newResolverID(h.outpoint())
|
|
return key[:]
|
|
}
|
|
|
|
const (
|
|
// expectedRemoteWitnessSuccessSize is the expected size of the witness
|
|
// on the remote commitment transaction for an outgoing HTLC that is
|
|
// swept on-chain by them with pre-image.
|
|
expectedRemoteWitnessSuccessSize = 5
|
|
|
|
// expectedLocalWitnessSuccessSize is the expected size of the witness
|
|
// on the local commitment transaction for an outgoing HTLC that is
|
|
// swept on-chain by them with pre-image.
|
|
expectedLocalWitnessSuccessSize = 3
|
|
|
|
// remotePreimageIndex index within the witness on the remote
|
|
// commitment transaction that will hold they pre-image if they go to
|
|
// sweep it on chain.
|
|
remotePreimageIndex = 3
|
|
|
|
// localPreimageIndex is the index within the witness on the local
|
|
// commitment transaction for an outgoing HTLC that will hold the
|
|
// pre-image if the remote party sweeps it.
|
|
localPreimageIndex = 1
|
|
|
|
// remoteTaprootWitnessSuccessSize is the expected size of the witness
|
|
// on the remote commitment for taproot channels. The spend path will
|
|
// look like
|
|
// - <sender sig> <receiver sig> <preimage> <success_script>
|
|
// <control_block>
|
|
remoteTaprootWitnessSuccessSize = 5
|
|
|
|
// localTaprootWitnessSuccessSize is the expected size of the witness
|
|
// on the local commitment for taproot channels. The spend path will
|
|
// look like
|
|
// - <receiver sig> <preimage> <success_script> <control_block>
|
|
localTaprootWitnessSuccessSize = 4
|
|
|
|
// taprootRemotePreimageIndex is the index within the witness on the
|
|
// taproot remote commitment spend that'll hold the pre-image if the
|
|
// remote party sweeps it.
|
|
taprootRemotePreimageIndex = 2
|
|
)
|
|
|
|
// claimCleanUp is a helper method that's called once the HTLC output is spent
|
|
// by the remote party. It'll extract the preimage, add it to the global cache,
|
|
// and finally send the appropriate clean up message.
|
|
func (h *htlcTimeoutResolver) claimCleanUp(
|
|
commitSpend *chainntnfs.SpendDetail) error {
|
|
|
|
// Depending on if this is our commitment or not, then we'll be looking
|
|
// for a different witness pattern.
|
|
spenderIndex := commitSpend.SpenderInputIndex
|
|
spendingInput := commitSpend.SpendingTx.TxIn[spenderIndex]
|
|
|
|
log.Infof("%T(%v): extracting preimage! remote party spent "+
|
|
"HTLC with tx=%v", h, h.htlcResolution.ClaimOutpoint,
|
|
spew.Sdump(commitSpend.SpendingTx))
|
|
|
|
// If this is the remote party's commitment, then we'll be looking for
|
|
// them to spend using the second-level success transaction.
|
|
var preimageBytes []byte
|
|
switch {
|
|
// For taproot channels, if the remote party has swept the HTLC, then
|
|
// the witness stack will look like:
|
|
//
|
|
// - <sender sig> <receiver sig> <preimage> <success_script>
|
|
// <control_block>
|
|
case h.isTaproot() && h.htlcResolution.SignedTimeoutTx == nil:
|
|
//nolint:ll
|
|
preimageBytes = spendingInput.Witness[taprootRemotePreimageIndex]
|
|
|
|
// The witness stack when the remote party sweeps the output on a
|
|
// regular channel to them looks like:
|
|
//
|
|
// - <0> <sender sig> <recvr sig> <preimage> <witness script>
|
|
case !h.isTaproot() && h.htlcResolution.SignedTimeoutTx == nil:
|
|
preimageBytes = spendingInput.Witness[remotePreimageIndex]
|
|
|
|
// If this is a taproot channel, and there's only a single witness
|
|
// element, then we're actually on the losing side of a breach
|
|
// attempt...
|
|
case h.isTaproot() && len(spendingInput.Witness) == 1:
|
|
return fmt.Errorf("breach attempt failed")
|
|
|
|
// Otherwise, they'll be spending directly from our commitment output.
|
|
// In which case the witness stack looks like:
|
|
//
|
|
// - <sig> <preimage> <witness script>
|
|
//
|
|
// For taproot channels, this looks like:
|
|
// - <receiver sig> <preimage> <success_script> <control_block>
|
|
//
|
|
// So we can target the same index.
|
|
default:
|
|
preimageBytes = spendingInput.Witness[localPreimageIndex]
|
|
}
|
|
|
|
preimage, err := lntypes.MakePreimage(preimageBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create pre-image from witness: %w",
|
|
err)
|
|
}
|
|
|
|
log.Infof("%T(%v): extracting preimage=%v from on-chain "+
|
|
"spend!", h, h.htlcResolution.ClaimOutpoint, preimage)
|
|
|
|
// With the preimage obtained, we can now add it to the global cache.
|
|
if err := h.PreimageDB.AddPreimages(preimage); err != nil {
|
|
log.Errorf("%T(%v): unable to add witness to cache",
|
|
h, h.htlcResolution.ClaimOutpoint)
|
|
}
|
|
|
|
var pre [32]byte
|
|
copy(pre[:], preimage[:])
|
|
|
|
// Finally, we'll send the clean up message, mark ourselves as
|
|
// resolved, then exit.
|
|
if err := h.DeliverResolutionMsg(ResolutionMsg{
|
|
SourceChan: h.ShortChanID,
|
|
HtlcIndex: h.htlc.HtlcIndex,
|
|
PreImage: &pre,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
h.resolved = true
|
|
|
|
// Checkpoint our resolver with a report which reflects the preimage
|
|
// claim by the remote party.
|
|
amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value)
|
|
report := &channeldb.ResolverReport{
|
|
OutPoint: h.htlcResolution.ClaimOutpoint,
|
|
Amount: amt,
|
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
|
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
|
|
SpendTxID: commitSpend.SpenderTxHash,
|
|
}
|
|
|
|
return h.Checkpoint(h, report)
|
|
}
|
|
|
|
// chainDetailsToWatch returns the output and script which we use to watch for
|
|
// spends from the direct HTLC output on the commitment transaction.
|
|
func (h *htlcTimeoutResolver) chainDetailsToWatch() (*wire.OutPoint, []byte, error) {
|
|
// If there's no timeout transaction, it means we are spending from a
|
|
// remote commit, then the claim output is the output directly on the
|
|
// commitment transaction, so we'll just use that.
|
|
if h.htlcResolution.SignedTimeoutTx == nil {
|
|
outPointToWatch := h.htlcResolution.ClaimOutpoint
|
|
scriptToWatch := h.htlcResolution.SweepSignDesc.Output.PkScript
|
|
|
|
return &outPointToWatch, scriptToWatch, nil
|
|
}
|
|
|
|
// If SignedTimeoutTx is not nil, this is the local party's commitment,
|
|
// and we'll need to grab watch the output that our timeout transaction
|
|
// points to. We can directly grab the outpoint, then also extract the
|
|
// witness script (the last element of the witness stack) to
|
|
// re-construct the pkScript we need to watch.
|
|
//
|
|
//nolint:ll
|
|
outPointToWatch := h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint
|
|
witness := h.htlcResolution.SignedTimeoutTx.TxIn[0].Witness
|
|
|
|
var (
|
|
scriptToWatch []byte
|
|
err error
|
|
)
|
|
switch {
|
|
// For taproot channels, then final witness element is the control
|
|
// block, and the one before it the witness script. We can use both of
|
|
// these together to reconstruct the taproot output key, then map that
|
|
// into a v1 witness program.
|
|
case h.isTaproot():
|
|
// First, we'll parse the control block into something we can
|
|
// use.
|
|
ctrlBlockBytes := witness[len(witness)-1]
|
|
ctrlBlock, err := txscript.ParseControlBlock(ctrlBlockBytes)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// With the control block, we'll grab the witness script, then
|
|
// use that to derive the tapscript root.
|
|
witnessScript := witness[len(witness)-2]
|
|
tapscriptRoot := ctrlBlock.RootHash(witnessScript)
|
|
|
|
// Once we have the root, then we can derive the output key
|
|
// from the internal key, then turn that into a witness
|
|
// program.
|
|
outputKey := txscript.ComputeTaprootOutputKey(
|
|
ctrlBlock.InternalKey, tapscriptRoot,
|
|
)
|
|
scriptToWatch, err = txscript.PayToTaprootScript(outputKey)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// For regular channels, the witness script is the last element on the
|
|
// stack. We can then use this to re-derive the output that we're
|
|
// watching on chain.
|
|
default:
|
|
scriptToWatch, err = input.WitnessScriptHash(
|
|
witness[len(witness)-1],
|
|
)
|
|
}
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return &outPointToWatch, scriptToWatch, nil
|
|
}
|
|
|
|
// isPreimageSpend returns true if the passed spend on the specified commitment
|
|
// is a success spend that reveals the pre-image or not.
|
|
func isPreimageSpend(isTaproot bool, spend *chainntnfs.SpendDetail,
|
|
localCommit bool) bool {
|
|
|
|
// Based on the spending input index and transaction, obtain the
|
|
// witness that tells us what type of spend this is.
|
|
spenderIndex := spend.SpenderInputIndex
|
|
spendingInput := spend.SpendingTx.TxIn[spenderIndex]
|
|
spendingWitness := spendingInput.Witness
|
|
|
|
switch {
|
|
// If this is a taproot remote commitment, then we can detect the type
|
|
// of spend via the leaf revealed in the control block and the witness
|
|
// itself.
|
|
//
|
|
// The keyspend (revocation path) is just a single signature, while the
|
|
// timeout and success paths are most distinct.
|
|
//
|
|
// The success path will look like:
|
|
//
|
|
// - <sender sig> <receiver sig> <preimage> <success_script>
|
|
// <control_block>
|
|
case isTaproot && !localCommit:
|
|
return checkSizeAndIndex(
|
|
spendingWitness, remoteTaprootWitnessSuccessSize,
|
|
taprootRemotePreimageIndex,
|
|
)
|
|
|
|
// Otherwise, then if this is our local commitment transaction, then if
|
|
// they're sweeping the transaction, it'll be directly from the output,
|
|
// skipping the second level.
|
|
//
|
|
// In this case, then there're two main tapscript paths, with the
|
|
// success case look like:
|
|
//
|
|
// - <receiver sig> <preimage> <success_script> <control_block>
|
|
case isTaproot && localCommit:
|
|
return checkSizeAndIndex(
|
|
spendingWitness, localTaprootWitnessSuccessSize,
|
|
localPreimageIndex,
|
|
)
|
|
|
|
// If this is the non-taproot, remote commitment then the only possible
|
|
// spends for outgoing HTLCs are:
|
|
//
|
|
// RECVR: <0> <sender sig> <recvr sig> <preimage> (2nd level success spend)
|
|
// REVOK: <sig> <key>
|
|
// SENDR: <sig> 0
|
|
//
|
|
// In this case, if 5 witness elements are present (factoring the
|
|
// witness script), and the 3rd element is the size of the pre-image,
|
|
// then this is a remote spend. If not, then we swept it ourselves, or
|
|
// revoked their output.
|
|
case !isTaproot && !localCommit:
|
|
return checkSizeAndIndex(
|
|
spendingWitness, expectedRemoteWitnessSuccessSize,
|
|
remotePreimageIndex,
|
|
)
|
|
|
|
// Otherwise, for our non-taproot commitment, the only possible spends
|
|
// for an outgoing HTLC are:
|
|
//
|
|
// SENDR: <0> <sendr sig> <recvr sig> <0> (2nd level timeout)
|
|
// RECVR: <recvr sig> <preimage>
|
|
// REVOK: <revoke sig> <revoke key>
|
|
//
|
|
// So the only success case has the pre-image as the 2nd (index 1)
|
|
// element in the witness.
|
|
case !isTaproot:
|
|
fallthrough
|
|
|
|
default:
|
|
return checkSizeAndIndex(
|
|
spendingWitness, expectedLocalWitnessSuccessSize,
|
|
localPreimageIndex,
|
|
)
|
|
}
|
|
}
|
|
|
|
// checkSizeAndIndex checks that the witness is of the expected size and that
|
|
// the witness element at the specified index is of the expected size.
|
|
func checkSizeAndIndex(witness wire.TxWitness, size, index int) bool {
|
|
if len(witness) != size {
|
|
return false
|
|
}
|
|
|
|
return len(witness[index]) == lntypes.HashSize
|
|
}
|
|
|
|
// Resolve kicks off full resolution of an outgoing HTLC output. If it's our
|
|
// commitment, it isn't resolved until we see the second level HTLC txn
|
|
// confirmed. If it's the remote party's commitment, we don't resolve until we
|
|
// see a direct sweep via the timeout clause.
|
|
//
|
|
// NOTE: Part of the ContractResolver interface.
|
|
func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
|
// If we're already resolved, then we can exit early.
|
|
if h.resolved {
|
|
h.log.Errorf("already resolved")
|
|
return nil, nil
|
|
}
|
|
|
|
// If this is an output on the remote party's commitment transaction,
|
|
// use the direct-spend path to sweep the htlc.
|
|
if h.isRemoteCommitOutput() {
|
|
return nil, h.resolveRemoteCommitOutput()
|
|
}
|
|
|
|
// If this is a zero-fee HTLC, we now handle the spend from our
|
|
// commitment transaction.
|
|
if h.isZeroFeeOutput() {
|
|
return nil, h.resolveTimeoutTx()
|
|
}
|
|
|
|
// If this is an output on our own commitment using pre-anchor channel
|
|
// type, we will let the utxo nursery handle it.
|
|
return nil, h.resolveSecondLevelTxLegacy()
|
|
}
|
|
|
|
// sweepTimeoutTx sends a second level timeout transaction to the sweeper.
|
|
// This transaction uses the SINLGE|ANYONECANPAY flag.
|
|
func (h *htlcTimeoutResolver) sweepTimeoutTx() error {
|
|
var inp input.Input
|
|
if h.isTaproot() {
|
|
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutTaprootInput(
|
|
h.htlcResolution.SignedTimeoutTx,
|
|
h.htlcResolution.SignDetails,
|
|
h.broadcastHeight,
|
|
input.WithResolutionBlob(
|
|
h.htlcResolution.ResolutionBlob,
|
|
),
|
|
))
|
|
} else {
|
|
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutAnchorInput(
|
|
h.htlcResolution.SignedTimeoutTx,
|
|
h.htlcResolution.SignDetails,
|
|
h.broadcastHeight,
|
|
))
|
|
}
|
|
|
|
// Calculate the budget.
|
|
//
|
|
// TODO(yy): the budget is twice the output's value, which is needed as
|
|
// we don't force sweep the output now. To prevent cascading force
|
|
// closes, we use all its output value plus a wallet input as the
|
|
// budget. This is a temporary solution until we can optionally cancel
|
|
// the incoming HTLC, more details in,
|
|
// - https://github.com/lightningnetwork/lnd/issues/7969
|
|
budget := calculateBudget(
|
|
btcutil.Amount(inp.SignDesc().Output.Value), 2, 0,
|
|
)
|
|
|
|
h.log.Infof("offering 2nd-level HTLC timeout tx to sweeper "+
|
|
"with deadline=%v, budget=%v", h.incomingHTLCExpiryHeight,
|
|
budget)
|
|
|
|
// For an outgoing HTLC, it must be swept before the RefundTimeout of
|
|
// its incoming HTLC is reached.
|
|
_, err := h.Sweeper.SweepInput(
|
|
inp,
|
|
sweep.Params{
|
|
Budget: budget,
|
|
DeadlineHeight: h.incomingHTLCExpiryHeight,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// resolveSecondLevelTxLegacy sends a second level timeout transaction to the
|
|
// utxo nursery. This transaction uses the legacy SIGHASH_ALL flag.
|
|
func (h *htlcTimeoutResolver) resolveSecondLevelTxLegacy() error {
|
|
h.log.Debug("incubating htlc output")
|
|
|
|
// The utxo nursery will take care of broadcasting the second-level
|
|
// timeout tx and sweeping its output once it confirms.
|
|
err := h.IncubateOutputs(
|
|
h.ChanPoint, fn.Some(h.htlcResolution),
|
|
fn.None[lnwallet.IncomingHtlcResolution](),
|
|
h.broadcastHeight, h.incomingHTLCExpiryHeight,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return h.resolveTimeoutTx()
|
|
}
|
|
|
|
// sweepDirectHtlcOutput sends the direct spend of the HTLC output to the
|
|
// sweeper. This is used when the remote party goes on chain, and we're able to
|
|
// sweep an HTLC we offered after a timeout. Only the CLTV encumbered outputs
|
|
// are resolved via this path.
|
|
func (h *htlcTimeoutResolver) sweepDirectHtlcOutput() error {
|
|
var htlcWitnessType input.StandardWitnessType
|
|
if h.isTaproot() {
|
|
htlcWitnessType = input.TaprootHtlcOfferedRemoteTimeout
|
|
} else {
|
|
htlcWitnessType = input.HtlcOfferedRemoteTimeout
|
|
}
|
|
|
|
sweepInput := input.NewCsvInputWithCltv(
|
|
&h.htlcResolution.ClaimOutpoint, htlcWitnessType,
|
|
&h.htlcResolution.SweepSignDesc, h.broadcastHeight,
|
|
h.htlcResolution.CsvDelay, h.htlcResolution.Expiry,
|
|
input.WithResolutionBlob(h.htlcResolution.ResolutionBlob),
|
|
)
|
|
|
|
// Calculate the budget.
|
|
//
|
|
// TODO(yy): the budget is twice the output's value, which is needed as
|
|
// we don't force sweep the output now. To prevent cascading force
|
|
// closes, we use all its output value plus a wallet input as the
|
|
// budget. This is a temporary solution until we can optionally cancel
|
|
// the incoming HTLC, more details in,
|
|
// - https://github.com/lightningnetwork/lnd/issues/7969
|
|
budget := calculateBudget(
|
|
btcutil.Amount(sweepInput.SignDesc().Output.Value), 2, 0,
|
|
)
|
|
|
|
log.Infof("%T(%x): offering offered remote timeout HTLC output to "+
|
|
"sweeper with deadline %v and budget=%v at height=%v",
|
|
h, h.htlc.RHash[:], h.incomingHTLCExpiryHeight, budget,
|
|
h.broadcastHeight)
|
|
|
|
_, err := h.Sweeper.SweepInput(
|
|
sweepInput,
|
|
sweep.Params{
|
|
Budget: budget,
|
|
|
|
// This is an outgoing HTLC, so we want to make sure
|
|
// that we sweep it before the incoming HTLC expires.
|
|
DeadlineHeight: h.incomingHTLCExpiryHeight,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// watchHtlcSpend watches for a spend of the HTLC output. For neutrino backend,
|
|
// it will check blocks for the confirmed spend. For btcd and bitcoind, it will
|
|
// check both the mempool and the blocks.
|
|
func (h *htlcTimeoutResolver) watchHtlcSpend() (*chainntnfs.SpendDetail,
|
|
error) {
|
|
|
|
// TODO(yy): outpointToWatch is always h.HtlcOutpoint(), can refactor
|
|
// to remove the redundancy.
|
|
outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If there's no mempool configured, which is the case for SPV node
|
|
// such as neutrino, then we will watch for confirmed spend only.
|
|
if h.Mempool == nil {
|
|
return h.waitForConfirmedSpend(outpointToWatch, scriptToWatch)
|
|
}
|
|
|
|
// Watch for a spend of the HTLC output in both the mempool and blocks.
|
|
return h.waitForMempoolOrBlockSpend(*outpointToWatch, scriptToWatch)
|
|
}
|
|
|
|
// waitForConfirmedSpend waits for the HTLC output to be spent and confirmed in
|
|
// a block, returns the spend details.
|
|
func (h *htlcTimeoutResolver) waitForConfirmedSpend(op *wire.OutPoint,
|
|
pkScript []byte) (*chainntnfs.SpendDetail, error) {
|
|
|
|
// We'll block here until either we exit, or the HTLC output on the
|
|
// commitment transaction has been spent.
|
|
spend, err := waitForSpend(
|
|
op, pkScript, h.broadcastHeight, h.Notifier, h.quit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return spend, err
|
|
}
|
|
|
|
// Stop signals the resolver to cancel any current resolution processes, and
|
|
// suspend.
|
|
//
|
|
// NOTE: Part of the ContractResolver interface.
|
|
func (h *htlcTimeoutResolver) Stop() {
|
|
h.log.Debugf("stopping...")
|
|
defer h.log.Debugf("stopped")
|
|
|
|
close(h.quit)
|
|
}
|
|
|
|
// IsResolved returns true if the stored state in the resolve is fully
|
|
// resolved. In this case the target output can be forgotten.
|
|
//
|
|
// NOTE: Part of the ContractResolver interface.
|
|
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 we have a SignedTimeoutTx but no SignDetails, this is a local
|
|
// commitment for a non-anchor channel, which was handled by the utxo
|
|
// nursery.
|
|
if h.htlcResolution.SignDetails == nil && h.
|
|
htlcResolution.SignedTimeoutTx != nil {
|
|
return nil
|
|
}
|
|
|
|
h.reportLock.Lock()
|
|
defer h.reportLock.Unlock()
|
|
cpy := h.currentReport
|
|
return &cpy
|
|
}
|
|
|
|
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,
|
|
)
|
|
}
|
|
|
|
// If there's no timeout transaction, then we're already effectively in
|
|
// level two.
|
|
stage := uint32(1)
|
|
if h.htlcResolution.SignedTimeoutTx == nil {
|
|
stage = 2
|
|
}
|
|
|
|
h.currentReport = ContractReport{
|
|
Outpoint: h.htlcResolution.ClaimOutpoint,
|
|
Type: ReportOutputOutgoingHtlc,
|
|
Amount: finalAmt,
|
|
MaturityHeight: h.htlcResolution.Expiry,
|
|
LimboBalance: finalAmt,
|
|
Stage: stage,
|
|
}
|
|
}
|
|
|
|
// Encode writes an encoded version of the ContractResolver into the passed
|
|
// Writer.
|
|
//
|
|
// NOTE: Part of the ContractResolver interface.
|
|
func (h *htlcTimeoutResolver) Encode(w io.Writer) error {
|
|
// First, we'll write out the relevant fields of the
|
|
// OutgoingHtlcResolution to the writer.
|
|
if err := encodeOutgoingResolution(w, &h.htlcResolution); err != nil {
|
|
return err
|
|
}
|
|
|
|
// With that portion written, we can now write out the fields specific
|
|
// to the resolver itself.
|
|
if err := binary.Write(w, endian, h.outputIncubating); err != nil {
|
|
return err
|
|
}
|
|
if err := binary.Write(w, endian, h.resolved); err != nil {
|
|
return err
|
|
}
|
|
if err := binary.Write(w, endian, h.broadcastHeight); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := binary.Write(w, endian, h.htlc.HtlcIndex); err != nil {
|
|
return err
|
|
}
|
|
|
|
// We encode the sign details last for backwards compatibility.
|
|
err := encodeSignDetails(w, h.htlcResolution.SignDetails)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// newTimeoutResolverFromReader attempts to decode an encoded ContractResolver
|
|
// from the passed Reader instance, returning an active ContractResolver
|
|
// instance.
|
|
func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) (
|
|
*htlcTimeoutResolver, error) {
|
|
|
|
h := &htlcTimeoutResolver{
|
|
contractResolverKit: *newContractResolverKit(resCfg),
|
|
}
|
|
|
|
// First, we'll read out all the mandatory fields of the
|
|
// OutgoingHtlcResolution that we store.
|
|
if err := decodeOutgoingResolution(r, &h.htlcResolution); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// With those fields read, we can now read back the fields that are
|
|
// specific to the resolver itself.
|
|
if err := binary.Read(r, endian, &h.outputIncubating); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := binary.Read(r, endian, &h.resolved); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := binary.Read(r, endian, &h.broadcastHeight); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := binary.Read(r, endian, &h.htlc.HtlcIndex); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sign details is a new field that was added to the htlc resolution,
|
|
// so it is serialized last for backwards compatibility. We try to read
|
|
// it, but don't error out if there are not bytes left.
|
|
signDetails, err := decodeSignDetails(r)
|
|
if err == nil {
|
|
h.htlcResolution.SignDetails = signDetails
|
|
} else if err != io.EOF && err != io.ErrUnexpectedEOF {
|
|
return nil, err
|
|
}
|
|
|
|
h.initReport()
|
|
h.initLogger(fmt.Sprintf("%T(%v)", h, h.outpoint()))
|
|
|
|
return h, nil
|
|
}
|
|
|
|
// Supplement adds additional information to the resolver that is required
|
|
// before Resolve() is called.
|
|
//
|
|
// NOTE: Part of the htlcContractResolver interface.
|
|
func (h *htlcTimeoutResolver) Supplement(htlc channeldb.HTLC) {
|
|
h.htlc = htlc
|
|
}
|
|
|
|
// HtlcPoint returns the htlc's outpoint on the commitment tx.
|
|
//
|
|
// NOTE: Part of the htlcContractResolver interface.
|
|
func (h *htlcTimeoutResolver) HtlcPoint() wire.OutPoint {
|
|
return h.htlcResolution.HtlcPoint()
|
|
}
|
|
|
|
// SupplementDeadline sets the incomingHTLCExpiryHeight for this outgoing htlc
|
|
// resolver.
|
|
//
|
|
// NOTE: Part of the htlcContractResolver interface.
|
|
func (h *htlcTimeoutResolver) SupplementDeadline(d fn.Option[int32]) {
|
|
h.incomingHTLCExpiryHeight = d
|
|
}
|
|
|
|
// A compile time assertion to ensure htlcTimeoutResolver meets the
|
|
// ContractResolver interface.
|
|
var _ htlcContractResolver = (*htlcTimeoutResolver)(nil)
|
|
|
|
// spendResult is used to hold the result of a spend event from either a
|
|
// mempool spend or a block spend.
|
|
type spendResult struct {
|
|
// spend contains the details of the spend.
|
|
spend *chainntnfs.SpendDetail
|
|
|
|
// err is the error that occurred during the spend notification.
|
|
err error
|
|
}
|
|
|
|
// waitForMempoolOrBlockSpend waits for the htlc output to be spent by a
|
|
// transaction that's either be found in the mempool or in a block.
|
|
func (h *htlcTimeoutResolver) waitForMempoolOrBlockSpend(op wire.OutPoint,
|
|
pkScript []byte) (*chainntnfs.SpendDetail, error) {
|
|
|
|
log.Infof("%T(%v): waiting for spent of HTLC output %v to be found "+
|
|
"in mempool or block", h, h.htlcResolution.ClaimOutpoint, op)
|
|
|
|
// Subscribe for block spent(confirmed).
|
|
blockSpent, err := h.Notifier.RegisterSpendNtfn(
|
|
&op, pkScript, h.broadcastHeight,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("register spend: %w", err)
|
|
}
|
|
|
|
// Subscribe for mempool spent(unconfirmed).
|
|
mempoolSpent, err := h.Mempool.SubscribeMempoolSpent(op)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("register mempool spend: %w", err)
|
|
}
|
|
|
|
// Create a result chan that will be used to receive the spending
|
|
// events.
|
|
result := make(chan *spendResult, 2)
|
|
|
|
// Create a goroutine that will wait for either a mempool spend or a
|
|
// block spend.
|
|
//
|
|
// NOTE: no need to use waitgroup here as when the resolver exits, the
|
|
// goroutine will return on the quit channel.
|
|
go h.consumeSpendEvents(result, blockSpent.Spend, mempoolSpent.Spend)
|
|
|
|
// Wait for the spend event to be received.
|
|
select {
|
|
case event := <-result:
|
|
// Cancel the mempool subscription as we don't need it anymore.
|
|
h.Mempool.CancelMempoolSpendEvent(mempoolSpent)
|
|
|
|
return event.spend, event.err
|
|
|
|
case <-h.quit:
|
|
return nil, errResolverShuttingDown
|
|
}
|
|
}
|
|
|
|
// consumeSpendEvents consumes the spend events from the block and mempool
|
|
// subscriptions. It exits when a spend event is received from the block, or
|
|
// the resolver itself quits. When a spend event is received from the mempool,
|
|
// however, it won't exit but continuing to wait for a spend event from the
|
|
// block subscription.
|
|
//
|
|
// NOTE: there could be a case where we found the preimage in the mempool,
|
|
// which will be added to our preimage beacon and settle the incoming link,
|
|
// meanwhile the timeout sweep tx confirms. This outgoing HTLC is "free" money
|
|
// and is not swept here.
|
|
//
|
|
// TODO(yy): sweep the outgoing htlc if it's confirmed.
|
|
func (h *htlcTimeoutResolver) consumeSpendEvents(resultChan chan *spendResult,
|
|
blockSpent, mempoolSpent <-chan *chainntnfs.SpendDetail) {
|
|
|
|
op := h.HtlcPoint()
|
|
|
|
// Create a result chan to hold the results.
|
|
result := &spendResult{}
|
|
|
|
// Wait for a spend event to arrive.
|
|
for {
|
|
select {
|
|
// If a spend event is received from the block, this outgoing
|
|
// htlc is spent either by the remote via the preimage or by us
|
|
// via the timeout. We can exit the loop and `claimCleanUp`
|
|
// will feed the preimage to the beacon if found. This treats
|
|
// the block as the final judge and the preimage spent won't
|
|
// appear in the mempool afterwards.
|
|
//
|
|
// NOTE: if a reorg happens, the preimage spend can appear in
|
|
// the mempool again. Though a rare case, we should handle it
|
|
// in a dedicated reorg system.
|
|
case spendDetail, ok := <-blockSpent:
|
|
if !ok {
|
|
result.err = fmt.Errorf("block spent err: %w",
|
|
errResolverShuttingDown)
|
|
} else {
|
|
log.Debugf("Found confirmed spend of HTLC "+
|
|
"output %s in tx=%s", op,
|
|
spendDetail.SpenderTxHash)
|
|
|
|
result.spend = spendDetail
|
|
|
|
// Once confirmed, persist the state on disk if
|
|
// we haven't seen the output's spending tx in
|
|
// mempool before.
|
|
}
|
|
|
|
// Send the result and exit the loop.
|
|
resultChan <- result
|
|
|
|
return
|
|
|
|
// If a spend event is received from the mempool, this can be
|
|
// either the 2nd stage timeout tx or a preimage spend from the
|
|
// remote. We will further check whether the spend reveals the
|
|
// preimage and add it to the preimage beacon to settle the
|
|
// incoming link.
|
|
//
|
|
// NOTE: we won't exit the loop here so we can continue to
|
|
// watch for the block spend to check point the resolution.
|
|
case spendDetail, ok := <-mempoolSpent:
|
|
if !ok {
|
|
result.err = fmt.Errorf("mempool spent err: %w",
|
|
errResolverShuttingDown)
|
|
|
|
// This is an internal error so we exit.
|
|
resultChan <- result
|
|
|
|
return
|
|
}
|
|
|
|
log.Debugf("Found mempool spend of HTLC output %s "+
|
|
"in tx=%s", op, spendDetail.SpenderTxHash)
|
|
|
|
// Check whether the spend reveals the preimage, if not
|
|
// continue the loop.
|
|
hasPreimage := isPreimageSpend(
|
|
h.isTaproot(), spendDetail,
|
|
!h.isRemoteCommitOutput(),
|
|
)
|
|
if !hasPreimage {
|
|
log.Debugf("HTLC output %s spent doesn't "+
|
|
"reveal preimage", op)
|
|
continue
|
|
}
|
|
|
|
// Found the preimage spend, send the result and
|
|
// continue the loop.
|
|
result.spend = spendDetail
|
|
resultChan <- result
|
|
|
|
continue
|
|
|
|
// If the resolver exits, we exit the goroutine.
|
|
case <-h.quit:
|
|
result.err = errResolverShuttingDown
|
|
resultChan <- result
|
|
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// isRemoteCommitOutput returns a bool to indicate whether the htlc output is
|
|
// on the remote commitment.
|
|
func (h *htlcTimeoutResolver) isRemoteCommitOutput() bool {
|
|
// If we don't have a timeout transaction, then this means that this is
|
|
// an output on the remote party's commitment transaction.
|
|
return h.htlcResolution.SignedTimeoutTx == nil
|
|
}
|
|
|
|
// isZeroFeeOutput returns a boolean indicating whether the htlc output is from
|
|
// a anchor-enabled channel, which uses the sighash SINGLE|ANYONECANPAY.
|
|
func (h *htlcTimeoutResolver) isZeroFeeOutput() bool {
|
|
// If we have non-nil SignDetails, this means it has 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.
|
|
return h.htlcResolution.SignedTimeoutTx != nil &&
|
|
h.htlcResolution.SignDetails != nil
|
|
}
|
|
|
|
// waitHtlcSpendAndCheckPreimage waits for the htlc output to be spent and
|
|
// checks whether the spending reveals the preimage. If the preimage is found,
|
|
// it will be added to the preimage beacon to settle the incoming link, and a
|
|
// nil spend details will be returned. Otherwise, the spend details will be
|
|
// returned, indicating this is a non-preimage spend.
|
|
func (h *htlcTimeoutResolver) waitHtlcSpendAndCheckPreimage() (
|
|
*chainntnfs.SpendDetail, error) {
|
|
|
|
// Wait for the htlc output to be spent, which can happen in one of the
|
|
// paths,
|
|
// 1. The remote party spends the htlc output using the preimage.
|
|
// 2. The local party spends the htlc timeout tx from the local
|
|
// commitment.
|
|
// 3. The local party spends the htlc output directlt from the remote
|
|
// commitment.
|
|
spend, err := h.watchHtlcSpend()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the spend reveals the pre-image, then we'll enter the clean up
|
|
// workflow to pass the preimage back to the incoming link, add it to
|
|
// the witness cache, and exit.
|
|
if isPreimageSpend(h.isTaproot(), spend, !h.isRemoteCommitOutput()) {
|
|
return nil, h.claimCleanUp(spend)
|
|
}
|
|
|
|
return spend, nil
|
|
}
|
|
|
|
// sweepTimeoutTxOutput attempts to sweep the output of the second level
|
|
// timeout tx.
|
|
func (h *htlcTimeoutResolver) sweepTimeoutTxOutput() error {
|
|
h.log.Debugf("sweeping output %v from 2nd-level HTLC timeout tx",
|
|
h.htlcResolution.ClaimOutpoint)
|
|
|
|
// This should be non-blocking as we will only attempt to sweep the
|
|
// output when the second level tx has already been confirmed. In other
|
|
// words, waitHtlcSpendAndCheckPreimage will return immediately.
|
|
commitSpend, err := h.waitHtlcSpendAndCheckPreimage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Exit early if the spend is nil, as this means it's a remote spend
|
|
// using the preimage path, which is handled in claimCleanUp.
|
|
if commitSpend == nil {
|
|
h.log.Infof("preimage spend detected, skipping 2nd-level " +
|
|
"HTLC output sweep")
|
|
|
|
return nil
|
|
}
|
|
|
|
waitHeight := h.deriveWaitHeight(h.htlcResolution.CsvDelay, commitSpend)
|
|
|
|
// Now that the sweeper has broadcasted the second-level transaction,
|
|
// it has confirmed, and we have checkpointed our state, we'll sweep
|
|
// the second level output. We report the resolver has moved the next
|
|
// stage.
|
|
h.reportLock.Lock()
|
|
h.currentReport.Stage = 2
|
|
h.currentReport.MaturityHeight = waitHeight
|
|
h.reportLock.Unlock()
|
|
|
|
if h.hasCLTV() {
|
|
h.log.Infof("waiting for CSV and CLTV lock to expire at "+
|
|
"height %v", waitHeight)
|
|
} else {
|
|
h.log.Infof("waiting for CSV lock to expire at height %v",
|
|
waitHeight)
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
|
|
var witType input.StandardWitnessType
|
|
if h.isTaproot() {
|
|
witType = input.TaprootHtlcOfferedTimeoutSecondLevel
|
|
} else {
|
|
witType = input.HtlcOfferedTimeoutSecondLevel
|
|
}
|
|
|
|
// Let the sweeper sweep the second-level output now that the CSV/CLTV
|
|
// locks have expired.
|
|
inp := h.makeSweepInput(
|
|
op, witType,
|
|
input.LeaseHtlcOfferedTimeoutSecondLevel,
|
|
&h.htlcResolution.SweepSignDesc,
|
|
h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight),
|
|
h.htlc.RHash, h.htlcResolution.ResolutionBlob,
|
|
)
|
|
|
|
// Calculate the budget for this sweep.
|
|
budget := calculateBudget(
|
|
btcutil.Amount(inp.SignDesc().Output.Value),
|
|
h.Budget.NoDeadlineHTLCRatio,
|
|
h.Budget.NoDeadlineHTLC,
|
|
)
|
|
|
|
h.log.Infof("offering output from 2nd-level timeout tx to sweeper "+
|
|
"with no deadline and budget=%v", budget)
|
|
|
|
// TODO(yy): use the result chan returned from SweepInput to get the
|
|
// confirmation status of this sweeping tx so we don't need to make
|
|
// anothe subscription via `RegisterSpendNtfn` for this outpoint here
|
|
// in the resolver.
|
|
_, err = h.Sweeper.SweepInput(
|
|
inp,
|
|
sweep.Params{
|
|
Budget: budget,
|
|
|
|
// For second level success tx, there's no rush
|
|
// to get it confirmed, so we use a nil
|
|
// deadline.
|
|
DeadlineHeight: fn.None[int32](),
|
|
},
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// checkpointStageOne creates a checkpoint for the first stage of the htlc
|
|
// timeout transaction. This is used to ensure that the resolver can resume
|
|
// watching for the second stage spend in case of a restart.
|
|
func (h *htlcTimeoutResolver) checkpointStageOne(
|
|
spendTxid chainhash.Hash) error {
|
|
|
|
h.log.Debugf("checkpoint stage one spend of HTLC output %v, spent "+
|
|
"in tx %v", h.outpoint(), spendTxid)
|
|
|
|
// Now that the second-level transaction has confirmed, we checkpoint
|
|
// the state so we'll go to the next stage in case of restarts.
|
|
h.outputIncubating = true
|
|
|
|
// Create stage-one report.
|
|
report := &channeldb.ResolverReport{
|
|
OutPoint: h.outpoint(),
|
|
Amount: h.htlc.Amt.ToSatoshis(),
|
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
|
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
|
SpendTxID: &spendTxid,
|
|
}
|
|
|
|
// At this point, the second-level transaction is sufficiently
|
|
// confirmed. We can now send back our clean up message, failing the
|
|
// HTLC on the incoming link.
|
|
failureMsg := &lnwire.FailPermanentChannelFailure{}
|
|
err := h.DeliverResolutionMsg(ResolutionMsg{
|
|
SourceChan: h.ShortChanID,
|
|
HtlcIndex: h.htlc.HtlcIndex,
|
|
Failure: failureMsg,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return h.Checkpoint(h, report)
|
|
}
|
|
|
|
// checkpointClaim checkpoints the timeout resolver with the reports it needs.
|
|
func (h *htlcTimeoutResolver) checkpointClaim(
|
|
spendDetail *chainntnfs.SpendDetail) error {
|
|
|
|
h.log.Infof("resolving htlc with incoming fail msg, output=%v "+
|
|
"confirmed in tx=%v", spendDetail.SpentOutPoint,
|
|
spendDetail.SpenderTxHash)
|
|
|
|
// Create a resolver report for the claiming of the HTLC.
|
|
amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value)
|
|
report := &channeldb.ResolverReport{
|
|
OutPoint: *spendDetail.SpentOutPoint,
|
|
Amount: amt,
|
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
|
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
|
|
SpendTxID: spendDetail.SpenderTxHash,
|
|
}
|
|
|
|
// Finally, we checkpoint the resolver with our report(s).
|
|
h.resolved = true
|
|
|
|
return h.Checkpoint(h, report)
|
|
}
|
|
|
|
// resolveRemoteCommitOutput handles sweeping an HTLC output on the remote
|
|
// commitment with via the timeout path. In this case we can sweep the output
|
|
// directly, and don't have to broadcast a second-level transaction.
|
|
func (h *htlcTimeoutResolver) resolveRemoteCommitOutput() error {
|
|
h.log.Debug("waiting for direct-timeout spend of the htlc to confirm")
|
|
|
|
// Wait for the direct-timeout HTLC sweep tx to confirm.
|
|
spend, err := h.watchHtlcSpend()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If the spend reveals the preimage, then we'll enter the clean up
|
|
// workflow to pass the preimage back to the incoming link, add it to
|
|
// the witness cache, and exit.
|
|
if isPreimageSpend(h.isTaproot(), spend, !h.isRemoteCommitOutput()) {
|
|
return h.claimCleanUp(spend)
|
|
}
|
|
|
|
// Send the clean up msg to fail the incoming HTLC.
|
|
failureMsg := &lnwire.FailPermanentChannelFailure{}
|
|
err = h.DeliverResolutionMsg(ResolutionMsg{
|
|
SourceChan: h.ShortChanID,
|
|
HtlcIndex: h.htlc.HtlcIndex,
|
|
Failure: failureMsg,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO(yy): should also update the `RecoveredBalance` and
|
|
// `LimboBalance` like other paths?
|
|
|
|
// Checkpoint the resolver, and write the outcome to disk.
|
|
return h.checkpointClaim(spend)
|
|
}
|
|
|
|
// resolveTimeoutTx waits for the sweeping tx of the second-level
|
|
// timeout tx to confirm and offers the output from the timeout tx to the
|
|
// sweeper.
|
|
func (h *htlcTimeoutResolver) resolveTimeoutTx() error {
|
|
h.log.Debug("waiting for first-stage 2nd-level HTLC timeout tx to " +
|
|
"confirm")
|
|
|
|
// Wait for the second level transaction to confirm.
|
|
spend, err := h.watchHtlcSpend()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If the spend reveals the preimage, then we'll enter the clean up
|
|
// workflow to pass the preimage back to the incoming link, add it to
|
|
// the witness cache, and exit.
|
|
if isPreimageSpend(h.isTaproot(), spend, !h.isRemoteCommitOutput()) {
|
|
return h.claimCleanUp(spend)
|
|
}
|
|
|
|
op := h.htlcResolution.ClaimOutpoint
|
|
spenderTxid := *spend.SpenderTxHash
|
|
|
|
// If the timeout tx is a re-signed tx, we will need to find the actual
|
|
// spent outpoint from the spending tx.
|
|
if h.isZeroFeeOutput() {
|
|
op = wire.OutPoint{
|
|
Hash: spenderTxid,
|
|
Index: spend.SpenderInputIndex,
|
|
}
|
|
}
|
|
|
|
// If the 2nd-stage sweeping has already been started, we can
|
|
// fast-forward to start the resolving process for the stage two
|
|
// output.
|
|
if h.outputIncubating {
|
|
return h.resolveTimeoutTxOutput(op)
|
|
}
|
|
|
|
h.log.Infof("2nd-level HTLC timeout tx=%v confirmed", spenderTxid)
|
|
|
|
// Start the process to sweep the output from the timeout tx.
|
|
if h.isZeroFeeOutput() {
|
|
err = h.sweepTimeoutTxOutput()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create a checkpoint since the timeout tx is confirmed and the sweep
|
|
// request has been made.
|
|
if err := h.checkpointStageOne(spenderTxid); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start the resolving process for the stage two output.
|
|
return h.resolveTimeoutTxOutput(op)
|
|
}
|
|
|
|
// resolveTimeoutTxOutput waits for the spend of the output from the 2nd-level
|
|
// timeout tx.
|
|
func (h *htlcTimeoutResolver) resolveTimeoutTxOutput(op wire.OutPoint) error {
|
|
h.log.Debugf("waiting for second-stage 2nd-level timeout tx output %v "+
|
|
"to be spent after csv_delay=%v", op, h.htlcResolution.CsvDelay)
|
|
|
|
spend, err := waitForSpend(
|
|
&op, h.htlcResolution.SweepSignDesc.Output.PkScript,
|
|
h.broadcastHeight, h.Notifier, h.quit,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h.reportLock.Lock()
|
|
h.currentReport.RecoveredBalance = h.currentReport.LimboBalance
|
|
h.currentReport.LimboBalance = 0
|
|
h.reportLock.Unlock()
|
|
|
|
return h.checkpointClaim(spend)
|
|
}
|
|
|
|
// Launch creates an input based on the details of the outgoing htlc resolution
|
|
// and offers it to the sweeper.
|
|
func (h *htlcTimeoutResolver) Launch() error {
|
|
if h.launched {
|
|
h.log.Tracef("already launched")
|
|
return nil
|
|
}
|
|
|
|
h.log.Debugf("launching resolver...")
|
|
h.launched = true
|
|
|
|
switch {
|
|
// If we're already resolved, then we can exit early.
|
|
case h.resolved:
|
|
h.log.Errorf("already resolved")
|
|
return nil
|
|
|
|
// If this is an output on the remote party's commitment transaction,
|
|
// use the direct timeout spend path.
|
|
//
|
|
// NOTE: When the outputIncubating is false, it means that the output
|
|
// has been offered to the utxo nursery as starting in 0.18.4, we
|
|
// stopped marking this flag for direct timeout spends (#9062). In that
|
|
// case, we will do nothing and let the utxo nursery handle it.
|
|
case h.isRemoteCommitOutput() && !h.outputIncubating:
|
|
return h.sweepDirectHtlcOutput()
|
|
|
|
// If this is an anchor type channel, we now sweep either the
|
|
// second-level timeout tx or the output from the second-level timeout
|
|
// tx.
|
|
case h.isZeroFeeOutput():
|
|
// If the second-level timeout tx has already been swept, we
|
|
// can go ahead and sweep its output.
|
|
if h.outputIncubating {
|
|
return h.sweepTimeoutTxOutput()
|
|
}
|
|
|
|
// Otherwise, sweep the second level tx.
|
|
return h.sweepTimeoutTx()
|
|
|
|
// If this is an output on our own commitment using pre-anchor channel
|
|
// type, we will let the utxo nursery handle it via Resolve.
|
|
//
|
|
// TODO(yy): handle the legacy output by offering it to the sweeper.
|
|
default:
|
|
return nil
|
|
}
|
|
}
|