diff --git a/client.go b/client.go index cf54815..526c2a6 100644 --- a/client.go +++ b/client.go @@ -17,6 +17,8 @@ import ( "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/sweepbatcher" + "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/routing/route" "google.golang.org/grpc" @@ -60,7 +62,7 @@ var ( // probeTimeout is the maximum time until a probe is allowed to take. probeTimeout = 3 * time.Minute - republishDelay = 10 * time.Second + repushDelay = 1 * time.Second // MinerFeeEstimationFailed is a magic number that is returned in a // quote call as the miner fee if the fee estimation in lnd's wallet @@ -133,7 +135,8 @@ type ClientConfig struct { // NewClient returns a new instance to initiate swaps with. func NewClient(dbDir string, loopDB loopdb.SwapStore, - cfg *ClientConfig) (*Client, func(), error) { + sweeperDb sweepbatcher.BatcherStore, cfg *ClientConfig) ( + *Client, func(), error) { lsatStore, err := lsat.NewFileStore(dbDir) if err != nil { @@ -161,27 +164,36 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore, Lnd: cfg.Lnd, } + verifySchnorrSig := func(pubKey *btcec.PublicKey, hash, sig []byte) error { + schnorrSig, err := schnorr.ParseSignature(sig) + if err != nil { + return err + } + + if !schnorrSig.Verify(hash, pubKey) { + return fmt.Errorf("invalid signature") + } + + return nil + } + + batcher := sweepbatcher.NewBatcher( + cfg.Lnd.WalletKit, cfg.Lnd.ChainNotifier, cfg.Lnd.Signer, + swapServerClient.MultiMuSig2SignSweep, verifySchnorrSig, + cfg.Lnd.ChainParams, sweeperDb, loopDB, + ) + executor := newExecutor(&executorConfig{ lnd: cfg.Lnd, store: loopDB, sweeper: sweeper, + batcher: batcher, createExpiryTimer: config.CreateExpiryTimer, loopOutMaxParts: cfg.LoopOutMaxParts, totalPaymentTimeout: cfg.TotalPaymentTimeout, maxPaymentRetries: cfg.MaxPaymentRetries, cancelSwap: swapServerClient.CancelLoopOutSwap, - verifySchnorrSig: func(pubKey *btcec.PublicKey, hash, sig []byte) error { - schnorrSig, err := schnorr.ParseSignature(sig) - if err != nil { - return err - } - - if !schnorrSig.Verify(hash, pubKey) { - return fmt.Errorf("invalid signature") - } - - return nil - }, + verifySchnorrSig: verifySchnorrSig, }) client := &Client{ @@ -232,7 +244,7 @@ func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) { LastUpdate: swp.LastUpdateTime(), } - htlc, err := GetHtlc( + htlc, err := utils.GetHtlc( swp.Hash, &swp.Contract.SwapContract, s.lndServices.ChainParams, ) @@ -265,7 +277,7 @@ func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) { LastUpdate: swp.LastUpdateTime(), } - htlc, err := GetHtlc( + htlc, err := utils.GetHtlc( swp.Hash, &swp.Contract.SwapContract, s.lndServices.ChainParams, ) @@ -540,7 +552,7 @@ func (s *Client) getLoopOutSweepFee(ctx context.Context, confTarget int32) ( return 0, err } - scriptVersion := GetHtlcScriptVersion( + scriptVersion := utils.GetHtlcScriptVersion( loopdb.CurrentProtocolVersion(), ) @@ -731,7 +743,7 @@ func (s *Client) estimateFee(ctx context.Context, amt btcutil.Amount, // Generate a dummy address for fee estimation. witnessProg := [32]byte{} - scriptVersion := GetHtlcScriptVersion( + scriptVersion := utils.GetHtlcScriptVersion( loopdb.CurrentProtocolVersion(), ) diff --git a/client_test.go b/client_test.go index 18c24cc..c6c942c 100644 --- a/client_test.go +++ b/client_test.go @@ -13,6 +13,7 @@ import ( "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/test" + "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" @@ -146,8 +147,6 @@ func TestLoopOutFailWrongAmount(t *testing.T) { // TestLoopOutResume tests that swaps in various states are properly resumed // after a restart. func TestLoopOutResume(t *testing.T) { - defer test.Guard(t)() - defaultConfs := loopdb.DefaultLoopOutHtlcConfirmations storedVersion := []loopdb.ProtocolVersion{ @@ -279,7 +278,7 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed, preimageRevealed, int32(confs), ) - htlc, err := GetHtlc( + htlc, err := utils.GetHtlc( hash, &pendingSwap.Contract.SwapContract, &chaincfg.TestNet3Params, ) @@ -304,7 +303,7 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed, func(r error) {}, func(r error) {}, preimageRevealed, - confIntent, GetHtlcScriptVersion(protocolVersion), + confIntent, utils.GetHtlcScriptVersion(protocolVersion), ) } @@ -317,15 +316,28 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash, signalPrepaymentResult(nil) - ctx.AssertRegisterSpendNtfn(confIntent.PkScript) - // 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. 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. 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. if scriptVersion != swap.HtlcV3 { <-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 // our MuSig2 signing attempts. if scriptVersion == swap.HtlcV3 { - 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.assertPreimagePush(ctx.store.LoopOutSwaps[hash].Preimage) <-ctx.Context.Lnd.SignOutputRawChannel } @@ -388,6 +393,8 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash, ctx.NotifySpend(sweepTx, 0) + ctx.AssertRegisterConf(true, 3) + ctx.assertStatus(loopdb.StateSuccess) ctx.assertStoreFinished(loopdb.StateSuccess) diff --git a/cmd/loop/loopout.go b/cmd/loop/loopout.go index 73d50a3..a2ceb98 100644 --- a/cmd/loop/loopout.go +++ b/cmd/loop/loopout.go @@ -238,6 +238,7 @@ func loopOut(ctx *cli.Context) error { resp, err := client.LoopOut(context.Background(), &looprpc.LoopOutRequest{ Amt: int64(amt), Dest: destAddr, + IsExternalAddr: destAddr != "", Account: account, AccountAddrType: accountAddrType, MaxMinerFee: int64(limits.maxMinerFee), diff --git a/executor.go b/executor.go index 9451b15..257074f 100644 --- a/executor.go +++ b/executor.go @@ -13,6 +13,7 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/queue" ) @@ -23,6 +24,8 @@ type executorConfig struct { sweeper *sweep.Sweeper + batcher *sweepbatcher.Batcher + store loopdb.SwapStore createExpiryTimer func(expiry time.Duration) <-chan time.Time @@ -71,6 +74,7 @@ func (s *executor) run(mainCtx context.Context, err error blockEpochChan <-chan int32 blockErrorChan <-chan error + batcherErrChan chan error ) for { @@ -121,6 +125,21 @@ func (s *executor) run(mainCtx context.Context, return mainCtx.Err() } + batcherErrChan = make(chan error, 1) + + s.wg.Add(1) + go func() { + defer s.wg.Done() + + err := s.batcher.Run(mainCtx) + if err != nil { + select { + case batcherErrChan <- err: + case <-mainCtx.Done(): + } + } + }() + // Start main event loop. log.Infof("Starting event loop at height %v", height) @@ -156,6 +175,7 @@ func (s *executor) run(mainCtx context.Context, err := newSwap.execute(mainCtx, &executeConfig{ statusChan: statusChan, sweeper: s.sweeper, + batcher: s.batcher, blockEpochChan: queue.ChanOut(), timerFactory: s.executorConfig.createExpiryTimer, loopOutMaxParts: s.executorConfig.loopOutMaxParts, @@ -211,6 +231,9 @@ func (s *executor) run(mainCtx context.Context, case err := <-blockErrorChan: return fmt.Errorf("block error: %v", err) + case err := <-batcherErrChan: + return fmt.Errorf("batcher error: %v", err) + case <-mainCtx.Done(): return mainCtx.Err() } diff --git a/interface.go b/interface.go index 1a48492..17affe8 100644 --- a/interface.go +++ b/interface.go @@ -20,6 +20,11 @@ type OutRequest struct { // Destination address for the swap. 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 // paid for payment to the server. This limit is applied during path // finding. Typically this value is taken from the response of the diff --git a/labels/lnd_labels.go b/labels/lnd_labels.go index c21dee5..aa69add 100644 --- a/labels/lnd_labels.go +++ b/labels/lnd_labels.go @@ -17,6 +17,8 @@ const ( // loopInTimeout is the label used for loop in swaps to sweep an HTLC // that has timed out. loopInSweepTimeout = "InSweepTimeout" + + loopOutBatchSweepSuccess = "BatchOutSweepSuccess -- %d" ) // 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) } +// 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. func LoopInHtlcLabel(swapHash string) string { return fmt.Sprintf(loopdLabelPattern, loopInHtlc, swapHash) diff --git a/liquidity/autoloop_test.go b/liquidity/autoloop_test.go index 3799748..561a0e1 100644 --- a/liquidity/autoloop_test.go +++ b/liquidity/autoloop_test.go @@ -422,6 +422,7 @@ func TestAutoloopAddress(t *testing.T) { Amount: amt, // Define the expected destination address. DestAddr: addr, + IsExternalAddr: true, MaxSwapRoutingFee: maxRouteFee, MaxPrepayRoutingFee: ppmToSat( quote1.PrepayAmount, prepayFeePPM, @@ -439,6 +440,7 @@ func TestAutoloopAddress(t *testing.T) { Amount: amt, // Define the expected destination address. DestAddr: addr, + IsExternalAddr: true, MaxSwapRoutingFee: maxRouteFee, MaxPrepayRoutingFee: ppmToSat( quote2.PrepayAmount, routeFeePPM, diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index d41b25e..acf01ef 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -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. 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( ctx, swap, defaultAmountBackoffRetry, defaultAmountBackoff, diff --git a/liquidity/loopout_builder.go b/liquidity/loopout_builder.go index 8048aea..b2c6040 100644 --- a/liquidity/loopout_builder.go +++ b/liquidity/loopout_builder.go @@ -138,6 +138,7 @@ func (b *loopOutBuilder) buildSwap(ctx context.Context, pubkey route.Vertex, // already validated them. request := loop.OutRequest{ Amount: amount, + IsExternalAddr: false, OutgoingChanSet: chanSet, MaxPrepayRoutingFee: prepayMaxFee, MaxSwapRoutingFee: routeMaxFee, @@ -160,9 +161,11 @@ func (b *loopOutBuilder) buildSwap(ctx context.Context, pubkey route.Vertex, if len(params.Account) > 0 { account = params.Account addrType = params.AccountAddrType + request.IsExternalAddr = true } if params.DestAddr != nil { request.DestAddr = params.DestAddr + request.IsExternalAddr = true } else { addr, err := b.cfg.Lnd.WalletKit.NextAddr( ctx, account, addrType, false, diff --git a/loopd/daemon.go b/loopd/daemon.go index 1d7027d..747780d 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -18,6 +18,7 @@ import ( "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/loopd/perms" "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightninglabs/loop/instantout/reservation" loop_looprpc "github.com/lightninglabs/loop/looprpc" @@ -412,9 +413,11 @@ func (d *Daemon) initialize(withMacaroonService bool) error { return err } + sweeperDb := sweepbatcher.NewSQLStore(baseDb, chainParams) + // Create an instance of the loop client library. swapClient, clientCleanup, err := getClient( - d.cfg, swapDb, &d.lnd.LndServices, + d.cfg, swapDb, sweeperDb, &d.lnd.LndServices, ) if err != nil { return err diff --git a/loopd/log.go b/loopd/log.go index b9ccb74..873a385 100644 --- a/loopd/log.go +++ b/loopd/log.go @@ -9,6 +9,7 @@ import ( "github.com/lightninglabs/loop/instantout/reservation" "github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/signal" @@ -32,6 +33,7 @@ func SetupLoggers(root *build.RotatingLogWriter, intercept signal.Interceptor) { lnd.SetSubLogger(root, Subsystem, log) lnd.AddSubLogger(root, "LOOP", intercept, loop.UseLogger) + lnd.AddSubLogger(root, "SWEEP", intercept, sweepbatcher.UseLogger) lnd.AddSubLogger(root, "LNDC", intercept, lndclient.UseLogger) lnd.AddSubLogger(root, "STORE", intercept, loopdb.UseLogger) lnd.AddSubLogger(root, lsat.Subsystem, intercept, lsat.UseLogger) diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 845885c..05f9156 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -100,6 +100,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context, log.Infof("Loop out request received") var sweepAddr btcutil.Address + var isExternalAddr bool var err error //nolint:lll switch { @@ -117,6 +118,8 @@ func (s *swapClientServer) LoopOut(ctx context.Context, return nil, fmt.Errorf("decode address: %v", err) } + isExternalAddr = true + case in.Account != "" && in.AccountAddrType == clientrpc.AddressType_ADDRESS_TYPE_UNKNOWN: return nil, liquidity.ErrAccountAndAddrType @@ -141,6 +144,8 @@ func (s *swapClientServer) LoopOut(ctx context.Context, "%v", err) } + isExternalAddr = true + default: // Generate sweep address if none specified. sweepAddr, err = s.lnd.WalletKit.NextAddr( @@ -166,6 +171,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context, req := &loop.OutRequest{ Amount: btcutil.Amount(in.Amt), DestAddr: sweepAddr, + IsExternalAddr: isExternalAddr, MaxMinerFee: btcutil.Amount(in.MaxMinerFee), MaxPrepayAmount: btcutil.Amount(in.MaxPrepayAmt), MaxPrepayRoutingFee: btcutil.Amount(in.MaxPrepayRoutingFee), diff --git a/loopd/utils.go b/loopd/utils.go index 78f8f4a..ef763e2 100644 --- a/loopd/utils.go +++ b/loopd/utils.go @@ -11,13 +11,15 @@ import ( "github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/ticker" ) // getClient returns an instance of the swap client. func getClient(cfg *Config, swapDb loopdb.SwapStore, - lnd *lndclient.LndServices) (*loop.Client, func(), error) { + sweeperDb sweepbatcher.BatcherStore, lnd *lndclient.LndServices) ( + *loop.Client, func(), error) { clientConfig := &loop.ClientConfig{ ServerAddress: cfg.Server.Host, @@ -33,7 +35,7 @@ func getClient(cfg *Config, swapDb loopdb.SwapStore, } swapClient, cleanUp, err := loop.NewClient( - cfg.DataDir, swapDb, clientConfig, + cfg.DataDir, swapDb, sweeperDb, clientConfig, ) if err != nil { return nil, nil, err diff --git a/loopd/view.go b/loopd/view.go index a6eaf13..79a0a1e 100644 --- a/loopd/view.go +++ b/loopd/view.go @@ -8,6 +8,8 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/sweepbatcher" + "github.com/lightninglabs/loop/utils" ) // view prints all swaps currently in the database. @@ -25,12 +27,16 @@ func view(config *Config, lisCfg *ListenerCfg) error { return err } - swapDb, _, err := openDatabase(config, chainParams) + swapDb, baseDb, err := openDatabase(config, chainParams) if err != nil { return err } - swapClient, cleanup, err := getClient(config, swapDb, &lnd.LndServices) + sweeperDb := sweepbatcher.NewSQLStore(baseDb, chainParams) + + swapClient, cleanup, err := getClient( + config, swapDb, sweeperDb, &lnd.LndServices, + ) if err != nil { return err } @@ -56,7 +62,7 @@ func viewOut(swapClient *loop.Client, chainParams *chaincfg.Params) error { for _, s := range swaps { s := s - htlc, err := loop.GetHtlc( + htlc, err := utils.GetHtlc( s.Hash, &s.Contract.SwapContract, chainParams, ) if err != nil { @@ -107,7 +113,7 @@ func viewIn(swapClient *loop.Client, chainParams *chaincfg.Params) error { for _, s := range swaps { s := s - htlc, err := loop.GetHtlc( + htlc, err := utils.GetHtlc( s.Hash, &s.Contract.SwapContract, chainParams, ) if err != nil { diff --git a/loopdb/loopout.go b/loopdb/loopout.go index 9ebb858..2507784 100644 --- a/loopdb/loopout.go +++ b/loopdb/loopout.go @@ -24,6 +24,10 @@ type LoopOutContract struct { // DestAddr is the destination address of the loop out swap. 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 // initiate the loop out swap. SwapInvoice string diff --git a/loopdb/sql_store.go b/loopdb/sql_store.go index 9613ec5..83de95c 100644 --- a/loopdb/sql_store.go +++ b/loopdb/sql_store.go @@ -9,6 +9,7 @@ import ( "time" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightninglabs/loop/loopdb/sqlc" "github.com/lightningnetwork/lnd/keychain" @@ -31,13 +32,16 @@ func (s *BaseDB) FetchLoopOutSwaps(ctx context.Context) ([]*LoopOut, loopOuts = make([]*LoopOut, len(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 { return err } - loopOut, err := s.convertLoopOutRow( - sqlc.GetLoopOutSwapRow(swap), updates, + loopOut, err := ConvertLoopOutRow( + s.network, sqlc.GetLoopOutSwapRow(swap), + updates, ) if err != nil { return err @@ -72,8 +76,8 @@ func (s *BaseDB) FetchLoopOutSwap(ctx context.Context, return err } - loopOut, err = s.convertLoopOutRow( - swap, updates, + loopOut, err = ConvertLoopOutRow( + s.network, swap, updates, ) if err != nil { return err @@ -430,6 +434,7 @@ func loopOutToInsertArgs(hash lntypes.Hash, return sqlc.InsertLoopOutParams{ SwapHash: hash[:], DestAddress: loopOut.DestAddr.String(), + SingleSweep: loopOut.IsExternalAddr, SwapInvoice: loopOut.SwapInvoice, MaxSwapRoutingFee: int64(loopOut.MaxSwapRoutingFee), 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. -func (s *BaseDB) convertLoopOutRow(row sqlc.GetLoopOutSwapRow, +func ConvertLoopOutRow(network *chaincfg.Params, row sqlc.GetLoopOutSwapRow, updates []sqlc.SwapUpdate) (*LoopOut, error) { htlcKeys, err := fetchHtlcKeys( @@ -498,7 +503,7 @@ func (s *BaseDB) convertLoopOutRow(row sqlc.GetLoopOutSwapRow, return nil, err } - destAddress, err := btcutil.DecodeAddress(row.DestAddress, s.network) + destAddress, err := btcutil.DecodeAddress(row.DestAddress, network) if err != nil { return nil, err } @@ -523,6 +528,7 @@ func (s *BaseDB) convertLoopOutRow(row sqlc.GetLoopOutSwapRow, ProtocolVersion: ProtocolVersion(row.ProtocolVersion), }, DestAddr: destAddress, + IsExternalAddr: row.SingleSweep, SwapInvoice: row.SwapInvoice, MaxSwapRoutingFee: btcutil.Amount(row.MaxSwapRoutingFee), SweepConfTarget: row.SweepConfTarget, diff --git a/loopdb/sqlc/batch.sql.go b/loopdb/sqlc/batch.sql.go new file mode 100644 index 0000000..9469292 --- /dev/null +++ b/loopdb/sqlc/batch.sql.go @@ -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 +} diff --git a/loopdb/sqlc/migrations/000004_is_wallet_addr.down.sql b/loopdb/sqlc/migrations/000004_is_wallet_addr.down.sql new file mode 100644 index 0000000..e2e9d3f --- /dev/null +++ b/loopdb/sqlc/migrations/000004_is_wallet_addr.down.sql @@ -0,0 +1 @@ +ALTER TABLE loopout_swaps DROP COLUMN single_sweep; \ No newline at end of file diff --git a/loopdb/sqlc/migrations/000004_is_wallet_addr.up.sql b/loopdb/sqlc/migrations/000004_is_wallet_addr.up.sql new file mode 100644 index 0000000..26940fd --- /dev/null +++ b/loopdb/sqlc/migrations/000004_is_wallet_addr.up.sql @@ -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; \ No newline at end of file diff --git a/loopdb/sqlc/migrations/000005_sweep_batcher.down.sql b/loopdb/sqlc/migrations/000005_sweep_batcher.down.sql new file mode 100644 index 0000000..bf27a39 --- /dev/null +++ b/loopdb/sqlc/migrations/000005_sweep_batcher.down.sql @@ -0,0 +1,2 @@ +DROP TABLE sweep_batches; +DROP TABLE sweeps; \ No newline at end of file diff --git a/loopdb/sqlc/migrations/000005_sweep_batcher.up.sql b/loopdb/sqlc/migrations/000005_sweep_batcher.up.sql new file mode 100644 index 0000000..c8b0ed6 --- /dev/null +++ b/loopdb/sqlc/migrations/000005_sweep_batcher.up.sql @@ -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) +); diff --git a/loopdb/sqlc/models.go b/loopdb/sqlc/models.go index 413b146..d10b67d 100644 --- a/loopdb/sqlc/models.go +++ b/loopdb/sqlc/models.go @@ -42,6 +42,7 @@ type LoopoutSwap struct { PrepayInvoice string MaxPrepayRoutingFee int64 PublicationDeadline time.Time + SingleSweep bool } type Reservation struct { @@ -90,3 +91,23 @@ type SwapUpdate struct { OnchainCost 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 +} diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index 69ae559..a5578ba 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -9,8 +9,10 @@ import ( ) type Querier interface { + ConfirmBatch(ctx context.Context, id int32) error CreateReservation(ctx context.Context, arg CreateReservationParams) error FetchLiquidityParams(ctx context.Context) ([]byte, error) + GetBatchSweeps(ctx context.Context, batchID int32) ([]GetBatchSweepsRow, error) GetLoopInSwap(ctx context.Context, swapHash []byte) (GetLoopInSwapRow, error) GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, error) GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopOutSwapRow, error) @@ -19,14 +21,19 @@ type Querier interface { GetReservationUpdates(ctx context.Context, reservationID []byte) ([]ReservationUpdate, error) GetReservations(ctx context.Context) ([]Reservation, 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 InsertLoopIn(ctx context.Context, arg InsertLoopInParams) error InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) error InsertReservationUpdate(ctx context.Context, arg InsertReservationUpdateParams) error InsertSwap(ctx context.Context, arg InsertSwapParams) error InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error + UpdateBatch(ctx context.Context, arg UpdateBatchParams) error UpdateReservation(ctx context.Context, arg UpdateReservationParams) error UpsertLiquidityParams(ctx context.Context, params []byte) error + UpsertSweep(ctx context.Context, arg UpsertSweepParams) error } var _ Querier = (*Queries)(nil) diff --git a/loopdb/sqlc/queries/batch.sql b/loopdb/sqlc/queries/batch.sql new file mode 100644 index 0000000..336ccf1 --- /dev/null +++ b/loopdb/sqlc/queries/batch.sql @@ -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; diff --git a/loopdb/sqlc/queries/swaps.sql b/loopdb/sqlc/queries/swaps.sql index b1c8da0..925ede6 100644 --- a/loopdb/sqlc/queries/swaps.sql +++ b/loopdb/sqlc/queries/swaps.sql @@ -104,9 +104,10 @@ INSERT INTO loopout_swaps ( outgoing_chan_set, prepay_invoice, max_prepay_routing_fee, - publication_deadline + publication_deadline, + single_sweep ) 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 diff --git a/loopdb/sqlc/swaps.sql.go b/loopdb/sqlc/swaps.sql.go index 793dfa9..8249942 100644 --- a/loopdb/sqlc/swaps.sql.go +++ b/loopdb/sqlc/swaps.sql.go @@ -169,7 +169,7 @@ func (q *Queries) GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, erro const getLoopOutSwap = `-- name: GetLoopOutSwap :one 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, - 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 FROM swaps @@ -203,6 +203,7 @@ type GetLoopOutSwapRow struct { PrepayInvoice string MaxPrepayRoutingFee int64 PublicationDeadline time.Time + SingleSweep bool SwapHash_3 []byte SenderScriptPubkey []byte ReceiverScriptPubkey []byte @@ -237,6 +238,7 @@ func (q *Queries) GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopO &i.PrepayInvoice, &i.MaxPrepayRoutingFee, &i.PublicationDeadline, + &i.SingleSweep, &i.SwapHash_3, &i.SenderScriptPubkey, &i.ReceiverScriptPubkey, @@ -251,7 +253,7 @@ func (q *Queries) GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopO const getLoopOutSwaps = `-- name: GetLoopOutSwaps :many 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, - 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 FROM swaps @@ -285,6 +287,7 @@ type GetLoopOutSwapsRow struct { PrepayInvoice string MaxPrepayRoutingFee int64 PublicationDeadline time.Time + SingleSweep bool SwapHash_3 []byte SenderScriptPubkey []byte ReceiverScriptPubkey []byte @@ -325,6 +328,7 @@ func (q *Queries) GetLoopOutSwaps(ctx context.Context) ([]GetLoopOutSwapsRow, er &i.PrepayInvoice, &i.MaxPrepayRoutingFee, &i.PublicationDeadline, + &i.SingleSweep, &i.SwapHash_3, &i.SenderScriptPubkey, &i.ReceiverScriptPubkey, @@ -465,9 +469,10 @@ INSERT INTO loopout_swaps ( outgoing_chan_set, prepay_invoice, max_prepay_routing_fee, - publication_deadline + publication_deadline, + single_sweep ) 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 MaxPrepayRoutingFee int64 PublicationDeadline time.Time + SingleSweep bool } 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.MaxPrepayRoutingFee, arg.PublicationDeadline, + arg.SingleSweep, ) return err } diff --git a/loopdb/store.go b/loopdb/store.go index a52ef89..3726ea2 100644 --- a/loopdb/store.go +++ b/loopdb/store.go @@ -137,6 +137,9 @@ var ( // errInvalidKey is returned when a serialized key is not the expected // length. errInvalidKey = fmt.Errorf("invalid serialized key") + + // errUnimplemented is returned when a method is not implemented. + errUnimplemented = fmt.Errorf("unimplemented method") ) const ( @@ -990,19 +993,19 @@ func (s *boltSwapStore) fetchLoopInSwap(rootBucket *bbolt.Bucket, func (b *boltSwapStore) BatchCreateLoopOut(ctx context.Context, swaps map[lntypes.Hash]*LoopOutContract) error { - return errors.New("not implemented") + return errUnimplemented } // BatchCreateLoopIn creates a batch of loop in swaps to the store. func (b *boltSwapStore) BatchCreateLoopIn(ctx context.Context, swaps map[lntypes.Hash]*LoopInContract) error { - return errors.New("not implemented") + return errUnimplemented } // BatchInsertUpdate inserts batch of swap updates to the store. func (b *boltSwapStore) BatchInsertUpdate(ctx context.Context, updateData map[lntypes.Hash][]BatchInsertUpdateData) error { - return errors.New("not implemented") + return errUnimplemented } diff --git a/loopdb/store_mock.go b/loopdb/store_mock.go new file mode 100644 index 0000000..6057e2a --- /dev/null +++ b/loopdb/store_mock.go @@ -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") +} diff --git a/loopin.go b/loopin.go index 5fb7908..f2e495e 100644 --- a/loopin.go +++ b/loopin.go @@ -18,6 +18,7 @@ import ( "github.com/lightninglabs/loop/labels" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" + "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/chainntnfs" invpkg "github.com/lightningnetwork/lnd/invoices" "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 // loopInSwap. func (s *loopInSwap) initHtlcs() error { - htlc, err := GetHtlc( + htlc, err := utils.GetHtlc( s.hash, &s.SwapContract, s.swapKit.lnd.ChainParams, ) if err != nil { diff --git a/loopin_test.go b/loopin_test.go index b648d8c..768869f 100644 --- a/loopin_test.go +++ b/loopin_test.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/test" + "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/chainntnfs" invpkg "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/routing/route" @@ -61,7 +62,7 @@ func testLoopInSuccess(t *testing.T) { inSwap := initResult.swap - ctx.store.assertLoopInStored() + ctx.store.AssertLoopInStored() errChan := make(chan error) go func() { @@ -82,7 +83,7 @@ func testLoopInSuccess(t *testing.T) { require.Nil(t, swapInfo.OutgoingChanSet) ctx.assertState(loopdb.StateHtlcPublished) - ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + ctx.store.AssertLoopInState(loopdb.StateHtlcPublished) // Expect htlc to be published. 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 // and on chain fee. - state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + state := ctx.store.AssertLoopInState(loopdb.StateHtlcPublished) require.NotNil(t, state.HtlcTxHash) require.Equal(t, cost, state.Cost) @@ -119,7 +120,7 @@ func testLoopInSuccess(t *testing.T) { // Swap is expected to move to the state InvoiceSettled ctx.assertState(loopdb.StateInvoiceSettled) - ctx.store.assertLoopInState(loopdb.StateInvoiceSettled) + ctx.store.AssertLoopInState(loopdb.StateInvoiceSettled) // Server spends htlc. successTx := wire.MsgTx{} @@ -138,7 +139,7 @@ func testLoopInSuccess(t *testing.T) { } ctx.assertState(loopdb.StateSuccess) - ctx.store.assertLoopInState(loopdb.StateSuccess) + ctx.store.AssertLoopInState(loopdb.StateSuccess) require.NoError(t, <-errChan) } @@ -213,7 +214,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) { require.NoError(t, err) inSwap := initResult.swap - ctx.store.assertLoopInStored() + ctx.store.AssertLoopInStored() errChan := make(chan error) go func() { @@ -227,7 +228,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) { ctx.assertState(loopdb.StateInitiated) ctx.assertState(loopdb.StateHtlcPublished) - ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + ctx.store.AssertLoopInState(loopdb.StateHtlcPublished) var ( 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 // hash and cost. - state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + state := ctx.store.AssertLoopInState(loopdb.StateHtlcPublished) require.NotNil(t, state.HtlcTxHash) require.Equal(t, cost, state.Cost) } else { @@ -280,7 +281,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) { invalidAmt := externalValue != 0 && externalValue != int64(req.Amount) if invalidAmt { ctx.assertState(loopdb.StateFailIncorrectHtlcAmt) - ctx.store.assertLoopInState(loopdb.StateFailIncorrectHtlcAmt) + ctx.store.AssertLoopInState(loopdb.StateFailIncorrectHtlcAmt) require.NoError(t, <-errChan) return @@ -329,7 +330,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) { ctx.updateInvoiceState(0, invpkg.ContractCanceled) ctx.assertState(loopdb.StateFailTimeout) - state := ctx.store.assertLoopInState(loopdb.StateFailTimeout) + state := ctx.store.AssertLoopInState(loopdb.StateFailTimeout) require.Equal(t, cost, state.Cost) require.NoError(t, <-errChan) @@ -449,7 +450,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool, pendSwap.Loop.Events[0].Cost = cost } - htlc, err := GetHtlc( + htlc, err := utils.GetHtlc( testPreimage.Hash(), &contract.SwapContract, cfg.lnd.ChainParams, ) @@ -503,7 +504,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool, } ctx.assertState(loopdb.StateHtlcPublished) - ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + ctx.store.AssertLoopInState(loopdb.StateHtlcPublished) // Expect htlc to be published. 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 // hash. - state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + state := ctx.store.AssertLoopInState(loopdb.StateHtlcPublished) require.NotNil(t, state.HtlcTxHash) } else { 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 ctx.assertState(loopdb.StateInvoiceSettled) - ctx.store.assertLoopInState(loopdb.StateInvoiceSettled) + ctx.store.AssertLoopInState(loopdb.StateInvoiceSettled) // Server spends htlc. successTx := wire.MsgTx{} @@ -566,7 +567,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool, } 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 // 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 // database. - ctx.store.assertLoopInState(loopdb.StateFailAbandoned) + ctx.store.AssertLoopInState(loopdb.StateFailAbandoned) // Ensure that the swap was abandoned and the execution stopped. err = <-ctx.errChan @@ -668,7 +669,7 @@ func TestAbandonSettledInvoiceState(t *testing.T) { // Swap is expected to move to the state InvoiceSettled ctx.assertState(loopdb.StateInvoiceSettled) - ctx.store.assertLoopInState(loopdb.StateInvoiceSettled) + ctx.store.AssertLoopInState(loopdb.StateInvoiceSettled) // The client requests to abandon the published htlc state. inSwap.abandonChan <- struct{}{} @@ -678,7 +679,7 @@ func TestAbandonSettledInvoiceState(t *testing.T) { // Ensure that the swap is also in the StateFailAbandoned state in the // database. - ctx.store.assertLoopInState(loopdb.StateFailAbandoned) + ctx.store.AssertLoopInState(loopdb.StateFailAbandoned) // Ensure that the swap was abandoned and the execution stopped. err = <-ctx.errChan @@ -728,14 +729,14 @@ func advanceToPublishedHtlc(t *testing.T, ctx *loopInTestContext) SwapInfo { require.Equal(t, loopdb.StateInitiated, swapInfo.State) ctx.assertState(loopdb.StateHtlcPublished) - ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + ctx.store.AssertLoopInState(loopdb.StateHtlcPublished) // Expect htlc to be published. htlcTx := <-ctx.lnd.SendOutputsChannel // Expect the same state to be written again with the htlc tx hash // 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). <-ctx.lnd.RegisterConfChannel @@ -765,7 +766,7 @@ func startNewLoopIn(t *testing.T, ctx *loopInTestContext, height int32) ( inSwap := initResult.swap - ctx.store.assertLoopInStored() + ctx.store.AssertLoopInStored() go func() { err := inSwap.execute(context.Background(), ctx.cfg, height) diff --git a/loopin_testcontext_test.go b/loopin_testcontext_test.go index 6bf0ad1..cc48990 100644 --- a/loopin_testcontext_test.go +++ b/loopin_testcontext_test.go @@ -18,7 +18,7 @@ type loopInTestContext struct { t *testing.T lnd *test.LndMockServices server *serverMock - store *storeMock + store *loopdb.StoreMock sweeper *sweep.Sweeper cfg *executeConfig statusChan chan SwapInfo @@ -31,7 +31,7 @@ type loopInTestContext struct { func newLoopInTestContext(t *testing.T) *loopInTestContext { lnd := test.NewMockLnd() server := newServerMock(lnd) - store := newStoreMock(t) + store := loopdb.NewStoreMock(t) sweeper := sweep.Sweeper{Lnd: &lnd.LndServices} blockEpochChan := make(chan interface{}) diff --git a/loopout.go b/loopout.go index f1b1be2..8035b9d 100644 --- a/loopout.go +++ b/loopout.go @@ -1,7 +1,6 @@ package loop import ( - "bytes" "context" "crypto/rand" "crypto/sha256" @@ -12,22 +11,19 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/lndclient" - "github.com/lightninglabs/loop/labels" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/sweepbatcher" + "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" - "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntypes" - "github.com/lightningnetwork/lnd/zpay32" ) const ( @@ -90,6 +86,7 @@ type loopOutSwap struct { // executeConfig contains extra configuration to execute the swap. type executeConfig struct { sweeper *sweep.Sweeper + batcher *sweepbatcher.Batcher statusChan chan<- SwapInfo blockEpochChan <-chan interface{} timerFactory func(time.Duration) <-chan time.Time @@ -172,6 +169,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig, contract := loopdb.LoopOutContract{ SwapInvoice: swapResp.swapInvoice, DestAddr: request.DestAddr, + IsExternalAddr: request.IsExternalAddr, MaxSwapRoutingFee: request.MaxSwapRoutingFee, SweepConfTarget: request.SweepConfTarget, HtlcConfirmations: confs, @@ -206,7 +204,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig, swapKit.lastUpdateTime = initiationTime // Create the htlc. - htlc, err := GetHtlc( + htlc, err := utils.GetHtlc( swapKit.hash, swapKit.contract, swapKit.lnd.ChainParams, ) 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 // recommendation and possibly for cancel. - paymentAddr, err := obtainSwapPaymentAddr(contract.SwapInvoice, cfg) + paymentAddr, err := utils.ObtainSwapPaymentAddr( + contract.SwapInvoice, cfg.lnd.ChainParams, + ) if err != nil { return nil, err } @@ -262,7 +262,7 @@ func resumeLoopOutSwap(cfg *swapConfig, pend *loopdb.LoopOut, ) // Create the htlc. - htlc, err := GetHtlc( + htlc, err := utils.GetHtlc( swapKit.hash, swapKit.contract, swapKit.lnd.ChainParams, ) 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 // recommendation and possibly for cancel. - paymentAddr, err := obtainSwapPaymentAddr( - pend.Contract.SwapInvoice, cfg, + paymentAddr, err := utils.ObtainSwapPaymentAddr( + pend.Contract.SwapInvoice, cfg.lnd.ChainParams, ) if err != nil { return nil, err @@ -301,24 +301,6 @@ func resumeLoopOutSwap(cfg *swapConfig, pend *loopdb.LoopOut, 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. func (s *loopOutSwap) sendUpdate(ctx context.Context) error { 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. - spendDetails, err := s.waitForHtlcSpendConfirmed( + spendTx, err := s.waitForHtlcSpendConfirmedV2( globalCtx, *htlcOutpoint, htlcValue, ) 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 // its spend, so we can exit. - if spendDetails == nil { + if spendTx == 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 // may be swept by a different (fee) sweep tx from a previous run. htlcInput, err := swap.GetTxInputByOutpoint( - spendDetails.SpendingTx, htlcOutpoint, + spendTx, htlcOutpoint, ) if err != nil { return err @@ -560,7 +542,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error { s.cost.Server -= htlcValue s.cost.Onchain = htlcValue - - btcutil.Amount(spendDetails.SpendingTx.TxOut[0].Value) + btcutil.Amount(spendTx.TxOut[0].Value) s.state = loopdb.StateSuccess } else { @@ -1019,26 +1001,36 @@ func (s *loopOutSwap) waitForConfirmedHtlc(globalCtx context.Context) ( return txConf, nil } -// waitForHtlcSpendConfirmed waits for the htlc to be spent either by our own -// sweep or a server revocation tx. During this process, this function will try -// to spend the htlc every block by calling spendFunc. -// -// TODO: Improve retry/fee increase mechanism. Once in the mempool, server can -// sweep offchain. So we must make sure we sweep successfully before on-chain -// timeout. -func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, +// waitForHtlcSpendConfirmedV2 waits for the htlc to be spent either by our own +// sweep or a server revocation tx. +func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context, htlcOutpoint wire.OutPoint, htlcValue btcutil.Amount) ( - *chainntnfs.SpendDetail, error) { + *wire.MsgTx, error) { + + spendChan := make(chan *wire.MsgTx) + spendErrChan := make(chan error, 1) + quitChan := make(chan bool, 1) + + defer func() { + quitChan <- true + }() + + notifier := sweepbatcher.SpendNotifier{ + SpendChan: spendChan, + SpendErrChan: spendErrChan, + QuitChan: quitChan, + } + + sweepReq := sweepbatcher.SweepRequest{ + SwapHash: s.hash, + Outpoint: htlcOutpoint, + Value: htlcValue, + Notifier: ¬ifier, + } // Register the htlc spend notification. ctx, cancel := context.WithCancel(globalCtx) defer cancel() - spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn( - ctx, &htlcOutpoint, s.htlc.PkScript, s.InitiationHeight, - ) - if err != nil { - return nil, fmt.Errorf("register spend ntfn: %v", err) - } // Track our payment status so that we can detect whether our off chain // htlc is settled. We track this information to determine whether it is @@ -1055,26 +1047,20 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, // is used to decide whether we need to push our preimage to // the server. paymentComplete bool - // musigSweepTryCount tracts the number of cooperative, MuSig2 - // sweep attempts. - musigSweepTryCount int - // musigSweepSuccess tracks whether at least one MuSig2 sweep - // txn was successfully published to the mempool. - musigSweepSuccess bool ) - timerChan := s.timerFactory(republishDelay) + timerChan := s.timerFactory(repushDelay) + for { select { // Htlc spend, break loop. - case spendDetails := <-spendChan: - s.log.Infof("Htlc spend by tx: %v", - spendDetails.SpenderTxHash) + case spendTx := <-spendChan: + s.log.Infof("Htlc spend by tx: %v", spendTx.TxHash()) - return spendDetails, nil + return spendTx, nil // Spend notification error. - case err := <-spendErr: + case err := <-spendErrChan: return nil, err // Receive status updates for our payment so that we can detect @@ -1116,121 +1102,51 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, return nil, err } - // New block arrived, update height and restart the republish - // timer. + // New block arrived, update height and try pushing preimage. case notification := <-s.blockEpochChan: s.height = notification.(int32) - timerChan = s.timerFactory(republishDelay) + timerChan = s.timerFactory(repushDelay) - // Some time after start or after arrival of a new block, try - // to spend again. case <-timerChan: - if IsTaprootSwap(&s.SwapContract) { - // sweepConfTarget will return false if the - // preimage is not revealed yet but the conf - // target is closer than 20 blocks. In this case - // to be sure we won't attempt to sweep at all - // and we won't reveal the preimage either. - _, canSweep := s.sweepConfTarget() - if !canSweep { - s.log.Infof("Aborting swap, timed " + - "out on-chain") - - s.state = loopdb.StateFailTimeout - err := s.persistState(ctx) - if err != nil { - log.Warnf("unable to persist " + - "state") - } - - return nil, nil - } - - // When using taproot HTLCs we're pushing the - // preimage before attempting to sweep. This - // way the server will know that the swap will - // go through and we'll be able to MuSig2 - // cosign our sweep transaction. In the worst - // case if the server is uncooperative for any - // reason we can still sweep using scriptpath - // spend. - err = s.setStatePreimageRevealed(ctx) + // sweepConfTarget will return false if the preimage is + // not revealed yet but the conf target is closer than + // 20 blocks. In this case to be sure we won't attempt + // to sweep at all and we won't reveal the preimage + // either. + _, canSweep := s.sweepConfTarget() + if !canSweep { + s.log.Infof("Aborting swap, timed " + + "out on-chain") + + s.state = loopdb.StateFailTimeout + err := s.persistState(ctx) if err != nil { - return nil, err - } - - if !paymentComplete { - // Push the preimage for as long as the - // server is able to settle the swap - // invoice. So that we can continue - // with the MuSig2 sweep afterwards. - s.pushPreimage(ctx) + log.Warnf("unable to persist " + + "state") } - // Now attempt to publish a MuSig2 sweep txn. - // Only attempt at most maxMusigSweepRetires - // times to still leave time for an emergency - // script path sweep. - if musigSweepTryCount < maxMusigSweepRetries { - success := s.sweepMuSig2( - ctx, htlcOutpoint, htlcValue, - ) - if !success { - musigSweepTryCount++ - } else { - // Mark that we had a sweep - // that was successful. There's - // no need for the script spend - // now we can just keep pushing - // new sweeps to bump the fee. - musigSweepSuccess = true - } - } else if !musigSweepSuccess { - // Attempt to script path sweep. If the - // sweep fails, we can't do any better - // than go on and try again later as - // the preimage is already revealed and - // the server settled the swap payment. - // From the server's point of view the - // swap is succeeded at this point so - // we are free to retry as long as we - // want. - err := s.sweep( - ctx, htlcOutpoint, htlcValue, - ) - if err != nil { - log.Warnf("Failed to publish "+ - "non-cooperative "+ - "sweep: %v", err) - } - } + return nil, nil + } - // If the result of our spend func was that the - // swap has reached a final state, then we - // return nil spend details, because there is - // no further action required for this swap. - if s.state.Type() != loopdb.StateTypePending { - return nil, nil - } - } else { - err := s.sweep(ctx, htlcOutpoint, htlcValue) - if err != nil { - return nil, err - } + // Send the sweep to the sweeper. + err := s.batcher.AddSweep(&sweepReq) + if err != nil { + return nil, err + } - // If the result of our spend func was that the - // swap has reached a final state, then we - // return nil spend details, because there is no - // further action required for this swap. - if s.state.Type() != loopdb.StateTypePending { - return nil, nil - } + // Now that the sweep is taken care of, we can update + // our state. + err = s.setStatePreimageRevealed(ctx) + if err != nil { + return nil, err + } - // If our off chain payment is not yet complete, - // we try to push our preimage to the server. - if !paymentComplete { - s.pushPreimage(ctx) - } + if !paymentComplete { + // Push the preimage for as long as the + // server is able to settle the swap + // invoice. So that we can continue + // with the MuSig2 sweep afterwards. + s.pushPreimage(ctx) } // Context canceled. @@ -1339,129 +1255,63 @@ func (s *loopOutSwap) failOffChain(ctx context.Context, paymentType paymentType, } } -// createMuSig2SweepTxn creates a taproot keyspend sweep transaction and -// attempts to cooperate with the server to create a MuSig2 signature witness. -func (s *loopOutSwap) createMuSig2SweepTxn( - ctx context.Context, htlcOutpoint wire.OutPoint, - htlcValue btcutil.Amount, fee btcutil.Amount) (*wire.MsgTx, error) { - - // First assemble our taproot keyspend sweep transaction and get the - // sig hash. - sweepTx, sweepTxPsbt, sigHash, err := s.sweeper.CreateUnsignedTaprootKeySpendSweepTx( - ctx, uint32(s.height), s.htlc, htlcOutpoint, htlcValue, fee, - s.DestAddr, - ) - if err != nil { - return nil, err - } - - var ( - signers [][]byte - muSig2Version input.MuSig2Version - ) +func (s *loopOutSwap) setStatePreimageRevealed(ctx context.Context) error { + if s.state != loopdb.StatePreimageRevealed { + s.state = loopdb.StatePreimageRevealed - // Depending on the MuSig2 version we either pass 32 byte Schnorr - // public keys or normal 33 byte public keys. - if s.ProtocolVersion >= loopdb.ProtocolVersionMuSig2 { - muSig2Version = input.MuSig2Version100RC2 - signers = [][]byte{ - s.HtlcKeys.SenderInternalPubKey[:], - s.HtlcKeys.ReceiverInternalPubKey[:], - } - } else { - muSig2Version = input.MuSig2Version040 - signers = [][]byte{ - s.HtlcKeys.SenderInternalPubKey[1:], - s.HtlcKeys.ReceiverInternalPubKey[1:], + err := s.persistState(ctx) + if err != nil { + return err } } - htlcScript, ok := s.htlc.HtlcScript.(*swap.HtlcScriptV3) - if !ok { - return nil, fmt.Errorf("non taproot htlc") - } - - // Now we're creating a local MuSig2 session using the receiver key's - // key locator and the htlc's root hash. - musig2SessionInfo, err := s.lnd.Signer.MuSig2CreateSession( - ctx, muSig2Version, &s.HtlcKeys.ClientScriptKeyLocator, signers, - lndclient.MuSig2TaprootTweakOpt(htlcScript.RootHash[:], false), - ) - if err != nil { - return nil, err - } + return nil +} - // With the session active, we can now send the server our public nonce - // and the sig hash, so that it can create it's own MuSig2 session and - // return the server side nonce and partial signature. - serverNonce, serverSig, err := s.swapKit.server.MuSig2SignSweep( - ctx, s.SwapContract.ProtocolVersion, s.hash, - s.swapInvoicePaymentAddr, musig2SessionInfo.PublicNonce[:], - sweepTxPsbt, - ) - if err != nil { - return nil, err - } +// validateLoopOutContract validates the contract parameters against our +// request. +func validateLoopOutContract(lnd *lndclient.LndServices, request *OutRequest, + swapHash lntypes.Hash, response *newLoopOutResponse) error { - var serverPublicNonce [musig2.PubNonceSize]byte - copy(serverPublicNonce[:], serverNonce) + // Check invoice amounts. + chainParams := lnd.ChainParams - // Register the server's nonce before attempting to create our partial - // signature. - haveAllNonces, err := s.lnd.Signer.MuSig2RegisterNonces( - ctx, musig2SessionInfo.SessionID, - [][musig2.PubNonceSize]byte{serverPublicNonce}, + _, _, swapInvoiceHash, swapInvoiceAmt, err := swap.DecodeInvoice( + chainParams, response.swapInvoice, ) if err != nil { - return nil, err + return err } - // Sanity check that we have all the nonces. - if !haveAllNonces { - return nil, fmt.Errorf("invalid MuSig2 session: nonces missing") + if swapInvoiceHash != swapHash { + return fmt.Errorf( + "cannot initiate swap, swap invoice hash %v not equal "+ + "generated swap hash %v", swapInvoiceHash, swapHash) } - var digest [32]byte - copy(digest[:], sigHash) - - // Since our MuSig2 session has all nonces, we can now create the local - // partial signature by signing the sig hash. - _, err = s.lnd.Signer.MuSig2Sign( - ctx, musig2SessionInfo.SessionID, digest, false, + _, _, _, prepayInvoiceAmt, err := swap.DecodeInvoice( + chainParams, response.prepayInvoice, ) if err != nil { - return nil, err + return err } - // Now combine the partial signatures to use the final combined - // signature in the sweep transaction's witness. - haveAllSigs, finalSig, err := s.lnd.Signer.MuSig2CombineSig( - ctx, musig2SessionInfo.SessionID, [][]byte{serverSig}, - ) - if err != nil { - return nil, err - } + swapFee := swapInvoiceAmt + prepayInvoiceAmt - request.Amount + if swapFee > request.MaxSwapFee { + log.Warnf("Swap fee %v exceeding maximum of %v", + swapFee, request.MaxSwapFee) - if !haveAllSigs { - return nil, fmt.Errorf("failed to combine signatures") + return ErrSwapFeeTooHigh } - // To be sure that we're good, parse and validate that the combined - // signature is indeed valid for the sig hash and the internal pubkey. - err = s.executeConfig.verifySchnorrSig( - htlcScript.TaprootKey, sigHash, finalSig, - ) - if err != nil { - return nil, err - } + if prepayInvoiceAmt > request.MaxPrepayAmount { + log.Warnf("Prepay amount %v exceeding maximum of %v", + prepayInvoiceAmt, request.MaxPrepayAmount) - // Now that we know the signature is correct, we can fill it in to our - // witness. - sweepTx.TxIn[0].Witness = wire.TxWitness{ - finalSig, + return ErrPrepayAmountTooHigh } - return sweepTx, nil + return nil } // sweepConfTarget returns the confirmation target for the htlc sweep or false @@ -1499,215 +1349,3 @@ func (s *loopOutSwap) sweepConfTarget() (int32, bool) { return confTarget, true } - -// clampSweepFee will clamp the passed in sweep fee to the maximum configured -// miner fee. Returns false if sweeping should not continue. Note that in the -// MuSig2 case we always continue as the preimage is revealed to the server -// before cooperatively signing the sweep transaction. -func (s *loopOutSwap) clampSweepFee(fee btcutil.Amount) (btcutil.Amount, bool) { - // Ensure it doesn't exceed our maximum fee allowed. - if fee > s.MaxMinerFee { - s.log.Warnf("Required fee %v exceeds max miner fee of %v", - fee, s.MaxMinerFee) - - if s.state == loopdb.StatePreimageRevealed { - // The currently required fee exceeds the max, but we - // already revealed the preimage. The best we can do now - // is to republish with the max fee. - fee = s.MaxMinerFee - } else { - s.log.Warnf("Not revealing preimage") - return 0, false - } - } - - return fee, true -} - -// sweepMuSig2 attempts to sweep the on-chain HTLC using MuSig2. If anything -// fails, we'll log it but will simply return to allow further retries. Since -// the preimage is revealed by the time we attempt to MuSig2 sweep, we'll need -// to fall back to a script spend sweep if all MuSig2 sweep attempts fail (for -// example the server could be down due to maintenance or any other issue -// making the cooperative sweep fail). -func (s *loopOutSwap) sweepMuSig2(ctx context.Context, - htlcOutpoint wire.OutPoint, htlcValue btcutil.Amount) bool { - - addInputToEstimator := func(e *input.TxWeightEstimator) error { - e.AddTaprootKeySpendInput(txscript.SigHashDefault) - return nil - } - - confTarget, _ := s.sweepConfTarget() - fee, err := s.sweeper.GetSweepFee( - ctx, addInputToEstimator, s.DestAddr, confTarget, - ) - if err != nil { - s.log.Warnf("Failed to estimate fee MuSig2 sweep txn: %v", err) - return false - } - - fee, _ = s.clampSweepFee(fee) - - // Now attempt the co-signing of the txn. - sweepTx, err := s.createMuSig2SweepTxn( - ctx, htlcOutpoint, htlcValue, fee, - ) - if err != nil { - s.log.Warnf("Failed to create MuSig2 sweep txn: %v", err) - return false - } - - // Finally, try publish the txn. - s.log.Infof("Sweep on chain HTLC using MuSig2 to address %v "+ - "fee %v (tx %v)", s.DestAddr, fee, sweepTx.TxHash()) - - err = s.lnd.WalletKit.PublishTransaction( - ctx, sweepTx, - labels.LoopOutSweepSuccess(swap.ShortHash(&s.hash)), - ) - if err != nil { - var sweepTxBuf bytes.Buffer - if err := sweepTx.Serialize(&sweepTxBuf); err != nil { - s.log.Warnf("Unable to serialize sweep txn: %v", err) - } - - s.log.Warnf("Publish of MuSig2 sweep failed: %v. Raw tx: %x", - err, sweepTxBuf.Bytes()) - - return false - } - - return true -} - -func (s *loopOutSwap) setStatePreimageRevealed(ctx context.Context) error { - if s.state != loopdb.StatePreimageRevealed { - s.state = loopdb.StatePreimageRevealed - - err := s.persistState(ctx) - if err != nil { - return err - } - } - - return nil -} - -// sweep tries to sweep the given htlc to a destination address. It takes into -// account the max miner fee and unless the preimage is already revealed -// (MuSig2 case), marks the preimage as revealed when it published the tx. If -// the preimage has not yet been revealed, and the time during which we can -// safely reveal it has passed, the swap will be marked as failed, and the -// function will return. -func (s *loopOutSwap) sweep(ctx context.Context, htlcOutpoint wire.OutPoint, - htlcValue btcutil.Amount) error { - - confTarget, canSweep := s.sweepConfTarget() - if !canSweep { - return nil - } - - fee, err := s.sweeper.GetSweepFee( - ctx, s.htlc.AddSuccessToEstimator, s.DestAddr, confTarget, - ) - if err != nil { - return err - } - - fee, canSweep = s.clampSweepFee(fee) - if !canSweep { - return nil - } - - witnessFunc := func(sig []byte) (wire.TxWitness, error) { - return s.htlc.GenSuccessWitness(sig, s.Preimage) - } - - // Retrieve the full script required to unlock the output. - redeemScript := s.htlc.SuccessScript() - - // Create sweep tx. - sweepTx, err := s.sweeper.CreateSweepTx( - ctx, s.height, s.htlc.SuccessSequence(), s.htlc, - htlcOutpoint, s.contract.HtlcKeys.ReceiverScriptKey, - redeemScript, witnessFunc, htlcValue, fee, s.DestAddr, - ) - if err != nil { - return err - } - - // Before publishing the tx, already mark the preimage as revealed. This - // is a precaution in case the publish call never returns and would - // leave us thinking we didn't reveal yet. - err = s.setStatePreimageRevealed(ctx) - if err != nil { - return err - } - - // Publish tx. - s.log.Infof("Sweep on chain HTLC to address %v with fee %v (tx %v)", - s.DestAddr, fee, sweepTx.TxHash()) - - err = s.lnd.WalletKit.PublishTransaction( - ctx, sweepTx, - labels.LoopOutSweepSuccess(swap.ShortHash(&s.hash)), - ) - if err != nil { - var sweepTxBuf bytes.Buffer - if err := sweepTx.Serialize(&sweepTxBuf); err != nil { - s.log.Warnf("Unable to serialize sweep txn: %v", err) - } - - s.log.Warnf("Publish sweep failed: %v. Raw tx: %x", - err, sweepTxBuf.Bytes()) - } - - return nil -} - -// validateLoopOutContract validates the contract parameters against our -// request. -func validateLoopOutContract(lnd *lndclient.LndServices, request *OutRequest, - swapHash lntypes.Hash, response *newLoopOutResponse) error { - - // Check invoice amounts. - chainParams := lnd.ChainParams - - _, _, swapInvoiceHash, swapInvoiceAmt, err := swap.DecodeInvoice( - chainParams, response.swapInvoice, - ) - if err != nil { - return err - } - - if swapInvoiceHash != swapHash { - return fmt.Errorf( - "cannot initiate swap, swap invoice hash %v not equal "+ - "generated swap hash %v", swapInvoiceHash, swapHash) - } - - _, _, _, prepayInvoiceAmt, err := swap.DecodeInvoice( - chainParams, response.prepayInvoice, - ) - if err != nil { - return err - } - - swapFee := swapInvoiceAmt + prepayInvoiceAmt - request.Amount - if swapFee > request.MaxSwapFee { - log.Warnf("Swap fee %v exceeding maximum of %v", - swapFee, request.MaxSwapFee) - - return ErrSwapFeeTooHigh - } - - if prepayInvoiceAmt > request.MaxPrepayAmount { - log.Warnf("Prepay amount %v exceeding maximum of %v", - prepayInvoiceAmt, request.MaxPrepayAmount) - - return ErrPrepayAmountTooHigh - } - - return nil -} diff --git a/loopout_test.go b/loopout_test.go index 37a7458..dfbeb62 100644 --- a/loopout_test.go +++ b/loopout_test.go @@ -14,6 +14,7 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -45,7 +46,7 @@ func testLoopOutPaymentParameters(t *testing.T) { lnd := test.NewMockLnd() ctx := test.NewContext(t, lnd) server := newServerMock(lnd) - store := newStoreMock(t) + store := loopdb.NewStoreMock(t) expiryChan := make(chan time.Time) timerFactory := func(_ time.Duration) <-chan time.Time { @@ -99,7 +100,7 @@ func testLoopOutPaymentParameters(t *testing.T) { errChan <- err }() - store.assertLoopOutStored() + store.AssertLoopOutStored() state := <-statusChan require.Equal(t, loopdb.StateInitiated, state.State) @@ -168,7 +169,7 @@ func testLateHtlcPublish(t *testing.T) { server := newServerMock(lnd) - store := newStoreMock(t) + store := loopdb.NewStoreMock(t) expiryChan := make(chan time.Time) timerFactory := func(expiry time.Duration) <-chan time.Time { @@ -208,7 +209,7 @@ func testLateHtlcPublish(t *testing.T) { errChan <- err }() - store.assertLoopOutStored() + store.AssertLoopOutStored() status := <-statusChan require.Equal(t, loopdb.StateInitiated, status.State) @@ -228,7 +229,7 @@ func testLateHtlcPublish(t *testing.T) { errors.New(lndclient.PaymentResultUnknownPaymentHash), ) - store.assertStoreFinished(loopdb.StateFailTimeout) + store.AssertStoreFinished(loopdb.StateFailTimeout) status = <-statusChan require.Equal(t, loopdb.StateFailTimeout, status.State) @@ -273,7 +274,7 @@ func testCustomSweepConfTarget(t *testing.T) { ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 10000) cfg := newSwapConfig( - &lnd.LndServices, newStoreMock(t), server, + &lnd.LndServices, loopdb.NewStoreMock(t), server, ) initResult, err := newLoopOutSwap( @@ -293,13 +294,33 @@ func testCustomSweepConfTarget(t *testing.T) { 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 := swap.execute(context.Background(), &executeConfig{ + err := batcher.Run(tctx) + if err != nil { + errChan <- err + } + }() + + go func() { + err := swap.execute(tctx, &executeConfig{ statusChan: statusChan, blockEpochChan: blockEpochChan, timerFactory: timerFactory, sweeper: sweeper, + batcher: batcher, cancelSwap: server.CancelLoopOutSwap, verifySchnorrSig: mockVerifySchnorrSigFail, }, ctx.Lnd.Height) @@ -310,7 +331,7 @@ func testCustomSweepConfTarget(t *testing.T) { }() // The swap should be found in its initial state. - cfg.store.(*storeMock).assertLoopOutStored() + cfg.store.(*loopdb.StoreMock).AssertLoopOutStored() state := <-statusChan require.Equal(t, loopdb.StateInitiated, state.State) @@ -335,22 +356,27 @@ func testCustomSweepConfTarget(t *testing.T) { 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 // preimage push tracking. trackPayment := ctx.AssertTrackPayment() 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. if !IsTaprootSwap(&swap.SwapContract) { <-ctx.Lnd.SignOutputRawChannel } - cfg.store.(*storeMock).assertLoopOutState(loopdb.StatePreimageRevealed) + cfg.store.(*loopdb.StoreMock).AssertLoopOutState(loopdb.StatePreimageRevealed) status := <-statusChan 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 // confirmation target. - _ = assertSweepTx(testReq.SweepConfTarget) + sweepTx := assertSweepTx(testReq.SweepConfTarget) // Once we have published an on chain sweep, we expect a preimage to // have been pushed to our server. @@ -426,24 +452,14 @@ func testCustomSweepConfTarget(t *testing.T) { State: lnrpc.Payment_SUCCEEDED, } - // We'll then notify the height at which we begin using the default - // 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. + // Notify the batch for the spend. 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 require.Equal(t, loopdb.StateSuccess, status.State) require.NoError(t, <-errChan) @@ -493,7 +509,7 @@ func testPreimagePush(t *testing.T) { ) cfg := newSwapConfig( - &lnd.LndServices, newStoreMock(t), server, + &lnd.LndServices, loopdb.NewStoreMock(t), server, ) initResult, err := newLoopOutSwap( @@ -511,13 +527,33 @@ func testPreimagePush(t *testing.T) { 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() { err := swap.execute(context.Background(), &executeConfig{ statusChan: statusChan, blockEpochChan: blockEpochChan, timerFactory: timerFactory, sweeper: sweeper, + batcher: batcher, cancelSwap: server.CancelLoopOutSwap, verifySchnorrSig: mockVerifySchnorrSigFail, }, ctx.Lnd.Height) @@ -528,7 +564,7 @@ func testPreimagePush(t *testing.T) { }() // The swap should be found in its initial state. - cfg.store.(*storeMock).assertLoopOutStored() + cfg.store.(*loopdb.StoreMock).AssertLoopOutStored() state := <-statusChan require.Equal(t, loopdb.StateInitiated, state.State) @@ -553,10 +589,6 @@ func testPreimagePush(t *testing.T) { 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 // preimage push tracking. trackPayment := ctx.AssertTrackPayment() @@ -567,11 +599,20 @@ func testPreimagePush(t *testing.T) { // preimage is not revealed, we also do not expect a preimage push. 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 // preimage before sweeping in order for the server to trust us with // our MuSig2 signing attempts. if IsTaprootSwap(&swap.SwapContract) { - cfg.store.(*storeMock).assertLoopOutState( + cfg.store.(*loopdb.StoreMock).AssertLoopOutState( loopdb.StatePreimageRevealed, ) status := <-statusChan @@ -582,15 +623,6 @@ func testPreimagePush(t *testing.T) { preimage := <-server.preimagePush require.Equal(t, swap.Preimage, preimage) - // Try MuSig2 signing first and fail it so that we go for a - // normal sweep. - for i := 0; i < maxMusigSweepRetries; i++ { - expiryChan <- time.Now() - - preimage := <-server.preimagePush - require.Equal(t, swap.Preimage, preimage) - } - <-ctx.Lnd.SignOutputRawChannel // We expect the sweep tx to have been published. @@ -611,6 +643,10 @@ func testPreimagePush(t *testing.T) { // 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. blockEpochChan <- ctx.Lnd.Height + 2 + + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2) + require.NoError(t, err) + expiryChan <- testTime if IsTaprootSwap(&swap.SwapContract) { @@ -624,7 +660,7 @@ func testPreimagePush(t *testing.T) { if !IsTaprootSwap(&swap.SwapContract) { // This is the first time we have swept, so we expect our // preimage revealed state to be set. - cfg.store.(*storeMock).assertLoopOutState( + cfg.store.(*loopdb.StoreMock).AssertLoopOutState( loopdb.StatePreimageRevealed, ) status := <-statusChan @@ -648,6 +684,10 @@ func testPreimagePush(t *testing.T) { // chain yet so we can test our preimage push retry logic. Instead, we // tick the expiry chan again to prompt another sweep. expiryChan <- testTime + + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2) + require.NoError(t, err) + if IsTaprootSwap(&swap.SwapContract) { preimage := <-server.preimagePush 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 // would hang if we pushed the preimage here. expiryChan <- testTime + + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2) + require.NoError(t, err) + <-ctx.Lnd.SignOutputRawChannel sweepTx := ctx.ReceiveTx() @@ -685,7 +729,11 @@ func testPreimagePush(t *testing.T) { // spend our sweepTx and assert that the swap succeeds. 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 require.Equal( t, status.State, loopdb.StateSuccess, @@ -720,7 +768,7 @@ func testFailedOffChainCancelation(t *testing.T) { testReq.Expiry = lnd.Height + 20 cfg := newSwapConfig( - &lnd.LndServices, newStoreMock(t), server, + &lnd.LndServices, loopdb.NewStoreMock(t), server, ) initResult, err := newLoopOutSwap( @@ -754,7 +802,7 @@ func testFailedOffChainCancelation(t *testing.T) { }() // The swap should be found in its initial state. - cfg.store.(*storeMock).assertLoopOutStored() + cfg.store.(*loopdb.StoreMock).AssertLoopOutStored() state := <-statusChan require.Equal(t, loopdb.StateInitiated, state.State) @@ -837,7 +885,7 @@ func testFailedOffChainCancelation(t *testing.T) { server.assertSwapCanceled(t, swapCancelation) // Finally, the swap should be recorded with failed off chain timeout. - cfg.store.(*storeMock).assertLoopOutState( + cfg.store.(*loopdb.StoreMock).AssertLoopOutState( loopdb.StateFailOffchainPayments, ) state = <-statusChan @@ -874,7 +922,7 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { ) cfg := newSwapConfig( - &lnd.LndServices, newStoreMock(t), server, + &lnd.LndServices, loopdb.NewStoreMock(t), server, ) initResult, err := newLoopOutSwap( @@ -892,8 +940,6 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { return expiryChan } - errChan := make(chan error) - // Mock a successful signature verify to make sure we don't fail // creating the MuSig2 sweep. mockVerifySchnorrSigSuccess := func(pubKey *btcec.PublicKey, hash, @@ -902,12 +948,33 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { 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() { err := swap.execute(context.Background(), &executeConfig{ statusChan: statusChan, blockEpochChan: blockEpochChan, timerFactory: timerFactory, sweeper: sweeper, + batcher: batcher, cancelSwap: server.CancelLoopOutSwap, verifySchnorrSig: mockVerifySchnorrSigSuccess, }, ctx.Lnd.Height) @@ -918,7 +985,7 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { }() // The swap should be found in its initial state. - cfg.store.(*storeMock).assertLoopOutStored() + cfg.store.(*loopdb.StoreMock).AssertLoopOutStored() state := <-statusChan require.Equal(t, loopdb.StateInitiated, state.State) @@ -943,10 +1010,6 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { 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 // preimage push tracking. trackPayment := ctx.AssertTrackPayment() @@ -957,10 +1020,19 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { // preimage is not revealed, we also do not expect a preimage push. 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 // preimage before sweeping in order for the server to trust us with // our MuSig2 signing attempts. - cfg.store.(*storeMock).assertLoopOutState( + cfg.store.(*loopdb.StoreMock).AssertLoopOutState( loopdb.StatePreimageRevealed, ) 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 // fees are acceptably low so we expect our sweep to be published. blockEpochChan <- ctx.Lnd.Height + 2 + + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2) + require.NoError(t, err) + expiryChan <- testTime preimage = <-server.preimagePush @@ -1010,7 +1086,11 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { // spend our sweepTx and assert that the swap succeeds. 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 require.Equal(t, status.State, loopdb.StateSuccess) require.NoError(t, <-errChan) diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index f1b6cba..b049f8c 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -608,6 +608,11 @@ type LoopOutRequest struct { // //The address type of the account specified in the account field. AccountAddrType AddressType `protobuf:"varint,16,opt,name=account_addr_type,json=accountAddrType,proto3,enum=looprpc.AddressType" json:"account_addr_type,omitempty"` + // + //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. + IsExternalAddr bool `protobuf:"varint,17,opt,name=is_external_addr,json=isExternalAddr,proto3" json:"is_external_addr,omitempty"` } func (x *LoopOutRequest) Reset() { @@ -755,6 +760,13 @@ func (x *LoopOutRequest) GetAccountAddrType() AddressType { return AddressType_ADDRESS_TYPE_UNKNOWN } +func (x *LoopOutRequest) GetIsExternalAddr() bool { + if x != nil { + return x.IsExternalAddr + } + return false +} + type LoopInRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -3582,7 +3594,7 @@ var file_client_proto_rawDesc = []byte{ 0x0a, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x1a, 0x1a, 0x73, 0x77, 0x61, 0x70, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0x89, 0x05, 0x0a, 0x0e, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, + 0x6f, 0x74, 0x6f, 0x22, 0xb3, 0x05, 0x0a, 0x0e, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x6d, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x14, @@ -3622,535 +3634,537 @@ var file_client_proto_rawDesc = []byte{ 0x11, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0f, - 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x54, 0x79, 0x70, 0x65, 0x22, - 0xd4, 0x02, 0x0a, 0x0d, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, - 0x61, 0x6d, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, - 0x66, 0x65, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x53, 0x77, - 0x61, 0x70, 0x46, 0x65, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x5f, 0x6d, 0x69, 0x6e, - 0x65, 0x72, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x61, - 0x78, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x46, 0x65, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x61, 0x73, - 0x74, 0x5f, 0x68, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6c, 0x61, 0x73, - 0x74, 0x48, 0x6f, 0x70, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x65, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x48, 0x74, 0x6c, 0x63, 0x12, 0x28, 0x0a, 0x10, 0x68, 0x74, 0x6c, - 0x63, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x0e, 0x68, 0x74, 0x6c, 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x54, 0x61, 0x72, - 0x67, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x69, - 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x6e, - 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x0b, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, - 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, - 0x52, 0x0a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x0a, 0x07, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0xeb, 0x01, 0x0a, 0x0c, 0x53, 0x77, 0x61, 0x70, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, - 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x69, - 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0c, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, - 0x52, 0x0b, 0x68, 0x74, 0x6c, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2c, 0x0a, - 0x12, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x70, 0x32, - 0x77, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x68, 0x74, 0x6c, 0x63, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x50, 0x32, 0x77, 0x73, 0x68, 0x12, 0x2a, 0x0a, 0x11, 0x68, - 0x74, 0x6c, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x70, 0x32, 0x74, 0x72, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x74, 0x6c, 0x63, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x50, 0x32, 0x74, 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4a, 0x04, - 0x08, 0x04, 0x10, 0x05, 0x22, 0x10, 0x0a, 0x0e, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xf7, 0x04, 0x0a, 0x0a, 0x53, 0x77, 0x61, 0x70, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x03, 0x61, 0x6d, 0x74, 0x12, 0x12, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, - 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x69, - 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x6c, - 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3d, 0x0a, 0x0e, 0x66, 0x61, 0x69, 0x6c, 0x75, - 0x72, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, - 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x0d, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, - 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0e, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x28, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0c, 0x68, 0x74, 0x6c, - 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x02, 0x18, 0x01, 0x52, 0x0b, 0x68, 0x74, 0x6c, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x2c, 0x0a, 0x12, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x5f, 0x70, 0x32, 0x77, 0x73, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x68, 0x74, - 0x6c, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x50, 0x32, 0x77, 0x73, 0x68, 0x12, 0x2a, - 0x0a, 0x11, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x70, - 0x32, 0x74, 0x72, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x74, 0x6c, 0x63, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x50, 0x32, 0x74, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, - 0x73, 0x74, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0a, 0x63, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x63, - 0x6f, 0x73, 0x74, 0x5f, 0x6f, 0x6e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0b, 0x63, 0x6f, 0x73, 0x74, 0x4f, 0x6e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x23, - 0x0a, 0x0d, 0x63, 0x6f, 0x73, 0x74, 0x5f, 0x6f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x63, 0x6f, 0x73, 0x74, 0x4f, 0x66, 0x66, 0x63, 0x68, - 0x61, 0x69, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x6f, 0x70, 0x18, - 0x10, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x6f, 0x70, 0x12, 0x2a, - 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x5f, - 0x73, 0x65, 0x74, 0x18, 0x11, 0x20, 0x03, 0x28, 0x04, 0x52, 0x0f, 0x6f, 0x75, 0x74, 0x67, 0x6f, - 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, - 0x62, 0x65, 0x6c, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, - 0x22, 0x56, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x42, 0x0a, 0x10, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x73, 0x77, 0x61, - 0x70, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, - 0x70, 0x73, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x0e, 0x6c, 0x69, 0x73, 0x74, 0x53, 0x77, - 0x61, 0x70, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x9b, 0x02, 0x0a, 0x0f, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x44, 0x0a, 0x09, - 0x73, 0x77, 0x61, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x27, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, - 0x61, 0x70, 0x73, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, - 0x70, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x6f, 0x6e, - 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, - 0x67, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, - 0x67, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x04, - 0x52, 0x0f, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x65, - 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x27, 0x0a, 0x10, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, - 0x69, 0x6e, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x6f, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x0d, 0x6c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x4c, 0x61, 0x73, 0x74, 0x48, 0x6f, 0x70, - 0x22, 0x34, 0x0a, 0x0e, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x46, 0x69, 0x6c, 0x74, - 0x65, 0x72, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4e, 0x59, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4c, - 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x4c, 0x4f, 0x4f, - 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x02, 0x22, 0x3e, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, - 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x05, 0x73, - 0x77, 0x61, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6f, 0x6f, - 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, - 0x05, 0x73, 0x77, 0x61, 0x70, 0x73, 0x22, 0x21, 0x0a, 0x0f, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x22, 0x0e, 0x0a, 0x0c, 0x54, 0x65, 0x72, - 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x7f, 0x0a, 0x0f, 0x49, 0x6e, 0x54, - 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x0f, - 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6d, 0x69, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x77, 0x61, 0x70, - 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6d, - 0x61, 0x78, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x4a, 0x04, 0x08, 0x01, - 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, - 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xcc, 0x01, 0x0a, 0x10, 0x4f, - 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x26, 0x0a, 0x0f, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6d, 0x69, 0x6e, 0x53, 0x77, 0x61, - 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x61, 0x78, 0x5f, 0x73, - 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x24, 0x0a, 0x0e, 0x6d, 0x69, 0x6e, 0x5f, 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, - 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6d, 0x69, 0x6e, 0x43, 0x6c, 0x74, 0x76, - 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x6c, 0x74, - 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6d, - 0x61, 0x78, 0x43, 0x6c, 0x74, 0x76, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x4a, 0x04, 0x08, 0x01, 0x10, - 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, - 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xa8, 0x02, 0x0a, 0x0c, 0x51, 0x75, - 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x6d, 0x74, 0x12, 0x1f, 0x0a, 0x0b, - 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x23, 0x0a, - 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x48, 0x74, - 0x6c, 0x63, 0x12, 0x3a, 0x0a, 0x19, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x17, 0x73, 0x77, 0x61, 0x70, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x27, - 0x0a, 0x10, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x69, 0x6e, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, - 0x6f, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x6c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, - 0x4c, 0x61, 0x73, 0x74, 0x48, 0x6f, 0x70, 0x12, 0x41, 0x0a, 0x13, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, - 0x69, 0x6e, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x06, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x52, 0x10, 0x6c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x0f, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x73, 0x77, 0x61, 0x70, - 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, - 0x73, 0x77, 0x61, 0x70, 0x46, 0x65, 0x65, 0x53, 0x61, 0x74, 0x12, 0x2f, 0x0a, 0x14, 0x68, 0x74, - 0x6c, 0x63, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, - 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x68, 0x74, 0x6c, 0x63, 0x50, 0x75, - 0x62, 0x6c, 0x69, 0x73, 0x68, 0x46, 0x65, 0x65, 0x53, 0x61, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, - 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x09, 0x63, 0x6c, 0x74, 0x76, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, - 0x6e, 0x66, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4a, 0x04, 0x08, 0x02, 0x10, - 0x03, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0xf3, 0x01, 0x0a, 0x10, 0x4f, 0x75, 0x74, 0x51, - 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0c, - 0x73, 0x77, 0x61, 0x70, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0a, 0x73, 0x77, 0x61, 0x70, 0x46, 0x65, 0x65, 0x53, 0x61, 0x74, 0x12, 0x24, - 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x70, 0x61, 0x79, 0x5f, 0x61, 0x6d, 0x74, 0x5f, 0x73, 0x61, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x70, 0x61, 0x79, 0x41, 0x6d, - 0x74, 0x53, 0x61, 0x74, 0x12, 0x2b, 0x0a, 0x12, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x73, 0x77, 0x65, - 0x65, 0x70, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0f, 0x68, 0x74, 0x6c, 0x63, 0x53, 0x77, 0x65, 0x65, 0x70, 0x46, 0x65, 0x65, 0x53, 0x61, - 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x73, 0x77, - 0x61, 0x70, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x09, 0x63, 0x6c, 0x74, 0x76, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x1f, 0x0a, 0x0b, - 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0x70, 0x0a, - 0x0c, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, - 0x03, 0x61, 0x6d, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x6d, 0x74, 0x12, - 0x19, 0x0a, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x6f, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x07, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x6f, 0x70, 0x12, 0x33, 0x0a, 0x0b, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x12, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, - 0x69, 0x6e, 0x74, 0x52, 0x0a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x73, 0x22, - 0x0f, 0x0a, 0x0d, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x0f, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0x3c, 0x0a, 0x0e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x73, - 0x61, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, - 0xcb, 0x02, 0x0a, 0x09, 0x4c, 0x73, 0x61, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x23, 0x0a, - 0x0d, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x62, 0x61, 0x73, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, - 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, - 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x29, 0x0a, 0x10, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x5f, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x0f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, - 0x12, 0x28, 0x0a, 0x10, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x69, 0x64, 0x5f, - 0x6d, 0x73, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x61, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x50, 0x61, 0x69, 0x64, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x31, 0x0a, 0x15, 0x72, 0x6f, - 0x75, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x70, 0x61, 0x69, 0x64, 0x5f, 0x6d, - 0x73, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, 0x6f, 0x75, 0x74, 0x69, - 0x6e, 0x67, 0x46, 0x65, 0x65, 0x50, 0x61, 0x69, 0x64, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x21, 0x0a, - 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x69, 0x6d, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, - 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xc8, 0x01, - 0x0a, 0x09, 0x4c, 0x6f, 0x6f, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x70, - 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x0c, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x61, 0x69, 0x6c, 0x5f, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x66, 0x61, 0x69, 0x6c, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x75, 0x6d, 0x5f, 0x70, 0x65, 0x6e, 0x64, - 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6d, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x73, - 0x75, 0x6d, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x6d, 0x74, 0x12, 0x2a, 0x0a, 0x11, - 0x73, 0x75, 0x6d, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x6d, - 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x73, 0x75, 0x6d, 0x53, 0x75, 0x63, 0x63, - 0x65, 0x65, 0x64, 0x65, 0x64, 0x41, 0x6d, 0x74, 0x22, 0x10, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc0, 0x02, 0x0a, 0x0f, 0x47, - 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, - 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x70, 0x63, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x70, 0x63, 0x4c, 0x69, 0x73, 0x74, 0x65, - 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x74, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x74, 0x4c, 0x69, 0x73, 0x74, - 0x65, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x5f, 0x70, - 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x63, 0x61, 0x72, - 0x6f, 0x6f, 0x6e, 0x50, 0x61, 0x74, 0x68, 0x12, 0x22, 0x0a, 0x0d, 0x74, 0x6c, 0x73, 0x5f, 0x63, - 0x65, 0x72, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x74, 0x6c, 0x73, 0x43, 0x65, 0x72, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x38, 0x0a, 0x0e, 0x6c, - 0x6f, 0x6f, 0x70, 0x5f, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, - 0x6f, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x0c, 0x6c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, - 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x36, 0x0a, 0x0d, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x69, 0x6e, - 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, - 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, - 0x52, 0x0b, 0x6c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x22, 0x1b, 0x0a, - 0x19, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, - 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x94, 0x09, 0x0a, 0x13, 0x4c, - 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, - 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, - 0x12, 0x17, 0x0a, 0x07, 0x66, 0x65, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x10, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x06, 0x66, 0x65, 0x65, 0x50, 0x70, 0x6d, 0x12, 0x3d, 0x0a, 0x1c, 0x73, 0x77, 0x65, - 0x65, 0x70, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x61, 0x74, 0x5f, - 0x70, 0x65, 0x72, 0x5f, 0x76, 0x62, 0x79, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x17, 0x73, 0x77, 0x65, 0x65, 0x70, 0x46, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x53, 0x61, 0x74, - 0x50, 0x65, 0x72, 0x56, 0x62, 0x79, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, - 0x73, 0x77, 0x61, 0x70, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x53, 0x77, 0x61, 0x70, 0x46, 0x65, 0x65, 0x50, 0x70, - 0x6d, 0x12, 0x2d, 0x0a, 0x13, 0x6d, 0x61, 0x78, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, - 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, - 0x6d, 0x61, 0x78, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x46, 0x65, 0x65, 0x50, 0x70, 0x6d, - 0x12, 0x3a, 0x0a, 0x1a, 0x6d, 0x61, 0x78, 0x5f, 0x70, 0x72, 0x65, 0x70, 0x61, 0x79, 0x5f, 0x72, - 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x16, 0x6d, 0x61, 0x78, 0x50, 0x72, 0x65, 0x70, 0x61, 0x79, 0x52, - 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x46, 0x65, 0x65, 0x50, 0x70, 0x6d, 0x12, 0x24, 0x0a, 0x0e, - 0x6d, 0x61, 0x78, 0x5f, 0x70, 0x72, 0x65, 0x70, 0x61, 0x79, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x50, 0x72, 0x65, 0x70, 0x61, 0x79, 0x53, - 0x61, 0x74, 0x12, 0x29, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x5f, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x5f, - 0x66, 0x65, 0x65, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x6d, - 0x61, 0x78, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x46, 0x65, 0x65, 0x53, 0x61, 0x74, 0x12, 0x2a, 0x0a, - 0x11, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x73, 0x77, 0x65, 0x65, 0x70, 0x43, - 0x6f, 0x6e, 0x66, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x61, 0x69, - 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, 0x5f, 0x73, 0x65, 0x63, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x42, - 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, 0x53, 0x65, 0x63, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, 0x74, - 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x75, 0x74, - 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x12, 0x2e, 0x0a, 0x13, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, - 0x70, 0x5f, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x11, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x42, 0x75, 0x64, 0x67, - 0x65, 0x74, 0x53, 0x61, 0x74, 0x12, 0x3d, 0x0a, 0x19, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, - 0x70, 0x5f, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x73, - 0x65, 0x63, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x18, 0x01, 0x52, 0x16, 0x61, 0x75, - 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x53, 0x74, 0x61, 0x72, - 0x74, 0x53, 0x65, 0x63, 0x12, 0x2b, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x6d, 0x61, 0x78, - 0x5f, 0x69, 0x6e, 0x5f, 0x66, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x0f, 0x61, 0x75, 0x74, 0x6f, 0x4d, 0x61, 0x78, 0x49, 0x6e, 0x46, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x69, 0x6e, 0x53, - 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x61, 0x78, - 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, - 0x74, 0x12, 0x28, 0x0a, 0x10, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x68, 0x74, 0x6c, - 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x61, - 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x5f, 0x61, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x61, 0x75, 0x74, 0x6f, - 0x6c, 0x6f, 0x6f, 0x70, 0x44, 0x65, 0x73, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, - 0x4a, 0x0a, 0x22, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x62, 0x75, 0x64, 0x67, - 0x65, 0x74, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x70, 0x65, 0x72, 0x69, 0x6f, - 0x64, 0x5f, 0x73, 0x65, 0x63, 0x18, 0x13, 0x20, 0x01, 0x28, 0x04, 0x52, 0x1e, 0x61, 0x75, 0x74, - 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, - 0x73, 0x68, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x53, 0x65, 0x63, 0x12, 0x3f, 0x0a, 0x1c, 0x61, - 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x5f, 0x6c, - 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x18, 0x14, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x19, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x42, 0x75, 0x64, 0x67, 0x65, - 0x74, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x23, 0x0a, 0x0d, - 0x65, 0x61, 0x73, 0x79, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x18, 0x15, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0c, 0x65, 0x61, 0x73, 0x79, 0x41, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, - 0x70, 0x12, 0x42, 0x0a, 0x1e, 0x65, 0x61, 0x73, 0x79, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, - 0x6f, 0x70, 0x5f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, - 0x73, 0x61, 0x74, 0x18, 0x16, 0x20, 0x01, 0x28, 0x04, 0x52, 0x1a, 0x65, 0x61, 0x73, 0x79, 0x41, - 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x54, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x53, 0x61, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x17, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x40, 0x0a, 0x11, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x5f, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x18, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x6c, 0x6f, 0x6f, - 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, - 0x52, 0x0f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x54, 0x79, 0x70, - 0x65, 0x22, 0x84, 0x02, 0x0a, 0x0d, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, - 0x75, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, - 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, - 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x79, - 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, - 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x69, 0x6e, - 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, - 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x6f, 0x75, 0x74, - 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x54, - 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22, 0x59, 0x0a, 0x19, 0x53, 0x65, 0x74, 0x4c, - 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, - 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, - 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x73, 0x22, 0x1c, 0x0a, 0x1a, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, - 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x72, 0x0a, 0x0c, 0x44, 0x69, 0x73, 0x71, - 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, - 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, - 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, - 0x2b, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x13, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x52, 0x65, - 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0xb6, 0x01, 0x0a, - 0x14, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x08, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x6f, 0x75, - 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, - 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x52, 0x07, 0x6c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6c, 0x6f, 0x6f, - 0x70, 0x5f, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x28, 0x0a, 0x10, 0x69, 0x73, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, + 0x64, 0x64, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, 0x73, 0x45, 0x78, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x22, 0xd4, 0x02, 0x0a, 0x0d, 0x4c, 0x6f, + 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, + 0x6d, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x6d, 0x74, 0x12, 0x20, 0x0a, + 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x53, 0x77, 0x61, 0x70, 0x46, 0x65, 0x65, 0x12, + 0x22, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x5f, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x66, 0x65, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x4d, 0x69, 0x6e, 0x65, 0x72, + 0x46, 0x65, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x6f, 0x70, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x6f, 0x70, 0x12, 0x23, + 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x48, + 0x74, 0x6c, 0x63, 0x12, 0x28, 0x0a, 0x10, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x63, 0x6f, 0x6e, 0x66, + 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x68, + 0x74, 0x6c, 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x14, 0x0a, + 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, + 0x62, 0x65, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, + 0x72, 0x12, 0x33, 0x0a, 0x0b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x73, + 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x52, 0x0a, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x48, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x22, 0xeb, 0x01, 0x0a, 0x0c, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x12, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, + 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x69, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, + 0x12, 0x25, 0x0a, 0x0c, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0b, 0x68, 0x74, 0x6c, 0x63, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x68, 0x74, 0x6c, 0x63, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x70, 0x32, 0x77, 0x73, 0x68, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x10, 0x68, 0x74, 0x6c, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x50, 0x32, 0x77, 0x73, 0x68, 0x12, 0x2a, 0x0a, 0x11, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x70, 0x32, 0x74, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0f, 0x68, 0x74, 0x6c, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x50, 0x32, 0x74, + 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x10, + 0x0a, 0x0e, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x22, 0xf7, 0x04, 0x0a, 0x0a, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x6d, + 0x74, 0x12, 0x12, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, + 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x69, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, + 0x12, 0x25, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, + 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x3d, 0x0a, 0x0e, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x52, 0x0d, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x69, 0x6e, 0x69, 0x74, 0x69, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x6c, 0x61, 0x73, + 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0c, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0b, 0x68, + 0x74, 0x6c, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x68, 0x74, + 0x6c, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x70, 0x32, 0x77, 0x73, 0x68, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x68, 0x74, 0x6c, 0x63, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x50, 0x32, 0x77, 0x73, 0x68, 0x12, 0x2a, 0x0a, 0x11, 0x68, 0x74, 0x6c, 0x63, + 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x70, 0x32, 0x74, 0x72, 0x18, 0x12, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x74, 0x6c, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x50, 0x32, 0x74, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x63, 0x6f, 0x73, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x73, 0x74, 0x5f, 0x6f, 0x6e, + 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x6f, 0x73, + 0x74, 0x4f, 0x6e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x73, 0x74, + 0x5f, 0x6f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0c, 0x63, 0x6f, 0x73, 0x74, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x0a, + 0x08, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x6f, 0x70, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x07, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x6f, 0x70, 0x12, 0x2a, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x67, + 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x11, 0x20, + 0x03, 0x28, 0x04, 0x52, 0x0f, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, + 0x6e, 0x53, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x0f, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x56, 0x0a, 0x10, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x42, + 0x0a, 0x10, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, + 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x52, 0x0e, 0x6c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x22, 0x9b, 0x02, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, + 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x44, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x46, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x0c, + 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0b, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x6e, 0x6c, 0x79, 0x12, + 0x2a, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x68, 0x61, 0x6e, + 0x5f, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x04, 0x52, 0x0f, 0x6f, 0x75, 0x74, 0x67, + 0x6f, 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x12, 0x27, 0x0a, 0x10, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x69, 0x6e, 0x5f, 0x6c, 0x61, 0x73, + 0x74, 0x5f, 0x68, 0x6f, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x6c, 0x6f, 0x6f, + 0x70, 0x49, 0x6e, 0x4c, 0x61, 0x73, 0x74, 0x48, 0x6f, 0x70, 0x22, 0x34, 0x0a, 0x0e, 0x53, 0x77, + 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x07, 0x0a, 0x03, + 0x41, 0x4e, 0x59, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, + 0x54, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x02, + 0x22, 0x3e, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x05, 0x73, 0x77, 0x61, 0x70, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, + 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x05, 0x73, 0x77, 0x61, 0x70, 0x73, + 0x22, 0x21, 0x0a, 0x0f, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x02, 0x69, 0x64, 0x22, 0x0e, 0x0a, 0x0c, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x22, 0x7f, 0x0a, 0x0f, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x77, + 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0d, 0x6d, 0x69, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, + 0x0a, 0x0f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x53, 0x77, 0x61, 0x70, + 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, + 0x10, 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, + 0x08, 0x07, 0x10, 0x08, 0x22, 0xcc, 0x01, 0x0a, 0x10, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x69, 0x6e, + 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0d, 0x6d, 0x69, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x53, + 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x69, 0x6e, + 0x5f, 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x0c, 0x6d, 0x69, 0x6e, 0x43, 0x6c, 0x74, 0x76, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, + 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, + 0x61, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x43, 0x6c, 0x74, 0x76, + 0x44, 0x65, 0x6c, 0x74, 0x61, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, + 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, + 0x07, 0x10, 0x08, 0x22, 0xa8, 0x02, 0x0a, 0x0c, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x03, 0x61, 0x6d, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x63, 0x6f, 0x6e, + 0x66, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, + 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x48, 0x74, 0x6c, 0x63, 0x12, 0x3a, 0x0a, 0x19, + 0x73, 0x77, 0x61, 0x70, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x64, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x17, 0x73, 0x77, 0x61, 0x70, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x44, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x27, 0x0a, 0x10, 0x6c, 0x6f, 0x6f, 0x70, + 0x5f, 0x69, 0x6e, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x6f, 0x70, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0d, 0x6c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x4c, 0x61, 0x73, 0x74, 0x48, 0x6f, + 0x70, 0x12, 0x41, 0x0a, 0x13, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x69, 0x6e, 0x5f, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, + 0x6e, 0x74, 0x52, 0x10, 0x6c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, + 0x69, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0xb0, + 0x01, 0x0a, 0x0f, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, + 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x73, 0x77, 0x61, 0x70, 0x46, 0x65, + 0x65, 0x53, 0x61, 0x74, 0x12, 0x2f, 0x0a, 0x14, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x11, 0x68, 0x74, 0x6c, 0x63, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x46, + 0x65, 0x65, 0x53, 0x61, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, + 0x6c, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x63, 0x6c, 0x74, 0x76, 0x44, + 0x65, 0x6c, 0x74, 0x61, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x54, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x04, 0x10, + 0x05, 0x22, 0xf3, 0x01, 0x0a, 0x10, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x66, + 0x65, 0x65, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x73, 0x77, + 0x61, 0x70, 0x46, 0x65, 0x65, 0x53, 0x61, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x70, + 0x61, 0x79, 0x5f, 0x61, 0x6d, 0x74, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0c, 0x70, 0x72, 0x65, 0x70, 0x61, 0x79, 0x41, 0x6d, 0x74, 0x53, 0x61, 0x74, 0x12, 0x2b, + 0x0a, 0x12, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x66, 0x65, 0x65, + 0x5f, 0x73, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x68, 0x74, 0x6c, 0x63, + 0x53, 0x77, 0x65, 0x65, 0x70, 0x46, 0x65, 0x65, 0x53, 0x61, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x73, + 0x77, 0x61, 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x65, 0x73, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x73, 0x77, 0x61, 0x70, 0x50, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x44, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x74, 0x76, 0x5f, + 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x63, 0x6c, 0x74, + 0x76, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x63, 0x6f, 0x6e, + 0x66, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x62, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x6d, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x61, 0x73, + 0x74, 0x5f, 0x68, 0x6f, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6c, 0x61, 0x73, + 0x74, 0x48, 0x6f, 0x70, 0x12, 0x33, 0x0a, 0x0b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x68, 0x69, + 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x52, 0x0a, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x50, 0x72, 0x6f, + 0x62, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3c, 0x0a, 0x0e, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, + 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x73, 0x61, 0x74, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, 0xcb, 0x02, 0x0a, 0x09, 0x4c, 0x73, + 0x61, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x62, 0x61, 0x73, 0x65, 0x5f, + 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, + 0x62, 0x61, 0x73, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, + 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, + 0x29, 0x0a, 0x10, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x72, 0x65, 0x69, 0x6d, + 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x61, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x69, 0x64, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x69, 0x64, + 0x4d, 0x73, 0x61, 0x74, 0x12, 0x31, 0x0a, 0x15, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x5f, + 0x66, 0x65, 0x65, 0x5f, 0x70, 0x61, 0x69, 0x64, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x46, 0x65, 0x65, 0x50, + 0x61, 0x69, 0x64, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, + 0x69, 0x6d, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xc8, 0x01, 0x0a, 0x09, 0x4c, 0x6f, 0x6f, 0x70, + 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x70, 0x65, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x0c, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x1d, 0x0a, 0x0a, 0x66, 0x61, 0x69, 0x6c, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x09, 0x66, 0x61, 0x69, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, + 0x0a, 0x0f, 0x73, 0x75, 0x6d, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6d, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x73, 0x75, 0x6d, 0x50, 0x65, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x41, 0x6d, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x75, 0x6d, 0x5f, 0x73, 0x75, + 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x6d, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0f, 0x73, 0x75, 0x6d, 0x53, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x41, + 0x6d, 0x74, 0x22, 0x10, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0xc0, 0x02, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x1d, 0x0a, 0x0a, + 0x72, 0x70, 0x63, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x72, 0x70, 0x63, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x72, + 0x65, 0x73, 0x74, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x72, 0x65, 0x73, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x12, 0x23, 0x0a, 0x0d, + 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x61, 0x74, + 0x68, 0x12, 0x22, 0x0a, 0x0d, 0x74, 0x6c, 0x73, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x70, 0x61, + 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x74, 0x6c, 0x73, 0x43, 0x65, 0x72, + 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x38, 0x0a, 0x0e, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x6f, 0x75, + 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x52, 0x0c, 0x6c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, + 0x36, 0x0a, 0x0d, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x0b, 0x6c, 0x6f, 0x6f, 0x70, + 0x49, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x22, 0x1b, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4c, 0x69, + 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x94, 0x09, 0x0a, 0x13, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, + 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x05, + 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x6f, + 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, + 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x65, + 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x10, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x66, 0x65, 0x65, + 0x50, 0x70, 0x6d, 0x12, 0x3d, 0x0a, 0x1c, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x66, 0x65, 0x65, + 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x76, 0x62, + 0x79, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x17, 0x73, 0x77, 0x65, 0x65, 0x70, + 0x46, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x56, 0x62, 0x79, + 0x74, 0x65, 0x12, 0x27, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x66, + 0x65, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x61, + 0x78, 0x53, 0x77, 0x61, 0x70, 0x46, 0x65, 0x65, 0x50, 0x70, 0x6d, 0x12, 0x2d, 0x0a, 0x13, 0x6d, + 0x61, 0x78, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x70, + 0x70, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x52, 0x6f, 0x75, + 0x74, 0x69, 0x6e, 0x67, 0x46, 0x65, 0x65, 0x50, 0x70, 0x6d, 0x12, 0x3a, 0x0a, 0x1a, 0x6d, 0x61, + 0x78, 0x5f, 0x70, 0x72, 0x65, 0x70, 0x61, 0x79, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, + 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x16, + 0x6d, 0x61, 0x78, 0x50, 0x72, 0x65, 0x70, 0x61, 0x79, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, + 0x46, 0x65, 0x65, 0x50, 0x70, 0x6d, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x70, 0x72, + 0x65, 0x70, 0x61, 0x79, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, + 0x6d, 0x61, 0x78, 0x50, 0x72, 0x65, 0x70, 0x61, 0x79, 0x53, 0x61, 0x74, 0x12, 0x29, 0x0a, 0x11, + 0x6d, 0x61, 0x78, 0x5f, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x61, + 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x4d, 0x69, 0x6e, 0x65, + 0x72, 0x46, 0x65, 0x65, 0x53, 0x61, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x77, 0x65, 0x65, 0x70, + 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0f, 0x73, 0x77, 0x65, 0x65, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x54, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x62, + 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, 0x5f, 0x73, 0x65, 0x63, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x11, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x42, 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, + 0x53, 0x65, 0x63, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x12, + 0x2e, 0x0a, 0x13, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x62, 0x75, 0x64, 0x67, + 0x65, 0x74, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x61, 0x75, + 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x53, 0x61, 0x74, 0x12, + 0x3d, 0x0a, 0x19, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x62, 0x75, 0x64, 0x67, + 0x65, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x04, 0x42, 0x02, 0x18, 0x01, 0x52, 0x16, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, + 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x63, 0x12, 0x2b, + 0x0a, 0x12, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x6e, 0x5f, 0x66, 0x6c, + 0x69, 0x67, 0x68, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x61, 0x75, 0x74, 0x6f, + 0x4d, 0x61, 0x78, 0x49, 0x6e, 0x46, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d, + 0x69, 0x6e, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x69, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, + 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x61, + 0x78, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x68, + 0x74, 0x6c, 0x63, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, + 0x11, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x68, 0x74, 0x6c, 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x54, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, + 0x70, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x44, 0x65, + 0x73, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x4a, 0x0a, 0x22, 0x61, 0x75, 0x74, + 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x65, 0x66, + 0x72, 0x65, 0x73, 0x68, 0x5f, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x5f, 0x73, 0x65, 0x63, 0x18, + 0x13, 0x20, 0x01, 0x28, 0x04, 0x52, 0x1e, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x42, + 0x75, 0x64, 0x67, 0x65, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x50, 0x65, 0x72, 0x69, + 0x6f, 0x64, 0x53, 0x65, 0x63, 0x12, 0x3f, 0x0a, 0x1c, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, + 0x70, 0x5f, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, + 0x66, 0x72, 0x65, 0x73, 0x68, 0x18, 0x14, 0x20, 0x01, 0x28, 0x04, 0x52, 0x19, 0x61, 0x75, 0x74, + 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x4c, 0x61, 0x73, 0x74, 0x52, + 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x61, 0x73, 0x79, 0x5f, 0x61, + 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x18, 0x15, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x65, + 0x61, 0x73, 0x79, 0x41, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x12, 0x42, 0x0a, 0x1e, 0x65, + 0x61, 0x73, 0x79, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x6c, 0x6f, 0x63, + 0x61, 0x6c, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x16, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x1a, 0x65, 0x61, 0x73, 0x79, 0x41, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x6f, + 0x70, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x61, 0x74, 0x12, + 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x17, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x40, 0x0a, 0x11, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x18, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0f, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x54, 0x79, 0x70, 0x65, 0x22, 0x84, 0x02, 0x0a, 0x0d, + 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1d, 0x0a, + 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x09, + 0x73, 0x77, 0x61, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x11, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, + 0x70, 0x65, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, + 0x62, 0x6b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, + 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, + 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x11, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, + 0x6f, 0x6c, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, + 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x11, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, + 0x6c, 0x64, 0x22, 0x59, 0x0a, 0x19, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, + 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x3c, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, + 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x73, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x1c, 0x0a, + 0x1a, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, + 0x61, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x15, 0x0a, 0x13, 0x53, + 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x72, 0x0a, 0x0c, 0x44, 0x69, 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, + 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, + 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x06, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, + 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0xb6, 0x01, 0x0a, 0x14, 0x53, 0x75, 0x67, 0x67, 0x65, + 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x32, 0x0a, 0x08, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, + 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x07, 0x6c, 0x6f, 0x6f, 0x70, + 0x4f, 0x75, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x69, 0x6e, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, + 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x06, 0x6c, 0x6f, + 0x6f, 0x70, 0x49, 0x6e, 0x12, 0x39, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, + 0x66, 0x69, 0x65, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, + 0x70, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, + 0x64, 0x52, 0x0c, 0x64, 0x69, 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x22, + 0x57, 0x0a, 0x12, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x31, 0x0a, 0x16, 0x69, 0x5f, 0x6b, 0x6e, 0x6f, 0x77, 0x5f, + 0x77, 0x68, 0x61, 0x74, 0x5f, 0x69, 0x5f, 0x61, 0x6d, 0x5f, 0x64, 0x6f, 0x69, 0x6e, 0x67, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x69, 0x4b, 0x6e, 0x6f, 0x77, 0x57, 0x68, 0x61, 0x74, + 0x49, 0x41, 0x6d, 0x44, 0x6f, 0x69, 0x6e, 0x67, 0x22, 0x15, 0x0a, 0x13, 0x41, 0x62, 0x61, 0x6e, + 0x64, 0x6f, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x19, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5a, 0x0a, 0x18, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, + 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, + 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xa9, 0x01, 0x0a, 0x11, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, + 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x6f, 0x75, 0x74, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x76, 0x6f, 0x75, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x79, 0x2a, 0x3b, 0x0a, 0x0b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x44, 0x44, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x54, + 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x10, 0x01, 0x2a, + 0x25, 0x0a, 0x08, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x4c, + 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4c, 0x4f, 0x4f, + 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x01, 0x2a, 0x73, 0x0a, 0x09, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x50, 0x52, 0x45, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x52, + 0x45, 0x56, 0x45, 0x41, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x48, 0x54, 0x4c, + 0x43, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, + 0x07, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, + 0x49, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, + 0x45, 0x5f, 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x2a, 0xbe, 0x02, 0x0a, 0x0d, + 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x17, 0x0a, + 0x13, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, + 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, + 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4f, 0x46, 0x46, 0x43, 0x48, 0x41, 0x49, + 0x4e, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, + 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, + 0x20, 0x0a, 0x1c, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, + 0x4e, 0x5f, 0x53, 0x57, 0x45, 0x45, 0x50, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, + 0x03, 0x12, 0x25, 0x0a, 0x21, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, + 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, + 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x04, 0x12, 0x1c, 0x0a, 0x18, 0x46, 0x41, 0x49, 0x4c, + 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x54, 0x45, 0x4d, 0x50, 0x4f, + 0x52, 0x41, 0x52, 0x59, 0x10, 0x05, 0x12, 0x23, 0x0a, 0x1f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, + 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, + 0x43, 0x54, 0x5f, 0x41, 0x4d, 0x4f, 0x55, 0x4e, 0x54, 0x10, 0x06, 0x12, 0x1c, 0x0a, 0x18, 0x46, + 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x41, 0x42, + 0x41, 0x4e, 0x44, 0x4f, 0x4e, 0x45, 0x44, 0x10, 0x07, 0x12, 0x31, 0x0a, 0x2d, 0x46, 0x41, 0x49, + 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x53, 0x55, + 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x52, 0x4d, + 0x45, 0x44, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x08, 0x2a, 0x2f, 0x0a, 0x11, + 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0d, + 0x0a, 0x09, 0x54, 0x48, 0x52, 0x45, 0x53, 0x48, 0x4f, 0x4c, 0x44, 0x10, 0x01, 0x2a, 0xa6, 0x03, + 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x6f, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x13, + 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x22, 0x0a, 0x1e, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, + 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x44, 0x47, 0x45, 0x54, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, + 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x55, 0x54, + 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x53, 0x57, 0x45, 0x45, 0x50, 0x5f, 0x46, + 0x45, 0x45, 0x53, 0x10, 0x02, 0x12, 0x1e, 0x0a, 0x1a, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, + 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x44, 0x47, 0x45, 0x54, 0x5f, 0x45, 0x4c, 0x41, 0x50, + 0x53, 0x45, 0x44, 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, + 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x5f, 0x46, 0x4c, 0x49, 0x47, 0x48, 0x54, 0x10, 0x04, + 0x12, 0x18, 0x0a, 0x14, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, + 0x53, 0x57, 0x41, 0x50, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x55, + 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4d, 0x49, 0x4e, 0x45, 0x52, 0x5f, + 0x46, 0x45, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, + 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x45, 0x50, 0x41, 0x59, 0x10, 0x07, 0x12, 0x1f, 0x0a, + 0x1b, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x46, 0x41, 0x49, + 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x42, 0x41, 0x43, 0x4b, 0x4f, 0x46, 0x46, 0x10, 0x08, 0x12, 0x18, + 0x0a, 0x14, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4c, 0x4f, + 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x09, 0x12, 0x17, 0x0a, 0x13, 0x41, 0x55, 0x54, 0x4f, + 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, + 0x0a, 0x12, 0x1c, 0x0a, 0x18, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, + 0x5f, 0x4c, 0x49, 0x51, 0x55, 0x49, 0x44, 0x49, 0x54, 0x59, 0x5f, 0x4f, 0x4b, 0x10, 0x0b, 0x12, + 0x23, 0x0a, 0x1f, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x42, + 0x55, 0x44, 0x47, 0x45, 0x54, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, + 0x4e, 0x54, 0x10, 0x0c, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, + 0x53, 0x4f, 0x4e, 0x5f, 0x46, 0x45, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, + 0x49, 0x45, 0x4e, 0x54, 0x10, 0x0d, 0x32, 0xa3, 0x09, 0x0a, 0x0a, 0x53, 0x77, 0x61, 0x70, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x07, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, + 0x12, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, + 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x37, 0x0a, 0x06, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x12, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x52, 0x06, 0x6c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x12, 0x39, 0x0a, 0x0c, 0x64, 0x69, - 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x73, 0x71, 0x75, - 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x0c, 0x64, 0x69, 0x73, 0x71, 0x75, 0x61, 0x6c, - 0x69, 0x66, 0x69, 0x65, 0x64, 0x22, 0x57, 0x0a, 0x12, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, - 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x31, 0x0a, 0x16, 0x69, - 0x5f, 0x6b, 0x6e, 0x6f, 0x77, 0x5f, 0x77, 0x68, 0x61, 0x74, 0x5f, 0x69, 0x5f, 0x61, 0x6d, 0x5f, - 0x64, 0x6f, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x69, 0x4b, 0x6e, - 0x6f, 0x77, 0x57, 0x68, 0x61, 0x74, 0x49, 0x41, 0x6d, 0x44, 0x6f, 0x69, 0x6e, 0x67, 0x22, 0x15, - 0x0a, 0x13, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, - 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x22, 0x5a, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0c, - 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, - 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xa9, 0x01, 0x0a, - 0x11, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x65, - 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x76, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x76, 0x6f, 0x75, 0x74, - 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x2a, 0x3b, 0x0a, 0x0b, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x44, 0x44, 0x52, 0x45, - 0x53, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, - 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x54, 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x50, 0x55, 0x42, - 0x4b, 0x45, 0x59, 0x10, 0x01, 0x2a, 0x25, 0x0a, 0x08, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, - 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x00, 0x12, - 0x0b, 0x0a, 0x07, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x01, 0x2a, 0x73, 0x0a, 0x09, - 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x4e, 0x49, - 0x54, 0x49, 0x41, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x50, 0x52, 0x45, 0x49, - 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x52, 0x45, 0x56, 0x45, 0x41, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, - 0x12, 0x0a, 0x0e, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, - 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, - 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, - 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x44, 0x10, - 0x05, 0x2a, 0xbe, 0x02, 0x0a, 0x0d, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, - 0x73, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x13, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, - 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, - 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4f, - 0x46, 0x46, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x46, 0x41, 0x49, - 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, - 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x20, 0x0a, 0x1c, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, - 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x53, 0x57, 0x45, 0x45, 0x50, 0x5f, 0x54, 0x49, - 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x03, 0x12, 0x25, 0x0a, 0x21, 0x46, 0x41, 0x49, 0x4c, 0x55, - 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, - 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x04, 0x12, 0x1c, - 0x0a, 0x18, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, - 0x5f, 0x54, 0x45, 0x4d, 0x50, 0x4f, 0x52, 0x41, 0x52, 0x59, 0x10, 0x05, 0x12, 0x23, 0x0a, 0x1f, - 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, - 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x41, 0x4d, 0x4f, 0x55, 0x4e, 0x54, 0x10, - 0x06, 0x12, 0x1c, 0x0a, 0x18, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, - 0x53, 0x4f, 0x4e, 0x5f, 0x41, 0x42, 0x41, 0x4e, 0x44, 0x4f, 0x4e, 0x45, 0x44, 0x10, 0x07, 0x12, - 0x31, 0x0a, 0x2d, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, - 0x4e, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x43, - 0x4f, 0x4e, 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, - 0x10, 0x08, 0x2a, 0x2f, 0x0a, 0x11, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, - 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, - 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x48, 0x52, 0x45, 0x53, 0x48, 0x4f, 0x4c, - 0x44, 0x10, 0x01, 0x2a, 0xa6, 0x03, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x6f, 0x52, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x13, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, - 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x22, 0x0a, 0x1e, 0x41, - 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x44, 0x47, 0x45, - 0x54, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, - 0x1a, 0x0a, 0x16, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x53, - 0x57, 0x45, 0x45, 0x50, 0x5f, 0x46, 0x45, 0x45, 0x53, 0x10, 0x02, 0x12, 0x1e, 0x0a, 0x1a, 0x41, - 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x44, 0x47, 0x45, - 0x54, 0x5f, 0x45, 0x4c, 0x41, 0x50, 0x53, 0x45, 0x44, 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x41, - 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x5f, 0x46, 0x4c, - 0x49, 0x47, 0x48, 0x54, 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, - 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x05, - 0x12, 0x19, 0x0a, 0x15, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, - 0x4d, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x41, - 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x45, 0x50, 0x41, - 0x59, 0x10, 0x07, 0x12, 0x1f, 0x0a, 0x1b, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, - 0x4f, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x42, 0x41, 0x43, 0x4b, 0x4f, - 0x46, 0x46, 0x10, 0x08, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, - 0x53, 0x4f, 0x4e, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x09, 0x12, 0x17, - 0x0a, 0x13, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4c, 0x4f, - 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x0a, 0x12, 0x1c, 0x0a, 0x18, 0x41, 0x55, 0x54, 0x4f, 0x5f, - 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4c, 0x49, 0x51, 0x55, 0x49, 0x44, 0x49, 0x54, 0x59, - 0x5f, 0x4f, 0x4b, 0x10, 0x0b, 0x12, 0x23, 0x0a, 0x1f, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, - 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x44, 0x47, 0x45, 0x54, 0x5f, 0x49, 0x4e, 0x53, 0x55, - 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x10, 0x0c, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x55, - 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x46, 0x45, 0x45, 0x5f, 0x49, 0x4e, - 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x10, 0x0d, 0x32, 0xa3, 0x09, 0x0a, - 0x0a, 0x53, 0x77, 0x61, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x07, 0x4c, - 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x12, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, - 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, - 0x12, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x49, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, - 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x39, 0x0a, 0x07, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x12, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, - 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, - 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x4c, 0x69, - 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, - 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, - 0x0a, 0x08, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, - 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x48, 0x0a, 0x0b, 0x41, 0x62, 0x61, - 0x6e, 0x64, 0x6f, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x12, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, - 0x70, 0x63, 0x2e, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, - 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65, + 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, + 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x07, 0x4d, 0x6f, 0x6e, + 0x69, 0x74, 0x6f, 0x72, 0x12, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4d, + 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, + 0x73, 0x12, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, + 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x53, 0x77, 0x61, 0x70, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, + 0x77, 0x61, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, + 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x48, 0x0a, 0x0b, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x53, 0x77, + 0x61, 0x70, 0x12, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x62, 0x61, + 0x6e, 0x64, 0x6f, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, + 0x6e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, + 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x15, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4f, + 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x40, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, + 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x65, - 0x72, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, - 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, - 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, - 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, - 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, 0x6f, - 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, - 0x72, 0x70, 0x63, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x54, 0x65, 0x72, - 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x47, 0x65, - 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x6c, - 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, - 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, - 0x05, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, - 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, - 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4c, 0x73, 0x61, 0x74, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, - 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, - 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x49, 0x6e, - 0x66, 0x6f, 0x12, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6f, - 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, - 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x22, 0x2e, 0x6c, 0x6f, - 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, - 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, - 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x5d, 0x0a, - 0x12, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, - 0x61, 0x6d, 0x73, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, - 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, - 0x63, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, - 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, - 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x1c, 0x2e, 0x6c, - 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, - 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6f, 0x6f, - 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x10, 0x4c, 0x69, 0x73, - 0x74, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x20, 0x2e, - 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x65, - 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x21, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, - 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 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, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x72, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, + 0x70, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x49, + 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x50, 0x72, 0x6f, 0x62, 0x65, + 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, + 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x40, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4c, 0x73, 0x61, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x12, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, + 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x3c, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x17, 0x2e, 0x6c, + 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x56, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x47, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, + 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x5d, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x69, + 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x22, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, + 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x23, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x4c, + 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, + 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, + 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x65, 0x72, + 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, + 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, + 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, 0x6c, 0x6f, + 0x6f, 0x70, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/looprpc/client.proto b/looprpc/client.proto index 6b33852..7343dd7 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -235,6 +235,13 @@ message LoopOutRequest { The address type of the account specified in the account field. */ 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; } /* diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index 03996cc..ab0120a 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -1110,6 +1110,10 @@ "account_addr_type": { "$ref": "#/definitions/looprpcAddressType", "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." } } }, diff --git a/release_notes.md b/release_notes.md index 38afc60..7178953 100644 --- a/release_notes.md +++ b/release_notes.md @@ -16,6 +16,15 @@ This file tracks release notes for the loop client. #### 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 #### Bug Fixes diff --git a/server_mock_test.go b/server_mock_test.go index 78a8528..9f52a2c 100644 --- a/server_mock_test.go +++ b/server_mock_test.go @@ -8,6 +8,7 @@ import ( "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/test" @@ -268,6 +269,15 @@ func (s *serverMock) MuSig2SignSweep(_ context.Context, _ loopdb.ProtocolVersion 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, _ lntypes.Hash, _ [32]byte) error { diff --git a/store_mock_test.go b/store_mock_test.go deleted file mode 100644 index 06c134d..0000000 --- a/store_mock_test.go +++ /dev/null @@ -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") -} diff --git a/swap.go b/swap.go index 61db839..bd5bea9 100644 --- a/swap.go +++ b/swap.go @@ -4,11 +4,10 @@ import ( "context" "time" - "github.com/btcsuite/btcd/chaincfg" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" - "github.com/lightningnetwork/lnd/input" + "github.com/lightninglabs/loop/utils" "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 // uses the v3 (taproot) htlc. func IsTaprootSwap(swapContract *loopdb.SwapContract) bool { - return 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 + return utils.GetHtlcScriptVersion(swapContract.ProtocolVersion) == swap.HtlcV3 } // swapInfo constructs and returns a filled SwapInfo from diff --git a/swap_server_client.go b/swap_server_client.go index 359c4dc..87760f3 100644 --- a/swap_server_client.go +++ b/swap_server_client.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/aperture/lsat" "github.com/lightninglabs/loop/loopdb" looprpc "github.com/lightninglabs/loop/swapserverrpc" @@ -775,6 +776,51 @@ func (s *grpcSwapServerClient) MuSig2SignSweep(ctx context.Context, return res.Nonce, res.PartialSignature, nil } +// MultiMuSig2SignSweep calls the server to cooperatively sign an input in +// a batch transaction that attempts to sweep multiple htlcs at once. This +// method is called once per input signed. The prevoutMap is a map of all the +// prevout information for each spend outpoint. Returns the server's nonce and +// partial signature. +func (s *grpcSwapServerClient) MultiMuSig2SignSweep(ctx context.Context, + protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash, + paymentAddr [32]byte, nonce []byte, sweepTxPsbt []byte, + prevoutMap map[wire.OutPoint]*wire.TxOut) ( + []byte, []byte, error) { + + prevOutInfo := make([]*looprpc.PrevoutInfo, 0, len(prevoutMap)) + for prevOut, txOut := range prevoutMap { + txOut := *txOut + prevOut := prevOut + + prevOutInfo = append(prevOutInfo, + &looprpc.PrevoutInfo{ + TxidBytes: prevOut.Hash[:], + OutputIndex: prevOut.Index, + Value: uint64(txOut.Value), + PkScript: txOut.PkScript, + }) + } + + req := &looprpc.MuSig2SignSweepReq{ + ProtocolVersion: looprpc.ProtocolVersion(protocolVersion), + SwapHash: swapHash[:], + PaymentAddress: paymentAddr[:], + Nonce: nonce, + SweepTxPsbt: sweepTxPsbt, + PrevoutInfo: prevOutInfo, + } + + rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) + defer rpcCancel() + + res, err := s.server.MuSig2SignSweep(rpcCtx, req) + if err != nil { + return nil, nil, err + } + + return res.Nonce, res.PartialSignature, nil +} + // PushKey sends the client's HTLC internal key associated with the swap to // the server. func (s *grpcSwapServerClient) PushKey(ctx context.Context, diff --git a/swapserverrpc/server.pb.go b/swapserverrpc/server.pb.go index f3cda03..2387052 100644 --- a/swapserverrpc/server.pb.go +++ b/swapserverrpc/server.pb.go @@ -1973,8 +1973,7 @@ type ServerProbeRequest struct { // 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"` - // 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. Target []byte `protobuf:"bytes,3,opt,name=target,proto3" json:"target,omitempty"` // 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"` // The psbt of the sweep txn. 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() { @@ -2426,6 +2427,88 @@ func (x *MuSig2SignSweepReq) GetSweepTxPsbt() []byte { 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 { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2440,7 +2523,7 @@ type MuSig2SignSweepRes struct { func (x *MuSig2SignSweepRes) Reset() { *x = MuSig2SignSweepRes{} if protoimpl.UnsafeEnabled { - mi := &file_server_proto_msgTypes[28] + mi := &file_server_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2453,7 +2536,7 @@ func (x *MuSig2SignSweepRes) String() string { func (*MuSig2SignSweepRes) ProtoMessage() {} func (x *MuSig2SignSweepRes) ProtoReflect() protoreflect.Message { - mi := &file_server_proto_msgTypes[28] + mi := &file_server_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2466,7 +2549,7 @@ func (x *MuSig2SignSweepRes) ProtoReflect() protoreflect.Message { // Deprecated: Use MuSig2SignSweepRes.ProtoReflect.Descriptor instead. func (*MuSig2SignSweepRes) Descriptor() ([]byte, []int) { - return file_server_proto_rawDescGZIP(), []int{28} + return file_server_proto_rawDescGZIP(), []int{29} } func (x *MuSig2SignSweepRes) GetNonce() []byte { @@ -2499,7 +2582,7 @@ type ServerPushKeyReq struct { func (x *ServerPushKeyReq) Reset() { *x = ServerPushKeyReq{} if protoimpl.UnsafeEnabled { - mi := &file_server_proto_msgTypes[29] + mi := &file_server_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2512,7 +2595,7 @@ func (x *ServerPushKeyReq) String() string { func (*ServerPushKeyReq) ProtoMessage() {} func (x *ServerPushKeyReq) ProtoReflect() protoreflect.Message { - mi := &file_server_proto_msgTypes[29] + mi := &file_server_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2525,7 +2608,7 @@ func (x *ServerPushKeyReq) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerPushKeyReq.ProtoReflect.Descriptor instead. func (*ServerPushKeyReq) Descriptor() ([]byte, []int) { - return file_server_proto_rawDescGZIP(), []int{29} + return file_server_proto_rawDescGZIP(), []int{30} } func (x *ServerPushKeyReq) GetProtocolVersion() ProtocolVersion { @@ -2558,7 +2641,7 @@ type ServerPushKeyRes struct { func (x *ServerPushKeyRes) Reset() { *x = ServerPushKeyRes{} if protoimpl.UnsafeEnabled { - mi := &file_server_proto_msgTypes[30] + mi := &file_server_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2571,7 +2654,7 @@ func (x *ServerPushKeyRes) String() string { func (*ServerPushKeyRes) ProtoMessage() {} func (x *ServerPushKeyRes) ProtoReflect() protoreflect.Message { - mi := &file_server_proto_msgTypes[30] + mi := &file_server_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2584,7 +2667,7 @@ func (x *ServerPushKeyRes) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerPushKeyRes.ProtoReflect.Descriptor instead. 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 @@ -2598,7 +2681,7 @@ type FetchL402Request struct { func (x *FetchL402Request) Reset() { *x = FetchL402Request{} if protoimpl.UnsafeEnabled { - mi := &file_server_proto_msgTypes[31] + mi := &file_server_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2611,7 +2694,7 @@ func (x *FetchL402Request) String() string { func (*FetchL402Request) ProtoMessage() {} func (x *FetchL402Request) ProtoReflect() protoreflect.Message { - mi := &file_server_proto_msgTypes[31] + mi := &file_server_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2624,7 +2707,7 @@ func (x *FetchL402Request) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchL402Request.ProtoReflect.Descriptor instead. 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 @@ -2638,7 +2721,7 @@ type FetchL402Response struct { func (x *FetchL402Response) Reset() { *x = FetchL402Response{} if protoimpl.UnsafeEnabled { - mi := &file_server_proto_msgTypes[32] + mi := &file_server_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2651,7 +2734,7 @@ func (x *FetchL402Response) String() string { func (*FetchL402Response) ProtoMessage() {} func (x *FetchL402Response) ProtoReflect() protoreflect.Message { - mi := &file_server_proto_msgTypes[32] + mi := &file_server_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2664,7 +2747,7 @@ func (x *FetchL402Response) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchL402Response.ProtoReflect.Descriptor instead. 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 @@ -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, 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, - 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, 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, @@ -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, 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, - 0x73, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x50, 0x73, 0x62, 0x74, 0x4a, 0x04, 0x08, 0x05, 0x10, - 0x06, 0x22, 0x57, 0x0a, 0x12, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, - 0x77, 0x65, 0x65, 0x70, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x2b, 0x0a, - 0x11, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, - 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x10, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 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, 0x32, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, - 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x68, 0x61, 0x73, - 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x48, 0x61, 0x73, - 0x68, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x72, - 0x69, 0x76, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x69, 0x76, 0x6b, 0x65, 0x79, 0x22, 0x12, 0x0a, 0x10, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, - 0x22, 0x12, 0x0a, 0x10, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x32, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x22, 0x13, 0x0a, 0x11, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, - 0x32, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0xef, 0x01, 0x0a, 0x0f, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, - 0x06, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x4d, 0x55, 0x4c, - 0x54, 0x49, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x19, 0x0a, - 0x15, 0x4e, 0x41, 0x54, 0x49, 0x56, 0x45, 0x5f, 0x53, 0x45, 0x47, 0x57, 0x49, 0x54, 0x5f, 0x4c, - 0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x52, 0x45, 0x49, - 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x50, 0x55, 0x53, 0x48, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, - 0x55, 0x54, 0x10, 0x03, 0x12, 0x18, 0x0a, 0x14, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x45, 0x58, 0x50, - 0x49, 0x52, 0x59, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x04, 0x12, 0x0b, - 0x0a, 0x07, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x56, 0x32, 0x10, 0x05, 0x12, 0x11, 0x0a, 0x0d, 0x4d, - 0x55, 0x4c, 0x54, 0x49, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x06, 0x12, 0x13, - 0x0a, 0x0f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, - 0x4c, 0x10, 0x07, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x52, 0x4f, 0x42, 0x45, 0x10, 0x08, 0x12, 0x12, - 0x0a, 0x0e, 0x52, 0x4f, 0x55, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x50, 0x4c, 0x55, 0x47, 0x49, 0x4e, - 0x10, 0x09, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x56, 0x33, 0x10, 0x0a, 0x12, - 0x0a, 0x0a, 0x06, 0x4d, 0x55, 0x53, 0x49, 0x47, 0x32, 0x10, 0x0b, 0x2a, 0x9e, 0x04, 0x0a, 0x0f, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x14, 0x0a, 0x10, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, - 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, - 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0x01, - 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, - 0x53, 0x53, 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, - 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x12, - 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, - 0x5f, 0x4e, 0x4f, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, 0x04, 0x12, 0x25, 0x0a, 0x21, 0x53, 0x45, - 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x56, 0x41, - 0x4c, 0x49, 0x44, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x4d, 0x4f, 0x55, 0x4e, 0x54, 0x10, - 0x05, 0x12, 0x23, 0x0a, 0x1f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, - 0x45, 0x44, 0x5f, 0x4f, 0x46, 0x46, 0x5f, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x54, 0x49, 0x4d, - 0x45, 0x4f, 0x55, 0x54, 0x10, 0x06, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, - 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, - 0x07, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, - 0x45, 0x44, 0x5f, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x44, 0x45, 0x41, 0x44, 0x4c, 0x49, 0x4e, 0x45, - 0x10, 0x08, 0x12, 0x22, 0x0a, 0x1e, 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, - 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x09, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, - 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, - 0x45, 0x44, 0x10, 0x0a, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x55, - 0x4e, 0x45, 0x58, 0x50, 0x45, 0x43, 0x54, 0x45, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, - 0x45, 0x10, 0x0b, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x48, 0x54, - 0x4c, 0x43, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x10, 0x0c, 0x12, 0x1f, - 0x0a, 0x1b, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x5f, - 0x50, 0x52, 0x45, 0x50, 0x41, 0x59, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x0d, 0x12, - 0x20, 0x0a, 0x1c, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, - 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, - 0x0e, 0x12, 0x27, 0x0a, 0x23, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, - 0x45, 0x44, 0x5f, 0x4d, 0x55, 0x4c, 0x54, 0x49, 0x50, 0x4c, 0x45, 0x5f, 0x53, 0x57, 0x41, 0x50, - 0x5f, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x53, 0x10, 0x0f, 0x12, 0x20, 0x0a, 0x1c, 0x53, 0x45, - 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x49, 0x54, - 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x10, 0x2a, 0x4a, 0x0a, 0x10, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x11, 0x0a, 0x0d, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, - 0x4e, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x52, 0x45, 0x50, 0x41, 0x59, 0x5f, 0x52, 0x4f, - 0x55, 0x54, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, - 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x02, 0x2a, 0xf1, 0x01, 0x0a, 0x14, 0x50, 0x61, 0x79, - 0x6d, 0x65, 0x6e, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, - 0x6e, 0x12, 0x1b, 0x0a, 0x17, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, - 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1e, - 0x0a, 0x1a, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, - 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x1f, - 0x0a, 0x1b, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, - 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x02, 0x12, - 0x1c, 0x0a, 0x18, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, - 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x30, 0x0a, - 0x2c, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, - 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x50, 0x41, - 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, 0x53, 0x10, 0x04, 0x12, - 0x2b, 0x0a, 0x27, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, - 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, - 0x4e, 0x54, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x05, 0x2a, 0x27, 0x0a, 0x0d, - 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x08, 0x0a, - 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x57, 0x5f, 0x48, - 0x49, 0x47, 0x48, 0x10, 0x01, 0x32, 0xdc, 0x0a, 0x0a, 0x0a, 0x53, 0x77, 0x61, 0x70, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x12, 0x4f, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, - 0x65, 0x72, 0x6d, 0x73, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, - 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, - 0x54, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x4f, 0x0a, 0x0e, 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x6f, 0x70, - 0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x12, 0x1d, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, - 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, - 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, - 0x74, 0x50, 0x75, 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x29, 0x2e, - 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, - 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x50, 0x75, 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, - 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, - 0x50, 0x75, 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, - 0x75, 0x6f, 0x74, 0x65, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, - 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, - 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, - 0x65, 0x72, 0x6d, 0x73, 0x12, 0x21, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, - 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, - 0x72, 0x6d, 0x73, 0x12, 0x4c, 0x0a, 0x0d, 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, - 0x53, 0x77, 0x61, 0x70, 0x12, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1d, 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, - 0x65, 0x12, 0x54, 0x0a, 0x0b, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, - 0x12, 0x21, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, 0x17, 0x53, 0x75, 0x62, 0x73, 0x63, - 0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x73, 0x12, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, - 0x12, 0x65, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f, 0x6f, - 0x70, 0x49, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, - 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6c, - 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, - 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x11, 0x43, 0x61, 0x6e, 0x63, 0x65, - 0x6c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x12, 0x21, 0x2e, 0x6c, - 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x6f, 0x6f, - 0x70, 0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, - 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x12, 0x1b, 0x2e, 0x6c, - 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f, - 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, - 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x16, 0x52, 0x65, 0x63, 0x6f, 0x6d, - 0x6d, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x63, 0x6f, - 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, - 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, - 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x12, 0x57, 0x0a, 0x13, 0x52, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x12, 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x65, - 0x71, 0x1a, 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, - 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0f, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, - 0x53, 0x77, 0x65, 0x65, 0x70, 0x12, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, - 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77, 0x65, 0x65, 0x70, 0x52, - 0x65, 0x71, 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x75, 0x53, - 0x69, 0x67, 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, + 0x73, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x50, 0x73, 0x62, 0x74, 0x12, 0x37, 0x0a, 0x0c, 0x70, + 0x72, 0x65, 0x76, 0x6f, 0x75, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x07, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x65, 0x76, + 0x6f, 0x75, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x76, 0x6f, 0x75, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, 0x82, 0x01, 0x0a, 0x0b, 0x50, + 0x72, 0x65, 0x76, 0x6f, 0x75, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x6b, 0x5f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x6b, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x1d, 0x0a, + 0x0a, 0x74, 0x78, 0x69, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x09, 0x74, 0x78, 0x69, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x0b, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, + 0x57, 0x0a, 0x12, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77, 0x65, + 0x65, 0x70, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x70, + 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x53, + 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 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, 0x32, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, + 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x48, 0x61, 0x73, 0x68, 0x12, + 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x72, 0x69, 0x76, + 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x69, 0x76, 0x6b, 0x65, 0x79, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x50, 0x75, 0x73, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x22, 0x12, + 0x0a, 0x10, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x32, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x13, 0x0a, 0x11, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4c, 0x34, 0x30, 0x32, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0xef, 0x01, 0x0a, 0x0f, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x4c, + 0x45, 0x47, 0x41, 0x43, 0x59, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x4d, 0x55, 0x4c, 0x54, 0x49, + 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x4e, + 0x41, 0x54, 0x49, 0x56, 0x45, 0x5f, 0x53, 0x45, 0x47, 0x57, 0x49, 0x54, 0x5f, 0x4c, 0x4f, 0x4f, + 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x52, 0x45, 0x49, 0x4d, 0x41, + 0x47, 0x45, 0x5f, 0x50, 0x55, 0x53, 0x48, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, + 0x10, 0x03, 0x12, 0x18, 0x0a, 0x14, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, + 0x59, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x04, 0x12, 0x0b, 0x0a, 0x07, + 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x56, 0x32, 0x10, 0x05, 0x12, 0x11, 0x0a, 0x0d, 0x4d, 0x55, 0x4c, + 0x54, 0x49, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x06, 0x12, 0x13, 0x0a, 0x0f, + 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, + 0x07, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x52, 0x4f, 0x42, 0x45, 0x10, 0x08, 0x12, 0x12, 0x0a, 0x0e, + 0x52, 0x4f, 0x55, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x50, 0x4c, 0x55, 0x47, 0x49, 0x4e, 0x10, 0x09, + 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x56, 0x33, 0x10, 0x0a, 0x12, 0x0a, 0x0a, + 0x06, 0x4d, 0x55, 0x53, 0x49, 0x47, 0x32, 0x10, 0x0b, 0x2a, 0x9e, 0x04, 0x0a, 0x0f, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, + 0x10, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x48, 0x54, + 0x4c, 0x43, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0x01, 0x12, 0x12, + 0x0a, 0x0e, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, + 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, + 0x4c, 0x45, 0x44, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x12, 0x19, 0x0a, + 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x4e, + 0x4f, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, 0x04, 0x12, 0x25, 0x0a, 0x21, 0x53, 0x45, 0x52, 0x56, + 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, + 0x44, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x4d, 0x4f, 0x55, 0x4e, 0x54, 0x10, 0x05, 0x12, + 0x23, 0x0a, 0x1f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, + 0x5f, 0x4f, 0x46, 0x46, 0x5f, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, + 0x55, 0x54, 0x10, 0x06, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, + 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x07, 0x12, + 0x1f, 0x0a, 0x1b, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, + 0x5f, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x44, 0x45, 0x41, 0x44, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x08, + 0x12, 0x22, 0x0a, 0x1e, 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, 0x54, 0x49, + 0x4f, 0x4e, 0x10, 0x09, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x54, + 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, 0x44, + 0x10, 0x0a, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x55, 0x4e, 0x45, + 0x58, 0x50, 0x45, 0x43, 0x54, 0x45, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, + 0x0b, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x48, 0x54, 0x4c, 0x43, + 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x10, 0x0c, 0x12, 0x1f, 0x0a, 0x1b, + 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x52, + 0x45, 0x50, 0x41, 0x59, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x0d, 0x12, 0x20, 0x0a, + 0x1c, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x49, + 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x0e, 0x12, + 0x27, 0x0a, 0x23, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, + 0x5f, 0x4d, 0x55, 0x4c, 0x54, 0x49, 0x50, 0x4c, 0x45, 0x5f, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x53, + 0x43, 0x52, 0x49, 0x50, 0x54, 0x53, 0x10, 0x0f, 0x12, 0x20, 0x0a, 0x1c, 0x53, 0x45, 0x52, 0x56, + 0x45, 0x52, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, + 0x4c, 0x49, 0x5a, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x10, 0x2a, 0x4a, 0x0a, 0x10, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, + 0x0a, 0x0d, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x52, 0x45, 0x50, 0x41, 0x59, 0x5f, 0x52, 0x4f, 0x55, 0x54, + 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x52, + 0x4f, 0x55, 0x54, 0x45, 0x10, 0x02, 0x2a, 0xf1, 0x01, 0x0a, 0x14, 0x50, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, + 0x1b, 0x0a, 0x17, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, + 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, + 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, + 0x4f, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x1f, 0x0a, 0x1b, + 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, + 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x02, 0x12, 0x1c, 0x0a, + 0x18, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, + 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x30, 0x0a, 0x2c, 0x4c, + 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, + 0x4e, 0x5f, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x4d, + 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, 0x53, 0x10, 0x04, 0x12, 0x2b, 0x0a, + 0x27, 0x4c, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, + 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, + 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x05, 0x2a, 0x27, 0x0a, 0x0d, 0x52, 0x6f, + 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, + 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x57, 0x5f, 0x48, 0x49, 0x47, + 0x48, 0x10, 0x01, 0x32, 0xdc, 0x0a, 0x0a, 0x0a, 0x53, 0x77, 0x61, 0x70, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x12, 0x4f, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72, + 0x6d, 0x73, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65, + 0x72, 0x6d, 0x73, 0x12, 0x4f, 0x0a, 0x0e, 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, + 0x74, 0x53, 0x77, 0x61, 0x70, 0x12, 0x1d, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x50, + 0x75, 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x29, 0x2e, 0x6c, 0x6f, + 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, + 0x4f, 0x75, 0x74, 0x50, 0x75, 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x50, 0x75, + 0x73, 0x68, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, + 0x74, 0x65, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, + 0x6f, 0x74, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x72, + 0x6d, 0x73, 0x12, 0x21, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, + 0x73, 0x12, 0x4c, 0x0a, 0x0d, 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x53, 0x77, + 0x61, 0x70, 0x12, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1d, 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, 0x65, 0x12, + 0x54, 0x0a, 0x0b, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x21, + 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, + 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, 0x17, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, + 0x62, 0x65, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, + 0x12, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x62, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, + 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x65, + 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f, 0x6f, 0x70, 0x49, + 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, + 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6c, 0x6f, 0x6f, + 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4c, 0x6f, + 0x6f, 0x70, 0x49, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x11, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, + 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x12, 0x21, 0x2e, 0x6c, 0x6f, 0x6f, + 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, + 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x6f, + 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, + 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x62, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, + 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x16, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, + 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, + 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, + 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x52, 0x65, 0x71, 0x1a, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, + 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x12, 0x57, 0x0a, 0x13, 0x52, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f, + 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, + 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x71, 0x1a, + 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x73, + 0x12, 0x4b, 0x0a, 0x0f, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77, + 0x65, 0x65, 0x70, 0x12, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x75, + 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x53, 0x77, 0x65, 0x65, 0x70, 0x52, 0x65, 0x71, + 0x1a, 0x1b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x75, 0x53, 0x69, 0x67, + 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 ( @@ -3157,7 +3252,7 @@ func file_server_proto_rawDescGZIP() []byte { } 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{}{ (ProtocolVersion)(0), // 0: looprpc.ProtocolVersion (ServerSwapState)(0), // 1: looprpc.ServerSwapState @@ -3192,19 +3287,20 @@ var file_server_proto_goTypes = []interface{}{ (*ReportRoutingResultReq)(nil), // 30: looprpc.ReportRoutingResultReq (*ReportRoutingResultRes)(nil), // 31: looprpc.ReportRoutingResultRes (*MuSig2SignSweepReq)(nil), // 32: looprpc.MuSig2SignSweepReq - (*MuSig2SignSweepRes)(nil), // 33: looprpc.MuSig2SignSweepRes - (*ServerPushKeyReq)(nil), // 34: looprpc.ServerPushKeyReq - (*ServerPushKeyRes)(nil), // 35: looprpc.ServerPushKeyRes - (*FetchL402Request)(nil), // 36: looprpc.FetchL402Request - (*FetchL402Response)(nil), // 37: looprpc.FetchL402Response - (*RouteHint)(nil), // 38: looprpc.RouteHint + (*PrevoutInfo)(nil), // 33: looprpc.PrevoutInfo + (*MuSig2SignSweepRes)(nil), // 34: looprpc.MuSig2SignSweepRes + (*ServerPushKeyReq)(nil), // 35: looprpc.ServerPushKeyReq + (*ServerPushKeyRes)(nil), // 36: looprpc.ServerPushKeyRes + (*FetchL402Request)(nil), // 37: looprpc.FetchL402Request + (*FetchL402Response)(nil), // 38: looprpc.FetchL402Response + (*RouteHint)(nil), // 39: looprpc.RouteHint } var file_server_proto_depIdxs = []int32{ 0, // 0: looprpc.ServerLoopOutRequest.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, // 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, // 6: looprpc.ServerLoopInTermsRequest.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 22, // 15: looprpc.CancelLoopOutSwapRequest.route_cancel:type_name -> looprpc.RouteCancel 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 4, // 19: looprpc.RecommendRoutingPluginRes.plugin:type_name -> looprpc.RoutingPlugin 0, // 20: looprpc.ReportRoutingResultReq.protocol_version:type_name -> looprpc.ProtocolVersion 4, // 21: looprpc.ReportRoutingResultReq.plugin:type_name -> looprpc.RoutingPlugin 0, // 22: looprpc.MuSig2SignSweepReq.protocol_version:type_name -> looprpc.ProtocolVersion - 0, // 23: looprpc.ServerPushKeyReq.protocol_version:type_name -> looprpc.ProtocolVersion - 9, // 24: looprpc.SwapServer.LoopOutTerms:input_type -> looprpc.ServerLoopOutTermsRequest - 5, // 25: looprpc.SwapServer.NewLoopOutSwap:input_type -> looprpc.ServerLoopOutRequest - 17, // 26: looprpc.SwapServer.LoopOutPushPreimage:input_type -> looprpc.ServerLoopOutPushPreimageRequest - 7, // 27: looprpc.SwapServer.LoopOutQuote:input_type -> looprpc.ServerLoopOutQuoteRequest - 15, // 28: looprpc.SwapServer.LoopInTerms:input_type -> looprpc.ServerLoopInTermsRequest - 11, // 29: looprpc.SwapServer.NewLoopInSwap:input_type -> looprpc.ServerLoopInRequest - 13, // 30: looprpc.SwapServer.LoopInQuote:input_type -> looprpc.ServerLoopInQuoteRequest - 19, // 31: looprpc.SwapServer.SubscribeLoopOutUpdates:input_type -> looprpc.SubscribeUpdatesRequest - 19, // 32: looprpc.SwapServer.SubscribeLoopInUpdates:input_type -> looprpc.SubscribeUpdatesRequest - 24, // 33: looprpc.SwapServer.CancelLoopOutSwap:input_type -> looprpc.CancelLoopOutSwapRequest - 26, // 34: looprpc.SwapServer.Probe:input_type -> looprpc.ServerProbeRequest - 28, // 35: looprpc.SwapServer.RecommendRoutingPlugin:input_type -> looprpc.RecommendRoutingPluginReq - 30, // 36: looprpc.SwapServer.ReportRoutingResult:input_type -> looprpc.ReportRoutingResultReq - 32, // 37: looprpc.SwapServer.MuSig2SignSweep:input_type -> looprpc.MuSig2SignSweepReq - 34, // 38: looprpc.SwapServer.PushKey:input_type -> looprpc.ServerPushKeyReq - 36, // 39: looprpc.SwapServer.FetchL402:input_type -> looprpc.FetchL402Request - 10, // 40: looprpc.SwapServer.LoopOutTerms:output_type -> looprpc.ServerLoopOutTerms - 6, // 41: looprpc.SwapServer.NewLoopOutSwap:output_type -> looprpc.ServerLoopOutResponse - 18, // 42: looprpc.SwapServer.LoopOutPushPreimage:output_type -> looprpc.ServerLoopOutPushPreimageResponse - 8, // 43: looprpc.SwapServer.LoopOutQuote:output_type -> looprpc.ServerLoopOutQuote - 16, // 44: looprpc.SwapServer.LoopInTerms:output_type -> looprpc.ServerLoopInTerms - 12, // 45: looprpc.SwapServer.NewLoopInSwap:output_type -> looprpc.ServerLoopInResponse - 14, // 46: looprpc.SwapServer.LoopInQuote:output_type -> looprpc.ServerLoopInQuoteResponse - 20, // 47: looprpc.SwapServer.SubscribeLoopOutUpdates:output_type -> looprpc.SubscribeLoopOutUpdatesResponse - 21, // 48: looprpc.SwapServer.SubscribeLoopInUpdates:output_type -> looprpc.SubscribeLoopInUpdatesResponse - 25, // 49: looprpc.SwapServer.CancelLoopOutSwap:output_type -> looprpc.CancelLoopOutSwapResponse - 27, // 50: looprpc.SwapServer.Probe:output_type -> looprpc.ServerProbeResponse - 29, // 51: looprpc.SwapServer.RecommendRoutingPlugin:output_type -> looprpc.RecommendRoutingPluginRes - 31, // 52: looprpc.SwapServer.ReportRoutingResult:output_type -> looprpc.ReportRoutingResultRes - 33, // 53: looprpc.SwapServer.MuSig2SignSweep:output_type -> looprpc.MuSig2SignSweepRes - 35, // 54: looprpc.SwapServer.PushKey:output_type -> looprpc.ServerPushKeyRes - 37, // 55: looprpc.SwapServer.FetchL402:output_type -> looprpc.FetchL402Response - 40, // [40:56] is the sub-list for method output_type - 24, // [24:40] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 33, // 23: looprpc.MuSig2SignSweepReq.prevout_info:type_name -> looprpc.PrevoutInfo + 0, // 24: looprpc.ServerPushKeyReq.protocol_version:type_name -> looprpc.ProtocolVersion + 9, // 25: looprpc.SwapServer.LoopOutTerms:input_type -> looprpc.ServerLoopOutTermsRequest + 5, // 26: looprpc.SwapServer.NewLoopOutSwap:input_type -> looprpc.ServerLoopOutRequest + 17, // 27: looprpc.SwapServer.LoopOutPushPreimage:input_type -> looprpc.ServerLoopOutPushPreimageRequest + 7, // 28: looprpc.SwapServer.LoopOutQuote:input_type -> looprpc.ServerLoopOutQuoteRequest + 15, // 29: looprpc.SwapServer.LoopInTerms:input_type -> looprpc.ServerLoopInTermsRequest + 11, // 30: looprpc.SwapServer.NewLoopInSwap:input_type -> looprpc.ServerLoopInRequest + 13, // 31: looprpc.SwapServer.LoopInQuote:input_type -> looprpc.ServerLoopInQuoteRequest + 19, // 32: looprpc.SwapServer.SubscribeLoopOutUpdates:input_type -> looprpc.SubscribeUpdatesRequest + 19, // 33: looprpc.SwapServer.SubscribeLoopInUpdates:input_type -> looprpc.SubscribeUpdatesRequest + 24, // 34: looprpc.SwapServer.CancelLoopOutSwap:input_type -> looprpc.CancelLoopOutSwapRequest + 26, // 35: looprpc.SwapServer.Probe:input_type -> looprpc.ServerProbeRequest + 28, // 36: looprpc.SwapServer.RecommendRoutingPlugin:input_type -> looprpc.RecommendRoutingPluginReq + 30, // 37: looprpc.SwapServer.ReportRoutingResult:input_type -> looprpc.ReportRoutingResultReq + 32, // 38: looprpc.SwapServer.MuSig2SignSweep:input_type -> looprpc.MuSig2SignSweepReq + 35, // 39: looprpc.SwapServer.PushKey:input_type -> looprpc.ServerPushKeyReq + 37, // 40: looprpc.SwapServer.FetchL402:input_type -> looprpc.FetchL402Request + 10, // 41: looprpc.SwapServer.LoopOutTerms:output_type -> looprpc.ServerLoopOutTerms + 6, // 42: looprpc.SwapServer.NewLoopOutSwap:output_type -> looprpc.ServerLoopOutResponse + 18, // 43: looprpc.SwapServer.LoopOutPushPreimage:output_type -> looprpc.ServerLoopOutPushPreimageResponse + 8, // 44: looprpc.SwapServer.LoopOutQuote:output_type -> looprpc.ServerLoopOutQuote + 16, // 45: looprpc.SwapServer.LoopInTerms:output_type -> looprpc.ServerLoopInTerms + 12, // 46: looprpc.SwapServer.NewLoopInSwap:output_type -> looprpc.ServerLoopInResponse + 14, // 47: looprpc.SwapServer.LoopInQuote:output_type -> looprpc.ServerLoopInQuoteResponse + 20, // 48: looprpc.SwapServer.SubscribeLoopOutUpdates:output_type -> looprpc.SubscribeLoopOutUpdatesResponse + 21, // 49: looprpc.SwapServer.SubscribeLoopInUpdates:output_type -> looprpc.SubscribeLoopInUpdatesResponse + 25, // 50: looprpc.SwapServer.CancelLoopOutSwap:output_type -> looprpc.CancelLoopOutSwapResponse + 27, // 51: looprpc.SwapServer.Probe:output_type -> looprpc.ServerProbeResponse + 29, // 52: looprpc.SwapServer.RecommendRoutingPlugin:output_type -> looprpc.RecommendRoutingPluginRes + 31, // 53: looprpc.SwapServer.ReportRoutingResult:output_type -> looprpc.ReportRoutingResultRes + 34, // 54: looprpc.SwapServer.MuSig2SignSweep:output_type -> looprpc.MuSig2SignSweepRes + 36, // 55: looprpc.SwapServer.PushKey:output_type -> looprpc.ServerPushKeyRes + 38, // 56: looprpc.SwapServer.FetchL402:output_type -> looprpc.FetchL402Response + 41, // [41:57] is the sub-list for method output_type + 25, // [25:41] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension 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() } @@ -3607,7 +3704,7 @@ func file_server_proto_init() { } } file_server_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MuSig2SignSweepRes); i { + switch v := v.(*PrevoutInfo); i { case 0: return &v.state case 1: @@ -3619,7 +3716,7 @@ func file_server_proto_init() { } } file_server_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ServerPushKeyReq); i { + switch v := v.(*MuSig2SignSweepRes); i { case 0: return &v.state case 1: @@ -3631,7 +3728,7 @@ func file_server_proto_init() { } } file_server_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ServerPushKeyRes); i { + switch v := v.(*ServerPushKeyReq); i { case 0: return &v.state case 1: @@ -3643,7 +3740,7 @@ func file_server_proto_init() { } } file_server_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FetchL402Request); i { + switch v := v.(*ServerPushKeyRes); i { case 0: return &v.state case 1: @@ -3655,6 +3752,18 @@ func file_server_proto_init() { } } 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 { case 0: return &v.state @@ -3676,7 +3785,7 @@ func file_server_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_server_proto_rawDesc, NumEnums: 5, - NumMessages: 33, + NumMessages: 34, NumExtensions: 0, NumServices: 1, }, diff --git a/swapserverrpc/server.proto b/swapserverrpc/server.proto index 2dc5aaa..63a50b4 100644 --- a/swapserverrpc/server.proto +++ b/swapserverrpc/server.proto @@ -1,12 +1,11 @@ syntax = "proto3"; -import "common.proto"; - // 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 // call the wrong endpoints. Luckily with the go_package option we can have // different golang and RPC package names to fix protobuf namespace conflicts. package looprpc; +import "common.proto"; option go_package = "github.com/lightninglabs/loop/swapserverrpc"; @@ -515,7 +514,6 @@ message ServerProbeRequest { // The protocol version that the client adheres to. ProtocolVersion protocol_version = 1; - // The probe amount. uint64 amt = 2; // The target node for the probe. @@ -598,6 +596,23 @@ message MuSig2SignSweepReq { // The psbt of the sweep txn. 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 { diff --git a/sweepbatcher/log.go b/sweepbatcher/log.go new file mode 100644 index 0000000..20a3136 --- /dev/null +++ b/sweepbatcher/log.go @@ -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 +} diff --git a/sweepbatcher/store.go b/sweepbatcher/store.go new file mode 100644 index 0000000..2bd3ea2 --- /dev/null +++ b/sweepbatcher/store.go @@ -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, + } +} diff --git a/sweepbatcher/store_mock.go b/sweepbatcher/store_mock.go new file mode 100644 index 0000000..fef88c7 --- /dev/null +++ b/sweepbatcher/store_mock.go @@ -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 +} diff --git a/sweepbatcher/sweep_batch.go b/sweepbatcher/sweep_batch.go new file mode 100644 index 0000000..971b83d --- /dev/null +++ b/sweepbatcher/sweep_batch.go @@ -0,0 +1,1382 @@ +package sweepbatcher + +import ( + "bytes" + "context" + "fmt" + "math" + "sync" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btclog" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/labels" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/swap" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" +) + +const ( + // defaultFeeRateStep is the default value by which the batch tx's + // fee rate is increased when an rbf is attempted. + defaultFeeRateStep = chainfee.SatPerKWeight(100) + + // defaultBatchConfTarget is the default confirmation target of the + // batch transaction. + defaultBatchConfTarget = 12 + + // batchConfHeight is the default confirmation height of the batch + // transaction. + batchConfHeight = 3 + + // maxFeeToSwapAmtRatio is the maximum fee to swap amount ratio that + // we allow for a batch transaction. + maxFeeToSwapAmtRatio = 0.2 +) + +var ( + ErrBatchShuttingDown = fmt.Errorf("batch shutting down") +) + +// sweep stores any data related to sweeping a specific outpoint. +type sweep struct { + // swapHash is the hash of the swap that the sweep belongs to. + swapHash lntypes.Hash + + // outpoint is the outpoint being swept. + outpoint wire.OutPoint + + // value is the value of the outpoint being swept. + value btcutil.Amount + + // confTarget is the confirmation target of the sweep. + confTarget int32 + + // timeout is the timeout of the swap that the sweep belongs to. + timeout int32 + + // initiationHeight is the height at which the swap was initiated. + initiationHeight int32 + + // htlc is the HTLC that is being swept. + htlc swap.Htlc + + // preimage is the preimage of the HTLC that is being swept. + preimage lntypes.Preimage + + // swapInvoicePaymentAddr is the payment address of the swap invoice. + swapInvoicePaymentAddr [32]byte + + // htlcKeys is the set of keys used to sign the HTLC. + htlcKeys loopdb.HtlcKeys + + // htlcSuccessEstimator is a function that estimates the weight of the + // HTLC success script. + htlcSuccessEstimator func(*input.TxWeightEstimator) error + + // protocolVersion is the protocol version of the swap that the sweep + // belongs to. + protocolVersion loopdb.ProtocolVersion + + // isExternalAddr is true if the sweep spends to a non-wallet address. + isExternalAddr bool + + // destAddr is the destination address of the sweep. + destAddr btcutil.Address + + // notifier is a collection of channels used to communicate the status + // of the sweep back to the swap that requested it. + notifier *SpendNotifier +} + +// batchState is the state of the batch. +type batchState uint8 + +const ( + // Open is the state in which the batch is able to accept new sweeps. + Open batchState = 0 + + // Closed is the state in which the batch is no longer able to accept + // new sweeps. + Closed batchState = 1 + + // Confirmed is the state in which the batch transaction has reached the + // configured conf height. + Confirmed batchState = 2 +) + +// batchConfig is the configuration for a batch. +type batchConfig struct { + // maxTimeoutDistance is the maximum timeout distance that 2 distinct + // sweeps can have in the same batch. + maxTimeoutDistance int32 + + // batchConfTarget is the confirmation target of the batch transaction. + batchConfTarget int32 + + // batchPublishDelay is the delay between receiving a new block and + // publishing the batch transaction. + batchPublishDelay time.Duration +} + +// rbfCache stores data related to our last fee bump. +type rbfCache struct { + // LastHeight is the last height at which we increased our feerate. + LastHeight int32 + + // FeeRate is the last used fee rate we used to publish a batch tx. + FeeRate chainfee.SatPerKWeight +} + +// batch is a collection of sweeps that are published together. +type batch struct { + // id is the primary identifier of this batch. + id int32 + + // state is the current state of the batch. + state batchState + + // primarySweepID is the swap hash of the primary sweep in the batch. + primarySweepID lntypes.Hash + + // sweeps store the sweeps that this batch currently contains. + sweeps map[lntypes.Hash]sweep + + // currentHeight is the current block height. + currentHeight int32 + + // blockEpochChan is the channel over which block epoch notifications + // are received. + blockEpochChan chan int32 + + // spendChan is the channel over which spend notifications are received. + spendChan chan *chainntnfs.SpendDetail + + // confChan is the channel over which confirmation notifications are + // received. + confChan chan *chainntnfs.TxConfirmation + + // reorgChan is the channel over which reorg notifications are received. + reorgChan chan struct{} + + // errChan is the channel over which errors are received. + errChan chan error + + // batchTx is the transaction that is currently being monitored for + // confirmations. + batchTxid *chainhash.Hash + + // batchPkScript is the pkScript of the batch transaction's output. + batchPkScript []byte + + // batchAddress is the address of the batch transaction's output. + batchAddress btcutil.Address + + // rbfCache stores data related to the RBF fee bumping mechanism. + rbfCache rbfCache + + // callEnter is used to sequentialize calls to the batch handler's + // main event loop. + callEnter chan struct{} + + // callLeave is used to resume the execution flow of the batch handler's + // main event loop. + callLeave chan struct{} + + // quit signals that the batch must stop. + quit chan struct{} + + // wallet is the wallet client used to create and publish the batch + // transaction. + wallet lndclient.WalletKitClient + + // chainNotifier is the chain notifier client used to monitor the + // blockchain for spends and confirmations. + chainNotifier lndclient.ChainNotifierClient + + // signerClient is the signer client used to sign the batch transaction. + signerClient lndclient.SignerClient + + // muSig2Kit includes all the required functionality to collect + // and verify signatures by the swap server in order to cooperatively + // sweep funds. + muSig2SignSweep MuSig2SignSweep + + // verifySchnorrSig is a function that verifies a schnorr signature. + verifySchnorrSig VerifySchnorrSig + + // purger is a function that can take a sweep which is being purged and + // hand it over to the batcher for further processing. + purger Purger + + // store includes all the database interactions that are needed by the + // batch. + store BatcherStore + + // cfg is the configuration for this batch. + cfg *batchConfig + + // log is the logger for this batch. + log btclog.Logger + + wg sync.WaitGroup +} + +// Purger is a function that takes a sweep request and feeds it back to the +// batcher main entry point. The name is inspired by its purpose, which is to +// purge the batch from sweeps that didn't make it to the confirmed tx. +type Purger func(sweepReq *SweepRequest) error + +// batchKit is a kit of dependencies that are used to initialize a batch. This +// struct is only used as a wrapper for the arguments that are required to +// create a new batch. +type batchKit struct { + id int32 + batchTxid *chainhash.Hash + batchPkScript []byte + state batchState + primaryID lntypes.Hash + sweeps map[lntypes.Hash]sweep + rbfCache rbfCache + returnChan chan SweepRequest + wallet lndclient.WalletKitClient + chainNotifier lndclient.ChainNotifierClient + signerClient lndclient.SignerClient + musig2SignSweep MuSig2SignSweep + verifySchnorrSig VerifySchnorrSig + purger Purger + store BatcherStore + log btclog.Logger +} + +// scheduleNextCall schedules the next call to the batch handler's main event +// loop. It returns a function that must be called when the call is finished. +func (b *batch) scheduleNextCall() (func(), error) { + select { + case b.callEnter <- struct{}{}: + + case <-b.quit: + return func() {}, ErrBatchShuttingDown + } + + return func() { + b.callLeave <- struct{}{} + }, nil +} + +// NewBatch creates a new batch. +func NewBatch(cfg batchConfig, bk batchKit) *batch { + return &batch{ + // We set the ID to a negative value to flag that this batch has + // never been persisted, so it needs to be assigned a new ID. + id: -1, + state: Open, + sweeps: make(map[lntypes.Hash]sweep), + blockEpochChan: make(chan int32), + spendChan: make(chan *chainntnfs.SpendDetail), + confChan: make(chan *chainntnfs.TxConfirmation, 1), + reorgChan: make(chan struct{}, 1), + errChan: make(chan error, 1), + callEnter: make(chan struct{}), + callLeave: make(chan struct{}), + quit: make(chan struct{}), + batchTxid: bk.batchTxid, + wallet: bk.wallet, + chainNotifier: bk.chainNotifier, + signerClient: bk.signerClient, + muSig2SignSweep: bk.musig2SignSweep, + verifySchnorrSig: bk.verifySchnorrSig, + purger: bk.purger, + store: bk.store, + cfg: &cfg, + } +} + +// NewBatchFromDB creates a new batch that already existed in storage. +func NewBatchFromDB(cfg batchConfig, bk batchKit) *batch { + return &batch{ + id: bk.id, + state: bk.state, + primarySweepID: bk.primaryID, + sweeps: bk.sweeps, + blockEpochChan: make(chan int32), + spendChan: make(chan *chainntnfs.SpendDetail), + confChan: make(chan *chainntnfs.TxConfirmation, 1), + reorgChan: make(chan struct{}, 1), + errChan: make(chan error, 1), + callEnter: make(chan struct{}), + callLeave: make(chan struct{}), + quit: make(chan struct{}), + batchTxid: bk.batchTxid, + batchPkScript: bk.batchPkScript, + rbfCache: bk.rbfCache, + wallet: bk.wallet, + chainNotifier: bk.chainNotifier, + signerClient: bk.signerClient, + muSig2SignSweep: bk.musig2SignSweep, + verifySchnorrSig: bk.verifySchnorrSig, + purger: bk.purger, + store: bk.store, + log: bk.log, + cfg: &cfg, + } +} + +// addSweep tries to add a sweep to the batch. If this is the first sweep being +// added to the batch then it also sets the primary sweep ID. +func (b *batch) addSweep(ctx context.Context, sweep *sweep) (bool, error) { + done, err := b.scheduleNextCall() + defer done() + + if err != nil { + return false, err + } + + // If the provided sweep is nil, we can't proceed with any checks, so + // we just return early. + if sweep == nil { + return false, nil + } + + // Before we run through the acceptance checks, let's just see if this + // sweep is already in our batch. In that case, just update the sweep. + _, ok := b.sweeps[sweep.swapHash] + if ok { + // If the sweep was resumed from storage, and the swap requested + // to sweep again, a new sweep notifier will be created by the + // swap. By re-assigning to the batch's sweep we make sure that + // everything, including the notifier, is up to date. + b.sweeps[sweep.swapHash] = *sweep + + // If this is the primary sweep, we also need to update the + // batch's confirmation target. + if b.primarySweepID == sweep.swapHash { + b.cfg.batchConfTarget = sweep.confTarget + } + + return true, nil + } + + // Since all the actions of the batch happen sequentially, we could + // arrive here after the batch got closed because of a spend. In this + // case we cannot add the sweep to this batch. + if b.state != Open { + return false, nil + } + + // If this batch contains a single sweep that spends to a non-wallet + // address, or the incoming sweep is spending to non-wallet address, + // we cannot add this sweep to the batch. + for _, s := range b.sweeps { + if s.isExternalAddr || sweep.isExternalAddr { + return false, nil + } + } + + // Check the timeout of the incoming sweep against the timeout of all + // already contained sweeps. If that difference exceeds the configured + // maximum we cannot add this sweep. + for _, s := range b.sweeps { + timeoutDistance := + int32(math.Abs(float64(sweep.timeout - s.timeout))) + + if timeoutDistance > b.cfg.maxTimeoutDistance { + return false, nil + } + } + + // Past this point we know that a new incoming sweep passes the + // acceptance criteria and is now ready to be added to this batch. + + // If this is the first sweep being added to the batch, make it the + // primary sweep. + if b.primarySweepID == lntypes.ZeroHash { + b.primarySweepID = sweep.swapHash + b.cfg.batchConfTarget = sweep.confTarget + + // We also need to start the spend monitor for this new primary + // sweep. + err := b.monitorSpend(ctx, *sweep) + if err != nil { + return false, err + } + } + + // Add the sweep to the batch's sweeps. + b.log.Infof("adding sweep %x", sweep.swapHash[:6]) + b.sweeps[sweep.swapHash] = *sweep + + return true, b.persistSweep(ctx, *sweep, false) +} + +// sweepExists returns true if the batch contains the sweep with the given hash. +func (b *batch) sweepExists(hash lntypes.Hash) bool { + done, err := b.scheduleNextCall() + defer done() + if err != nil { + return false + } + + _, ok := b.sweeps[hash] + + return ok +} + +// Wait waits for the batch to gracefully stop. +func (b *batch) Wait() { + b.log.Infof("Stopping") + b.wg.Wait() +} + +// Run is the batch's main event loop. +func (b *batch) Run(ctx context.Context) error { + runCtx, cancel := context.WithCancel(ctx) + defer func() { + cancel() + close(b.quit) + b.wg.Wait() + }() + + if b.muSig2SignSweep == nil { + return fmt.Errorf("no musig2 signer available") + } + + blockChan, blockErrChan, err := + b.chainNotifier.RegisterBlockEpochNtfn(runCtx) + if err != nil { + return err + } + + // If a primary sweep exists we immediately start monitoring for its + // spend. + if b.primarySweepID != lntypes.ZeroHash { + sweep := b.sweeps[b.primarySweepID] + err := b.monitorSpend(runCtx, sweep) + if err != nil { + return err + } + } + + // We use a timer in order to not publish new transactions at the same + // time as the block epoch notification. This is done to prevent + // unnecessary transaction publishments when a spend is detected on that + // block. + var timerChan <-chan time.Time + + b.log.Infof("started, primary %x, total sweeps %v", + b.primarySweepID[0:6], len(b.sweeps)) + + for { + select { + case <-b.callEnter: + <-b.callLeave + + case height := <-blockChan: + b.log.Debugf("received block %v", height) + + // Set the timer to publish the batch transaction after + // the configured delay. + timerChan = time.After(b.cfg.batchPublishDelay) + b.currentHeight = height + + case <-timerChan: + if b.state == Open { + err := b.publish(ctx) + if err != nil { + return err + } + } + + case spend := <-b.spendChan: + err := b.handleSpend(runCtx, spend.SpendingTx) + if err != nil { + return err + } + + case <-b.confChan: + return b.handleConf(runCtx) + + case <-b.reorgChan: + b.state = Open + b.log.Warnf("reorg detected, batch is able to accept " + + "new sweeps") + + err := b.monitorSpend(ctx, b.sweeps[b.primarySweepID]) + if err != nil { + return err + } + + case err := <-blockErrChan: + return err + + case err := <-b.errChan: + return err + + case <-runCtx.Done(): + return runCtx.Err() + } + } +} + +// publish creates and publishes the latest batch transaction to the network. +func (b *batch) publish(ctx context.Context) error { + var ( + err error + fee btcutil.Amount + coopSuccess bool + ) + + // Run the RBF rate update. + err = b.updateRbfRate(ctx) + if err != nil { + return err + } + + fee, err, coopSuccess = b.publishBatchCoop(ctx) + if err != nil { + b.log.Warnf("co-op publish error: %v", err) + } + + if !coopSuccess { + fee, err = b.publishBatch(ctx) + } + if err != nil { + b.log.Warnf("publish error: %v", err) + return nil + } + + b.log.Infof("published, total sweeps: %v, fees: %v", len(b.sweeps), fee) + for _, sweep := range b.sweeps { + b.log.Infof("published sweep %x, value: %v", + sweep.swapHash[:6], sweep.value) + } + + return b.persist(ctx) +} + +// publishBatch creates and publishes the batch transaction. It will consult the +// RBFCache to determine the fee rate to use. +func (b *batch) publishBatch(ctx context.Context) (btcutil.Amount, error) { + // Create the batch transaction. + batchTx := wire.NewMsgTx(2) + batchTx.LockTime = uint32(b.currentHeight) + + var ( + batchAmt btcutil.Amount + prevOuts = make([]*wire.TxOut, 0, len(b.sweeps)) + signDescs = make([]*lndclient.SignDescriptor, 0, len(b.sweeps)) + sweeps = make([]sweep, 0, len(b.sweeps)) + fee btcutil.Amount + inputCounter int + addrOverride bool + ) + + var weightEstimate input.TxWeightEstimator + + // Add all the sweeps to the batch transaction. + for _, sweep := range b.sweeps { + if sweep.isExternalAddr { + addrOverride = true + } + + batchAmt += sweep.value + batchTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: sweep.outpoint, + Sequence: sweep.htlc.SuccessSequence(), + }) + + err := sweep.htlcSuccessEstimator(&weightEstimate) + if err != nil { + return 0, err + } + + // Append this sweep to an array of sweeps. This is needed to + // keep the order of sweeps stored, as iterating the sweeps map + // does not guarantee same order. + sweeps = append(sweeps, sweep) + + // Create and store the previous outpoint for this sweep. + prevOuts = append(prevOuts, &wire.TxOut{ + Value: int64(sweep.value), + PkScript: sweep.htlc.PkScript, + }) + + key, err := btcec.ParsePubKey( + sweep.htlcKeys.ReceiverScriptKey[:], + ) + if err != nil { + return fee, err + } + + // Create and store the sign descriptor for this sweep. + signDesc := lndclient.SignDescriptor{ + WitnessScript: sweep.htlc.SuccessScript(), + Output: prevOuts[len(prevOuts)-1], + HashType: sweep.htlc.SigHash(), + InputIndex: inputCounter, + KeyDesc: keychain.KeyDescriptor{ + PubKey: key, + }, + } + + inputCounter++ + + if sweep.htlc.Version == swap.HtlcV3 { + signDesc.SignMethod = input.TaprootScriptSpendSignMethod + } + + signDescs = append(signDescs, &signDesc) + } + + var address btcutil.Address + + if addrOverride { + // Sanity check, there should be exactly 1 sweep in this batch. + if len(sweeps) != 1 { + return 0, fmt.Errorf("external address sweep batched " + + "with other sweeps") + } + + address = sweeps[0].destAddr + } else { + var err error + address, err = b.getBatchDestAddr(ctx) + if err != nil { + return fee, err + } + } + + batchPkScript, err := txscript.PayToAddrScript(address) + if err != nil { + return fee, err + } + + weightEstimate.AddP2TROutput() + + totalWeight := int64(weightEstimate.Weight()) + + fee = b.rbfCache.FeeRate.FeeForWeight(totalWeight) + + // Clamp the calculated fee to the max allowed fee amount for the batch. + fee = clampBatchFee(fee, batchAmt) + + // Add the batch transaction output, which excludes the fees paid to + // miners. + batchTx.AddTxOut(&wire.TxOut{ + PkScript: batchPkScript, + Value: int64(batchAmt - fee), + }) + + // Collect the signatures for our inputs. + rawSigs, err := b.signerClient.SignOutputRaw( + ctx, batchTx, signDescs, prevOuts, + ) + if err != nil { + return fee, err + } + + for i, sweep := range sweeps { + // Generate the success witness for the sweep. + witness, err := sweep.htlc.GenSuccessWitness( + rawSigs[i], sweep.preimage, + ) + if err != nil { + return fee, err + } + + // Add the success witness to our batch transaction's inputs. + batchTx.TxIn[i].Witness = witness + } + + b.log.Debugf("attempting to publish non-coop tx with feerate=%v, "+ + "totalfee=%v, sweeps=%v, destAddr=%s", b.rbfCache.FeeRate, fee, + len(batchTx.TxIn), address.String()) + + err = b.wallet.PublishTransaction( + ctx, batchTx, labels.LoopOutBatchSweepSuccess(b.id), + ) + if err != nil { + return fee, err + } + + // Store the batch transaction's txid and pkScript, for monitoring + // purposes. + txHash := batchTx.TxHash() + b.batchTxid = &txHash + b.batchPkScript = batchPkScript + + return fee, nil +} + +// publishBatchCoop attempts to construct and publish a batch transaction that +// collects all the required signatures interactively from the server. This +// helps with collecting the funds immediately without revealing any information +// related to the HTLC script. +func (b *batch) publishBatchCoop(ctx context.Context) (btcutil.Amount, + error, bool) { + + var ( + batchAmt = btcutil.Amount(0) + sweeps = make([]sweep, 0, len(b.sweeps)) + fee = btcutil.Amount(0) + weightEstimate input.TxWeightEstimator + addrOverride bool + ) + + // Sanity check, there should be at least 1 sweep in this batch. + if len(b.sweeps) == 0 { + return 0, fmt.Errorf("no sweeps in batch"), false + } + + // Create the batch transaction. + batchTx := &wire.MsgTx{ + Version: 2, + LockTime: uint32(b.currentHeight), + } + + for _, sweep := range b.sweeps { + // Append this sweep to an array of sweeps. This is needed to + // keep the order of sweeps stored, as iterating the sweeps map + // does not guarantee same order. + sweeps = append(sweeps, sweep) + } + + // Add all the sweeps to the batch transaction. + for _, sweep := range sweeps { + if sweep.isExternalAddr { + addrOverride = true + } + + // Keep track of the total amount this batch is sweeping back. + batchAmt += sweep.value + + // Add this sweep's input to the transaction. + batchTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: sweep.outpoint, + }) + + weightEstimate.AddTaprootKeySpendInput(txscript.SigHashAll) + } + + var address btcutil.Address + + if addrOverride { + // Sanity check, there should be exactly 1 sweep in this batch. + if len(sweeps) != 1 { + return 0, fmt.Errorf("external address sweep batched " + + "with other sweeps"), false + } + + address = sweeps[0].destAddr + } else { + var err error + address, err = b.getBatchDestAddr(ctx) + if err != nil { + return fee, err, false + } + } + + batchPkScript, err := txscript.PayToAddrScript(address) + if err != nil { + return fee, err, false + } + + weightEstimate.AddP2TROutput() + + totalWeight := int64(weightEstimate.Weight()) + + fee = b.rbfCache.FeeRate.FeeForWeight(totalWeight) + + // Clamp the calculated fee to the max allowed fee amount for the batch. + fee = clampBatchFee(fee, batchAmt) + + // Add the batch transaction output, which excludes the fees paid to + // miners. + batchTx.AddTxOut(&wire.TxOut{ + PkScript: batchPkScript, + Value: int64(batchAmt - fee), + }) + + packet, err := psbt.NewFromUnsignedTx(batchTx) + if err != nil { + return fee, err, false + } + + if len(packet.Inputs) != len(sweeps) { + return fee, fmt.Errorf("invalid number of packet inputs"), false + } + + prevOuts := make(map[wire.OutPoint]*wire.TxOut) + + for i, sweep := range sweeps { + txOut := &wire.TxOut{ + Value: int64(sweep.value), + PkScript: sweep.htlc.PkScript, + } + + prevOuts[sweep.outpoint] = txOut + packet.Inputs[i].WitnessUtxo = txOut + } + + var psbtBuf bytes.Buffer + err = packet.Serialize(&psbtBuf) + if err != nil { + return fee, err, false + } + + prevOutputFetcher := txscript.NewMultiPrevOutFetcher(prevOuts) + + // Attempt to cooperatively sign the batch tx with the server. + err = b.coopSignBatchTx( + ctx, packet, sweeps, prevOutputFetcher, prevOuts, psbtBuf, + ) + if err != nil { + return fee, err, false + } + + b.log.Debugf("attempting to publish coop tx with feerate=%v, "+ + "totalfee=%v, sweeps=%v, destAddr=%s", b.rbfCache.FeeRate, fee, + len(batchTx.TxIn), address.String()) + + err = b.wallet.PublishTransaction( + ctx, batchTx, labels.LoopOutBatchSweepSuccess(b.id), + ) + if err != nil { + return fee, err, true + } + + // Store the batch transaction's txid and pkScript, for monitoring + // purposes. + txHash := batchTx.TxHash() + b.batchTxid = &txHash + b.batchPkScript = batchPkScript + + return fee, nil, true +} + +// coopSignBatchTx collects the necessary signatures from the server in order +// to cooperatively sweep the funds. +func (b *batch) coopSignBatchTx(ctx context.Context, packet *psbt.Packet, + sweeps []sweep, prevOutputFetcher *txscript.MultiPrevOutFetcher, + prevOuts map[wire.OutPoint]*wire.TxOut, psbtBuf bytes.Buffer) error { + + for i, sweep := range sweeps { + sweep := sweep + + sigHashes := txscript.NewTxSigHashes( + packet.UnsignedTx, prevOutputFetcher, + ) + + sigHash, err := txscript.CalcTaprootSignatureHash( + sigHashes, txscript.SigHashDefault, packet.UnsignedTx, + i, prevOutputFetcher, + ) + if err != nil { + return err + } + + var ( + signers [][]byte + muSig2Version input.MuSig2Version + ) + + // Depending on the MuSig2 version we either pass 32 byte + // Schnorr public keys or normal 33 byte public keys. + if sweep.protocolVersion >= loopdb.ProtocolVersionMuSig2 { + muSig2Version = input.MuSig2Version100RC2 + signers = [][]byte{ + sweep.htlcKeys.SenderInternalPubKey[:], + sweep.htlcKeys.ReceiverInternalPubKey[:], + } + } else { + muSig2Version = input.MuSig2Version040 + signers = [][]byte{ + sweep.htlcKeys.SenderInternalPubKey[1:], + sweep.htlcKeys.ReceiverInternalPubKey[1:], + } + } + + htlcScript, ok := sweep.htlc.HtlcScript.(*swap.HtlcScriptV3) + if !ok { + return fmt.Errorf("invalid htlc script version") + } + + // Now we're creating a local MuSig2 session using the receiver + // key's key locator and the htlc's root hash. + musig2SessionInfo, err := b.signerClient.MuSig2CreateSession( + ctx, muSig2Version, + &sweep.htlcKeys.ClientScriptKeyLocator, signers, + lndclient.MuSig2TaprootTweakOpt( + htlcScript.RootHash[:], false, + ), + ) + if err != nil { + return err + } + + // With the session active, we can now send the server our + // public nonce and the sig hash, so that it can create it's own + // MuSig2 session and return the server side nonce and partial + // signature. + serverNonce, serverSig, err := b.muSig2SignSweep( + ctx, sweep.protocolVersion, sweep.swapHash, + sweep.swapInvoicePaymentAddr, + musig2SessionInfo.PublicNonce[:], psbtBuf.Bytes(), + prevOuts, + ) + if err != nil { + return err + } + + var serverPublicNonce [musig2.PubNonceSize]byte + copy(serverPublicNonce[:], serverNonce) + + // Register the server's nonce before attempting to create our + // partial signature. + haveAllNonces, err := b.signerClient.MuSig2RegisterNonces( + ctx, musig2SessionInfo.SessionID, + [][musig2.PubNonceSize]byte{serverPublicNonce}, + ) + if err != nil { + return err + } + + // Sanity check that we have all the nonces. + if !haveAllNonces { + return fmt.Errorf("invalid MuSig2 session: " + + "nonces missing") + } + + var digest [32]byte + copy(digest[:], sigHash) + + // Since our MuSig2 session has all nonces, we can now create + // the local partial signature by signing the sig hash. + _, err = b.signerClient.MuSig2Sign( + ctx, musig2SessionInfo.SessionID, digest, false, + ) + if err != nil { + return err + } + + // Now combine the partial signatures to use the final combined + // signature in the sweep transaction's witness. + haveAllSigs, finalSig, err := b.signerClient.MuSig2CombineSig( + ctx, musig2SessionInfo.SessionID, [][]byte{serverSig}, + ) + if err != nil { + return err + } + + if !haveAllSigs { + return fmt.Errorf("failed to combine signatures") + } + + // To be sure that we're good, parse and validate that the + // combined signature is indeed valid for the sig hash and the + // internal pubkey. + err = b.verifySchnorrSig( + htlcScript.TaprootKey, sigHash, finalSig, + ) + if err != nil { + return err + } + + packet.UnsignedTx.TxIn[i].Witness = wire.TxWitness{ + finalSig, + } + } + + return nil +} + +// updateRbfRate updates the fee rate we should use for the new batch +// transaction. This fee rate does not guarantee RBF success, but the continuous +// increase leads to an eventual successful RBF replacement. +func (b *batch) updateRbfRate(ctx context.Context) error { + // If the feeRate is unset then we never published before, so we + // retrieve the fee estimate from our wallet. + if b.rbfCache.FeeRate == 0 { + b.log.Infof("initializing rbf fee rate for conf target=%v", + b.cfg.batchConfTarget) + rate, err := b.wallet.EstimateFeeRate( + ctx, b.cfg.batchConfTarget, + ) + if err != nil { + return err + } + + // Set the initial value for our fee rate. + b.rbfCache.FeeRate = rate + } else { + // Bump the fee rate by the configured step. + b.rbfCache.FeeRate += defaultFeeRateStep + } + + b.rbfCache.LastHeight = b.currentHeight + + return b.persist(ctx) +} + +// monitorSpend monitors the primary sweep's outpoint for spends. The reason we +// monitor the primary sweep's outpoint is because the primary sweep was the +// first sweep that entered this batch, therefore it is present in all the +// versions of the batch transaction. This means that even if an older version +// of the batch transaction gets confirmed, due to the uncertainty of RBF +// replacements and network propagation, we can always detect the transaction. +func (b *batch) monitorSpend(ctx context.Context, primarySweep sweep) error { + spendCtx, cancel := context.WithCancel(ctx) + + spendChan, spendErr, err := b.chainNotifier.RegisterSpendNtfn( + spendCtx, &primarySweep.outpoint, primarySweep.htlc.PkScript, + primarySweep.initiationHeight, + ) + if err != nil { + cancel() + return err + } + + b.wg.Add(1) + go func() { + defer cancel() + defer b.wg.Done() + + b.log.Infof("monitoring spend for outpoint %s", + primarySweep.outpoint.String()) + + for { + select { + case spend := <-spendChan: + select { + case b.spendChan <- spend: + + case <-ctx.Done(): + } + + return + + case err := <-spendErr: + b.writeToErrChan(err) + return + + case <-ctx.Done(): + return + } + } + }() + + return nil +} + +// monitorConfirmations monitors the batch transaction for confirmations. +func (b *batch) monitorConfirmations(ctx context.Context) error { + reorgChan := make(chan struct{}) + + confCtx, cancel := context.WithCancel(ctx) + + confChan, errChan, err := b.chainNotifier.RegisterConfirmationsNtfn( + confCtx, b.batchTxid, b.batchPkScript, batchConfHeight, + b.currentHeight, lndclient.WithReOrgChan(reorgChan), + ) + if err != nil { + cancel() + return err + } + + b.wg.Add(1) + go func() { + defer cancel() + defer b.wg.Done() + + for { + select { + case conf := <-confChan: + select { + case b.confChan <- conf: + + case <-ctx.Done(): + } + return + + case err := <-errChan: + b.writeToErrChan(err) + return + + case <-reorgChan: + // A re-org has been detected. We set the batch + // state back to open since our batch + // transaction is no longer present in any + // block. We can accept more sweeps and try to + // publish new transactions, at this point we + // need to monitor again for a new spend. + select { + case b.reorgChan <- struct{}{}: + case <-ctx.Done(): + } + return + + case <-ctx.Done(): + return + } + } + }() + + return nil +} + +// handleSpend handles a spend notification. +func (b *batch) handleSpend(ctx context.Context, spendTx *wire.MsgTx) error { + var ( + txHash = spendTx.TxHash() + purgeList = make([]SweepRequest, 0, len(b.sweeps)) + notifyList = make([]sweep, 0, len(b.sweeps)) + ) + b.batchTxid = &txHash + b.batchPkScript = spendTx.TxOut[0].PkScript + + // As a previous version of the batch transaction may get confirmed, + // which does not contain the latest sweeps, we need to detect the + // sweeps that did not make it to the confirmed transaction and feed + // them back to the batcher. This will ensure that the sweeps will enter + // a new batch instead of remaining dangling. + for _, sweep := range b.sweeps { + found := false + + for _, txIn := range spendTx.TxIn { + if txIn.PreviousOutPoint == sweep.outpoint { + found = true + notifyList = append(notifyList, sweep) + } + } + + // If the sweep's outpoint was not found in the transaction's + // inputs this means it was left out. So we delete it from this + // batch and feed it back to the batcher. + if !found { + newSweep := sweep + delete(b.sweeps, sweep.swapHash) + purgeList = append(purgeList, SweepRequest{ + SwapHash: newSweep.swapHash, + Outpoint: newSweep.outpoint, + Value: newSweep.value, + Notifier: newSweep.notifier, + }) + } + } + + for _, sweep := range notifyList { + // Save the sweep as completed. + err := b.persistSweep(ctx, sweep, true) + if err != nil { + return err + } + + // If the sweep's notifier is empty then this means that a swap + // is not waiting to read an update from it, so we can skip + // the notification part. + if sweep.notifier == nil || + *sweep.notifier == (SpendNotifier{}) { + + continue + } + + // Dispatch the sweep notifier, we don't care about the outcome + // of this action so we don't wait for it. + go notifySweepSpend(ctx, sweep, spendTx) + } + + // Proceed with purging the sweeps. This will feed the sweeps that + // didn't make it to the confirmed batch transaction back to the batcher + // for re-entry. This batch doesn't care for the outcome of this + // operation so we don't wait for it. + go func() { + // Iterate over the purge list and feed the sweeps back to the + // batcher. + for _, sweep := range purgeList { + sweep := sweep + + err := b.purger(&sweep) + if err != nil { + b.log.Errorf("unable to purge sweep %x: %v", + sweep.SwapHash[:6], err) + } + } + }() + + b.log.Infof("spent, total sweeps: %v, purged sweeps: %v", + len(notifyList), len(purgeList)) + + err := b.monitorConfirmations(ctx) + if err != nil { + return err + } + + // We are no longer able to accept new sweeps, so we mark the batch as + // closed and persist on storage. + b.state = Closed + + return b.persist(ctx) +} + +// handleConf handles a confirmation notification. This is the final step of the +// batch. Here we signal to the batcher that this batch was completed. +func (b *batch) handleConf(ctx context.Context) error { + b.log.Infof("confirmed") + b.state = Confirmed + + return b.store.ConfirmBatch(ctx, b.id) +} + +// isComplete returns true if the batch is completed. This method is used by the +// batcher for lazy deletion of batches. +func (b *batch) isComplete() bool { + done, err := b.scheduleNextCall() + defer done() + + // We override the ErrBatchShuttingDown error as that is the expected + // error to be returned by the scheduler once the batch's main run loop + // has exited. + if err != nil && err != ErrBatchShuttingDown { + return false + } + return b.state == Confirmed +} + +// persist updates the batch in the database. +func (b *batch) persist(ctx context.Context) error { + bch := &dbBatch{} + + bch.ID = b.id + bch.State = stateEnumToString(b.state) + + if b.batchTxid != nil { + bch.BatchTxid = *b.batchTxid + } + + bch.BatchPkScript = b.batchPkScript + bch.LastRbfHeight = b.rbfCache.LastHeight + bch.LastRbfSatPerKw = int32(b.rbfCache.FeeRate) + bch.MaxTimeoutDistance = b.cfg.maxTimeoutDistance + + return b.store.UpdateSweepBatch(ctx, bch) +} + +// getBatchDestAddr returns the batch's destination address. If the batch +// has already generated an address then the same one will be returned. +func (b *batch) getBatchDestAddr(ctx context.Context) (btcutil.Address, error) { + var address btcutil.Address + + // If a batch address is set, use that. Otherwise, generate a + // new address. + if b.batchAddress != nil { + address = b.batchAddress + } else { + var err error + + // Generate a wallet address for the batch transaction's output. + address, err = b.wallet.NextAddr( + ctx, "", walletrpc.AddressType_TAPROOT_PUBKEY, false, + ) + if err != nil { + return address, err + } + + // Save that new address in order to re-use in future + // versions of the batch tx. + b.batchAddress = address + } + + return address, nil +} + +func (b *batch) insertAndAcquireID(ctx context.Context) (int32, error) { + bch := &dbBatch{} + bch.State = stateEnumToString(b.state) + bch.MaxTimeoutDistance = b.cfg.maxTimeoutDistance + + id, err := b.store.InsertSweepBatch(ctx, bch) + if err != nil { + return 0, err + } + + b.id = id + b.log = batchPrefixLogger(fmt.Sprintf("%d", b.id)) + + return id, nil +} + +// notifySweepSpend writes the spendTx to the sweep's notifier channel. +func notifySweepSpend(ctx context.Context, s sweep, spendTx *wire.MsgTx) { + select { + // Try to write the update to the notification channel. + case s.notifier.SpendChan <- spendTx: + + // If a quit signal was provided by the swap, continue. + case <-s.notifier.QuitChan: + + // If the context was canceled, return. + case <-ctx.Done(): + } +} + +func (b *batch) writeToErrChan(err error) { + select { + case b.errChan <- err: + default: + } +} + +func (b *batch) persistSweep(ctx context.Context, sweep sweep, + completed bool) error { + + return b.store.UpsertSweep(ctx, &dbSweep{ + BatchID: b.id, + SwapHash: sweep.swapHash, + Outpoint: sweep.outpoint, + Amount: sweep.value, + Completed: completed, + }) +} + +// clampBatchFee takes the fee amount and total amount of the sweeps in the +// batch and makes sure the fee is not too high. If the fee is too high, it is +// clamped to the maximum allowed fee. +func clampBatchFee(fee btcutil.Amount, + totalAmount btcutil.Amount) btcutil.Amount { + + maxFeeAmount := btcutil.Amount(float64(totalAmount) * + maxFeeToSwapAmtRatio) + + if fee > maxFeeAmount { + return maxFeeAmount + } + + return fee +} + +func stateEnumToString(state batchState) string { + switch state { + case Open: + return batchOpen + + case Closed: + return batchClosed + + case Confirmed: + return batchConfirmed + } + + return "" +} diff --git a/sweepbatcher/sweep_batcher.go b/sweepbatcher/sweep_batcher.go new file mode 100644 index 0000000..5548085 --- /dev/null +++ b/sweepbatcher/sweep_batcher.go @@ -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 +} diff --git a/sweepbatcher/sweep_batcher_test.go b/sweepbatcher/sweep_batcher_test.go new file mode 100644 index 0000000..2d233b9 --- /dev/null +++ b/sweepbatcher/sweep_batcher_test.go @@ -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)) +} diff --git a/test/chainnotifier_mock.go b/test/chainnotifier_mock.go index 836e50f..f155d11 100644 --- a/test/chainnotifier_mock.go +++ b/test/chainnotifier_mock.go @@ -73,31 +73,40 @@ func (c *mockChainNotifier) RegisterBlockEpochNtfn(ctx context.Context) ( chan int32, chan error, error) { 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) go func() { 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 + c.lnd.lock.Lock() select { case blockEpochChan <- c.lnd.Height: case <-ctx.Done(): - return } + c.lnd.lock.Unlock() - for { - select { - case m := <-c.lnd.epochChannel: - select { - case blockEpochChan <- m: - case <-ctx.Done(): - return - } - case <-ctx.Done(): - return - } - } + <-ctx.Done() }() return blockEpochChan, blockErrorChan, nil diff --git a/test/context.go b/test/context.go index 638bcc7..5fae593 100644 --- a/test/context.go +++ b/test/context.go @@ -259,3 +259,9 @@ func (ctx *Context) GetOutputIndex(tx *wire.MsgTx, func (ctx *Context) NotifyServerHeight(height int32) { 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) +} diff --git a/test/lnd_services_mock.go b/test/lnd_services_mock.go index 1dad7cc..db44474 100644 --- a/test/lnd_services_mock.go +++ b/test/lnd_services_mock.go @@ -4,7 +4,6 @@ import ( "context" "errors" "sync" - "time" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" @@ -63,13 +62,13 @@ func NewMockLnd() *LndMockServices { SignOutputRawChannel: make(chan SignOutputRawRequest), - FailInvoiceChannel: make(chan lntypes.Hash, 2), - epochChannel: make(chan int32), - Height: testStartingHeight, - NodePubkey: testNodePubkey, - Signature: testSignature, - SignatureMsg: testSignatureMsg, - Invoices: make(map[lntypes.Hash]*lndclient.Invoice), + FailInvoiceChannel: make(chan lntypes.Hash, 2), + blockHeightListeners: make([]chan int32, 0), + Height: testStartingHeight, + NodePubkey: testNodePubkey, + Signature: testSignature, + SignatureMsg: testSignatureMsg, + Invoices: make(map[lntypes.Hash]*lndclient.Invoice), } lightningClient.lnd = &lnd @@ -139,7 +138,7 @@ type LndMockServices struct { SendOutputsChannel chan wire.MsgTx SettleInvoiceChannel chan lntypes.Preimage FailInvoiceChannel chan lntypes.Hash - epochChannel chan int32 + blockHeightListeners []chan int32 ConfChannel chan *chainntnfs.TxConfirmation RegisterConfChannel chan *ConfRegistration @@ -177,15 +176,28 @@ type LndMockServices struct { 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. func (s *LndMockServices) NotifyHeight(height int32) error { + s.lock.Lock() + defer s.lock.Unlock() s.Height = height - select { - case s.epochChannel <- height: - case <-time.After(Timeout): - return ErrTimeout + for _, listener := range s.blockHeightListeners { + lis := listener + go func() { + lis <- height + }() } + return nil } diff --git a/testcontext_test.go b/testcontext_test.go index e465037..423eb31 100644 --- a/testcontext_test.go +++ b/testcontext_test.go @@ -13,6 +13,7 @@ import ( "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/lnrpc" @@ -36,7 +37,7 @@ type testContext struct { serverMock *serverMock swapClient *Client statusChan chan SwapInfo - store *storeMock + store *loopdb.StoreMock expiryChan chan time.Time runErr chan error stop func() @@ -51,6 +52,24 @@ func mockVerifySchnorrSigFail(pubKey *btcec.PublicKey, hash, 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 { sweeper := &sweep.Sweeper{ Lnd: config.LndServices, @@ -58,10 +77,20 @@ func newSwapClient(config *clientConfig) *Client { 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{ lnd: lndServices, store: config.Store, sweeper: sweeper, + batcher: batcher, createExpiryTimer: config.CreateExpiryTimer, cancelSwap: config.Server.CancelLoopOutSwap, verifySchnorrSig: mockVerifySchnorrSigFail, @@ -83,15 +112,15 @@ func createClientTestContext(t *testing.T, clientLnd := test.NewMockLnd() serverMock := newServerMock(clientLnd) - store := newStoreMock(t) + store := loopdb.NewStoreMock(t) for _, s := range pendingSwaps { - store.loopOutSwaps[s.Hash] = s.Contract + store.LoopOutSwaps[s.Hash] = s.Contract updates := []loopdb.SwapStateData{} for _, e := range s.Events { updates = append(updates, e.SwapStateData) } - store.loopOutUpdates[s.Hash] = updates + store.LoopOutUpdates[s.Hash] = updates } expiryChan := make(chan time.Time) @@ -147,7 +176,7 @@ func (ctx *testContext) finish() { } func (ctx *testContext) assertIsDone() { 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 { case <-ctx.statusChan: @@ -159,19 +188,19 @@ func (ctx *testContext) assertIsDone() { func (ctx *testContext) assertStored() { ctx.Context.T.Helper() - ctx.store.assertLoopOutStored() + ctx.store.AssertLoopOutStored() } func (ctx *testContext) assertStorePreimageReveal() { ctx.Context.T.Helper() - ctx.store.assertStorePreimageReveal() + ctx.store.AssertStorePreimageReveal() } func (ctx *testContext) assertStoreFinished(expectedResult loopdb.SwapState) { ctx.Context.T.Helper() - ctx.store.assertStoreFinished(expectedResult) + ctx.store.AssertStoreFinished(expectedResult) } 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") } } + +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) +} diff --git a/utils/htlc_utils.go b/utils/htlc_utils.go new file mode 100644 index 0000000..3d5bd24 --- /dev/null +++ b/utils/htlc_utils.go @@ -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 +}