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.
pull/497/head
Andras Banki-Horvath 2 years ago
parent 32557a57ea
commit 82b58e5c0e
No known key found for this signature in database
GPG Key ID: 80E5375C094198D8

@ -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 // Because there is no reliable payment yet, an invoice is assumed to be
// paid after resume. // paid after resume.
testLoopOutSuccess(ctx, amt, hash, testLoopOutSuccess(ctx, amt, hash,
func(r error) {}, func(r error) {},
func(r error) {}, func(r error) {},
@ -336,14 +335,31 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash,
// Publish tick. // Publish tick.
ctx.expiryChan <- testTime ctx.expiryChan <- testTime
// Expect a signing request. // Expect a signing request in the non taproot case.
<-ctx.Lnd.SignOutputRawChannel if scriptVersion != swap.HtlcV3 {
<-ctx.Lnd.SignOutputRawChannel
}
if !preimageRevealed { if !preimageRevealed {
ctx.assertStatus(loopdb.StatePreimageRevealed) ctx.assertStatus(loopdb.StatePreimageRevealed)
ctx.assertStorePreimageReveal() 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. // Expect client on-chain sweep of HTLC.
sweepTx := ctx.ReceiveTx() sweepTx := ctx.ReceiveTx()
@ -376,10 +392,11 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash,
preimage, err := lntypes.MakePreimage(clientPreImage) preimage, err := lntypes.MakePreimage(clientPreImage)
require.NoError(ctx.T, err) require.NoError(ctx.T, err)
ctx.assertPreimagePush(preimage) if scriptVersion != swap.HtlcV3 {
ctx.assertPreimagePush(preimage)
// Simulate server pulling payment. // Simulate server pulling payment.
signalSwapPaymentResult(nil) signalSwapPaymentResult(nil)
}
ctx.NotifySpend(sweepTx, 0) ctx.NotifySpend(sweepTx, 0)

@ -10,8 +10,11 @@ import (
"sync" "sync"
"time" "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/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/labels" "github.com/lightninglabs/loop/labels"
@ -20,18 +23,25 @@ import (
"github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/sweep"
"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/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/zpay32" "github.com/lightningnetwork/lnd/zpay32"
) )
// loopInternalHops indicate the number of hops that a loop out swap makes in const (
// the server's off-chain infrastructure. We are ok reporting failure distances // loopInternalHops indicate the number of hops that a loop out swap
// from the server up until this point, because every swap takes these two // makes in the server's off-chain infrastructure. We are ok reporting
// hops, so surfacing this information does not identify the client in any way. // failure distances from the server up until this point, because every
// After this point, the client does not report failure distances, so that // swap takes these two hops, so surfacing this information does not
// sender-privacy is preserved. // identify the client in any way. After this point, the client does not
const loopInternalHops = 2 // 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 ( var (
// MinLoopOutPreimageRevealDelta configures the minimum number of // MinLoopOutPreimageRevealDelta configures the minimum number of
@ -169,8 +179,8 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
SwapContract: loopdb.SwapContract{ SwapContract: loopdb.SwapContract{
InitiationHeight: currentHeight, InitiationHeight: currentHeight,
InitiationTime: initiationTime, InitiationTime: initiationTime,
ReceiverKey: receiverKey,
SenderKey: swapResp.senderKey, SenderKey: swapResp.senderKey,
ReceiverKey: receiverKey,
ClientKeyLocator: keyDesc.KeyLocator, ClientKeyLocator: keyDesc.KeyLocator,
Preimage: swapPreimage, Preimage: swapPreimage,
AmountRequested: request.Amount, 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. // Try to spend htlc and continue (rbf) until a spend has confirmed.
spendDetails, err := s.waitForHtlcSpendConfirmed(globalCtx, spendDetails, err := s.waitForHtlcSpendConfirmed(
*htlcOutpoint, globalCtx, *htlcOutpoint, htlcValue,
func() error {
return s.sweep(globalCtx, *htlcOutpoint, htlcValue)
},
) )
if err != nil { if err != nil {
return err 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 // sweep offchain. So we must make sure we sweep successfully before on-chain
// timeout. // timeout.
func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context,
htlc wire.OutPoint, spendFunc func() error) (*chainntnfs.SpendDetail, htlcOutpoint wire.OutPoint, htlcValue btcutil.Amount) (
error) { *chainntnfs.SpendDetail, error) {
// Register the htlc spend notification. // Register the htlc spend notification.
ctx, cancel := context.WithCancel(globalCtx) ctx, cancel := context.WithCancel(globalCtx)
defer cancel() defer cancel()
spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn( spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn(
ctx, &htlc, s.htlc.PkScript, s.InitiationHeight, ctx, &htlcOutpoint, s.htlc.PkScript, s.InitiationHeight,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("register spend ntfn: %v", err) 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) return nil, fmt.Errorf("track payment: %v", err)
} }
// paymentComplete tracks whether our payment is complete, and is used var (
// to decide whether we need to push our preimage to the server. // paymentComplete tracks whether our payment is complete, and
var paymentComplete bool // 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) timerChan := s.timerFactory(republishDelay)
for { for {
select { select {
// Htlc spend, break loop. // Htlc spend, break loop.
case spendDetails := <-spendChan: 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 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 // Some time after start or after arrival of a new block, try
// to spend again. // to spend again.
case <-timerChan: case <-timerChan:
err := spendFunc() if IsTaprootSwap(&s.SwapContract) {
if err != nil { // sweepConfTarget will return false if the
return nil, err // 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 return nil, nil
// has reached a final state, then we return nil spend }
// details, because there is no further action required
// for this swap. // When using taproot HTLCs we're pushing the
if s.state.Type() != loopdb.StateTypePending { // preimage before attempting to sweep. This
return nil, nil // 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 // If the result of our spend func was that the
// try to push our preimage to the server. // swap has reached a final state, then we
if !paymentComplete { // return nil spend details, because there is
s.pushPreimage(ctx) // 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. // 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 // createMuSig2SweepTxn creates a taproot keyspend sweep transaction and
// account the max miner fee and marks the preimage as revealed when it // attempts to cooperate with the server to create a MuSig2 signature witness.
// published the tx. If the preimage has not yet been revealed, and the time func (s *loopOutSwap) createMuSig2SweepTxn(
// during which we can safely reveal it has passed, the swap will be marked ctx context.Context, htlcOutpoint wire.OutPoint,
// as failed, and the function will return. htlcValue btcutil.Amount, fee btcutil.Amount) (*wire.MsgTx, error) {
//
// TODO: Use lnd sweeper?
func (s *loopOutSwap) sweep(ctx context.Context,
htlcOutpoint wire.OutPoint,
htlcValue btcutil.Amount) error {
witnessFunc := func(sig []byte) (wire.TxWitness, error) { // First assemble our taproot keyspend sweep transaction and get the
return s.htlc.GenSuccessWitness(sig, s.Preimage) // 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. var schnorrSenderKey, schnorrReceiverKey [32]byte
redeemScript := s.htlc.SuccessScript() 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 remainingBlocks := s.CltvExpiry - s.height
blocksToLastReveal := remainingBlocks - MinLoopOutPreimageRevealDelta blocksToLastReveal := remainingBlocks - MinLoopOutPreimageRevealDelta
preimageRevealed := s.state == loopdb.StatePreimageRevealed preimageRevealed := s.state == loopdb.StatePreimageRevealed
@ -1271,7 +1473,7 @@ func (s *loopOutSwap) sweep(ctx context.Context,
s.height) s.height)
s.state = loopdb.StateFailTimeout s.state = loopdb.StateFailTimeout
return nil return 0, false
} }
// Calculate the transaction fee based on the confirmation target // Calculate the transaction fee based on the confirmation target
@ -1286,42 +1488,84 @@ func (s *loopOutSwap) sweep(ctx context.Context,
confTarget = DefaultSweepConfTarget confTarget = DefaultSweepConfTarget
} }
fee, err := s.sweeper.GetSweepFee( return confTarget, true
ctx, s.htlc.AddSuccessToEstimator, s.DestAddr, confTarget, }
)
if err != nil {
return err
}
// 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. // Ensure it doesn't exceed our maximum fee allowed.
if fee > s.MaxMinerFee { if fee > s.MaxMinerFee {
s.log.Warnf("Required fee %v exceeds max miner fee of %v", s.log.Warnf("Required fee %v exceeds max miner fee of %v",
fee, s.MaxMinerFee) fee, s.MaxMinerFee)
if preimageRevealed { if s.state == loopdb.StatePreimageRevealed {
// The currently required fee exceeds the max, but we // The currently required fee exceeds the max, but we
// already revealed the preimage. The best we can do now // already revealed the preimage. The best we can do now
// is to republish with the max fee. // is to republish with the max fee.
fee = s.MaxMinerFee fee = s.MaxMinerFee
} else { } else {
s.log.Warnf("Not revealing preimage") s.log.Warnf("Not revealing preimage")
return nil return 0, false
} }
} }
// Create sweep tx. return fee, true
sweepTx, err := s.sweeper.CreateSweepTx( }
ctx, s.height, s.htlc.SuccessSequence(), s.htlc, htlcOutpoint,
s.ReceiverKey, redeemScript, witnessFunc, htlcValue, fee, // sweepMuSig2 attempts to sweep the on-chain HTLC using MuSig2. If anything
s.DestAddr, // 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 { 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 fee, _ = s.clampSweepFee(fee)
// is a precaution in case the publish call never returns and would
// leave us thinking we didn't reveal yet. // 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 { if s.state != loopdb.StatePreimageRevealed {
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. // Publish tx.
s.log.Infof("Sweep on chain HTLC to address %v with fee %v (tx %v)", s.log.Infof("Sweep on chain HTLC to address %v with fee %v (tx %v)",
s.DestAddr, fee, sweepTx.TxHash()) s.DestAddr, fee, sweepTx.TxHash())
@ -1364,8 +1662,8 @@ func validateLoopOutContract(lnd *lndclient.LndServices,
if swapInvoiceHash != swapHash { if swapInvoiceHash != swapHash {
return fmt.Errorf( return fmt.Errorf(
"cannot initiate swap, swap invoice hash %v not equal generated swap hash %v", "cannot initiate swap, swap invoice hash %v not equal "+
swapInvoiceHash, swapHash) "generated swap hash %v", swapInvoiceHash, swapHash)
} }
_, _, _, prepayInvoiceAmt, err := swap.DecodeInvoice( _, _, _, prepayInvoiceAmt, err := swap.DecodeInvoice(

@ -39,7 +39,6 @@ func TestLoopOutPaymentParameters(t *testing.T) {
// TestLoopOutPaymentParameters tests the first part of the loop out process up // TestLoopOutPaymentParameters tests the first part of the loop out process up
// to the point where the off-chain payments are made. // to the point where the off-chain payments are made.
func testLoopOutPaymentParameters(t *testing.T) { func testLoopOutPaymentParameters(t *testing.T) {
defer test.Guard(t)() defer test.Guard(t)()
// Set up test context objects. // Set up test context objects.
@ -372,7 +371,9 @@ func testCustomSweepConfTarget(t *testing.T) {
expiryChan <- time.Now() expiryChan <- time.Now()
// Expect a signing request for the HTLC success transaction. // 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) cfg.store.(*storeMock).assertLoopOutState(loopdb.StatePreimageRevealed)
status := <-statusChan status := <-statusChan
@ -381,6 +382,24 @@ func testCustomSweepConfTarget(t *testing.T) {
loopdb.StatePreimageRevealed, status.State) 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 // assertSweepTx performs some sanity checks on a sweep transaction to
// ensure it was constructed correctly. // ensure it was constructed correctly.
assertSweepTx := func(expConfTarget int32) *wire.MsgTx { 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 // Once we have published an on chain sweep, we expect a preimage to
// have been pushed to our server. // have been pushed to our server.
preimage := <-server.preimagePush if !IsTaprootSwap(&swap.SwapContract) {
require.Equal(t, swap.Preimage, preimage) preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
}
// Now that we have pushed our preimage to the sever, we send an update // 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 // 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. // preimage is not revealed, we also do not expect a preimage push.
expiryChan <- testTime 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 // 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 // races by setting the fee estimate too soon, let's sleep here a bit
// to ensure the first sweep fails. // to ensure the first sweep fails.
@ -597,30 +648,45 @@ func testPreimagePush(t *testing.T) {
blockEpochChan <- ctx.Lnd.Height + 2 blockEpochChan <- ctx.Lnd.Height + 2
expiryChan <- testTime expiryChan <- testTime
if IsTaprootSwap(&swap.SwapContract) {
preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
}
// Expect a signing request for the HTLC success transaction. // Expect a signing request for the HTLC success transaction.
<-ctx.Lnd.SignOutputRawChannel <-ctx.Lnd.SignOutputRawChannel
// This is the first time we have swept, so we expect our preimage if !IsTaprootSwap(&swap.SwapContract) {
// revealed state to be set. // This is the first time we have swept, so we expect our
cfg.store.(*storeMock).assertLoopOutState(loopdb.StatePreimageRevealed) // preimage revealed state to be set.
status := <-statusChan cfg.store.(*storeMock).assertLoopOutState(
require.Equal( loopdb.StatePreimageRevealed,
t, status.State, loopdb.StatePreimageRevealed, )
) status := <-statusChan
require.Equal(
t, status.State, loopdb.StatePreimageRevealed,
)
}
// We expect the sweep tx to have been published. // We expect the sweep tx to have been published.
ctx.ReceiveTx() ctx.ReceiveTx()
// Once we have published an on chain sweep, we expect a preimage to if !IsTaprootSwap(&swap.SwapContract) {
// have been pushed to the server after the sweep. // Once we have published an on chain sweep, we expect a
preimage := <-server.preimagePush // preimage to have been pushed to the server after the sweep.
require.Equal(t, swap.Preimage, preimage) preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
}
// To mock a server failure, we do not send a payment settled update // 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 // 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 // chain yet so we can test our preimage push retry logic. Instead, we
// tick the expiry chan again to prompt another sweep. // tick the expiry chan again to prompt another sweep.
expiryChan <- testTime 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 // We expect another signing request for out sweep, and publish of our
// sweep transaction. // 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 // Since we have not yet been notified of an off chain settle, and we
// have attempted to sweep again, we expect another preimage push // have attempted to sweep again, we expect another preimage push
// attempt. // 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 // This time, we send a payment succeeded update into our payment stream
// to reflect that the server received our preimage push and settled off // 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) ctx.NotifySpend(sweepTx, 0)
cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess) cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess)
status = <-statusChan status := <-statusChan
require.Equal( require.Equal(
t, status.State, loopdb.StateSuccess, t, status.State, loopdb.StateSuccess,
) )
@ -668,12 +737,11 @@ func TestExpiryBeforeReveal(t *testing.T) {
testExpiryBeforeReveal(t) testExpiryBeforeReveal(t)
}) })
t.Run("experimental protocol", func(t *testing.T) { // Note that there's no point of testing this case with the new
loopdb.EnableExperimentalProtocol() // protocol where we use taproot htlc and attempt MuSig2 sweep. The
defer loopdb.ResetCurrentProtocolVersion() // reason is that the preimage is revealed to the server once the
// htlc is confirmed in order to facilitate the cooperative signing of
testExpiryBeforeReveal(t) // the sweep transaction.
})
} }
func testExpiryBeforeReveal(t *testing.T) { func testExpiryBeforeReveal(t *testing.T) {

@ -866,7 +866,9 @@ func (h *HtlcScriptV3) GenTimeoutWitness(
// IsSuccessWitness checks whether the given stack is valid for // IsSuccessWitness checks whether the given stack is valid for
// redeeming the htlc. // redeeming the htlc.
func (h *HtlcScriptV3) IsSuccessWitness(witness wire.TxWitness) bool { 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 // TimeoutScript returns the redeem script required to unlock the htlc after

@ -19,6 +19,66 @@ type Sweeper struct {
Lnd *lndclient.LndServices 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. // CreateSweepTx creates an htlc sweep tx.
func (s *Sweeper) CreateSweepTx( func (s *Sweeper) CreateSweepTx(
globalCtx context.Context, height int32, sequence uint32, globalCtx context.Context, height int32, sequence uint32,

Loading…
Cancel
Save