From 82b58e5c0ed7f4cef4d5628dd0d9244d6fab8efa Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Thu, 5 May 2022 11:59:31 +0200 Subject: [PATCH] loopout: attempt cooperative musig2 sweep This commit adds optional cooperative musig2 sweep by calling the server to create a partial signature for the sweep if we'd otherwise be allowed to spend the htlc. If the cooperative musig2 spend fails, we always fall back to use the scriptpath spend. --- client_test.go | 31 +++- loopout.go | 436 +++++++++++++++++++++++++++++++++++++++-------- loopout_test.go | 116 ++++++++++--- swap/htlc.go | 4 +- sweep/sweeper.go | 60 +++++++ 5 files changed, 546 insertions(+), 101 deletions(-) diff --git a/client_test.go b/client_test.go index 7a77f10..46b5a28 100644 --- a/client_test.go +++ b/client_test.go @@ -309,7 +309,6 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed, // Because there is no reliable payment yet, an invoice is assumed to be // paid after resume. - testLoopOutSuccess(ctx, amt, hash, func(r error) {}, func(r error) {}, @@ -336,14 +335,31 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash, // Publish tick. ctx.expiryChan <- testTime - // Expect a signing request. - <-ctx.Lnd.SignOutputRawChannel + // Expect a signing request in the non taproot case. + if scriptVersion != swap.HtlcV3 { + <-ctx.Lnd.SignOutputRawChannel + } if !preimageRevealed { ctx.assertStatus(loopdb.StatePreimageRevealed) ctx.assertStorePreimageReveal() } + // When using taproot htlcs the flow is different as we do reveal the + // preimage before sweeping in order for the server to trust us with + // our MuSig2 signing attempts. + if scriptVersion == swap.HtlcV3 { + ctx.assertPreimagePush(testPreimage) + + // Try MuSig2 signing first and fail it so that we go for a + // normal sweep. + for i := 0; i < maxMusigSweepRetries; i++ { + ctx.expiryChan <- testTime + ctx.assertPreimagePush(testPreimage) + } + <-ctx.Lnd.SignOutputRawChannel + } + // Expect client on-chain sweep of HTLC. sweepTx := ctx.ReceiveTx() @@ -376,10 +392,11 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash, preimage, err := lntypes.MakePreimage(clientPreImage) require.NoError(ctx.T, err) - ctx.assertPreimagePush(preimage) - - // Simulate server pulling payment. - signalSwapPaymentResult(nil) + if scriptVersion != swap.HtlcV3 { + ctx.assertPreimagePush(preimage) + // Simulate server pulling payment. + signalSwapPaymentResult(nil) + } ctx.NotifySpend(sweepTx, 0) diff --git a/loopout.go b/loopout.go index 9368e40..6fff4bd 100644 --- a/loopout.go +++ b/loopout.go @@ -10,8 +10,11 @@ import ( "sync" "time" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/labels" @@ -20,18 +23,25 @@ import ( "github.com/lightninglabs/loop/sweep" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/zpay32" ) -// loopInternalHops indicate the number of hops that a loop out swap makes in -// the server's off-chain infrastructure. We are ok reporting failure distances -// from the server up until this point, because every swap takes these two -// hops, so surfacing this information does not identify the client in any way. -// After this point, the client does not report failure distances, so that -// sender-privacy is preserved. -const loopInternalHops = 2 +const ( + // loopInternalHops indicate the number of hops that a loop out swap + // makes in the server's off-chain infrastructure. We are ok reporting + // failure distances from the server up until this point, because every + // swap takes these two hops, so surfacing this information does not + // identify the client in any way. After this point, the client does not + // report failure distances, so that sender-privacy is preserved. + loopInternalHops = 2 + + // We'll try to sweep with MuSig2 at most 10 times. If that fails we'll + // fail back to using standard scriptspend sweep. + maxMusigSweepRetries = 10 +) var ( // MinLoopOutPreimageRevealDelta configures the minimum number of @@ -169,8 +179,8 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig, SwapContract: loopdb.SwapContract{ InitiationHeight: currentHeight, InitiationTime: initiationTime, - ReceiverKey: receiverKey, SenderKey: swapResp.senderKey, + ReceiverKey: receiverKey, ClientKeyLocator: keyDesc.KeyLocator, Preimage: swapPreimage, AmountRequested: request.Amount, @@ -526,11 +536,8 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error { } // Try to spend htlc and continue (rbf) until a spend has confirmed. - spendDetails, err := s.waitForHtlcSpendConfirmed(globalCtx, - *htlcOutpoint, - func() error { - return s.sweep(globalCtx, *htlcOutpoint, htlcValue) - }, + spendDetails, err := s.waitForHtlcSpendConfirmed( + globalCtx, *htlcOutpoint, htlcValue, ) if err != nil { return err @@ -1025,14 +1032,14 @@ func (s *loopOutSwap) waitForConfirmedHtlc(globalCtx context.Context) ( // sweep offchain. So we must make sure we sweep successfully before on-chain // timeout. func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, - htlc wire.OutPoint, spendFunc func() error) (*chainntnfs.SpendDetail, - error) { + htlcOutpoint wire.OutPoint, htlcValue btcutil.Amount) ( + *chainntnfs.SpendDetail, error) { // Register the htlc spend notification. ctx, cancel := context.WithCancel(globalCtx) defer cancel() spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn( - ctx, &htlc, s.htlc.PkScript, s.InitiationHeight, + ctx, &htlcOutpoint, s.htlc.PkScript, s.InitiationHeight, ) if err != nil { return nil, fmt.Errorf("register spend ntfn: %v", err) @@ -1048,16 +1055,26 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, return nil, fmt.Errorf("track payment: %v", err) } - // paymentComplete tracks whether our payment is complete, and is used - // to decide whether we need to push our preimage to the server. - var paymentComplete bool + var ( + // paymentComplete tracks whether our payment is complete, and + // is used to decide whether we need to push our preimage to + // the server. + paymentComplete bool + // musigSweepTryCount tracts the number of cooperative, MuSig2 + // sweep attempts. + musigSweepTryCount int + // musigSweepSuccess tracks whether at least one MuSig2 sweep + // txn was successfully published to the mempool. + musigSweepSuccess bool + ) timerChan := s.timerFactory(republishDelay) for { select { // Htlc spend, break loop. case spendDetails := <-spendChan: - s.log.Infof("Htlc spend by tx: %v", spendDetails.SpenderTxHash) + s.log.Infof("Htlc spend by tx: %v", + spendDetails.SpenderTxHash) return spendDetails, nil @@ -1113,23 +1130,112 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, // Some time after start or after arrival of a new block, try // to spend again. case <-timerChan: - err := spendFunc() - if err != nil { - return nil, err - } + if IsTaprootSwap(&s.SwapContract) { + // sweepConfTarget will return false if the + // preimage is not revealed yet but the conf + // target is closer than 20 blocks. In this case + // to be sure we won't attempt to sweep at all + // and we won't reveal the preimage either. + _, canSweep := s.sweepConfTarget() + if !canSweep { + s.log.Infof("Aborting swap, timed " + + "out on-chain") + + s.state = loopdb.StateFailTimeout + err := s.persistState(ctx) + if err != nil { + log.Warnf("unable to persist " + + "state") + } - // If the result of our spend func was that the swap - // has reached a final state, then we return nil spend - // details, because there is no further action required - // for this swap. - if s.state.Type() != loopdb.StateTypePending { - return nil, nil - } + return nil, nil + } + + // When using taproot HTLCs we're pushing the + // preimage before attempting to sweep. This + // way the server will know that the swap will + // go through and we'll be able to MuSig2 + // cosign our sweep transaction. In the worst + // case if the server is uncooperative for any + // reason we can still sweep using scriptpath + // spend. + err = s.setStatePreimageRevealed(ctx) + if err != nil { + return nil, err + } + + if !paymentComplete { + // Push the preimage for as long as the + // server is able to settle the swap + // invoice. So that we can continue + // with the MuSig2 sweep afterwards. + s.pushPreimage(ctx) + } + + // Now attempt to publish a MuSig2 sweep txn. + // Only attempt at most maxMusigSweepRetires + // times to still leave time for an emergency + // script path sweep. + if musigSweepTryCount < maxMusigSweepRetries { + success := s.sweepMuSig2( + ctx, htlcOutpoint, htlcValue, + ) + if !success { + musigSweepTryCount++ + } else { + // Mark that we had a sweep + // that was successful. There's + // no need for the script spend + // now we can just keep pushing + // new sweeps to bump the fee. + musigSweepSuccess = true + } + } else if !musigSweepSuccess { + // Attempt to script path sweep. If the + // sweep fails, we can't do any better + // than go on and try again later as + // the preimage is alredy revealed and + // the server settled the swap payment. + // From the server's point of view the + // swap is succeeded at this point so + // we are free to retry as long as we + // want. + err := s.sweep( + ctx, htlcOutpoint, htlcValue, + ) + if err != nil { + log.Warnf("Failed to publish "+ + "non-cooperative "+ + "sweep: %v", err) + } + } - // If our off chain payment is not yet complete, we - // try to push our preimage to the server. - if !paymentComplete { - s.pushPreimage(ctx) + // If the result of our spend func was that the + // swap has reached a final state, then we + // return nil spend details, because there is + // no further action required for this swap. + if s.state.Type() != loopdb.StateTypePending { + return nil, nil + } + } else { + err := s.sweep(ctx, htlcOutpoint, htlcValue) + if err != nil { + return nil, err + } + + // If the result of our spend func was that the + // swap has reached a final state, then we + // return nil spend details, because there is no + // further action required for this swap. + if s.state.Type() != loopdb.StateTypePending { + return nil, nil + } + + // If our off chain payment is not yet complete, + // we try to push our preimage to the server. + if !paymentComplete { + s.pushPreimage(ctx) + } } // Context canceled. @@ -1238,24 +1344,120 @@ func (s *loopOutSwap) failOffChain(ctx context.Context, paymentType paymentType, } } -// sweep tries to sweep the given htlc to a destination address. It takes into -// account the max miner fee and marks the preimage as revealed when it -// published the tx. If the preimage has not yet been revealed, and the time -// during which we can safely reveal it has passed, the swap will be marked -// as failed, and the function will return. -// -// TODO: Use lnd sweeper? -func (s *loopOutSwap) sweep(ctx context.Context, - htlcOutpoint wire.OutPoint, - htlcValue btcutil.Amount) error { +// createMuSig2SweepTxn creates a taproot keyspend sweep transaction and +// attempts to cooperate with the server to create a MuSig2 signature witness. +func (s *loopOutSwap) createMuSig2SweepTxn( + ctx context.Context, htlcOutpoint wire.OutPoint, + htlcValue btcutil.Amount, fee btcutil.Amount) (*wire.MsgTx, error) { - witnessFunc := func(sig []byte) (wire.TxWitness, error) { - return s.htlc.GenSuccessWitness(sig, s.Preimage) + // First assemble our taproot keyspend sweep transaction and get the + // sig hash. + sweepTx, sigHash, err := s.sweeper.CreateUnsignedTaprootKeySpendSweepTx( + ctx, uint32(s.height), s.htlc, htlcOutpoint, htlcValue, fee, + s.DestAddr, + ) + if err != nil { + return nil, err } - // Retrieve the full script required to unlock the output. - redeemScript := s.htlc.SuccessScript() + var schnorrSenderKey, schnorrReceiverKey [32]byte + copy(schnorrSenderKey[:], s.SenderKey[1:]) + copy(schnorrReceiverKey[:], s.ReceiverKey[1:]) + + htlc, ok := s.htlc.HtlcScript.(*swap.HtlcScriptV3) + if !ok { + return nil, fmt.Errorf("non taproot htlc") + } + + // Now we're creating a local MuSig2 session using the receiver key's + // key locator and the htlc's root hash. + musig2SessionInfo, err := s.lnd.Signer.MuSig2CreateSession( + ctx, &s.ClientKeyLocator, + [][32]byte{schnorrSenderKey, schnorrReceiverKey}, + lndclient.MuSig2TaprootTweakOpt(htlc.RootHash[:], false), + ) + if err != nil { + return nil, err + } + + // With the session active, we can now send the server our public nonce + // and the sig hash, so that it can create it's own MuSig2 session and + // return the server side nonce and partial signature. + serverNonce, serverSig, err := s.swapKit.server.MuSig2SignSweep( + ctx, s.SwapContract.ProtocolVersion, s.hash, + s.swapInvoicePaymentAddr, musig2SessionInfo.PublicNonce[:], + sigHash, + ) + if err != nil { + return nil, err + } + + var serverPublicNonce [musig2.PubNonceSize]byte + copy(serverPublicNonce[:], serverNonce) + + // Register the server's nonce before attempting to create our partial + // signature. + haveAllNonces, err := s.lnd.Signer.MuSig2RegisterNonces( + ctx, musig2SessionInfo.SessionID, + [][musig2.PubNonceSize]byte{serverPublicNonce}, + ) + if err != nil { + return nil, err + } + + // Sanity check that we have all the nonces. + if !haveAllNonces { + return nil, fmt.Errorf("invalid MuSig2 session: nonces missing") + } + + var digest [32]byte + copy(digest[:], sigHash) + + // Since our MuSig2 session has all nonces, we can now create the local + // partial signature by signing the sig hash. + _, err = s.lnd.Signer.MuSig2Sign( + ctx, musig2SessionInfo.SessionID, digest, false, + ) + if err != nil { + return nil, err + } + + // Now combine the partial signatures to use the final combined + // signature in the sweep transaction's witness. + haveAllSigs, finalSig, err := s.lnd.Signer.MuSig2CombineSig( + ctx, musig2SessionInfo.SessionID, [][]byte{serverSig}, + ) + if err != nil { + return nil, err + } + + if !haveAllSigs { + return nil, fmt.Errorf("failed to combine signatures") + } + + // To be sure that we're good, parse and validate that the combined + // signature is indeed valid for the sig hash and the internal pubkey. + sig, err := schnorr.ParseSignature(finalSig) + if err != nil { + return nil, err + } + + if !sig.Verify(sigHash, htlc.TaprootKey) { + return nil, fmt.Errorf("invalid combined signature") + } + // Now that we know the signature is correct, we can fill it in to our + // witness. + sweepTx.TxIn[0].Witness = wire.TxWitness{ + finalSig, + } + + return sweepTx, nil +} + +// sweepConfTarget returns the confirmation target for the htlc sweep or false +// if we're too late. +func (s *loopOutSwap) sweepConfTarget() (int32, bool) { remainingBlocks := s.CltvExpiry - s.height blocksToLastReveal := remainingBlocks - MinLoopOutPreimageRevealDelta preimageRevealed := s.state == loopdb.StatePreimageRevealed @@ -1271,7 +1473,7 @@ func (s *loopOutSwap) sweep(ctx context.Context, s.height) s.state = loopdb.StateFailTimeout - return nil + return 0, false } // Calculate the transaction fee based on the confirmation target @@ -1286,42 +1488,84 @@ func (s *loopOutSwap) sweep(ctx context.Context, confTarget = DefaultSweepConfTarget } - fee, err := s.sweeper.GetSweepFee( - ctx, s.htlc.AddSuccessToEstimator, s.DestAddr, confTarget, - ) - if err != nil { - return err - } + return confTarget, true +} +// clampSweepFee will clamp the passed in sweep fee to the maximum configured +// miner fee. Returns false if sweeping should not continue. Note that in the +// MuSig2 case we always continue as the preimage is revealed to the server +// before cooperatively signing the sweep transaction. +func (s *loopOutSwap) clampSweepFee(fee btcutil.Amount) (btcutil.Amount, bool) { // Ensure it doesn't exceed our maximum fee allowed. if fee > s.MaxMinerFee { s.log.Warnf("Required fee %v exceeds max miner fee of %v", fee, s.MaxMinerFee) - if preimageRevealed { + if s.state == loopdb.StatePreimageRevealed { // The currently required fee exceeds the max, but we // already revealed the preimage. The best we can do now // is to republish with the max fee. fee = s.MaxMinerFee } else { s.log.Warnf("Not revealing preimage") - return nil + return 0, false } } - // Create sweep tx. - sweepTx, err := s.sweeper.CreateSweepTx( - ctx, s.height, s.htlc.SuccessSequence(), s.htlc, htlcOutpoint, - s.ReceiverKey, redeemScript, witnessFunc, htlcValue, fee, - s.DestAddr, + return fee, true +} + +// sweepMuSig2 attempts to sweep the on-chain HTLC using MuSig2. If anything +// fails, we'll log it but will simply return to allow further retries. Since +// the preimage is revealed by the time we attempt to MuSig2 sweep, we'll need +// to fall back to a script spend sweep if all MuSig2 sweep attempts fail (for +// example the server could be down due to maintenance or any other issue +// making the cooperative sweep fail). +func (s *loopOutSwap) sweepMuSig2(ctx context.Context, + htlcOutpoint wire.OutPoint, htlcValue btcutil.Amount) bool { + + addInputToEstimator := func(e *input.TxWeightEstimator) error { + e.AddTaprootKeySpendInput(txscript.SigHashDefault) + return nil + } + + confTarget, _ := s.sweepConfTarget() + fee, err := s.sweeper.GetSweepFee( + ctx, addInputToEstimator, s.DestAddr, confTarget, ) if err != nil { - return err + s.log.Warnf("Failed to estimate fee MuSig2 sweep txn: %v", err) + return false } - // Before publishing the tx, already mark the preimage as revealed. This - // is a precaution in case the publish call never returns and would - // leave us thinking we didn't reveal yet. + fee, _ = s.clampSweepFee(fee) + + // Now attempt the co-signing of the txn. + sweepTx, err := s.createMuSig2SweepTxn( + ctx, htlcOutpoint, htlcValue, fee, + ) + if err != nil { + s.log.Warnf("Failed to create MuSig2 sweep txn: %v", err) + return false + } + + // Finally, try publish the txn. + s.log.Infof("Sweep on chain HTLC using MuSig2 to address %v "+ + "fee %v (tx %v)", s.DestAddr, fee, sweepTx.TxHash()) + + err = s.lnd.WalletKit.PublishTransaction( + ctx, sweepTx, + labels.LoopOutSweepSuccess(swap.ShortHash(&s.hash)), + ) + if err != nil { + s.log.Warnf("Publish of MuSig2 sweep failed: %v", err) + return false + } + + return true +} + +func (s *loopOutSwap) setStatePreimageRevealed(ctx context.Context) error { if s.state != loopdb.StatePreimageRevealed { s.state = loopdb.StatePreimageRevealed @@ -1331,6 +1575,60 @@ func (s *loopOutSwap) sweep(ctx context.Context, } } + return nil +} + +// sweep tries to sweep the given htlc to a destination address. It takes into +// account the max miner fee and unless the preimage is already revealed +// (MuSig2 case), marks the preimage as revealed when it published the tx. If +// the preimage has not yet been revealed, and the time during which we can +// safely reveal it has passed, the swap will be marked as failed, and the +// function will return. +func (s *loopOutSwap) sweep(ctx context.Context, htlcOutpoint wire.OutPoint, + htlcValue btcutil.Amount) error { + + confTarget, canSweep := s.sweepConfTarget() + if !canSweep { + return nil + } + + fee, err := s.sweeper.GetSweepFee( + ctx, s.htlc.AddSuccessToEstimator, s.DestAddr, confTarget, + ) + if err != nil { + return err + } + + fee, canSweep = s.clampSweepFee(fee) + if !canSweep { + return nil + } + + witnessFunc := func(sig []byte) (wire.TxWitness, error) { + return s.htlc.GenSuccessWitness(sig, s.Preimage) + } + + // Retrieve the full script required to unlock the output. + redeemScript := s.htlc.SuccessScript() + + // Create sweep tx. + sweepTx, err := s.sweeper.CreateSweepTx( + ctx, s.height, s.htlc.SuccessSequence(), s.htlc, + htlcOutpoint, s.ReceiverKey, redeemScript, witnessFunc, + htlcValue, fee, s.DestAddr, + ) + if err != nil { + return err + } + + // Before publishing the tx, already mark the preimage as revealed. This + // is a precaution in case the publish call never returns and would + // leave us thinking we didn't reveal yet. + err = s.setStatePreimageRevealed(ctx) + if err != nil { + return err + } + // Publish tx. s.log.Infof("Sweep on chain HTLC to address %v with fee %v (tx %v)", s.DestAddr, fee, sweepTx.TxHash()) @@ -1364,8 +1662,8 @@ func validateLoopOutContract(lnd *lndclient.LndServices, if swapInvoiceHash != swapHash { return fmt.Errorf( - "cannot initiate swap, swap invoice hash %v not equal generated swap hash %v", - swapInvoiceHash, swapHash) + "cannot initiate swap, swap invoice hash %v not equal "+ + "generated swap hash %v", swapInvoiceHash, swapHash) } _, _, _, prepayInvoiceAmt, err := swap.DecodeInvoice( diff --git a/loopout_test.go b/loopout_test.go index d23e1a3..5af0ee9 100644 --- a/loopout_test.go +++ b/loopout_test.go @@ -39,7 +39,6 @@ func TestLoopOutPaymentParameters(t *testing.T) { // TestLoopOutPaymentParameters tests the first part of the loop out process up // to the point where the off-chain payments are made. func testLoopOutPaymentParameters(t *testing.T) { - defer test.Guard(t)() // Set up test context objects. @@ -372,7 +371,9 @@ func testCustomSweepConfTarget(t *testing.T) { expiryChan <- time.Now() // Expect a signing request for the HTLC success transaction. - <-ctx.Lnd.SignOutputRawChannel + if !IsTaprootSwap(&swap.SwapContract) { + <-ctx.Lnd.SignOutputRawChannel + } cfg.store.(*storeMock).assertLoopOutState(loopdb.StatePreimageRevealed) status := <-statusChan @@ -381,6 +382,24 @@ func testCustomSweepConfTarget(t *testing.T) { loopdb.StatePreimageRevealed, status.State) } + // When using taproot htlcs the flow is different as we do reveal the + // preimage before sweeping in order for the server to trust us with + // our MuSig2 signing attempts. + if IsTaprootSwap(&swap.SwapContract) { + preimage := <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + + // Try MuSig2 signing first and fail it so that we go for a + // normal sweep. + for i := 0; i < maxMusigSweepRetries; i++ { + expiryChan <- time.Now() + preimage := <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + } + + <-ctx.Lnd.SignOutputRawChannel + } + // assertSweepTx performs some sanity checks on a sweep transaction to // ensure it was constructed correctly. assertSweepTx := func(expConfTarget int32) *wire.MsgTx { @@ -424,8 +443,10 @@ func testCustomSweepConfTarget(t *testing.T) { // Once we have published an on chain sweep, we expect a preimage to // have been pushed to our server. - preimage := <-server.preimagePush - require.Equal(t, swap.Preimage, preimage) + if !IsTaprootSwap(&swap.SwapContract) { + preimage := <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + } // Now that we have pushed our preimage to the sever, we send an update // indicating that our off chain htlc is settled. We do this so that @@ -581,6 +602,36 @@ func testPreimagePush(t *testing.T) { // preimage is not revealed, we also do not expect a preimage push. expiryChan <- testTime + // When using taproot htlcs the flow is different as we do reveal the + // preimage before sweeping in order for the server to trust us with + // our MuSig2 signing attempts. + if IsTaprootSwap(&swap.SwapContract) { + cfg.store.(*storeMock).assertLoopOutState( + loopdb.StatePreimageRevealed, + ) + status := <-statusChan + require.Equal( + t, status.State, loopdb.StatePreimageRevealed, + ) + + preimage := <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + + // Try MuSig2 signing first and fail it so that we go for a + // normal sweep. + for i := 0; i < maxMusigSweepRetries; i++ { + expiryChan <- time.Now() + + preimage := <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + } + + <-ctx.Lnd.SignOutputRawChannel + + // We expect the sweep tx to have been published. + ctx.ReceiveTx() + } + // Since we don't have a reliable mechanism to non-intrusively avoid // races by setting the fee estimate too soon, let's sleep here a bit // to ensure the first sweep fails. @@ -597,30 +648,45 @@ func testPreimagePush(t *testing.T) { blockEpochChan <- ctx.Lnd.Height + 2 expiryChan <- testTime + if IsTaprootSwap(&swap.SwapContract) { + preimage := <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + } + // Expect a signing request for the HTLC success transaction. <-ctx.Lnd.SignOutputRawChannel - // This is the first time we have swept, so we expect our preimage - // revealed state to be set. - cfg.store.(*storeMock).assertLoopOutState(loopdb.StatePreimageRevealed) - status := <-statusChan - require.Equal( - t, status.State, loopdb.StatePreimageRevealed, - ) + if !IsTaprootSwap(&swap.SwapContract) { + // This is the first time we have swept, so we expect our + // preimage revealed state to be set. + cfg.store.(*storeMock).assertLoopOutState( + loopdb.StatePreimageRevealed, + ) + status := <-statusChan + require.Equal( + t, status.State, loopdb.StatePreimageRevealed, + ) + } // We expect the sweep tx to have been published. ctx.ReceiveTx() - // Once we have published an on chain sweep, we expect a preimage to - // have been pushed to the server after the sweep. - preimage := <-server.preimagePush - require.Equal(t, swap.Preimage, preimage) + if !IsTaprootSwap(&swap.SwapContract) { + // Once we have published an on chain sweep, we expect a + // preimage to have been pushed to the server after the sweep. + preimage := <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + } // To mock a server failure, we do not send a payment settled update // for our off chain payment yet. We also do not confirm our sweep on // chain yet so we can test our preimage push retry logic. Instead, we // tick the expiry chan again to prompt another sweep. expiryChan <- testTime + if IsTaprootSwap(&swap.SwapContract) { + preimage := <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + } // We expect another signing request for out sweep, and publish of our // sweep transaction. @@ -630,8 +696,11 @@ func testPreimagePush(t *testing.T) { // Since we have not yet been notified of an off chain settle, and we // have attempted to sweep again, we expect another preimage push // attempt. - preimage = <-server.preimagePush - require.Equal(t, swap.Preimage, preimage) + + if !IsTaprootSwap(&swap.SwapContract) { + preimage := <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + } // This time, we send a payment succeeded update into our payment stream // to reflect that the server received our preimage push and settled off @@ -652,7 +721,7 @@ func testPreimagePush(t *testing.T) { ctx.NotifySpend(sweepTx, 0) cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess) - status = <-statusChan + status := <-statusChan require.Equal( t, status.State, loopdb.StateSuccess, ) @@ -668,12 +737,11 @@ func TestExpiryBeforeReveal(t *testing.T) { testExpiryBeforeReveal(t) }) - t.Run("experimental protocol", func(t *testing.T) { - loopdb.EnableExperimentalProtocol() - defer loopdb.ResetCurrentProtocolVersion() - - testExpiryBeforeReveal(t) - }) + // Note that there's no point of testing this case with the new + // protocol where we use taproot htlc and attempt MuSig2 sweep. The + // reason is that the preimage is revealed to the server once the + // htlc is confirmed in order to facilitate the cooperative signing of + // the sweep transaction. } func testExpiryBeforeReveal(t *testing.T) { diff --git a/swap/htlc.go b/swap/htlc.go index 5df0309..3fdc7c9 100644 --- a/swap/htlc.go +++ b/swap/htlc.go @@ -866,7 +866,9 @@ func (h *HtlcScriptV3) GenTimeoutWitness( // IsSuccessWitness checks whether the given stack is valid for // redeeming the htlc. func (h *HtlcScriptV3) IsSuccessWitness(witness wire.TxWitness) bool { - return len(witness) == 4 + // The witness has four elements if this is a script spend or one + // element if this is a keyspend. + return len(witness) == 4 || len(witness) == 1 } // TimeoutScript returns the redeem script required to unlock the htlc after diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 775fe19..7389a9c 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -19,6 +19,66 @@ type Sweeper struct { Lnd *lndclient.LndServices } +// CreateUnsignedTaprootKeySpendSweepTx creates a taproot htlc sweep tx using +// keyspend. Returns the raw unsigned txn and the sighash or an error. +func (s *Sweeper) CreateUnsignedTaprootKeySpendSweepTx( + ctx context.Context, lockTime uint32, + htlc *swap.Htlc, htlcOutpoint wire.OutPoint, + amount, fee btcutil.Amount, destAddr btcutil.Address) ( + *wire.MsgTx, []byte, error) { + + if htlc.Version != swap.HtlcV3 { + return nil, nil, fmt.Errorf("invalid htlc version") + } + + // Compose tx. + sweepTx := wire.NewMsgTx(2) + sweepTx.LockTime = lockTime + + // Add HTLC input. + sweepTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: htlcOutpoint, + SignatureScript: htlc.SigScript, + }) + + // Add output for the destination address. + sweepPkScript, err := txscript.PayToAddrScript(destAddr) + if err != nil { + return nil, nil, err + } + + sweepTx.AddTxOut(&wire.TxOut{ + PkScript: sweepPkScript, + Value: int64(amount - fee), + }) + + // We need our previous outputs for taproot spends, and there's no + // harm including them for segwit v0, so we always include our prevOut. + prevOut := []*wire.TxOut{ + { + Value: int64(amount), + PkScript: htlc.PkScript, + }, + } + + // We now need to create the raw sighash of the transaction, as that + // will be the message we're signing collaboratively. + prevOutputFetcher := txscript.NewCannedPrevOutputFetcher( + prevOut[0].PkScript, prevOut[0].Value, + ) + sigHashes := txscript.NewTxSigHashes(sweepTx, prevOutputFetcher) + + taprootSigHash, err := txscript.CalcTaprootSignatureHash( + sigHashes, txscript.SigHashDefault, sweepTx, 0, + prevOutputFetcher, + ) + if err != nil { + return nil, nil, err + } + + return sweepTx, taprootSigHash, nil +} + // CreateSweepTx creates an htlc sweep tx. func (s *Sweeper) CreateSweepTx( globalCtx context.Context, height int32, sequence uint32,