diff --git a/client.go b/client.go index 24562dc..526c2a6 100644 --- a/client.go +++ b/client.go @@ -17,6 +17,7 @@ import ( "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/routing/route" @@ -61,7 +62,7 @@ var ( // probeTimeout is the maximum time until a probe is allowed to take. probeTimeout = 3 * time.Minute - republishDelay = 10 * time.Second + repushDelay = 1 * time.Second // MinerFeeEstimationFailed is a magic number that is returned in a // quote call as the miner fee if the fee estimation in lnd's wallet @@ -134,7 +135,8 @@ type ClientConfig struct { // NewClient returns a new instance to initiate swaps with. func NewClient(dbDir string, loopDB loopdb.SwapStore, - cfg *ClientConfig) (*Client, func(), error) { + sweeperDb sweepbatcher.BatcherStore, cfg *ClientConfig) ( + *Client, func(), error) { lsatStore, err := lsat.NewFileStore(dbDir) if err != nil { @@ -162,27 +164,36 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore, Lnd: cfg.Lnd, } + verifySchnorrSig := func(pubKey *btcec.PublicKey, hash, sig []byte) error { + schnorrSig, err := schnorr.ParseSignature(sig) + if err != nil { + return err + } + + if !schnorrSig.Verify(hash, pubKey) { + return fmt.Errorf("invalid signature") + } + + return nil + } + + batcher := sweepbatcher.NewBatcher( + cfg.Lnd.WalletKit, cfg.Lnd.ChainNotifier, cfg.Lnd.Signer, + swapServerClient.MultiMuSig2SignSweep, verifySchnorrSig, + cfg.Lnd.ChainParams, sweeperDb, loopDB, + ) + executor := newExecutor(&executorConfig{ lnd: cfg.Lnd, store: loopDB, sweeper: sweeper, + batcher: batcher, createExpiryTimer: config.CreateExpiryTimer, loopOutMaxParts: cfg.LoopOutMaxParts, totalPaymentTimeout: cfg.TotalPaymentTimeout, maxPaymentRetries: cfg.MaxPaymentRetries, cancelSwap: swapServerClient.CancelLoopOutSwap, - verifySchnorrSig: func(pubKey *btcec.PublicKey, hash, sig []byte) error { - schnorrSig, err := schnorr.ParseSignature(sig) - if err != nil { - return err - } - - if !schnorrSig.Verify(hash, pubKey) { - return fmt.Errorf("invalid signature") - } - - return nil - }, + verifySchnorrSig: verifySchnorrSig, }) client := &Client{ diff --git a/executor.go b/executor.go index 9451b15..257074f 100644 --- a/executor.go +++ b/executor.go @@ -13,6 +13,7 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/queue" ) @@ -23,6 +24,8 @@ type executorConfig struct { sweeper *sweep.Sweeper + batcher *sweepbatcher.Batcher + store loopdb.SwapStore createExpiryTimer func(expiry time.Duration) <-chan time.Time @@ -71,6 +74,7 @@ func (s *executor) run(mainCtx context.Context, err error blockEpochChan <-chan int32 blockErrorChan <-chan error + batcherErrChan chan error ) for { @@ -121,6 +125,21 @@ func (s *executor) run(mainCtx context.Context, return mainCtx.Err() } + batcherErrChan = make(chan error, 1) + + s.wg.Add(1) + go func() { + defer s.wg.Done() + + err := s.batcher.Run(mainCtx) + if err != nil { + select { + case batcherErrChan <- err: + case <-mainCtx.Done(): + } + } + }() + // Start main event loop. log.Infof("Starting event loop at height %v", height) @@ -156,6 +175,7 @@ func (s *executor) run(mainCtx context.Context, err := newSwap.execute(mainCtx, &executeConfig{ statusChan: statusChan, sweeper: s.sweeper, + batcher: s.batcher, blockEpochChan: queue.ChanOut(), timerFactory: s.executorConfig.createExpiryTimer, loopOutMaxParts: s.executorConfig.loopOutMaxParts, @@ -211,6 +231,9 @@ func (s *executor) run(mainCtx context.Context, case err := <-blockErrorChan: return fmt.Errorf("block error: %v", err) + case err := <-batcherErrChan: + return fmt.Errorf("batcher error: %v", err) + case <-mainCtx.Done(): return mainCtx.Err() } diff --git a/loopd/daemon.go b/loopd/daemon.go index 1d7027d..747780d 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -18,6 +18,7 @@ import ( "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/loopd/perms" "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightninglabs/loop/instantout/reservation" loop_looprpc "github.com/lightninglabs/loop/looprpc" @@ -412,9 +413,11 @@ func (d *Daemon) initialize(withMacaroonService bool) error { return err } + sweeperDb := sweepbatcher.NewSQLStore(baseDb, chainParams) + // Create an instance of the loop client library. swapClient, clientCleanup, err := getClient( - d.cfg, swapDb, &d.lnd.LndServices, + d.cfg, swapDb, sweeperDb, &d.lnd.LndServices, ) if err != nil { return err diff --git a/loopd/utils.go b/loopd/utils.go index 78f8f4a..ef763e2 100644 --- a/loopd/utils.go +++ b/loopd/utils.go @@ -11,13 +11,15 @@ import ( "github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/ticker" ) // getClient returns an instance of the swap client. func getClient(cfg *Config, swapDb loopdb.SwapStore, - lnd *lndclient.LndServices) (*loop.Client, func(), error) { + sweeperDb sweepbatcher.BatcherStore, lnd *lndclient.LndServices) ( + *loop.Client, func(), error) { clientConfig := &loop.ClientConfig{ ServerAddress: cfg.Server.Host, @@ -33,7 +35,7 @@ func getClient(cfg *Config, swapDb loopdb.SwapStore, } swapClient, cleanUp, err := loop.NewClient( - cfg.DataDir, swapDb, clientConfig, + cfg.DataDir, swapDb, sweeperDb, clientConfig, ) if err != nil { return nil, nil, err diff --git a/loopd/view.go b/loopd/view.go index aedd39c..79a0a1e 100644 --- a/loopd/view.go +++ b/loopd/view.go @@ -8,6 +8,7 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightninglabs/loop/utils" ) @@ -26,12 +27,16 @@ func view(config *Config, lisCfg *ListenerCfg) error { return err } - swapDb, _, err := openDatabase(config, chainParams) + swapDb, baseDb, err := openDatabase(config, chainParams) if err != nil { return err } - swapClient, cleanup, err := getClient(config, swapDb, &lnd.LndServices) + sweeperDb := sweepbatcher.NewSQLStore(baseDb, chainParams) + + swapClient, cleanup, err := getClient( + config, swapDb, sweeperDb, &lnd.LndServices, + ) if err != nil { return err } diff --git a/loopout.go b/loopout.go index 3bb5f5b..8035b9d 100644 --- a/loopout.go +++ b/loopout.go @@ -1,7 +1,6 @@ package loop import ( - "bytes" "context" "crypto/rand" "crypto/sha256" @@ -12,20 +11,17 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2" - "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" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightninglabs/loop/utils" "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" ) @@ -90,6 +86,7 @@ type loopOutSwap struct { // executeConfig contains extra configuration to execute the swap. type executeConfig struct { sweeper *sweep.Sweeper + batcher *sweepbatcher.Batcher statusChan chan<- SwapInfo blockEpochChan <-chan interface{} timerFactory func(time.Duration) <-chan time.Time @@ -517,7 +514,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error { } // Try to spend htlc and continue (rbf) until a spend has confirmed. - spendDetails, err := s.waitForHtlcSpendConfirmed( + spendTx, err := s.waitForHtlcSpendConfirmedV2( globalCtx, *htlcOutpoint, htlcValue, ) if err != nil { @@ -526,7 +523,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error { // If spend details are nil, we resolved the swap without waiting for // its spend, so we can exit. - if spendDetails == nil { + if spendTx == nil { return nil } @@ -534,7 +531,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error { // don't just try to match with the hash of our sweep tx, because it // may be swept by a different (fee) sweep tx from a previous run. htlcInput, err := swap.GetTxInputByOutpoint( - spendDetails.SpendingTx, htlcOutpoint, + spendTx, htlcOutpoint, ) if err != nil { return err @@ -545,7 +542,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error { s.cost.Server -= htlcValue s.cost.Onchain = htlcValue - - btcutil.Amount(spendDetails.SpendingTx.TxOut[0].Value) + btcutil.Amount(spendTx.TxOut[0].Value) s.state = loopdb.StateSuccess } else { @@ -1004,26 +1001,36 @@ func (s *loopOutSwap) waitForConfirmedHtlc(globalCtx context.Context) ( return txConf, nil } -// waitForHtlcSpendConfirmed waits for the htlc to be spent either by our own -// sweep or a server revocation tx. During this process, this function will try -// to spend the htlc every block by calling spendFunc. -// -// TODO: Improve retry/fee increase mechanism. Once in the mempool, server can -// sweep offchain. So we must make sure we sweep successfully before on-chain -// timeout. -func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, +// waitForHtlcSpendConfirmedV2 waits for the htlc to be spent either by our own +// sweep or a server revocation tx. +func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context, htlcOutpoint wire.OutPoint, htlcValue btcutil.Amount) ( - *chainntnfs.SpendDetail, error) { + *wire.MsgTx, error) { + + spendChan := make(chan *wire.MsgTx) + spendErrChan := make(chan error, 1) + quitChan := make(chan bool, 1) + + defer func() { + quitChan <- true + }() + + notifier := sweepbatcher.SpendNotifier{ + SpendChan: spendChan, + SpendErrChan: spendErrChan, + QuitChan: quitChan, + } + + sweepReq := sweepbatcher.SweepRequest{ + SwapHash: s.hash, + Outpoint: htlcOutpoint, + Value: htlcValue, + Notifier: ¬ifier, + } // Register the htlc spend notification. ctx, cancel := context.WithCancel(globalCtx) defer cancel() - spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn( - ctx, &htlcOutpoint, s.htlc.PkScript, s.InitiationHeight, - ) - if err != nil { - return nil, fmt.Errorf("register spend ntfn: %v", err) - } // Track our payment status so that we can detect whether our off chain // htlc is settled. We track this information to determine whether it is @@ -1040,26 +1047,20 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, // 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(repushDelay) + for { select { // Htlc spend, break loop. - case spendDetails := <-spendChan: - s.log.Infof("Htlc spend by tx: %v", - spendDetails.SpenderTxHash) + case spendTx := <-spendChan: + s.log.Infof("Htlc spend by tx: %v", spendTx.TxHash()) - return spendDetails, nil + return spendTx, nil // Spend notification error. - case err := <-spendErr: + case err := <-spendErrChan: return nil, err // Receive status updates for our payment so that we can detect @@ -1101,121 +1102,51 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, return nil, err } - // New block arrived, update height and restart the republish - // timer. + // New block arrived, update height and try pushing preimage. case notification := <-s.blockEpochChan: s.height = notification.(int32) - timerChan = s.timerFactory(republishDelay) + timerChan = s.timerFactory(repushDelay) - // Some time after start or after arrival of a new block, try - // to spend again. case <-timerChan: - 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") - } - - 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) + // 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 { - 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) + log.Warnf("unable to persist " + + "state") } - // 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 already 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) - } - } + return nil, nil + } - // 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 - } + // Send the sweep to the sweeper. + err := s.batcher.AddSweep(&sweepReq) + 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 - } + // Now that the sweep is taken care of, we can update + // our state. + err = s.setStatePreimageRevealed(ctx) + if err != nil { + return nil, 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 !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) } // Context canceled. @@ -1324,129 +1255,63 @@ func (s *loopOutSwap) failOffChain(ctx context.Context, paymentType paymentType, } } -// 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) { - - // First assemble our taproot keyspend sweep transaction and get the - // sig hash. - sweepTx, sweepTxPsbt, sigHash, err := s.sweeper.CreateUnsignedTaprootKeySpendSweepTx( - ctx, uint32(s.height), s.htlc, htlcOutpoint, htlcValue, fee, - s.DestAddr, - ) - if err != nil { - return nil, err - } - - var ( - signers [][]byte - muSig2Version input.MuSig2Version - ) +func (s *loopOutSwap) setStatePreimageRevealed(ctx context.Context) error { + if s.state != loopdb.StatePreimageRevealed { + s.state = loopdb.StatePreimageRevealed - // Depending on the MuSig2 version we either pass 32 byte Schnorr - // public keys or normal 33 byte public keys. - if s.ProtocolVersion >= loopdb.ProtocolVersionMuSig2 { - muSig2Version = input.MuSig2Version100RC2 - signers = [][]byte{ - s.HtlcKeys.SenderInternalPubKey[:], - s.HtlcKeys.ReceiverInternalPubKey[:], - } - } else { - muSig2Version = input.MuSig2Version040 - signers = [][]byte{ - s.HtlcKeys.SenderInternalPubKey[1:], - s.HtlcKeys.ReceiverInternalPubKey[1:], + err := s.persistState(ctx) + if err != nil { + return err } } - htlcScript, 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, muSig2Version, &s.HtlcKeys.ClientScriptKeyLocator, signers, - lndclient.MuSig2TaprootTweakOpt(htlcScript.RootHash[:], false), - ) - if err != nil { - return nil, err - } + return nil +} - // 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[:], - sweepTxPsbt, - ) - if err != nil { - return nil, err - } +// validateLoopOutContract validates the contract parameters against our +// request. +func validateLoopOutContract(lnd *lndclient.LndServices, request *OutRequest, + swapHash lntypes.Hash, response *newLoopOutResponse) error { - var serverPublicNonce [musig2.PubNonceSize]byte - copy(serverPublicNonce[:], serverNonce) + // Check invoice amounts. + chainParams := lnd.ChainParams - // 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}, + _, _, swapInvoiceHash, swapInvoiceAmt, err := swap.DecodeInvoice( + chainParams, response.swapInvoice, ) if err != nil { - return nil, err + return err } - // Sanity check that we have all the nonces. - if !haveAllNonces { - return nil, fmt.Errorf("invalid MuSig2 session: nonces missing") + if swapInvoiceHash != swapHash { + return fmt.Errorf( + "cannot initiate swap, swap invoice hash %v not equal "+ + "generated swap hash %v", swapInvoiceHash, swapHash) } - 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, + _, _, _, prepayInvoiceAmt, err := swap.DecodeInvoice( + chainParams, response.prepayInvoice, ) if err != nil { - return nil, err + return 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 - } + swapFee := swapInvoiceAmt + prepayInvoiceAmt - request.Amount + if swapFee > request.MaxSwapFee { + log.Warnf("Swap fee %v exceeding maximum of %v", + swapFee, request.MaxSwapFee) - if !haveAllSigs { - return nil, fmt.Errorf("failed to combine signatures") + return ErrSwapFeeTooHigh } - // 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. - err = s.executeConfig.verifySchnorrSig( - htlcScript.TaprootKey, sigHash, finalSig, - ) - if err != nil { - return nil, err - } + if prepayInvoiceAmt > request.MaxPrepayAmount { + log.Warnf("Prepay amount %v exceeding maximum of %v", + prepayInvoiceAmt, request.MaxPrepayAmount) - // Now that we know the signature is correct, we can fill it in to our - // witness. - sweepTx.TxIn[0].Witness = wire.TxWitness{ - finalSig, + return ErrPrepayAmountTooHigh } - return sweepTx, nil + return nil } // sweepConfTarget returns the confirmation target for the htlc sweep or false @@ -1484,215 +1349,3 @@ func (s *loopOutSwap) sweepConfTarget() (int32, bool) { 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 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 0, false - } - } - - 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 { - s.log.Warnf("Failed to estimate fee MuSig2 sweep txn: %v", err) - return false - } - - 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 { - var sweepTxBuf bytes.Buffer - if err := sweepTx.Serialize(&sweepTxBuf); err != nil { - s.log.Warnf("Unable to serialize sweep txn: %v", err) - } - - s.log.Warnf("Publish of MuSig2 sweep failed: %v. Raw tx: %x", - err, sweepTxBuf.Bytes()) - - return false - } - - return true -} - -func (s *loopOutSwap) setStatePreimageRevealed(ctx context.Context) error { - if s.state != loopdb.StatePreimageRevealed { - s.state = loopdb.StatePreimageRevealed - - err := s.persistState(ctx) - if err != nil { - return err - } - } - - 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.contract.HtlcKeys.ReceiverScriptKey, - 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()) - - err = s.lnd.WalletKit.PublishTransaction( - ctx, sweepTx, - labels.LoopOutSweepSuccess(swap.ShortHash(&s.hash)), - ) - if err != nil { - var sweepTxBuf bytes.Buffer - if err := sweepTx.Serialize(&sweepTxBuf); err != nil { - s.log.Warnf("Unable to serialize sweep txn: %v", err) - } - - s.log.Warnf("Publish sweep failed: %v. Raw tx: %x", - err, sweepTxBuf.Bytes()) - } - - return nil -} - -// validateLoopOutContract validates the contract parameters against our -// request. -func validateLoopOutContract(lnd *lndclient.LndServices, request *OutRequest, - swapHash lntypes.Hash, response *newLoopOutResponse) error { - - // Check invoice amounts. - chainParams := lnd.ChainParams - - _, _, swapInvoiceHash, swapInvoiceAmt, err := swap.DecodeInvoice( - chainParams, response.swapInvoice, - ) - if err != nil { - return err - } - - if swapInvoiceHash != swapHash { - return fmt.Errorf( - "cannot initiate swap, swap invoice hash %v not equal "+ - "generated swap hash %v", swapInvoiceHash, swapHash) - } - - _, _, _, prepayInvoiceAmt, err := swap.DecodeInvoice( - chainParams, response.prepayInvoice, - ) - if err != nil { - return err - } - - swapFee := swapInvoiceAmt + prepayInvoiceAmt - request.Amount - if swapFee > request.MaxSwapFee { - log.Warnf("Swap fee %v exceeding maximum of %v", - swapFee, request.MaxSwapFee) - - return ErrSwapFeeTooHigh - } - - if prepayInvoiceAmt > request.MaxPrepayAmount { - log.Warnf("Prepay amount %v exceeding maximum of %v", - prepayInvoiceAmt, request.MaxPrepayAmount) - - return ErrPrepayAmountTooHigh - } - - return nil -} diff --git a/swap_server_client.go b/swap_server_client.go index 359c4dc..87760f3 100644 --- a/swap_server_client.go +++ b/swap_server_client.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/aperture/lsat" "github.com/lightninglabs/loop/loopdb" looprpc "github.com/lightninglabs/loop/swapserverrpc" @@ -775,6 +776,51 @@ func (s *grpcSwapServerClient) MuSig2SignSweep(ctx context.Context, return res.Nonce, res.PartialSignature, nil } +// MultiMuSig2SignSweep calls the server to cooperatively sign an input in +// a batch transaction that attempts to sweep multiple htlcs at once. This +// method is called once per input signed. The prevoutMap is a map of all the +// prevout information for each spend outpoint. Returns the server's nonce and +// partial signature. +func (s *grpcSwapServerClient) MultiMuSig2SignSweep(ctx context.Context, + protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash, + paymentAddr [32]byte, nonce []byte, sweepTxPsbt []byte, + prevoutMap map[wire.OutPoint]*wire.TxOut) ( + []byte, []byte, error) { + + prevOutInfo := make([]*looprpc.PrevoutInfo, 0, len(prevoutMap)) + for prevOut, txOut := range prevoutMap { + txOut := *txOut + prevOut := prevOut + + prevOutInfo = append(prevOutInfo, + &looprpc.PrevoutInfo{ + TxidBytes: prevOut.Hash[:], + OutputIndex: prevOut.Index, + Value: uint64(txOut.Value), + PkScript: txOut.PkScript, + }) + } + + req := &looprpc.MuSig2SignSweepReq{ + ProtocolVersion: looprpc.ProtocolVersion(protocolVersion), + SwapHash: swapHash[:], + PaymentAddress: paymentAddr[:], + Nonce: nonce, + SweepTxPsbt: sweepTxPsbt, + PrevoutInfo: prevOutInfo, + } + + rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) + defer rpcCancel() + + res, err := s.server.MuSig2SignSweep(rpcCtx, req) + if err != nil { + return nil, nil, err + } + + return res.Nonce, res.PartialSignature, nil +} + // PushKey sends the client's HTLC internal key associated with the swap to // the server. func (s *grpcSwapServerClient) PushKey(ctx context.Context,