loop: integrate sweepbatcher to loopout flow

pull/634/head
George Tsagkarelis 3 months ago
parent 849d26bba6
commit 0914074b10
No known key found for this signature in database
GPG Key ID: E08DEA9B12B66AF6

@ -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{

@ -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()
}

@ -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

@ -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

@ -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
}

@ -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: &notifier,
}
// 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
}

@ -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,

Loading…
Cancel
Save