diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 0b3e8688d..8a2384bf5 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -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 + // - + // + 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> + switch { + // For taproot channels, if the remote party has swept the HTLC, then + // the witness stack will look like: + // + // - + // + 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> + 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: - // - // * + + // 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: + // + // - + // + // For taproot channels, this looks like: + // - + // + // 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: + // + // - + // + 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: + // + // - + 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> (2nd level success spend) // REVOK: @@ -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> <0> (2nd level timeout) // RECVR: @@ -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 { diff --git a/lnutils/memory.go b/lnutils/memory.go new file mode 100644 index 000000000..e9966418c --- /dev/null +++ b/lnutils/memory.go @@ -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 +}