contractcourt: update htlcTimeoutResolver for taproot chans

This commit is contained in:
Olaoluwa Osuntokun 2023-03-01 22:16:07 -08:00
parent 47f70dae3a
commit 23f7ee39c7
No known key found for this signature in database
GPG key ID: 3BBD59E99B280306
2 changed files with 174 additions and 29 deletions

View file

@ -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,8 +361,12 @@ 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
// commitment, it isn't resolved until we see the second level HTLC txn // commitment, it isn't resolved until we see the second level HTLC txn
@ -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
View 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
}