mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-04 09:48:19 +01:00
contractcourt: update htlcTimeoutResolver for taproot chans
This commit is contained in:
parent
47f70dae3a
commit
23f7ee39c7
2 changed files with 174 additions and 29 deletions
|
@ -7,12 +7,14 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
|
"github.com/lightningnetwork/lnd/lnutils"
|
||||||
"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"
|
"github.com/lightningnetwork/lnd/sweep"
|
||||||
|
@ -78,6 +80,13 @@ func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution,
|
||||||
return h
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ResolverKey returns an identifier which should be globally unique for this
|
// ResolverKey returns an identifier which should be globally unique for this
|
||||||
// particular resolver within the chain the original contract resides within.
|
// particular resolver within the chain the original contract resides within.
|
||||||
//
|
//
|
||||||
|
@ -113,6 +122,18 @@ const (
|
||||||
// commitment transaction for an outgoing HTLC that will hold the
|
// commitment transaction for an outgoing HTLC that will hold the
|
||||||
// pre-image if the remote party sweeps it.
|
// pre-image if the remote party sweeps it.
|
||||||
localPreimageIndex = 1
|
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
|
||||||
|
|
||||||
|
// 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
|
// claimCleanUp is a helper method that's called once the HTLC output is spent
|
||||||
|
@ -133,17 +154,38 @@ func (h *htlcTimeoutResolver) claimCleanUp(
|
||||||
// If this is the remote party's commitment, then we'll be looking for
|
// If this is the remote party's commitment, then we'll be looking for
|
||||||
// them to spend using the second-level success transaction.
|
// them to spend using the second-level success transaction.
|
||||||
var preimageBytes []byte
|
var preimageBytes []byte
|
||||||
if h.htlcResolution.SignedTimeoutTx == nil {
|
switch {
|
||||||
// The witness stack when the remote party sweeps the output to
|
// For taproot channels, if the remote party has swept the HTLC, then
|
||||||
// them looks like:
|
// the witness stack will look like:
|
||||||
//
|
//
|
||||||
// * <0> <sender sig> <recvr sig> <preimage> <witness script>
|
// - <sender sig> <receiver sig> <preimage> <success_script>
|
||||||
|
// <control_block>
|
||||||
|
case h.isTaproot() && h.htlcResolution.SignedTimeoutTx == nil:
|
||||||
|
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]
|
preimageBytes = spendingInput.Witness[remotePreimageIndex]
|
||||||
} else {
|
|
||||||
// Otherwise, they'll be spending directly from our commitment
|
// If this is a taproot channel, and there's only a single witness
|
||||||
// output. In which case the witness stack looks like:
|
// element, then we're actually on the losing side of a breach
|
||||||
|
// attempt...
|
||||||
|
case h.isTaproot() && len(spendingInput.Witness) == 1:
|
||||||
|
return nil, 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>
|
// - <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]
|
preimageBytes = spendingInput.Witness[localPreimageIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +252,45 @@ func (h *htlcTimeoutResolver) chainDetailsToWatch() (*wire.OutPoint, []byte, err
|
||||||
// re-construct the pkScript we need to watch.
|
// re-construct the pkScript we need to watch.
|
||||||
outPointToWatch := h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint
|
outPointToWatch := h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint
|
||||||
witness := h.htlcResolution.SignedTimeoutTx.TxIn[0].Witness
|
witness := h.htlcResolution.SignedTimeoutTx.TxIn[0].Witness
|
||||||
scriptToWatch, err := input.WitnessScriptHash(witness[len(witness)-1])
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -220,15 +300,45 @@ func (h *htlcTimeoutResolver) chainDetailsToWatch() (*wire.OutPoint, []byte, err
|
||||||
|
|
||||||
// isPreimageSpend returns true if the passed spend on the specified commitment
|
// isPreimageSpend returns true if the passed spend on the specified commitment
|
||||||
// is a success spend that reveals the pre-image or not.
|
// is a success spend that reveals the pre-image or not.
|
||||||
func isPreimageSpend(spend *chainntnfs.SpendDetail, localCommit bool) bool {
|
func isPreimageSpend(isTaproot bool, spend *chainntnfs.SpendDetail,
|
||||||
|
localCommit bool) bool {
|
||||||
|
|
||||||
// Based on the spending input index and transaction, obtain the
|
// Based on the spending input index and transaction, obtain the
|
||||||
// witness that tells us what type of spend this is.
|
// witness that tells us what type of spend this is.
|
||||||
spenderIndex := spend.SpenderInputIndex
|
spenderIndex := spend.SpenderInputIndex
|
||||||
spendingInput := spend.SpendingTx.TxIn[spenderIndex]
|
spendingInput := spend.SpendingTx.TxIn[spenderIndex]
|
||||||
spendingWitness := spendingInput.Witness
|
spendingWitness := spendingInput.Witness
|
||||||
|
|
||||||
// If this is the remote commitment then the only possible spends for
|
switch {
|
||||||
// outgoing HTLCs are:
|
// 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:
|
||||||
|
preImageIdx := taprootRemotePreimageIndex
|
||||||
|
return len(spendingWitness) == remoteTaprootWitnessSuccessSize &&
|
||||||
|
len(spendingWitness[preImageIdx]) == lntypes.HashSize
|
||||||
|
|
||||||
|
// 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 len(spendingWitness[localPreimageIndex]) == lntypes.HashSize
|
||||||
|
|
||||||
|
// 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)
|
// RECVR: <0> <sender sig> <recvr sig> <preimage> (2nd level success spend)
|
||||||
// REVOK: <sig> <key>
|
// REVOK: <sig> <key>
|
||||||
|
@ -238,13 +348,12 @@ func isPreimageSpend(spend *chainntnfs.SpendDetail, localCommit bool) bool {
|
||||||
// witness script), and the 3rd element is the size of the pre-image,
|
// 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
|
// then this is a remote spend. If not, then we swept it ourselves, or
|
||||||
// revoked their output.
|
// revoked their output.
|
||||||
if !localCommit {
|
case !isTaproot && !localCommit:
|
||||||
return len(spendingWitness) == expectedRemoteWitnessSuccessSize &&
|
return len(spendingWitness) == expectedRemoteWitnessSuccessSize &&
|
||||||
len(spendingWitness[remotePreimageIndex]) == lntypes.HashSize
|
len(spendingWitness[remotePreimageIndex]) == lntypes.HashSize
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, for our commitment, the only possible spends for an
|
// Otherwise, for our non-taproot commitment, the only possible spends
|
||||||
// outgoing HTLC are:
|
// for an outgoing HTLC are:
|
||||||
//
|
//
|
||||||
// SENDR: <0> <sendr sig> <recvr sig> <0> (2nd level timeout)
|
// SENDR: <0> <sendr sig> <recvr sig> <0> (2nd level timeout)
|
||||||
// RECVR: <recvr sig> <preimage>
|
// RECVR: <recvr sig> <preimage>
|
||||||
|
@ -252,7 +361,11 @@ func isPreimageSpend(spend *chainntnfs.SpendDetail, localCommit bool) bool {
|
||||||
//
|
//
|
||||||
// So the only success case has the pre-image as the 2nd (index 1)
|
// So the only success case has the pre-image as the 2nd (index 1)
|
||||||
// element in the witness.
|
// element in the witness.
|
||||||
|
case !isTaproot:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
return len(spendingWitness[localPreimageIndex]) == lntypes.HashSize
|
return len(spendingWitness[localPreimageIndex]) == lntypes.HashSize
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve kicks off full resolution of an outgoing HTLC output. If it's our
|
// Resolve kicks off full resolution of an outgoing HTLC output. If it's our
|
||||||
|
@ -279,7 +392,8 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
||||||
// workflow to pass the pre-image back to the incoming link, add it to
|
// workflow to pass the pre-image back to the incoming link, add it to
|
||||||
// the witness cache, and exit.
|
// the witness cache, and exit.
|
||||||
if isPreimageSpend(
|
if isPreimageSpend(
|
||||||
commitSpend, h.htlcResolution.SignedTimeoutTx != nil,
|
h.isTaproot(), commitSpend,
|
||||||
|
h.htlcResolution.SignedTimeoutTx != nil,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
log.Infof("%T(%v): HTLC has been swept with pre-image by "+
|
log.Infof("%T(%v): HTLC has been swept with pre-image by "+
|
||||||
|
@ -317,19 +431,32 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx() error {
|
||||||
h, h.htlc.RHash[:],
|
h, h.htlc.RHash[:],
|
||||||
spew.Sdump(h.htlcResolution.SignedTimeoutTx))
|
spew.Sdump(h.htlcResolution.SignedTimeoutTx))
|
||||||
|
|
||||||
inp := input.MakeHtlcSecondLevelTimeoutAnchorInput(
|
var inp input.Input
|
||||||
|
if h.isTaproot() {
|
||||||
|
//nolint:lll
|
||||||
|
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutTaprootInput(
|
||||||
h.htlcResolution.SignedTimeoutTx,
|
h.htlcResolution.SignedTimeoutTx,
|
||||||
h.htlcResolution.SignDetails,
|
h.htlcResolution.SignDetails,
|
||||||
h.broadcastHeight,
|
h.broadcastHeight,
|
||||||
)
|
))
|
||||||
|
} else {
|
||||||
|
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutAnchorInput(
|
||||||
|
h.htlcResolution.SignedTimeoutTx,
|
||||||
|
h.htlcResolution.SignDetails,
|
||||||
|
h.broadcastHeight,
|
||||||
|
))
|
||||||
|
}
|
||||||
_, err := h.Sweeper.SweepInput(
|
_, err := h.Sweeper.SweepInput(
|
||||||
&inp, sweep.Params{
|
inp,
|
||||||
|
sweep.Params{
|
||||||
Fee: sweep.FeePreference{
|
Fee: sweep.FeePreference{
|
||||||
ConfTarget: secondLevelConfTarget,
|
ConfTarget: secondLevelConfTarget,
|
||||||
},
|
},
|
||||||
Force: true,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(yy): checkpoint here?
|
// TODO(yy): checkpoint here?
|
||||||
return err
|
return err
|
||||||
|
@ -368,6 +495,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error)
|
||||||
case h.htlcResolution.SignDetails != nil && !h.outputIncubating:
|
case h.htlcResolution.SignDetails != nil && !h.outputIncubating:
|
||||||
if err := h.sweepSecondLevelTx(); err != nil {
|
if err := h.sweepSecondLevelTx(); err != nil {
|
||||||
log.Errorf("Sending timeout tx to sweeper: %v", err)
|
log.Errorf("Sending timeout tx to sweeper: %v", err)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,6 +504,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error)
|
||||||
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
|
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
|
||||||
if err := h.sendSecondLevelTxLegacy(); err != nil {
|
if err := h.sendSecondLevelTxLegacy(); err != nil {
|
||||||
log.Errorf("Sending timeout tx to nursery: %v", err)
|
log.Errorf("Sending timeout tx to nursery: %v", err)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -515,10 +644,18 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
|
||||||
Index: commitSpend.SpenderInputIndex,
|
Index: commitSpend.SpenderInputIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var csvWitnessType input.StandardWitnessType
|
||||||
|
if h.isTaproot() {
|
||||||
|
//nolint:lll
|
||||||
|
csvWitnessType = input.TaprootHtlcOfferedTimeoutSecondLevel
|
||||||
|
} else {
|
||||||
|
csvWitnessType = input.HtlcOfferedTimeoutSecondLevel
|
||||||
|
}
|
||||||
|
|
||||||
// Let the sweeper sweep the second-level output now that the
|
// Let the sweeper sweep the second-level output now that the
|
||||||
// CSV/CLTV locks have expired.
|
// CSV/CLTV locks have expired.
|
||||||
inp := h.makeSweepInput(
|
inp := h.makeSweepInput(
|
||||||
op, input.HtlcOfferedTimeoutSecondLevel,
|
op, csvWitnessType,
|
||||||
input.LeaseHtlcOfferedTimeoutSecondLevel,
|
input.LeaseHtlcOfferedTimeoutSecondLevel,
|
||||||
&h.htlcResolution.SweepSignDesc,
|
&h.htlcResolution.SweepSignDesc,
|
||||||
h.htlcResolution.CsvDelay, h.broadcastHeight,
|
h.htlcResolution.CsvDelay, h.broadcastHeight,
|
||||||
|
@ -905,7 +1042,7 @@ func (h *htlcTimeoutResolver) consumeSpendEvents(resultChan chan *spendResult,
|
||||||
// Check whether the spend reveals the preimage, if not
|
// Check whether the spend reveals the preimage, if not
|
||||||
// continue the loop.
|
// continue the loop.
|
||||||
hasPreimage := isPreimageSpend(
|
hasPreimage := isPreimageSpend(
|
||||||
spendDetail,
|
h.isTaproot(), spendDetail,
|
||||||
h.htlcResolution.SignedTimeoutTx != nil,
|
h.htlcResolution.SignedTimeoutTx != nil,
|
||||||
)
|
)
|
||||||
if !hasPreimage {
|
if !hasPreimage {
|
||||||
|
|
8
lnutils/memory.go
Normal file
8
lnutils/memory.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package lnutils
|
||||||
|
|
||||||
|
// PTr returns the pointer of the given value. This is useful in instances
|
||||||
|
// where a function returns the value, but a pointer is wanted. Without this,
|
||||||
|
// then an intermediate variable is needed.
|
||||||
|
func Ptr[T any](v T) *T {
|
||||||
|
return &v
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue