Merge pull request #634 from GeorgeTsagk/sweep-batcher

Loop Out Sweep Batcher
update-to-v0.27.0-beta
George Tsagkarelis 4 months ago committed by GitHub
commit df2db8055b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -17,6 +17,8 @@ import (
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"google.golang.org/grpc" "google.golang.org/grpc"
@ -60,7 +62,7 @@ var (
// probeTimeout is the maximum time until a probe is allowed to take. // probeTimeout is the maximum time until a probe is allowed to take.
probeTimeout = 3 * time.Minute probeTimeout = 3 * time.Minute
republishDelay = 10 * time.Second repushDelay = 1 * time.Second
// MinerFeeEstimationFailed is a magic number that is returned in a // MinerFeeEstimationFailed is a magic number that is returned in a
// quote call as the miner fee if the fee estimation in lnd's wallet // quote call as the miner fee if the fee estimation in lnd's wallet
@ -133,7 +135,8 @@ type ClientConfig struct {
// NewClient returns a new instance to initiate swaps with. // NewClient returns a new instance to initiate swaps with.
func NewClient(dbDir string, loopDB loopdb.SwapStore, 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) lsatStore, err := lsat.NewFileStore(dbDir)
if err != nil { if err != nil {
@ -161,27 +164,36 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
Lnd: cfg.Lnd, 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{ executor := newExecutor(&executorConfig{
lnd: cfg.Lnd, lnd: cfg.Lnd,
store: loopDB, store: loopDB,
sweeper: sweeper, sweeper: sweeper,
batcher: batcher,
createExpiryTimer: config.CreateExpiryTimer, createExpiryTimer: config.CreateExpiryTimer,
loopOutMaxParts: cfg.LoopOutMaxParts, loopOutMaxParts: cfg.LoopOutMaxParts,
totalPaymentTimeout: cfg.TotalPaymentTimeout, totalPaymentTimeout: cfg.TotalPaymentTimeout,
maxPaymentRetries: cfg.MaxPaymentRetries, maxPaymentRetries: cfg.MaxPaymentRetries,
cancelSwap: swapServerClient.CancelLoopOutSwap, cancelSwap: swapServerClient.CancelLoopOutSwap,
verifySchnorrSig: func(pubKey *btcec.PublicKey, hash, sig []byte) error { verifySchnorrSig: verifySchnorrSig,
schnorrSig, err := schnorr.ParseSignature(sig)
if err != nil {
return err
}
if !schnorrSig.Verify(hash, pubKey) {
return fmt.Errorf("invalid signature")
}
return nil
},
}) })
client := &Client{ client := &Client{
@ -232,7 +244,7 @@ func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) {
LastUpdate: swp.LastUpdateTime(), LastUpdate: swp.LastUpdateTime(),
} }
htlc, err := GetHtlc( htlc, err := utils.GetHtlc(
swp.Hash, &swp.Contract.SwapContract, swp.Hash, &swp.Contract.SwapContract,
s.lndServices.ChainParams, s.lndServices.ChainParams,
) )
@ -265,7 +277,7 @@ func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) {
LastUpdate: swp.LastUpdateTime(), LastUpdate: swp.LastUpdateTime(),
} }
htlc, err := GetHtlc( htlc, err := utils.GetHtlc(
swp.Hash, &swp.Contract.SwapContract, swp.Hash, &swp.Contract.SwapContract,
s.lndServices.ChainParams, s.lndServices.ChainParams,
) )
@ -540,7 +552,7 @@ func (s *Client) getLoopOutSweepFee(ctx context.Context, confTarget int32) (
return 0, err return 0, err
} }
scriptVersion := GetHtlcScriptVersion( scriptVersion := utils.GetHtlcScriptVersion(
loopdb.CurrentProtocolVersion(), loopdb.CurrentProtocolVersion(),
) )
@ -731,7 +743,7 @@ func (s *Client) estimateFee(ctx context.Context, amt btcutil.Amount,
// Generate a dummy address for fee estimation. // Generate a dummy address for fee estimation.
witnessProg := [32]byte{} witnessProg := [32]byte{}
scriptVersion := GetHtlcScriptVersion( scriptVersion := utils.GetHtlcScriptVersion(
loopdb.CurrentProtocolVersion(), loopdb.CurrentProtocolVersion(),
) )

@ -13,6 +13,7 @@ import (
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/test"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -146,8 +147,6 @@ func TestLoopOutFailWrongAmount(t *testing.T) {
// TestLoopOutResume tests that swaps in various states are properly resumed // TestLoopOutResume tests that swaps in various states are properly resumed
// after a restart. // after a restart.
func TestLoopOutResume(t *testing.T) { func TestLoopOutResume(t *testing.T) {
defer test.Guard(t)()
defaultConfs := loopdb.DefaultLoopOutHtlcConfirmations defaultConfs := loopdb.DefaultLoopOutHtlcConfirmations
storedVersion := []loopdb.ProtocolVersion{ storedVersion := []loopdb.ProtocolVersion{
@ -279,7 +278,7 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed,
preimageRevealed, int32(confs), preimageRevealed, int32(confs),
) )
htlc, err := GetHtlc( htlc, err := utils.GetHtlc(
hash, &pendingSwap.Contract.SwapContract, hash, &pendingSwap.Contract.SwapContract,
&chaincfg.TestNet3Params, &chaincfg.TestNet3Params,
) )
@ -304,7 +303,7 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed,
func(r error) {}, func(r error) {},
func(r error) {}, func(r error) {},
preimageRevealed, preimageRevealed,
confIntent, GetHtlcScriptVersion(protocolVersion), confIntent, utils.GetHtlcScriptVersion(protocolVersion),
) )
} }
@ -317,15 +316,28 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash,
signalPrepaymentResult(nil) signalPrepaymentResult(nil)
ctx.AssertRegisterSpendNtfn(confIntent.PkScript)
// Assert that a call to track payment was sent, and respond with status // Assert that a call to track payment was sent, and respond with status
// in flight so that our swap will push its preimage to the server. // in flight so that our swap will push its preimage to the server.
ctx.trackPayment(lnrpc.Payment_IN_FLIGHT) ctx.trackPayment(lnrpc.Payment_IN_FLIGHT)
// We need to notify the height, as the loopout is going to attempt a
// sweep when a new block is received.
err := ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1)
require.NoError(ctx.Context.T, err)
// Publish tick. // Publish tick.
ctx.expiryChan <- testTime ctx.expiryChan <- testTime
// One spend notifier is registered by batch to watch primary sweep.
ctx.AssertRegisterSpendNtfn(confIntent.PkScript)
ctx.AssertEpochListeners(2)
// Mock the blockheight again as that's when the batch will broadcast
// the tx.
err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1)
require.NoError(ctx.Context.T, err)
// Expect a signing request in the non taproot case. // Expect a signing request in the non taproot case.
if scriptVersion != swap.HtlcV3 { if scriptVersion != swap.HtlcV3 {
<-ctx.Context.Lnd.SignOutputRawChannel <-ctx.Context.Lnd.SignOutputRawChannel
@ -340,14 +352,7 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash,
// preimage before sweeping in order for the server to trust us with // preimage before sweeping in order for the server to trust us with
// our MuSig2 signing attempts. // our MuSig2 signing attempts.
if scriptVersion == swap.HtlcV3 { if scriptVersion == swap.HtlcV3 {
ctx.assertPreimagePush(ctx.store.loopOutSwaps[hash].Preimage) ctx.assertPreimagePush(ctx.store.LoopOutSwaps[hash].Preimage)
// 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(ctx.store.loopOutSwaps[hash].Preimage)
}
<-ctx.Context.Lnd.SignOutputRawChannel <-ctx.Context.Lnd.SignOutputRawChannel
} }
@ -388,6 +393,8 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash,
ctx.NotifySpend(sweepTx, 0) ctx.NotifySpend(sweepTx, 0)
ctx.AssertRegisterConf(true, 3)
ctx.assertStatus(loopdb.StateSuccess) ctx.assertStatus(loopdb.StateSuccess)
ctx.assertStoreFinished(loopdb.StateSuccess) ctx.assertStoreFinished(loopdb.StateSuccess)

@ -238,6 +238,7 @@ func loopOut(ctx *cli.Context) error {
resp, err := client.LoopOut(context.Background(), &looprpc.LoopOutRequest{ resp, err := client.LoopOut(context.Background(), &looprpc.LoopOutRequest{
Amt: int64(amt), Amt: int64(amt),
Dest: destAddr, Dest: destAddr,
IsExternalAddr: destAddr != "",
Account: account, Account: account,
AccountAddrType: accountAddrType, AccountAddrType: accountAddrType,
MaxMinerFee: int64(limits.maxMinerFee), MaxMinerFee: int64(limits.maxMinerFee),

@ -13,6 +13,7 @@ import (
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/queue" "github.com/lightningnetwork/lnd/queue"
) )
@ -23,6 +24,8 @@ type executorConfig struct {
sweeper *sweep.Sweeper sweeper *sweep.Sweeper
batcher *sweepbatcher.Batcher
store loopdb.SwapStore store loopdb.SwapStore
createExpiryTimer func(expiry time.Duration) <-chan time.Time createExpiryTimer func(expiry time.Duration) <-chan time.Time
@ -71,6 +74,7 @@ func (s *executor) run(mainCtx context.Context,
err error err error
blockEpochChan <-chan int32 blockEpochChan <-chan int32
blockErrorChan <-chan error blockErrorChan <-chan error
batcherErrChan chan error
) )
for { for {
@ -121,6 +125,21 @@ func (s *executor) run(mainCtx context.Context,
return mainCtx.Err() 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. // Start main event loop.
log.Infof("Starting event loop at height %v", height) 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{ err := newSwap.execute(mainCtx, &executeConfig{
statusChan: statusChan, statusChan: statusChan,
sweeper: s.sweeper, sweeper: s.sweeper,
batcher: s.batcher,
blockEpochChan: queue.ChanOut(), blockEpochChan: queue.ChanOut(),
timerFactory: s.executorConfig.createExpiryTimer, timerFactory: s.executorConfig.createExpiryTimer,
loopOutMaxParts: s.executorConfig.loopOutMaxParts, loopOutMaxParts: s.executorConfig.loopOutMaxParts,
@ -211,6 +231,9 @@ func (s *executor) run(mainCtx context.Context,
case err := <-blockErrorChan: case err := <-blockErrorChan:
return fmt.Errorf("block error: %v", err) return fmt.Errorf("block error: %v", err)
case err := <-batcherErrChan:
return fmt.Errorf("batcher error: %v", err)
case <-mainCtx.Done(): case <-mainCtx.Done():
return mainCtx.Err() return mainCtx.Err()
} }

@ -20,6 +20,11 @@ type OutRequest struct {
// Destination address for the swap. // Destination address for the swap.
DestAddr btcutil.Address DestAddr btcutil.Address
// IsExternalAddr indicates whether the provided destination address
// does not belong to the underlying wallet. This helps indicate
// whether the sweep of this swap can be batched or not.
IsExternalAddr bool
// MaxSwapRoutingFee is the maximum off-chain fee in msat that may be // MaxSwapRoutingFee is the maximum off-chain fee in msat that may be
// paid for payment to the server. This limit is applied during path // paid for payment to the server. This limit is applied during path
// finding. Typically this value is taken from the response of the // finding. Typically this value is taken from the response of the

@ -17,6 +17,8 @@ const (
// loopInTimeout is the label used for loop in swaps to sweep an HTLC // loopInTimeout is the label used for loop in swaps to sweep an HTLC
// that has timed out. // that has timed out.
loopInSweepTimeout = "InSweepTimeout" loopInSweepTimeout = "InSweepTimeout"
loopOutBatchSweepSuccess = "BatchOutSweepSuccess -- %d"
) )
// LoopOutSweepSuccess returns the label used for loop out swaps to sweep the // LoopOutSweepSuccess returns the label used for loop out swaps to sweep the
@ -25,6 +27,11 @@ func LoopOutSweepSuccess(swapHash string) string {
return fmt.Sprintf(loopdLabelPattern, loopOutSweepSuccess, swapHash) return fmt.Sprintf(loopdLabelPattern, loopOutSweepSuccess, swapHash)
} }
// LoopOutBatchSweepSuccess returns the label used for loop out sweep batcher.
func LoopOutBatchSweepSuccess(batchID int32) string {
return fmt.Sprintf(loopOutBatchSweepSuccess, batchID)
}
// LoopInHtlcLabel returns the label used for loop in swaps to publish an HTLC. // LoopInHtlcLabel returns the label used for loop in swaps to publish an HTLC.
func LoopInHtlcLabel(swapHash string) string { func LoopInHtlcLabel(swapHash string) string {
return fmt.Sprintf(loopdLabelPattern, loopInHtlc, swapHash) return fmt.Sprintf(loopdLabelPattern, loopInHtlc, swapHash)

@ -422,6 +422,7 @@ func TestAutoloopAddress(t *testing.T) {
Amount: amt, Amount: amt,
// Define the expected destination address. // Define the expected destination address.
DestAddr: addr, DestAddr: addr,
IsExternalAddr: true,
MaxSwapRoutingFee: maxRouteFee, MaxSwapRoutingFee: maxRouteFee,
MaxPrepayRoutingFee: ppmToSat( MaxPrepayRoutingFee: ppmToSat(
quote1.PrepayAmount, prepayFeePPM, quote1.PrepayAmount, prepayFeePPM,
@ -439,6 +440,7 @@ func TestAutoloopAddress(t *testing.T) {
Amount: amt, Amount: amt,
// Define the expected destination address. // Define the expected destination address.
DestAddr: addr, DestAddr: addr,
IsExternalAddr: true,
MaxSwapRoutingFee: maxRouteFee, MaxSwapRoutingFee: maxRouteFee,
MaxPrepayRoutingFee: ppmToSat( MaxPrepayRoutingFee: ppmToSat(
quote2.PrepayAmount, routeFeePPM, quote2.PrepayAmount, routeFeePPM,

@ -450,6 +450,13 @@ func (m *Manager) autoloop(ctx context.Context) error {
// Create a copy of our range var so that we can reference it. // Create a copy of our range var so that we can reference it.
swap := swap swap := swap
// Check if the parameter for custom address is defined for loop
// outs.
if m.params.DestAddr != nil {
swap.DestAddr = m.params.DestAddr
swap.IsExternalAddr = true
}
go m.dispatchStickyLoopOut( go m.dispatchStickyLoopOut(
ctx, swap, defaultAmountBackoffRetry, ctx, swap, defaultAmountBackoffRetry,
defaultAmountBackoff, defaultAmountBackoff,

@ -138,6 +138,7 @@ func (b *loopOutBuilder) buildSwap(ctx context.Context, pubkey route.Vertex,
// already validated them. // already validated them.
request := loop.OutRequest{ request := loop.OutRequest{
Amount: amount, Amount: amount,
IsExternalAddr: false,
OutgoingChanSet: chanSet, OutgoingChanSet: chanSet,
MaxPrepayRoutingFee: prepayMaxFee, MaxPrepayRoutingFee: prepayMaxFee,
MaxSwapRoutingFee: routeMaxFee, MaxSwapRoutingFee: routeMaxFee,
@ -160,9 +161,11 @@ func (b *loopOutBuilder) buildSwap(ctx context.Context, pubkey route.Vertex,
if len(params.Account) > 0 { if len(params.Account) > 0 {
account = params.Account account = params.Account
addrType = params.AccountAddrType addrType = params.AccountAddrType
request.IsExternalAddr = true
} }
if params.DestAddr != nil { if params.DestAddr != nil {
request.DestAddr = params.DestAddr request.DestAddr = params.DestAddr
request.IsExternalAddr = true
} else { } else {
addr, err := b.cfg.Lnd.WalletKit.NextAddr( addr, err := b.cfg.Lnd.WalletKit.NextAddr(
ctx, account, addrType, false, ctx, account, addrType, false,

@ -18,6 +18,7 @@ import (
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/loopd/perms" "github.com/lightninglabs/loop/loopd/perms"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/instantout/reservation" "github.com/lightninglabs/loop/instantout/reservation"
loop_looprpc "github.com/lightninglabs/loop/looprpc" loop_looprpc "github.com/lightninglabs/loop/looprpc"
@ -412,9 +413,11 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
return err return err
} }
sweeperDb := sweepbatcher.NewSQLStore(baseDb, chainParams)
// Create an instance of the loop client library. // Create an instance of the loop client library.
swapClient, clientCleanup, err := getClient( swapClient, clientCleanup, err := getClient(
d.cfg, swapDb, &d.lnd.LndServices, d.cfg, swapDb, sweeperDb, &d.lnd.LndServices,
) )
if err != nil { if err != nil {
return err return err

@ -9,6 +9,7 @@ import (
"github.com/lightninglabs/loop/instantout/reservation" "github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/liquidity"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/signal"
@ -32,6 +33,7 @@ func SetupLoggers(root *build.RotatingLogWriter, intercept signal.Interceptor) {
lnd.SetSubLogger(root, Subsystem, log) lnd.SetSubLogger(root, Subsystem, log)
lnd.AddSubLogger(root, "LOOP", intercept, loop.UseLogger) lnd.AddSubLogger(root, "LOOP", intercept, loop.UseLogger)
lnd.AddSubLogger(root, "SWEEP", intercept, sweepbatcher.UseLogger)
lnd.AddSubLogger(root, "LNDC", intercept, lndclient.UseLogger) lnd.AddSubLogger(root, "LNDC", intercept, lndclient.UseLogger)
lnd.AddSubLogger(root, "STORE", intercept, loopdb.UseLogger) lnd.AddSubLogger(root, "STORE", intercept, loopdb.UseLogger)
lnd.AddSubLogger(root, lsat.Subsystem, intercept, lsat.UseLogger) lnd.AddSubLogger(root, lsat.Subsystem, intercept, lsat.UseLogger)

@ -100,6 +100,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
log.Infof("Loop out request received") log.Infof("Loop out request received")
var sweepAddr btcutil.Address var sweepAddr btcutil.Address
var isExternalAddr bool
var err error var err error
//nolint:lll //nolint:lll
switch { switch {
@ -117,6 +118,8 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
return nil, fmt.Errorf("decode address: %v", err) return nil, fmt.Errorf("decode address: %v", err)
} }
isExternalAddr = true
case in.Account != "" && in.AccountAddrType == clientrpc.AddressType_ADDRESS_TYPE_UNKNOWN: case in.Account != "" && in.AccountAddrType == clientrpc.AddressType_ADDRESS_TYPE_UNKNOWN:
return nil, liquidity.ErrAccountAndAddrType return nil, liquidity.ErrAccountAndAddrType
@ -141,6 +144,8 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
"%v", err) "%v", err)
} }
isExternalAddr = true
default: default:
// Generate sweep address if none specified. // Generate sweep address if none specified.
sweepAddr, err = s.lnd.WalletKit.NextAddr( sweepAddr, err = s.lnd.WalletKit.NextAddr(
@ -166,6 +171,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
req := &loop.OutRequest{ req := &loop.OutRequest{
Amount: btcutil.Amount(in.Amt), Amount: btcutil.Amount(in.Amt),
DestAddr: sweepAddr, DestAddr: sweepAddr,
IsExternalAddr: isExternalAddr,
MaxMinerFee: btcutil.Amount(in.MaxMinerFee), MaxMinerFee: btcutil.Amount(in.MaxMinerFee),
MaxPrepayAmount: btcutil.Amount(in.MaxPrepayAmt), MaxPrepayAmount: btcutil.Amount(in.MaxPrepayAmt),
MaxPrepayRoutingFee: btcutil.Amount(in.MaxPrepayRoutingFee), MaxPrepayRoutingFee: btcutil.Amount(in.MaxPrepayRoutingFee),

@ -11,13 +11,15 @@ import (
"github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/liquidity"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/ticker" "github.com/lightningnetwork/lnd/ticker"
) )
// getClient returns an instance of the swap client. // getClient returns an instance of the swap client.
func getClient(cfg *Config, swapDb loopdb.SwapStore, 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{ clientConfig := &loop.ClientConfig{
ServerAddress: cfg.Server.Host, ServerAddress: cfg.Server.Host,
@ -33,7 +35,7 @@ func getClient(cfg *Config, swapDb loopdb.SwapStore,
} }
swapClient, cleanUp, err := loop.NewClient( swapClient, cleanUp, err := loop.NewClient(
cfg.DataDir, swapDb, clientConfig, cfg.DataDir, swapDb, sweeperDb, clientConfig,
) )
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

@ -8,6 +8,8 @@ import (
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/utils"
) )
// view prints all swaps currently in the database. // view prints all swaps currently in the database.
@ -25,12 +27,16 @@ func view(config *Config, lisCfg *ListenerCfg) error {
return err return err
} }
swapDb, _, err := openDatabase(config, chainParams) swapDb, baseDb, err := openDatabase(config, chainParams)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -56,7 +62,7 @@ func viewOut(swapClient *loop.Client, chainParams *chaincfg.Params) error {
for _, s := range swaps { for _, s := range swaps {
s := s s := s
htlc, err := loop.GetHtlc( htlc, err := utils.GetHtlc(
s.Hash, &s.Contract.SwapContract, chainParams, s.Hash, &s.Contract.SwapContract, chainParams,
) )
if err != nil { if err != nil {
@ -107,7 +113,7 @@ func viewIn(swapClient *loop.Client, chainParams *chaincfg.Params) error {
for _, s := range swaps { for _, s := range swaps {
s := s s := s
htlc, err := loop.GetHtlc( htlc, err := utils.GetHtlc(
s.Hash, &s.Contract.SwapContract, chainParams, s.Hash, &s.Contract.SwapContract, chainParams,
) )
if err != nil { if err != nil {

@ -24,6 +24,10 @@ type LoopOutContract struct {
// DestAddr is the destination address of the loop out swap. // DestAddr is the destination address of the loop out swap.
DestAddr btcutil.Address DestAddr btcutil.Address
// IsExternalAddr indicates whether the destination address does not
// belong to the backing lnd node.
IsExternalAddr bool
// SwapInvoice is the invoice that is to be paid by the client to // SwapInvoice is the invoice that is to be paid by the client to
// initiate the loop out swap. // initiate the loop out swap.
SwapInvoice string SwapInvoice string

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightninglabs/loop/loopdb/sqlc" "github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
@ -31,13 +32,16 @@ func (s *BaseDB) FetchLoopOutSwaps(ctx context.Context) ([]*LoopOut,
loopOuts = make([]*LoopOut, len(swaps)) loopOuts = make([]*LoopOut, len(swaps))
for i, swap := range swaps { for i, swap := range swaps {
updates, err := s.Queries.GetSwapUpdates(ctx, swap.SwapHash) updates, err := s.Queries.GetSwapUpdates(
ctx, swap.SwapHash,
)
if err != nil { if err != nil {
return err return err
} }
loopOut, err := s.convertLoopOutRow( loopOut, err := ConvertLoopOutRow(
sqlc.GetLoopOutSwapRow(swap), updates, s.network, sqlc.GetLoopOutSwapRow(swap),
updates,
) )
if err != nil { if err != nil {
return err return err
@ -72,8 +76,8 @@ func (s *BaseDB) FetchLoopOutSwap(ctx context.Context,
return err return err
} }
loopOut, err = s.convertLoopOutRow( loopOut, err = ConvertLoopOutRow(
swap, updates, s.network, swap, updates,
) )
if err != nil { if err != nil {
return err return err
@ -430,6 +434,7 @@ func loopOutToInsertArgs(hash lntypes.Hash,
return sqlc.InsertLoopOutParams{ return sqlc.InsertLoopOutParams{
SwapHash: hash[:], SwapHash: hash[:],
DestAddress: loopOut.DestAddr.String(), DestAddress: loopOut.DestAddr.String(),
SingleSweep: loopOut.IsExternalAddr,
SwapInvoice: loopOut.SwapInvoice, SwapInvoice: loopOut.SwapInvoice,
MaxSwapRoutingFee: int64(loopOut.MaxSwapRoutingFee), MaxSwapRoutingFee: int64(loopOut.MaxSwapRoutingFee),
SweepConfTarget: loopOut.SweepConfTarget, SweepConfTarget: loopOut.SweepConfTarget,
@ -479,9 +484,9 @@ func swapToHtlcKeysInsertArgs(hash lntypes.Hash,
} }
} }
// convertLoopOutRow converts a database row containing a loop out swap to a // ConvertLoopOutRow converts a database row containing a loop out swap to a
// LoopOut struct. // LoopOut struct.
func (s *BaseDB) convertLoopOutRow(row sqlc.GetLoopOutSwapRow, func ConvertLoopOutRow(network *chaincfg.Params, row sqlc.GetLoopOutSwapRow,
updates []sqlc.SwapUpdate) (*LoopOut, error) { updates []sqlc.SwapUpdate) (*LoopOut, error) {
htlcKeys, err := fetchHtlcKeys( htlcKeys, err := fetchHtlcKeys(
@ -498,7 +503,7 @@ func (s *BaseDB) convertLoopOutRow(row sqlc.GetLoopOutSwapRow,
return nil, err return nil, err
} }
destAddress, err := btcutil.DecodeAddress(row.DestAddress, s.network) destAddress, err := btcutil.DecodeAddress(row.DestAddress, network)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -523,6 +528,7 @@ func (s *BaseDB) convertLoopOutRow(row sqlc.GetLoopOutSwapRow,
ProtocolVersion: ProtocolVersion(row.ProtocolVersion), ProtocolVersion: ProtocolVersion(row.ProtocolVersion),
}, },
DestAddr: destAddress, DestAddr: destAddress,
IsExternalAddr: row.SingleSweep,
SwapInvoice: row.SwapInvoice, SwapInvoice: row.SwapInvoice,
MaxSwapRoutingFee: btcutil.Amount(row.MaxSwapRoutingFee), MaxSwapRoutingFee: btcutil.Amount(row.MaxSwapRoutingFee),
SweepConfTarget: row.SweepConfTarget, SweepConfTarget: row.SweepConfTarget,

@ -0,0 +1,317 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.17.2
// source: batch.sql
package sqlc
import (
"context"
"database/sql"
"time"
)
const confirmBatch = `-- name: ConfirmBatch :exec
UPDATE
sweep_batches
SET
confirmed = TRUE
WHERE
id = $1
`
func (q *Queries) ConfirmBatch(ctx context.Context, id int32) error {
_, err := q.db.ExecContext(ctx, confirmBatch, id)
return err
}
const getBatchSweeps = `-- name: GetBatchSweeps :many
SELECT
sweeps.id, sweeps.swap_hash, sweeps.batch_id, sweeps.outpoint_txid, sweeps.outpoint_index, sweeps.amt, sweeps.completed,
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.single_sweep,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM
sweeps
JOIN
swaps ON sweeps.swap_hash = swaps.swap_hash
JOIN
loopout_swaps ON sweeps.swap_hash = loopout_swaps.swap_hash
JOIN
htlc_keys ON sweeps.swap_hash = htlc_keys.swap_hash
WHERE
sweeps.batch_id = $1
ORDER BY
sweeps.id ASC
`
type GetBatchSweepsRow struct {
ID int32
SwapHash []byte
BatchID int32
OutpointTxid []byte
OutpointIndex int32
Amt int64
Completed bool
ID_2 int32
SwapHash_2 []byte
Preimage []byte
InitiationTime time.Time
AmountRequested int64
CltvExpiry int32
MaxMinerFee int64
MaxSwapFee int64
InitiationHeight int32
ProtocolVersion int32
Label string
SwapHash_3 []byte
DestAddress string
SwapInvoice string
MaxSwapRoutingFee int64
SweepConfTarget int32
HtlcConfirmations int32
OutgoingChanSet string
PrepayInvoice string
MaxPrepayRoutingFee int64
PublicationDeadline time.Time
SingleSweep bool
SwapHash_4 []byte
SenderScriptPubkey []byte
ReceiverScriptPubkey []byte
SenderInternalPubkey []byte
ReceiverInternalPubkey []byte
ClientKeyFamily int32
ClientKeyIndex int32
}
func (q *Queries) GetBatchSweeps(ctx context.Context, batchID int32) ([]GetBatchSweepsRow, error) {
rows, err := q.db.QueryContext(ctx, getBatchSweeps, batchID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetBatchSweepsRow
for rows.Next() {
var i GetBatchSweepsRow
if err := rows.Scan(
&i.ID,
&i.SwapHash,
&i.BatchID,
&i.OutpointTxid,
&i.OutpointIndex,
&i.Amt,
&i.Completed,
&i.ID_2,
&i.SwapHash_2,
&i.Preimage,
&i.InitiationTime,
&i.AmountRequested,
&i.CltvExpiry,
&i.MaxMinerFee,
&i.MaxSwapFee,
&i.InitiationHeight,
&i.ProtocolVersion,
&i.Label,
&i.SwapHash_3,
&i.DestAddress,
&i.SwapInvoice,
&i.MaxSwapRoutingFee,
&i.SweepConfTarget,
&i.HtlcConfirmations,
&i.OutgoingChanSet,
&i.PrepayInvoice,
&i.MaxPrepayRoutingFee,
&i.PublicationDeadline,
&i.SingleSweep,
&i.SwapHash_4,
&i.SenderScriptPubkey,
&i.ReceiverScriptPubkey,
&i.SenderInternalPubkey,
&i.ReceiverInternalPubkey,
&i.ClientKeyFamily,
&i.ClientKeyIndex,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSweepStatus = `-- name: GetSweepStatus :one
SELECT
COALESCE(s.completed, f.false_value) AS completed
FROM
(SELECT false AS false_value) AS f
LEFT JOIN
sweeps s ON s.swap_hash = $1
`
func (q *Queries) GetSweepStatus(ctx context.Context, swapHash []byte) (bool, error) {
row := q.db.QueryRowContext(ctx, getSweepStatus, swapHash)
var completed bool
err := row.Scan(&completed)
return completed, err
}
const getUnconfirmedBatches = `-- name: GetUnconfirmedBatches :many
SELECT
id, confirmed, batch_tx_id, batch_pk_script, last_rbf_height, last_rbf_sat_per_kw, max_timeout_distance
FROM
sweep_batches
WHERE
confirmed = FALSE
`
func (q *Queries) GetUnconfirmedBatches(ctx context.Context) ([]SweepBatch, error) {
rows, err := q.db.QueryContext(ctx, getUnconfirmedBatches)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SweepBatch
for rows.Next() {
var i SweepBatch
if err := rows.Scan(
&i.ID,
&i.Confirmed,
&i.BatchTxID,
&i.BatchPkScript,
&i.LastRbfHeight,
&i.LastRbfSatPerKw,
&i.MaxTimeoutDistance,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertBatch = `-- name: InsertBatch :one
INSERT INTO sweep_batches (
confirmed,
batch_tx_id,
batch_pk_script,
last_rbf_height,
last_rbf_sat_per_kw,
max_timeout_distance
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
) RETURNING id
`
type InsertBatchParams struct {
Confirmed bool
BatchTxID sql.NullString
BatchPkScript []byte
LastRbfHeight sql.NullInt32
LastRbfSatPerKw sql.NullInt32
MaxTimeoutDistance int32
}
func (q *Queries) InsertBatch(ctx context.Context, arg InsertBatchParams) (int32, error) {
row := q.db.QueryRowContext(ctx, insertBatch,
arg.Confirmed,
arg.BatchTxID,
arg.BatchPkScript,
arg.LastRbfHeight,
arg.LastRbfSatPerKw,
arg.MaxTimeoutDistance,
)
var id int32
err := row.Scan(&id)
return id, err
}
const updateBatch = `-- name: UpdateBatch :exec
UPDATE sweep_batches SET
confirmed = $2,
batch_tx_id = $3,
batch_pk_script = $4,
last_rbf_height = $5,
last_rbf_sat_per_kw = $6
WHERE id = $1
`
type UpdateBatchParams struct {
ID int32
Confirmed bool
BatchTxID sql.NullString
BatchPkScript []byte
LastRbfHeight sql.NullInt32
LastRbfSatPerKw sql.NullInt32
}
func (q *Queries) UpdateBatch(ctx context.Context, arg UpdateBatchParams) error {
_, err := q.db.ExecContext(ctx, updateBatch,
arg.ID,
arg.Confirmed,
arg.BatchTxID,
arg.BatchPkScript,
arg.LastRbfHeight,
arg.LastRbfSatPerKw,
)
return err
}
const upsertSweep = `-- name: UpsertSweep :exec
INSERT INTO sweeps (
swap_hash,
batch_id,
outpoint_txid,
outpoint_index,
amt,
completed
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
) ON CONFLICT (swap_hash) DO UPDATE SET
batch_id = $2,
outpoint_txid = $3,
outpoint_index = $4,
amt = $5,
completed = $6
`
type UpsertSweepParams struct {
SwapHash []byte
BatchID int32
OutpointTxid []byte
OutpointIndex int32
Amt int64
Completed bool
}
func (q *Queries) UpsertSweep(ctx context.Context, arg UpsertSweepParams) error {
_, err := q.db.ExecContext(ctx, upsertSweep,
arg.SwapHash,
arg.BatchID,
arg.OutpointTxid,
arg.OutpointIndex,
arg.Amt,
arg.Completed,
)
return err
}

@ -0,0 +1 @@
ALTER TABLE loopout_swaps DROP COLUMN single_sweep;

@ -0,0 +1,4 @@
-- is_external_addr indicates whether the destination address of the swap is not
-- a wallet address. The default value used is TRUE in order to maintain the old
-- behavior of swaps which doesn't override the destination address.
ALTER TABLE loopout_swaps ADD single_sweep BOOLEAN NOT NULL DEFAULT TRUE;

@ -0,0 +1,2 @@
DROP TABLE sweep_batches;
DROP TABLE sweeps;

@ -0,0 +1,58 @@
-- sweep_batches stores the on-going swaps that are batched together.
CREATE TABLE sweep_batches (
-- id is the autoincrementing primary key of the batch.
id INTEGER PRIMARY KEY,
-- confirmed indicates whether this batch is confirmed.
confirmed BOOLEAN NOT NULL DEFAULT FALSE,
-- batch_tx_id is the transaction id of the batch transaction.
batch_tx_id TEXT,
-- batch_pk_script is the pkscript of the batch transaction's output.
batch_pk_script BLOB,
-- last_rbf_height was the last height at which we attempted to publish
-- an rbf replacement transaction.
last_rbf_height INTEGER,
-- last_rbf_sat_per_kw was the last sat per kw fee rate we used for the
-- last published transaction.
last_rbf_sat_per_kw INTEGER,
-- max_timeout_distance is the maximum distance the timeouts of the
-- sweeps can have in the batch.
max_timeout_distance INTEGER NOT NULL
);
-- sweeps stores the individual sweeps that are part of a batch.
CREATE TABLE sweeps (
-- id is the autoincrementing primary key.
id INTEGER PRIMARY KEY,
-- swap_hash is the hash of the swap that is being swept.
swap_hash BLOB NOT NULL UNIQUE,
-- batch_id is the id of the batch this swap is part of.
batch_id INTEGER NOT NULL,
-- outpoint_txid is the transaction id of the output being swept.
outpoint_txid BLOB NOT NULL,
-- outpoint_index is the index of the output being swept.
outpoint_index INTEGER NOT NULL,
-- amt is the amount of the output being swept.
amt BIGINT NOT NULL,
-- completed indicates whether the sweep has been completed.
completed BOOLEAN NOT NULL DEFAULT FALSE,
-- Foreign key constraint to ensure that we reference an existing batch
-- id.
FOREIGN KEY (batch_id) REFERENCES sweep_batches(id),
-- Foreign key constraint to ensure that swap_hash references an
-- existing swap.
FOREIGN KEY (swap_hash) REFERENCES swaps(swap_hash)
);

@ -42,6 +42,7 @@ type LoopoutSwap struct {
PrepayInvoice string PrepayInvoice string
MaxPrepayRoutingFee int64 MaxPrepayRoutingFee int64
PublicationDeadline time.Time PublicationDeadline time.Time
SingleSweep bool
} }
type Reservation struct { type Reservation struct {
@ -90,3 +91,23 @@ type SwapUpdate struct {
OnchainCost int64 OnchainCost int64
OffchainCost int64 OffchainCost int64
} }
type Sweep struct {
ID int32
SwapHash []byte
BatchID int32
OutpointTxid []byte
OutpointIndex int32
Amt int64
Completed bool
}
type SweepBatch struct {
ID int32
Confirmed bool
BatchTxID sql.NullString
BatchPkScript []byte
LastRbfHeight sql.NullInt32
LastRbfSatPerKw sql.NullInt32
MaxTimeoutDistance int32
}

@ -9,8 +9,10 @@ import (
) )
type Querier interface { type Querier interface {
ConfirmBatch(ctx context.Context, id int32) error
CreateReservation(ctx context.Context, arg CreateReservationParams) error CreateReservation(ctx context.Context, arg CreateReservationParams) error
FetchLiquidityParams(ctx context.Context) ([]byte, error) FetchLiquidityParams(ctx context.Context) ([]byte, error)
GetBatchSweeps(ctx context.Context, batchID int32) ([]GetBatchSweepsRow, error)
GetLoopInSwap(ctx context.Context, swapHash []byte) (GetLoopInSwapRow, error) GetLoopInSwap(ctx context.Context, swapHash []byte) (GetLoopInSwapRow, error)
GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, error) GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, error)
GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopOutSwapRow, error) GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopOutSwapRow, error)
@ -19,14 +21,19 @@ type Querier interface {
GetReservationUpdates(ctx context.Context, reservationID []byte) ([]ReservationUpdate, error) GetReservationUpdates(ctx context.Context, reservationID []byte) ([]ReservationUpdate, error)
GetReservations(ctx context.Context) ([]Reservation, error) GetReservations(ctx context.Context) ([]Reservation, error)
GetSwapUpdates(ctx context.Context, swapHash []byte) ([]SwapUpdate, error) GetSwapUpdates(ctx context.Context, swapHash []byte) ([]SwapUpdate, error)
GetSweepStatus(ctx context.Context, swapHash []byte) (bool, error)
GetUnconfirmedBatches(ctx context.Context) ([]SweepBatch, error)
InsertBatch(ctx context.Context, arg InsertBatchParams) (int32, error)
InsertHtlcKeys(ctx context.Context, arg InsertHtlcKeysParams) error InsertHtlcKeys(ctx context.Context, arg InsertHtlcKeysParams) error
InsertLoopIn(ctx context.Context, arg InsertLoopInParams) error InsertLoopIn(ctx context.Context, arg InsertLoopInParams) error
InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) error InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) error
InsertReservationUpdate(ctx context.Context, arg InsertReservationUpdateParams) error InsertReservationUpdate(ctx context.Context, arg InsertReservationUpdateParams) error
InsertSwap(ctx context.Context, arg InsertSwapParams) error InsertSwap(ctx context.Context, arg InsertSwapParams) error
InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error
UpdateBatch(ctx context.Context, arg UpdateBatchParams) error
UpdateReservation(ctx context.Context, arg UpdateReservationParams) error UpdateReservation(ctx context.Context, arg UpdateReservationParams) error
UpsertLiquidityParams(ctx context.Context, params []byte) error UpsertLiquidityParams(ctx context.Context, params []byte) error
UpsertSweep(ctx context.Context, arg UpsertSweepParams) error
} }
var _ Querier = (*Queries)(nil) var _ Querier = (*Queries)(nil)

@ -0,0 +1,91 @@
-- name: GetUnconfirmedBatches :many
SELECT
*
FROM
sweep_batches
WHERE
confirmed = FALSE;
-- name: InsertBatch :one
INSERT INTO sweep_batches (
confirmed,
batch_tx_id,
batch_pk_script,
last_rbf_height,
last_rbf_sat_per_kw,
max_timeout_distance
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
) RETURNING id;
-- name: UpdateBatch :exec
UPDATE sweep_batches SET
confirmed = $2,
batch_tx_id = $3,
batch_pk_script = $4,
last_rbf_height = $5,
last_rbf_sat_per_kw = $6
WHERE id = $1;
-- name: ConfirmBatch :exec
UPDATE
sweep_batches
SET
confirmed = TRUE
WHERE
id = $1;
-- name: UpsertSweep :exec
INSERT INTO sweeps (
swap_hash,
batch_id,
outpoint_txid,
outpoint_index,
amt,
completed
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
) ON CONFLICT (swap_hash) DO UPDATE SET
batch_id = $2,
outpoint_txid = $3,
outpoint_index = $4,
amt = $5,
completed = $6;
-- name: GetBatchSweeps :many
SELECT
sweeps.*,
swaps.*,
loopout_swaps.*,
htlc_keys.*
FROM
sweeps
JOIN
swaps ON sweeps.swap_hash = swaps.swap_hash
JOIN
loopout_swaps ON sweeps.swap_hash = loopout_swaps.swap_hash
JOIN
htlc_keys ON sweeps.swap_hash = htlc_keys.swap_hash
WHERE
sweeps.batch_id = $1
ORDER BY
sweeps.id ASC;
-- name: GetSweepStatus :one
SELECT
COALESCE(s.completed, f.false_value) AS completed
FROM
(SELECT false AS false_value) AS f
LEFT JOIN
sweeps s ON s.swap_hash = $1;

@ -104,9 +104,10 @@ INSERT INTO loopout_swaps (
outgoing_chan_set, outgoing_chan_set,
prepay_invoice, prepay_invoice,
max_prepay_routing_fee, max_prepay_routing_fee,
publication_deadline publication_deadline,
single_sweep
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
); );
-- name: InsertLoopIn :exec -- name: InsertLoopIn :exec

@ -169,7 +169,7 @@ func (q *Queries) GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, erro
const getLoopOutSwap = `-- name: GetLoopOutSwap :one const getLoopOutSwap = `-- name: GetLoopOutSwap :one
SELECT SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label, swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.single_sweep,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM FROM
swaps swaps
@ -203,6 +203,7 @@ type GetLoopOutSwapRow struct {
PrepayInvoice string PrepayInvoice string
MaxPrepayRoutingFee int64 MaxPrepayRoutingFee int64
PublicationDeadline time.Time PublicationDeadline time.Time
SingleSweep bool
SwapHash_3 []byte SwapHash_3 []byte
SenderScriptPubkey []byte SenderScriptPubkey []byte
ReceiverScriptPubkey []byte ReceiverScriptPubkey []byte
@ -237,6 +238,7 @@ func (q *Queries) GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopO
&i.PrepayInvoice, &i.PrepayInvoice,
&i.MaxPrepayRoutingFee, &i.MaxPrepayRoutingFee,
&i.PublicationDeadline, &i.PublicationDeadline,
&i.SingleSweep,
&i.SwapHash_3, &i.SwapHash_3,
&i.SenderScriptPubkey, &i.SenderScriptPubkey,
&i.ReceiverScriptPubkey, &i.ReceiverScriptPubkey,
@ -251,7 +253,7 @@ func (q *Queries) GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopO
const getLoopOutSwaps = `-- name: GetLoopOutSwaps :many const getLoopOutSwaps = `-- name: GetLoopOutSwaps :many
SELECT SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label, swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.single_sweep,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM FROM
swaps swaps
@ -285,6 +287,7 @@ type GetLoopOutSwapsRow struct {
PrepayInvoice string PrepayInvoice string
MaxPrepayRoutingFee int64 MaxPrepayRoutingFee int64
PublicationDeadline time.Time PublicationDeadline time.Time
SingleSweep bool
SwapHash_3 []byte SwapHash_3 []byte
SenderScriptPubkey []byte SenderScriptPubkey []byte
ReceiverScriptPubkey []byte ReceiverScriptPubkey []byte
@ -325,6 +328,7 @@ func (q *Queries) GetLoopOutSwaps(ctx context.Context) ([]GetLoopOutSwapsRow, er
&i.PrepayInvoice, &i.PrepayInvoice,
&i.MaxPrepayRoutingFee, &i.MaxPrepayRoutingFee,
&i.PublicationDeadline, &i.PublicationDeadline,
&i.SingleSweep,
&i.SwapHash_3, &i.SwapHash_3,
&i.SenderScriptPubkey, &i.SenderScriptPubkey,
&i.ReceiverScriptPubkey, &i.ReceiverScriptPubkey,
@ -465,9 +469,10 @@ INSERT INTO loopout_swaps (
outgoing_chan_set, outgoing_chan_set,
prepay_invoice, prepay_invoice,
max_prepay_routing_fee, max_prepay_routing_fee,
publication_deadline publication_deadline,
single_sweep
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
) )
` `
@ -482,6 +487,7 @@ type InsertLoopOutParams struct {
PrepayInvoice string PrepayInvoice string
MaxPrepayRoutingFee int64 MaxPrepayRoutingFee int64
PublicationDeadline time.Time PublicationDeadline time.Time
SingleSweep bool
} }
func (q *Queries) InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) error { func (q *Queries) InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) error {
@ -496,6 +502,7 @@ func (q *Queries) InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) er
arg.PrepayInvoice, arg.PrepayInvoice,
arg.MaxPrepayRoutingFee, arg.MaxPrepayRoutingFee,
arg.PublicationDeadline, arg.PublicationDeadline,
arg.SingleSweep,
) )
return err return err
} }

@ -137,6 +137,9 @@ var (
// errInvalidKey is returned when a serialized key is not the expected // errInvalidKey is returned when a serialized key is not the expected
// length. // length.
errInvalidKey = fmt.Errorf("invalid serialized key") errInvalidKey = fmt.Errorf("invalid serialized key")
// errUnimplemented is returned when a method is not implemented.
errUnimplemented = fmt.Errorf("unimplemented method")
) )
const ( const (
@ -990,19 +993,19 @@ func (s *boltSwapStore) fetchLoopInSwap(rootBucket *bbolt.Bucket,
func (b *boltSwapStore) BatchCreateLoopOut(ctx context.Context, func (b *boltSwapStore) BatchCreateLoopOut(ctx context.Context,
swaps map[lntypes.Hash]*LoopOutContract) error { swaps map[lntypes.Hash]*LoopOutContract) error {
return errors.New("not implemented") return errUnimplemented
} }
// BatchCreateLoopIn creates a batch of loop in swaps to the store. // BatchCreateLoopIn creates a batch of loop in swaps to the store.
func (b *boltSwapStore) BatchCreateLoopIn(ctx context.Context, func (b *boltSwapStore) BatchCreateLoopIn(ctx context.Context,
swaps map[lntypes.Hash]*LoopInContract) error { swaps map[lntypes.Hash]*LoopInContract) error {
return errors.New("not implemented") return errUnimplemented
} }
// BatchInsertUpdate inserts batch of swap updates to the store. // BatchInsertUpdate inserts batch of swap updates to the store.
func (b *boltSwapStore) BatchInsertUpdate(ctx context.Context, func (b *boltSwapStore) BatchInsertUpdate(ctx context.Context,
updateData map[lntypes.Hash][]BatchInsertUpdateData) error { updateData map[lntypes.Hash][]BatchInsertUpdateData) error {
return errors.New("not implemented") return errUnimplemented
} }

@ -0,0 +1,339 @@
package loopdb
import (
"context"
"errors"
"testing"
"time"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)
// StoreMock implements a mock client swap store.
type StoreMock struct {
LoopOutSwaps map[lntypes.Hash]*LoopOutContract
LoopOutUpdates map[lntypes.Hash][]SwapStateData
loopOutStoreChan chan LoopOutContract
loopOutUpdateChan chan SwapStateData
LoopInSwaps map[lntypes.Hash]*LoopInContract
LoopInUpdates map[lntypes.Hash][]SwapStateData
loopInStoreChan chan LoopInContract
loopInUpdateChan chan SwapStateData
t *testing.T
}
// NewStoreMock instantiates a new mock store.
func NewStoreMock(t *testing.T) *StoreMock {
return &StoreMock{
loopOutStoreChan: make(chan LoopOutContract, 1),
loopOutUpdateChan: make(chan SwapStateData, 1),
LoopOutSwaps: make(map[lntypes.Hash]*LoopOutContract),
LoopOutUpdates: make(map[lntypes.Hash][]SwapStateData),
loopInStoreChan: make(chan LoopInContract, 1),
loopInUpdateChan: make(chan SwapStateData, 1),
LoopInSwaps: make(map[lntypes.Hash]*LoopInContract),
LoopInUpdates: make(map[lntypes.Hash][]SwapStateData),
t: t,
}
}
// FetchLoopOutSwaps returns all swaps currently in the store.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) FetchLoopOutSwaps(ctx context.Context) ([]*LoopOut, error) {
result := []*LoopOut{}
for hash, contract := range s.LoopOutSwaps {
updates := s.LoopOutUpdates[hash]
events := make([]*LoopEvent, len(updates))
for i, u := range updates {
events[i] = &LoopEvent{
SwapStateData: u,
}
}
swap := &LoopOut{
Loop: Loop{
Hash: hash,
Events: events,
},
Contract: contract,
}
result = append(result, swap)
}
return result, nil
}
// FetchLoopOutSwaps returns all swaps currently in the store.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) FetchLoopOutSwap(ctx context.Context,
hash lntypes.Hash) (*LoopOut, error) {
contract, ok := s.LoopOutSwaps[hash]
if !ok {
return nil, errors.New("swap not found")
}
updates := s.LoopOutUpdates[hash]
events := make([]*LoopEvent, len(updates))
for i, u := range updates {
events[i] = &LoopEvent{
SwapStateData: u,
}
}
swap := &LoopOut{
Loop: Loop{
Hash: hash,
Events: events,
},
Contract: contract,
}
return swap, nil
}
// CreateLoopOut adds an initiated swap to the store.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) CreateLoopOut(ctx context.Context, hash lntypes.Hash,
swap *LoopOutContract) error {
_, ok := s.LoopOutSwaps[hash]
if ok {
return errors.New("swap already exists")
}
s.LoopOutSwaps[hash] = swap
s.LoopOutUpdates[hash] = []SwapStateData{}
s.loopOutStoreChan <- *swap
return nil
}
// FetchLoopInSwaps returns all in swaps currently in the store.
func (s *StoreMock) FetchLoopInSwaps(ctx context.Context) ([]*LoopIn,
error) {
result := []*LoopIn{}
for hash, contract := range s.LoopInSwaps {
updates := s.LoopInUpdates[hash]
events := make([]*LoopEvent, len(updates))
for i, u := range updates {
events[i] = &LoopEvent{
SwapStateData: u,
}
}
swap := &LoopIn{
Loop: Loop{
Hash: hash,
Events: events,
},
Contract: contract,
}
result = append(result, swap)
}
return result, nil
}
// CreateLoopIn adds an initiated loop in swap to the store.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) CreateLoopIn(ctx context.Context, hash lntypes.Hash,
swap *LoopInContract) error {
_, ok := s.LoopInSwaps[hash]
if ok {
return errors.New("swap already exists")
}
s.LoopInSwaps[hash] = swap
s.LoopInUpdates[hash] = []SwapStateData{}
s.loopInStoreChan <- *swap
return nil
}
// UpdateLoopOut stores a new event for a target loop out swap. This appends to
// the event log for a particular swap as it goes through the various stages in
// its lifetime.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) UpdateLoopOut(ctx context.Context, hash lntypes.Hash,
time time.Time, state SwapStateData) error {
updates, ok := s.LoopOutUpdates[hash]
if !ok {
return errors.New("swap does not exists")
}
updates = append(updates, state)
s.LoopOutUpdates[hash] = updates
s.loopOutUpdateChan <- state
return nil
}
// UpdateLoopIn stores a new event for a target loop in swap. This appends to
// the event log for a particular swap as it goes through the various stages in
// its lifetime.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) UpdateLoopIn(ctx context.Context, hash lntypes.Hash,
time time.Time, state SwapStateData) error {
updates, ok := s.LoopInUpdates[hash]
if !ok {
return errors.New("swap does not exists")
}
updates = append(updates, state)
s.LoopInUpdates[hash] = updates
s.loopInUpdateChan <- state
return nil
}
// PutLiquidityParams writes the serialized `manager.Parameters` bytes into the
// bucket.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) PutLiquidityParams(ctx context.Context,
params []byte) error {
return nil
}
// FetchLiquidityParams reads the serialized `manager.Parameters` bytes from
// the bucket.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) FetchLiquidityParams(ctx context.Context) ([]byte, error) {
return nil, nil
}
// Close closes the store.
func (s *StoreMock) Close() error {
return nil
}
// isDone asserts that the store mock has no pending operations.
func (s *StoreMock) IsDone() error {
select {
case <-s.loopOutStoreChan:
return errors.New("storeChan not empty")
default:
}
select {
case <-s.loopOutUpdateChan:
return errors.New("updateChan not empty")
default:
}
return nil
}
// AssertLoopOutStored asserts that a swap is stored.
func (s *StoreMock) AssertLoopOutStored() {
s.t.Helper()
select {
case <-s.loopOutStoreChan:
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be stored")
}
}
// AssertLoopOutState asserts that a specified state transition is persisted to
// disk.
func (s *StoreMock) AssertLoopOutState(expectedState SwapState) {
s.t.Helper()
select {
case state := <-s.loopOutUpdateChan:
require.Equal(s.t, expectedState, state.State)
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap state to be stored")
}
}
// AssertLoopInStored asserts that a loop-in swap is stored.
func (s *StoreMock) AssertLoopInStored() {
s.t.Helper()
select {
case <-s.loopInStoreChan:
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be stored")
}
}
// assertLoopInState asserts that a specified state transition is persisted to
// disk.
func (s *StoreMock) AssertLoopInState(
expectedState SwapState) SwapStateData {
s.t.Helper()
state := <-s.loopInUpdateChan
require.Equal(s.t, expectedState, state.State)
return state
}
// AssertStorePreimageReveal asserts that a swap is marked as preimage revealed.
func (s *StoreMock) AssertStorePreimageReveal() {
s.t.Helper()
select {
case state := <-s.loopOutUpdateChan:
require.Equal(s.t, StatePreimageRevealed, state.State)
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be marked as preimage revealed")
}
}
// AssertStoreFinished asserts that a swap is marked as finished.
func (s *StoreMock) AssertStoreFinished(expectedResult SwapState) {
s.t.Helper()
select {
case state := <-s.loopOutUpdateChan:
require.Equal(s.t, expectedResult, state.State)
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be finished")
}
}
// BatchCreateLoopOut creates many loop out swaps in a batch.
func (b *StoreMock) BatchCreateLoopOut(ctx context.Context,
swaps map[lntypes.Hash]*LoopOutContract) error {
return errors.New("not implemented")
}
// BatchCreateLoopIn creates many loop in swaps in a batch.
func (b *StoreMock) BatchCreateLoopIn(ctx context.Context,
swaps map[lntypes.Hash]*LoopInContract) error {
return errors.New("not implemented")
}
// BatchInsertUpdate inserts many updates for a swap in a batch.
func (b *StoreMock) BatchInsertUpdate(ctx context.Context,
updateData map[lntypes.Hash][]BatchInsertUpdateData) error {
return errors.New("not implemented")
}

@ -18,6 +18,7 @@ import (
"github.com/lightninglabs/loop/labels" "github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
invpkg "github.com/lightningnetwork/lnd/invoices" invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
@ -442,7 +443,7 @@ func validateLoopInContract(height int32, response *newLoopInResponse) error {
// initHtlcs creates and updates the native and nested segwit htlcs of the // initHtlcs creates and updates the native and nested segwit htlcs of the
// loopInSwap. // loopInSwap.
func (s *loopInSwap) initHtlcs() error { func (s *loopInSwap) initHtlcs() error {
htlc, err := GetHtlc( htlc, err := utils.GetHtlc(
s.hash, &s.SwapContract, s.swapKit.lnd.ChainParams, s.hash, &s.SwapContract, s.swapKit.lnd.ChainParams,
) )
if err != nil { if err != nil {

@ -9,6 +9,7 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/test"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
invpkg "github.com/lightningnetwork/lnd/invoices" invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
@ -61,7 +62,7 @@ func testLoopInSuccess(t *testing.T) {
inSwap := initResult.swap inSwap := initResult.swap
ctx.store.assertLoopInStored() ctx.store.AssertLoopInStored()
errChan := make(chan error) errChan := make(chan error)
go func() { go func() {
@ -82,7 +83,7 @@ func testLoopInSuccess(t *testing.T) {
require.Nil(t, swapInfo.OutgoingChanSet) require.Nil(t, swapInfo.OutgoingChanSet)
ctx.assertState(loopdb.StateHtlcPublished) ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.assertLoopInState(loopdb.StateHtlcPublished) ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
// Expect htlc to be published. // Expect htlc to be published.
htlcTx := <-ctx.lnd.SendOutputsChannel htlcTx := <-ctx.lnd.SendOutputsChannel
@ -95,7 +96,7 @@ func testLoopInSuccess(t *testing.T) {
// Expect the same state to be written again with the htlc tx hash // Expect the same state to be written again with the htlc tx hash
// and on chain fee. // and on chain fee.
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished) state := ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
require.NotNil(t, state.HtlcTxHash) require.NotNil(t, state.HtlcTxHash)
require.Equal(t, cost, state.Cost) require.Equal(t, cost, state.Cost)
@ -119,7 +120,7 @@ func testLoopInSuccess(t *testing.T) {
// Swap is expected to move to the state InvoiceSettled // Swap is expected to move to the state InvoiceSettled
ctx.assertState(loopdb.StateInvoiceSettled) ctx.assertState(loopdb.StateInvoiceSettled)
ctx.store.assertLoopInState(loopdb.StateInvoiceSettled) ctx.store.AssertLoopInState(loopdb.StateInvoiceSettled)
// Server spends htlc. // Server spends htlc.
successTx := wire.MsgTx{} successTx := wire.MsgTx{}
@ -138,7 +139,7 @@ func testLoopInSuccess(t *testing.T) {
} }
ctx.assertState(loopdb.StateSuccess) ctx.assertState(loopdb.StateSuccess)
ctx.store.assertLoopInState(loopdb.StateSuccess) ctx.store.AssertLoopInState(loopdb.StateSuccess)
require.NoError(t, <-errChan) require.NoError(t, <-errChan)
} }
@ -213,7 +214,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) {
require.NoError(t, err) require.NoError(t, err)
inSwap := initResult.swap inSwap := initResult.swap
ctx.store.assertLoopInStored() ctx.store.AssertLoopInStored()
errChan := make(chan error) errChan := make(chan error)
go func() { go func() {
@ -227,7 +228,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) {
ctx.assertState(loopdb.StateInitiated) ctx.assertState(loopdb.StateInitiated)
ctx.assertState(loopdb.StateHtlcPublished) ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.assertLoopInState(loopdb.StateHtlcPublished) ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
var ( var (
htlcTx wire.MsgTx htlcTx wire.MsgTx
@ -246,7 +247,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) {
// Expect the same state to be written again with the htlc tx // Expect the same state to be written again with the htlc tx
// hash and cost. // hash and cost.
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished) state := ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
require.NotNil(t, state.HtlcTxHash) require.NotNil(t, state.HtlcTxHash)
require.Equal(t, cost, state.Cost) require.Equal(t, cost, state.Cost)
} else { } else {
@ -280,7 +281,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) {
invalidAmt := externalValue != 0 && externalValue != int64(req.Amount) invalidAmt := externalValue != 0 && externalValue != int64(req.Amount)
if invalidAmt { if invalidAmt {
ctx.assertState(loopdb.StateFailIncorrectHtlcAmt) ctx.assertState(loopdb.StateFailIncorrectHtlcAmt)
ctx.store.assertLoopInState(loopdb.StateFailIncorrectHtlcAmt) ctx.store.AssertLoopInState(loopdb.StateFailIncorrectHtlcAmt)
require.NoError(t, <-errChan) require.NoError(t, <-errChan)
return return
@ -329,7 +330,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) {
ctx.updateInvoiceState(0, invpkg.ContractCanceled) ctx.updateInvoiceState(0, invpkg.ContractCanceled)
ctx.assertState(loopdb.StateFailTimeout) ctx.assertState(loopdb.StateFailTimeout)
state := ctx.store.assertLoopInState(loopdb.StateFailTimeout) state := ctx.store.AssertLoopInState(loopdb.StateFailTimeout)
require.Equal(t, cost, state.Cost) require.Equal(t, cost, state.Cost)
require.NoError(t, <-errChan) require.NoError(t, <-errChan)
@ -449,7 +450,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
pendSwap.Loop.Events[0].Cost = cost pendSwap.Loop.Events[0].Cost = cost
} }
htlc, err := GetHtlc( htlc, err := utils.GetHtlc(
testPreimage.Hash(), &contract.SwapContract, testPreimage.Hash(), &contract.SwapContract,
cfg.lnd.ChainParams, cfg.lnd.ChainParams,
) )
@ -503,7 +504,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
} }
ctx.assertState(loopdb.StateHtlcPublished) ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.assertLoopInState(loopdb.StateHtlcPublished) ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
// Expect htlc to be published. // Expect htlc to be published.
htlcTx = <-ctx.lnd.SendOutputsChannel htlcTx = <-ctx.lnd.SendOutputsChannel
@ -515,7 +516,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
// Expect the same state to be written again with the htlc tx // Expect the same state to be written again with the htlc tx
// hash. // hash.
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished) state := ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
require.NotNil(t, state.HtlcTxHash) require.NotNil(t, state.HtlcTxHash)
} else { } else {
ctx.assertState(loopdb.StateHtlcPublished) ctx.assertState(loopdb.StateHtlcPublished)
@ -547,7 +548,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
// Swap is expected to move to the state InvoiceSettled // Swap is expected to move to the state InvoiceSettled
ctx.assertState(loopdb.StateInvoiceSettled) ctx.assertState(loopdb.StateInvoiceSettled)
ctx.store.assertLoopInState(loopdb.StateInvoiceSettled) ctx.store.AssertLoopInState(loopdb.StateInvoiceSettled)
// Server spends htlc. // Server spends htlc.
successTx := wire.MsgTx{} successTx := wire.MsgTx{}
@ -566,7 +567,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
} }
ctx.assertState(loopdb.StateSuccess) ctx.assertState(loopdb.StateSuccess)
finalState := ctx.store.assertLoopInState(loopdb.StateSuccess) finalState := ctx.store.AssertLoopInState(loopdb.StateSuccess)
// We expect our server fee to reflect as the difference between htlc // We expect our server fee to reflect as the difference between htlc
// value and invoice amount paid. We use our original on-chain cost, set // value and invoice amount paid. We use our original on-chain cost, set
@ -597,7 +598,7 @@ func TestAbandonPublishedHtlcState(t *testing.T) {
// Ensure that the swap is also in the StateFailAbandoned state in the // Ensure that the swap is also in the StateFailAbandoned state in the
// database. // database.
ctx.store.assertLoopInState(loopdb.StateFailAbandoned) ctx.store.AssertLoopInState(loopdb.StateFailAbandoned)
// Ensure that the swap was abandoned and the execution stopped. // Ensure that the swap was abandoned and the execution stopped.
err = <-ctx.errChan err = <-ctx.errChan
@ -668,7 +669,7 @@ func TestAbandonSettledInvoiceState(t *testing.T) {
// Swap is expected to move to the state InvoiceSettled // Swap is expected to move to the state InvoiceSettled
ctx.assertState(loopdb.StateInvoiceSettled) ctx.assertState(loopdb.StateInvoiceSettled)
ctx.store.assertLoopInState(loopdb.StateInvoiceSettled) ctx.store.AssertLoopInState(loopdb.StateInvoiceSettled)
// The client requests to abandon the published htlc state. // The client requests to abandon the published htlc state.
inSwap.abandonChan <- struct{}{} inSwap.abandonChan <- struct{}{}
@ -678,7 +679,7 @@ func TestAbandonSettledInvoiceState(t *testing.T) {
// Ensure that the swap is also in the StateFailAbandoned state in the // Ensure that the swap is also in the StateFailAbandoned state in the
// database. // database.
ctx.store.assertLoopInState(loopdb.StateFailAbandoned) ctx.store.AssertLoopInState(loopdb.StateFailAbandoned)
// Ensure that the swap was abandoned and the execution stopped. // Ensure that the swap was abandoned and the execution stopped.
err = <-ctx.errChan err = <-ctx.errChan
@ -728,14 +729,14 @@ func advanceToPublishedHtlc(t *testing.T, ctx *loopInTestContext) SwapInfo {
require.Equal(t, loopdb.StateInitiated, swapInfo.State) require.Equal(t, loopdb.StateInitiated, swapInfo.State)
ctx.assertState(loopdb.StateHtlcPublished) ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.assertLoopInState(loopdb.StateHtlcPublished) ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
// Expect htlc to be published. // Expect htlc to be published.
htlcTx := <-ctx.lnd.SendOutputsChannel htlcTx := <-ctx.lnd.SendOutputsChannel
// Expect the same state to be written again with the htlc tx hash // Expect the same state to be written again with the htlc tx hash
// and on chain fee. // and on chain fee.
ctx.store.assertLoopInState(loopdb.StateHtlcPublished) ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
// Expect register for htlc conf (only one, since the htlc is p2tr). // Expect register for htlc conf (only one, since the htlc is p2tr).
<-ctx.lnd.RegisterConfChannel <-ctx.lnd.RegisterConfChannel
@ -765,7 +766,7 @@ func startNewLoopIn(t *testing.T, ctx *loopInTestContext, height int32) (
inSwap := initResult.swap inSwap := initResult.swap
ctx.store.assertLoopInStored() ctx.store.AssertLoopInStored()
go func() { go func() {
err := inSwap.execute(context.Background(), ctx.cfg, height) err := inSwap.execute(context.Background(), ctx.cfg, height)

@ -18,7 +18,7 @@ type loopInTestContext struct {
t *testing.T t *testing.T
lnd *test.LndMockServices lnd *test.LndMockServices
server *serverMock server *serverMock
store *storeMock store *loopdb.StoreMock
sweeper *sweep.Sweeper sweeper *sweep.Sweeper
cfg *executeConfig cfg *executeConfig
statusChan chan SwapInfo statusChan chan SwapInfo
@ -31,7 +31,7 @@ type loopInTestContext struct {
func newLoopInTestContext(t *testing.T) *loopInTestContext { func newLoopInTestContext(t *testing.T) *loopInTestContext {
lnd := test.NewMockLnd() lnd := test.NewMockLnd()
server := newServerMock(lnd) server := newServerMock(lnd)
store := newStoreMock(t) store := loopdb.NewStoreMock(t)
sweeper := sweep.Sweeper{Lnd: &lnd.LndServices} sweeper := sweep.Sweeper{Lnd: &lnd.LndServices}
blockEpochChan := make(chan interface{}) blockEpochChan := make(chan interface{})

@ -1,7 +1,6 @@
package loop package loop
import ( import (
"bytes"
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
@ -12,22 +11,19 @@ import (
"time" "time"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"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/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/utils"
"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"
) )
const ( const (
@ -90,6 +86,7 @@ type loopOutSwap struct {
// executeConfig contains extra configuration to execute the swap. // executeConfig contains extra configuration to execute the swap.
type executeConfig struct { type executeConfig struct {
sweeper *sweep.Sweeper sweeper *sweep.Sweeper
batcher *sweepbatcher.Batcher
statusChan chan<- SwapInfo statusChan chan<- SwapInfo
blockEpochChan <-chan interface{} blockEpochChan <-chan interface{}
timerFactory func(time.Duration) <-chan time.Time timerFactory func(time.Duration) <-chan time.Time
@ -172,6 +169,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
contract := loopdb.LoopOutContract{ contract := loopdb.LoopOutContract{
SwapInvoice: swapResp.swapInvoice, SwapInvoice: swapResp.swapInvoice,
DestAddr: request.DestAddr, DestAddr: request.DestAddr,
IsExternalAddr: request.IsExternalAddr,
MaxSwapRoutingFee: request.MaxSwapRoutingFee, MaxSwapRoutingFee: request.MaxSwapRoutingFee,
SweepConfTarget: request.SweepConfTarget, SweepConfTarget: request.SweepConfTarget,
HtlcConfirmations: confs, HtlcConfirmations: confs,
@ -206,7 +204,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
swapKit.lastUpdateTime = initiationTime swapKit.lastUpdateTime = initiationTime
// Create the htlc. // Create the htlc.
htlc, err := GetHtlc( htlc, err := utils.GetHtlc(
swapKit.hash, swapKit.contract, swapKit.lnd.ChainParams, swapKit.hash, swapKit.contract, swapKit.lnd.ChainParams,
) )
if err != nil { if err != nil {
@ -219,7 +217,9 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
// Obtain the payment addr since we'll need it later for routing plugin // Obtain the payment addr since we'll need it later for routing plugin
// recommendation and possibly for cancel. // recommendation and possibly for cancel.
paymentAddr, err := obtainSwapPaymentAddr(contract.SwapInvoice, cfg) paymentAddr, err := utils.ObtainSwapPaymentAddr(
contract.SwapInvoice, cfg.lnd.ChainParams,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -262,7 +262,7 @@ func resumeLoopOutSwap(cfg *swapConfig, pend *loopdb.LoopOut,
) )
// Create the htlc. // Create the htlc.
htlc, err := GetHtlc( htlc, err := utils.GetHtlc(
swapKit.hash, swapKit.contract, swapKit.lnd.ChainParams, swapKit.hash, swapKit.contract, swapKit.lnd.ChainParams,
) )
if err != nil { if err != nil {
@ -274,8 +274,8 @@ func resumeLoopOutSwap(cfg *swapConfig, pend *loopdb.LoopOut,
// Obtain the payment addr since we'll need it later for routing plugin // Obtain the payment addr since we'll need it later for routing plugin
// recommendation and possibly for cancel. // recommendation and possibly for cancel.
paymentAddr, err := obtainSwapPaymentAddr( paymentAddr, err := utils.ObtainSwapPaymentAddr(
pend.Contract.SwapInvoice, cfg, pend.Contract.SwapInvoice, cfg.lnd.ChainParams,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -301,24 +301,6 @@ func resumeLoopOutSwap(cfg *swapConfig, pend *loopdb.LoopOut,
return swap, nil return swap, nil
} }
// obtainSwapPaymentAddr will retrieve the payment addr from the passed invoice.
func obtainSwapPaymentAddr(swapInvoice string, cfg *swapConfig) (
*[32]byte, error) {
swapPayReq, err := zpay32.Decode(
swapInvoice, cfg.lnd.ChainParams,
)
if err != nil {
return nil, err
}
if swapPayReq.PaymentAddr == nil {
return nil, fmt.Errorf("expected payment address for invoice")
}
return swapPayReq.PaymentAddr, nil
}
// sendUpdate reports an update to the swap state. // sendUpdate reports an update to the swap state.
func (s *loopOutSwap) sendUpdate(ctx context.Context) error { func (s *loopOutSwap) sendUpdate(ctx context.Context) error {
info := s.swapInfo() info := s.swapInfo()
@ -532,7 +514,7 @@ 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( spendTx, err := s.waitForHtlcSpendConfirmedV2(
globalCtx, *htlcOutpoint, htlcValue, globalCtx, *htlcOutpoint, htlcValue,
) )
if err != nil { if err != nil {
@ -541,7 +523,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
// If spend details are nil, we resolved the swap without waiting for // If spend details are nil, we resolved the swap without waiting for
// its spend, so we can exit. // its spend, so we can exit.
if spendDetails == nil { if spendTx == nil {
return nil return nil
} }
@ -549,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 // 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. // may be swept by a different (fee) sweep tx from a previous run.
htlcInput, err := swap.GetTxInputByOutpoint( htlcInput, err := swap.GetTxInputByOutpoint(
spendDetails.SpendingTx, htlcOutpoint, spendTx, htlcOutpoint,
) )
if err != nil { if err != nil {
return err return err
@ -560,7 +542,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
s.cost.Server -= htlcValue s.cost.Server -= htlcValue
s.cost.Onchain = htlcValue - s.cost.Onchain = htlcValue -
btcutil.Amount(spendDetails.SpendingTx.TxOut[0].Value) btcutil.Amount(spendTx.TxOut[0].Value)
s.state = loopdb.StateSuccess s.state = loopdb.StateSuccess
} else { } else {
@ -1019,26 +1001,36 @@ func (s *loopOutSwap) waitForConfirmedHtlc(globalCtx context.Context) (
return txConf, nil return txConf, nil
} }
// waitForHtlcSpendConfirmed waits for the htlc to be spent either by our own // waitForHtlcSpendConfirmedV2 waits for the htlc to be spent either by our own
// sweep or a server revocation tx. During this process, this function will try // sweep or a server revocation tx.
// to spend the htlc every block by calling spendFunc. func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context,
//
// 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,
htlcOutpoint wire.OutPoint, htlcValue btcutil.Amount) ( 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. // 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(
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 // 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 // htlc is settled. We track this information to determine whether it is
@ -1055,26 +1047,20 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context,
// is used to decide whether we need to push our preimage to // is used to decide whether we need to push our preimage to
// the server. // the server.
paymentComplete bool 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 { for {
select { select {
// Htlc spend, break loop. // Htlc spend, break loop.
case spendDetails := <-spendChan: case spendTx := <-spendChan:
s.log.Infof("Htlc spend by tx: %v", s.log.Infof("Htlc spend by tx: %v", spendTx.TxHash())
spendDetails.SpenderTxHash)
return spendDetails, nil return spendTx, nil
// Spend notification error. // Spend notification error.
case err := <-spendErr: case err := <-spendErrChan:
return nil, err return nil, err
// Receive status updates for our payment so that we can detect // Receive status updates for our payment so that we can detect
@ -1116,121 +1102,51 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context,
return nil, err return nil, err
} }
// New block arrived, update height and restart the republish // New block arrived, update height and try pushing preimage.
// timer.
case notification := <-s.blockEpochChan: case notification := <-s.blockEpochChan:
s.height = notification.(int32) 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: case <-timerChan:
if IsTaprootSwap(&s.SwapContract) { // sweepConfTarget will return false if the preimage is
// sweepConfTarget will return false if the // not revealed yet but the conf target is closer than
// preimage is not revealed yet but the conf // 20 blocks. In this case to be sure we won't attempt
// target is closer than 20 blocks. In this case // to sweep at all and we won't reveal the preimage
// to be sure we won't attempt to sweep at all // either.
// and we won't reveal the preimage either. _, canSweep := s.sweepConfTarget()
_, canSweep := s.sweepConfTarget() if !canSweep {
if !canSweep { s.log.Infof("Aborting swap, timed " +
s.log.Infof("Aborting swap, timed " + "out on-chain")
"out on-chain")
s.state = loopdb.StateFailTimeout
s.state = loopdb.StateFailTimeout err := s.persistState(ctx)
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)
if err != nil { if err != nil {
return nil, err log.Warnf("unable to persist " +
} "state")
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. return nil, nil
// 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)
}
}
// If the result of our spend func was that the // Send the sweep to the sweeper.
// swap has reached a final state, then we err := s.batcher.AddSweep(&sweepReq)
// return nil spend details, because there is if err != nil {
// no further action required for this swap. return nil, err
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 // Now that the sweep is taken care of, we can update
// swap has reached a final state, then we // our state.
// return nil spend details, because there is no err = s.setStatePreimageRevealed(ctx)
// further action required for this swap. if err != nil {
if s.state.Type() != loopdb.StateTypePending { return nil, err
return nil, nil }
}
// If our off chain payment is not yet complete, if !paymentComplete {
// we try to push our preimage to the server. // Push the preimage for as long as the
if !paymentComplete { // server is able to settle the swap
s.pushPreimage(ctx) // invoice. So that we can continue
} // with the MuSig2 sweep afterwards.
s.pushPreimage(ctx)
} }
// Context canceled. // Context canceled.
@ -1339,129 +1255,63 @@ func (s *loopOutSwap) failOffChain(ctx context.Context, paymentType paymentType,
} }
} }
// createMuSig2SweepTxn creates a taproot keyspend sweep transaction and func (s *loopOutSwap) setStatePreimageRevealed(ctx context.Context) error {
// attempts to cooperate with the server to create a MuSig2 signature witness. if s.state != loopdb.StatePreimageRevealed {
func (s *loopOutSwap) createMuSig2SweepTxn( s.state = loopdb.StatePreimageRevealed
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
)
// Depending on the MuSig2 version we either pass 32 byte Schnorr err := s.persistState(ctx)
// public keys or normal 33 byte public keys. if err != nil {
if s.ProtocolVersion >= loopdb.ProtocolVersionMuSig2 { return err
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:],
} }
} }
htlcScript, ok := s.htlc.HtlcScript.(*swap.HtlcScriptV3) return nil
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
}
// With the session active, we can now send the server our public nonce // validateLoopOutContract validates the contract parameters against our
// and the sig hash, so that it can create it's own MuSig2 session and // request.
// return the server side nonce and partial signature. func validateLoopOutContract(lnd *lndclient.LndServices, request *OutRequest,
serverNonce, serverSig, err := s.swapKit.server.MuSig2SignSweep( swapHash lntypes.Hash, response *newLoopOutResponse) error {
ctx, s.SwapContract.ProtocolVersion, s.hash,
s.swapInvoicePaymentAddr, musig2SessionInfo.PublicNonce[:],
sweepTxPsbt,
)
if err != nil {
return nil, err
}
var serverPublicNonce [musig2.PubNonceSize]byte // Check invoice amounts.
copy(serverPublicNonce[:], serverNonce) chainParams := lnd.ChainParams
// Register the server's nonce before attempting to create our partial _, _, swapInvoiceHash, swapInvoiceAmt, err := swap.DecodeInvoice(
// signature. chainParams, response.swapInvoice,
haveAllNonces, err := s.lnd.Signer.MuSig2RegisterNonces(
ctx, musig2SessionInfo.SessionID,
[][musig2.PubNonceSize]byte{serverPublicNonce},
) )
if err != nil { if err != nil {
return nil, err return err
} }
// Sanity check that we have all the nonces. if swapInvoiceHash != swapHash {
if !haveAllNonces { return fmt.Errorf(
return nil, fmt.Errorf("invalid MuSig2 session: nonces missing") "cannot initiate swap, swap invoice hash %v not equal "+
"generated swap hash %v", swapInvoiceHash, swapHash)
} }
var digest [32]byte _, _, _, prepayInvoiceAmt, err := swap.DecodeInvoice(
copy(digest[:], sigHash) chainParams, response.prepayInvoice,
// 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 { if err != nil {
return nil, err return err
} }
// Now combine the partial signatures to use the final combined swapFee := swapInvoiceAmt + prepayInvoiceAmt - request.Amount
// signature in the sweep transaction's witness. if swapFee > request.MaxSwapFee {
haveAllSigs, finalSig, err := s.lnd.Signer.MuSig2CombineSig( log.Warnf("Swap fee %v exceeding maximum of %v",
ctx, musig2SessionInfo.SessionID, [][]byte{serverSig}, swapFee, request.MaxSwapFee)
)
if err != nil {
return nil, err
}
if !haveAllSigs { return ErrSwapFeeTooHigh
return nil, fmt.Errorf("failed to combine signatures")
} }
// To be sure that we're good, parse and validate that the combined if prepayInvoiceAmt > request.MaxPrepayAmount {
// signature is indeed valid for the sig hash and the internal pubkey. log.Warnf("Prepay amount %v exceeding maximum of %v",
err = s.executeConfig.verifySchnorrSig( prepayInvoiceAmt, request.MaxPrepayAmount)
htlcScript.TaprootKey, sigHash, finalSig,
)
if err != nil {
return nil, err
}
// Now that we know the signature is correct, we can fill it in to our return ErrPrepayAmountTooHigh
// witness.
sweepTx.TxIn[0].Witness = wire.TxWitness{
finalSig,
} }
return sweepTx, nil return nil
} }
// sweepConfTarget returns the confirmation target for the htlc sweep or false // sweepConfTarget returns the confirmation target for the htlc sweep or false
@ -1499,215 +1349,3 @@ func (s *loopOutSwap) sweepConfTarget() (int32, bool) {
return confTarget, true 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/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
@ -45,7 +46,7 @@ func testLoopOutPaymentParameters(t *testing.T) {
lnd := test.NewMockLnd() lnd := test.NewMockLnd()
ctx := test.NewContext(t, lnd) ctx := test.NewContext(t, lnd)
server := newServerMock(lnd) server := newServerMock(lnd)
store := newStoreMock(t) store := loopdb.NewStoreMock(t)
expiryChan := make(chan time.Time) expiryChan := make(chan time.Time)
timerFactory := func(_ time.Duration) <-chan time.Time { timerFactory := func(_ time.Duration) <-chan time.Time {
@ -99,7 +100,7 @@ func testLoopOutPaymentParameters(t *testing.T) {
errChan <- err errChan <- err
}() }()
store.assertLoopOutStored() store.AssertLoopOutStored()
state := <-statusChan state := <-statusChan
require.Equal(t, loopdb.StateInitiated, state.State) require.Equal(t, loopdb.StateInitiated, state.State)
@ -168,7 +169,7 @@ func testLateHtlcPublish(t *testing.T) {
server := newServerMock(lnd) server := newServerMock(lnd)
store := newStoreMock(t) store := loopdb.NewStoreMock(t)
expiryChan := make(chan time.Time) expiryChan := make(chan time.Time)
timerFactory := func(expiry time.Duration) <-chan time.Time { timerFactory := func(expiry time.Duration) <-chan time.Time {
@ -208,7 +209,7 @@ func testLateHtlcPublish(t *testing.T) {
errChan <- err errChan <- err
}() }()
store.assertLoopOutStored() store.AssertLoopOutStored()
status := <-statusChan status := <-statusChan
require.Equal(t, loopdb.StateInitiated, status.State) require.Equal(t, loopdb.StateInitiated, status.State)
@ -228,7 +229,7 @@ func testLateHtlcPublish(t *testing.T) {
errors.New(lndclient.PaymentResultUnknownPaymentHash), errors.New(lndclient.PaymentResultUnknownPaymentHash),
) )
store.assertStoreFinished(loopdb.StateFailTimeout) store.AssertStoreFinished(loopdb.StateFailTimeout)
status = <-statusChan status = <-statusChan
require.Equal(t, loopdb.StateFailTimeout, status.State) require.Equal(t, loopdb.StateFailTimeout, status.State)
@ -273,7 +274,7 @@ func testCustomSweepConfTarget(t *testing.T) {
ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 10000) ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 10000)
cfg := newSwapConfig( cfg := newSwapConfig(
&lnd.LndServices, newStoreMock(t), server, &lnd.LndServices, loopdb.NewStoreMock(t), server,
) )
initResult, err := newLoopOutSwap( initResult, err := newLoopOutSwap(
@ -293,13 +294,33 @@ func testCustomSweepConfTarget(t *testing.T) {
return expiryChan return expiryChan
} }
errChan := make(chan error) errChan := make(chan error, 2)
batcherStore := sweepbatcher.NewStoreMock()
batcher := sweepbatcher.NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
mockMuSig2SignSweep, mockVerifySchnorrSigSuccess,
lnd.ChainParams, batcherStore, cfg.store,
)
tctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { go func() {
err := swap.execute(context.Background(), &executeConfig{ err := batcher.Run(tctx)
if err != nil {
errChan <- err
}
}()
go func() {
err := swap.execute(tctx, &executeConfig{
statusChan: statusChan, statusChan: statusChan,
blockEpochChan: blockEpochChan, blockEpochChan: blockEpochChan,
timerFactory: timerFactory, timerFactory: timerFactory,
sweeper: sweeper, sweeper: sweeper,
batcher: batcher,
cancelSwap: server.CancelLoopOutSwap, cancelSwap: server.CancelLoopOutSwap,
verifySchnorrSig: mockVerifySchnorrSigFail, verifySchnorrSig: mockVerifySchnorrSigFail,
}, ctx.Lnd.Height) }, ctx.Lnd.Height)
@ -310,7 +331,7 @@ func testCustomSweepConfTarget(t *testing.T) {
}() }()
// The swap should be found in its initial state. // The swap should be found in its initial state.
cfg.store.(*storeMock).assertLoopOutStored() cfg.store.(*loopdb.StoreMock).AssertLoopOutStored()
state := <-statusChan state := <-statusChan
require.Equal(t, loopdb.StateInitiated, state.State) require.Equal(t, loopdb.StateInitiated, state.State)
@ -335,22 +356,27 @@ func testCustomSweepConfTarget(t *testing.T) {
ctx.NotifyConf(htlcTx) ctx.NotifyConf(htlcTx)
// The client should then register for a spend of the HTLC and attempt
// to sweep it using the custom confirmation target.
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
// Assert that we made a query to track our payment, as required for // Assert that we made a query to track our payment, as required for
// preimage push tracking. // preimage push tracking.
trackPayment := ctx.AssertTrackPayment() trackPayment := ctx.AssertTrackPayment()
expiryChan <- time.Now() expiryChan <- time.Now()
// The client should then register for a spend of the HTLC and attempt
// to sweep it using the custom confirmation target.
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
ctx.AssertEpochListeners(1)
err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1)
require.NoError(t, err)
// Expect a signing request for the HTLC success transaction. // Expect a signing request for the HTLC success transaction.
if !IsTaprootSwap(&swap.SwapContract) { if !IsTaprootSwap(&swap.SwapContract) {
<-ctx.Lnd.SignOutputRawChannel <-ctx.Lnd.SignOutputRawChannel
} }
cfg.store.(*storeMock).assertLoopOutState(loopdb.StatePreimageRevealed) cfg.store.(*loopdb.StoreMock).AssertLoopOutState(loopdb.StatePreimageRevealed)
status := <-statusChan status := <-statusChan
require.Equal(t, loopdb.StatePreimageRevealed, status.State) require.Equal(t, loopdb.StatePreimageRevealed, status.State)
@ -409,7 +435,7 @@ func testCustomSweepConfTarget(t *testing.T) {
// The sweep should have a fee that corresponds to the custom // The sweep should have a fee that corresponds to the custom
// confirmation target. // confirmation target.
_ = assertSweepTx(testReq.SweepConfTarget) sweepTx := assertSweepTx(testReq.SweepConfTarget)
// 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.
@ -426,24 +452,14 @@ func testCustomSweepConfTarget(t *testing.T) {
State: lnrpc.Payment_SUCCEEDED, State: lnrpc.Payment_SUCCEEDED,
} }
// We'll then notify the height at which we begin using the default // Notify the batch for the spend.
// confirmation target.
defaultConfTargetHeight := ctx.Lnd.Height +
testLoopOutMinOnChainCltvDelta - DefaultSweepConfTargetDelta
blockEpochChan <- defaultConfTargetHeight
expiryChan <- time.Now()
// Expect another signing request.
<-ctx.Lnd.SignOutputRawChannel
// We should expect to see another sweep using the higher fee since the
// spend hasn't been confirmed yet.
sweepTx := assertSweepTx(DefaultSweepConfTarget)
// Notify the spend so that the swap reaches its final state.
ctx.NotifySpend(sweepTx, 0) ctx.NotifySpend(sweepTx, 0)
cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess) // After receiving the notification the batch will start monitoring the
// confirmations.
ctx.AssertRegisterConf(true, 3)
cfg.store.(*loopdb.StoreMock).AssertLoopOutState(loopdb.StateSuccess)
status = <-statusChan status = <-statusChan
require.Equal(t, loopdb.StateSuccess, status.State) require.Equal(t, loopdb.StateSuccess, status.State)
require.NoError(t, <-errChan) require.NoError(t, <-errChan)
@ -493,7 +509,7 @@ func testPreimagePush(t *testing.T) {
) )
cfg := newSwapConfig( cfg := newSwapConfig(
&lnd.LndServices, newStoreMock(t), server, &lnd.LndServices, loopdb.NewStoreMock(t), server,
) )
initResult, err := newLoopOutSwap( initResult, err := newLoopOutSwap(
@ -511,13 +527,33 @@ func testPreimagePush(t *testing.T) {
return expiryChan return expiryChan
} }
errChan := make(chan error) errChan := make(chan error, 2)
batcherStore := sweepbatcher.NewStoreMock()
batcher := sweepbatcher.NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
mockMuSig2SignSweep, mockVerifySchnorrSigSuccess,
lnd.ChainParams, batcherStore, cfg.store,
)
tctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
err := batcher.Run(tctx)
if err != nil {
errChan <- err
}
}()
go func() { go func() {
err := swap.execute(context.Background(), &executeConfig{ err := swap.execute(context.Background(), &executeConfig{
statusChan: statusChan, statusChan: statusChan,
blockEpochChan: blockEpochChan, blockEpochChan: blockEpochChan,
timerFactory: timerFactory, timerFactory: timerFactory,
sweeper: sweeper, sweeper: sweeper,
batcher: batcher,
cancelSwap: server.CancelLoopOutSwap, cancelSwap: server.CancelLoopOutSwap,
verifySchnorrSig: mockVerifySchnorrSigFail, verifySchnorrSig: mockVerifySchnorrSigFail,
}, ctx.Lnd.Height) }, ctx.Lnd.Height)
@ -528,7 +564,7 @@ func testPreimagePush(t *testing.T) {
}() }()
// The swap should be found in its initial state. // The swap should be found in its initial state.
cfg.store.(*storeMock).assertLoopOutStored() cfg.store.(*loopdb.StoreMock).AssertLoopOutStored()
state := <-statusChan state := <-statusChan
require.Equal(t, loopdb.StateInitiated, state.State) require.Equal(t, loopdb.StateInitiated, state.State)
@ -553,10 +589,6 @@ func testPreimagePush(t *testing.T) {
ctx.NotifyConf(htlcTx) ctx.NotifyConf(htlcTx)
// The client should then register for a spend of the HTLC and attempt
// to sweep it using the custom confirmation target.
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
// Assert that we made a query to track our payment, as required for // Assert that we made a query to track our payment, as required for
// preimage push tracking. // preimage push tracking.
trackPayment := ctx.AssertTrackPayment() trackPayment := ctx.AssertTrackPayment()
@ -567,11 +599,20 @@ 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
// The client should then register for a spend of the HTLC and attempt
// to sweep it using the custom confirmation target.
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
ctx.AssertEpochListeners(1)
err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1)
require.NoError(t, err)
// When using taproot htlcs the flow is different as we do reveal the // 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 // preimage before sweeping in order for the server to trust us with
// our MuSig2 signing attempts. // our MuSig2 signing attempts.
if IsTaprootSwap(&swap.SwapContract) { if IsTaprootSwap(&swap.SwapContract) {
cfg.store.(*storeMock).assertLoopOutState( cfg.store.(*loopdb.StoreMock).AssertLoopOutState(
loopdb.StatePreimageRevealed, loopdb.StatePreimageRevealed,
) )
status := <-statusChan status := <-statusChan
@ -582,15 +623,6 @@ func testPreimagePush(t *testing.T) {
preimage := <-server.preimagePush preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage) 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 <-ctx.Lnd.SignOutputRawChannel
// We expect the sweep tx to have been published. // We expect the sweep tx to have been published.
@ -611,6 +643,10 @@ func testPreimagePush(t *testing.T) {
// Now when we report a new block and tick our expiry fee timer, and // Now when we report a new block and tick our expiry fee timer, and
// fees are acceptably low so we expect our sweep to be published. // fees are acceptably low so we expect our sweep to be published.
blockEpochChan <- ctx.Lnd.Height + 2 blockEpochChan <- ctx.Lnd.Height + 2
err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2)
require.NoError(t, err)
expiryChan <- testTime expiryChan <- testTime
if IsTaprootSwap(&swap.SwapContract) { if IsTaprootSwap(&swap.SwapContract) {
@ -624,7 +660,7 @@ func testPreimagePush(t *testing.T) {
if !IsTaprootSwap(&swap.SwapContract) { if !IsTaprootSwap(&swap.SwapContract) {
// This is the first time we have swept, so we expect our // This is the first time we have swept, so we expect our
// preimage revealed state to be set. // preimage revealed state to be set.
cfg.store.(*storeMock).assertLoopOutState( cfg.store.(*loopdb.StoreMock).AssertLoopOutState(
loopdb.StatePreimageRevealed, loopdb.StatePreimageRevealed,
) )
status := <-statusChan status := <-statusChan
@ -648,6 +684,10 @@ func testPreimagePush(t *testing.T) {
// 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
err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2)
require.NoError(t, err)
if IsTaprootSwap(&swap.SwapContract) { if IsTaprootSwap(&swap.SwapContract) {
preimage := <-server.preimagePush preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage) require.Equal(t, swap.Preimage, preimage)
@ -678,6 +718,10 @@ func testPreimagePush(t *testing.T) {
// push. The test's mocked preimage channel is un-buffered, so our test // push. The test's mocked preimage channel is un-buffered, so our test
// would hang if we pushed the preimage here. // would hang if we pushed the preimage here.
expiryChan <- testTime expiryChan <- testTime
err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2)
require.NoError(t, err)
<-ctx.Lnd.SignOutputRawChannel <-ctx.Lnd.SignOutputRawChannel
sweepTx := ctx.ReceiveTx() sweepTx := ctx.ReceiveTx()
@ -685,7 +729,11 @@ func testPreimagePush(t *testing.T) {
// spend our sweepTx and assert that the swap succeeds. // spend our sweepTx and assert that the swap succeeds.
ctx.NotifySpend(sweepTx, 0) ctx.NotifySpend(sweepTx, 0)
cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess) // After receiving the spend ntfn the batch will start monitoring for
// confs.
ctx.AssertRegisterConf(true, 3)
cfg.store.(*loopdb.StoreMock).AssertLoopOutState(loopdb.StateSuccess)
status := <-statusChan status := <-statusChan
require.Equal( require.Equal(
t, status.State, loopdb.StateSuccess, t, status.State, loopdb.StateSuccess,
@ -720,7 +768,7 @@ func testFailedOffChainCancelation(t *testing.T) {
testReq.Expiry = lnd.Height + 20 testReq.Expiry = lnd.Height + 20
cfg := newSwapConfig( cfg := newSwapConfig(
&lnd.LndServices, newStoreMock(t), server, &lnd.LndServices, loopdb.NewStoreMock(t), server,
) )
initResult, err := newLoopOutSwap( initResult, err := newLoopOutSwap(
@ -754,7 +802,7 @@ func testFailedOffChainCancelation(t *testing.T) {
}() }()
// The swap should be found in its initial state. // The swap should be found in its initial state.
cfg.store.(*storeMock).assertLoopOutStored() cfg.store.(*loopdb.StoreMock).AssertLoopOutStored()
state := <-statusChan state := <-statusChan
require.Equal(t, loopdb.StateInitiated, state.State) require.Equal(t, loopdb.StateInitiated, state.State)
@ -837,7 +885,7 @@ func testFailedOffChainCancelation(t *testing.T) {
server.assertSwapCanceled(t, swapCancelation) server.assertSwapCanceled(t, swapCancelation)
// Finally, the swap should be recorded with failed off chain timeout. // Finally, the swap should be recorded with failed off chain timeout.
cfg.store.(*storeMock).assertLoopOutState( cfg.store.(*loopdb.StoreMock).AssertLoopOutState(
loopdb.StateFailOffchainPayments, loopdb.StateFailOffchainPayments,
) )
state = <-statusChan state = <-statusChan
@ -874,7 +922,7 @@ func TestLoopOutMuSig2Sweep(t *testing.T) {
) )
cfg := newSwapConfig( cfg := newSwapConfig(
&lnd.LndServices, newStoreMock(t), server, &lnd.LndServices, loopdb.NewStoreMock(t), server,
) )
initResult, err := newLoopOutSwap( initResult, err := newLoopOutSwap(
@ -892,8 +940,6 @@ func TestLoopOutMuSig2Sweep(t *testing.T) {
return expiryChan return expiryChan
} }
errChan := make(chan error)
// Mock a successful signature verify to make sure we don't fail // Mock a successful signature verify to make sure we don't fail
// creating the MuSig2 sweep. // creating the MuSig2 sweep.
mockVerifySchnorrSigSuccess := func(pubKey *btcec.PublicKey, hash, mockVerifySchnorrSigSuccess := func(pubKey *btcec.PublicKey, hash,
@ -902,12 +948,33 @@ func TestLoopOutMuSig2Sweep(t *testing.T) {
return nil return nil
} }
errChan := make(chan error, 2)
batcherStore := sweepbatcher.NewStoreMock()
batcher := sweepbatcher.NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
mockMuSig2SignSweep, mockVerifySchnorrSigSuccess,
lnd.ChainParams, batcherStore, cfg.store,
)
tctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
err := batcher.Run(tctx)
if err != nil {
errChan <- err
}
}()
go func() { go func() {
err := swap.execute(context.Background(), &executeConfig{ err := swap.execute(context.Background(), &executeConfig{
statusChan: statusChan, statusChan: statusChan,
blockEpochChan: blockEpochChan, blockEpochChan: blockEpochChan,
timerFactory: timerFactory, timerFactory: timerFactory,
sweeper: sweeper, sweeper: sweeper,
batcher: batcher,
cancelSwap: server.CancelLoopOutSwap, cancelSwap: server.CancelLoopOutSwap,
verifySchnorrSig: mockVerifySchnorrSigSuccess, verifySchnorrSig: mockVerifySchnorrSigSuccess,
}, ctx.Lnd.Height) }, ctx.Lnd.Height)
@ -918,7 +985,7 @@ func TestLoopOutMuSig2Sweep(t *testing.T) {
}() }()
// The swap should be found in its initial state. // The swap should be found in its initial state.
cfg.store.(*storeMock).assertLoopOutStored() cfg.store.(*loopdb.StoreMock).AssertLoopOutStored()
state := <-statusChan state := <-statusChan
require.Equal(t, loopdb.StateInitiated, state.State) require.Equal(t, loopdb.StateInitiated, state.State)
@ -943,10 +1010,6 @@ func TestLoopOutMuSig2Sweep(t *testing.T) {
ctx.NotifyConf(htlcTx) ctx.NotifyConf(htlcTx)
// The client should then register for a spend of the HTLC and attempt
// to sweep it using the custom confirmation target.
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
// Assert that we made a query to track our payment, as required for // Assert that we made a query to track our payment, as required for
// preimage push tracking. // preimage push tracking.
trackPayment := ctx.AssertTrackPayment() trackPayment := ctx.AssertTrackPayment()
@ -957,10 +1020,19 @@ func TestLoopOutMuSig2Sweep(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
// The client should then register for a spend of the HTLC and attempt
// to sweep it using the custom confirmation target.
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
ctx.AssertEpochListeners(1)
err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1)
require.NoError(t, err)
// When using taproot htlcs the flow is different as we do reveal the // 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 // preimage before sweeping in order for the server to trust us with
// our MuSig2 signing attempts. // our MuSig2 signing attempts.
cfg.store.(*storeMock).assertLoopOutState( cfg.store.(*loopdb.StoreMock).AssertLoopOutState(
loopdb.StatePreimageRevealed, loopdb.StatePreimageRevealed,
) )
status := <-statusChan status := <-statusChan
@ -988,6 +1060,10 @@ func TestLoopOutMuSig2Sweep(t *testing.T) {
// Now when we report a new block and tick our expiry fee timer, and // Now when we report a new block and tick our expiry fee timer, and
// fees are acceptably low so we expect our sweep to be published. // fees are acceptably low so we expect our sweep to be published.
blockEpochChan <- ctx.Lnd.Height + 2 blockEpochChan <- ctx.Lnd.Height + 2
err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2)
require.NoError(t, err)
expiryChan <- testTime expiryChan <- testTime
preimage = <-server.preimagePush preimage = <-server.preimagePush
@ -1010,7 +1086,11 @@ func TestLoopOutMuSig2Sweep(t *testing.T) {
// spend our sweepTx and assert that the swap succeeds. // spend our sweepTx and assert that the swap succeeds.
ctx.NotifySpend(sweepTx, 0) ctx.NotifySpend(sweepTx, 0)
cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess) // After receiving the spend ntfn the batch will start monitoring for
// confs.
ctx.AssertRegisterConf(true, 3)
cfg.store.(*loopdb.StoreMock).AssertLoopOutState(loopdb.StateSuccess)
status = <-statusChan status = <-statusChan
require.Equal(t, status.State, loopdb.StateSuccess) require.Equal(t, status.State, loopdb.StateSuccess)
require.NoError(t, <-errChan) require.NoError(t, <-errChan)

File diff suppressed because it is too large Load Diff

@ -235,6 +235,13 @@ message LoopOutRequest {
The address type of the account specified in the account field. The address type of the account specified in the account field.
*/ */
AddressType account_addr_type = 16; AddressType account_addr_type = 16;
/*
A flag indicating whether the defined destination address does not belong to
the wallet. This is used to flag whether this loop out swap could have its
associated sweep batched.
*/
bool is_external_addr = 17;
} }
/* /*

@ -1110,6 +1110,10 @@
"account_addr_type": { "account_addr_type": {
"$ref": "#/definitions/looprpcAddressType", "$ref": "#/definitions/looprpcAddressType",
"description": "The address type of the account specified in the account field." "description": "The address type of the account specified in the account field."
},
"is_external_addr": {
"type": "boolean",
"description": "A flag indicating whether the defined destination address does not belong to\nthe wallet. This is used to flag whether this loop out swap could have its\nassociated sweep batched."
} }
} }
}, },

@ -16,6 +16,15 @@ This file tracks release notes for the loop client.
#### New Features #### New Features
* Sweep Batcher: A new sub-system was added that handles all the loopout
sweeps. Successful loopout HTLCs will no longer be swept back to the wallet via
individual transactions but will instead form a single transaction that holds
multiple inputs and pays to a single output. This will significantly reduce
chain fee costs as it's using less block space by directly consolidating all the
htlcs to a single address. Loopouts that pay to non-wallet addresses will still
use individual transactions as their output cannot be mutated.
#### Breaking Changes #### Breaking Changes
#### Bug Fixes #### Bug Fixes

@ -8,6 +8,7 @@ import (
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/test"
@ -268,6 +269,15 @@ func (s *serverMock) MuSig2SignSweep(_ context.Context, _ loopdb.ProtocolVersion
return nil, nil, nil return nil, nil, nil
} }
func (s *serverMock) 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) {
return nil, nil, nil
}
func (s *serverMock) PushKey(_ context.Context, _ loopdb.ProtocolVersion, func (s *serverMock) PushKey(_ context.Context, _ loopdb.ProtocolVersion,
_ lntypes.Hash, _ [32]byte) error { _ lntypes.Hash, _ [32]byte) error {

@ -1,322 +0,0 @@
package loop
import (
"context"
"errors"
"testing"
"time"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)
// storeMock implements a mock client swap store.
type storeMock struct {
loopOutSwaps map[lntypes.Hash]*loopdb.LoopOutContract
loopOutUpdates map[lntypes.Hash][]loopdb.SwapStateData
loopOutStoreChan chan loopdb.LoopOutContract
loopOutUpdateChan chan loopdb.SwapStateData
loopInSwaps map[lntypes.Hash]*loopdb.LoopInContract
loopInUpdates map[lntypes.Hash][]loopdb.SwapStateData
loopInStoreChan chan loopdb.LoopInContract
loopInUpdateChan chan loopdb.SwapStateData
t *testing.T
}
// NewStoreMock instantiates a new mock store.
func newStoreMock(t *testing.T) *storeMock {
return &storeMock{
loopOutStoreChan: make(chan loopdb.LoopOutContract, 1),
loopOutUpdateChan: make(chan loopdb.SwapStateData, 1),
loopOutSwaps: make(map[lntypes.Hash]*loopdb.LoopOutContract),
loopOutUpdates: make(map[lntypes.Hash][]loopdb.SwapStateData),
loopInStoreChan: make(chan loopdb.LoopInContract, 1),
loopInUpdateChan: make(chan loopdb.SwapStateData, 1),
loopInSwaps: make(map[lntypes.Hash]*loopdb.LoopInContract),
loopInUpdates: make(map[lntypes.Hash][]loopdb.SwapStateData),
t: t,
}
}
// FetchLoopOutSwaps returns all swaps currently in the store.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *storeMock) FetchLoopOutSwaps(ctx context.Context) ([]*loopdb.LoopOut, error) {
result := []*loopdb.LoopOut{}
for hash, contract := range s.loopOutSwaps {
updates := s.loopOutUpdates[hash]
events := make([]*loopdb.LoopEvent, len(updates))
for i, u := range updates {
events[i] = &loopdb.LoopEvent{
SwapStateData: u,
}
}
swap := &loopdb.LoopOut{
Loop: loopdb.Loop{
Hash: hash,
Events: events,
},
Contract: contract,
}
result = append(result, swap)
}
return result, nil
}
// FetchLoopOutSwaps returns all swaps currently in the store.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *storeMock) FetchLoopOutSwap(ctx context.Context,
hash lntypes.Hash) (*loopdb.LoopOut, error) {
contract, ok := s.loopOutSwaps[hash]
if !ok {
return nil, errors.New("swap not found")
}
updates := s.loopOutUpdates[hash]
events := make([]*loopdb.LoopEvent, len(updates))
for i, u := range updates {
events[i] = &loopdb.LoopEvent{
SwapStateData: u,
}
}
swap := &loopdb.LoopOut{
Loop: loopdb.Loop{
Hash: hash,
Events: events,
},
Contract: contract,
}
return swap, nil
}
// CreateLoopOut adds an initiated swap to the store.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *storeMock) CreateLoopOut(ctx context.Context, hash lntypes.Hash,
swap *loopdb.LoopOutContract) error {
_, ok := s.loopOutSwaps[hash]
if ok {
return errors.New("swap already exists")
}
s.loopOutSwaps[hash] = swap
s.loopOutUpdates[hash] = []loopdb.SwapStateData{}
s.loopOutStoreChan <- *swap
return nil
}
// FetchLoopInSwaps returns all in swaps currently in the store.
func (s *storeMock) FetchLoopInSwaps(ctx context.Context) ([]*loopdb.LoopIn,
error) {
result := []*loopdb.LoopIn{}
for hash, contract := range s.loopInSwaps {
updates := s.loopInUpdates[hash]
events := make([]*loopdb.LoopEvent, len(updates))
for i, u := range updates {
events[i] = &loopdb.LoopEvent{
SwapStateData: u,
}
}
swap := &loopdb.LoopIn{
Loop: loopdb.Loop{
Hash: hash,
Events: events,
},
Contract: contract,
}
result = append(result, swap)
}
return result, nil
}
// CreateLoopIn adds an initiated loop in swap to the store.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *storeMock) CreateLoopIn(ctx context.Context, hash lntypes.Hash,
swap *loopdb.LoopInContract) error {
_, ok := s.loopInSwaps[hash]
if ok {
return errors.New("swap already exists")
}
s.loopInSwaps[hash] = swap
s.loopInUpdates[hash] = []loopdb.SwapStateData{}
s.loopInStoreChan <- *swap
return nil
}
// UpdateLoopOut stores a new event for a target loop out swap. This appends to
// the event log for a particular swap as it goes through the various stages in
// its lifetime.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *storeMock) UpdateLoopOut(ctx context.Context, hash lntypes.Hash,
time time.Time, state loopdb.SwapStateData) error {
updates, ok := s.loopOutUpdates[hash]
if !ok {
return errors.New("swap does not exists")
}
updates = append(updates, state)
s.loopOutUpdates[hash] = updates
s.loopOutUpdateChan <- state
return nil
}
// UpdateLoopIn stores a new event for a target loop in swap. This appends to
// the event log for a particular swap as it goes through the various stages in
// its lifetime.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *storeMock) UpdateLoopIn(ctx context.Context, hash lntypes.Hash,
time time.Time, state loopdb.SwapStateData) error {
updates, ok := s.loopInUpdates[hash]
if !ok {
return errors.New("swap does not exists")
}
updates = append(updates, state)
s.loopInUpdates[hash] = updates
s.loopInUpdateChan <- state
return nil
}
// PutLiquidityParams writes the serialized `manager.Parameters` bytes into the
// bucket.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *storeMock) PutLiquidityParams(ctx context.Context,
params []byte) error {
return nil
}
// FetchLiquidityParams reads the serialized `manager.Parameters` bytes from
// the bucket.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *storeMock) FetchLiquidityParams(ctx context.Context) ([]byte, error) {
return nil, nil
}
func (s *storeMock) Close() error {
return nil
}
func (s *storeMock) isDone() error {
select {
case <-s.loopOutStoreChan:
return errors.New("storeChan not empty")
default:
}
select {
case <-s.loopOutUpdateChan:
return errors.New("updateChan not empty")
default:
}
return nil
}
func (s *storeMock) assertLoopOutStored() {
s.t.Helper()
select {
case <-s.loopOutStoreChan:
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be stored")
}
}
func (s *storeMock) assertLoopOutState(expectedState loopdb.SwapState) {
s.t.Helper()
state := <-s.loopOutUpdateChan
if state.State != expectedState {
s.t.Fatalf("expected state %v, got %v", expectedState, state)
}
}
func (s *storeMock) assertLoopInStored() {
s.t.Helper()
<-s.loopInStoreChan
}
// assertLoopInState asserts that a specified state transition is persisted to
// disk.
func (s *storeMock) assertLoopInState(
expectedState loopdb.SwapState) loopdb.SwapStateData {
s.t.Helper()
state := <-s.loopInUpdateChan
require.Equal(s.t, expectedState, state.State)
return state
}
func (s *storeMock) assertStorePreimageReveal() {
s.t.Helper()
select {
case state := <-s.loopOutUpdateChan:
require.Equal(s.t, loopdb.StatePreimageRevealed, state.State)
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be marked as preimage revealed")
}
}
func (s *storeMock) assertStoreFinished(expectedResult loopdb.SwapState) {
s.t.Helper()
select {
case state := <-s.loopOutUpdateChan:
require.Equal(s.t, expectedResult, state.State)
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be finished")
}
}
func (b *storeMock) BatchCreateLoopOut(ctx context.Context,
swaps map[lntypes.Hash]*loopdb.LoopOutContract) error {
return errors.New("not implemented")
}
func (b *storeMock) BatchCreateLoopIn(ctx context.Context,
swaps map[lntypes.Hash]*loopdb.LoopInContract) error {
return errors.New("not implemented")
}
func (b *storeMock) BatchInsertUpdate(ctx context.Context,
updateData map[lntypes.Hash][]loopdb.BatchInsertUpdateData) error {
return errors.New("not implemented")
}

@ -4,11 +4,10 @@ import (
"context" "context"
"time" "time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/input" "github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
) )
@ -50,59 +49,10 @@ func newSwapKit(hash lntypes.Hash, swapType swap.Type, cfg *swapConfig,
} }
} }
// GetHtlcScriptVersion returns the correct HTLC script version for the passed
// protocol version.
func GetHtlcScriptVersion(
protocolVersion loopdb.ProtocolVersion) swap.ScriptVersion {
// If the swap was initiated before we had our v3 script, use v2.
if protocolVersion < loopdb.ProtocolVersionHtlcV3 ||
protocolVersion == loopdb.ProtocolVersionUnrecorded {
return swap.HtlcV2
}
return swap.HtlcV3
}
// IsTaproot returns true if the swap referenced by the passed swap contract // IsTaproot returns true if the swap referenced by the passed swap contract
// uses the v3 (taproot) htlc. // uses the v3 (taproot) htlc.
func IsTaprootSwap(swapContract *loopdb.SwapContract) bool { func IsTaprootSwap(swapContract *loopdb.SwapContract) bool {
return GetHtlcScriptVersion(swapContract.ProtocolVersion) == swap.HtlcV3 return utils.GetHtlcScriptVersion(swapContract.ProtocolVersion) == swap.HtlcV3
}
// GetHtlc composes and returns the on-chain swap script.
func GetHtlc(hash lntypes.Hash, contract *loopdb.SwapContract,
chainParams *chaincfg.Params) (*swap.Htlc, error) {
switch GetHtlcScriptVersion(contract.ProtocolVersion) {
case swap.HtlcV2:
return swap.NewHtlcV2(
contract.CltvExpiry, contract.HtlcKeys.SenderScriptKey,
contract.HtlcKeys.ReceiverScriptKey, hash,
chainParams,
)
case swap.HtlcV3:
// Swaps that implement the new MuSig2 protocol will be expected
// to use the 1.0RC2 MuSig2 key derivation scheme.
muSig2Version := input.MuSig2Version040
if contract.ProtocolVersion >= loopdb.ProtocolVersionMuSig2 {
muSig2Version = input.MuSig2Version100RC2
}
return swap.NewHtlcV3(
muSig2Version,
contract.CltvExpiry,
contract.HtlcKeys.SenderInternalPubKey,
contract.HtlcKeys.ReceiverInternalPubKey,
contract.HtlcKeys.SenderScriptKey,
contract.HtlcKeys.ReceiverScriptKey,
hash, chainParams,
)
}
return nil, swap.ErrInvalidScriptVersion
} }
// swapInfo constructs and returns a filled SwapInfo from // swapInfo constructs and returns a filled SwapInfo from

@ -14,6 +14,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/aperture/lsat" "github.com/lightninglabs/aperture/lsat"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
looprpc "github.com/lightninglabs/loop/swapserverrpc" looprpc "github.com/lightninglabs/loop/swapserverrpc"
@ -775,6 +776,51 @@ func (s *grpcSwapServerClient) MuSig2SignSweep(ctx context.Context,
return res.Nonce, res.PartialSignature, nil 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 // PushKey sends the client's HTLC internal key associated with the swap to
// the server. // the server.
func (s *grpcSwapServerClient) PushKey(ctx context.Context, func (s *grpcSwapServerClient) PushKey(ctx context.Context,

@ -1973,8 +1973,7 @@ type ServerProbeRequest struct {
// The protocol version that the client adheres to. // The protocol version that the client adheres to.
ProtocolVersion ProtocolVersion `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3,enum=looprpc.ProtocolVersion" json:"protocol_version,omitempty"` ProtocolVersion ProtocolVersion `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3,enum=looprpc.ProtocolVersion" json:"protocol_version,omitempty"`
// The probe amount. Amt uint64 `protobuf:"varint,2,opt,name=amt,proto3" json:"amt,omitempty"`
Amt uint64 `protobuf:"varint,2,opt,name=amt,proto3" json:"amt,omitempty"`
// The target node for the probe. // The target node for the probe.
Target []byte `protobuf:"bytes,3,opt,name=target,proto3" json:"target,omitempty"` Target []byte `protobuf:"bytes,3,opt,name=target,proto3" json:"target,omitempty"`
// Optional last hop to use when probing the client. // Optional last hop to use when probing the client.
@ -2357,6 +2356,8 @@ type MuSig2SignSweepReq struct {
Nonce []byte `protobuf:"bytes,4,opt,name=nonce,proto3" json:"nonce,omitempty"` Nonce []byte `protobuf:"bytes,4,opt,name=nonce,proto3" json:"nonce,omitempty"`
// The psbt of the sweep txn. // The psbt of the sweep txn.
SweepTxPsbt []byte `protobuf:"bytes,6,opt,name=sweep_tx_psbt,json=sweepTxPsbt,proto3" json:"sweep_tx_psbt,omitempty"` SweepTxPsbt []byte `protobuf:"bytes,6,opt,name=sweep_tx_psbt,json=sweepTxPsbt,proto3" json:"sweep_tx_psbt,omitempty"`
// The prevout information of the sweep txn.
PrevoutInfo []*PrevoutInfo `protobuf:"bytes,7,rep,name=prevout_info,json=prevoutInfo,proto3" json:"prevout_info,omitempty"`
} }
func (x *MuSig2SignSweepReq) Reset() { func (x *MuSig2SignSweepReq) Reset() {
@ -2426,6 +2427,88 @@ func (x *MuSig2SignSweepReq) GetSweepTxPsbt() []byte {
return nil return nil
} }
func (x *MuSig2SignSweepReq) GetPrevoutInfo() []*PrevoutInfo {
if x != nil {
return x.PrevoutInfo
}
return nil
}
type PrevoutInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The value of the txout.
Value uint64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"`
// The pk_script of the txout.
PkScript []byte `protobuf:"bytes,2,opt,name=pk_script,json=pkScript,proto3" json:"pk_script,omitempty"`
// The txid of the txout.
TxidBytes []byte `protobuf:"bytes,3,opt,name=txid_bytes,json=txidBytes,proto3" json:"txid_bytes,omitempty"`
// The index of the txout.
OutputIndex uint32 `protobuf:"varint,4,opt,name=output_index,json=outputIndex,proto3" json:"output_index,omitempty"`
}
func (x *PrevoutInfo) Reset() {
*x = PrevoutInfo{}
if protoimpl.UnsafeEnabled {
mi := &file_server_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PrevoutInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PrevoutInfo) ProtoMessage() {}
func (x *PrevoutInfo) ProtoReflect() protoreflect.Message {
mi := &file_server_proto_msgTypes[28]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PrevoutInfo.ProtoReflect.Descriptor instead.
func (*PrevoutInfo) Descriptor() ([]byte, []int) {
return file_server_proto_rawDescGZIP(), []int{28}
}
func (x *PrevoutInfo) GetValue() uint64 {
if x != nil {
return x.Value
}
return 0
}
func (x *PrevoutInfo) GetPkScript() []byte {
if x != nil {
return x.PkScript
}
return nil
}
func (x *PrevoutInfo) GetTxidBytes() []byte {
if x != nil {
return x.TxidBytes
}
return nil
}
func (x *PrevoutInfo) GetOutputIndex() uint32 {
if x != nil {
return x.OutputIndex
}
return 0
}
type MuSig2SignSweepRes struct { type MuSig2SignSweepRes struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -2440,7 +2523,7 @@ type MuSig2SignSweepRes struct {
func (x *MuSig2SignSweepRes) Reset() { func (x *MuSig2SignSweepRes) Reset() {
*x = MuSig2SignSweepRes{} *x = MuSig2SignSweepRes{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_server_proto_msgTypes[28] mi := &file_server_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -2453,7 +2536,7 @@ func (x *MuSig2SignSweepRes) String() string {
func (*MuSig2SignSweepRes) ProtoMessage() {} func (*MuSig2SignSweepRes) ProtoMessage() {}
func (x *MuSig2SignSweepRes) ProtoReflect() protoreflect.Message { func (x *MuSig2SignSweepRes) ProtoReflect() protoreflect.Message {
mi := &file_server_proto_msgTypes[28] mi := &file_server_proto_msgTypes[29]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -2466,7 +2549,7 @@ func (x *MuSig2SignSweepRes) ProtoReflect() protoreflect.Message {
// Deprecated: Use MuSig2SignSweepRes.ProtoReflect.Descriptor instead. // Deprecated: Use MuSig2SignSweepRes.ProtoReflect.Descriptor instead.
func (*MuSig2SignSweepRes) Descriptor() ([]byte, []int) { func (*MuSig2SignSweepRes) Descriptor() ([]byte, []int) {
return file_server_proto_rawDescGZIP(), []int{28} return file_server_proto_rawDescGZIP(), []int{29}
} }
func (x *MuSig2SignSweepRes) GetNonce() []byte { func (x *MuSig2SignSweepRes) GetNonce() []byte {
@ -2499,7 +2582,7 @@ type ServerPushKeyReq struct {
func (x *ServerPushKeyReq) Reset() { func (x *ServerPushKeyReq) Reset() {
*x = ServerPushKeyReq{} *x = ServerPushKeyReq{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_server_proto_msgTypes[29] mi := &file_server_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -2512,7 +2595,7 @@ func (x *ServerPushKeyReq) String() string {
func (*ServerPushKeyReq) ProtoMessage() {} func (*ServerPushKeyReq) ProtoMessage() {}
func (x *ServerPushKeyReq) ProtoReflect() protoreflect.Message { func (x *ServerPushKeyReq) ProtoReflect() protoreflect.Message {
mi := &file_server_proto_msgTypes[29] mi := &file_server_proto_msgTypes[30]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -2525,7 +2608,7 @@ func (x *ServerPushKeyReq) ProtoReflect() protoreflect.Message {
// Deprecated: Use ServerPushKeyReq.ProtoReflect.Descriptor instead. // Deprecated: Use ServerPushKeyReq.ProtoReflect.Descriptor instead.
func (*ServerPushKeyReq) Descriptor() ([]byte, []int) { func (*ServerPushKeyReq) Descriptor() ([]byte, []int) {
return file_server_proto_rawDescGZIP(), []int{29} return file_server_proto_rawDescGZIP(), []int{30}
} }
func (x *ServerPushKeyReq) GetProtocolVersion() ProtocolVersion { func (x *ServerPushKeyReq) GetProtocolVersion() ProtocolVersion {
@ -2558,7 +2641,7 @@ type ServerPushKeyRes struct {
func (x *ServerPushKeyRes) Reset() { func (x *ServerPushKeyRes) Reset() {
*x = ServerPushKeyRes{} *x = ServerPushKeyRes{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_server_proto_msgTypes[30] mi := &file_server_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -2571,7 +2654,7 @@ func (x *ServerPushKeyRes) String() string {
func (*ServerPushKeyRes) ProtoMessage() {} func (*ServerPushKeyRes) ProtoMessage() {}
func (x *ServerPushKeyRes) ProtoReflect() protoreflect.Message { func (x *ServerPushKeyRes) ProtoReflect() protoreflect.Message {
mi := &file_server_proto_msgTypes[30] mi := &file_server_proto_msgTypes[31]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -2584,7 +2667,7 @@ func (x *ServerPushKeyRes) ProtoReflect() protoreflect.Message {
// Deprecated: Use ServerPushKeyRes.ProtoReflect.Descriptor instead. // Deprecated: Use ServerPushKeyRes.ProtoReflect.Descriptor instead.
func (*ServerPushKeyRes) Descriptor() ([]byte, []int) { func (*ServerPushKeyRes) Descriptor() ([]byte, []int) {
return file_server_proto_rawDescGZIP(), []int{30} return file_server_proto_rawDescGZIP(), []int{31}
} }
// FetchL402Request is an empty request sent from the client to the server to // FetchL402Request is an empty request sent from the client to the server to
@ -2598,7 +2681,7 @@ type FetchL402Request struct {
func (x *FetchL402Request) Reset() { func (x *FetchL402Request) Reset() {
*x = FetchL402Request{} *x = FetchL402Request{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_server_proto_msgTypes[31] mi := &file_server_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -2611,7 +2694,7 @@ func (x *FetchL402Request) String() string {
func (*FetchL402Request) ProtoMessage() {} func (*FetchL402Request) ProtoMessage() {}
func (x *FetchL402Request) ProtoReflect() protoreflect.Message { func (x *FetchL402Request) ProtoReflect() protoreflect.Message {
mi := &file_server_proto_msgTypes[31] mi := &file_server_proto_msgTypes[32]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -2624,7 +2707,7 @@ func (x *FetchL402Request) ProtoReflect() protoreflect.Message {
// Deprecated: Use FetchL402Request.ProtoReflect.Descriptor instead. // Deprecated: Use FetchL402Request.ProtoReflect.Descriptor instead.
func (*FetchL402Request) Descriptor() ([]byte, []int) { func (*FetchL402Request) Descriptor() ([]byte, []int) {
return file_server_proto_rawDescGZIP(), []int{31} return file_server_proto_rawDescGZIP(), []int{32}
} }
// FetchL402Response is an empty response sent from the server to the client to // FetchL402Response is an empty response sent from the server to the client to
@ -2638,7 +2721,7 @@ type FetchL402Response struct {
func (x *FetchL402Response) Reset() { func (x *FetchL402Response) Reset() {
*x = FetchL402Response{} *x = FetchL402Response{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_server_proto_msgTypes[32] mi := &file_server_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -2651,7 +2734,7 @@ func (x *FetchL402Response) String() string {
func (*FetchL402Response) ProtoMessage() {} func (*FetchL402Response) ProtoMessage() {}
func (x *FetchL402Response) ProtoReflect() protoreflect.Message { func (x *FetchL402Response) ProtoReflect() protoreflect.Message {
mi := &file_server_proto_msgTypes[32] mi := &file_server_proto_msgTypes[33]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -2664,7 +2747,7 @@ func (x *FetchL402Response) ProtoReflect() protoreflect.Message {
// Deprecated: Use FetchL402Response.ProtoReflect.Descriptor instead. // Deprecated: Use FetchL402Response.ProtoReflect.Descriptor instead.
func (*FetchL402Response) Descriptor() ([]byte, []int) { func (*FetchL402Response) Descriptor() ([]byte, []int) {
return file_server_proto_rawDescGZIP(), []int{32} return file_server_proto_rawDescGZIP(), []int{33}
} }
var File_server_proto protoreflect.FileDescriptor var File_server_proto protoreflect.FileDescriptor
@ -2947,7 +3030,7 @@ var file_server_proto_rawDesc = []byte{
0x74, 0x69, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61,
0x6c, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x18, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x18, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52,
0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x73, 0x22, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x73, 0x22,
0xdf, 0x01, 0x0a, 0x12, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77, 0x98, 0x02, 0x0a, 0x12, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77,
0x65, 0x65, 0x70, 0x52, 0x65, 0x71, 0x12, 0x43, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x65, 0x65, 0x70, 0x52, 0x65, 0x71, 0x12, 0x43, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
0x6f, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x6f, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
0x32, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f,
@ -2960,188 +3043,200 @@ var file_server_proto_rawDesc = []byte{
0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x73, 0x77, 0x65, 0x65, 0x70, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x73, 0x77, 0x65, 0x65, 0x70,
0x5f, 0x74, 0x78, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x5f, 0x74, 0x78, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b,
0x73, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x50, 0x73, 0x62, 0x74, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x73, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x50, 0x73, 0x62, 0x74, 0x12, 0x37, 0x0a, 0x0c, 0x70,
0x06, 0x22, 0x57, 0x0a, 0x12, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x72, 0x65, 0x76, 0x6f, 0x75, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x07, 0x20, 0x03, 0x28,
0x77, 0x65, 0x65, 0x70, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x65, 0x76,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x6f, 0x75, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x76, 0x6f, 0x75, 0x74,
0x11, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x49, 0x6e, 0x66, 0x6f, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, 0x82, 0x01, 0x0a, 0x0b, 0x50,
0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x72, 0x65, 0x76, 0x6f, 0x75, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
0x6c, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x10, 0x53, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x12, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x6b, 0x5f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x02, 0x20,
0x43, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x6b, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x1d, 0x0a,
0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x0a, 0x74, 0x78, 0x69, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x0c, 0x52, 0x09, 0x74, 0x78, 0x69, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c,
0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01,
0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x28, 0x0d, 0x52, 0x0b, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22,
0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x48, 0x61, 0x73, 0x57, 0x0a, 0x12, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77, 0x65,
0x68, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x72, 0x65, 0x70, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01,
0x69, 0x76, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x69, 0x6e, 0x74, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x70,
0x65, 0x72, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x69, 0x76, 0x6b, 0x65, 0x79, 0x22, 0x12, 0x0a, 0x10, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65,
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x53,
0x22, 0x12, 0x0a, 0x10, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x32, 0x52, 0x65, 0x71, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x72,
0x75, 0x65, 0x73, 0x74, 0x22, 0x13, 0x0a, 0x11, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x12, 0x43, 0x0a,
0x32, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0xef, 0x01, 0x0a, 0x0f, 0x50, 0x72, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70,
0x06, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x4d, 0x55, 0x4c, 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x54, 0x49, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x6e, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69,
0x15, 0x4e, 0x41, 0x54, 0x49, 0x56, 0x45, 0x5f, 0x53, 0x45, 0x47, 0x57, 0x49, 0x54, 0x5f, 0x4c, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18,
0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x52, 0x45, 0x49, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x48, 0x61, 0x73, 0x68, 0x12,
0x4d, 0x41, 0x47, 0x45, 0x5f, 0x50, 0x55, 0x53, 0x48, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x72, 0x69, 0x76,
0x55, 0x54, 0x10, 0x03, 0x12, 0x18, 0x0a, 0x14, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x45, 0x58, 0x50, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x49, 0x52, 0x59, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x04, 0x12, 0x0b, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x69, 0x76, 0x6b, 0x65, 0x79, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x65,
0x0a, 0x07, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x56, 0x32, 0x10, 0x05, 0x12, 0x11, 0x0a, 0x0d, 0x4d, 0x72, 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x22, 0x12,
0x55, 0x4c, 0x54, 0x49, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x06, 0x12, 0x13, 0x0a, 0x10, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x32, 0x52, 0x65, 0x71, 0x75, 0x65,
0x0a, 0x0f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x73, 0x74, 0x22, 0x13, 0x0a, 0x11, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x32, 0x52,
0x4c, 0x10, 0x07, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x52, 0x4f, 0x42, 0x45, 0x10, 0x08, 0x12, 0x12, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0xef, 0x01, 0x0a, 0x0f, 0x50, 0x72, 0x6f, 0x74,
0x0a, 0x0e, 0x52, 0x4f, 0x55, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x50, 0x4c, 0x55, 0x47, 0x49, 0x4e, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x4c,
0x10, 0x09, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x56, 0x33, 0x10, 0x0a, 0x12, 0x45, 0x47, 0x41, 0x43, 0x59, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x4d, 0x55, 0x4c, 0x54, 0x49,
0x0a, 0x0a, 0x06, 0x4d, 0x55, 0x53, 0x49, 0x47, 0x32, 0x10, 0x0b, 0x2a, 0x9e, 0x04, 0x0a, 0x0f, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x4e,
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x41, 0x54, 0x49, 0x56, 0x45, 0x5f, 0x53, 0x45, 0x47, 0x57, 0x49, 0x54, 0x5f, 0x4c, 0x4f, 0x4f,
0x14, 0x0a, 0x10, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x52, 0x45, 0x49, 0x4d, 0x41,
0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x47, 0x45, 0x5f, 0x50, 0x55, 0x53, 0x48, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54,
0x48, 0x54, 0x4c, 0x43, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0x01, 0x10, 0x03, 0x12, 0x18, 0x0a, 0x14, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52,
0x12, 0x12, 0x0a, 0x0e, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x59, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x04, 0x12, 0x0b, 0x0a, 0x07,
0x53, 0x53, 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x56, 0x32, 0x10, 0x05, 0x12, 0x11, 0x0a, 0x0d, 0x4d, 0x55, 0x4c,
0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x12, 0x54, 0x49, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x06, 0x12, 0x13, 0x0a, 0x0f,
0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10,
0x5f, 0x4e, 0x4f, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, 0x04, 0x12, 0x25, 0x0a, 0x21, 0x53, 0x45, 0x07, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x52, 0x4f, 0x42, 0x45, 0x10, 0x08, 0x12, 0x12, 0x0a, 0x0e,
0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x52, 0x4f, 0x55, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x50, 0x4c, 0x55, 0x47, 0x49, 0x4e, 0x10, 0x09,
0x4c, 0x49, 0x44, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x4d, 0x4f, 0x55, 0x4e, 0x54, 0x10, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x56, 0x33, 0x10, 0x0a, 0x12, 0x0a, 0x0a,
0x05, 0x12, 0x23, 0x0a, 0x1f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x06, 0x4d, 0x55, 0x53, 0x49, 0x47, 0x32, 0x10, 0x0b, 0x2a, 0x9e, 0x04, 0x0a, 0x0f, 0x53, 0x65,
0x45, 0x44, 0x5f, 0x4f, 0x46, 0x46, 0x5f, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x72, 0x76, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a,
0x45, 0x4f, 0x55, 0x54, 0x10, 0x06, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x10, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x45,
0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x48, 0x54,
0x07, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x4c, 0x43, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0x01, 0x12, 0x12,
0x45, 0x44, 0x5f, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x44, 0x45, 0x41, 0x44, 0x4c, 0x49, 0x4e, 0x45, 0x0a, 0x0e, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53,
0x10, 0x08, 0x12, 0x22, 0x0a, 0x1e, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49,
0x4c, 0x45, 0x44, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x41, 0x4c, 0x45, 0x44, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x12, 0x19, 0x0a,
0x54, 0x49, 0x4f, 0x4e, 0x10, 0x09, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x4e,
0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x4f, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, 0x04, 0x12, 0x25, 0x0a, 0x21, 0x53, 0x45, 0x52, 0x56,
0x45, 0x44, 0x10, 0x0a, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x55, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49,
0x4e, 0x45, 0x58, 0x50, 0x45, 0x43, 0x54, 0x45, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x44, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x4d, 0x4f, 0x55, 0x4e, 0x54, 0x10, 0x05, 0x12,
0x45, 0x10, 0x0b, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x48, 0x54, 0x23, 0x0a, 0x1f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44,
0x4c, 0x43, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x10, 0x0c, 0x12, 0x1f, 0x5f, 0x4f, 0x46, 0x46, 0x5f, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f,
0x0a, 0x1b, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x54, 0x10, 0x06, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46,
0x50, 0x52, 0x45, 0x50, 0x41, 0x59, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x0d, 0x12, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x07, 0x12,
0x20, 0x0a, 0x1c, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x1f, 0x0a, 0x1b, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44,
0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x5f, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x44, 0x45, 0x41, 0x44, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x08,
0x0e, 0x12, 0x27, 0x0a, 0x23, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x12, 0x22, 0x0a, 0x1e, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45,
0x45, 0x44, 0x5f, 0x4d, 0x55, 0x4c, 0x54, 0x49, 0x50, 0x4c, 0x45, 0x5f, 0x53, 0x57, 0x41, 0x50, 0x44, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x41, 0x54, 0x49,
0x5f, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x53, 0x10, 0x0f, 0x12, 0x20, 0x0a, 0x1c, 0x53, 0x45, 0x4f, 0x4e, 0x10, 0x09, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x54,
0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, 0x44,
0x49, 0x41, 0x4c, 0x49, 0x5a, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x10, 0x2a, 0x4a, 0x0a, 0x10, 0x10, 0x0a, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x55, 0x4e, 0x45,
0x52, 0x6f, 0x75, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x58, 0x50, 0x45, 0x43, 0x54, 0x45, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10,
0x12, 0x11, 0x0a, 0x0d, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x0b, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x48, 0x54, 0x4c, 0x43,
0x4e, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x52, 0x45, 0x50, 0x41, 0x59, 0x5f, 0x52, 0x4f, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x10, 0x0c, 0x12, 0x1f, 0x0a, 0x1b,
0x55, 0x54, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x52,
0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x02, 0x2a, 0xf1, 0x01, 0x0a, 0x14, 0x50, 0x61, 0x79, 0x45, 0x50, 0x41, 0x59, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x0d, 0x12, 0x20, 0x0a,
0x6d, 0x65, 0x6e, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x1c, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x49,
0x6e, 0x12, 0x1b, 0x0a, 0x17, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x0e, 0x12,
0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1e, 0x27, 0x0a, 0x23, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44,
0x0a, 0x1a, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x5f, 0x4d, 0x55, 0x4c, 0x54, 0x49, 0x50, 0x4c, 0x45, 0x5f, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x53,
0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x1f, 0x43, 0x52, 0x49, 0x50, 0x54, 0x53, 0x10, 0x0f, 0x12, 0x20, 0x0a, 0x1c, 0x53, 0x45, 0x52, 0x56,
0x0a, 0x1b, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41,
0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x02, 0x12, 0x4c, 0x49, 0x5a, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x10, 0x2a, 0x4a, 0x0a, 0x10, 0x52, 0x6f,
0x1c, 0x0a, 0x18, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x75, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11,
0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x30, 0x0a, 0x0a, 0x0d, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10,
0x2c, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x52, 0x45, 0x50, 0x41, 0x59, 0x5f, 0x52, 0x4f, 0x55, 0x54,
0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x50, 0x41, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x52,
0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, 0x53, 0x10, 0x04, 0x12, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x02, 0x2a, 0xf1, 0x01, 0x0a, 0x14, 0x50, 0x61, 0x79, 0x6d, 0x65,
0x2b, 0x0a, 0x27, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x6e, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12,
0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x1b, 0x0a, 0x17, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52,
0x4e, 0x54, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x05, 0x2a, 0x27, 0x0a, 0x0d, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a,
0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x08, 0x0a, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53,
0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x57, 0x5f, 0x48, 0x4f, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x1f, 0x0a, 0x1b,
0x49, 0x47, 0x48, 0x10, 0x01, 0x32, 0xdc, 0x0a, 0x0a, 0x0a, 0x53, 0x77, 0x61, 0x70, 0x53, 0x65, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53,
0x72, 0x76, 0x65, 0x72, 0x12, 0x4f, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x02, 0x12, 0x1c, 0x0a,
0x65, 0x72, 0x6d, 0x73, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x18, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41,
0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x30, 0x0a, 0x2c, 0x4c,
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f,
0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x4e, 0x5f, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x4d,
0x54, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x4f, 0x0a, 0x0e, 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x6f, 0x70, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, 0x53, 0x10, 0x04, 0x12, 0x2b, 0x0a,
0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x12, 0x1d, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x27, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41,
0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x05, 0x2a, 0x27, 0x0a, 0x0d, 0x52, 0x6f,
0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x57, 0x5f, 0x48, 0x49, 0x47,
0x74, 0x50, 0x75, 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x29, 0x2e, 0x48, 0x10, 0x01, 0x32, 0xdc, 0x0a, 0x0a, 0x0a, 0x53, 0x77, 0x61, 0x70, 0x53, 0x65, 0x72, 0x76,
0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x65, 0x72, 0x12, 0x4f, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72,
0x6f, 0x70, 0x4f, 0x75, 0x74, 0x50, 0x75, 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x6d, 0x73, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52,
0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63,
0x50, 0x75, 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x72, 0x6d, 0x73, 0x12, 0x4f, 0x0a, 0x0e, 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75,
0x75, 0x6f, 0x74, 0x65, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x53, 0x77, 0x61, 0x70, 0x12, 0x1d, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e,
0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53,
0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70,
0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x50,
0x65, 0x72, 0x6d, 0x73, 0x12, 0x21, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x29, 0x2e, 0x6c, 0x6f,
0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x4f, 0x75, 0x74, 0x50, 0x75, 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x52,
0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63,
0x72, 0x6d, 0x73, 0x12, 0x4c, 0x0a, 0x0d, 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x50, 0x75,
0x53, 0x77, 0x61, 0x70, 0x12, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f,
0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x74, 0x65, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72,
0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52,
0x65, 0x12, 0x54, 0x0a, 0x0b, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63,
0x12, 0x21, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75,
0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x72,
0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6d, 0x73, 0x12, 0x21, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72,
0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, 0x17, 0x53, 0x75, 0x62, 0x73, 0x63, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e,
0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d,
0x65, 0x73, 0x12, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x12, 0x4c, 0x0a, 0x0d, 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x53, 0x77,
0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x61, 0x70, 0x12, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x55, 0x1a, 0x1d, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65,
0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x12, 0x65, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f, 0x6f, 0x54, 0x0a, 0x0b, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x21,
0x70, 0x49, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c,
0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x55, 0x70, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6c, 0x74, 0x1a, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76,
0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73,
0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, 0x17, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x11, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x62, 0x65, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73,
0x6c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x12, 0x21, 0x2e, 0x6c, 0x12, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63,
0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x6f, 0x6f, 0x72, 0x69, 0x62, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x70, 0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62,
0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x55, 0x70, 0x64,
0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x65,
0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f, 0x6f, 0x70, 0x49,
0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72,
0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x55, 0x70, 0x64, 0x61,
0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6c, 0x6f, 0x6f,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x16, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f,
0x6d, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6f, 0x70, 0x49, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x11, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c,
0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x12, 0x21, 0x2e, 0x6c, 0x6f, 0x6f,
0x69, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f,
0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e,
0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x12, 0x57, 0x0a, 0x13, 0x52, 0x65, 0x70, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x6f,
0x6f, 0x72, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x12, 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f,
0x74, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x62, 0x65,
0x71, 0x1a, 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70,
0x72, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x73,
0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0f, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x16, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65,
0x53, 0x77, 0x65, 0x65, 0x70, 0x12, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12,
0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77, 0x65, 0x65, 0x70, 0x52, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d,
0x65, 0x71, 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x75, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e,
0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77, 0x65, 0x65, 0x70, 0x52, 0x65, 0x73, 0x12, 0x52, 0x65, 0x71, 0x1a, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65,
0x3f, 0x0a, 0x07, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c,
0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x75, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x12, 0x57, 0x0a, 0x13, 0x52, 0x65, 0x70, 0x6f, 0x72,
0x65, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f,
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52,
0x12, 0x42, 0x0a, 0x09, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x32, 0x12, 0x19, 0x2e, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x71, 0x1a,
0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74,
0x32, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x73,
0x70, 0x63, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x32, 0x52, 0x65, 0x73, 0x70, 0x12, 0x4b, 0x0a, 0x0f, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77,
0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x65, 0x65, 0x70, 0x12, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x75,
0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77, 0x65, 0x65, 0x70, 0x52, 0x65, 0x71,
0x2f, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x73, 0x77, 0x61, 0x70, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x75, 0x53, 0x69, 0x67,
0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77, 0x65, 0x65, 0x70, 0x52, 0x65, 0x73, 0x12, 0x3f, 0x0a,
0x07, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72,
0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79,
0x52, 0x65, 0x71, 0x1a, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x12, 0x42,
0x0a, 0x09, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x32, 0x12, 0x19, 0x2e, 0x6c, 0x6f,
0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x32, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63,
0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x32, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c,
0x6f, 0x6f, 0x70, 0x2f, 0x73, 0x77, 0x61, 0x70, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70,
0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@ -3157,7 +3252,7 @@ func file_server_proto_rawDescGZIP() []byte {
} }
var file_server_proto_enumTypes = make([]protoimpl.EnumInfo, 5) var file_server_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
var file_server_proto_msgTypes = make([]protoimpl.MessageInfo, 33) var file_server_proto_msgTypes = make([]protoimpl.MessageInfo, 34)
var file_server_proto_goTypes = []interface{}{ var file_server_proto_goTypes = []interface{}{
(ProtocolVersion)(0), // 0: looprpc.ProtocolVersion (ProtocolVersion)(0), // 0: looprpc.ProtocolVersion
(ServerSwapState)(0), // 1: looprpc.ServerSwapState (ServerSwapState)(0), // 1: looprpc.ServerSwapState
@ -3192,19 +3287,20 @@ var file_server_proto_goTypes = []interface{}{
(*ReportRoutingResultReq)(nil), // 30: looprpc.ReportRoutingResultReq (*ReportRoutingResultReq)(nil), // 30: looprpc.ReportRoutingResultReq
(*ReportRoutingResultRes)(nil), // 31: looprpc.ReportRoutingResultRes (*ReportRoutingResultRes)(nil), // 31: looprpc.ReportRoutingResultRes
(*MuSig2SignSweepReq)(nil), // 32: looprpc.MuSig2SignSweepReq (*MuSig2SignSweepReq)(nil), // 32: looprpc.MuSig2SignSweepReq
(*MuSig2SignSweepRes)(nil), // 33: looprpc.MuSig2SignSweepRes (*PrevoutInfo)(nil), // 33: looprpc.PrevoutInfo
(*ServerPushKeyReq)(nil), // 34: looprpc.ServerPushKeyReq (*MuSig2SignSweepRes)(nil), // 34: looprpc.MuSig2SignSweepRes
(*ServerPushKeyRes)(nil), // 35: looprpc.ServerPushKeyRes (*ServerPushKeyReq)(nil), // 35: looprpc.ServerPushKeyReq
(*FetchL402Request)(nil), // 36: looprpc.FetchL402Request (*ServerPushKeyRes)(nil), // 36: looprpc.ServerPushKeyRes
(*FetchL402Response)(nil), // 37: looprpc.FetchL402Response (*FetchL402Request)(nil), // 37: looprpc.FetchL402Request
(*RouteHint)(nil), // 38: looprpc.RouteHint (*FetchL402Response)(nil), // 38: looprpc.FetchL402Response
(*RouteHint)(nil), // 39: looprpc.RouteHint
} }
var file_server_proto_depIdxs = []int32{ var file_server_proto_depIdxs = []int32{
0, // 0: looprpc.ServerLoopOutRequest.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 0: looprpc.ServerLoopOutRequest.protocol_version:type_name -> looprpc.ProtocolVersion
0, // 1: looprpc.ServerLoopOutQuoteRequest.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 1: looprpc.ServerLoopOutQuoteRequest.protocol_version:type_name -> looprpc.ProtocolVersion
0, // 2: looprpc.ServerLoopOutTermsRequest.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 2: looprpc.ServerLoopOutTermsRequest.protocol_version:type_name -> looprpc.ProtocolVersion
0, // 3: looprpc.ServerLoopInRequest.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 3: looprpc.ServerLoopInRequest.protocol_version:type_name -> looprpc.ProtocolVersion
38, // 4: looprpc.ServerLoopInQuoteRequest.route_hints:type_name -> looprpc.RouteHint 39, // 4: looprpc.ServerLoopInQuoteRequest.route_hints:type_name -> looprpc.RouteHint
0, // 5: looprpc.ServerLoopInQuoteRequest.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 5: looprpc.ServerLoopInQuoteRequest.protocol_version:type_name -> looprpc.ProtocolVersion
0, // 6: looprpc.ServerLoopInTermsRequest.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 6: looprpc.ServerLoopInTermsRequest.protocol_version:type_name -> looprpc.ProtocolVersion
0, // 7: looprpc.ServerLoopOutPushPreimageRequest.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 7: looprpc.ServerLoopOutPushPreimageRequest.protocol_version:type_name -> looprpc.ProtocolVersion
@ -3217,50 +3313,51 @@ var file_server_proto_depIdxs = []int32{
0, // 14: looprpc.CancelLoopOutSwapRequest.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 14: looprpc.CancelLoopOutSwapRequest.protocol_version:type_name -> looprpc.ProtocolVersion
22, // 15: looprpc.CancelLoopOutSwapRequest.route_cancel:type_name -> looprpc.RouteCancel 22, // 15: looprpc.CancelLoopOutSwapRequest.route_cancel:type_name -> looprpc.RouteCancel
0, // 16: looprpc.ServerProbeRequest.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 16: looprpc.ServerProbeRequest.protocol_version:type_name -> looprpc.ProtocolVersion
38, // 17: looprpc.ServerProbeRequest.route_hints:type_name -> looprpc.RouteHint 39, // 17: looprpc.ServerProbeRequest.route_hints:type_name -> looprpc.RouteHint
0, // 18: looprpc.RecommendRoutingPluginReq.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 18: looprpc.RecommendRoutingPluginReq.protocol_version:type_name -> looprpc.ProtocolVersion
4, // 19: looprpc.RecommendRoutingPluginRes.plugin:type_name -> looprpc.RoutingPlugin 4, // 19: looprpc.RecommendRoutingPluginRes.plugin:type_name -> looprpc.RoutingPlugin
0, // 20: looprpc.ReportRoutingResultReq.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 20: looprpc.ReportRoutingResultReq.protocol_version:type_name -> looprpc.ProtocolVersion
4, // 21: looprpc.ReportRoutingResultReq.plugin:type_name -> looprpc.RoutingPlugin 4, // 21: looprpc.ReportRoutingResultReq.plugin:type_name -> looprpc.RoutingPlugin
0, // 22: looprpc.MuSig2SignSweepReq.protocol_version:type_name -> looprpc.ProtocolVersion 0, // 22: looprpc.MuSig2SignSweepReq.protocol_version:type_name -> looprpc.ProtocolVersion
0, // 23: looprpc.ServerPushKeyReq.protocol_version:type_name -> looprpc.ProtocolVersion 33, // 23: looprpc.MuSig2SignSweepReq.prevout_info:type_name -> looprpc.PrevoutInfo
9, // 24: looprpc.SwapServer.LoopOutTerms:input_type -> looprpc.ServerLoopOutTermsRequest 0, // 24: looprpc.ServerPushKeyReq.protocol_version:type_name -> looprpc.ProtocolVersion
5, // 25: looprpc.SwapServer.NewLoopOutSwap:input_type -> looprpc.ServerLoopOutRequest 9, // 25: looprpc.SwapServer.LoopOutTerms:input_type -> looprpc.ServerLoopOutTermsRequest
17, // 26: looprpc.SwapServer.LoopOutPushPreimage:input_type -> looprpc.ServerLoopOutPushPreimageRequest 5, // 26: looprpc.SwapServer.NewLoopOutSwap:input_type -> looprpc.ServerLoopOutRequest
7, // 27: looprpc.SwapServer.LoopOutQuote:input_type -> looprpc.ServerLoopOutQuoteRequest 17, // 27: looprpc.SwapServer.LoopOutPushPreimage:input_type -> looprpc.ServerLoopOutPushPreimageRequest
15, // 28: looprpc.SwapServer.LoopInTerms:input_type -> looprpc.ServerLoopInTermsRequest 7, // 28: looprpc.SwapServer.LoopOutQuote:input_type -> looprpc.ServerLoopOutQuoteRequest
11, // 29: looprpc.SwapServer.NewLoopInSwap:input_type -> looprpc.ServerLoopInRequest 15, // 29: looprpc.SwapServer.LoopInTerms:input_type -> looprpc.ServerLoopInTermsRequest
13, // 30: looprpc.SwapServer.LoopInQuote:input_type -> looprpc.ServerLoopInQuoteRequest 11, // 30: looprpc.SwapServer.NewLoopInSwap:input_type -> looprpc.ServerLoopInRequest
19, // 31: looprpc.SwapServer.SubscribeLoopOutUpdates:input_type -> looprpc.SubscribeUpdatesRequest 13, // 31: looprpc.SwapServer.LoopInQuote:input_type -> looprpc.ServerLoopInQuoteRequest
19, // 32: looprpc.SwapServer.SubscribeLoopInUpdates:input_type -> looprpc.SubscribeUpdatesRequest 19, // 32: looprpc.SwapServer.SubscribeLoopOutUpdates:input_type -> looprpc.SubscribeUpdatesRequest
24, // 33: looprpc.SwapServer.CancelLoopOutSwap:input_type -> looprpc.CancelLoopOutSwapRequest 19, // 33: looprpc.SwapServer.SubscribeLoopInUpdates:input_type -> looprpc.SubscribeUpdatesRequest
26, // 34: looprpc.SwapServer.Probe:input_type -> looprpc.ServerProbeRequest 24, // 34: looprpc.SwapServer.CancelLoopOutSwap:input_type -> looprpc.CancelLoopOutSwapRequest
28, // 35: looprpc.SwapServer.RecommendRoutingPlugin:input_type -> looprpc.RecommendRoutingPluginReq 26, // 35: looprpc.SwapServer.Probe:input_type -> looprpc.ServerProbeRequest
30, // 36: looprpc.SwapServer.ReportRoutingResult:input_type -> looprpc.ReportRoutingResultReq 28, // 36: looprpc.SwapServer.RecommendRoutingPlugin:input_type -> looprpc.RecommendRoutingPluginReq
32, // 37: looprpc.SwapServer.MuSig2SignSweep:input_type -> looprpc.MuSig2SignSweepReq 30, // 37: looprpc.SwapServer.ReportRoutingResult:input_type -> looprpc.ReportRoutingResultReq
34, // 38: looprpc.SwapServer.PushKey:input_type -> looprpc.ServerPushKeyReq 32, // 38: looprpc.SwapServer.MuSig2SignSweep:input_type -> looprpc.MuSig2SignSweepReq
36, // 39: looprpc.SwapServer.FetchL402:input_type -> looprpc.FetchL402Request 35, // 39: looprpc.SwapServer.PushKey:input_type -> looprpc.ServerPushKeyReq
10, // 40: looprpc.SwapServer.LoopOutTerms:output_type -> looprpc.ServerLoopOutTerms 37, // 40: looprpc.SwapServer.FetchL402:input_type -> looprpc.FetchL402Request
6, // 41: looprpc.SwapServer.NewLoopOutSwap:output_type -> looprpc.ServerLoopOutResponse 10, // 41: looprpc.SwapServer.LoopOutTerms:output_type -> looprpc.ServerLoopOutTerms
18, // 42: looprpc.SwapServer.LoopOutPushPreimage:output_type -> looprpc.ServerLoopOutPushPreimageResponse 6, // 42: looprpc.SwapServer.NewLoopOutSwap:output_type -> looprpc.ServerLoopOutResponse
8, // 43: looprpc.SwapServer.LoopOutQuote:output_type -> looprpc.ServerLoopOutQuote 18, // 43: looprpc.SwapServer.LoopOutPushPreimage:output_type -> looprpc.ServerLoopOutPushPreimageResponse
16, // 44: looprpc.SwapServer.LoopInTerms:output_type -> looprpc.ServerLoopInTerms 8, // 44: looprpc.SwapServer.LoopOutQuote:output_type -> looprpc.ServerLoopOutQuote
12, // 45: looprpc.SwapServer.NewLoopInSwap:output_type -> looprpc.ServerLoopInResponse 16, // 45: looprpc.SwapServer.LoopInTerms:output_type -> looprpc.ServerLoopInTerms
14, // 46: looprpc.SwapServer.LoopInQuote:output_type -> looprpc.ServerLoopInQuoteResponse 12, // 46: looprpc.SwapServer.NewLoopInSwap:output_type -> looprpc.ServerLoopInResponse
20, // 47: looprpc.SwapServer.SubscribeLoopOutUpdates:output_type -> looprpc.SubscribeLoopOutUpdatesResponse 14, // 47: looprpc.SwapServer.LoopInQuote:output_type -> looprpc.ServerLoopInQuoteResponse
21, // 48: looprpc.SwapServer.SubscribeLoopInUpdates:output_type -> looprpc.SubscribeLoopInUpdatesResponse 20, // 48: looprpc.SwapServer.SubscribeLoopOutUpdates:output_type -> looprpc.SubscribeLoopOutUpdatesResponse
25, // 49: looprpc.SwapServer.CancelLoopOutSwap:output_type -> looprpc.CancelLoopOutSwapResponse 21, // 49: looprpc.SwapServer.SubscribeLoopInUpdates:output_type -> looprpc.SubscribeLoopInUpdatesResponse
27, // 50: looprpc.SwapServer.Probe:output_type -> looprpc.ServerProbeResponse 25, // 50: looprpc.SwapServer.CancelLoopOutSwap:output_type -> looprpc.CancelLoopOutSwapResponse
29, // 51: looprpc.SwapServer.RecommendRoutingPlugin:output_type -> looprpc.RecommendRoutingPluginRes 27, // 51: looprpc.SwapServer.Probe:output_type -> looprpc.ServerProbeResponse
31, // 52: looprpc.SwapServer.ReportRoutingResult:output_type -> looprpc.ReportRoutingResultRes 29, // 52: looprpc.SwapServer.RecommendRoutingPlugin:output_type -> looprpc.RecommendRoutingPluginRes
33, // 53: looprpc.SwapServer.MuSig2SignSweep:output_type -> looprpc.MuSig2SignSweepRes 31, // 53: looprpc.SwapServer.ReportRoutingResult:output_type -> looprpc.ReportRoutingResultRes
35, // 54: looprpc.SwapServer.PushKey:output_type -> looprpc.ServerPushKeyRes 34, // 54: looprpc.SwapServer.MuSig2SignSweep:output_type -> looprpc.MuSig2SignSweepRes
37, // 55: looprpc.SwapServer.FetchL402:output_type -> looprpc.FetchL402Response 36, // 55: looprpc.SwapServer.PushKey:output_type -> looprpc.ServerPushKeyRes
40, // [40:56] is the sub-list for method output_type 38, // 56: looprpc.SwapServer.FetchL402:output_type -> looprpc.FetchL402Response
24, // [24:40] is the sub-list for method input_type 41, // [41:57] is the sub-list for method output_type
24, // [24:24] is the sub-list for extension type_name 25, // [25:41] is the sub-list for method input_type
24, // [24:24] is the sub-list for extension extendee 25, // [25:25] is the sub-list for extension type_name
0, // [0:24] is the sub-list for field type_name 25, // [25:25] is the sub-list for extension extendee
0, // [0:25] is the sub-list for field type_name
} }
func init() { file_server_proto_init() } func init() { file_server_proto_init() }
@ -3607,7 +3704,7 @@ func file_server_proto_init() {
} }
} }
file_server_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { file_server_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*MuSig2SignSweepRes); i { switch v := v.(*PrevoutInfo); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@ -3619,7 +3716,7 @@ func file_server_proto_init() {
} }
} }
file_server_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { file_server_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ServerPushKeyReq); i { switch v := v.(*MuSig2SignSweepRes); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@ -3631,7 +3728,7 @@ func file_server_proto_init() {
} }
} }
file_server_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { file_server_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ServerPushKeyRes); i { switch v := v.(*ServerPushKeyReq); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@ -3643,7 +3740,7 @@ func file_server_proto_init() {
} }
} }
file_server_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { file_server_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*FetchL402Request); i { switch v := v.(*ServerPushKeyRes); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@ -3655,6 +3752,18 @@ func file_server_proto_init() {
} }
} }
file_server_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { file_server_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*FetchL402Request); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_server_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*FetchL402Response); i { switch v := v.(*FetchL402Response); i {
case 0: case 0:
return &v.state return &v.state
@ -3676,7 +3785,7 @@ func file_server_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_server_proto_rawDesc, RawDescriptor: file_server_proto_rawDesc,
NumEnums: 5, NumEnums: 5,
NumMessages: 33, NumMessages: 34,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

@ -1,12 +1,11 @@
syntax = "proto3"; syntax = "proto3";
import "common.proto";
// We can't change this to swapserverrpc, it would be a breaking change because // We can't change this to swapserverrpc, it would be a breaking change because
// the package name is also contained in the HTTP URIs and old clients would // the package name is also contained in the HTTP URIs and old clients would
// call the wrong endpoints. Luckily with the go_package option we can have // call the wrong endpoints. Luckily with the go_package option we can have
// different golang and RPC package names to fix protobuf namespace conflicts. // different golang and RPC package names to fix protobuf namespace conflicts.
package looprpc; package looprpc;
import "common.proto";
option go_package = "github.com/lightninglabs/loop/swapserverrpc"; option go_package = "github.com/lightninglabs/loop/swapserverrpc";
@ -515,7 +514,6 @@ message ServerProbeRequest {
// The protocol version that the client adheres to. // The protocol version that the client adheres to.
ProtocolVersion protocol_version = 1; ProtocolVersion protocol_version = 1;
// The probe amount.
uint64 amt = 2; uint64 amt = 2;
// The target node for the probe. // The target node for the probe.
@ -598,6 +596,23 @@ message MuSig2SignSweepReq {
// The psbt of the sweep txn. // The psbt of the sweep txn.
bytes sweep_tx_psbt = 6; bytes sweep_tx_psbt = 6;
// The prevout information of the sweep txn.
repeated PrevoutInfo prevout_info = 7;
}
message PrevoutInfo {
// The value of the txout.
uint64 value = 1;
// The pk_script of the txout.
bytes pk_script = 2;
// The txid of the txout.
bytes txid_bytes = 3;
// The index of the txout.
uint32 output_index = 4;
} }
message MuSig2SignSweepRes { message MuSig2SignSweepRes {

@ -0,0 +1,30 @@
package sweepbatcher
import (
"fmt"
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the
// caller requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger("SWEEP", nil))
}
// batchPrefixLogger returns a logger that prefixes all log messages with the ID.
func batchPrefixLogger(batchID string) btclog.Logger {
return build.NewPrefixLog(fmt.Sprintf("[Batch %s]", batchID), log)
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

@ -0,0 +1,371 @@
package sweepbatcher
import (
"context"
"database/sql"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lntypes"
)
type BaseDB interface {
// ConfirmBatch confirms a batch by setting the state to confirmed.
ConfirmBatch(ctx context.Context, id int32) error
// GetBatchSweeps fetches all the sweeps that are part a batch.
GetBatchSweeps(ctx context.Context, batchID int32) (
[]sqlc.GetBatchSweepsRow, error)
// GetSweepStatus returns true if the sweep has been completed.
GetSweepStatus(ctx context.Context, swapHash []byte) (bool, error)
// GetSwapUpdates fetches all the updates for a swap.
GetSwapUpdates(ctx context.Context, swapHash []byte) (
[]sqlc.SwapUpdate, error)
// FetchUnconfirmedSweepBatches fetches all the batches from the
// database that are not in a confirmed state.
GetUnconfirmedBatches(ctx context.Context) ([]sqlc.SweepBatch, error)
// InsertBatch inserts a batch into the database, returning the id of
// the inserted batch.
InsertBatch(ctx context.Context, arg sqlc.InsertBatchParams) (
int32, error)
// UpdateBatch updates a batch in the database.
UpdateBatch(ctx context.Context, arg sqlc.UpdateBatchParams) error
// UpsertSweep inserts a sweep into the database, or updates an existing
// sweep if it already exists.
UpsertSweep(ctx context.Context, arg sqlc.UpsertSweepParams) error
// ExecTx allows for executing a function in the context of a database
// transaction.
ExecTx(ctx context.Context, txOptions loopdb.TxOptions,
txBody func(*sqlc.Queries) error) error
}
// SQLStore manages the reservations in the database.
type SQLStore struct {
baseDb BaseDB
network *chaincfg.Params
clock clock.Clock
}
// NewSQLStore creates a new SQLStore.
func NewSQLStore(db BaseDB, network *chaincfg.Params) *SQLStore {
return &SQLStore{
baseDb: db,
network: network,
clock: clock.NewDefaultClock(),
}
}
// FetchUnconfirmedSweepBatches fetches all the batches from the database that
// are not in a confirmed state.
func (s *SQLStore) FetchUnconfirmedSweepBatches(ctx context.Context) ([]*dbBatch,
error) {
var batches []*dbBatch
dbBatches, err := s.baseDb.GetUnconfirmedBatches(ctx)
if err != nil {
return nil, err
}
for _, dbBatch := range dbBatches {
batch := convertBatchRow(dbBatch)
if err != nil {
return nil, err
}
batches = append(batches, batch)
}
return batches, err
}
// InsertSweepBatch inserts a batch into the database, returning the id of the
// inserted batch.
func (s *SQLStore) InsertSweepBatch(ctx context.Context, batch *dbBatch) (int32,
error) {
return s.baseDb.InsertBatch(ctx, batchToInsertArgs(*batch))
}
// UpdateSweepBatch updates a batch in the database.
func (s *SQLStore) UpdateSweepBatch(ctx context.Context, batch *dbBatch) error {
return s.baseDb.UpdateBatch(ctx, batchToUpdateArgs(*batch))
}
// ConfirmBatch confirms a batch by setting the state to confirmed.
func (s *SQLStore) ConfirmBatch(ctx context.Context, id int32) error {
return s.baseDb.ConfirmBatch(ctx, id)
}
// FetchBatchSweeps fetches all the sweeps that are part a batch.
func (s *SQLStore) FetchBatchSweeps(ctx context.Context, id int32) (
[]*dbSweep, error) {
readOpts := loopdb.NewSqlReadOpts()
var sweeps []*dbSweep
err := s.baseDb.ExecTx(ctx, readOpts, func(tx *sqlc.Queries) error {
dbSweeps, err := tx.GetBatchSweeps(ctx, id)
if err != nil {
return err
}
for _, dbSweep := range dbSweeps {
updates, err := s.baseDb.GetSwapUpdates(
ctx, dbSweep.SwapHash,
)
if err != nil {
return err
}
sweep, err := s.convertSweepRow(dbSweep, updates)
if err != nil {
return err
}
sweeps = append(sweeps, &sweep)
}
return nil
})
if err != nil {
return nil, err
}
return sweeps, nil
}
// UpsertSweep inserts a sweep into the database, or updates an existing sweep
// if it already exists.
func (s *SQLStore) UpsertSweep(ctx context.Context, sweep *dbSweep) error {
return s.baseDb.UpsertSweep(ctx, sweepToUpsertArgs(*sweep))
}
// GetSweepStatus returns true if the sweep has been completed.
func (s *SQLStore) GetSweepStatus(ctx context.Context, swapHash lntypes.Hash) (
bool, error) {
return s.baseDb.GetSweepStatus(ctx, swapHash[:])
}
type dbBatch struct {
// ID is the unique identifier of the batch.
ID int32
// State is the current state of the batch.
State string
// BatchTxid is the txid of the batch transaction.
BatchTxid chainhash.Hash
// BatchPkScript is the pkscript of the batch transaction.
BatchPkScript []byte
// LastRbfHeight is the height at which the last RBF attempt was made.
LastRbfHeight int32
// LastRbfSatPerKw is the sat per kw of the last RBF attempt.
LastRbfSatPerKw int32
// MaxTimeoutDistance is the maximum timeout distance of the batch.
MaxTimeoutDistance int32
}
type dbSweep struct {
// ID is the unique identifier of the sweep.
ID int32
// BatchID is the ID of the batch that the sweep belongs to.
BatchID int32
// SwapHash is the hash of the swap that the sweep belongs to.
SwapHash lntypes.Hash
// Outpoint is the outpoint of the sweep.
Outpoint wire.OutPoint
// Amount is the amount of the sweep.
Amount btcutil.Amount
// Completed indicates whether this sweep is completed.
Completed bool
// LoopOut is the loop out that the sweep belongs to.
LoopOut *loopdb.LoopOut
}
// convertBatchRow converts a batch row from db to a sweepbatcher.Batch struct.
func convertBatchRow(row sqlc.SweepBatch) *dbBatch {
batch := dbBatch{
ID: row.ID,
}
if row.Confirmed {
batch.State = batchOpen
}
if row.BatchTxID.Valid {
err := chainhash.Decode(&batch.BatchTxid, row.BatchTxID.String)
if err != nil {
return nil
}
}
batch.BatchPkScript = row.BatchPkScript
if row.LastRbfHeight.Valid {
batch.LastRbfHeight = row.LastRbfHeight.Int32
}
if row.LastRbfSatPerKw.Valid {
batch.LastRbfSatPerKw = row.LastRbfSatPerKw.Int32
}
batch.MaxTimeoutDistance = row.MaxTimeoutDistance
return &batch
}
// BatchToUpsertArgs converts a Batch struct to the arguments needed to insert
// it into the database.
func batchToInsertArgs(batch dbBatch) sqlc.InsertBatchParams {
args := sqlc.InsertBatchParams{
Confirmed: false,
BatchTxID: sql.NullString{
Valid: true,
String: batch.BatchTxid.String(),
},
BatchPkScript: batch.BatchPkScript,
LastRbfHeight: sql.NullInt32{
Valid: true,
Int32: batch.LastRbfHeight,
},
LastRbfSatPerKw: sql.NullInt32{
Valid: true,
Int32: batch.LastRbfSatPerKw,
},
MaxTimeoutDistance: batch.MaxTimeoutDistance,
}
if batch.State == batchConfirmed {
args.Confirmed = true
}
return args
}
// BatchToUpsertArgs converts a Batch struct to the arguments needed to insert
// it into the database.
func batchToUpdateArgs(batch dbBatch) sqlc.UpdateBatchParams {
args := sqlc.UpdateBatchParams{
ID: batch.ID,
Confirmed: false,
BatchTxID: sql.NullString{
Valid: true,
String: batch.BatchTxid.String(),
},
BatchPkScript: batch.BatchPkScript,
LastRbfHeight: sql.NullInt32{
Valid: true,
Int32: batch.LastRbfHeight,
},
LastRbfSatPerKw: sql.NullInt32{
Valid: true,
Int32: batch.LastRbfSatPerKw,
},
}
if batch.State == batchConfirmed {
args.Confirmed = true
}
return args
}
// convertSweepRow converts a sweep row from db to a sweep struct.
func (s *SQLStore) convertSweepRow(row sqlc.GetBatchSweepsRow,
updates []sqlc.SwapUpdate) (dbSweep, error) {
sweep := dbSweep{
ID: row.ID,
BatchID: row.BatchID,
Amount: btcutil.Amount(row.Amt),
}
swapHash, err := lntypes.MakeHash(row.SwapHash)
if err != nil {
return sweep, err
}
sweep.SwapHash = swapHash
hash, err := chainhash.NewHash(row.OutpointTxid)
if err != nil {
return sweep, err
}
sweep.Outpoint = wire.OutPoint{
Hash: *hash,
Index: uint32(row.OutpointIndex),
}
sweep.LoopOut, err = loopdb.ConvertLoopOutRow(
s.network,
sqlc.GetLoopOutSwapRow{
ID: row.ID,
SwapHash: row.SwapHash,
Preimage: row.Preimage,
InitiationTime: row.InitiationTime,
AmountRequested: row.AmountRequested,
CltvExpiry: row.CltvExpiry,
MaxMinerFee: row.MaxMinerFee,
MaxSwapFee: row.MaxSwapFee,
InitiationHeight: row.InitiationHeight,
ProtocolVersion: row.ProtocolVersion,
Label: row.Label,
DestAddress: row.DestAddress,
SwapInvoice: row.SwapInvoice,
MaxSwapRoutingFee: row.MaxSwapRoutingFee,
SweepConfTarget: row.SweepConfTarget,
HtlcConfirmations: row.HtlcConfirmations,
OutgoingChanSet: row.OutgoingChanSet,
PrepayInvoice: row.PrepayInvoice,
MaxPrepayRoutingFee: row.MaxPrepayRoutingFee,
PublicationDeadline: row.PublicationDeadline,
SingleSweep: row.SingleSweep,
SenderScriptPubkey: row.SenderScriptPubkey,
ReceiverScriptPubkey: row.ReceiverScriptPubkey,
SenderInternalPubkey: row.SenderInternalPubkey,
ReceiverInternalPubkey: row.ReceiverInternalPubkey,
ClientKeyFamily: row.ClientKeyFamily,
ClientKeyIndex: row.ClientKeyIndex,
}, updates,
)
return sweep, err
}
// sweepToUpsertArgs converts a Sweep struct to the arguments needed to insert.
func sweepToUpsertArgs(sweep dbSweep) sqlc.UpsertSweepParams {
return sqlc.UpsertSweepParams{
SwapHash: sweep.SwapHash[:],
BatchID: sweep.BatchID,
OutpointTxid: sweep.Outpoint.Hash[:],
OutpointIndex: int32(sweep.Outpoint.Index),
Amt: int64(sweep.Amount),
Completed: sweep.Completed,
}
}

@ -0,0 +1,125 @@
package sweepbatcher
import (
"context"
"errors"
"sort"
"github.com/lightningnetwork/lnd/lntypes"
)
// StoreMock implements a mock client swap store.
type StoreMock struct {
batches map[int32]dbBatch
sweeps map[lntypes.Hash]dbSweep
}
// NewStoreMock instantiates a new mock store.
func NewStoreMock() *StoreMock {
return &StoreMock{
batches: make(map[int32]dbBatch),
sweeps: make(map[lntypes.Hash]dbSweep),
}
}
// FetchUnconfirmedBatches fetches all the loop out sweep batches from the
// database that are not in a confirmed state.
func (s *StoreMock) FetchUnconfirmedSweepBatches(ctx context.Context) (
[]*dbBatch, error) {
result := []*dbBatch{}
for _, batch := range s.batches {
batch := batch
if batch.State != "confirmed" {
result = append(result, &batch)
}
}
return result, nil
}
// InsertSweepBatch inserts a batch into the database, returning the id of the
// inserted batch.
func (s *StoreMock) InsertSweepBatch(ctx context.Context,
batch *dbBatch) (int32, error) {
var id int32
if len(s.batches) == 0 {
id = 0
} else {
id = int32(len(s.batches))
}
s.batches[id] = *batch
return id, nil
}
// UpdateSweepBatch updates a batch in the database.
func (s *StoreMock) UpdateSweepBatch(ctx context.Context,
batch *dbBatch) error {
s.batches[batch.ID] = *batch
return nil
}
// ConfirmBatch confirms a batch.
func (s *StoreMock) ConfirmBatch(ctx context.Context, id int32) error {
batch, ok := s.batches[id]
if !ok {
return errors.New("batch not found")
}
batch.State = "confirmed"
s.batches[batch.ID] = batch
return nil
}
// FetchBatchSweeps fetches all the sweeps that belong to a batch.
func (s *StoreMock) FetchBatchSweeps(ctx context.Context,
id int32) ([]*dbSweep, error) {
result := []*dbSweep{}
for _, sweep := range s.sweeps {
sweep := sweep
if sweep.BatchID == id {
result = append(result, &sweep)
}
}
sort.Slice(result, func(i, j int) bool {
return result[i].ID < result[j].ID
})
return result, nil
}
// UpsertSweep inserts a sweep into the database, or updates an existing sweep.
func (s *StoreMock) UpsertSweep(ctx context.Context, sweep *dbSweep) error {
s.sweeps[sweep.SwapHash] = *sweep
return nil
}
// GetSweepStatus returns the status of a sweep.
func (s *StoreMock) GetSweepStatus(ctx context.Context,
swapHash lntypes.Hash) (bool, error) {
sweep, ok := s.sweeps[swapHash]
if !ok {
return false, nil
}
return sweep.Completed, nil
}
// Close closes the store.
func (s *StoreMock) Close() error {
return nil
}
// AssertSweepStored asserts that a sweep is stored.
func (s *StoreMock) AssertSweepStored(id lntypes.Hash) bool {
_, ok := s.sweeps[id]
return ok
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,657 @@
package sweepbatcher
import (
"context"
"fmt"
"sync"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
const (
// defaultMaxTimeoutDistance is the default maximum timeout distance
// of sweeps that can appear in the same batch.
defaultMaxTimeoutDistance = 288
// batchOpen is the string representation of the state of a batch that
// is open.
batchOpen = "open"
// batchClosed is the string representation of the state of a batch
// that is closed.
batchClosed = "closed"
// batchConfirmed is the string representation of the state of a batch
// that is confirmed.
batchConfirmed = "confirmed"
// defaultMainnetPublishDelay is the default publish delay that is used
// for mainnet.
defaultMainnetPublishDelay = 5 * time.Second
// defaultTestnetPublishDelay is the default publish delay that is used
// for testnet.
defaultPublishDelay = 500 * time.Millisecond
)
type BatcherStore interface {
// FetchUnconfirmedSweepBatches fetches all the batches from the
// database that are not in a confirmed state.
FetchUnconfirmedSweepBatches(ctx context.Context) ([]*dbBatch,
error)
// InsertSweepBatch inserts a batch into the database, returning the id
// of the inserted batch.
InsertSweepBatch(ctx context.Context,
batch *dbBatch) (int32, error)
// UpdateSweepBatch updates a batch in the database.
UpdateSweepBatch(ctx context.Context,
batch *dbBatch) error
// ConfirmBatch confirms a batch by setting its state to confirmed.
ConfirmBatch(ctx context.Context, id int32) error
// FetchBatchSweeps fetches all the sweeps that belong to a batch.
FetchBatchSweeps(ctx context.Context,
id int32) ([]*dbSweep, error)
// UpsertSweep inserts a sweep into the database, or updates an existing
// sweep if it already exists.
UpsertSweep(ctx context.Context, sweep *dbSweep) error
// GetSweepStatus returns the completed status of the sweep.
GetSweepStatus(ctx context.Context, swapHash lntypes.Hash) (
bool, error)
}
// MuSig2SignSweep is a function that can be used to sign a sweep transaction
// cooperatively with the swap server.
type MuSig2SignSweep func(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)
// VerifySchnorrSig is a function that can be used to verify a schnorr
// signature.
type VerifySchnorrSig func(pubKey *btcec.PublicKey, hash, sig []byte) error
// SweepRequest is a request to sweep a specific outpoint.
type SweepRequest struct {
// SwapHash is the hash of the swap that is being swept.
SwapHash lntypes.Hash
// Outpoint is the outpoint that is being swept.
Outpoint wire.OutPoint
// Value is the value of the outpoint that is being swept.
Value btcutil.Amount
// Notifier is a notifier that is used to notify the requester of this
// sweep that the sweep was successful.
Notifier *SpendNotifier
}
// SpendNotifier is a notifier that is used to notify the requester of a sweep
// that the sweep was successful.
type SpendNotifier struct {
// SpendChan is a channel where the spend details are received.
SpendChan chan *wire.MsgTx
// SpendErrChan is a channel where spend errors are received.
SpendErrChan chan error
// QuitChan is a channel that can be closed to stop the notifier.
QuitChan chan bool
}
var (
ErrBatcherShuttingDown = fmt.Errorf("batcher shutting down")
)
// Batcher is a system that is responsible for accepting sweep requests and
// placing them in appropriate batches. It will spin up new batches as needed.
type Batcher struct {
// batches is a map of batch IDs to the currently active batches.
batches map[int32]*batch
// sweepReqs is a channel where sweep requests are received.
sweepReqs chan SweepRequest
// errChan is a channel where errors are received.
errChan chan error
// quit signals that the batch must stop.
quit chan struct{}
// wallet is the wallet kit client that is used by batches.
wallet lndclient.WalletKitClient
// chainNotifier is the chain notifier client that is used by batches.
chainNotifier lndclient.ChainNotifierClient
// signerClient is the signer client that is used by batches.
signerClient lndclient.SignerClient
// musig2ServerKit includes all the required functionality to collect
// and verify signatures by the swap server in order to cooperatively
// sweep funds.
musig2ServerSign MuSig2SignSweep
// verifySchnorrSig is a function that can be used to verify a schnorr
// signature.
VerifySchnorrSig VerifySchnorrSig
// chainParams are the chain parameters of the chain that is used by
// batches.
chainParams *chaincfg.Params
// store includes all the database interactions that are needed by the
// batcher and the batches.
store BatcherStore
// swapStore includes all the database interactions that are needed for
// interacting with swaps.
swapStore loopdb.SwapStore
// wg is a waitgroup that is used to wait for all the goroutines to
// exit.
wg sync.WaitGroup
}
// NewBatcher creates a new Batcher instance.
func NewBatcher(wallet lndclient.WalletKitClient,
chainNotifier lndclient.ChainNotifierClient,
signerClient lndclient.SignerClient, musig2ServerSigner MuSig2SignSweep,
verifySchnorrSig VerifySchnorrSig, chainparams *chaincfg.Params,
store BatcherStore, swapStore loopdb.SwapStore) *Batcher {
return &Batcher{
batches: make(map[int32]*batch),
sweepReqs: make(chan SweepRequest),
errChan: make(chan error, 1),
quit: make(chan struct{}),
wallet: wallet,
chainNotifier: chainNotifier,
signerClient: signerClient,
musig2ServerSign: musig2ServerSigner,
VerifySchnorrSig: verifySchnorrSig,
chainParams: chainparams,
store: store,
swapStore: swapStore,
}
}
// Run starts the batcher and processes incoming sweep requests.
func (b *Batcher) Run(ctx context.Context) error {
runCtx, cancel := context.WithCancel(ctx)
defer func() {
cancel()
for _, batch := range b.batches {
batch.Wait()
}
b.wg.Wait()
}()
// First we fetch all the batches that are not in a confirmed state from
// the database. We will then resume the execution of these batches.
batches, err := b.FetchUnconfirmedBatches(runCtx)
if err != nil {
return err
}
for _, batch := range batches {
err := b.spinUpBatchFromDB(runCtx, batch)
if err != nil {
return err
}
}
for {
select {
case sweepReq := <-b.sweepReqs:
sweep, err := b.fetchSweep(runCtx, sweepReq)
if err != nil {
return err
}
err = b.handleSweep(runCtx, sweep, sweepReq.Notifier)
if err != nil {
return err
}
case err := <-b.errChan:
return err
case <-runCtx.Done():
return runCtx.Err()
}
}
}
// AddSweep adds a sweep request to the batcher for handling. This will either
// place the sweep in an existing batch or create a new one.
func (b *Batcher) AddSweep(sweepReq *SweepRequest) error {
select {
case b.sweepReqs <- *sweepReq:
return nil
case <-b.quit:
return ErrBatcherShuttingDown
}
}
// handleSweep handles a sweep request by either placing it in an existing
// batch, or by spinning up a new batch for it.
func (b *Batcher) handleSweep(ctx context.Context, sweep *sweep,
notifier *SpendNotifier) error {
completed, err := b.store.GetSweepStatus(ctx, sweep.swapHash)
if err != nil {
return err
}
log.Infof("Batcher handling sweep %x, completed=%v", sweep.swapHash[:6],
completed)
// If the sweep has already been completed in a confirmed batch then we
// can't attach its notifier to the batch as that is no longer running.
// Instead we directly detect and return the spend here.
if completed && *notifier != (SpendNotifier{}) {
go b.monitorSpendAndNotify(ctx, sweep, notifier)
return nil
}
sweep.notifier = notifier
// Check if the sweep is already in a batch. If that is the case, we
// provide the sweep to that batch and return.
for _, batch := range b.batches {
// This is a check to see if a batch is completed. In that case
// we just lazily delete it and continue our scan.
if batch.isComplete() {
delete(b.batches, batch.id)
continue
}
if batch.sweepExists(sweep.swapHash) {
accepted, err := batch.addSweep(ctx, sweep)
if err != nil {
return err
}
if !accepted {
return fmt.Errorf("existing sweep %x was not "+
"accepted by batch %d", sweep.swapHash[:6],
batch.id)
}
}
}
// If one of the batches accepts the sweep, we provide it to that batch.
for _, batch := range b.batches {
accepted, err := batch.addSweep(ctx, sweep)
if err != nil && err != ErrBatchShuttingDown {
return err
}
// If the sweep was accepted by this batch, we return, our job
// is done.
if accepted {
return nil
}
}
// If no batch is capable of accepting the sweep, we spin up a fresh
// batch and hand the sweep over to it.
batch, err := b.spinUpBatch(ctx)
if err != nil {
return err
}
// Add the sweep to the fresh batch.
accepted, err := batch.addSweep(ctx, sweep)
if err != nil {
return err
}
// If the sweep wasn't accepted by the fresh batch something is wrong,
// we should return the error.
if !accepted {
return fmt.Errorf("sweep %x was not accepted by new batch %d",
sweep.swapHash[:6], batch.id)
}
return nil
}
// spinUpBatch spins up a new batch and returns it.
func (b *Batcher) spinUpBatch(ctx context.Context) (*batch, error) {
cfg := batchConfig{
maxTimeoutDistance: defaultMaxTimeoutDistance,
batchConfTarget: defaultBatchConfTarget,
}
switch b.chainParams {
case &chaincfg.MainNetParams:
cfg.batchPublishDelay = defaultMainnetPublishDelay
default:
cfg.batchPublishDelay = defaultPublishDelay
}
batchKit := batchKit{
returnChan: b.sweepReqs,
wallet: b.wallet,
chainNotifier: b.chainNotifier,
signerClient: b.signerClient,
musig2SignSweep: b.musig2ServerSign,
verifySchnorrSig: b.VerifySchnorrSig,
purger: b.AddSweep,
store: b.store,
}
batch := NewBatch(cfg, batchKit)
id, err := batch.insertAndAcquireID(ctx)
if err != nil {
return nil, err
}
// We add the batch to our map of batches and start it.
b.batches[id] = batch
b.wg.Add(1)
go func() {
defer b.wg.Done()
err := batch.Run(ctx)
if err != nil {
_ = b.writeToErrChan(ctx, err)
}
}()
return batch, nil
}
// spinUpBatchDB spins up a batch that already existed in storage, then
// returns it.
func (b *Batcher) spinUpBatchFromDB(ctx context.Context, batch *batch) error {
cfg := batchConfig{
maxTimeoutDistance: batch.cfg.maxTimeoutDistance,
batchConfTarget: defaultBatchConfTarget,
}
rbfCache := rbfCache{
LastHeight: batch.rbfCache.LastHeight,
FeeRate: batch.rbfCache.FeeRate,
}
dbSweeps, err := b.store.FetchBatchSweeps(ctx, batch.id)
if err != nil {
return err
}
if len(dbSweeps) == 0 {
return fmt.Errorf("batch %d has no sweeps", batch.id)
}
primarySweep := dbSweeps[0]
sweeps := make(map[lntypes.Hash]sweep)
for _, dbSweep := range dbSweeps {
sweep, err := b.convertSweep(dbSweep)
if err != nil {
return err
}
sweeps[sweep.swapHash] = *sweep
}
batchKit := batchKit{
id: batch.id,
batchTxid: batch.batchTxid,
batchPkScript: batch.batchPkScript,
state: batch.state,
primaryID: primarySweep.SwapHash,
sweeps: sweeps,
rbfCache: rbfCache,
returnChan: b.sweepReqs,
wallet: b.wallet,
chainNotifier: b.chainNotifier,
signerClient: b.signerClient,
musig2SignSweep: b.musig2ServerSign,
verifySchnorrSig: b.VerifySchnorrSig,
purger: b.AddSweep,
store: b.store,
log: batchPrefixLogger(fmt.Sprintf("%d", batch.id)),
}
newBatch := NewBatchFromDB(cfg, batchKit)
// We add the batch to our map of batches and start it.
b.batches[batch.id] = newBatch
b.wg.Add(1)
go func() {
defer b.wg.Done()
err := newBatch.Run(ctx)
if err != nil {
_ = b.writeToErrChan(ctx, err)
}
}()
return nil
}
// FetchUnconfirmedBatches fetches all the batches from the database that are
// not in a confirmed state.
func (b *Batcher) FetchUnconfirmedBatches(ctx context.Context) ([]*batch,
error) {
dbBatches, err := b.store.FetchUnconfirmedSweepBatches(ctx)
if err != nil {
return nil, err
}
batches := make([]*batch, 0, len(dbBatches))
for _, bch := range dbBatches {
bch := bch
batch := batch{}
batch.id = bch.ID
switch bch.State {
case batchOpen:
batch.state = Open
case batchClosed:
batch.state = Closed
case batchConfirmed:
batch.state = Confirmed
}
batch.batchTxid = &bch.BatchTxid
batch.batchPkScript = bch.BatchPkScript
rbfCache := rbfCache{
LastHeight: bch.LastRbfHeight,
FeeRate: chainfee.SatPerKWeight(bch.LastRbfSatPerKw),
}
batch.rbfCache = rbfCache
bchCfg := batchConfig{
maxTimeoutDistance: bch.MaxTimeoutDistance,
}
batch.cfg = &bchCfg
batches = append(batches, &batch)
}
return batches, nil
}
// monitorSpendAndNotify monitors the spend of a specific outpoint and writes
// the response back to the response channel.
func (b *Batcher) monitorSpendAndNotify(ctx context.Context, sweep *sweep,
notifier *SpendNotifier) {
b.wg.Add(1)
defer b.wg.Done()
spendCtx, cancel := context.WithCancel(ctx)
defer cancel()
spendChan, spendErr, err := b.chainNotifier.RegisterSpendNtfn(
spendCtx, &sweep.outpoint, sweep.htlc.PkScript,
sweep.initiationHeight,
)
if err != nil {
select {
case notifier.SpendErrChan <- err:
case <-ctx.Done():
}
_ = b.writeToErrChan(ctx, err)
return
}
log.Infof("Batcher monitoring spend for swap %x", sweep.swapHash[:6])
for {
select {
case spend := <-spendChan:
select {
case notifier.SpendChan <- spend.SpendingTx:
case <-ctx.Done():
}
return
case err := <-spendErr:
select {
case notifier.SpendErrChan <- err:
case <-ctx.Done():
}
_ = b.writeToErrChan(ctx, err)
return
case <-notifier.QuitChan:
return
case <-ctx.Done():
return
}
}
}
func (b *Batcher) writeToErrChan(ctx context.Context, err error) error {
select {
case b.errChan <- err:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// convertSweep converts a fetched sweep from the database to a sweep that is
// ready to be processed by the batcher.
func (b *Batcher) convertSweep(dbSweep *dbSweep) (*sweep, error) {
swap := dbSweep.LoopOut
htlc, err := utils.GetHtlc(
dbSweep.SwapHash, &swap.Contract.SwapContract, b.chainParams,
)
if err != nil {
return nil, err
}
swapPaymentAddr, err := utils.ObtainSwapPaymentAddr(
swap.Contract.SwapInvoice, b.chainParams,
)
if err != nil {
return nil, err
}
return &sweep{
swapHash: swap.Hash,
outpoint: dbSweep.Outpoint,
value: dbSweep.Amount,
confTarget: swap.Contract.SweepConfTarget,
timeout: swap.Contract.CltvExpiry,
initiationHeight: swap.Contract.InitiationHeight,
htlc: *htlc,
preimage: swap.Contract.Preimage,
swapInvoicePaymentAddr: *swapPaymentAddr,
htlcKeys: swap.Contract.HtlcKeys,
htlcSuccessEstimator: htlc.AddSuccessToEstimator,
protocolVersion: swap.Contract.ProtocolVersion,
isExternalAddr: swap.Contract.IsExternalAddr,
destAddr: swap.Contract.DestAddr,
}, nil
}
// fetchSweep fetches the sweep related information from the database.
func (b *Batcher) fetchSweep(ctx context.Context,
sweepReq SweepRequest) (*sweep, error) {
swapHash, err := lntypes.MakeHash(sweepReq.SwapHash[:])
if err != nil {
return nil, fmt.Errorf("failed to parse swapHash: %v", err)
}
swap, err := b.swapStore.FetchLoopOutSwap(ctx, swapHash)
if err != nil {
return nil, fmt.Errorf("failed to fetch loop out for %x: %v",
swapHash[:6], err)
}
htlc, err := utils.GetHtlc(
swapHash, &swap.Contract.SwapContract, b.chainParams,
)
if err != nil {
return nil, fmt.Errorf("failed to get htlc: %v", err)
}
swapPaymentAddr, err := utils.ObtainSwapPaymentAddr(
swap.Contract.SwapInvoice, b.chainParams,
)
if err != nil {
return nil, fmt.Errorf("failed to get payment addr: %v", err)
}
return &sweep{
swapHash: swap.Hash,
outpoint: sweepReq.Outpoint,
value: sweepReq.Value,
confTarget: swap.Contract.SweepConfTarget,
timeout: swap.Contract.CltvExpiry,
initiationHeight: swap.Contract.InitiationHeight,
htlc: *htlc,
preimage: swap.Contract.Preimage,
swapInvoicePaymentAddr: *swapPaymentAddr,
htlcKeys: swap.Contract.HtlcKeys,
htlcSuccessEstimator: htlc.AddSuccessToEstimator,
protocolVersion: swap.Contract.ProtocolVersion,
isExternalAddr: swap.Contract.IsExternalAddr,
destAddr: swap.Contract.DestAddr,
}, nil
}

@ -0,0 +1,986 @@
package sweepbatcher
import (
"context"
"strings"
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)
const (
swapInvoice = "lntb1230n1pjjszzgpp5j76f03wrkya4sm4gxv6az5nmz5aqsvmn4" +
"tpguu2sdvdyygedqjgqdq9xyerxcqzzsxqr23ssp5rwzmwtfjmsgranfk8sr" +
"4p4gcgmvyd42uug8pxteg2mkk23ndvkqs9qyyssq44ruk3ex59cmv4dm6k4v" +
"0kc6c0gcqjs0gkljfyd6c6uatqa2f67xlx3pcg5tnvcae5p3jju8ra77e87d" +
"vhhs0jrx53wnc0fq9rkrhmqqelyx7l"
eventuallyCheckFrequency = 100 * time.Millisecond
ntfnBufferSize = 1024
)
func testMuSig2SignSweep(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) {
return nil, nil, nil
}
var dummyNotifier = SpendNotifier{
SpendChan: make(chan *wire.MsgTx, ntfnBufferSize),
SpendErrChan: make(chan error, ntfnBufferSize),
QuitChan: make(chan bool, ntfnBufferSize),
}
// TestSweepBatcherBatchCreation tests that sweep requests enter the expected
// batch based on their timeout distance.
func TestSweepBatcherBatchCreation(t *testing.T) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
store := loopdb.NewStoreMock(t)
batcherStore := NewStoreMock()
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, nil, lnd.ChainParams, batcherStore, store)
go func() {
err := batcher.Run(ctx)
if !strings.Contains(err.Error(), "context canceled") {
require.NoError(t, err)
}
}()
// Create a sweep request.
sweepReq1 := SweepRequest{
SwapHash: lntypes.Hash{1, 1, 1},
Value: 111,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
},
Notifier: &dummyNotifier,
}
swap1 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111,
AmountRequested: 111,
},
SwapInvoice: swapInvoice,
}
err := store.CreateLoopOut(ctx, sweepReq1.SwapHash, swap1)
require.NoError(t, err)
store.AssertLoopOutStored()
// Deliver sweep request to batcher.
batcher.sweepReqs <- sweepReq1
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Insert the same swap twice, this should be a noop.
batcher.sweepReqs <- sweepReq1
// Once batcher receives sweep request it will eventually spin up a
// batch.
require.Eventually(t, func() bool {
return len(batcher.batches) == 1
}, test.Timeout, eventuallyCheckFrequency)
// Create a second sweep request that has a timeout distance less than
// our configured threshold.
sweepReq2 := SweepRequest{
SwapHash: lntypes.Hash{2, 2, 2},
Value: 222,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
},
Notifier: &dummyNotifier,
}
swap2 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111 + defaultMaxTimeoutDistance - 1,
AmountRequested: 222,
},
SwapInvoice: swapInvoice,
}
err = store.CreateLoopOut(ctx, sweepReq2.SwapHash, swap2)
require.NoError(t, err)
store.AssertLoopOutStored()
batcher.sweepReqs <- sweepReq2
// Batcher should not create a second batch as timeout distance is small
// enough.
require.Eventually(t, func() bool {
return len(batcher.batches) == 1
}, test.Timeout, eventuallyCheckFrequency)
// Create a third sweep request that has more timeout distance than
// the default.
sweepReq3 := SweepRequest{
SwapHash: lntypes.Hash{3, 3, 3},
Value: 333,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{3, 3},
Index: 3,
},
Notifier: &dummyNotifier,
}
swap3 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111 + defaultMaxTimeoutDistance + 1,
AmountRequested: 333,
},
SwapInvoice: swapInvoice,
}
err = store.CreateLoopOut(ctx, sweepReq3.SwapHash, swap3)
require.NoError(t, err)
store.AssertLoopOutStored()
batcher.sweepReqs <- sweepReq3
// Batcher should create a second batch as timeout distance is greater
// than the threshold
require.Eventually(t, func() bool {
return len(batcher.batches) == 2
}, test.Timeout, eventuallyCheckFrequency)
// Since the second batch got created we check that it registered its
// primary sweep's spend.
<-lnd.RegisterSpendChannel
require.Eventually(t, func() bool {
// Verify that each batch has the correct number of sweeps in it.
for _, batch := range batcher.batches {
switch batch.primarySweepID {
case sweepReq1.SwapHash:
if len(batch.sweeps) != 2 {
return false
}
case sweepReq3.SwapHash:
if len(batch.sweeps) != 1 {
return false
}
}
}
return true
}, test.Timeout, eventuallyCheckFrequency)
// Check that all sweeps were stored.
require.True(t, batcherStore.AssertSweepStored(sweepReq1.SwapHash))
require.True(t, batcherStore.AssertSweepStored(sweepReq2.SwapHash))
require.True(t, batcherStore.AssertSweepStored(sweepReq3.SwapHash))
}
// TestSweepBatcherSimpleLifecycle tests the simple lifecycle of the batches
// that are created and run by the batcher.
func TestSweepBatcherSimpleLifecycle(t *testing.T) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
store := loopdb.NewStoreMock(t)
batcherStore := NewStoreMock()
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, nil, lnd.ChainParams, batcherStore, store)
go func() {
err := batcher.Run(ctx)
if !strings.Contains(err.Error(), "context canceled") {
require.NoError(t, err)
}
}()
// Create a sweep request.
sweepReq1 := SweepRequest{
SwapHash: lntypes.Hash{1, 1, 1},
Value: 111,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
},
Notifier: &dummyNotifier,
}
swap1 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111,
AmountRequested: 111,
},
SwapInvoice: swapInvoice,
SweepConfTarget: 111,
}
err := store.CreateLoopOut(ctx, sweepReq1.SwapHash, swap1)
require.NoError(t, err)
store.AssertLoopOutStored()
// Deliver sweep request to batcher.
batcher.sweepReqs <- sweepReq1
// Eventually request will be consumed and a new batch will spin up.
require.Eventually(t, func() bool {
return len(batcher.batches) == 1
}, test.Timeout, eventuallyCheckFrequency)
// When batch is successfully created it will execute it's first step,
// which leads to a spend monitor of the primary sweep.
<-lnd.RegisterSpendChannel
// Find the batch and assign it to a local variable for easier access.
batch := &batch{}
for _, btch := range batcher.batches {
if btch.primarySweepID == sweepReq1.SwapHash {
batch = btch
}
}
require.Eventually(t, func() bool {
// Batch should have the sweep stored.
return len(batch.sweeps) == 1
}, test.Timeout, eventuallyCheckFrequency)
// The primary sweep id should be that of the first inserted sweep.
require.Equal(t, batch.primarySweepID, sweepReq1.SwapHash)
err = lnd.NotifyHeight(601)
require.NoError(t, err)
// After receiving a height notification the batch will step again,
// leading to a new spend monitoring.
require.Eventually(t, func() bool {
return batch.currentHeight == 601
}, test.Timeout, eventuallyCheckFrequency)
// Create the spending tx that will trigger the spend monitor of the
// batch.
spendingTx := &wire.MsgTx{
Version: 1,
// Since the spend monitor is registered on the primary sweep's
// outpoint we insert that outpoint here.
TxIn: []*wire.TxIn{
{
PreviousOutPoint: sweepReq1.Outpoint,
},
},
TxOut: []*wire.TxOut{
{
PkScript: []byte{3, 2, 1},
},
},
}
spendingTxHash := spendingTx.TxHash()
// Mock the spend notification that spends the swap.
spendDetail := &chainntnfs.SpendDetail{
SpentOutPoint: &sweepReq1.Outpoint,
SpendingTx: spendingTx,
SpenderTxHash: &spendingTxHash,
SpenderInputIndex: 0,
SpendingHeight: 601,
}
// We notify the spend.
lnd.SpendChannel <- spendDetail
// After receiving the spend, the batch is now monitoring for confs.
<-lnd.RegisterConfChannel
// The batch should eventually read the spend notification and progress
// its state to closed.
require.Eventually(t, func() bool {
return batch.state == Closed
}, test.Timeout, eventuallyCheckFrequency)
err = lnd.NotifyHeight(604)
require.NoError(t, err)
// We mock the tx confirmation notification.
lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: spendingTx,
}
// Eventually the batch receives the confirmation notification and
// confirms itself.
require.Eventually(t, func() bool {
return batch.isComplete()
}, test.Timeout, eventuallyCheckFrequency)
}
// TestSweepBatcherSweepReentry tests that when an old version of the batch tx
// gets confirmed the sweep leftovers are sent back to the batcher.
func TestSweepBatcherSweepReentry(t *testing.T) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
store := loopdb.NewStoreMock(t)
batcherStore := NewStoreMock()
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, nil, lnd.ChainParams, batcherStore, store)
go func() {
err := batcher.Run(ctx)
if !strings.Contains(err.Error(), "context canceled") {
require.NoError(t, err)
}
}()
// Create some sweep requests with timeouts not too far away, in order
// to enter the same batch.
sweepReq1 := SweepRequest{
SwapHash: lntypes.Hash{1, 1, 1},
Value: 111,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
},
Notifier: &dummyNotifier,
}
swap1 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111,
AmountRequested: 111,
},
SwapInvoice: swapInvoice,
SweepConfTarget: 111,
}
err := store.CreateLoopOut(ctx, sweepReq1.SwapHash, swap1)
require.NoError(t, err)
store.AssertLoopOutStored()
sweepReq2 := SweepRequest{
SwapHash: lntypes.Hash{2, 2, 2},
Value: 222,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
},
Notifier: &dummyNotifier,
}
swap2 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111,
AmountRequested: 222,
},
SwapInvoice: swapInvoice,
SweepConfTarget: 111,
}
err = store.CreateLoopOut(ctx, sweepReq2.SwapHash, swap2)
require.NoError(t, err)
store.AssertLoopOutStored()
sweepReq3 := SweepRequest{
SwapHash: lntypes.Hash{3, 3, 3},
Value: 333,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{3, 3},
Index: 3,
},
Notifier: &dummyNotifier,
}
swap3 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111,
AmountRequested: 333,
},
SwapInvoice: swapInvoice,
SweepConfTarget: 111,
}
err = store.CreateLoopOut(ctx, sweepReq3.SwapHash, swap3)
require.NoError(t, err)
store.AssertLoopOutStored()
// Feed the sweeps to the batcher.
batcher.sweepReqs <- sweepReq1
// After inserting the primary (first) sweep, a spend monitor should be
// registered.
<-lnd.RegisterSpendChannel
batcher.sweepReqs <- sweepReq2
batcher.sweepReqs <- sweepReq3
// Batcher should create a batch for the sweeps.
require.Eventually(t, func() bool {
return len(batcher.batches) == 1
}, test.Timeout, eventuallyCheckFrequency)
// Find the batch and store it in a local variable for easier access.
b := &batch{}
for _, btch := range batcher.batches {
if btch.primarySweepID == sweepReq1.SwapHash {
b = btch
}
}
// Batcher should contain all sweeps.
require.Eventually(t, func() bool {
return len(b.sweeps) == 3
}, test.Timeout, eventuallyCheckFrequency)
// Verify that the batch has a primary sweep id that matches the first
// inserted sweep, sweep1.
require.Equal(t, b.primarySweepID, sweepReq1.SwapHash)
// Create the spending tx. In order to simulate an older version of the
// batch transaction being confirmed, we only insert the primary sweep's
// outpoint as a TxIn. This means that the other two sweeps did not
// appear in the spending transaction. (This simulates a possible
// scenario caused by RBF replacements.)
spendingTx := &wire.MsgTx{
Version: 1,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: sweepReq1.Outpoint,
},
},
TxOut: []*wire.TxOut{
{
Value: int64(sweepReq1.Value.ToUnit(btcutil.AmountSatoshi)),
PkScript: []byte{3, 2, 1},
},
},
}
spendingTxHash := spendingTx.TxHash()
spendDetail := &chainntnfs.SpendDetail{
SpentOutPoint: &sweepReq1.Outpoint,
SpendingTx: spendingTx,
SpenderTxHash: &spendingTxHash,
SpenderInputIndex: 0,
SpendingHeight: 601,
}
// Send the spending notification to the mock channel.
lnd.SpendChannel <- spendDetail
// After receiving the spend notification the batch should progress to
// the next step, which is monitoring for confirmations.
<-lnd.RegisterConfChannel
// Eventually the batch reads the notification and proceeds to a closed
// state.
require.Eventually(t, func() bool {
return b.state == Closed
}, test.Timeout, eventuallyCheckFrequency)
// While handling the spend notification the batch should detect that
// some sweeps did not appear in the spending tx, therefore it redirects
// them back to the batcher and the batcher inserts them in a new batch.
require.Eventually(t, func() bool {
return len(batcher.batches) == 2
}, test.Timeout, eventuallyCheckFrequency)
// Since second batch was created we check that it registered for its
// primary sweep's spend.
<-lnd.RegisterSpendChannel
// We mock the confirmation notification.
lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: spendingTx,
}
// Eventually the batch receives the confirmation notification,
// gracefully exits and the batcher deletes it.
require.Eventually(t, func() bool {
return len(batcher.batches) == 1
}, test.Timeout, eventuallyCheckFrequency)
// Find the other batch, which includes the sweeps that did not appear
// in the spending tx.
b = &batch{}
for _, btch := range batcher.batches {
b = btch
}
// After all the sweeps enter, it should contain 2 sweeps.
require.Eventually(t, func() bool {
return len(b.sweeps) == 2
}, test.Timeout, eventuallyCheckFrequency)
// The batch should be in an open state.
require.Equal(t, b.state, Open)
}
// TestSweepBatcherNonWalletAddr tests that sweep requests that sweep to a non
// wallet address enter individual batches.
func TestSweepBatcherNonWalletAddr(t *testing.T) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
store := loopdb.NewStoreMock(t)
batcherStore := NewStoreMock()
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, nil, lnd.ChainParams, batcherStore, store)
go func() {
err := batcher.Run(ctx)
if !strings.Contains(err.Error(), "context canceled") {
require.NoError(t, err)
}
}()
// Create a sweep request.
sweepReq1 := SweepRequest{
SwapHash: lntypes.Hash{1, 1, 1},
Value: 111,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
},
Notifier: &dummyNotifier,
}
swap1 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111,
AmountRequested: 111,
},
IsExternalAddr: true,
SwapInvoice: swapInvoice,
}
err := store.CreateLoopOut(ctx, sweepReq1.SwapHash, swap1)
require.NoError(t, err)
store.AssertLoopOutStored()
// Deliver sweep request to batcher.
batcher.sweepReqs <- sweepReq1
// Once batcher receives sweep request it will eventually spin up a
// batch.
require.Eventually(t, func() bool {
return len(batcher.batches) == 1
}, test.Timeout, eventuallyCheckFrequency)
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Insert the same swap twice, this should be a noop.
batcher.sweepReqs <- sweepReq1
// Create a second sweep request that has a timeout distance less than
// our configured threshold.
sweepReq2 := SweepRequest{
SwapHash: lntypes.Hash{2, 2, 2},
Value: 222,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
},
Notifier: &dummyNotifier,
}
swap2 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111 + defaultMaxTimeoutDistance - 1,
AmountRequested: 222,
},
SwapInvoice: swapInvoice,
IsExternalAddr: true,
}
err = store.CreateLoopOut(ctx, sweepReq2.SwapHash, swap2)
require.NoError(t, err)
store.AssertLoopOutStored()
batcher.sweepReqs <- sweepReq2
// Batcher should create a second batch as first batch is a non wallet
// addr batch.
require.Eventually(t, func() bool {
return len(batcher.batches) == 2
}, test.Timeout, eventuallyCheckFrequency)
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Create a third sweep request that has more timeout distance than
// the default.
sweepReq3 := SweepRequest{
SwapHash: lntypes.Hash{3, 3, 3},
Value: 333,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{3, 3},
Index: 3,
},
Notifier: &dummyNotifier,
}
swap3 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111 + defaultMaxTimeoutDistance + 1,
AmountRequested: 333,
},
SwapInvoice: swapInvoice,
IsExternalAddr: true,
}
err = store.CreateLoopOut(ctx, sweepReq3.SwapHash, swap3)
require.NoError(t, err)
store.AssertLoopOutStored()
batcher.sweepReqs <- sweepReq3
// Batcher should create a new batch as timeout distance is greater than
// the threshold
require.Eventually(t, func() bool {
return len(batcher.batches) == 3
}, test.Timeout, eventuallyCheckFrequency)
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
require.Eventually(t, func() bool {
// Verify that each batch has the correct number of sweeps in it.
for _, batch := range batcher.batches {
switch batch.primarySweepID {
case sweepReq1.SwapHash:
if len(batch.sweeps) != 1 {
return false
}
case sweepReq2.SwapHash:
if len(batch.sweeps) != 1 {
return false
}
case sweepReq3.SwapHash:
if len(batch.sweeps) != 1 {
return false
}
}
}
return true
}, test.Timeout, eventuallyCheckFrequency)
// Check that all sweeps were stored.
require.True(t, batcherStore.AssertSweepStored(sweepReq1.SwapHash))
require.True(t, batcherStore.AssertSweepStored(sweepReq2.SwapHash))
require.True(t, batcherStore.AssertSweepStored(sweepReq3.SwapHash))
}
// TestSweepBatcherComposite tests that sweep requests that sweep to both wallet
// addresses and non-wallet addresses enter the correct batches.
func TestSweepBatcherComposite(t *testing.T) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
store := loopdb.NewStoreMock(t)
batcherStore := NewStoreMock()
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, nil, lnd.ChainParams, batcherStore, store)
go func() {
err := batcher.Run(ctx)
if !strings.Contains(err.Error(), "context canceled") {
require.NoError(t, err)
}
}()
// Create a sweep request.
sweepReq1 := SweepRequest{
SwapHash: lntypes.Hash{1, 1, 1},
Value: 111,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
},
Notifier: &dummyNotifier,
}
swap1 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111,
AmountRequested: 111,
},
SwapInvoice: swapInvoice,
}
err := store.CreateLoopOut(ctx, sweepReq1.SwapHash, swap1)
require.NoError(t, err)
store.AssertLoopOutStored()
// Create a second sweep request that has a timeout distance less than
// our configured threshold.
sweepReq2 := SweepRequest{
SwapHash: lntypes.Hash{2, 2, 2},
Value: 222,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
},
Notifier: &dummyNotifier,
}
swap2 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111 + defaultMaxTimeoutDistance - 1,
AmountRequested: 222,
},
SwapInvoice: swapInvoice,
}
err = store.CreateLoopOut(ctx, sweepReq2.SwapHash, swap2)
require.NoError(t, err)
store.AssertLoopOutStored()
// Create a third sweep request that has less timeout distance than the
// default max, but is not spending to a wallet address.
sweepReq3 := SweepRequest{
SwapHash: lntypes.Hash{3, 3, 3},
Value: 333,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{3, 3},
Index: 3,
},
Notifier: &dummyNotifier,
}
swap3 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111 + defaultMaxTimeoutDistance - 3,
AmountRequested: 333,
},
SwapInvoice: swapInvoice,
IsExternalAddr: true,
}
err = store.CreateLoopOut(ctx, sweepReq3.SwapHash, swap3)
require.NoError(t, err)
store.AssertLoopOutStored()
// Create a fourth sweep request that has a timeout which is not valid
// for the first batch, so it will cause it to create a new batch.
sweepReq4 := SweepRequest{
SwapHash: lntypes.Hash{4, 4, 4},
Value: 444,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{4, 4},
Index: 4,
},
Notifier: &dummyNotifier,
}
swap4 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111 + defaultMaxTimeoutDistance + 1,
AmountRequested: 444,
},
SwapInvoice: swapInvoice,
}
err = store.CreateLoopOut(ctx, sweepReq4.SwapHash, swap4)
require.NoError(t, err)
store.AssertLoopOutStored()
// Create a fifth sweep request that has a timeout which is not valid
// for the first batch, but a valid timeout for the new batch.
sweepReq5 := SweepRequest{
SwapHash: lntypes.Hash{5, 5, 5},
Value: 555,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{5, 5},
Index: 5,
},
Notifier: &dummyNotifier,
}
swap5 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111 + defaultMaxTimeoutDistance + 5,
AmountRequested: 555,
},
SwapInvoice: swapInvoice,
}
err = store.CreateLoopOut(ctx, sweepReq5.SwapHash, swap5)
require.NoError(t, err)
store.AssertLoopOutStored()
// Create a sixth sweep request that has a valid timeout for the new
// batch, but is paying to a non-wallet address.
sweepReq6 := SweepRequest{
SwapHash: lntypes.Hash{6, 6, 6},
Value: 666,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{6, 6},
Index: 6,
},
Notifier: &dummyNotifier,
}
swap6 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111 + defaultMaxTimeoutDistance + 6,
AmountRequested: 666,
},
SwapInvoice: swapInvoice,
IsExternalAddr: true,
}
err = store.CreateLoopOut(ctx, sweepReq6.SwapHash, swap6)
require.NoError(t, err)
store.AssertLoopOutStored()
// Deliver sweep request to batcher.
batcher.sweepReqs <- sweepReq1
// Once batcher receives sweep request it will eventually spin up a
// batch.
require.Eventually(t, func() bool {
return len(batcher.batches) == 1
}, test.Timeout, eventuallyCheckFrequency)
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Insert the same swap twice, this should be a noop.
batcher.sweepReqs <- sweepReq1
batcher.sweepReqs <- sweepReq2
// Batcher should not create a second batch as timeout distance is small
// enough.
require.Eventually(t, func() bool {
return len(batcher.batches) == 1
}, test.Timeout, eventuallyCheckFrequency)
batcher.sweepReqs <- sweepReq3
// Batcher should create a second batch as this sweep pays to a non
// wallet address.
require.Eventually(t, func() bool {
return len(batcher.batches) == 2
}, test.Timeout, eventuallyCheckFrequency)
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
batcher.sweepReqs <- sweepReq4
// Batcher should create a third batch as timeout distance is greater
// than the threshold.
require.Eventually(t, func() bool {
return len(batcher.batches) == 3
}, test.Timeout, eventuallyCheckFrequency)
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
batcher.sweepReqs <- sweepReq5
// Batcher should not create a fourth batch as timeout distance is small
// enough for it to join the last batch.
require.Eventually(t, func() bool {
return len(batcher.batches) == 3
}, test.Timeout, eventuallyCheckFrequency)
batcher.sweepReqs <- sweepReq6
// Batcher should create a fourth batch as this sweep pays to a non
// wallet address.
require.Eventually(t, func() bool {
return len(batcher.batches) == 4
}, test.Timeout, eventuallyCheckFrequency)
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
require.Eventually(t, func() bool {
// Verify that each batch has the correct number of sweeps in
// it.
for _, batch := range batcher.batches {
switch batch.primarySweepID {
case sweepReq1.SwapHash:
if len(batch.sweeps) != 2 {
return false
}
case sweepReq3.SwapHash:
if len(batch.sweeps) != 1 {
return false
}
case sweepReq4.SwapHash:
if len(batch.sweeps) != 2 {
return false
}
case sweepReq6.SwapHash:
if len(batch.sweeps) != 1 {
return false
}
}
}
return true
}, test.Timeout, eventuallyCheckFrequency)
// Check that all sweeps were stored.
require.True(t, batcherStore.AssertSweepStored(sweepReq1.SwapHash))
require.True(t, batcherStore.AssertSweepStored(sweepReq2.SwapHash))
require.True(t, batcherStore.AssertSweepStored(sweepReq3.SwapHash))
require.True(t, batcherStore.AssertSweepStored(sweepReq4.SwapHash))
require.True(t, batcherStore.AssertSweepStored(sweepReq5.SwapHash))
require.True(t, batcherStore.AssertSweepStored(sweepReq6.SwapHash))
}

@ -73,31 +73,40 @@ func (c *mockChainNotifier) RegisterBlockEpochNtfn(ctx context.Context) (
chan int32, chan error, error) { chan int32, chan error, error) {
blockErrorChan := make(chan error, 1) blockErrorChan := make(chan error, 1)
blockEpochChan := make(chan int32) blockEpochChan := make(chan int32, 1)
c.lnd.lock.Lock()
c.lnd.blockHeightListeners = append(
c.lnd.blockHeightListeners, blockEpochChan,
)
c.lnd.lock.Unlock()
c.wg.Add(1) c.wg.Add(1)
go func() { go func() {
defer c.wg.Done() defer c.wg.Done()
defer func() {
c.lnd.lock.Lock()
defer c.lnd.lock.Unlock()
for i := 0; i < len(c.lnd.blockHeightListeners); i++ {
if c.lnd.blockHeightListeners[i] == blockEpochChan {
c.lnd.blockHeightListeners = append(
c.lnd.blockHeightListeners[:i],
c.lnd.blockHeightListeners[i+1:]...,
)
break
}
}
}()
// Send initial block height // Send initial block height
c.lnd.lock.Lock()
select { select {
case blockEpochChan <- c.lnd.Height: case blockEpochChan <- c.lnd.Height:
case <-ctx.Done(): case <-ctx.Done():
return
} }
c.lnd.lock.Unlock()
for { <-ctx.Done()
select {
case m := <-c.lnd.epochChannel:
select {
case blockEpochChan <- m:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}() }()
return blockEpochChan, blockErrorChan, nil return blockEpochChan, blockErrorChan, nil

@ -259,3 +259,9 @@ func (ctx *Context) GetOutputIndex(tx *wire.MsgTx,
func (ctx *Context) NotifyServerHeight(height int32) { func (ctx *Context) NotifyServerHeight(height int32) {
require.NoError(ctx.T, ctx.Lnd.NotifyHeight(height)) require.NoError(ctx.T, ctx.Lnd.NotifyHeight(height))
} }
func (ctx *Context) AssertEpochListeners(numListeners int32) {
require.Eventually(ctx.T, func() bool {
return ctx.Lnd.EpochSubscribers() == numListeners
}, Timeout, time.Millisecond*250)
}

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"sync" "sync"
"time"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
@ -63,13 +62,13 @@ func NewMockLnd() *LndMockServices {
SignOutputRawChannel: make(chan SignOutputRawRequest), SignOutputRawChannel: make(chan SignOutputRawRequest),
FailInvoiceChannel: make(chan lntypes.Hash, 2), FailInvoiceChannel: make(chan lntypes.Hash, 2),
epochChannel: make(chan int32), blockHeightListeners: make([]chan int32, 0),
Height: testStartingHeight, Height: testStartingHeight,
NodePubkey: testNodePubkey, NodePubkey: testNodePubkey,
Signature: testSignature, Signature: testSignature,
SignatureMsg: testSignatureMsg, SignatureMsg: testSignatureMsg,
Invoices: make(map[lntypes.Hash]*lndclient.Invoice), Invoices: make(map[lntypes.Hash]*lndclient.Invoice),
} }
lightningClient.lnd = &lnd lightningClient.lnd = &lnd
@ -139,7 +138,7 @@ type LndMockServices struct {
SendOutputsChannel chan wire.MsgTx SendOutputsChannel chan wire.MsgTx
SettleInvoiceChannel chan lntypes.Preimage SettleInvoiceChannel chan lntypes.Preimage
FailInvoiceChannel chan lntypes.Hash FailInvoiceChannel chan lntypes.Hash
epochChannel chan int32 blockHeightListeners []chan int32
ConfChannel chan *chainntnfs.TxConfirmation ConfChannel chan *chainntnfs.TxConfirmation
RegisterConfChannel chan *ConfRegistration RegisterConfChannel chan *ConfRegistration
@ -177,15 +176,28 @@ type LndMockServices struct {
lock sync.Mutex lock sync.Mutex
} }
// EpochSubscribers returns the number of subscribers to block epoch
// notifications.
func (s *LndMockServices) EpochSubscribers() int32 {
s.lock.Lock()
defer s.lock.Unlock()
return int32(len(s.blockHeightListeners))
}
// NotifyHeight notifies a new block height. // NotifyHeight notifies a new block height.
func (s *LndMockServices) NotifyHeight(height int32) error { func (s *LndMockServices) NotifyHeight(height int32) error {
s.lock.Lock()
defer s.lock.Unlock()
s.Height = height s.Height = height
select { for _, listener := range s.blockHeightListeners {
case s.epochChannel <- height: lis := listener
case <-time.After(Timeout): go func() {
return ErrTimeout lis <- height
}()
} }
return nil return nil
} }

@ -13,6 +13,7 @@ import (
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
@ -36,7 +37,7 @@ type testContext struct {
serverMock *serverMock serverMock *serverMock
swapClient *Client swapClient *Client
statusChan chan SwapInfo statusChan chan SwapInfo
store *storeMock store *loopdb.StoreMock
expiryChan chan time.Time expiryChan chan time.Time
runErr chan error runErr chan error
stop func() stop func()
@ -51,6 +52,24 @@ func mockVerifySchnorrSigFail(pubKey *btcec.PublicKey, hash,
return fmt.Errorf("invalid sig") return fmt.Errorf("invalid sig")
} }
// mockVerifySchnorrSigSuccess is used to simulate successful taproot keyspend
// signature verification. If passed to the executeConfig we'll test an
// uncooperative server and will fall back to scriptspend sweep.
func mockVerifySchnorrSigSuccess(pubKey *btcec.PublicKey, hash,
sig []byte) error {
return fmt.Errorf("invalid sig")
}
func mockMuSig2SignSweep(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) {
return nil, nil, nil
}
func newSwapClient(config *clientConfig) *Client { func newSwapClient(config *clientConfig) *Client {
sweeper := &sweep.Sweeper{ sweeper := &sweep.Sweeper{
Lnd: config.LndServices, Lnd: config.LndServices,
@ -58,10 +77,20 @@ func newSwapClient(config *clientConfig) *Client {
lndServices := config.LndServices lndServices := config.LndServices
batcherStore := sweepbatcher.NewStoreMock()
batcher := sweepbatcher.NewBatcher(
config.LndServices.WalletKit, config.LndServices.ChainNotifier,
config.LndServices.Signer, mockMuSig2SignSweep,
mockVerifySchnorrSigSuccess, config.LndServices.ChainParams,
batcherStore, config.Store,
)
executor := newExecutor(&executorConfig{ executor := newExecutor(&executorConfig{
lnd: lndServices, lnd: lndServices,
store: config.Store, store: config.Store,
sweeper: sweeper, sweeper: sweeper,
batcher: batcher,
createExpiryTimer: config.CreateExpiryTimer, createExpiryTimer: config.CreateExpiryTimer,
cancelSwap: config.Server.CancelLoopOutSwap, cancelSwap: config.Server.CancelLoopOutSwap,
verifySchnorrSig: mockVerifySchnorrSigFail, verifySchnorrSig: mockVerifySchnorrSigFail,
@ -83,15 +112,15 @@ func createClientTestContext(t *testing.T,
clientLnd := test.NewMockLnd() clientLnd := test.NewMockLnd()
serverMock := newServerMock(clientLnd) serverMock := newServerMock(clientLnd)
store := newStoreMock(t) store := loopdb.NewStoreMock(t)
for _, s := range pendingSwaps { for _, s := range pendingSwaps {
store.loopOutSwaps[s.Hash] = s.Contract store.LoopOutSwaps[s.Hash] = s.Contract
updates := []loopdb.SwapStateData{} updates := []loopdb.SwapStateData{}
for _, e := range s.Events { for _, e := range s.Events {
updates = append(updates, e.SwapStateData) updates = append(updates, e.SwapStateData)
} }
store.loopOutUpdates[s.Hash] = updates store.LoopOutUpdates[s.Hash] = updates
} }
expiryChan := make(chan time.Time) expiryChan := make(chan time.Time)
@ -147,7 +176,7 @@ func (ctx *testContext) finish() {
} }
func (ctx *testContext) assertIsDone() { func (ctx *testContext) assertIsDone() {
require.NoError(ctx.Context.T, ctx.Context.Lnd.IsDone()) require.NoError(ctx.Context.T, ctx.Context.Lnd.IsDone())
require.NoError(ctx.Context.T, ctx.store.isDone()) require.NoError(ctx.Context.T, ctx.store.IsDone())
select { select {
case <-ctx.statusChan: case <-ctx.statusChan:
@ -159,19 +188,19 @@ func (ctx *testContext) assertIsDone() {
func (ctx *testContext) assertStored() { func (ctx *testContext) assertStored() {
ctx.Context.T.Helper() ctx.Context.T.Helper()
ctx.store.assertLoopOutStored() ctx.store.AssertLoopOutStored()
} }
func (ctx *testContext) assertStorePreimageReveal() { func (ctx *testContext) assertStorePreimageReveal() {
ctx.Context.T.Helper() ctx.Context.T.Helper()
ctx.store.assertStorePreimageReveal() ctx.store.AssertStorePreimageReveal()
} }
func (ctx *testContext) assertStoreFinished(expectedResult loopdb.SwapState) { func (ctx *testContext) assertStoreFinished(expectedResult loopdb.SwapState) {
ctx.Context.T.Helper() ctx.Context.T.Helper()
ctx.store.assertStoreFinished(expectedResult) ctx.store.AssertStoreFinished(expectedResult)
} }
func (ctx *testContext) assertStatus(expectedState loopdb.SwapState) { func (ctx *testContext) assertStatus(expectedState loopdb.SwapState) {
@ -250,3 +279,11 @@ func (ctx *testContext) assertPreimagePush(preimage lntypes.Preimage) {
ctx.Context.T.Fatalf("preimage not pushed") ctx.Context.T.Fatalf("preimage not pushed")
} }
} }
func (ctx *testContext) AssertEpochListeners(numListeners int32) {
ctx.Context.T.Helper()
require.Eventually(ctx.Context.T, func() bool {
return ctx.Lnd.EpochSubscribers() == numListeners
}, test.Timeout, time.Millisecond*250)
}

@ -0,0 +1,77 @@
package utils
import (
"fmt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/zpay32"
)
// GetHtlc composes and returns the on-chain swap script.
func GetHtlc(hash lntypes.Hash, contract *loopdb.SwapContract,
chainParams *chaincfg.Params) (*swap.Htlc, error) {
switch GetHtlcScriptVersion(contract.ProtocolVersion) {
case swap.HtlcV2:
return swap.NewHtlcV2(
contract.CltvExpiry, contract.HtlcKeys.SenderScriptKey,
contract.HtlcKeys.ReceiverScriptKey, hash,
chainParams,
)
case swap.HtlcV3:
// Swaps that implement the new MuSig2 protocol will be expected
// to use the 1.0RC2 MuSig2 key derivation scheme.
muSig2Version := input.MuSig2Version040
if contract.ProtocolVersion >= loopdb.ProtocolVersionMuSig2 {
muSig2Version = input.MuSig2Version100RC2
}
return swap.NewHtlcV3(
muSig2Version,
contract.CltvExpiry,
contract.HtlcKeys.SenderInternalPubKey,
contract.HtlcKeys.ReceiverInternalPubKey,
contract.HtlcKeys.SenderScriptKey,
contract.HtlcKeys.ReceiverScriptKey,
hash, chainParams,
)
}
return nil, swap.ErrInvalidScriptVersion
}
// GetHtlcScriptVersion returns the correct HTLC script version for the passed
// protocol version.
func GetHtlcScriptVersion(
protocolVersion loopdb.ProtocolVersion) swap.ScriptVersion {
// If the swap was initiated before we had our v3 script, use v2.
if protocolVersion < loopdb.ProtocolVersionHtlcV3 ||
protocolVersion == loopdb.ProtocolVersionUnrecorded {
return swap.HtlcV2
}
return swap.HtlcV3
}
// ObtainSwapPaymentAddr will retrieve the payment addr from the passed invoice.
func ObtainSwapPaymentAddr(swapInvoice string, chainParams *chaincfg.Params) (
*[32]byte, error) {
swapPayReq, err := zpay32.Decode(swapInvoice, chainParams)
if err != nil {
return nil, err
}
if swapPayReq.PaymentAddr == nil {
return nil, fmt.Errorf("expected payment address for invoice")
}
return swapPayReq.PaymentAddr, nil
}
Loading…
Cancel
Save