diff --git a/client.go b/client.go index dbcbec7..1b906cf 100644 --- a/client.go +++ b/client.go @@ -108,6 +108,14 @@ type ClientConfig struct { // for a loop out swap. When greater than one, a multi-part payment may // be attempted. LoopOutMaxParts uint32 + + // TotalPaymentTimeout is the total amount of time until we time out + // off-chain payments (used in loop out). + TotalPaymentTimeout time.Duration + + // MaxPaymentRetries is the maximum times we retry an off-chain payment + // (used in loop out). + MaxPaymentRetries int } // NewClient returns a new instance to initiate swaps with. @@ -142,12 +150,14 @@ func NewClient(dbDir string, cfg *ClientConfig) (*Client, func(), error) { } executor := newExecutor(&executorConfig{ - lnd: cfg.Lnd, - store: store, - sweeper: sweeper, - createExpiryTimer: config.CreateExpiryTimer, - loopOutMaxParts: cfg.LoopOutMaxParts, - cancelSwap: swapServerClient.CancelLoopOutSwap, + lnd: cfg.Lnd, + store: store, + sweeper: sweeper, + createExpiryTimer: config.CreateExpiryTimer, + loopOutMaxParts: cfg.LoopOutMaxParts, + totalPaymentTimeout: cfg.TotalPaymentTimeout, + maxPaymentRetries: cfg.MaxPaymentRetries, + cancelSwap: swapServerClient.CancelLoopOutSwap, }) client := &Client{ diff --git a/executor.go b/executor.go index f759e2c..2ac94e7 100644 --- a/executor.go +++ b/executor.go @@ -26,6 +26,10 @@ type executorConfig struct { loopOutMaxParts uint32 + totalPaymentTimeout time.Duration + + maxPaymentRetries int + cancelSwap func(ctx context.Context, details *outCancelDetails) error } @@ -141,12 +145,14 @@ func (s *executor) run(mainCtx context.Context, defer s.wg.Done() err := newSwap.execute(mainCtx, &executeConfig{ - statusChan: statusChan, - sweeper: s.sweeper, - blockEpochChan: queue.ChanOut(), - timerFactory: s.executorConfig.createExpiryTimer, - loopOutMaxParts: s.executorConfig.loopOutMaxParts, - cancelSwap: s.executorConfig.cancelSwap, + statusChan: statusChan, + sweeper: s.sweeper, + blockEpochChan: queue.ChanOut(), + timerFactory: s.executorConfig.createExpiryTimer, + loopOutMaxParts: s.executorConfig.loopOutMaxParts, + totalPaymentTimout: s.executorConfig.totalPaymentTimeout, + maxPaymentRetries: s.executorConfig.maxPaymentRetries, + cancelSwap: s.executorConfig.cancelSwap, }, height) if err != nil && err != context.Canceled { log.Errorf("Execute error: %v", err) diff --git a/go.mod b/go.mod index b22506e..6a54778 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210822222949-9b5a201c344c github.com/coreos/bbolt v1.3.3 + github.com/davecgh/go-spew v1.1.1 github.com/fortytw2/leaktest v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 github.com/jessevdk/go-flags v1.4.0 diff --git a/loopd/config.go b/loopd/config.go index a528e30..619f21e 100644 --- a/loopd/config.go +++ b/loopd/config.go @@ -33,9 +33,11 @@ var ( LoopDirBase, DefaultNetwork, defaultConfigFilename, ) - defaultMaxLogFiles = 3 - defaultMaxLogFileSize = 10 - defaultLoopOutMaxParts = uint32(5) + defaultMaxLogFiles = 3 + defaultMaxLogFileSize = 10 + defaultLoopOutMaxParts = uint32(5) + defaultTotalPaymentTimeout = time.Minute * 60 + defaultMaxPaymentRetries = 3 // DefaultTLSCertFilename is the default file name for the autogenerated // TLS certificate. @@ -144,6 +146,9 @@ type Config struct { LoopOutMaxParts uint32 `long:"loopoutmaxparts" description:"The maximum number of payment parts that may be used for a loop out swap."` + TotalPaymentTimeout time.Duration `long:"totalpaymenttimeout" description:"The timeout to use for off-chain payments."` + MaxPaymentRetries int `long:"maxpaymentretries" description:"The maximum number of times an off-chain payment may be retried."` + Lnd *lndConfig `group:"lnd" namespace:"lnd"` Server *loopServerConfig `group:"server" namespace:"server"` @@ -165,19 +170,21 @@ func DefaultConfig() Config { Server: &loopServerConfig{ NoTLS: false, }, - LoopDir: LoopDirBase, - ConfigFile: defaultConfigFile, - DataDir: LoopDirBase, - LogDir: defaultLogDir, - MaxLogFiles: defaultMaxLogFiles, - MaxLogFileSize: defaultMaxLogFileSize, - DebugLevel: defaultLogLevel, - TLSCertPath: DefaultTLSCertPath, - TLSKeyPath: DefaultTLSKeyPath, - MacaroonPath: DefaultMacaroonPath, - MaxLSATCost: lsat.DefaultMaxCostSats, - MaxLSATFee: lsat.DefaultMaxRoutingFeeSats, - LoopOutMaxParts: defaultLoopOutMaxParts, + LoopDir: LoopDirBase, + ConfigFile: defaultConfigFile, + DataDir: LoopDirBase, + LogDir: defaultLogDir, + MaxLogFiles: defaultMaxLogFiles, + MaxLogFileSize: defaultMaxLogFileSize, + DebugLevel: defaultLogLevel, + TLSCertPath: DefaultTLSCertPath, + TLSKeyPath: DefaultTLSKeyPath, + MacaroonPath: DefaultMacaroonPath, + MaxLSATCost: lsat.DefaultMaxCostSats, + MaxLSATFee: lsat.DefaultMaxRoutingFeeSats, + LoopOutMaxParts: defaultLoopOutMaxParts, + TotalPaymentTimeout: defaultTotalPaymentTimeout, + MaxPaymentRetries: defaultMaxPaymentRetries, Lnd: &lndConfig{ Host: "localhost:10009", MacaroonPath: DefaultLndMacaroonPath, @@ -299,6 +306,17 @@ func Validate(cfg *Config) error { return fmt.Errorf("must specify --lnd.macaroonpath") } + // Allow at most 2x the default total payment timeout. + if cfg.TotalPaymentTimeout > 2*defaultTotalPaymentTimeout { + return fmt.Errorf("max total payment timeout allowed is at "+ + "most %v", 2*defaultTotalPaymentTimeout) + } + + // At least one retry. + if cfg.MaxPaymentRetries < 1 { + return fmt.Errorf("max payment retries must be positive") + } + return nil } diff --git a/loopd/utils.go b/loopd/utils.go index 753f6f5..2513526 100644 --- a/loopd/utils.go +++ b/loopd/utils.go @@ -17,14 +17,16 @@ func getClient(config *Config, lnd *lndclient.LndServices) (*loop.Client, func(), error) { clientConfig := &loop.ClientConfig{ - ServerAddress: config.Server.Host, - ProxyAddress: config.Server.Proxy, - SwapServerNoTLS: config.Server.NoTLS, - TLSPathServer: config.Server.TLSPath, - Lnd: lnd, - MaxLsatCost: btcutil.Amount(config.MaxLSATCost), - MaxLsatFee: btcutil.Amount(config.MaxLSATFee), - LoopOutMaxParts: config.LoopOutMaxParts, + ServerAddress: config.Server.Host, + ProxyAddress: config.Server.Proxy, + SwapServerNoTLS: config.Server.NoTLS, + TLSPathServer: config.Server.TLSPath, + Lnd: lnd, + MaxLsatCost: btcutil.Amount(config.MaxLSATCost), + MaxLsatFee: btcutil.Amount(config.MaxLSATFee), + LoopOutMaxParts: config.LoopOutMaxParts, + TotalPaymentTimeout: config.TotalPaymentTimeout, + MaxPaymentRetries: config.MaxPaymentRetries, } swapClient, cleanUp, err := loop.NewClient(config.DataDir, clientConfig) diff --git a/loopdb/protocol_version.go b/loopdb/protocol_version.go index c8664dc..202028c 100644 --- a/loopdb/protocol_version.go +++ b/loopdb/protocol_version.go @@ -47,13 +47,17 @@ const ( // the server to perform a probe to test inbound liquidty. ProtocolVersionProbe ProtocolVersion = 8 + // The client may ask the server to use a custom routing helper plugin + // in order to enhance off-chain payments corresponding to a swap. + ProtocolVersionRoutingPlugin = 9 + // ProtocolVersionUnrecorded is set for swaps were created before we // started saving protocol version with swaps. ProtocolVersionUnrecorded ProtocolVersion = math.MaxUint32 // CurrentRPCProtocolVersion defines the version of the RPC protocol // that is currently supported by the loop client. - CurrentRPCProtocolVersion = looprpc.ProtocolVersion_PROBE + CurrentRPCProtocolVersion = looprpc.ProtocolVersion_ROUTING_PLUGIN // CurrentInternalProtocolVersion defines the RPC current protocol in // the internal representation. @@ -95,6 +99,9 @@ func (p ProtocolVersion) String() string { case ProtocolVersionProbe: return "Probe" + case ProtocolVersionRoutingPlugin: + return "Routing Plugin" + default: return "Unknown" } diff --git a/loopdb/protocol_version_test.go b/loopdb/protocol_version_test.go index 9f30877..bcaa913 100644 --- a/loopdb/protocol_version_test.go +++ b/loopdb/protocol_version_test.go @@ -23,6 +23,7 @@ func TestProtocolVersionSanity(t *testing.T) { ProtocolVersionMultiLoopIn, ProtocolVersionLoopOutCancel, ProtocolVersionProbe, + ProtocolVersionRoutingPlugin, } rpcVersions := [...]looprpc.ProtocolVersion{ @@ -35,6 +36,7 @@ func TestProtocolVersionSanity(t *testing.T) { looprpc.ProtocolVersion_MULTI_LOOP_IN, looprpc.ProtocolVersion_LOOP_OUT_CANCEL, looprpc.ProtocolVersion_PROBE, + looprpc.ProtocolVersion_ROUTING_PLUGIN, } require.Equal(t, len(versions), len(rpcVersions)) diff --git a/loopout.go b/loopout.go index 2d4f619..c3ce345 100644 --- a/loopout.go +++ b/loopout.go @@ -52,10 +52,6 @@ var ( // // TODO(wilmer): tune? DefaultSweepConfTargetDelta = DefaultSweepConfTarget * 2 - - // paymentTimeout is the timeout for the loop out payment loop as - // communicated to lnd. - paymentTimeout = time.Minute * 30 ) // loopOutSwap contains all the in-memory state related to a pending loop out @@ -72,6 +68,8 @@ type loopOutSwap struct { // htlcTxHash is the confirmed htlc tx id. htlcTxHash *chainhash.Hash + swapInvoicePaymentAddr [32]byte + swapPaymentChan chan paymentResult prePaymentChan chan paymentResult @@ -80,12 +78,14 @@ type loopOutSwap struct { // executeConfig contains extra configuration to execute the swap. type executeConfig struct { - sweeper *sweep.Sweeper - statusChan chan<- SwapInfo - blockEpochChan <-chan interface{} - timerFactory func(d time.Duration) <-chan time.Time - loopOutMaxParts uint32 - cancelSwap func(context.Context, *outCancelDetails) error + sweeper *sweep.Sweeper + statusChan chan<- SwapInfo + blockEpochChan <-chan interface{} + timerFactory func(d time.Duration) <-chan time.Time + loopOutMaxParts uint32 + totalPaymentTimout time.Duration + maxPaymentRetries int + cancelSwap func(context.Context, *outCancelDetails) error } // loopOutInitResult contains information about a just-initiated loop out swap. @@ -198,10 +198,18 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig, // Log htlc address for debugging. swapKit.log.Infof("Htlc address: %v", htlc.Address) + // Obtain the payment addr since we'll need it later for routing plugin + // recommendation and possibly for cancel. + paymentAddr, err := obtainSwapPaymentAddr(contract.SwapInvoice, cfg) + if err != nil { + return nil, err + } + swap := &loopOutSwap{ - LoopOutContract: contract, - swapKit: *swapKit, - htlc: htlc, + LoopOutContract: contract, + swapKit: *swapKit, + htlc: htlc, + swapInvoicePaymentAddr: *paymentAddr, } // Persist the data before exiting this function, so that the caller @@ -244,11 +252,21 @@ func resumeLoopOutSwap(reqContext context.Context, cfg *swapConfig, // Log htlc address for debugging. swapKit.log.Infof("Htlc address: %v", htlc.Address) + // 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, + ) + if err != nil { + return nil, err + } + // Create the swap. swap := &loopOutSwap{ - LoopOutContract: *pend.Contract, - swapKit: *swapKit, - htlc: htlc, + LoopOutContract: *pend.Contract, + swapKit: *swapKit, + htlc: htlc, + swapInvoicePaymentAddr: *paymentAddr, } lastUpdate := pend.LastUpdate() @@ -263,6 +281,24 @@ func resumeLoopOutSwap(reqContext context.Context, cfg *swapConfig, 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() @@ -537,16 +573,29 @@ func (s *loopOutSwap) payInvoices(ctx context.Context) { // Pay the swap invoice. s.log.Infof("Sending swap payment %v", s.SwapInvoice) + // Ask the server if it recommends using a routing plugin. + pluginType, err := s.swapKit.server.RecommendRoutingPlugin( + ctx, s.swapInfo().SwapHash, s.swapInvoicePaymentAddr, + ) + if err != nil { + s.log.Warnf("Server couldn't recommend routing plugin: %v", err) + pluginType = RoutingPluginNone + } else { + s.log.Infof("Server recommended routing plugin: %v", pluginType) + } + + // Use the recommended routing plugin. s.swapPaymentChan = s.payInvoice( ctx, s.SwapInvoice, s.MaxSwapRoutingFee, - s.LoopOutContract.OutgoingChanSet, + s.LoopOutContract.OutgoingChanSet, pluginType, ) - // Pay the prepay invoice. + // Pay the prepay invoice. Won't use the routing plugin here as the + // prepay is trivially small and shouldn't normally need any help. s.log.Infof("Sending prepayment %v", s.PrepayInvoice) s.prePaymentChan = s.payInvoice( ctx, s.PrepayInvoice, s.MaxPrepayRoutingFee, - nil, + nil, RoutingPluginNone, ) } @@ -573,8 +622,8 @@ func (p paymentResult) failure() error { // payInvoice pays a single invoice. func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string, - maxFee btcutil.Amount, - outgoingChanIds loopdb.ChannelSet) chan paymentResult { + maxFee btcutil.Amount, outgoingChanIds loopdb.ChannelSet, + pluginType RoutingPluginType) chan paymentResult { resultChan := make(chan paymentResult) sendResult := func(result paymentResult) { @@ -588,7 +637,7 @@ func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string, var result paymentResult status, err := s.payInvoiceAsync( - ctx, invoice, maxFee, outgoingChanIds, + ctx, invoice, maxFee, outgoingChanIds, pluginType, ) if err != nil { result.err = err @@ -616,16 +665,39 @@ func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string, // payInvoiceAsync is the asynchronously executed part of paying an invoice. func (s *loopOutSwap) payInvoiceAsync(ctx context.Context, invoice string, maxFee btcutil.Amount, - outgoingChanIds loopdb.ChannelSet) (*lndclient.PaymentStatus, error) { + outgoingChanIds loopdb.ChannelSet, pluginType RoutingPluginType) ( + *lndclient.PaymentStatus, error) { // Extract hash from payment request. Unfortunately the request // components aren't available directly. chainParams := s.lnd.ChainParams - hash, _, err := swap.DecodeInvoice(chainParams, invoice) + target, routeHints, hash, amt, err := swap.DecodeInvoice( + chainParams, invoice, + ) if err != nil { return nil, err } + maxRetries := 1 + paymentTimeout := s.executeConfig.totalPaymentTimout + + // Attempt to acquire and initialize the routing plugin. + routingPlugin, err := AcquireRoutingPlugin( + ctx, pluginType, *s.lnd, target, routeHints, amt, + ) + if err != nil { + return nil, err + } + + if routingPlugin != nil { + s.log.Infof("Acquired routing plugin %v for payment %v", + pluginType, hash.String()) + + maxRetries = s.executeConfig.maxPaymentRetries + paymentTimeout /= time.Duration(maxRetries) + defer ReleaseRoutingPlugin(ctx) + } + req := lndclient.SendPaymentRequest{ MaxFee: maxFee, Invoice: invoice, @@ -635,12 +707,75 @@ func (s *loopOutSwap) payInvoiceAsync(ctx context.Context, } // Lookup state of the swap payment. - paymentStateCtx, cancel := context.WithCancel(ctx) + payCtx, cancel := context.WithCancel(ctx) defer cancel() - payStatusChan, payErrChan, err := s.lnd.Router.SendPayment( - paymentStateCtx, req, + start := time.Now() + paymentStatus, attempts, err := s.sendPaymentWithRetry( + payCtx, hash, &req, maxRetries, routingPlugin, pluginType, ) + + dt := time.Since(start) + paymentSuccess := err == nil && + paymentStatus.State == lnrpc.Payment_SUCCEEDED + + if err := s.swapKit.server.ReportRoutingResult( + ctx, s.swapInfo().SwapHash, s.swapInvoicePaymentAddr, pluginType, + paymentSuccess, int32(attempts), dt.Milliseconds(), + ); err != nil { + s.log.Warnf("Failed to report routing result: %v", err) + } + + return paymentStatus, err +} + +// sendPaymentWithRetry will send the payment, optionally with the passed +// routing plugin retrying at most maxRetries times. +func (s *loopOutSwap) sendPaymentWithRetry(ctx context.Context, + hash lntypes.Hash, req *lndclient.SendPaymentRequest, maxRetries int, + routingPlugin RoutingPlugin, pluginType RoutingPluginType) ( + *lndclient.PaymentStatus, int, error) { + + tryCount := 1 + for { + s.log.Infof("Payment (%v) try count %v/%v (plugin=%v)", + hash.String(), tryCount, maxRetries, + pluginType.String()) + + if routingPlugin != nil { + if err := routingPlugin.BeforePayment( + ctx, tryCount, maxRetries, + ); err != nil { + return nil, tryCount, err + } + } + + var err error + paymentStatus, err := s.awaitSendPayment(ctx, hash, req) + if err != nil { + return nil, tryCount, err + } + + // Payment has succeeded, we can return here. + if paymentStatus.State == lnrpc.Payment_SUCCEEDED { + return paymentStatus, tryCount, nil + } + + // Retry if the payment has timed out, or return here. + if tryCount > maxRetries || paymentStatus.FailureReason != + lnrpc.PaymentFailureReason_FAILURE_REASON_TIMEOUT { + + return paymentStatus, tryCount, nil + } + + tryCount++ + } +} + +func (s *loopOutSwap) awaitSendPayment(ctx context.Context, hash lntypes.Hash, + req *lndclient.SendPaymentRequest) (*lndclient.PaymentStatus, error) { + + payStatusChan, payErrChan, err := s.lnd.Router.SendPayment(ctx, *req) if err != nil { return nil, err } @@ -665,15 +800,16 @@ func (s *loopOutSwap) payInvoiceAsync(ctx context.Context, return nil, errors.New("unknown payment state") } - // Abort the swap in case of an error. An unknown payment error - // from TrackPayment is no longer expected here. + // Abort the swap in case of an error. An unknown + // payment error from TrackPayment is no longer expected + // here. case err := <-payErrChan: if err != channeldb.ErrAlreadyPaid { return nil, err } payStatusChan, payErrChan, err = - s.lnd.Router.TrackPayment(paymentStateCtx, hash) + s.lnd.Router.TrackPayment(ctx, hash) if err != nil { return nil, err } @@ -997,22 +1133,9 @@ func (s *loopOutSwap) failOffChain(ctx context.Context, paymentType paymentType, // Set our state to failed off chain timeout. s.state = loopdb.StateFailOffchainPayments - swapPayReq, err := zpay32.Decode( - s.LoopOutContract.SwapInvoice, s.swapConfig.lnd.ChainParams, - ) - if err != nil { - s.log.Errorf("could not decode swap invoice: %v", err) - return - } - - if swapPayReq.PaymentAddr == nil { - s.log.Errorf("expected payment address for invoice") - return - } - details := &outCancelDetails{ hash: s.hash, - paymentAddr: *swapPayReq.PaymentAddr, + paymentAddr: s.swapInvoicePaymentAddr, metadata: routeCancelMetadata{ paymentType: paymentType, failureReason: status.FailureReason, @@ -1194,7 +1317,7 @@ func validateLoopOutContract(lnd *lndclient.LndServices, // Check invoice amounts. chainParams := lnd.ChainParams - swapInvoiceHash, swapInvoiceAmt, err := swap.DecodeInvoice( + _, _, swapInvoiceHash, swapInvoiceAmt, err := swap.DecodeInvoice( chainParams, response.swapInvoice, ) if err != nil { @@ -1207,7 +1330,7 @@ func validateLoopOutContract(lnd *lndclient.LndServices, swapInvoiceHash, swapHash) } - _, prepayInvoiceAmt, err := swap.DecodeInvoice( + _, _, _, prepayInvoiceAmt, err := swap.DecodeInvoice( chainParams, response.prepayInvoice, ) if err != nil { diff --git a/release_notes.md b/release_notes.md index c71d135..c2befa0 100644 --- a/release_notes.md +++ b/release_notes.md @@ -16,6 +16,10 @@ This file tracks release notes for the loop client. #### New Features +* Loop client now supports optional routing plugins to improve off-chain payment + reliability. One such plugin that the client implemenets will gradually prefer + increasingly more expensive routes in case payments using cheap routes time out. + #### Breaking Changes #### Bug Fixes diff --git a/routing_plugin.go b/routing_plugin.go new file mode 100644 index 0000000..e30dd16 --- /dev/null +++ b/routing_plugin.go @@ -0,0 +1,659 @@ +package loop + +import ( + "context" + "fmt" + "sort" + "sync" + "time" + + "github.com/btcsuite/btclog" + "github.com/btcsuite/btcutil" + "github.com/davecgh/go-spew/spew" + "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/zpay32" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + // ErrRoutingPluginNotApplicable means that the selected routing plugin + // is not able to enhance routing given the current conditions and + // therefore shouldn't be used. + ErrRoutingPluginNotApplicable = fmt.Errorf("routing plugin not " + + "applicable") + + // ErrRoutingPluginNoMoreRetries means that the routing plugin can't + // effectively help the payment with more retries. + ErrRoutingPluginNoMoreRetries = fmt.Errorf("routing plugin can't " + + "retry more") +) + +var ( + routingPluginMx sync.Mutex + routingPluginInstance RoutingPlugin +) + +// RoutingPlugin is a generic interface for off-chain payment helpers. +type RoutingPlugin interface { + // Init initializes the routing plugin. + Init(ctx context.Context, target route.Vertex, + routeHints [][]zpay32.HopHint, amt btcutil.Amount) error + + // Done deinitializes the routing plugin (restoring any state the + // plugin might have changed). + Done(ctx context.Context) error + + // BeforePayment is called before each payment. Attempt counter is + // passed, counting attempts from 1. + BeforePayment(ctx context.Context, attempt int, maxAttempts int) error +} + +// makeRoutingPlugin is a helper to instantiate routing plugins. +func makeRoutingPlugin(pluginType RoutingPluginType, + lnd lndclient.LndServices, clock clock.Clock) RoutingPlugin { + + if pluginType == RoutingPluginLowHigh { + return &lowToHighRoutingPlugin{ + lnd: lnd, + clock: clock, + } + } + + return nil +} + +// AcquireRoutingPlugin will return a RoutingPlugin instance (or nil). As the +// LND instance used is a shared resource, currently only one requestor will be +// able to acquire a RoutingPlugin instance. If someone is already holding the +// instance a nil is returned. +func AcquireRoutingPlugin(ctx context.Context, pluginType RoutingPluginType, + lnd lndclient.LndServices, target route.Vertex, + routeHints [][]zpay32.HopHint, amt btcutil.Amount) ( + RoutingPlugin, error) { + + routingPluginMx.Lock() + defer routingPluginMx.Unlock() + + // Another swap is already using the routing plugin. + if routingPluginInstance != nil { + return nil, nil + } + + routingPluginInstance = makeRoutingPlugin( + pluginType, lnd, clock.NewDefaultClock(), + ) + if routingPluginInstance == nil { + return nil, nil + } + + // Initialize the plugin with the passed parameters. + err := routingPluginInstance.Init(ctx, target, routeHints, amt) + if err != nil { + if err == ErrRoutingPluginNotApplicable { + // Since the routing plugin is not applicable for this + // payment, we can immediately destruct it. + if err := routingPluginInstance.Done(ctx); err != nil { + log.Errorf("Error while releasing routing "+ + "plugin: %v", err) + } + + // ErrRoutingPluginNotApplicable is non critical, so + // we're masking this error as we can continue the swap + // flow without the routing plugin. + err = nil + } + + routingPluginInstance = nil + return nil, err + } + + return routingPluginInstance, nil +} + +// ReleaseRoutingPlugin will release the RoutingPlugin, allowing other +// requestors to acquire the instance. +func ReleaseRoutingPlugin(ctx context.Context) { + routingPluginMx.Lock() + defer routingPluginMx.Unlock() + + if routingPluginInstance == nil { + return + } + + if err := routingPluginInstance.Done(ctx); err != nil { + log.Errorf("Error while releasing routing plugin: %v", + err) + } + + routingPluginInstance = nil +} + +// lowToHighRoutingPlugin is a RoutingPlugin that implements "low to high" +// routing. This means that when we're attempting to pay to a target we'll +// gradually (with a linear step function) discard inbound peers to that target +// given routing timeouts. The lowToHighRoutingPlugin itself is responsible for +// manipulating LND's Mission Control to make such routing attempts possible. +type lowToHighRoutingPlugin struct { + lnd lndclient.LndServices + clock clock.Clock + target route.Vertex + amount btcutil.Amount + mcState map[route.Vertex]lndclient.MissionControlEntry + + // nodesByMaxFee holds nodes sorted by maximum fees that would be paid + // to the target node for the target amount. + nodesByMaxFee []nodeFeeInfo + + // mcChanged flags that the MC settings for the tracked nodes were + // changed and should be reset to their original state once the plugin + // is done. + mcChanged bool +} + +type nodeFeeInfo struct { + node route.Vertex + capacity btcutil.Amount + fee int64 +} + +// Enforce that lowToHighRoutingPlugin implements the RoutingPlugin interface. +var _ RoutingPlugin = (*lowToHighRoutingPlugin)(nil) + +// buildPrivateChannels creates the private channel map from the passed route +// hints. The code is taken and adapted from LND. Original source: +// lnd/routing/payment_session_source.go. +func buildPrivateChannels(routeHints [][]zpay32.HopHint, + target route.Vertex) map[route.Vertex][]*lndclient.ChannelEdge { + + edges := make(map[route.Vertex][]*lndclient.ChannelEdge) + + // Traverse through all of the available hop hints and include them in + // our edges map. + for _, routeHint := range routeHints { + // If multiple hop hints are provided within a single route + // hint, we'll assume they must be chained together and sorted + // in forward order in order to reach the target successfully. + for i, hopHint := range routeHint { + // In order to determine the end node of this hint, + // we'll need to look at the next hint's start node. If + // we've reached the end of the hints list, we can + // assume we've reached the target. + var toNode route.Vertex + if i != len(routeHint)-1 { + toNode = route.NewVertex(routeHint[i+1].NodeID) + } else { + toNode = target + } + + fromNode := route.NewVertex(hopHint.NodeID) + // Finally, create the channel edges from the hop hint + // and add them to list of edges. + edgeFrom := &lndclient.ChannelEdge{ + Node1: fromNode, + Node2: toNode, + ChannelID: hopHint.ChannelID, + Node1Policy: &lndclient.RoutingPolicy{ + TimeLockDelta: uint32( + hopHint.CLTVExpiryDelta, + ), + FeeBaseMsat: int64( + hopHint.FeeBaseMSat, + ), + FeeRateMilliMsat: int64( + hopHint.FeeProportionalMillionths, + ), + }, + } + edges[fromNode] = append(edges[fromNode], edgeFrom) + + // Note that we're adding the edge here in both + // directions as we don't actually use it to find a path + // but just to walk from the target until the first + // node that peers with multiple nodes. + edgeTo := &lndclient.ChannelEdge{ + Node1: toNode, + Node2: fromNode, + ChannelID: hopHint.ChannelID, + Node2Policy: &lndclient.RoutingPolicy{ + TimeLockDelta: uint32( + hopHint.CLTVExpiryDelta, + ), + FeeBaseMsat: int64( + hopHint.FeeBaseMSat, + ), + FeeRateMilliMsat: int64( + hopHint.FeeProportionalMillionths, + ), + }, + } + edges[toNode] = append(edges[toNode], edgeTo) + } + } + + return edges +} + +func getNodeInfo(ctx context.Context, lnd lndclient.LightningClient, + nodeID route.Vertex, + privateEdges map[route.Vertex][]*lndclient.ChannelEdge) ( + *lndclient.NodeInfo, error) { + + nodeInfo, err := lnd.GetNodeInfo(ctx, nodeID, true) + if err != nil { + status, ok := status.FromError(err) + if ok && status.Code() == codes.NotFound { + // It's still possible that we should know this node + // from the provided route hints even though it is not + // part of the public graph. If we don't know any + // private channels then just return the error. + if _, ok := privateEdges[nodeID]; !ok { + return nil, err + } + + nodeInfo = &lndclient.NodeInfo{ + Node: &lndclient.Node{ + PubKey: nodeID, + }, + } + } else { + return nil, err + } + } + + if len(privateEdges) > 0 { + for _, edge := range privateEdges[nodeID] { + nodeInfo.Channels = append(nodeInfo.Channels, *edge) + nodeInfo.TotalCapacity += edge.Capacity + } + } + + return nodeInfo, nil +} + +// saveMissionControlState will save the MC state for the node pairs formed by +// the passed nodes and target. +func (r *lowToHighRoutingPlugin) saveMissionControlState(ctx context.Context, + nodes map[route.Vertex]*lndclient.NodeInfo, target route.Vertex) error { + + entries, err := r.lnd.Router.QueryMissionControl(ctx) + if err != nil { + return err + } + + r.mcState = make(map[route.Vertex]lndclient.MissionControlEntry) + for _, entry := range entries { + // Skip pairs which we do not intend to change. + if _, ok := nodes[entry.NodeFrom]; !ok { + continue + } + + if entry.NodeTo != target { + continue + } + + r.mcState[entry.NodeFrom] = entry + } + + log.Debugf("Saved MC state: %v", spew.Sdump(r.mcState)) + return nil +} + +// nodesByMaxFee is a helper function to order the passed nodes by overall max +// fee towards the target node if we'd want to fwd the passed amount. +func nodesByMaxFee(amt btcutil.Amount, target route.Vertex, + nodes map[route.Vertex]*lndclient.NodeInfo) []nodeFeeInfo { + + // maxFeePerNode assigns the maximum fees that would be paid through + // selected nodes to the target node for the target amount. + maxFeePerNode := make(map[route.Vertex]int64) + totalCapacityPerNode := make(map[route.Vertex]btcutil.Amount) + + amtMsat := int64(amt * 1000) + for nodeID, node := range nodes { + var totalCapacity btcutil.Amount + for _, ch := range node.Channels { + var policy *lndclient.RoutingPolicy + if ch.Node1 == nodeID && ch.Node2 == target { + policy = ch.Node1Policy + } else if ch.Node1 == target && ch.Node2 == nodeID { + policy = ch.Node2Policy + } + + if policy == nil { + continue + } + + totalCapacity += ch.Capacity + + log.Debugf("'%v', policy=%v", + node.Alias, spew.Sdump(policy)) + fee := policy.FeeBaseMsat + + policy.FeeRateMilliMsat*amtMsat + + // For all peers we'll save the "maximum" routing fee + // for that peer. + if fee > maxFeePerNode[nodeID] { + maxFeePerNode[nodeID] = fee + } + } + totalCapacityPerNode[nodeID] = totalCapacity + } + + nodesByMaxFee := make([]nodeFeeInfo, 0, len(maxFeePerNode)) + + // Sort peers by maximum fee, so that we can later on disable edges + // in a gradual way. + for nodeID, fee := range maxFeePerNode { + nodesByMaxFee = append( + nodesByMaxFee, nodeFeeInfo{ + node: nodeID, + capacity: totalCapacityPerNode[nodeID], + fee: fee, + }, + ) + } + + sort.Slice(nodesByMaxFee, func(i, j int) bool { + return nodesByMaxFee[i].fee < nodesByMaxFee[j].fee + }) + + for i, nodeFee := range nodesByMaxFee { + log.Tracef("nodesByMaxFee[%v] = %v (%v)", i, + nodeFee.node.String(), nodeFee.fee) + } + + return nodesByMaxFee +} + +// Init will initialize the "low to high" routing plugin. It'll save the MC +// state and also preinit the internal state of the routing plugin. When the +// instance is released, the saved MC state can be restored. +func (r *lowToHighRoutingPlugin) Init(ctx context.Context, target route.Vertex, + routeHints [][]zpay32.HopHint, amt btcutil.Amount) error { + + // Prepare the private edges from the passed route hints. + privateEdges := buildPrivateChannels(routeHints, target) + + // Save the original target as the current "exit" node: where + // our payments should flow forward. + exit := target + + // Walk until the first fork (if there's any). This first + // fork will be where we're going to try to manipulate success + // probabilities to increasingly prefer more expensive edges. + + // We track all visited peers, so we won't end up walking + // back and forth on graphs that have no forks. + visited := map[route.Vertex]struct{}{ + target: {}, + } + + var ( + targetNodeInfo *lndclient.NodeInfo + err error + ) + + for { + targetNodeInfo, err = getNodeInfo( + ctx, r.lnd.Client, target, privateEdges, + ) + if err != nil { + return err + } + + // If the target node has only one or more channels but all + // connects to the same peer, then we need to walk further. + var ( + peer route.Vertex + peers []route.Vertex + ) + for _, edge := range targetNodeInfo.Channels { + if target != edge.Node1 { + peer = edge.Node1 + } else { + peer = edge.Node2 + } + + if _, ok := visited[peer]; !ok { + visited[peer] = struct{}{} + peers = append(peers, peer) + } + } + + if len(peers) == 1 { + exit = target + target = peers[0] + continue + } + + // If there are no more peers to visit then we can't use + // this routing plugin. + if len(peers) == 0 { + return ErrRoutingPluginNotApplicable + } + + // Found the first fork to our target. + break + } + + log.Debugf("Low/high plugin target: '%v' %v", targetNodeInfo.Alias, + targetNodeInfo.PubKey.String()) + + // Gather node info (including channels) for the nodes we're + // interested in. + targetChanged := exit != target + nodes := make(map[route.Vertex]*lndclient.NodeInfo) + for _, edge := range targetNodeInfo.Channels { + // Skip edges to the exit node since from there the route to + // the invoice target is always constructed from the same hops. + if targetChanged && + (edge.Node1 == exit || edge.Node2 == exit) { + + continue + } + + var peer route.Vertex + if edge.Node1 == target { + peer = edge.Node2 + } else { + peer = edge.Node1 + } + + nodeInfo, err := getNodeInfo(ctx, r.lnd.Client, peer, nil) + if err != nil { + return err + } + + nodes[peer] = nodeInfo + } + + // Get the nodes ordered by routing fee towards the target. + r.nodesByMaxFee = nodesByMaxFee(amt, target, nodes) + r.target = target + r.amount = amt + + // Save MC state. + err = r.saveMissionControlState(ctx, nodes, target) + if err != nil { + return err + } + + return nil +} + +// BeforePayment will reconfigure the mission control on each payment attempt. +func (r *lowToHighRoutingPlugin) BeforePayment(ctx context.Context, + currAttempt int, maxAttempts int) error { + + queryRoutesReq := lndclient.QueryRoutesRequest{ + Source: &r.lnd.NodePubkey, + PubKey: r.target, + AmtMsat: lnwire.MilliSatoshi(r.amount * 1000), + FeeLimitMsat: lnwire.MilliSatoshi(r.amount * 1000), + UseMissionControl: true, + } + + // If logging in trace level, query routes and log to see how what path + // we find before MC is manipulated. + if log.Level() == btclog.LevelTrace { + res, err := r.lnd.Client.QueryRoutes(ctx, queryRoutesReq) + log.Tracef("BeforePayment() QueryRoutes(1)=%v, err=%v", + spew.Sdump(res), err) + } + + // Do not do anything unless we tried to route the payment at least + // once. + if currAttempt < 2 { + return nil + } + + // Calculate the limit until we'll disable edges. The way we calculate + // this limit is that we take the minimum and maximum fee peers which + // define our fee range. Within this fee range we'll scale linearly + // where each step euqals to the range divided by maxAttempts. + minFee := r.nodesByMaxFee[0].fee + maxFee := r.nodesByMaxFee[len(r.nodesByMaxFee)-1].fee + limit := minFee + + ((maxFee-minFee)/int64(maxAttempts))*int64(currAttempt) + + // Create a timestamp just slightly in the future as Mission Control + // stores timestamps with sub second precision where as we send a unix + // timestamp it may occur that we can't override the last entries as + // they have the same unix timestamp. + // TODO(bhandras): not very reliable, ideally we'd need a force import + // for MC. + now := r.clock.Now().Add(time.Second) + + allowed := 0 + entries := make( + []lndclient.MissionControlEntry, 0, len(r.nodesByMaxFee), + ) + + for _, nodeFeeInfo := range r.nodesByMaxFee { + if nodeFeeInfo.fee < limit { + log.Debugf("Discouraging payments from %v to %v", + nodeFeeInfo.node, r.target) + entries = append( + entries, lndclient.MissionControlEntry{ + NodeFrom: nodeFeeInfo.node, + NodeTo: r.target, + FailTime: now, + FailAmt: 1, + }) + } else { + log.Debugf("Encouraging payments from %v to %v", + nodeFeeInfo.node, r.target) + entries = append( + entries, lndclient.MissionControlEntry{ + NodeFrom: nodeFeeInfo.node, + NodeTo: r.target, + SuccessTime: now, + SuccessAmt: lnwire.MilliSatoshi( + nodeFeeInfo.capacity * 1000, + ), + }) + allowed++ + } + } + + // There's no point retrying the payment since we discouraged using + // all inbound peers to the target. + if allowed == 0 { + return ErrRoutingPluginNoMoreRetries + } + + err := r.lnd.Router.ImportMissionControl(ctx, entries) + if err != nil { + return err + } + + // Flag that we have changed the MC state. + r.mcChanged = true + + log.Tracef("Imported MC state: %v", spew.Sdump(entries)) + + // If logging in trace level, query routes and log to see how our + // changes affected path finding. + if log.Level() == btclog.LevelTrace { + res, err := r.lnd.Client.QueryRoutes(ctx, queryRoutesReq) + log.Tracef("BeforePayment() QueryRoutes(2)=%v, err=%v", + spew.Sdump(res), err) + } + + return nil +} + +// Done will attempt to reconstruct the MC state for the affected node pairs to +// the same state as it was before using the routing plugin. For those node +// pairs where the beginning state was empty, we set success for the maximum +// capacity for the sake of simplicity. +func (r *lowToHighRoutingPlugin) Done(ctx context.Context) error { + if r.mcState == nil { + return nil + } + + defer func() { + r.mcState = nil + }() + + // If none of the selected pairs were manipulated we can skip ahead. + if !r.mcChanged { + log.Debugf("MC state not changed, skipping restore") + return nil + } + + // Roll the entry times forward (to be able to override recent updates). + // Use the "time travel" trick which is required to make overrides + // succeed. + now := r.clock.Now().Add(time.Second) + entries := make( + []lndclient.MissionControlEntry, 0, len(r.nodesByMaxFee), + ) + for _, nodeInfo := range r.nodesByMaxFee { + // We didn't have MC state for this node pair before, so just + // set it to succeed the max amount and fail anything more than + // that. This way we don't restrict forwarding for normal cases. + if _, ok := r.mcState[nodeInfo.node]; !ok { + capacity := lnwire.MilliSatoshi( + nodeInfo.capacity * 1000, + ) + entries = append( + entries, lndclient.MissionControlEntry{ + NodeFrom: nodeInfo.node, + NodeTo: r.target, + FailTime: now, + FailAmt: capacity + 1, + SuccessTime: now, + SuccessAmt: capacity, + }) + } else { + // We did have a MC entry for this pair, so we just bump + // the time to now + 1 sec. + entry := r.mcState[nodeInfo.node] + + if !entry.FailTime.IsZero() { + entry.FailTime = now + } + + if !entry.SuccessTime.IsZero() { + entry.SuccessTime = now + } + + entries = append(entries, entry) + } + } + + err := r.lnd.Router.ImportMissionControl(ctx, entries) + if err != nil { + return err + } + + log.Debugf("Restored partial MC state: %v", + spew.Sdump(entries)) + + return nil +} diff --git a/routing_plugin_test.go b/routing_plugin_test.go new file mode 100644 index 0000000..4c2b69c --- /dev/null +++ b/routing_plugin_test.go @@ -0,0 +1,698 @@ +package loop + +import ( + "context" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/zpay32" + "github.com/stretchr/testify/require" +) + +var ( + alice = route.Vertex{1} + bob = route.Vertex{2} + charlie = route.Vertex{3} + dave = route.Vertex{4} + eugene = route.Vertex{5} + loopNode = route.Vertex{99} + + privFrank, _ = btcec.NewPrivateKey(btcec.S256()) + frankPubKey = privFrank.PubKey() + frank = route.NewVertex(frankPubKey) + + privGeorge, _ = btcec.NewPrivateKey(btcec.S256()) + georgePubKey = privGeorge.PubKey() + george = route.NewVertex(georgePubKey) +) + +// testChan holds simplified test data for channels. +type testChan struct { + nodeID1 route.Vertex + nodeID2 route.Vertex + chanID uint64 + capacity int64 + feeBase1 int64 + feeRate1 int64 + feeBase2 int64 + feeRate2 int64 +} + +// makeTestNetwork is a helper creating mocked network data from test inputs. +func makeTestNetwork(channels []testChan) ([]lndclient.ChannelInfo, + map[uint64]*lndclient.ChannelEdge) { + + chanInfos := make([]lndclient.ChannelInfo, len(channels)) + edges := make(map[uint64]*lndclient.ChannelEdge, len(channels)) + for i, ch := range channels { + chanInfos[i] = lndclient.ChannelInfo{ + ChannelID: ch.chanID, + } + + edges[ch.chanID] = &lndclient.ChannelEdge{ + ChannelID: ch.chanID, + Capacity: btcutil.Amount(ch.capacity), + Node1: ch.nodeID1, + Node2: ch.nodeID2, + Node1Policy: &lndclient.RoutingPolicy{ + FeeBaseMsat: ch.feeBase1, + FeeRateMilliMsat: ch.feeRate1, + }, + Node2Policy: &lndclient.RoutingPolicy{ + FeeBaseMsat: ch.feeBase2, + FeeRateMilliMsat: ch.feeRate2, + }, + } + } + + return chanInfos, edges +} + +// TestLowHighRoutingPlugin tests that the low-high routing plugin does indeed +// gradually change MC settings in favour of more expensive inbound channels +// towards the Loop server. +func TestLowHighRoutingPlugin(t *testing.T) { + target := loopNode + amt := btcutil.Amount(50) + testTime := time.Now().UTC() + // We expect Mission Control entries to be set to now + 1 sec. + testTimeMc := testTime.Add(time.Second) + + tests := []struct { + name string + channels []testChan + routeHints [][]zpay32.HopHint + initError error + missionControlState [][]lndclient.MissionControlEntry + restoredMissionControlState []lndclient.MissionControlEntry + }{ + { + name: "degenerate network 1", + // + // Alice --- Loop + // + channels: []testChan{ + {alice, loopNode, 1, 1000, 1000, 1, 1000, 1}, + }, + initError: ErrRoutingPluginNotApplicable, + missionControlState: [][]lndclient.MissionControlEntry{ + // The original MC state we start with. + { + { + NodeFrom: alice, + NodeTo: loopNode, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + }, + }, + }, + { + name: "degenerate network 2", + // + // Alice --- Bob --- Loop + // + channels: []testChan{ + // Alice - Bob + {alice, bob, 1, 1000, 1000, 1, 1000, 1}, + // Bob - Loop + {bob, loopNode, 2, 1000, 1000, 1, 1000, 1}, + }, + initError: ErrRoutingPluginNotApplicable, + missionControlState: [][]lndclient.MissionControlEntry{ + // The original MC state we start with. + { + {}, + }, + }, + }, + { + name: "degenrate network 3", + // + // _____Bob_____ + // / \ + // Alice Dave---Loop + // \___ + // Charlie + // + channels: []testChan{ + {alice, bob, 1, 1000, 1000, 1, 1000, 1}, + {alice, charlie, 2, 1000, 1000, 1, 1000, 1}, + // Bob - Dave (cheap) + {bob, dave, 3, 1000, 1000, 1, 1000, 1}, + {dave, loopNode, 5, 1000, 1000, 1, 1000, 1}, + }, + initError: ErrRoutingPluginNotApplicable, + missionControlState: [][]lndclient.MissionControlEntry{ + // The original MC state we start with. + { + { + NodeFrom: bob, + NodeTo: dave, + FailTime: time.Time{}, + FailAmt: 0, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + }, + }, + restoredMissionControlState: []lndclient.MissionControlEntry{ + { + NodeFrom: bob, + NodeTo: dave, + FailTime: time.Time{}, + FailAmt: 0, + SuccessTime: testTimeMc, + SuccessAmt: 10000, + }, + }, + }, + { // nolint: dupl + name: "fork before loop node 1", + // + // _____Bob_____ + // / \ + // Alice Dave---Loop + // \___ ___/ + // Charlie + // + channels: []testChan{ + {alice, bob, 1, 1000, 1000, 1, 1000, 1}, + {alice, charlie, 2, 1000, 1000, 1, 1000, 1}, + // Bob - Dave (cheap) + {bob, dave, 3, 1000, 1000, 1, 1000, 1}, + // Charlie - Dave (expensive) + {charlie, dave, 4, 1000, 1000, 100, 1000, 1}, + {dave, loopNode, 5, 1000, 1000, 1, 1000, 1}, + }, + initError: nil, + missionControlState: [][]lndclient.MissionControlEntry{ + // The original MC state we start with. + { + { + NodeFrom: bob, + NodeTo: dave, + FailTime: time.Time{}, + FailAmt: 0, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + }, + // MC state set on the second attempt. + { + // Discourage Bob - Dave + { + NodeFrom: bob, + NodeTo: dave, + FailTime: testTimeMc, + FailAmt: 1, + }, + // Encourage Charlie - Dave + { + NodeFrom: charlie, + NodeTo: dave, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + }, + }, + restoredMissionControlState: []lndclient.MissionControlEntry{ + { + NodeFrom: bob, + NodeTo: dave, + FailTime: time.Time{}, + FailAmt: 0, + SuccessTime: testTimeMc, + SuccessAmt: 10000, + }, + { + NodeFrom: charlie, + NodeTo: dave, + FailTime: testTimeMc, + FailAmt: 1000001, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + }, + }, + { // nolint: dupl + name: "fork before loop node 1 with equal inbound fees", + // + // _____Bob_____ + // / \ + // Alice Dave---Loop + // \___ ___/ + // Charlie + // + channels: []testChan{ + {alice, bob, 1, 999, 1000, 1, 1000, 1}, + {alice, charlie, 2, 9999, 1000, 1, 1000, 1}, + // Bob - Dave (expensive) + {bob, dave, 3, 999, 1000, 100, 1000, 1}, + // Charlie - Dave (expensive) + {charlie, dave, 4, 999, 1000, 100, 1000, 1}, + {dave, loopNode, 5, 999, 1000, 1, 1000, 1}, + }, + initError: nil, + missionControlState: [][]lndclient.MissionControlEntry{ + // The original MC state we start with. + { + { + NodeFrom: dave, + NodeTo: loopNode, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + }, + // MC state on the second attempt encourages + // both inbound peers to make sure we do try + // to route through both. + { + { + NodeFrom: dave, + NodeTo: loopNode, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + { + NodeFrom: bob, + NodeTo: dave, + SuccessTime: testTimeMc, + SuccessAmt: 999000, + }, + { + NodeFrom: charlie, + NodeTo: dave, + SuccessTime: testTimeMc, + SuccessAmt: 999000, + }, + }, + }, + restoredMissionControlState: []lndclient.MissionControlEntry{ + { + NodeFrom: dave, + NodeTo: loopNode, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + { + NodeFrom: bob, + NodeTo: dave, + SuccessTime: testTimeMc, + SuccessAmt: 999000, + FailTime: testTimeMc, + FailAmt: 999001, + }, + { + NodeFrom: charlie, + NodeTo: dave, + SuccessTime: testTimeMc, + SuccessAmt: 999000, + FailTime: testTimeMc, + FailAmt: 999001, + }, + }, + }, + { + name: "fork before loop node 2", + // + // _____Bob_____ + // / \ + // Alice Eugene---Frank---George---Loop + // |\___ ___// + // | Charlie / + // \ / + // \___ ___/ + // Dave + // + channels: []testChan{ + {alice, bob, 1, 1000, 1000, 1, 1000, 1}, + {alice, charlie, 2, 1000, 1000, 1, 1000, 1}, + {alice, dave, 3, 1000, 1000, 1, 1000, 1}, + // Bob - Eugene (cheap) + {bob, eugene, 4, 1000, 1000, 1, 1000, 1}, + // Charlie - Eugene (more expensive) + {charlie, eugene, 5, 1000, 1000, 2, 1000, 1}, + // Dave - Eugene (most expensive) + {dave, eugene, 6, 1000, 1001, 2, 1000, 1}, + {eugene, frank, 7, 1000, 1000, 1, 1000, 1}, + }, + // Private channels: Frank - George - Loop + routeHints: [][]zpay32.HopHint{{ + { + NodeID: frankPubKey, + ChannelID: 8, + FeeBaseMSat: 1000, + FeeProportionalMillionths: 1, + }, + { + NodeID: georgePubKey, + ChannelID: 9, + FeeBaseMSat: 1000, + FeeProportionalMillionths: 1, + }, + }}, + initError: nil, + missionControlState: [][]lndclient.MissionControlEntry{ + // The original MC state we start with. + { + { + NodeFrom: charlie, + NodeTo: eugene, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + }, + // MC state set on the second attempt. + { + // Discourage Bob - Eugene + { + NodeFrom: bob, + NodeTo: eugene, + FailTime: testTimeMc, + FailAmt: 1, + }, + // Encourage Charlie - Eugene + { + NodeFrom: charlie, + NodeTo: eugene, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + // Encourage Dave - Eugene + { + NodeFrom: dave, + NodeTo: eugene, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + }, + // MC state set on the third attempt. + { + // Discourage Bob - Eugene + { + NodeFrom: bob, + NodeTo: eugene, + FailTime: testTimeMc, + FailAmt: 1, + }, + // Discourage Charlie - Eugene + { + NodeFrom: charlie, + NodeTo: eugene, + FailTime: testTimeMc, + FailAmt: 1, + }, + // Encourage Dave - Eugene + { + NodeFrom: dave, + NodeTo: eugene, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + }, + }, + restoredMissionControlState: []lndclient.MissionControlEntry{ + { + NodeFrom: bob, + NodeTo: eugene, + FailTime: testTimeMc, + FailAmt: 1000001, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + { + NodeFrom: charlie, + NodeTo: eugene, + FailTime: time.Time{}, + FailAmt: 0, + SuccessTime: testTimeMc, + SuccessAmt: 10000, + }, + { + NodeFrom: dave, + NodeTo: eugene, + FailTime: testTimeMc, + FailAmt: 1000001, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + }, + }, + { + name: "fork before loop node 3", + // + // _____Bob_____ + // / \ + // Alice Eugene---Frank---George---Loop + // |\___ ___/ / + // | Charlie / + // \ / + // \___ ___________________/ + // Dave + // + channels: []testChan{ + // Alice - Bob + {alice, bob, 1, 1000, 1000, 1, 1000, 1}, + // Alice - Charlie + {alice, charlie, 2, 1000, 1000, 1, 1000, 1}, + // Alice - Dave + {alice, dave, 3, 1000, 1000, 1, 1000, 1}, + // Bob - Eugene + {bob, eugene, 4, 1000, 1000, 1, 1000, 1}, + // Charlie - Eugene + {charlie, eugene, 5, 1000, 1000, 2, 1000, 1}, + // Dave - George (expensive) + {dave, george, 6, 1000, 1001, 2, 1000, 1}, + // Eugene - Frank + {eugene, frank, 7, 1000, 1000, 1, 1000, 1}, + // Frank - George (cheap) + {frank, george, 8, 1000, 1000, 1, 1000, 1}, + // George - Loop + {george, loopNode, 9, 1000, 1000, 1, 1000, 1}, + }, + initError: nil, + missionControlState: [][]lndclient.MissionControlEntry{ + // The original MC state we start with. + { + { + NodeFrom: charlie, + NodeTo: eugene, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + }, + // MC state set on the second attempt. + { + { + NodeFrom: charlie, + NodeTo: eugene, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + // Discourage Frank - George + { + NodeFrom: frank, + NodeTo: george, + FailTime: testTimeMc, + FailAmt: 1, + }, + // Encourage Dave - George + { + NodeFrom: dave, + NodeTo: george, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + }, + }, + restoredMissionControlState: []lndclient.MissionControlEntry{ + { + NodeFrom: charlie, + NodeTo: eugene, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + { + NodeFrom: frank, + NodeTo: george, + FailTime: testTimeMc, + FailAmt: 1000001, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + { + NodeFrom: dave, + NodeTo: george, + FailTime: testTimeMc, + FailAmt: 1000001, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + }, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + mockLnd := test.NewMockLnd() + + mockLnd.Channels, mockLnd.ChannelEdges = + makeTestNetwork(tc.channels) + + lnd := lndclient.LndServices{ + Client: mockLnd.Client, + Router: mockLnd.Router, + } + + testClock := clock.NewTestClock(testTime) + plugin := makeRoutingPlugin( + RoutingPluginLowHigh, lnd, testClock, + ) + require.NotNil(t, plugin) + + // Set start state for MC. + mockLnd.MissionControlState = tc.missionControlState[0] + + // Initialize the routing plugin. + require.Equal( + t, tc.initError, + plugin.Init( + context.TODO(), target, tc.routeHints, + amt, + ), + ) + + if tc.initError != nil { + // Make sure that MC state is untouched. + require.Equal( + t, tc.missionControlState[0], + mockLnd.MissionControlState, + ) + + return + } + + maxAttempts := len(tc.missionControlState) + for i, expectedState := range tc.missionControlState { + // Check that after each step, MC state is what + // we expect it to be. + require.NoError( + t, plugin.BeforePayment( + context.TODO(), + i+1, maxAttempts, + ), + ) + + require.ElementsMatch( + t, expectedState, + mockLnd.MissionControlState, + ) + } + + // Make sure we covered all inbound channels. + require.Error( + t, ErrRoutingPluginNoMoreRetries, + plugin.BeforePayment( + context.TODO(), maxAttempts, maxAttempts, + ), + ) + + // Deinitialize the routing plugin. + require.NoError(t, plugin.Done(context.TODO())) + + // Make sure that MC state is reset after Done() is + // called. + require.ElementsMatch( + t, tc.restoredMissionControlState, + mockLnd.MissionControlState, + ) + }) + } +} + +func TestRoutingPluginAcquireRelease(t *testing.T) { + mockLnd := test.NewMockLnd() + + // _____Bob_____ + // / \ + // Alice Dave---Loop + // \___ ___/ + // Charlie + // + channels := []testChan{ + {alice, bob, 1, 1000, 1000, 1, 1000, 1}, + {alice, charlie, 2, 1000, 1000, 1, 1000, 1}, + {bob, dave, 3, 1000, 1000, 1, 1000, 1}, + {charlie, dave, 4, 1000, 1000, 100, 1000, 1}, + {dave, loopNode, 5, 1000, 1000, 1, 1000, 1}, + } + + mockLnd.Channels, mockLnd.ChannelEdges = makeTestNetwork(channels) + lnd := lndclient.LndServices{ + Client: mockLnd.Client, + Router: mockLnd.Router, + } + + target := loopNode + amt := btcutil.Amount(50) + ctx := context.TODO() + + // RoutingPluginNone returns nil. + plugin, err := AcquireRoutingPlugin( + ctx, RoutingPluginNone, lnd, target, nil, amt, + ) + require.Nil(t, plugin) + require.NoError(t, err) + + // Attempting to acquire RoutingPluginNone again still returns nil. + plugin, err = AcquireRoutingPlugin( + ctx, RoutingPluginNone, lnd, target, nil, amt, + ) + require.Nil(t, plugin) + require.NoError(t, err) + + // Call ReleaseRoutingPlugin twice to ensure we can call it even when no + // plugin is acquired. + ReleaseRoutingPlugin(ctx) + ReleaseRoutingPlugin(ctx) + + // RoutingPluginNone returns nil. + plugin2, err := AcquireRoutingPlugin( + ctx, RoutingPluginNone, lnd, target, nil, amt, + ) + require.Nil(t, plugin2) + require.NoError(t, err) + + // Acquire is successful. + plugin, err = AcquireRoutingPlugin( + ctx, RoutingPluginLowHigh, lnd, target, nil, amt, + ) + require.NotNil(t, plugin) + require.NoError(t, err) + + // Plugin already acquired, above. + plugin2, err = AcquireRoutingPlugin( + ctx, RoutingPluginLowHigh, lnd, target, nil, amt, + ) + require.Nil(t, plugin2) + require.NoError(t, err) + + // Release acruired plugin. + ReleaseRoutingPlugin(ctx) + + // Acquire is successful. + plugin2, err = AcquireRoutingPlugin( + ctx, RoutingPluginLowHigh, lnd, target, nil, amt, + ) + require.NotNil(t, plugin2) + require.NoError(t, err) +} diff --git a/server_mock_test.go b/server_mock_test.go index e26f771..4c1356d 100644 --- a/server_mock_test.go +++ b/server_mock_test.go @@ -242,3 +242,15 @@ func (s *serverMock) Probe(ctx context.Context, amt btcutil.Amount, return nil } + +func (s *serverMock) RecommendRoutingPlugin(_ context.Context, _ lntypes.Hash, + _ [32]byte) (RoutingPluginType, error) { + + return RoutingPluginNone, nil +} + +func (s *serverMock) ReportRoutingResult(_ context.Context, _ lntypes.Hash, + _ [32]byte, _ RoutingPluginType, _ bool, _ int32, _ int64) error { + + return nil +} diff --git a/swap/fees.go b/swap/fees.go index c190926..c85bf2f 100644 --- a/swap/fees.go +++ b/swap/fees.go @@ -6,6 +6,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/zpay32" ) @@ -27,24 +28,30 @@ func FeeRateAsPercentage(feeRate int64) float64 { return float64(feeRate) / (FeeRateTotalParts / 100) } -// DecodeInvoice gets the hash and the amount of an invoice. +// DecodeInvoice gets the destination, hash and the amount of an invoice. // It requires an amount to be specified. -func DecodeInvoice(params *chaincfg.Params, - payReq string) (lntypes.Hash, btcutil.Amount, error) { +func DecodeInvoice(params *chaincfg.Params, payReq string) (route.Vertex, + [][]zpay32.HopHint, lntypes.Hash, btcutil.Amount, error) { swapPayReq, err := zpay32.Decode( payReq, params, ) if err != nil { - return lntypes.Hash{}, 0, err + return route.Vertex{}, nil, lntypes.Hash{}, 0, err } if swapPayReq.MilliSat == nil { - return lntypes.Hash{}, 0, errors.New("no amount in invoice") + return route.Vertex{}, nil, lntypes.Hash{}, 0, + errors.New("no amount in invoice") } var hash lntypes.Hash copy(hash[:], swapPayReq.PaymentHash[:]) - return hash, swapPayReq.MilliSat.ToSatoshis(), nil + var destination route.Vertex + destPubKey := swapPayReq.Destination.SerializeCompressed() + copy(destination[:], destPubKey) + + return destination, swapPayReq.RouteHints, hash, + swapPayReq.MilliSat.ToSatoshis(), nil } diff --git a/swap_server_client.go b/swap_server_client.go index 93e45ae..0a3af15 100644 --- a/swap_server_client.go +++ b/swap_server_client.go @@ -43,6 +43,30 @@ var ( "be provided") ) +// RoutingPluginType represents the routing plugin type directly. +type RoutingPluginType uint8 + +const ( + // RoutingPluginNone is recommended when the client shouldn't use any + // routing plugin. + RoutingPluginNone RoutingPluginType = 0 + + // RoutingPluginLowHigh is recommended when the client should use + // low-high routing method. + RoutingPluginLowHigh RoutingPluginType = 1 +) + +// String pretty prints a RoutingPluginType. +func (r RoutingPluginType) String() string { + switch r { + case RoutingPluginLowHigh: + return "Low/High" + + default: + return "None" + } +} + type swapServerClient interface { GetLoopOutTerms(ctx context.Context) ( *LoopOutTerms, error) @@ -86,6 +110,17 @@ type swapServerClient interface { // CancelLoopOutSwap cancels a loop out swap. CancelLoopOutSwap(ctx context.Context, details *outCancelDetails) error + + // RecommendRoutingPlugin asks the server for routing plugin + // recommendation for off-chain payment(s) of a swap. + RecommendRoutingPlugin(ctx context.Context, swapHash lntypes.Hash, + paymentAddr [32]byte) (RoutingPluginType, error) + + // ReportRoutingResult reports a routing result corresponding to a swap. + ReportRoutingResult(ctx context.Context, + swapHash lntypes.Hash, paymentAddr [32]byte, + plugin RoutingPluginType, success bool, attempts int32, + totalTime int64) error } type grpcSwapServerClient struct { @@ -619,6 +654,72 @@ func (s *grpcSwapServerClient) CancelLoopOutSwap(ctx context.Context, return err } +// RecommendRoutingPlugin asks the server for routing plugin recommendation for +// off-chain payment(s) of a swap. +func (s *grpcSwapServerClient) RecommendRoutingPlugin(ctx context.Context, + swapHash lntypes.Hash, paymentAddr [32]byte) (RoutingPluginType, error) { + + req := &looprpc.RecommendRoutingPluginReq{ + ProtocolVersion: loopdb.CurrentRPCProtocolVersion, + SwapHash: swapHash[:], + PaymentAddress: paymentAddr[:], + } + + rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) + defer rpcCancel() + + res, err := s.server.RecommendRoutingPlugin(rpcCtx, req) + if err != nil { + return RoutingPluginNone, err + } + + var plugin RoutingPluginType + switch res.Plugin { + case looprpc.RoutingPlugin_NONE: + plugin = RoutingPluginNone + + case looprpc.RoutingPlugin_LOW_HIGH: + plugin = RoutingPluginLowHigh + + default: + log.Warnf("Recommended routing plugin is unknown: %v", plugin) + plugin = RoutingPluginNone + } + + return plugin, nil +} + +// ReportRoutingResult reports a routing result corresponding to a swap. +func (s *grpcSwapServerClient) ReportRoutingResult(ctx context.Context, + swapHash lntypes.Hash, paymentAddr [32]byte, plugin RoutingPluginType, + success bool, attempts int32, totalTime int64) error { + + var rpcRoutingPlugin looprpc.RoutingPlugin + switch plugin { + case RoutingPluginLowHigh: + rpcRoutingPlugin = looprpc.RoutingPlugin_LOW_HIGH + + default: + rpcRoutingPlugin = looprpc.RoutingPlugin_NONE + } + + req := &looprpc.ReportRoutingResultReq{ + ProtocolVersion: loopdb.CurrentRPCProtocolVersion, + SwapHash: swapHash[:], + PaymentAddress: paymentAddr[:], + Plugin: rpcRoutingPlugin, + Success: success, + Attempts: attempts, + TotalTime: totalTime, + } + + rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) + defer rpcCancel() + + _, err := s.server.ReportRoutingResult(rpcCtx, req) + return err +} + func rpcRouteCancel(details *outCancelDetails) ( *looprpc.CancelLoopOutSwapRequest_RouteCancel, error) { diff --git a/swapserverrpc/server.pb.go b/swapserverrpc/server.pb.go index a483657..3a978ad 100644 --- a/swapserverrpc/server.pb.go +++ b/swapserverrpc/server.pb.go @@ -59,6 +59,9 @@ const ( // The client is able to ask the server to probe to test inbound // liquidity. ProtocolVersion_PROBE ProtocolVersion = 8 + // The client may ask the server to use a custom routing helper plugin in + // order to enhance off-chain payments corresponding to a swap. + ProtocolVersion_ROUTING_PLUGIN ProtocolVersion = 9 ) // Enum value maps for ProtocolVersion. @@ -73,6 +76,7 @@ var ( 6: "MULTI_LOOP_IN", 7: "LOOP_OUT_CANCEL", 8: "PROBE", + 9: "ROUTING_PLUGIN", } ProtocolVersion_value = map[string]int32{ "LEGACY": 0, @@ -84,6 +88,7 @@ var ( "MULTI_LOOP_IN": 6, "LOOP_OUT_CANCEL": 7, "PROBE": 8, + "ROUTING_PLUGIN": 9, } ) @@ -357,6 +362,54 @@ func (PaymentFailureReason) EnumDescriptor() ([]byte, []int) { return file_server_proto_rawDescGZIP(), []int{3} } +type RoutingPlugin int32 + +const ( + // Client won't use any plugins to help with payment routing. + RoutingPlugin_NONE RoutingPlugin = 0 + // Client will try more expensive routes for off-chain payments. + RoutingPlugin_LOW_HIGH RoutingPlugin = 1 +) + +// Enum value maps for RoutingPlugin. +var ( + RoutingPlugin_name = map[int32]string{ + 0: "NONE", + 1: "LOW_HIGH", + } + RoutingPlugin_value = map[string]int32{ + "NONE": 0, + "LOW_HIGH": 1, + } +) + +func (x RoutingPlugin) Enum() *RoutingPlugin { + p := new(RoutingPlugin) + *p = x + return p +} + +func (x RoutingPlugin) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RoutingPlugin) Descriptor() protoreflect.EnumDescriptor { + return file_server_proto_enumTypes[4].Descriptor() +} + +func (RoutingPlugin) Type() protoreflect.EnumType { + return &file_server_proto_enumTypes[4] +} + +func (x RoutingPlugin) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RoutingPlugin.Descriptor instead. +func (RoutingPlugin) EnumDescriptor() ([]byte, []int) { + return file_server_proto_rawDescGZIP(), []int{4} +} + type ServerLoopOutRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1947,6 +2000,260 @@ func (*ServerProbeResponse) Descriptor() ([]byte, []int) { return file_server_proto_rawDescGZIP(), []int{22} } +type RecommendRoutingPluginReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProtocolVersion ProtocolVersion `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3,enum=looprpc.ProtocolVersion" json:"protocol_version,omitempty"` + // The hash of the swap requesting a routing plugin. + SwapHash []byte `protobuf:"bytes,2,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"` + // The payment address for the swap invoice, used to ensure that only the + // swap owner can request routing plugin recommendation. + PaymentAddress []byte `protobuf:"bytes,3,opt,name=payment_address,json=paymentAddress,proto3" json:"payment_address,omitempty"` +} + +func (x *RecommendRoutingPluginReq) Reset() { + *x = RecommendRoutingPluginReq{} + if protoimpl.UnsafeEnabled { + mi := &file_server_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RecommendRoutingPluginReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RecommendRoutingPluginReq) ProtoMessage() {} + +func (x *RecommendRoutingPluginReq) ProtoReflect() protoreflect.Message { + mi := &file_server_proto_msgTypes[23] + 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 RecommendRoutingPluginReq.ProtoReflect.Descriptor instead. +func (*RecommendRoutingPluginReq) Descriptor() ([]byte, []int) { + return file_server_proto_rawDescGZIP(), []int{23} +} + +func (x *RecommendRoutingPluginReq) GetProtocolVersion() ProtocolVersion { + if x != nil { + return x.ProtocolVersion + } + return ProtocolVersion_LEGACY +} + +func (x *RecommendRoutingPluginReq) GetSwapHash() []byte { + if x != nil { + return x.SwapHash + } + return nil +} + +func (x *RecommendRoutingPluginReq) GetPaymentAddress() []byte { + if x != nil { + return x.PaymentAddress + } + return nil +} + +type RecommendRoutingPluginRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The routing plugin to use for off-chain payments. + Plugin RoutingPlugin `protobuf:"varint,1,opt,name=plugin,proto3,enum=looprpc.RoutingPlugin" json:"plugin,omitempty"` +} + +func (x *RecommendRoutingPluginRes) Reset() { + *x = RecommendRoutingPluginRes{} + if protoimpl.UnsafeEnabled { + mi := &file_server_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RecommendRoutingPluginRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RecommendRoutingPluginRes) ProtoMessage() {} + +func (x *RecommendRoutingPluginRes) ProtoReflect() protoreflect.Message { + mi := &file_server_proto_msgTypes[24] + 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 RecommendRoutingPluginRes.ProtoReflect.Descriptor instead. +func (*RecommendRoutingPluginRes) Descriptor() ([]byte, []int) { + return file_server_proto_rawDescGZIP(), []int{24} +} + +func (x *RecommendRoutingPluginRes) GetPlugin() RoutingPlugin { + if x != nil { + return x.Plugin + } + return RoutingPlugin_NONE +} + +type ReportRoutingResultReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProtocolVersion ProtocolVersion `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3,enum=looprpc.ProtocolVersion" json:"protocol_version,omitempty"` + // The swap hash. + SwapHash []byte `protobuf:"bytes,2,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"` + // The payment address for the swap invoice, used to ensure that only the + // swap owner can report routing result. + PaymentAddress []byte `protobuf:"bytes,3,opt,name=payment_address,json=paymentAddress,proto3" json:"payment_address,omitempty"` + // The routing plugin that was used. + Plugin RoutingPlugin `protobuf:"varint,4,opt,name=plugin,proto3,enum=looprpc.RoutingPlugin" json:"plugin,omitempty"` + // Whether this payment succeeded. + Success bool `protobuf:"varint,5,opt,name=success,proto3" json:"success,omitempty"` + // The number of payment attempts using the plugin. + Attempts int32 `protobuf:"varint,6,opt,name=attempts,proto3" json:"attempts,omitempty"` + // Total time used in milliseconds. + TotalTime int64 `protobuf:"varint,7,opt,name=total_time,json=totalTime,proto3" json:"total_time,omitempty"` +} + +func (x *ReportRoutingResultReq) Reset() { + *x = ReportRoutingResultReq{} + if protoimpl.UnsafeEnabled { + mi := &file_server_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReportRoutingResultReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportRoutingResultReq) ProtoMessage() {} + +func (x *ReportRoutingResultReq) ProtoReflect() protoreflect.Message { + mi := &file_server_proto_msgTypes[25] + 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 ReportRoutingResultReq.ProtoReflect.Descriptor instead. +func (*ReportRoutingResultReq) Descriptor() ([]byte, []int) { + return file_server_proto_rawDescGZIP(), []int{25} +} + +func (x *ReportRoutingResultReq) GetProtocolVersion() ProtocolVersion { + if x != nil { + return x.ProtocolVersion + } + return ProtocolVersion_LEGACY +} + +func (x *ReportRoutingResultReq) GetSwapHash() []byte { + if x != nil { + return x.SwapHash + } + return nil +} + +func (x *ReportRoutingResultReq) GetPaymentAddress() []byte { + if x != nil { + return x.PaymentAddress + } + return nil +} + +func (x *ReportRoutingResultReq) GetPlugin() RoutingPlugin { + if x != nil { + return x.Plugin + } + return RoutingPlugin_NONE +} + +func (x *ReportRoutingResultReq) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *ReportRoutingResultReq) GetAttempts() int32 { + if x != nil { + return x.Attempts + } + return 0 +} + +func (x *ReportRoutingResultReq) GetTotalTime() int64 { + if x != nil { + return x.TotalTime + } + return 0 +} + +type ReportRoutingResultRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ReportRoutingResultRes) Reset() { + *x = ReportRoutingResultRes{} + if protoimpl.UnsafeEnabled { + mi := &file_server_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReportRoutingResultRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportRoutingResultRes) ProtoMessage() {} + +func (x *ReportRoutingResultRes) ProtoReflect() protoreflect.Message { + mi := &file_server_proto_msgTypes[26] + 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 ReportRoutingResultRes.ProtoReflect.Descriptor instead. +func (*ReportRoutingResultRes) Descriptor() ([]byte, []int) { + return file_server_proto_rawDescGZIP(), []int{26} +} + var File_server_proto protoreflect.FileDescriptor var file_server_proto_rawDesc = []byte{ @@ -2176,136 +2483,187 @@ var file_server_proto_rawDesc = []byte{ 0x32, 0x12, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x52, 0x0a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0xc2, 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, 0x2a, 0xfc, 0x03, 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, 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, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa6, 0x01, 0x0a, 0x19, 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, 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, 0x27, 0x0a, 0x0f, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x22, 0x4b, 0x0a, 0x19, 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, 0x2e, 0x0a, + 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0xa8, 0x02, + 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 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, 0x27, 0x0a, 0x0f, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, + 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x52, 0x06, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x1a, 0x0a, + 0x08, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x08, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x5f, 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, 0x2a, 0xd6, 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, 0x2a, 0xfc, 0x03, 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, 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, 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, 0x32, 0xcf, 0x07, 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, + 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, 0x8a, 0x09, 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, 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, + 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, 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, 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, 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, + 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 ( @@ -2320,44 +2678,49 @@ func file_server_proto_rawDescGZIP() []byte { return file_server_proto_rawDescData } -var file_server_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_server_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_server_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_server_proto_msgTypes = make([]protoimpl.MessageInfo, 27) var file_server_proto_goTypes = []interface{}{ (ProtocolVersion)(0), // 0: looprpc.ProtocolVersion (ServerSwapState)(0), // 1: looprpc.ServerSwapState (RoutePaymentType)(0), // 2: looprpc.RoutePaymentType (PaymentFailureReason)(0), // 3: looprpc.PaymentFailureReason - (*ServerLoopOutRequest)(nil), // 4: looprpc.ServerLoopOutRequest - (*ServerLoopOutResponse)(nil), // 5: looprpc.ServerLoopOutResponse - (*ServerLoopOutQuoteRequest)(nil), // 6: looprpc.ServerLoopOutQuoteRequest - (*ServerLoopOutQuote)(nil), // 7: looprpc.ServerLoopOutQuote - (*ServerLoopOutTermsRequest)(nil), // 8: looprpc.ServerLoopOutTermsRequest - (*ServerLoopOutTerms)(nil), // 9: looprpc.ServerLoopOutTerms - (*ServerLoopInRequest)(nil), // 10: looprpc.ServerLoopInRequest - (*ServerLoopInResponse)(nil), // 11: looprpc.ServerLoopInResponse - (*ServerLoopInQuoteRequest)(nil), // 12: looprpc.ServerLoopInQuoteRequest - (*ServerLoopInQuoteResponse)(nil), // 13: looprpc.ServerLoopInQuoteResponse - (*ServerLoopInTermsRequest)(nil), // 14: looprpc.ServerLoopInTermsRequest - (*ServerLoopInTerms)(nil), // 15: looprpc.ServerLoopInTerms - (*ServerLoopOutPushPreimageRequest)(nil), // 16: looprpc.ServerLoopOutPushPreimageRequest - (*ServerLoopOutPushPreimageResponse)(nil), // 17: looprpc.ServerLoopOutPushPreimageResponse - (*SubscribeUpdatesRequest)(nil), // 18: looprpc.SubscribeUpdatesRequest - (*SubscribeLoopOutUpdatesResponse)(nil), // 19: looprpc.SubscribeLoopOutUpdatesResponse - (*SubscribeLoopInUpdatesResponse)(nil), // 20: looprpc.SubscribeLoopInUpdatesResponse - (*RouteCancel)(nil), // 21: looprpc.RouteCancel - (*HtlcAttempt)(nil), // 22: looprpc.HtlcAttempt - (*CancelLoopOutSwapRequest)(nil), // 23: looprpc.CancelLoopOutSwapRequest - (*CancelLoopOutSwapResponse)(nil), // 24: looprpc.CancelLoopOutSwapResponse - (*ServerProbeRequest)(nil), // 25: looprpc.ServerProbeRequest - (*ServerProbeResponse)(nil), // 26: looprpc.ServerProbeResponse - (*RouteHint)(nil), // 27: looprpc.RouteHint + (RoutingPlugin)(0), // 4: looprpc.RoutingPlugin + (*ServerLoopOutRequest)(nil), // 5: looprpc.ServerLoopOutRequest + (*ServerLoopOutResponse)(nil), // 6: looprpc.ServerLoopOutResponse + (*ServerLoopOutQuoteRequest)(nil), // 7: looprpc.ServerLoopOutQuoteRequest + (*ServerLoopOutQuote)(nil), // 8: looprpc.ServerLoopOutQuote + (*ServerLoopOutTermsRequest)(nil), // 9: looprpc.ServerLoopOutTermsRequest + (*ServerLoopOutTerms)(nil), // 10: looprpc.ServerLoopOutTerms + (*ServerLoopInRequest)(nil), // 11: looprpc.ServerLoopInRequest + (*ServerLoopInResponse)(nil), // 12: looprpc.ServerLoopInResponse + (*ServerLoopInQuoteRequest)(nil), // 13: looprpc.ServerLoopInQuoteRequest + (*ServerLoopInQuoteResponse)(nil), // 14: looprpc.ServerLoopInQuoteResponse + (*ServerLoopInTermsRequest)(nil), // 15: looprpc.ServerLoopInTermsRequest + (*ServerLoopInTerms)(nil), // 16: looprpc.ServerLoopInTerms + (*ServerLoopOutPushPreimageRequest)(nil), // 17: looprpc.ServerLoopOutPushPreimageRequest + (*ServerLoopOutPushPreimageResponse)(nil), // 18: looprpc.ServerLoopOutPushPreimageResponse + (*SubscribeUpdatesRequest)(nil), // 19: looprpc.SubscribeUpdatesRequest + (*SubscribeLoopOutUpdatesResponse)(nil), // 20: looprpc.SubscribeLoopOutUpdatesResponse + (*SubscribeLoopInUpdatesResponse)(nil), // 21: looprpc.SubscribeLoopInUpdatesResponse + (*RouteCancel)(nil), // 22: looprpc.RouteCancel + (*HtlcAttempt)(nil), // 23: looprpc.HtlcAttempt + (*CancelLoopOutSwapRequest)(nil), // 24: looprpc.CancelLoopOutSwapRequest + (*CancelLoopOutSwapResponse)(nil), // 25: looprpc.CancelLoopOutSwapResponse + (*ServerProbeRequest)(nil), // 26: looprpc.ServerProbeRequest + (*ServerProbeResponse)(nil), // 27: looprpc.ServerProbeResponse + (*RecommendRoutingPluginReq)(nil), // 28: looprpc.RecommendRoutingPluginReq + (*RecommendRoutingPluginRes)(nil), // 29: looprpc.RecommendRoutingPluginRes + (*ReportRoutingResultReq)(nil), // 30: looprpc.ReportRoutingResultReq + (*ReportRoutingResultRes)(nil), // 31: looprpc.ReportRoutingResultRes + (*RouteHint)(nil), // 32: 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 - 27, // 4: looprpc.ServerLoopInQuoteRequest.route_hints:type_name -> looprpc.RouteHint + 32, // 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 @@ -2365,39 +2728,47 @@ var file_server_proto_depIdxs = []int32{ 1, // 9: looprpc.SubscribeLoopOutUpdatesResponse.state:type_name -> looprpc.ServerSwapState 1, // 10: looprpc.SubscribeLoopInUpdatesResponse.state:type_name -> looprpc.ServerSwapState 2, // 11: looprpc.RouteCancel.route_type:type_name -> looprpc.RoutePaymentType - 22, // 12: looprpc.RouteCancel.attempts:type_name -> looprpc.HtlcAttempt + 23, // 12: looprpc.RouteCancel.attempts:type_name -> looprpc.HtlcAttempt 3, // 13: looprpc.RouteCancel.failure:type_name -> looprpc.PaymentFailureReason 0, // 14: looprpc.CancelLoopOutSwapRequest.protocol_version:type_name -> looprpc.ProtocolVersion - 21, // 15: looprpc.CancelLoopOutSwapRequest.route_cancel:type_name -> looprpc.RouteCancel + 22, // 15: looprpc.CancelLoopOutSwapRequest.route_cancel:type_name -> looprpc.RouteCancel 0, // 16: looprpc.ServerProbeRequest.protocol_version:type_name -> looprpc.ProtocolVersion - 27, // 17: looprpc.ServerProbeRequest.route_hints:type_name -> looprpc.RouteHint - 8, // 18: looprpc.SwapServer.LoopOutTerms:input_type -> looprpc.ServerLoopOutTermsRequest - 4, // 19: looprpc.SwapServer.NewLoopOutSwap:input_type -> looprpc.ServerLoopOutRequest - 16, // 20: looprpc.SwapServer.LoopOutPushPreimage:input_type -> looprpc.ServerLoopOutPushPreimageRequest - 6, // 21: looprpc.SwapServer.LoopOutQuote:input_type -> looprpc.ServerLoopOutQuoteRequest - 14, // 22: looprpc.SwapServer.LoopInTerms:input_type -> looprpc.ServerLoopInTermsRequest - 10, // 23: looprpc.SwapServer.NewLoopInSwap:input_type -> looprpc.ServerLoopInRequest - 12, // 24: looprpc.SwapServer.LoopInQuote:input_type -> looprpc.ServerLoopInQuoteRequest - 18, // 25: looprpc.SwapServer.SubscribeLoopOutUpdates:input_type -> looprpc.SubscribeUpdatesRequest - 18, // 26: looprpc.SwapServer.SubscribeLoopInUpdates:input_type -> looprpc.SubscribeUpdatesRequest - 23, // 27: looprpc.SwapServer.CancelLoopOutSwap:input_type -> looprpc.CancelLoopOutSwapRequest - 25, // 28: looprpc.SwapServer.Probe:input_type -> looprpc.ServerProbeRequest - 9, // 29: looprpc.SwapServer.LoopOutTerms:output_type -> looprpc.ServerLoopOutTerms - 5, // 30: looprpc.SwapServer.NewLoopOutSwap:output_type -> looprpc.ServerLoopOutResponse - 17, // 31: looprpc.SwapServer.LoopOutPushPreimage:output_type -> looprpc.ServerLoopOutPushPreimageResponse - 7, // 32: looprpc.SwapServer.LoopOutQuote:output_type -> looprpc.ServerLoopOutQuote - 15, // 33: looprpc.SwapServer.LoopInTerms:output_type -> looprpc.ServerLoopInTerms - 11, // 34: looprpc.SwapServer.NewLoopInSwap:output_type -> looprpc.ServerLoopInResponse - 13, // 35: looprpc.SwapServer.LoopInQuote:output_type -> looprpc.ServerLoopInQuoteResponse - 19, // 36: looprpc.SwapServer.SubscribeLoopOutUpdates:output_type -> looprpc.SubscribeLoopOutUpdatesResponse - 20, // 37: looprpc.SwapServer.SubscribeLoopInUpdates:output_type -> looprpc.SubscribeLoopInUpdatesResponse - 24, // 38: looprpc.SwapServer.CancelLoopOutSwap:output_type -> looprpc.CancelLoopOutSwapResponse - 26, // 39: looprpc.SwapServer.Probe:output_type -> looprpc.ServerProbeResponse - 29, // [29:40] is the sub-list for method output_type - 18, // [18:29] is the sub-list for method input_type - 18, // [18:18] is the sub-list for extension type_name - 18, // [18:18] is the sub-list for extension extendee - 0, // [0:18] is the sub-list for field type_name + 32, // 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 + 9, // 22: looprpc.SwapServer.LoopOutTerms:input_type -> looprpc.ServerLoopOutTermsRequest + 5, // 23: looprpc.SwapServer.NewLoopOutSwap:input_type -> looprpc.ServerLoopOutRequest + 17, // 24: looprpc.SwapServer.LoopOutPushPreimage:input_type -> looprpc.ServerLoopOutPushPreimageRequest + 7, // 25: looprpc.SwapServer.LoopOutQuote:input_type -> looprpc.ServerLoopOutQuoteRequest + 15, // 26: looprpc.SwapServer.LoopInTerms:input_type -> looprpc.ServerLoopInTermsRequest + 11, // 27: looprpc.SwapServer.NewLoopInSwap:input_type -> looprpc.ServerLoopInRequest + 13, // 28: looprpc.SwapServer.LoopInQuote:input_type -> looprpc.ServerLoopInQuoteRequest + 19, // 29: looprpc.SwapServer.SubscribeLoopOutUpdates:input_type -> looprpc.SubscribeUpdatesRequest + 19, // 30: looprpc.SwapServer.SubscribeLoopInUpdates:input_type -> looprpc.SubscribeUpdatesRequest + 24, // 31: looprpc.SwapServer.CancelLoopOutSwap:input_type -> looprpc.CancelLoopOutSwapRequest + 26, // 32: looprpc.SwapServer.Probe:input_type -> looprpc.ServerProbeRequest + 28, // 33: looprpc.SwapServer.RecommendRoutingPlugin:input_type -> looprpc.RecommendRoutingPluginReq + 30, // 34: looprpc.SwapServer.ReportRoutingResult:input_type -> looprpc.ReportRoutingResultReq + 10, // 35: looprpc.SwapServer.LoopOutTerms:output_type -> looprpc.ServerLoopOutTerms + 6, // 36: looprpc.SwapServer.NewLoopOutSwap:output_type -> looprpc.ServerLoopOutResponse + 18, // 37: looprpc.SwapServer.LoopOutPushPreimage:output_type -> looprpc.ServerLoopOutPushPreimageResponse + 8, // 38: looprpc.SwapServer.LoopOutQuote:output_type -> looprpc.ServerLoopOutQuote + 16, // 39: looprpc.SwapServer.LoopInTerms:output_type -> looprpc.ServerLoopInTerms + 12, // 40: looprpc.SwapServer.NewLoopInSwap:output_type -> looprpc.ServerLoopInResponse + 14, // 41: looprpc.SwapServer.LoopInQuote:output_type -> looprpc.ServerLoopInQuoteResponse + 20, // 42: looprpc.SwapServer.SubscribeLoopOutUpdates:output_type -> looprpc.SubscribeLoopOutUpdatesResponse + 21, // 43: looprpc.SwapServer.SubscribeLoopInUpdates:output_type -> looprpc.SubscribeLoopInUpdatesResponse + 25, // 44: looprpc.SwapServer.CancelLoopOutSwap:output_type -> looprpc.CancelLoopOutSwapResponse + 27, // 45: looprpc.SwapServer.Probe:output_type -> looprpc.ServerProbeResponse + 29, // 46: looprpc.SwapServer.RecommendRoutingPlugin:output_type -> looprpc.RecommendRoutingPluginRes + 31, // 47: looprpc.SwapServer.ReportRoutingResult:output_type -> looprpc.ReportRoutingResultRes + 35, // [35:48] is the sub-list for method output_type + 22, // [22:35] is the sub-list for method input_type + 22, // [22:22] is the sub-list for extension type_name + 22, // [22:22] is the sub-list for extension extendee + 0, // [0:22] is the sub-list for field type_name } func init() { file_server_proto_init() } @@ -2683,6 +3054,54 @@ func file_server_proto_init() { return nil } } + file_server_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RecommendRoutingPluginReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_server_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RecommendRoutingPluginRes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_server_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReportRoutingResultReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_server_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReportRoutingResultRes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_server_proto_msgTypes[19].OneofWrappers = []interface{}{ (*CancelLoopOutSwapRequest_RouteCancel)(nil), @@ -2692,8 +3111,8 @@ func file_server_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_server_proto_rawDesc, - NumEnums: 4, - NumMessages: 23, + NumEnums: 5, + NumMessages: 27, NumExtensions: 0, NumServices: 1, }, diff --git a/swapserverrpc/server.proto b/swapserverrpc/server.proto index cd55bce..5391a38 100644 --- a/swapserverrpc/server.proto +++ b/swapserverrpc/server.proto @@ -37,6 +37,12 @@ service SwapServer { returns (CancelLoopOutSwapResponse); rpc Probe (ServerProbeRequest) returns (ServerProbeResponse); + + rpc RecommendRoutingPlugin (RecommendRoutingPluginReq) + returns (RecommendRoutingPluginRes); + + rpc ReportRoutingResult (ReportRoutingResultReq) + returns (ReportRoutingResultRes); } /** @@ -82,6 +88,10 @@ enum ProtocolVersion { // The client is able to ask the server to probe to test inbound // liquidity. PROBE = 8; + + // The client may ask the server to use a custom routing helper plugin in + // order to enhance off-chain payments corresponding to a swap. + ROUTING_PLUGIN = 9; } message ServerLoopOutRequest { @@ -458,3 +468,53 @@ message ServerProbeRequest { message ServerProbeResponse { } + +message RecommendRoutingPluginReq { + ProtocolVersion protocol_version = 1; + + // The hash of the swap requesting a routing plugin. + bytes swap_hash = 2; + + // The payment address for the swap invoice, used to ensure that only the + // swap owner can request routing plugin recommendation. + bytes payment_address = 3; +} + +enum RoutingPlugin { + // Client won't use any plugins to help with payment routing. + NONE = 0; + + // Client will try more expensive routes for off-chain payments. + LOW_HIGH = 1; +} + +message RecommendRoutingPluginRes { + // The routing plugin to use for off-chain payments. + RoutingPlugin plugin = 1; +} + +message ReportRoutingResultReq { + ProtocolVersion protocol_version = 1; + + // The swap hash. + bytes swap_hash = 2; + + // The payment address for the swap invoice, used to ensure that only the + // swap owner can report routing result. + bytes payment_address = 3; + + // The routing plugin that was used. + RoutingPlugin plugin = 4; + + // Whether this payment succeeded. + bool success = 5; + + // The number of payment attempts using the plugin. + int32 attempts = 6; + + // Total time used in milliseconds. + int64 total_time = 7; +} + +message ReportRoutingResultRes { +} diff --git a/swapserverrpc/server_grpc.pb.go b/swapserverrpc/server_grpc.pb.go index b8c3e30..20f9f28 100644 --- a/swapserverrpc/server_grpc.pb.go +++ b/swapserverrpc/server_grpc.pb.go @@ -29,6 +29,8 @@ type SwapServerClient interface { SubscribeLoopInUpdates(ctx context.Context, in *SubscribeUpdatesRequest, opts ...grpc.CallOption) (SwapServer_SubscribeLoopInUpdatesClient, error) CancelLoopOutSwap(ctx context.Context, in *CancelLoopOutSwapRequest, opts ...grpc.CallOption) (*CancelLoopOutSwapResponse, error) Probe(ctx context.Context, in *ServerProbeRequest, opts ...grpc.CallOption) (*ServerProbeResponse, error) + RecommendRoutingPlugin(ctx context.Context, in *RecommendRoutingPluginReq, opts ...grpc.CallOption) (*RecommendRoutingPluginRes, error) + ReportRoutingResult(ctx context.Context, in *ReportRoutingResultReq, opts ...grpc.CallOption) (*ReportRoutingResultRes, error) } type swapServerClient struct { @@ -184,6 +186,24 @@ func (c *swapServerClient) Probe(ctx context.Context, in *ServerProbeRequest, op return out, nil } +func (c *swapServerClient) RecommendRoutingPlugin(ctx context.Context, in *RecommendRoutingPluginReq, opts ...grpc.CallOption) (*RecommendRoutingPluginRes, error) { + out := new(RecommendRoutingPluginRes) + err := c.cc.Invoke(ctx, "/looprpc.SwapServer/RecommendRoutingPlugin", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *swapServerClient) ReportRoutingResult(ctx context.Context, in *ReportRoutingResultReq, opts ...grpc.CallOption) (*ReportRoutingResultRes, error) { + out := new(ReportRoutingResultRes) + err := c.cc.Invoke(ctx, "/looprpc.SwapServer/ReportRoutingResult", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // SwapServerServer is the server API for SwapServer service. // All implementations must embed UnimplementedSwapServerServer // for forward compatibility @@ -199,6 +219,8 @@ type SwapServerServer interface { SubscribeLoopInUpdates(*SubscribeUpdatesRequest, SwapServer_SubscribeLoopInUpdatesServer) error CancelLoopOutSwap(context.Context, *CancelLoopOutSwapRequest) (*CancelLoopOutSwapResponse, error) Probe(context.Context, *ServerProbeRequest) (*ServerProbeResponse, error) + RecommendRoutingPlugin(context.Context, *RecommendRoutingPluginReq) (*RecommendRoutingPluginRes, error) + ReportRoutingResult(context.Context, *ReportRoutingResultReq) (*ReportRoutingResultRes, error) mustEmbedUnimplementedSwapServerServer() } @@ -239,6 +261,12 @@ func (UnimplementedSwapServerServer) CancelLoopOutSwap(context.Context, *CancelL func (UnimplementedSwapServerServer) Probe(context.Context, *ServerProbeRequest) (*ServerProbeResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Probe not implemented") } +func (UnimplementedSwapServerServer) RecommendRoutingPlugin(context.Context, *RecommendRoutingPluginReq) (*RecommendRoutingPluginRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method RecommendRoutingPlugin not implemented") +} +func (UnimplementedSwapServerServer) ReportRoutingResult(context.Context, *ReportRoutingResultReq) (*ReportRoutingResultRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method ReportRoutingResult not implemented") +} func (UnimplementedSwapServerServer) mustEmbedUnimplementedSwapServerServer() {} // UnsafeSwapServerServer may be embedded to opt out of forward compatibility for this service. @@ -456,6 +484,42 @@ func _SwapServer_Probe_Handler(srv interface{}, ctx context.Context, dec func(in return interceptor(ctx, in, info, handler) } +func _SwapServer_RecommendRoutingPlugin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RecommendRoutingPluginReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapServerServer).RecommendRoutingPlugin(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapServer/RecommendRoutingPlugin", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapServerServer).RecommendRoutingPlugin(ctx, req.(*RecommendRoutingPluginReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _SwapServer_ReportRoutingResult_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportRoutingResultReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapServerServer).ReportRoutingResult(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapServer/ReportRoutingResult", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapServerServer).ReportRoutingResult(ctx, req.(*ReportRoutingResultReq)) + } + return interceptor(ctx, in, info, handler) +} + // SwapServer_ServiceDesc is the grpc.ServiceDesc for SwapServer service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -499,6 +563,14 @@ var SwapServer_ServiceDesc = grpc.ServiceDesc{ MethodName: "Probe", Handler: _SwapServer_Probe_Handler, }, + { + MethodName: "RecommendRoutingPlugin", + Handler: _SwapServer_RecommendRoutingPlugin_Handler, + }, + { + MethodName: "ReportRoutingResult", + Handler: _SwapServer_ReportRoutingResult_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/test/lightning_client_mock.go b/test/lightning_client_mock.go index 2202223..92d803a 100644 --- a/test/lightning_client_mock.go +++ b/test/lightning_client_mock.go @@ -186,10 +186,14 @@ func (h *mockLightningClient) ListTransactions( func (h *mockLightningClient) GetNodeInfo(ctx context.Context, pubKeyBytes route.Vertex, includeChannels bool) (*lndclient.NodeInfo, error) { - nodeInfo := lndclient.NodeInfo{} + nodeInfo := &lndclient.NodeInfo{ + Node: &lndclient.Node{ + PubKey: pubKeyBytes, + }, + } if !includeChannels { - return nil, nil + return nodeInfo, nil } nodePubKey, err := route.NewVertexFromStr(h.lnd.NodePubkey) @@ -214,7 +218,7 @@ func (h *mockLightningClient) GetNodeInfo(ctx context.Context, nodeInfo.ChannelCount = len(nodeInfo.Channels) - return &nodeInfo, nil + return nodeInfo, nil } // GetChanInfo retrieves all the info the node has on the given channel diff --git a/test/lnd_services_mock.go b/test/lnd_services_mock.go index ccb14d8..ad227ef 100644 --- a/test/lnd_services_mock.go +++ b/test/lnd_services_mock.go @@ -163,11 +163,12 @@ type LndMockServices struct { // keyed by hash string. Invoices map[lntypes.Hash]*lndclient.Invoice - Channels []lndclient.ChannelInfo - ChannelEdges map[uint64]*lndclient.ChannelEdge - ClosedChannels []lndclient.ClosedChannel - ForwardingEvents []lndclient.ForwardingEvent - Payments []lndclient.Payment + Channels []lndclient.ChannelInfo + ChannelEdges map[uint64]*lndclient.ChannelEdge + ClosedChannels []lndclient.ClosedChannel + ForwardingEvents []lndclient.ForwardingEvent + Payments []lndclient.Payment + MissionControlState []lndclient.MissionControlEntry WaitForFinished func() diff --git a/test/router_mock.go b/test/router_mock.go index d094b8f..e14411a 100644 --- a/test/router_mock.go +++ b/test/router_mock.go @@ -42,3 +42,66 @@ func (r *mockRouter) TrackPayment(ctx context.Context, return statusChan, errorChan, nil } + +func (r *mockRouter) QueryMissionControl(ctx context.Context) ( + []lndclient.MissionControlEntry, error) { + + return r.lnd.MissionControlState, nil +} + +// ImpotMissionControl is a mocked reimplementation of the pair import. +// Reference: lnd/router/missioncontrol_state.go:importSnapshot(). +func (r *mockRouter) ImportMissionControl(ctx context.Context, + entries []lndclient.MissionControlEntry) error { + + for _, entry := range entries { + found := false + for i := range r.lnd.MissionControlState { + current := &r.lnd.MissionControlState[i] + if entry.NodeFrom == current.NodeFrom && + entry.NodeTo == current.NodeTo { + + // Mark that the entry has been found and updated. + found = true + + // Import failure result first. We ignore failure + // relax interval here for convenience. + current.FailTime = entry.FailTime + current.FailAmt = entry.FailAmt + + switch { + case entry.FailAmt == 0: + current.SuccessAmt = 0 + + case entry.FailAmt <= current.SuccessAmt: + current.SuccessAmt = entry.FailAmt - 1 + } + + // Import success result second. + current.SuccessTime = entry.SuccessTime + if entry.SuccessAmt > current.SuccessAmt { + current.SuccessAmt = entry.SuccessAmt + } + + if !current.FailTime.IsZero() && + entry.SuccessAmt >= current.FailAmt { + + current.FailAmt = entry.SuccessAmt + 1 + } + } + } + + if !found { + r.lnd.MissionControlState = append( + r.lnd.MissionControlState, entry, + ) + } + } + + return nil +} + +func (r *mockRouter) ResetMissionControl(ctx context.Context) error { + r.lnd.MissionControlState = []lndclient.MissionControlEntry{} + return nil +}