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"
"github.com/btcsuite/btcd/btcutil"
"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/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"
@ -78,6 +80,13 @@ func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution,
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
// 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
// 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
// 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
@ -133,17 +154,38 @@ func (h *htlcTimeoutResolver) claimCleanUp(
// 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
if h.htlcResolution.SignedTimeoutTx == nil {
// The witness stack when the remote party sweeps the output to
// them looks like:
//
// * <0> <sender sig> <recvr sig> <preimage> <witness script>
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:
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]
} else {
// Otherwise, they'll be spending directly from our commitment
// output. In which case the witness stack looks like:
//
// * <sig> <preimage> <witness script>
// 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 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>
//
// 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]
}
@ -210,7 +252,45 @@ func (h *htlcTimeoutResolver) chainDetailsToWatch() (*wire.OutPoint, []byte, err
// re-construct the pkScript we need to watch.
outPointToWatch := h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint
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 {
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
// 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
// witness that tells us what type of spend this is.
spenderIndex := spend.SpenderInputIndex
spendingInput := spend.SpendingTx.TxIn[spenderIndex]
spendingWitness := spendingInput.Witness
// If this is the remote commitment then the only possible spends for
// outgoing HTLCs are:
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:
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)
// 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,
// then this is a remote spend. If not, then we swept it ourselves, or
// revoked their output.
if !localCommit {
case !isTaproot && !localCommit:
return len(spendingWitness) == expectedRemoteWitnessSuccessSize &&
len(spendingWitness[remotePreimageIndex]) == lntypes.HashSize
}
// Otherwise, for our commitment, the only possible spends for an
// outgoing HTLC are:
// 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>
@ -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)
// element in the witness.
return len(spendingWitness[localPreimageIndex]) == lntypes.HashSize
case !isTaproot:
fallthrough
default:
return len(spendingWitness[localPreimageIndex]) == lntypes.HashSize
}
}
// 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
// the witness cache, and exit.
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 "+
@ -317,19 +431,32 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx() error {
h, h.htlc.RHash[:],
spew.Sdump(h.htlcResolution.SignedTimeoutTx))
inp := input.MakeHtlcSecondLevelTimeoutAnchorInput(
h.htlcResolution.SignedTimeoutTx,
h.htlcResolution.SignDetails,
h.broadcastHeight,
)
var inp input.Input
if h.isTaproot() {
//nolint:lll
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutTaprootInput(
h.htlcResolution.SignedTimeoutTx,
h.htlcResolution.SignDetails,
h.broadcastHeight,
))
} else {
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutAnchorInput(
h.htlcResolution.SignedTimeoutTx,
h.htlcResolution.SignDetails,
h.broadcastHeight,
))
}
_, err := h.Sweeper.SweepInput(
&inp, sweep.Params{
inp,
sweep.Params{
Fee: sweep.FeePreference{
ConfTarget: secondLevelConfTarget,
},
Force: true,
},
)
if err != nil {
return err
}
// TODO(yy): checkpoint here?
return err
@ -368,6 +495,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error)
case h.htlcResolution.SignDetails != nil && !h.outputIncubating:
if err := h.sweepSecondLevelTx(); err != nil {
log.Errorf("Sending timeout tx to sweeper: %v", err)
return nil, err
}
@ -376,6 +504,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error)
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
if err := h.sendSecondLevelTxLegacy(); err != nil {
log.Errorf("Sending timeout tx to nursery: %v", err)
return nil, err
}
}
@ -515,10 +644,18 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
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
// CSV/CLTV locks have expired.
inp := h.makeSweepInput(
op, input.HtlcOfferedTimeoutSecondLevel,
op, csvWitnessType,
input.LeaseHtlcOfferedTimeoutSecondLevel,
&h.htlcResolution.SweepSignDesc,
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
// continue the loop.
hasPreimage := isPreimageSpend(
spendDetail,
h.isTaproot(), spendDetail,
h.htlcResolution.SignedTimeoutTx != nil,
)
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
}