Merge pull request #634 from GeorgeTsagk/sweep-batcher

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

@ -17,6 +17,8 @@ import (
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/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(),
)

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

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

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

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

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

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

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

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

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

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

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

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

@ -8,6 +8,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 {

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

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

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

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

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

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

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

@ -42,6 +42,7 @@ type LoopoutSwap struct {
PrepayInvoice string
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
}

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

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

@ -104,9 +104,10 @@ INSERT INTO loopout_swaps (
outgoing_chan_set,
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

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

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

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

@ -18,6 +18,7 @@ import (
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/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 {

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

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

@ -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: &notifier,
}
// Register the htlc spend notification.
ctx, cancel := context.WithCancel(globalCtx)
defer cancel()
spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn(
ctx, &htlcOutpoint, s.htlc.PkScript, s.InitiationHeight,
)
if err != nil {
return nil, fmt.Errorf("register spend ntfn: %v", err)
}
// Track our payment status so that we can detect whether our off chain
// htlc is settled. We track this information to determine whether it is
@ -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
}

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

File diff suppressed because it is too large Load Diff

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

@ -1110,6 +1110,10 @@
"account_addr_type": {
"$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."
}
}
},

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

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

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

@ -4,11 +4,10 @@ import (
"context"
"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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

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

@ -73,31 +73,40 @@ func (c *mockChainNotifier) RegisterBlockEpochNtfn(ctx context.Context) (
chan int32, chan error, error) {
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

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

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

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

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