From 7ba18216960ea5213e3fcdfcb6a6c4286c8a0dee Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 8 Feb 2021 09:38:23 +0200 Subject: [PATCH 1/6] liquidity: fail suggest swaps when no rules are set In an effort to surface more information about why autoloop is not executing, we add an error when suggest swaps is called with no rules. In other cases we can surface a reason enum with each rule that is set, but in the case where we have no rules, there are no results to accompany with reasons. --- cmd/loop/liquidity.go | 20 +++++++++++++++++--- liquidity/liquidity.go | 14 ++++++++++++-- liquidity/liquidity_test.go | 4 +++- loopd/swapclient_server.go | 10 +++++++++- release_notes.md | 3 +++ 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/cmd/loop/liquidity.go b/cmd/loop/liquidity.go index 57b48ec..dc6b9bb 100644 --- a/cmd/loop/liquidity.go +++ b/cmd/loop/liquidity.go @@ -2,12 +2,15 @@ package main import ( "context" + "errors" "fmt" "strconv" "github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/looprpc" "github.com/urfave/cli" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var getLiquidityParamsCommand = cli.Command{ @@ -411,11 +414,22 @@ func suggestSwap(ctx *cli.Context) error { resp, err := client.SuggestSwaps( context.Background(), &looprpc.SuggestSwapsRequest{}, ) - if err != nil { + if err == nil { + printJSON(resp) + return nil + } + + // If we got an error because no rules are set, we want to display a + // friendly message. + rpcErr, ok := status.FromError(err) + if !ok { return err } - printJSON(resp) + if rpcErr.Code() != codes.FailedPrecondition { + return err + } - return nil + return errors.New("no rules set for autolooper, please set rules " + + "using the setrule command") } diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index f70a08a..ab276a8 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -178,6 +178,9 @@ var ( // less than the server minimum. ErrMinLessThanServer = errors.New("minimum swap amount is less than " + "server minimum") + + // ErrNoRules is returned when no rules are set for swap suggestions. + ErrNoRules = errors.New("no rules set for autoloop") ) // Config contains the external functionality required to run the @@ -439,7 +442,14 @@ func (m *Manager) Run(ctx context.Context) error { for { select { case <-m.cfg.AutoloopTicker.Ticks(): - if err := m.autoloop(ctx); err != nil { + err := m.autoloop(ctx) + switch err { + case ErrNoRules: + log.Debugf("No rules configured for autoloop") + + case nil: + + default: log.Errorf("autoloop failed: %v", err) } @@ -562,7 +572,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // If we have no rules set, exit early to avoid unnecessary calls to // lnd and the server. if len(m.params.ChannelRules) == 0 { - return nil, nil + return nil, ErrNoRules } // If our start date is in the future, we interpret this as meaning that diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index 0872394..7bbe1f7 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -496,10 +496,12 @@ func TestSuggestSwaps(t *testing.T) { name string rules map[lnwire.ShortChannelID]*ThresholdRule swaps []loop.OutRequest + err error }{ { name: "no rules", rules: map[lnwire.ShortChannelID]*ThresholdRule{}, + err: ErrNoRules, }, { name: "loop out", @@ -534,7 +536,7 @@ func TestSuggestSwaps(t *testing.T) { testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - testCase.swaps, nil, + testCase.swaps, testCase.err, ) }) } diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index abacd7a..ff310fc 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -21,6 +21,8 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/queue" "github.com/lightningnetwork/lnd/routing/route" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) const ( @@ -703,7 +705,13 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context, _ *looprpc.SuggestSwapsRequest) (*looprpc.SuggestSwapsResponse, error) { swaps, err := s.liquidityMgr.SuggestSwaps(ctx, false) - if err != nil { + switch err { + case liquidity.ErrNoRules: + return nil, status.Error(codes.FailedPrecondition, err.Error()) + + case nil: + + default: return nil, err } diff --git a/release_notes.md b/release_notes.md index c5ee3dd..acc5120 100644 --- a/release_notes.md +++ b/release_notes.md @@ -31,5 +31,8 @@ This file tracks release notes for the loop client. been renamed to `Autoloop`, `AutoloopBudgetSat` and `AutoloopBudgetStartSec`. * The `autoout` flag for enabling automatic dispatch of loop out swaps has been renamed to `autoloop` so that it can cover loop out and loop in. +* The `SuggestSwaps` rpc call will now fail with a `FailedPrecondition` grpc + error code if no rules are configured for the autolooper. Previously the rpc + would return an empty response. #### Bug Fixes From d5096cdc21678d67f9581ccfd1550c622ffb9204 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 8 Feb 2021 09:38:59 +0200 Subject: [PATCH 2/6] liquidity: relax restriction which requires no unrestricted swaps In practice, this restriction has proven to be too strict. Autoloop will now only hold off on a swap for a channel if a manual swap is specifically using that channel. --- docs/autoloop.md | 4 ---- liquidity/liquidity.go | 15 ++------------- liquidity/liquidity_test.go | 12 ++++++++---- release_notes.md | 4 ++++ 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/docs/autoloop.md b/docs/autoloop.md index 2a25232..ae9e244 100644 --- a/docs/autoloop.md +++ b/docs/autoloop.md @@ -224,8 +224,4 @@ in manually dispatched swaps - for loop out, this would mean the channel is specified in the outgoing channel swap, and for loop in the channel's peer is specified as the last hop for an ongoing swap. This check is put in place to prevent the autolooper from interfering with swaps you have created yourself. -If there is an ongoing swap that does not have a restriction placed on it (no -outgoing channel set, or last hop), then the autolooper will take no action -until it has resolved, because it does not know how that swap will affect -liquidity balances. diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index ab276a8..b9a8f9c 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -930,8 +930,6 @@ func (m *Manager) checkExistingAutoLoops(ctx context.Context, // getEligibleChannels takes lists of our existing loop out and in swaps, and // gets a list of channels that are not currently being utilized for a swap. -// If an unrestricted swap is ongoing, we return an empty set of channels -// because we don't know which channels balances it will affect. func (m *Manager) getEligibleChannels(ctx context.Context, loopOut []*loopdb.LoopOut, loopIn []*loopdb.LoopIn) ( []lndclient.ChannelInfo, error) { @@ -988,13 +986,6 @@ func (m *Manager) getEligibleChannels(ctx context.Context, continue } - if len(chanSet) == 0 { - log.Debugf("Ongoing unrestricted loop out: "+ - "%v, no suggestions at present", out.Hash) - - return nil, nil - } - for _, id := range chanSet { chanID := lnwire.NewShortChanIDFromInt(id) existingOut[chanID] = true @@ -1007,11 +998,9 @@ func (m *Manager) getEligibleChannels(ctx context.Context, continue } + // Skip over swaps that may come through any peer. if in.Contract.LastHop == nil { - log.Debugf("Ongoing unrestricted loop in: "+ - "%v, no suggestions at present", in.Hash) - - return nil, nil + continue } existingIn[*in.Contract.LastHop] = true diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index 7bbe1f7..168eefc 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -299,7 +299,7 @@ func TestRestrictedSuggestions(t *testing.T) { { name: "unrestricted loop out", channels: []lndclient.ChannelInfo{ - channel1, channel2, + channel1, }, loopOut: []*loopdb.LoopOut{ { @@ -308,12 +308,14 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: nil, + expected: []loop.OutRequest{ + chan1Rec, + }, }, { name: "unrestricted loop in", channels: []lndclient.ChannelInfo{ - channel1, channel2, + channel1, }, loopIn: []*loopdb.LoopIn{ { @@ -322,7 +324,9 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: nil, + expected: []loop.OutRequest{ + chan1Rec, + }, }, { name: "restricted loop out", diff --git a/release_notes.md b/release_notes.md index acc5120..e0af837 100644 --- a/release_notes.md +++ b/release_notes.md @@ -24,6 +24,10 @@ This file tracks release notes for the loop client. baked one with the exact permissions needed for Loop. If the now deprecated flag/option `--lnd.macaroondir` is used, it will fall back to use only the `admin.macaroon` from that directory. +* The rules used for autoloop have been relaxed to allow autoloop to dispatch + swaps even if there are manually initiated swaps that are not limited to a + single channel in progress. This change was made to allow autoloop to coexist + with manual swaps. #### Breaking Changes * The `AutoOut`, `AutoOutBudgetSat` and `AutoOutBudgetStartSec` fields in the From b9b75c3c32a744646f3cab33e196d33d465b0635 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 8 Feb 2021 09:39:01 +0200 Subject: [PATCH 3/6] liquidity: refactor eligible channels logic to ineligible channels This commit switches up our eligible channels logic to rather return a struct containing information about our current swap traffic. This change is made in preparation for returning reasons indicating why we did not perform a swap for a specific channel - when we only return eligible swaps, we lose the information about why all the excluded channels weren't used. --- liquidity/liquidity.go | 111 ++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index b9a8f9c..481fe5c 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -652,25 +652,32 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( return nil, nil } - eligible, err := m.getEligibleChannels(ctx, loopOut, loopIn) + channels, err := m.cfg.Lnd.Client.ListChannels(ctx) if err != nil { return nil, err } + // Get a summary of the channels and peers that are not eligible due + // to ongoing swaps. + traffic := m.currentSwapTraffic(loopOut, loopIn) + var suggestions []loop.OutRequest - for _, channel := range eligible { - channelID := lnwire.NewShortChanIDFromInt(channel.ChannelID) - rule, ok := m.params.ChannelRules[channelID] + + for _, channel := range channels { + balance := newBalances(channel) + + rule, ok := m.params.ChannelRules[balance.channelID] if !ok { continue } - balance := newBalances(channel) - - suggestion := rule.suggestSwap(balance, restrictions) + if !traffic.maySwap(channel.PubKeyBytes, balance.channelID) { + continue + } // We can have nil suggestions in the case where no action is // required, so we skip over them. + suggestion := rule.suggestSwap(balance, restrictions) if suggestion == nil { continue } @@ -928,17 +935,13 @@ func (m *Manager) checkExistingAutoLoops(ctx context.Context, return &summary, nil } -// getEligibleChannels takes lists of our existing loop out and in swaps, and -// gets a list of channels that are not currently being utilized for a swap. -func (m *Manager) getEligibleChannels(ctx context.Context, - loopOut []*loopdb.LoopOut, loopIn []*loopdb.LoopIn) ( - []lndclient.ChannelInfo, error) { +// currentSwapTraffic examines our existing swaps and returns a summary of the +// current activity which can be used to determine whether we should perform +// any swaps. +func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut, + loopIn []*loopdb.LoopIn) *swapTraffic { - var ( - existingOut = make(map[lnwire.ShortChannelID]bool) - existingIn = make(map[route.Vertex]bool) - failedOut = make(map[lnwire.ShortChannelID]time.Time) - ) + traffic := newSwapTraffic() // Failure cutoff is the most recent failure timestamp we will still // consider a channel eligible. Any channels involved in swaps that have @@ -971,7 +974,7 @@ func (m *Manager) getEligibleChannels(ctx context.Context, id, ) - failedOut[chanID] = failedAt + traffic.failedLoopOut[chanID] = failedAt } } } @@ -988,7 +991,7 @@ func (m *Manager) getEligibleChannels(ctx context.Context, for _, id := range chanSet { chanID := lnwire.NewShortChanIDFromInt(id) - existingOut[chanID] = true + traffic.ongoingLoopOut[chanID] = true } } @@ -1003,51 +1006,55 @@ func (m *Manager) getEligibleChannels(ctx context.Context, continue } - existingIn[*in.Contract.LastHop] = true + traffic.ongoingLoopIn[*in.Contract.LastHop] = true } - channels, err := m.cfg.Lnd.Client.ListChannels(ctx) - if err != nil { - return nil, err - } + return traffic +} - // Run through our set of channels and skip over any channels that - // are currently being utilized by a restricted swap (where restricted - // means that a loop out limited channels, or a loop in limited last - // hop). - var eligible []lndclient.ChannelInfo - for _, channel := range channels { - shortID := lnwire.NewShortChanIDFromInt(channel.ChannelID) +// swapTraffic contains a summary of our current and previously failed swaps. +type swapTraffic struct { + ongoingLoopOut map[lnwire.ShortChannelID]bool + ongoingLoopIn map[route.Vertex]bool + failedLoopOut map[lnwire.ShortChannelID]time.Time +} - lastFail, recentFail := failedOut[shortID] - if recentFail { - log.Debugf("Channel: %v not eligible for "+ - "suggestions, was part of a failed swap at: %v", - channel.ChannelID, lastFail) +func newSwapTraffic() *swapTraffic { + return &swapTraffic{ + ongoingLoopOut: make(map[lnwire.ShortChannelID]bool), + ongoingLoopIn: make(map[route.Vertex]bool), + failedLoopOut: make(map[lnwire.ShortChannelID]time.Time), + } +} - continue - } +// maySwap returns a boolean that indicates whether we may perform a swap for a +// peer and its set of channels. +func (s *swapTraffic) maySwap(peer route.Vertex, + chanID lnwire.ShortChannelID) bool { - if existingOut[shortID] { - log.Debugf("Channel: %v not eligible for "+ - "suggestions, ongoing loop out utilizing "+ - "channel", channel.ChannelID) + lastFail, recentFail := s.failedLoopOut[chanID] + if recentFail { + log.Debugf("Channel: %v not eligible for suggestions, was "+ + "part of a failed swap at: %v", chanID, lastFail) - continue - } + return false + } - if existingIn[channel.PubKeyBytes] { - log.Debugf("Channel: %v not eligible for "+ - "suggestions, ongoing loop in utilizing "+ - "peer", channel.ChannelID) + if s.ongoingLoopOut[chanID] { + log.Debugf("Channel: %v not eligible for suggestions, "+ + "ongoing loop out utilizing channel", chanID) - continue - } + return false + } + + if s.ongoingLoopIn[peer] { + log.Debugf("Peer: %x not eligible for suggestions ongoing "+ + "loop in utilizing peer", peer) - eligible = append(eligible, channel) + return false } - return eligible, nil + return true } // checkFeeLimits takes a set of fees for a swap and checks whether they exceed From 7c4d71b1754d6310dec9be66f1e1b0ceb8986691 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 8 Feb 2021 09:39:02 +0200 Subject: [PATCH 4/6] liquidity: add reasons for autoloops not executing --- liquidity/liquidity.go | 149 ++++++++++++++----- liquidity/liquidity_test.go | 289 +++++++++++++++++++++++++----------- liquidity/reasons.go | 62 ++++++++ loopd/swapclient_server.go | 4 +- 4 files changed, 373 insertions(+), 131 deletions(-) create mode 100644 liquidity/reasons.go diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 481fe5c..6586a00 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -516,12 +516,12 @@ func cloneParameters(params Parameters) Parameters { // autoloop gets a set of suggested swaps and dispatches them automatically if // we have automated looping enabled. func (m *Manager) autoloop(ctx context.Context) error { - swaps, err := m.SuggestSwaps(ctx, true) + suggestion, err := m.SuggestSwaps(ctx, true) if err != nil { return err } - for _, swap := range swaps { + for _, swap := range suggestion.OutSwaps { // If we don't actually have dispatch of swaps enabled, log // suggestions. if !m.params.Autoloop { @@ -557,6 +557,36 @@ func (m *Manager) ForceAutoLoop(ctx context.Context) error { } } +// Suggestions provides a set of suggested swaps, and the set of channels that +// were excluded from consideration. +type Suggestions struct { + // OutSwaps is the set of loop out swaps that we suggest executing. + OutSwaps []loop.OutRequest + + // DisqualifiedChans maps the set of channels that we do not recommend + // swaps on to the reason that we did not recommend a swap. + DisqualifiedChans map[lnwire.ShortChannelID]Reason +} + +func newSuggestions() *Suggestions { + return &Suggestions{ + DisqualifiedChans: make(map[lnwire.ShortChannelID]Reason), + } +} + +// singleReasonSuggestion is a helper function which returns a set of +// suggestions where all of our rules are disqualified due to a reason that +// applies to all of them (such as being out of budget). +func (m *Manager) singleReasonSuggestion(reason Reason) *Suggestions { + resp := newSuggestions() + + for id := range m.params.ChannelRules { + resp.DisqualifiedChans[id] = reason + } + + return resp +} + // SuggestSwaps returns a set of swap suggestions based on our current liquidity // balance for the set of rules configured for the manager, failing if there are // no rules set. It takes an autoloop boolean that indicates whether the @@ -564,7 +594,7 @@ func (m *Manager) ForceAutoLoop(ctx context.Context) error { // to determine the information we add to our swap suggestion and whether we // return any suggestions. func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( - []loop.OutRequest, error) { + *Suggestions, error) { m.paramsLock.Lock() defer m.paramsLock.Unlock() @@ -582,7 +612,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( log.Debugf("autoloop fee budget start time: %v is in "+ "the future", m.params.AutoFeeStartDate) - return nil, nil + return m.singleReasonSuggestion(ReasonBudgetNotStarted), nil } // Before we get any swap suggestions, we check what the current fee @@ -603,7 +633,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( satPerKwToSatPerVByte(estimate), satPerKwToSatPerVByte(m.params.SweepFeeRateLimit)) - return nil, nil + return m.singleReasonSuggestion(ReasonSweepFees), nil } // Get the current server side restrictions, combined with the client @@ -640,7 +670,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( m.params.AutoFeeBudget, summary.spentFees, summary.pendingFees) - return nil, nil + return m.singleReasonSuggestion(ReasonBudgetElapsed), nil } // If we have already reached our total allowed number of in flight @@ -649,7 +679,8 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( if allowedSwaps <= 0 { log.Debugf("%v autoloops allowed, %v in flight", m.params.MaxAutoInFlight, summary.inFlightCount) - return nil, nil + + return m.singleReasonSuggestion(ReasonInFlight), nil } channels, err := m.cfg.Lnd.Client.ListChannels(ctx) @@ -661,7 +692,10 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // to ongoing swaps. traffic := m.currentSwapTraffic(loopOut, loopIn) - var suggestions []loop.OutRequest + var ( + suggestions []loop.OutRequest + disqualified = make(map[lnwire.ShortChannelID]Reason) + ) for _, channel := range channels { balance := newBalances(channel) @@ -671,7 +705,11 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( continue } - if !traffic.maySwap(channel.PubKeyBytes, balance.channelID) { + // Check whether we can perform a swap, adding the channel to + // our set of disqualified swaps if it is not eligible. + reason := traffic.maySwap(channel.PubKeyBytes, balance.channelID) + if reason != ReasonNone { + disqualified[balance.channelID] = reason continue } @@ -679,6 +717,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // required, so we skip over them. suggestion := rule.suggestSwap(balance, restrictions) if suggestion == nil { + disqualified[balance.channelID] = ReasonLiquidityOk continue } @@ -700,11 +739,9 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // Check that the estimated fees for the suggested swap are // below the fee limits configured by the manager. - err = m.checkFeeLimits(quote, suggestion.Amount) - if err != nil { - log.Infof("suggestion: %v expected fees too high: %v", - suggestion, err) - + feeReason := m.checkFeeLimits(quote, suggestion.Amount) + if feeReason != ReasonNone { + disqualified[balance.channelID] = feeReason continue } @@ -717,10 +754,16 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( suggestions = append(suggestions, outRequest) } - // If we have no suggestions after we have applied all of our limits, - // just return. + // Finally, run through all possible swaps, excluding swaps that are + // not feasible due to fee or budget restrictions. + resp := &Suggestions{ + DisqualifiedChans: disqualified, + } + + // If we have no swaps to execute after we have applied all of our + // limits, just return our set of disqualified swaps. if len(suggestions) == 0 { - return nil, nil + return resp, nil } // Sort suggestions by amount in descending order. @@ -730,12 +773,38 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // Run through our suggested swaps in descending order of amount and // return all of the swaps which will fit within our remaining budget. - var ( - available = m.params.AutoFeeBudget - summary.totalFees() - inBudget []loop.OutRequest - ) + available := m.params.AutoFeeBudget - summary.totalFees() + + // setReason is a helper that adds a swap's channels to our disqualified + // list with the reason provided. + setReason := func(reason Reason, swap loop.OutRequest) { + for _, id := range swap.OutgoingChanSet { + chanID := lnwire.NewShortChanIDFromInt(id) + + resp.DisqualifiedChans[chanID] = reason + } + } for _, swap := range suggestions { + swap := swap + + // If we do not have enough funds available, or we hit our + // in flight limit, we record this value for the rest of the + // swaps. + var reason Reason + switch { + case available == 0: + reason = ReasonBudgetInsufficient + + case len(resp.OutSwaps) == allowedSwaps: + reason = ReasonInFlight + } + + if reason != ReasonNone { + setReason(reason, swap) + continue + } + fees := worstCaseOutFees( swap.MaxPrepayRoutingFee, swap.MaxSwapRoutingFee, swap.MaxSwapFee, swap.MaxMinerFee, swap.MaxPrepayAmount, @@ -746,17 +815,13 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // fall within the budget and decrement our available amount. if fees <= available { available -= fees - inBudget = append(inBudget, swap) - } - - // If we're out of budget, or we have hit the max number of - // swaps that we want to dispatch at one time, exit early. - if available == 0 || allowedSwaps == len(inBudget) { - break + resp.OutSwaps = append(resp.OutSwaps, swap) + } else { + setReason(ReasonBudgetInsufficient, swap) } } - return inBudget, nil + return resp, nil } // getSwapRestrictions queries the server for its latest swap size restrictions, @@ -1030,56 +1095,62 @@ func newSwapTraffic() *swapTraffic { // maySwap returns a boolean that indicates whether we may perform a swap for a // peer and its set of channels. func (s *swapTraffic) maySwap(peer route.Vertex, - chanID lnwire.ShortChannelID) bool { + chanID lnwire.ShortChannelID) Reason { lastFail, recentFail := s.failedLoopOut[chanID] if recentFail { log.Debugf("Channel: %v not eligible for suggestions, was "+ "part of a failed swap at: %v", chanID, lastFail) - return false + return ReasonFailureBackoff } if s.ongoingLoopOut[chanID] { log.Debugf("Channel: %v not eligible for suggestions, "+ "ongoing loop out utilizing channel", chanID) - return false + return ReasonLoopOut } if s.ongoingLoopIn[peer] { log.Debugf("Peer: %x not eligible for suggestions ongoing "+ "loop in utilizing peer", peer) - return false + return ReasonLoopIn } - return true + return ReasonNone } // checkFeeLimits takes a set of fees for a swap and checks whether they exceed // our swap limits. func (m *Manager) checkFeeLimits(quote *loop.LoopOutQuote, - swapAmt btcutil.Amount) error { + swapAmt btcutil.Amount) Reason { maxFee := ppmToSat(swapAmt, m.params.MaximumSwapFeePPM) if quote.SwapFee > maxFee { - return fmt.Errorf("quoted swap fee: %v > maximum swap fee: %v", + log.Debugf("quoted swap fee: %v > maximum swap fee: %v", quote.SwapFee, maxFee) + + return ReasonSwapFee } if quote.MinerFee > m.params.MaximumMinerFee { - return fmt.Errorf("quoted miner fee: %v > maximum miner "+ + log.Debugf("quoted miner fee: %v > maximum miner "+ "fee: %v", quote.MinerFee, m.params.MaximumMinerFee) + + return ReasonMinerFee } if quote.PrepayAmount > m.params.MaximumPrepay { - return fmt.Errorf("quoted prepay: %v > maximum prepay: %v", + log.Debugf("quoted prepay: %v > maximum prepay: %v", quote.PrepayAmount, m.params.MaximumPrepay) + + return ReasonPrepay } - return nil + return ReasonNone } // satPerKwToSatPerVByte converts sat per kWeight to sat per vByte. diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index 168eefc..557cf22 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -107,6 +107,10 @@ var ( } testRestrictions = NewRestrictions(1, 10000) + + // noneDisqualified can be used in tests where we don't have any + // disqualified channels so that we can use require.Equal. + noneDisqualified = make(map[lnwire.ShortChannelID]Reason) ) // newTestConfig creates a default test config. @@ -283,7 +287,7 @@ func TestRestrictedSuggestions(t *testing.T) { channels []lndclient.ChannelInfo loopOut []*loopdb.LoopOut loopIn []*loopdb.LoopIn - expected []loop.OutRequest + expected *Suggestions }{ { name: "no existing swaps", @@ -292,8 +296,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, loopOut: nil, loopIn: nil, - expected: []loop.OutRequest{ - chan1Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -308,8 +315,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: []loop.OutRequest{ - chan1Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -324,8 +334,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: []loop.OutRequest{ - chan1Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -338,8 +351,13 @@ func TestRestrictedSuggestions(t *testing.T) { Contract: chan1Out, }, }, - expected: []loop.OutRequest{ - chan2Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan2Rec, + }, + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonLoopOut, + }, }, }, { @@ -354,8 +372,13 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: []loop.OutRequest{ - chan1Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID2: ReasonLoopIn, + }, }, }, { @@ -373,7 +396,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: nil, + expected: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonFailureBackoff, + }, + }, }, { name: "swap failed before cutoff", @@ -390,8 +417,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: []loop.OutRequest{ - chan1Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -409,7 +439,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: nil, + expected: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonLoopOut, + }, + }, }, } @@ -447,21 +481,28 @@ func TestRestrictedSuggestions(t *testing.T) { // fee is above and below the configured limit. func TestSweepFeeLimit(t *testing.T) { tests := []struct { - name string - feeRate chainfee.SatPerKWeight - swaps []loop.OutRequest + name string + feeRate chainfee.SatPerKWeight + suggestions *Suggestions }{ { name: "fee estimate ok", feeRate: defaultSweepFeeRateLimit, - swaps: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { name: "fee estimate above limit", feeRate: defaultSweepFeeRateLimit + 1, - swaps: nil, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonSweepFees, + }, + }, }, } @@ -487,7 +528,7 @@ func TestSweepFeeLimit(t *testing.T) { testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - testCase.swaps, nil, + testCase.suggestions, nil, ) }) } @@ -497,10 +538,10 @@ func TestSweepFeeLimit(t *testing.T) { // the liquidity manager and the current set of channel balances. func TestSuggestSwaps(t *testing.T) { tests := []struct { - name string - rules map[lnwire.ShortChannelID]*ThresholdRule - swaps []loop.OutRequest - err error + name string + rules map[lnwire.ShortChannelID]*ThresholdRule + suggestions *Suggestions + err error }{ { name: "no rules", @@ -512,8 +553,11 @@ func TestSuggestSwaps(t *testing.T) { rules: map[lnwire.ShortChannelID]*ThresholdRule{ chanID1: chanRule, }, - swaps: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -521,7 +565,9 @@ func TestSuggestSwaps(t *testing.T) { rules: map[lnwire.ShortChannelID]*ThresholdRule{ chanID2: NewThresholdRule(10, 10), }, - swaps: nil, + suggestions: &Suggestions{ + DisqualifiedChans: noneDisqualified, + }, }, } @@ -540,7 +586,7 @@ func TestSuggestSwaps(t *testing.T) { testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - testCase.swaps, testCase.err, + testCase.suggestions, testCase.err, ) }) } @@ -549,15 +595,18 @@ func TestSuggestSwaps(t *testing.T) { // TestFeeLimits tests limiting of swap suggestions by fees. func TestFeeLimits(t *testing.T) { tests := []struct { - name string - quote *loop.LoopOutQuote - expected []loop.OutRequest + name string + quote *loop.LoopOutQuote + suggestions *Suggestions }{ { name: "fees ok", quote: testQuote, - expected: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -567,6 +616,11 @@ func TestFeeLimits(t *testing.T) { PrepayAmount: defaultMaximumPrepay + 1, MinerFee: 50, }, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonPrepay, + }, + }, }, { name: "insufficient miner fee", @@ -575,6 +629,11 @@ func TestFeeLimits(t *testing.T) { PrepayAmount: 100, MinerFee: defaultMaximumMinerFee + 1, }, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonMinerFee, + }, + }, }, { // Swap fee limited to 0.5% of 7500 = 37,5. @@ -584,6 +643,11 @@ func TestFeeLimits(t *testing.T) { PrepayAmount: 100, MinerFee: 500, }, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonSwapFee, + }, + }, }, } @@ -610,7 +674,7 @@ func TestFeeLimits(t *testing.T) { testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - testCase.expected, nil, + testCase.suggestions, nil, ) }) } @@ -641,8 +705,8 @@ func TestFeeBudget(t *testing.T) { // last update time to their total cost. existingSwaps map[time.Time]btcutil.Amount - // expectedSwaps is the set of swaps we expect to be suggested. - expectedSwaps []loop.OutRequest + // suggestions is the set of swaps we expect to be suggested. + suggestions *Suggestions }{ { // Two swaps will cost (78+5000)*2, set exactly 10156 @@ -650,8 +714,11 @@ func TestFeeBudget(t *testing.T) { name: "budget for 2 swaps, no existing", budget: 10156, maxMinerFee: 5000, - expectedSwaps: []loop.OutRequest{ - chan1Rec, chan2Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, chan2Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -660,8 +727,13 @@ func TestFeeBudget(t *testing.T) { name: "budget for 1 swaps, no existing", budget: 10155, maxMinerFee: 5000, - expectedSwaps: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID2: ReasonBudgetInsufficient, + }, }, }, { @@ -673,8 +745,11 @@ func TestFeeBudget(t *testing.T) { existingSwaps: map[time.Time]btcutil.Amount{ testBudgetStart.Add(time.Hour * -1): 200, }, - expectedSwaps: []loop.OutRequest{ - chan1Rec, chan2Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, chan2Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -686,8 +761,13 @@ func TestFeeBudget(t *testing.T) { existingSwaps: map[time.Time]btcutil.Amount{ testBudgetStart.Add(time.Hour): 500, }, - expectedSwaps: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID2: ReasonBudgetInsufficient, + }, }, }, { @@ -697,7 +777,12 @@ func TestFeeBudget(t *testing.T) { existingSwaps: map[time.Time]btcutil.Amount{ testBudgetStart.Add(time.Hour): 500, }, - expectedSwaps: nil, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonBudgetElapsed, + chanID2: ReasonBudgetElapsed, + }, + }, }, } @@ -760,14 +845,14 @@ func TestFeeBudget(t *testing.T) { // Set our custom max miner fee on each expected swap, // rather than having to create multiple vars for // different rates. - for i := range testCase.expectedSwaps { - testCase.expectedSwaps[i].MaxMinerFee = + for i := range testCase.suggestions.OutSwaps { + testCase.suggestions.OutSwaps[i].MaxMinerFee = testCase.maxMinerFee } testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - testCase.expectedSwaps, nil, + testCase.suggestions, nil, ) }) } @@ -780,20 +865,26 @@ func TestInFlightLimit(t *testing.T) { name string maxInFlight int existingSwaps []*loopdb.LoopOut - expectedSwaps []loop.OutRequest + suggestions *Suggestions }{ { name: "none in flight, extra space", maxInFlight: 3, - expectedSwaps: []loop.OutRequest{ - chan1Rec, chan2Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, chan2Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { name: "none in flight, exact match", maxInFlight: 2, - expectedSwaps: []loop.OutRequest{ - chan1Rec, chan2Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, chan2Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -804,8 +895,13 @@ func TestInFlightLimit(t *testing.T) { Contract: autoOutContract, }, }, - expectedSwaps: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID2: ReasonInFlight, + }, }, }, { @@ -816,7 +912,12 @@ func TestInFlightLimit(t *testing.T) { Contract: autoOutContract, }, }, - expectedSwaps: nil, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonInFlight, + chanID2: ReasonInFlight, + }, + }, }, { name: "max swaps exceeded", @@ -829,7 +930,12 @@ func TestInFlightLimit(t *testing.T) { Contract: autoOutContract, }, }, - expectedSwaps: nil, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonInFlight, + chanID2: ReasonInFlight, + }, + }, }, } @@ -860,7 +966,7 @@ func TestInFlightLimit(t *testing.T) { testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - testCase.expectedSwaps, nil, + testCase.suggestions, nil, ) }) } @@ -875,13 +981,18 @@ func TestSizeRestrictions(t *testing.T) { } outSwap = loop.OutRequest{ + Amount: 7000, OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()}, MaxPrepayRoutingFee: prepayFee, - MaxMinerFee: defaultMaximumMinerFee, - MaxSwapFee: testQuote.SwapFee, - MaxPrepayAmount: testQuote.PrepayAmount, - SweepConfTarget: loop.DefaultSweepConfTarget, - Initiator: autoloopSwapInitiator, + MaxSwapRoutingFee: ppmToSat( + 7000, + defaultRoutingFeePPM, + ), + MaxMinerFee: defaultMaximumMinerFee, + MaxSwapFee: testQuote.SwapFee, + MaxPrepayAmount: testQuote.PrepayAmount, + SweepConfTarget: loop.DefaultSweepConfTarget, + Initiator: autoloopSwapInitiator, } ) @@ -896,8 +1007,8 @@ func TestSizeRestrictions(t *testing.T) { // endpoint. serverRestrictions []Restrictions - // expectedAmount is the amount that we expect for our swap. - expectedAmount btcutil.Amount + // suggestions is the set of suggestions we expect. + suggestions *Suggestions // expectedError is the error we expect. expectedError error @@ -910,7 +1021,12 @@ func TestSizeRestrictions(t *testing.T) { serverRestrictions: []Restrictions{ serverRestrictions, serverRestrictions, }, - expectedAmount: 7500, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, + }, }, { name: "minimum more than server, no swap", @@ -920,7 +1036,11 @@ func TestSizeRestrictions(t *testing.T) { serverRestrictions: []Restrictions{ serverRestrictions, serverRestrictions, }, - expectedAmount: 0, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonLiquidityOk, + }, + }, }, { name: "maximum less than server, swap happens", @@ -930,7 +1050,12 @@ func TestSizeRestrictions(t *testing.T) { serverRestrictions: []Restrictions{ serverRestrictions, serverRestrictions, }, - expectedAmount: 7000, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + outSwap, + }, + DisqualifiedChans: noneDisqualified, + }, }, { // Originally, our client params are ok. But then the @@ -948,8 +1073,8 @@ func TestSizeRestrictions(t *testing.T) { Maximum: 6000, }, }, - expectedAmount: 0, - expectedError: ErrMaxExceedsServer, + suggestions: nil, + expectedError: ErrMaxExceedsServer, }, } @@ -981,25 +1106,9 @@ func TestSizeRestrictions(t *testing.T) { return &restrictions, nil } - - // If we expect a swap (non-zero amount), we add a - // swap to our set of expected swaps, and update amount - // and fee accordingly. - var expectedSwaps []loop.OutRequest - if testCase.expectedAmount != 0 { - outSwap.Amount = testCase.expectedAmount - - outSwap.MaxSwapRoutingFee = ppmToSat( - testCase.expectedAmount, - defaultRoutingFeePPM, - ) - - expectedSwaps = append(expectedSwaps, outSwap) - } - testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - expectedSwaps, testCase.expectedError, + testCase.suggestions, testCase.expectedError, ) require.Equal( @@ -1034,7 +1143,7 @@ func newSuggestSwapsSetup(cfg *Config, lnd *test.LndMockServices, // use the default parameters and setup two channels (channel1 + channel2) with // chanRule set for each. func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup, - expected []loop.OutRequest, expectedErr error) { + expected *Suggestions, expectedErr error) { t.Parallel() diff --git a/liquidity/reasons.go b/liquidity/reasons.go new file mode 100644 index 0000000..d685f61 --- /dev/null +++ b/liquidity/reasons.go @@ -0,0 +1,62 @@ +package liquidity + +// Reason is an enum which represents the various reasons we have for not +// executing a swap. +type Reason uint8 + +const ( + // ReasonNone is the zero value reason, added so that this enum can + // align with the numeric values used in our protobufs and avoid + // ambiguity around default zero values. + ReasonNone Reason = iota + + // ReasonBudgetNotStarted indicates that we do not recommend any swaps + // because the start time for our budget has not arrived yet. + ReasonBudgetNotStarted + + // ReasonSweepFees indicates that the estimated fees to sweep swaps + // are too high right now. + ReasonSweepFees + + // ReasonBudgetElapsed indicates that the autoloop budget for the + // period has been elapsed. + ReasonBudgetElapsed + + // ReasonInFlight indicates that the limit on in-flight automatically + // dispatched swaps has already been reached. + ReasonInFlight + + // ReasonSwapFee indicates that the server fee for a specific swap is + // too high. + ReasonSwapFee + + // ReasonMinerFee indicates that the miner fee for a specific swap is + // to high. + ReasonMinerFee + + // ReasonPrepay indicates that the prepay fee for a specific swap is + // too high. + ReasonPrepay + + // ReasonFailureBackoff indicates that a swap has recently failed for + // this target, and the backoff period has not yet passed. + ReasonFailureBackoff + + // ReasonLoopOut indicates that a loop out swap is currently utilizing + // the channel, so it is not eligible. + ReasonLoopOut + + // ReasonLoopIn indicates that a loop in swap is currently in flight + // for the peer, so it is not eligible. + ReasonLoopIn + + // ReasonLiquidityOk indicates that a target meets the liquidity + // balance expressed in its rule, so no swap is needed. + ReasonLiquidityOk + + // ReasonBudgetInsufficient indicates that we cannot perform a swap + // because we do not have enough pending budget available. This differs + // from budget elapsed, because we still have some budget available, + // but we have allocated it to other swaps. + ReasonBudgetInsufficient +) diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index ff310fc..cbfe2da 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -704,7 +704,7 @@ func rpcToRule(rule *looprpc.LiquidityRule) (*liquidity.ThresholdRule, error) { func (s *swapClientServer) SuggestSwaps(ctx context.Context, _ *looprpc.SuggestSwapsRequest) (*looprpc.SuggestSwapsResponse, error) { - swaps, err := s.liquidityMgr.SuggestSwaps(ctx, false) + suggestions, err := s.liquidityMgr.SuggestSwaps(ctx, false) switch err { case liquidity.ErrNoRules: return nil, status.Error(codes.FailedPrecondition, err.Error()) @@ -717,7 +717,7 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context, var loopOut []*looprpc.LoopOutRequest - for _, swap := range swaps { + for _, swap := range suggestions.OutSwaps { loopOut = append(loopOut, &looprpc.LoopOutRequest{ Amt: int64(swap.Amount), OutgoingChanSet: swap.OutgoingChanSet, From 22bd4cabb4454c4f8a3cc977677a48df183c1865 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 8 Feb 2021 09:39:03 +0200 Subject: [PATCH 5/6] looprpc: add disqualified channels to suggest swaps response --- cmd/loop/liquidity.go | 2 +- loopd/swapclient_server.go | 67 ++++- looprpc/client.pb.go | 482 ++++++++++++++++++++++++------------ looprpc/client.proto | 93 +++++++ looprpc/client.swagger.json | 41 +++ 5 files changed, 526 insertions(+), 159 deletions(-) diff --git a/cmd/loop/liquidity.go b/cmd/loop/liquidity.go index dc6b9bb..f3b823a 100644 --- a/cmd/loop/liquidity.go +++ b/cmd/loop/liquidity.go @@ -415,7 +415,7 @@ func suggestSwap(ctx *cli.Context) error { context.Background(), &looprpc.SuggestSwapsRequest{}, ) if err == nil { - printJSON(resp) + printRespJSON(resp) return nil } diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index cbfe2da..a4b9764 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -715,7 +715,10 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context, return nil, err } - var loopOut []*looprpc.LoopOutRequest + var ( + loopOut []*looprpc.LoopOutRequest + disqualified []*looprpc.Disqualified + ) for _, swap := range suggestions.OutSwaps { loopOut = append(loopOut, &looprpc.LoopOutRequest{ @@ -730,11 +733,71 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context, }) } + for id, reason := range suggestions.DisqualifiedChans { + autoloopReason, err := rpcAutoloopReason(reason) + if err != nil { + return nil, err + } + + exclChan := &looprpc.Disqualified{ + Reason: autoloopReason, + ChannelId: id.ToUint64(), + } + disqualified = append(disqualified, exclChan) + } + return &looprpc.SuggestSwapsResponse{ - LoopOut: loopOut, + LoopOut: loopOut, + Disqualified: disqualified, }, nil } +func rpcAutoloopReason(reason liquidity.Reason) (looprpc.AutoReason, error) { + switch reason { + case liquidity.ReasonNone: + return looprpc.AutoReason_AUTO_REASON_UNKNOWN, nil + + case liquidity.ReasonBudgetNotStarted: + return looprpc.AutoReason_AUTO_REASON_BUDGET_NOT_STARTED, nil + + case liquidity.ReasonSweepFees: + return looprpc.AutoReason_AUTO_REASON_SWEEP_FEES, nil + + case liquidity.ReasonBudgetElapsed: + return looprpc.AutoReason_AUTO_REASON_BUDGET_ELAPSED, nil + + case liquidity.ReasonInFlight: + return looprpc.AutoReason_AUTO_REASON_IN_FLIGHT, nil + + case liquidity.ReasonSwapFee: + return looprpc.AutoReason_AUTO_REASON_SWAP_FEE, nil + + case liquidity.ReasonMinerFee: + return looprpc.AutoReason_AUTO_REASON_MINER_FEE, nil + + case liquidity.ReasonPrepay: + return looprpc.AutoReason_AUTO_REASON_PREPAY, nil + + case liquidity.ReasonFailureBackoff: + return looprpc.AutoReason_AUTO_REASON_FAILURE_BACKOFF, nil + + case liquidity.ReasonLoopOut: + return looprpc.AutoReason_AUTO_REASON_LOOP_OUT, nil + + case liquidity.ReasonLoopIn: + return looprpc.AutoReason_AUTO_REASON_LOOP_IN, nil + + case liquidity.ReasonLiquidityOk: + return looprpc.AutoReason_AUTO_REASON_LIQUIDITY_OK, nil + + case liquidity.ReasonBudgetInsufficient: + return looprpc.AutoReason_AUTO_REASON_BUDGET_INSUFFICIENT, nil + + default: + return 0, fmt.Errorf("unknown autoloop reason: %v", reason) + } +} + // processStatusUpdates reads updates on the status channel and processes them. // // NOTE: This must run inside a goroutine as it blocks until the main context diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index 1a034d8..7e72f33 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -200,6 +200,99 @@ func (LiquidityRuleType) EnumDescriptor() ([]byte, []int) { return fileDescriptor_014de31d7ac8c57c, []int{3} } +type AutoReason int32 + +const ( + AutoReason_AUTO_REASON_UNKNOWN AutoReason = 0 + // + //Budget not started indicates that we do not recommend any swaps because + //the start time for our budget has not arrived yet. + AutoReason_AUTO_REASON_BUDGET_NOT_STARTED AutoReason = 1 + // + //Sweep fees indicates that the estimated fees to sweep swaps are too high + //right now. + AutoReason_AUTO_REASON_SWEEP_FEES AutoReason = 2 + // + //Budget elapsed indicates that the autoloop budget for the period has been + //elapsed. + AutoReason_AUTO_REASON_BUDGET_ELAPSED AutoReason = 3 + // + //In flight indicates that the limit on in-flight automatically dispatched + //swaps has already been reached. + AutoReason_AUTO_REASON_IN_FLIGHT AutoReason = 4 + // + //Swap fee indicates that the server fee for a specific swap is too high. + AutoReason_AUTO_REASON_SWAP_FEE AutoReason = 5 + // + //Miner fee indicates that the miner fee for a specific swap is to high. + AutoReason_AUTO_REASON_MINER_FEE AutoReason = 6 + // + //Prepay indicates that the prepay fee for a specific swap is too high. + AutoReason_AUTO_REASON_PREPAY AutoReason = 7 + // + //Failure backoff indicates that a swap has recently failed for this target, + //and the backoff period has not yet passed. + AutoReason_AUTO_REASON_FAILURE_BACKOFF AutoReason = 8 + // + //Loop out indicates that a loop out swap is currently utilizing the channel, + //so it is not eligible. + AutoReason_AUTO_REASON_LOOP_OUT AutoReason = 9 + // + //Loop In indicates that a loop in swap is currently in flight for the peer, + //so it is not eligible. + AutoReason_AUTO_REASON_LOOP_IN AutoReason = 10 + // + //Liquidity ok indicates that a target meets the liquidity balance expressed + //in its rule, so no swap is needed. + AutoReason_AUTO_REASON_LIQUIDITY_OK AutoReason = 11 + // + //Budget insufficient indicates that we cannot perform a swap because we do + //not have enough pending budget available. This differs from budget elapsed, + //because we still have some budget available, but we have allocated it to + //other swaps. + AutoReason_AUTO_REASON_BUDGET_INSUFFICIENT AutoReason = 12 +) + +var AutoReason_name = map[int32]string{ + 0: "AUTO_REASON_UNKNOWN", + 1: "AUTO_REASON_BUDGET_NOT_STARTED", + 2: "AUTO_REASON_SWEEP_FEES", + 3: "AUTO_REASON_BUDGET_ELAPSED", + 4: "AUTO_REASON_IN_FLIGHT", + 5: "AUTO_REASON_SWAP_FEE", + 6: "AUTO_REASON_MINER_FEE", + 7: "AUTO_REASON_PREPAY", + 8: "AUTO_REASON_FAILURE_BACKOFF", + 9: "AUTO_REASON_LOOP_OUT", + 10: "AUTO_REASON_LOOP_IN", + 11: "AUTO_REASON_LIQUIDITY_OK", + 12: "AUTO_REASON_BUDGET_INSUFFICIENT", +} + +var AutoReason_value = map[string]int32{ + "AUTO_REASON_UNKNOWN": 0, + "AUTO_REASON_BUDGET_NOT_STARTED": 1, + "AUTO_REASON_SWEEP_FEES": 2, + "AUTO_REASON_BUDGET_ELAPSED": 3, + "AUTO_REASON_IN_FLIGHT": 4, + "AUTO_REASON_SWAP_FEE": 5, + "AUTO_REASON_MINER_FEE": 6, + "AUTO_REASON_PREPAY": 7, + "AUTO_REASON_FAILURE_BACKOFF": 8, + "AUTO_REASON_LOOP_OUT": 9, + "AUTO_REASON_LOOP_IN": 10, + "AUTO_REASON_LIQUIDITY_OK": 11, + "AUTO_REASON_BUDGET_INSUFFICIENT": 12, +} + +func (x AutoReason) String() string { + return proto.EnumName(AutoReason_name, int32(x)) +} + +func (AutoReason) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_014de31d7ac8c57c, []int{4} +} + type LoopOutRequest struct { // //Requested swap amount in sat. This does not include the swap and miner fee. @@ -1954,20 +2047,75 @@ func (m *SuggestSwapsRequest) XXX_DiscardUnknown() { var xxx_messageInfo_SuggestSwapsRequest proto.InternalMessageInfo +type Disqualified struct { + // + //The short channel ID of the channel that was excluded from our suggestions. + ChannelId uint64 `protobuf:"varint,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + // + //The reason that we excluded the channel from the our suggestions. + Reason AutoReason `protobuf:"varint,2,opt,name=reason,proto3,enum=looprpc.AutoReason" json:"reason,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Disqualified) Reset() { *m = Disqualified{} } +func (m *Disqualified) String() string { return proto.CompactTextString(m) } +func (*Disqualified) ProtoMessage() {} +func (*Disqualified) Descriptor() ([]byte, []int) { + return fileDescriptor_014de31d7ac8c57c, []int{23} +} + +func (m *Disqualified) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Disqualified.Unmarshal(m, b) +} +func (m *Disqualified) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Disqualified.Marshal(b, m, deterministic) +} +func (m *Disqualified) XXX_Merge(src proto.Message) { + xxx_messageInfo_Disqualified.Merge(m, src) +} +func (m *Disqualified) XXX_Size() int { + return xxx_messageInfo_Disqualified.Size(m) +} +func (m *Disqualified) XXX_DiscardUnknown() { + xxx_messageInfo_Disqualified.DiscardUnknown(m) +} + +var xxx_messageInfo_Disqualified proto.InternalMessageInfo + +func (m *Disqualified) GetChannelId() uint64 { + if m != nil { + return m.ChannelId + } + return 0 +} + +func (m *Disqualified) GetReason() AutoReason { + if m != nil { + return m.Reason + } + return AutoReason_AUTO_REASON_UNKNOWN +} + type SuggestSwapsResponse struct { // //The set of recommended loop outs. - LoopOut []*LoopOutRequest `protobuf:"bytes,1,rep,name=loop_out,json=loopOut,proto3" json:"loop_out,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + LoopOut []*LoopOutRequest `protobuf:"bytes,1,rep,name=loop_out,json=loopOut,proto3" json:"loop_out,omitempty"` + // + //Disqualified contains the set of channels that swaps are not recommended + //for. + Disqualified []*Disqualified `protobuf:"bytes,2,rep,name=disqualified,proto3" json:"disqualified,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *SuggestSwapsResponse) Reset() { *m = SuggestSwapsResponse{} } func (m *SuggestSwapsResponse) String() string { return proto.CompactTextString(m) } func (*SuggestSwapsResponse) ProtoMessage() {} func (*SuggestSwapsResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_014de31d7ac8c57c, []int{23} + return fileDescriptor_014de31d7ac8c57c, []int{24} } func (m *SuggestSwapsResponse) XXX_Unmarshal(b []byte) error { @@ -1995,11 +2143,19 @@ func (m *SuggestSwapsResponse) GetLoopOut() []*LoopOutRequest { return nil } +func (m *SuggestSwapsResponse) GetDisqualified() []*Disqualified { + if m != nil { + return m.Disqualified + } + return nil +} + func init() { proto.RegisterEnum("looprpc.SwapType", SwapType_name, SwapType_value) proto.RegisterEnum("looprpc.SwapState", SwapState_name, SwapState_value) proto.RegisterEnum("looprpc.FailureReason", FailureReason_name, FailureReason_value) proto.RegisterEnum("looprpc.LiquidityRuleType", LiquidityRuleType_name, LiquidityRuleType_value) + proto.RegisterEnum("looprpc.AutoReason", AutoReason_name, AutoReason_value) proto.RegisterType((*LoopOutRequest)(nil), "looprpc.LoopOutRequest") proto.RegisterType((*LoopInRequest)(nil), "looprpc.LoopInRequest") proto.RegisterType((*SwapResponse)(nil), "looprpc.SwapResponse") @@ -2023,163 +2179,177 @@ func init() { proto.RegisterType((*SetLiquidityParamsRequest)(nil), "looprpc.SetLiquidityParamsRequest") proto.RegisterType((*SetLiquidityParamsResponse)(nil), "looprpc.SetLiquidityParamsResponse") proto.RegisterType((*SuggestSwapsRequest)(nil), "looprpc.SuggestSwapsRequest") + proto.RegisterType((*Disqualified)(nil), "looprpc.Disqualified") proto.RegisterType((*SuggestSwapsResponse)(nil), "looprpc.SuggestSwapsResponse") } func init() { proto.RegisterFile("client.proto", fileDescriptor_014de31d7ac8c57c) } var fileDescriptor_014de31d7ac8c57c = []byte{ - // 2389 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x58, 0xcd, 0x6f, 0xe3, 0xc6, - 0x15, 0x5f, 0x7d, 0x59, 0xd2, 0x13, 0x45, 0xd1, 0xe3, 0x5d, 0x5b, 0x56, 0x1c, 0xc4, 0xcb, 0x64, - 0x1b, 0xc7, 0x49, 0xac, 0xc6, 0x39, 0x25, 0x48, 0x0a, 0x68, 0x65, 0x39, 0x96, 0x6b, 0x4b, 0x2a, - 0x25, 0x6f, 0x90, 0xa2, 0x00, 0x31, 0x96, 0xc6, 0x16, 0x11, 0xf1, 0x23, 0xe4, 0x68, 0xd7, 0x46, - 0xd0, 0x16, 0x28, 0xd0, 0x73, 0x0f, 0xfd, 0x0f, 0x7a, 0xef, 0xad, 0xb7, 0xf6, 0xde, 0x4b, 0x4f, - 0xed, 0xb1, 0xd7, 0x5e, 0x7a, 0xe8, 0xff, 0x50, 0xcc, 0x1b, 0x92, 0x22, 0x65, 0xc9, 0x41, 0x0f, - 0xbd, 0x89, 0xef, 0xfd, 0xe6, 0xcd, 0x9b, 0xf7, 0xfd, 0x04, 0xca, 0x78, 0x66, 0x31, 0x87, 0x1f, - 0x79, 0xbe, 0xcb, 0x5d, 0x52, 0x9c, 0xb9, 0xae, 0xe7, 0x7b, 0xe3, 0xc6, 0xde, 0xad, 0xeb, 0xde, - 0xce, 0x58, 0x93, 0x7a, 0x56, 0x93, 0x3a, 0x8e, 0xcb, 0x29, 0xb7, 0x5c, 0x27, 0x90, 0x30, 0xfd, - 0x8f, 0x79, 0x50, 0x2f, 0x5c, 0xd7, 0xeb, 0xcf, 0xb9, 0xc1, 0xbe, 0x9b, 0xb3, 0x80, 0x13, 0x0d, - 0x72, 0xd4, 0xe6, 0xf5, 0xcc, 0x7e, 0xe6, 0x20, 0x67, 0x88, 0x9f, 0x84, 0x40, 0x7e, 0xc2, 0x02, - 0x5e, 0xcf, 0xee, 0x67, 0x0e, 0xca, 0x06, 0xfe, 0x26, 0x4d, 0x78, 0x6a, 0xd3, 0x3b, 0x33, 0x78, - 0x43, 0x3d, 0xd3, 0x77, 0xe7, 0xdc, 0x72, 0x6e, 0xcd, 0x1b, 0xc6, 0xea, 0x39, 0x3c, 0xb6, 0x69, - 0xd3, 0xbb, 0xe1, 0x1b, 0xea, 0x19, 0x92, 0x73, 0xca, 0x18, 0xf9, 0x14, 0xb6, 0xc5, 0x01, 0xcf, - 0x67, 0x1e, 0xbd, 0x4f, 0x1d, 0xc9, 0xe3, 0x91, 0x2d, 0x9b, 0xde, 0x0d, 0x90, 0x99, 0x38, 0xb4, - 0x0f, 0x4a, 0x7c, 0x8b, 0x80, 0x16, 0x10, 0x0a, 0xa1, 0x74, 0x81, 0x78, 0x0f, 0xd4, 0x84, 0x58, - 0xa1, 0xf8, 0x06, 0x62, 0x94, 0x58, 0x5c, 0xcb, 0xe6, 0x44, 0x87, 0xaa, 0x40, 0xd9, 0x96, 0xc3, - 0x7c, 0x14, 0x54, 0x44, 0x50, 0xc5, 0xa6, 0x77, 0x97, 0x82, 0x26, 0x24, 0x7d, 0x04, 0x9a, 0xb0, - 0x99, 0xe9, 0xce, 0xb9, 0x39, 0x9e, 0x52, 0xc7, 0x61, 0xb3, 0x7a, 0x69, 0x3f, 0x73, 0x90, 0x7f, - 0x99, 0xad, 0x67, 0x0c, 0x75, 0x26, 0xad, 0xd4, 0x96, 0x1c, 0x72, 0x08, 0x9b, 0xee, 0x9c, 0xdf, - 0xba, 0xe2, 0x11, 0x02, 0x6d, 0x06, 0x8c, 0xd7, 0x2b, 0xfb, 0xb9, 0x83, 0xbc, 0x51, 0x8b, 0x18, - 0x02, 0x3b, 0x64, 0x5c, 0x60, 0x83, 0x37, 0x8c, 0x79, 0xe6, 0xd8, 0x75, 0x6e, 0x4c, 0x4e, 0xfd, - 0x5b, 0xc6, 0xeb, 0xe5, 0xfd, 0xcc, 0x41, 0xc1, 0xa8, 0x21, 0xa3, 0xed, 0x3a, 0x37, 0x23, 0x24, - 0x93, 0x8f, 0x81, 0x4c, 0xf9, 0x6c, 0x8c, 0x50, 0xcb, 0xb7, 0xa5, 0xb3, 0xea, 0x55, 0x04, 0x6f, - 0x0a, 0x4e, 0x3b, 0xc9, 0x20, 0x9f, 0xc3, 0x2e, 0x1a, 0xc7, 0x9b, 0x5f, 0xcf, 0xac, 0x31, 0x12, - 0xcd, 0x09, 0xa3, 0x93, 0x99, 0xe5, 0xb0, 0x3a, 0x08, 0xed, 0x8d, 0x1d, 0x01, 0x18, 0x2c, 0xf8, - 0x27, 0x21, 0x9b, 0x3c, 0x85, 0xc2, 0x8c, 0x5e, 0xb3, 0x59, 0x5d, 0x41, 0xbf, 0xca, 0x0f, 0xb2, - 0x07, 0x65, 0xcb, 0xb1, 0xb8, 0x45, 0xb9, 0xeb, 0xd7, 0x55, 0xe4, 0x2c, 0x08, 0xfa, 0x6f, 0xb3, - 0x50, 0x15, 0xf1, 0xd2, 0x75, 0xd6, 0x87, 0xcb, 0xb2, 0xd3, 0xb2, 0x0f, 0x9c, 0xf6, 0xc0, 0x1d, - 0xb9, 0x87, 0xee, 0xd8, 0x85, 0xd2, 0x8c, 0x06, 0xdc, 0x9c, 0xba, 0x1e, 0x46, 0x88, 0x62, 0x14, - 0xc5, 0xf7, 0x99, 0xeb, 0x91, 0x77, 0xa1, 0xca, 0xee, 0x38, 0xf3, 0x1d, 0x3a, 0x33, 0x85, 0x49, - 0x30, 0x2c, 0x4a, 0x86, 0x12, 0x11, 0xcf, 0xf8, 0x6c, 0x4c, 0x0e, 0x40, 0x8b, 0x0d, 0x19, 0xd9, - 0x7c, 0x03, 0xcd, 0xa8, 0x46, 0x66, 0x0c, 0x4d, 0x1e, 0xdb, 0xa1, 0xb8, 0xd6, 0x0e, 0xa5, 0x65, - 0x3b, 0xfc, 0x3b, 0x03, 0x0a, 0x06, 0x38, 0x0b, 0x3c, 0xd7, 0x09, 0x18, 0x21, 0x90, 0xb5, 0x26, - 0x68, 0x85, 0x32, 0xc6, 0x4b, 0xd6, 0x9a, 0x88, 0x27, 0x58, 0x13, 0xf3, 0xfa, 0x9e, 0xb3, 0x00, - 0x5f, 0xa8, 0x18, 0x45, 0x6b, 0xf2, 0x52, 0x7c, 0x92, 0x17, 0xa0, 0xa0, 0x76, 0x74, 0x32, 0xf1, - 0x59, 0x10, 0xc8, 0xd4, 0xc2, 0x83, 0x15, 0x41, 0x6f, 0x49, 0x32, 0x39, 0x82, 0xad, 0x24, 0xcc, - 0x74, 0xbc, 0xe3, 0x37, 0xc1, 0x14, 0xed, 0x51, 0x96, 0xe1, 0x10, 0x22, 0x7b, 0xc8, 0x20, 0x1f, - 0x85, 0xd1, 0x13, 0xe1, 0x25, 0xbc, 0x80, 0x70, 0x2d, 0x01, 0x1f, 0x20, 0xfa, 0x05, 0xa8, 0x01, - 0xf3, 0x5f, 0x33, 0xdf, 0xb4, 0x59, 0x10, 0xd0, 0x5b, 0x86, 0x06, 0x2a, 0x1b, 0x55, 0x49, 0xbd, - 0x94, 0x44, 0x5d, 0x03, 0xf5, 0xd2, 0x75, 0x2c, 0xee, 0xfa, 0xa1, 0xcf, 0xf5, 0x3f, 0xe5, 0x01, - 0xc4, 0xeb, 0x87, 0x9c, 0xf2, 0x79, 0xb0, 0xb2, 0x62, 0x08, 0x6b, 0x64, 0xd7, 0x5a, 0xa3, 0xb2, - 0x6c, 0x8d, 0x3c, 0xbf, 0xf7, 0x64, 0x18, 0xa8, 0xc7, 0x9b, 0x47, 0x61, 0xed, 0x3a, 0x12, 0x77, - 0x8c, 0xee, 0x3d, 0x66, 0x20, 0x9b, 0x1c, 0x40, 0x21, 0xe0, 0x94, 0xcb, 0x8a, 0xa1, 0x1e, 0x93, - 0x14, 0x4e, 0xe8, 0xc2, 0x0c, 0x09, 0x20, 0x5f, 0x82, 0x7a, 0x43, 0xad, 0xd9, 0xdc, 0x67, 0xa6, - 0xcf, 0x68, 0xe0, 0x3a, 0x18, 0xc9, 0xea, 0xf1, 0x76, 0x7c, 0xe4, 0x54, 0xb2, 0x0d, 0xe4, 0x1a, - 0xd5, 0x9b, 0xe4, 0x27, 0x79, 0x1f, 0x6a, 0xa1, 0xab, 0x45, 0x3e, 0x71, 0xcb, 0x8e, 0x2a, 0x8f, - 0xba, 0x20, 0x8f, 0x2c, 0x5b, 0x68, 0xa4, 0x61, 0x90, 0xce, 0xbd, 0x09, 0xe5, 0x4c, 0x22, 0x65, - 0xfd, 0x51, 0x05, 0xfd, 0x0a, 0xc9, 0x88, 0x5c, 0x76, 0x78, 0x71, 0xb5, 0xc3, 0x57, 0x3b, 0x50, - 0x59, 0xe3, 0xc0, 0x35, 0xe1, 0x51, 0x5d, 0x17, 0x1e, 0xef, 0x40, 0x65, 0xec, 0x06, 0xdc, 0x94, - 0xfe, 0xc5, 0xa8, 0xce, 0x19, 0x20, 0x48, 0x43, 0xa4, 0x90, 0xe7, 0xa0, 0x20, 0xc0, 0x75, 0xc6, - 0x53, 0x6a, 0x39, 0x58, 0xa4, 0x72, 0x06, 0x1e, 0xea, 0x4b, 0x92, 0x48, 0x3e, 0x09, 0xb9, 0xb9, - 0x91, 0x18, 0x90, 0xf5, 0x16, 0x31, 0x21, 0x6d, 0x91, 0x52, 0xb5, 0x44, 0x4a, 0xe9, 0x04, 0xb4, - 0x0b, 0x2b, 0xe0, 0xc2, 0x5b, 0x41, 0x14, 0x4a, 0x3f, 0x81, 0xcd, 0x04, 0x2d, 0x4c, 0xa6, 0x0f, - 0xa0, 0x20, 0xaa, 0x47, 0x50, 0xcf, 0xec, 0xe7, 0x0e, 0x2a, 0xc7, 0x5b, 0x0f, 0x1c, 0x3d, 0x0f, - 0x0c, 0x89, 0xd0, 0x9f, 0x43, 0x4d, 0x10, 0xbb, 0xce, 0x8d, 0x1b, 0x55, 0x24, 0x35, 0x4e, 0x45, - 0x45, 0x04, 0x9e, 0xae, 0x82, 0x32, 0x62, 0xbe, 0x1d, 0x5f, 0xf9, 0x6b, 0xa8, 0x75, 0x9d, 0x90, - 0x12, 0x5e, 0xf8, 0x23, 0xa8, 0xd9, 0x96, 0x23, 0x4b, 0x16, 0xb5, 0xdd, 0xb9, 0xc3, 0x43, 0x87, - 0x57, 0x6d, 0xcb, 0x11, 0xf2, 0x5b, 0x48, 0x44, 0x5c, 0x54, 0xda, 0x42, 0xdc, 0x46, 0x88, 0x93, - 0xd5, 0x4d, 0xe2, 0xce, 0xf3, 0xa5, 0x8c, 0x96, 0x3d, 0xcf, 0x97, 0xb2, 0x5a, 0xee, 0x3c, 0x5f, - 0xca, 0x69, 0xf9, 0xf3, 0x7c, 0x29, 0xaf, 0x15, 0xce, 0xf3, 0xa5, 0xa2, 0x56, 0xd2, 0xff, 0x96, - 0x01, 0xad, 0x3f, 0xe7, 0xff, 0x57, 0x15, 0xb0, 0x31, 0x5a, 0x8e, 0x39, 0x9e, 0xf1, 0xd7, 0xe6, - 0x84, 0xcd, 0x38, 0x45, 0x77, 0x17, 0x0c, 0xc5, 0xb6, 0x9c, 0xf6, 0x8c, 0xbf, 0x3e, 0x11, 0xb4, - 0xa8, 0x7d, 0x26, 0x50, 0xe5, 0x10, 0x45, 0xef, 0x62, 0xd4, 0x0f, 0x3c, 0xe7, 0x0f, 0x19, 0x50, - 0x7e, 0x36, 0x77, 0x39, 0x5b, 0xdf, 0x12, 0x30, 0xf0, 0x16, 0x75, 0x38, 0x8b, 0x77, 0xc0, 0x78, - 0x51, 0x83, 0x1f, 0x94, 0xf4, 0xdc, 0x8a, 0x92, 0xfe, 0x68, 0xb3, 0xcb, 0x3f, 0xda, 0xec, 0xf4, - 0xdf, 0x65, 0x84, 0xd7, 0x43, 0x35, 0x43, 0x93, 0xef, 0x83, 0x12, 0x35, 0x29, 0x33, 0xa0, 0x91, - 0xc2, 0x10, 0xc8, 0x2e, 0x35, 0xa4, 0x38, 0xe5, 0x60, 0x82, 0xe1, 0x8d, 0xc1, 0x34, 0x46, 0x86, - 0x53, 0x8e, 0xe0, 0x0d, 0x24, 0x2b, 0x3c, 0xf0, 0x36, 0x40, 0xc2, 0x96, 0x05, 0x7c, 0x67, 0x79, - 0x9c, 0x30, 0xa4, 0x34, 0x61, 0x5e, 0x2b, 0xe8, 0x7f, 0x97, 0x51, 0xf0, 0xbf, 0xaa, 0xf4, 0x1e, - 0xa8, 0x8b, 0x61, 0x07, 0x31, 0xb2, 0xbf, 0x2a, 0x5e, 0x34, 0xed, 0x08, 0xd4, 0x87, 0x61, 0x1d, - 0x91, 0x73, 0x47, 0x5a, 0xed, 0x9a, 0xe0, 0x0c, 0x05, 0x23, 0x14, 0x89, 0xf3, 0x89, 0xb0, 0x2b, - 0xbd, 0xb7, 0x99, 0xc3, 0x4d, 0x1c, 0xf6, 0x64, 0xcf, 0xad, 0xa1, 0x3d, 0x25, 0xfd, 0x44, 0xf8, - 0xf6, 0xf1, 0x07, 0xea, 0x35, 0xa8, 0x8e, 0xdc, 0x6f, 0x99, 0x13, 0x27, 0xdb, 0x17, 0xa0, 0x46, - 0x84, 0xf0, 0x89, 0x87, 0xb0, 0xc1, 0x91, 0x12, 0x66, 0xf7, 0xa2, 0x8c, 0x5f, 0x04, 0x94, 0x23, - 0xd8, 0x08, 0x11, 0xfa, 0x9f, 0xb3, 0x50, 0x8e, 0xa9, 0x22, 0x48, 0xae, 0x69, 0xc0, 0x4c, 0x9b, - 0x8e, 0xa9, 0xef, 0xba, 0x4e, 0x98, 0xe3, 0x8a, 0x20, 0x5e, 0x86, 0x34, 0x51, 0xc2, 0xa2, 0x77, - 0x4c, 0x69, 0x30, 0x45, 0xeb, 0x28, 0x46, 0x25, 0xa4, 0x9d, 0xd1, 0x60, 0x4a, 0x3e, 0x00, 0x2d, - 0x82, 0x78, 0x3e, 0xb3, 0x6c, 0xd1, 0xf9, 0x64, 0x7f, 0xae, 0x85, 0xf4, 0x41, 0x48, 0x16, 0x05, - 0x5e, 0x26, 0x99, 0xe9, 0x51, 0x6b, 0x62, 0xda, 0xc2, 0x8a, 0x72, 0x5e, 0x55, 0x25, 0x7d, 0x40, - 0xad, 0xc9, 0x65, 0x40, 0x39, 0xf9, 0x04, 0x9e, 0x25, 0x86, 0xda, 0x04, 0x5c, 0x66, 0x31, 0xf1, - 0xe3, 0xa9, 0x36, 0x3e, 0xf2, 0x1c, 0x14, 0xd1, 0x31, 0xcc, 0xb1, 0xcf, 0x28, 0x67, 0x93, 0x30, - 0x8f, 0x2b, 0x82, 0xd6, 0x96, 0x24, 0x52, 0x87, 0x22, 0xbb, 0xf3, 0x2c, 0x9f, 0x4d, 0xb0, 0x63, - 0x94, 0x8c, 0xe8, 0x53, 0x1c, 0x0e, 0xb8, 0xeb, 0xd3, 0x5b, 0x66, 0x3a, 0xd4, 0x66, 0xe1, 0x88, - 0x52, 0x09, 0x69, 0x3d, 0x6a, 0x33, 0xfd, 0x2d, 0xd8, 0xfd, 0x8a, 0xf1, 0x0b, 0xeb, 0xbb, 0xb9, - 0x35, 0xb1, 0xf8, 0xfd, 0x80, 0xfa, 0x74, 0x51, 0x05, 0xff, 0x5a, 0x80, 0xad, 0x34, 0x8b, 0x71, - 0xe6, 0x8b, 0x0e, 0x54, 0xf0, 0xe7, 0x33, 0x16, 0x79, 0x67, 0xd1, 0x31, 0x63, 0xb0, 0x31, 0x9f, - 0x31, 0x43, 0x82, 0xc8, 0x97, 0xb0, 0xb7, 0x08, 0x31, 0x5f, 0xf4, 0xc0, 0x80, 0x72, 0xd3, 0x63, - 0xbe, 0xf9, 0x5a, 0x74, 0x7a, 0xb4, 0x3e, 0x66, 0xa5, 0x8c, 0x36, 0x83, 0x72, 0x11, 0x71, 0x03, - 0xe6, 0xbf, 0x12, 0x6c, 0xf2, 0x3e, 0x68, 0xc9, 0x51, 0xd1, 0xf4, 0x3c, 0x1b, 0x3d, 0x91, 0x8f, - 0xab, 0x99, 0xb0, 0x97, 0x67, 0x93, 0x8f, 0x41, 0xec, 0x07, 0x66, 0xca, 0xc2, 0x9e, 0x1d, 0x26, - 0xbd, 0x90, 0xb1, 0x58, 0x1a, 0x04, 0xfc, 0x73, 0x68, 0xac, 0x5e, 0x36, 0xf0, 0x54, 0x01, 0x4f, - 0x6d, 0xaf, 0x58, 0x38, 0xc4, 0xd9, 0xf4, 0x46, 0x21, 0x3c, 0xb8, 0x81, 0xf8, 0xc5, 0x46, 0x21, - 0x72, 0xe6, 0x03, 0xd8, 0x4c, 0x8d, 0xb0, 0x08, 0x2c, 0x22, 0x50, 0x4d, 0x8c, 0xb1, 0x71, 0x7a, - 0x2d, 0x8f, 0xff, 0xa5, 0xd5, 0xe3, 0xff, 0x11, 0x6c, 0x45, 0x83, 0xcb, 0x35, 0x1d, 0x7f, 0xeb, - 0xde, 0xdc, 0x98, 0x01, 0x1b, 0x63, 0x51, 0xce, 0x1b, 0x9b, 0x21, 0xeb, 0xa5, 0xe4, 0x0c, 0xd9, - 0x98, 0x34, 0xa0, 0x44, 0xe7, 0xdc, 0x15, 0x3e, 0xc2, 0x46, 0x5c, 0x32, 0xe2, 0x6f, 0x21, 0x2b, - 0xfa, 0x6d, 0x5e, 0xcf, 0x27, 0xb7, 0x4c, 0x96, 0x8b, 0x8a, 0x94, 0x15, 0xb1, 0x5e, 0x22, 0x47, - 0xe8, 0xf9, 0x19, 0xec, 0x3e, 0xc0, 0x73, 0xea, 0x73, 0xd4, 0x40, 0x91, 0x36, 0x5b, 0x3a, 0x25, - 0xd8, 0x42, 0x8d, 0x0f, 0x81, 0x08, 0x8e, 0x29, 0x4c, 0x62, 0x39, 0xe6, 0xcd, 0xcc, 0xba, 0x9d, - 0x72, 0x9c, 0x43, 0xf2, 0x46, 0x4d, 0x70, 0x2e, 0xe9, 0x5d, 0xd7, 0x39, 0x45, 0xf2, 0xaa, 0x4e, - 0xa7, 0x86, 0x3e, 0xff, 0xa1, 0x4e, 0x57, 0x4b, 0xc5, 0x86, 0xc4, 0xe9, 0x7f, 0xc9, 0x40, 0x35, - 0x15, 0x9c, 0x58, 0xa4, 0xe4, 0x9e, 0x66, 0x86, 0x93, 0x40, 0xde, 0x28, 0x87, 0x94, 0xee, 0x84, - 0x1c, 0x85, 0xe3, 0x66, 0x16, 0x67, 0xc2, 0xc6, 0xea, 0x08, 0x4f, 0xcc, 0x9d, 0x1f, 0x03, 0xb1, - 0x9c, 0xb1, 0x6b, 0x8b, 0x18, 0xe2, 0x53, 0x9f, 0x05, 0x53, 0x77, 0x36, 0xc1, 0x38, 0xad, 0x1a, - 0x9b, 0x11, 0x67, 0x14, 0x31, 0x04, 0x3c, 0x5e, 0x0d, 0x17, 0xf0, 0xbc, 0x84, 0x47, 0x9c, 0x18, - 0xae, 0x7f, 0x03, 0xbb, 0xc3, 0x75, 0x59, 0x4a, 0xbe, 0x00, 0xf0, 0xe2, 0xdc, 0xc4, 0x97, 0x54, - 0x8e, 0xf7, 0x1e, 0x2a, 0xbc, 0xc8, 0x5f, 0x23, 0x81, 0xd7, 0xf7, 0xa0, 0xb1, 0x4a, 0xb4, 0x2c, - 0xc4, 0xfa, 0x33, 0xd8, 0x1a, 0xce, 0x6f, 0x6f, 0xd9, 0xd2, 0x44, 0x76, 0x0e, 0x4f, 0xd3, 0xe4, - 0xb0, 0x6e, 0x1f, 0x43, 0x29, 0xda, 0x8f, 0xc3, 0xda, 0xb0, 0xb3, 0x50, 0x24, 0xf5, 0x17, 0x82, - 0x51, 0x0c, 0x97, 0xe5, 0xc3, 0x17, 0x50, 0x8a, 0x66, 0x78, 0xa2, 0x40, 0xe9, 0xa2, 0xdf, 0x1f, - 0x98, 0xfd, 0xab, 0x91, 0xf6, 0x84, 0x54, 0xa0, 0x88, 0x5f, 0xdd, 0x9e, 0x96, 0x39, 0x0c, 0xa0, - 0x1c, 0x8f, 0xf0, 0xa4, 0x0a, 0xe5, 0x6e, 0xaf, 0x3b, 0xea, 0xb6, 0x46, 0x9d, 0x13, 0xed, 0x09, - 0x79, 0x06, 0x9b, 0x03, 0xa3, 0xd3, 0xbd, 0x6c, 0x7d, 0xd5, 0x31, 0x8d, 0xce, 0xab, 0x4e, 0xeb, - 0xa2, 0x73, 0xa2, 0x65, 0x08, 0x01, 0xf5, 0x6c, 0x74, 0xd1, 0x36, 0x07, 0x57, 0x2f, 0x2f, 0xba, - 0xc3, 0xb3, 0xce, 0x89, 0x96, 0x15, 0x32, 0x87, 0x57, 0xed, 0x76, 0x67, 0x38, 0xd4, 0x72, 0x04, - 0x60, 0xe3, 0xb4, 0xd5, 0x15, 0xe0, 0x3c, 0xd9, 0x82, 0x5a, 0xb7, 0xf7, 0xaa, 0xdf, 0x6d, 0x77, - 0xcc, 0x61, 0x67, 0x34, 0x12, 0xc4, 0xc2, 0xe1, 0x7f, 0x32, 0x50, 0x4d, 0x6d, 0x01, 0x64, 0x07, - 0xb6, 0xc4, 0x91, 0x2b, 0x43, 0xdc, 0xd4, 0x1a, 0xf6, 0x7b, 0x66, 0xaf, 0xdf, 0xeb, 0x68, 0x4f, - 0xc8, 0x5b, 0xb0, 0xb3, 0xc4, 0xe8, 0x9f, 0x9e, 0xb6, 0xcf, 0x5a, 0x42, 0x79, 0xd2, 0x80, 0xed, - 0x25, 0xe6, 0xa8, 0x7b, 0xd9, 0x11, 0xaf, 0xcc, 0x92, 0x7d, 0xd8, 0x5b, 0xe2, 0x0d, 0xbf, 0xee, - 0x74, 0x06, 0x31, 0x22, 0x47, 0x5e, 0xc0, 0xf3, 0x25, 0x44, 0xb7, 0x37, 0xbc, 0x3a, 0x3d, 0xed, - 0xb6, 0xbb, 0x9d, 0xde, 0xc8, 0x7c, 0xd5, 0xba, 0xb8, 0xea, 0x68, 0x79, 0xb2, 0x07, 0xf5, 0xe5, - 0x4b, 0x3a, 0x97, 0x83, 0xbe, 0xd1, 0x32, 0xbe, 0xd1, 0x0a, 0xe4, 0x5d, 0x78, 0xe7, 0x81, 0x90, - 0x76, 0xdf, 0x30, 0x3a, 0xed, 0x91, 0xd9, 0xba, 0xec, 0x5f, 0xf5, 0x46, 0xda, 0xc6, 0x61, 0x53, - 0x4c, 0xda, 0x4b, 0x01, 0x2e, 0x4c, 0x76, 0xd5, 0xfb, 0x69, 0xaf, 0xff, 0x75, 0x4f, 0x7b, 0x22, - 0x2c, 0x3f, 0x3a, 0x33, 0x3a, 0xc3, 0xb3, 0xfe, 0xc5, 0x89, 0x96, 0x39, 0xfe, 0x67, 0x59, 0x6e, - 0x79, 0x6d, 0xfc, 0x5f, 0x89, 0x18, 0x50, 0x0c, 0xdd, 0x4c, 0xd6, 0x39, 0xbe, 0xf1, 0x2c, 0x35, - 0xa9, 0xc7, 0x91, 0xb6, 0xf3, 0x9b, 0x7f, 0xfc, 0xeb, 0xf7, 0xd9, 0x4d, 0x5d, 0x69, 0xbe, 0xfe, - 0xa4, 0x29, 0x10, 0x4d, 0x77, 0xce, 0x3f, 0xcf, 0x1c, 0x92, 0x3e, 0x6c, 0xc8, 0x7f, 0x13, 0xc8, - 0x76, 0x4a, 0x64, 0xfc, 0xf7, 0xc2, 0x3a, 0x89, 0xdb, 0x28, 0x51, 0xd3, 0x2b, 0xb1, 0x44, 0xcb, - 0x11, 0x02, 0x3f, 0x83, 0x62, 0xb8, 0xab, 0x26, 0x94, 0x4c, 0x6f, 0xaf, 0x8d, 0x55, 0xeb, 0xc4, - 0x8f, 0x33, 0xe4, 0xe7, 0x50, 0x8e, 0x37, 0x11, 0xb2, 0x9b, 0xc8, 0xb1, 0x74, 0x7e, 0x34, 0x1a, - 0xab, 0x58, 0x69, 0xb5, 0x88, 0x1a, 0xab, 0x85, 0x5b, 0x0a, 0xb9, 0x92, 0x79, 0x20, 0xb6, 0x14, - 0x52, 0x4f, 0x5d, 0x9f, 0x58, 0x5c, 0x56, 0x2a, 0xa6, 0x37, 0x50, 0xe4, 0x53, 0x42, 0x52, 0x22, - 0x9b, 0xdf, 0x5b, 0x93, 0x5f, 0x92, 0x5f, 0x80, 0x12, 0x3a, 0x00, 0x77, 0x09, 0xb2, 0x30, 0x56, - 0x72, 0xe1, 0x69, 0x2c, 0x1e, 0xb3, 0xbc, 0x75, 0xac, 0x90, 0xee, 0xce, 0x79, 0x93, 0xa3, 0xb4, - 0xeb, 0x58, 0x3a, 0xce, 0xa8, 0x09, 0xe9, 0xc9, 0x69, 0x3f, 0x2d, 0x3d, 0x35, 0xcd, 0xea, 0xfb, - 0x28, 0xbd, 0x41, 0xea, 0x29, 0xe9, 0xdf, 0x09, 0x4c, 0xf3, 0x7b, 0x6a, 0x73, 0xf1, 0x02, 0x55, - 0x8c, 0x28, 0xe8, 0xf2, 0x47, 0xdf, 0xb0, 0xb0, 0xda, 0xd2, 0xee, 0xa6, 0xef, 0xe2, 0x25, 0x5b, - 0x64, 0x33, 0x11, 0x0a, 0xf1, 0x0b, 0x16, 0xd2, 0x1f, 0x7d, 0x43, 0x52, 0x7a, 0xfa, 0x09, 0xef, - 0xa0, 0xf4, 0x5d, 0xb2, 0x93, 0x94, 0x9e, 0x7c, 0xc1, 0x37, 0x50, 0x15, 0x77, 0x44, 0x43, 0x6a, - 0x90, 0x88, 0xe4, 0xd4, 0x24, 0xdc, 0xd8, 0x79, 0x40, 0x4f, 0x67, 0x07, 0xa9, 0xe1, 0x15, 0x01, - 0xe5, 0x4d, 0x39, 0xfd, 0x12, 0x0e, 0xe4, 0xe1, 0xfc, 0x46, 0xf4, 0x58, 0xce, 0xda, 0xe1, 0xae, - 0xf1, 0x68, 0x8b, 0xd0, 0xf7, 0xf0, 0xc2, 0x6d, 0xf2, 0x14, 0x2f, 0x8c, 0x00, 0x4d, 0x4f, 0xca, - 0xff, 0x15, 0x90, 0xe1, 0x63, 0xb7, 0xae, 0x6d, 0x56, 0x8d, 0x77, 0x1f, 0xc5, 0xa4, 0x0d, 0xaa, - 0xaf, 0xbc, 0x5c, 0xa4, 0x30, 0x03, 0x25, 0xd9, 0x7f, 0xc8, 0xe2, 0x2d, 0x2b, 0xba, 0x55, 0xe3, - 0xed, 0x35, 0xdc, 0xf0, 0xb6, 0x3a, 0xde, 0x46, 0x88, 0x26, 0x6e, 0x13, 0x83, 0x48, 0x33, 0x90, - 0xb0, 0xeb, 0x0d, 0xfc, 0x03, 0xfc, 0xd3, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0xe8, 0x03, 0x91, - 0x2d, 0x37, 0x17, 0x00, 0x00, + // 2605 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x59, 0x4b, 0x73, 0x1b, 0xc7, + 0xf1, 0x17, 0x5e, 0x04, 0xd0, 0x58, 0x00, 0xcb, 0xa1, 0x44, 0x82, 0x30, 0x6d, 0x51, 0x6b, 0xeb, + 0x6f, 0x9a, 0xb6, 0xc5, 0xbf, 0xe9, 0x93, 0x5d, 0x76, 0xaa, 0x40, 0x70, 0x29, 0x42, 0x26, 0x01, + 0x78, 0x01, 0xc8, 0x25, 0x57, 0xaa, 0xb6, 0x86, 0xc0, 0x90, 0xdc, 0x32, 0xf6, 0xa1, 0xdd, 0x81, + 0x44, 0x95, 0x2b, 0x49, 0x55, 0x2a, 0x3e, 0xe7, 0x90, 0x6f, 0x90, 0x7b, 0x6e, 0xb9, 0x25, 0xf7, + 0x5c, 0x72, 0x4a, 0x8e, 0xb9, 0xe6, 0x92, 0x43, 0xbe, 0x43, 0x6a, 0x7a, 0x76, 0x17, 0xbb, 0x20, + 0x40, 0x55, 0x0e, 0xb9, 0x11, 0xdd, 0xbf, 0xe9, 0x9e, 0x7e, 0x4e, 0xf7, 0x12, 0x94, 0xf1, 0xd4, + 0x62, 0x0e, 0x7f, 0xe2, 0xf9, 0x2e, 0x77, 0x49, 0x71, 0xea, 0xba, 0x9e, 0xef, 0x8d, 0x9b, 0x3b, + 0x57, 0xae, 0x7b, 0x35, 0x65, 0x07, 0xd4, 0xb3, 0x0e, 0xa8, 0xe3, 0xb8, 0x9c, 0x72, 0xcb, 0x75, + 0x02, 0x09, 0xd3, 0xfe, 0x90, 0x87, 0xda, 0x99, 0xeb, 0x7a, 0xbd, 0x19, 0x37, 0xd8, 0xcb, 0x19, + 0x0b, 0x38, 0x51, 0x21, 0x47, 0x6d, 0xde, 0xc8, 0xec, 0x66, 0xf6, 0x72, 0x86, 0xf8, 0x93, 0x10, + 0xc8, 0x4f, 0x58, 0xc0, 0x1b, 0xd9, 0xdd, 0xcc, 0x5e, 0xd9, 0xc0, 0xbf, 0xc9, 0x01, 0xdc, 0xb7, + 0xe9, 0x8d, 0x19, 0xbc, 0xa6, 0x9e, 0xe9, 0xbb, 0x33, 0x6e, 0x39, 0x57, 0xe6, 0x25, 0x63, 0x8d, + 0x1c, 0x1e, 0x5b, 0xb7, 0xe9, 0xcd, 0xe0, 0x35, 0xf5, 0x0c, 0xc9, 0x39, 0x61, 0x8c, 0x7c, 0x0e, + 0x9b, 0xe2, 0x80, 0xe7, 0x33, 0x8f, 0xbe, 0x49, 0x1d, 0xc9, 0xe3, 0x91, 0x0d, 0x9b, 0xde, 0xf4, + 0x91, 0x99, 0x38, 0xb4, 0x0b, 0x4a, 0xac, 0x45, 0x40, 0x0b, 0x08, 0x85, 0x50, 0xba, 0x40, 0x7c, + 0x00, 0xb5, 0x84, 0x58, 0x71, 0xf1, 0x35, 0xc4, 0x28, 0xb1, 0xb8, 0x96, 0xcd, 0x89, 0x06, 0x55, + 0x81, 0xb2, 0x2d, 0x87, 0xf9, 0x28, 0xa8, 0x88, 0xa0, 0x8a, 0x4d, 0x6f, 0xce, 0x05, 0x4d, 0x48, + 0xfa, 0x04, 0x54, 0xe1, 0x33, 0xd3, 0x9d, 0x71, 0x73, 0x7c, 0x4d, 0x1d, 0x87, 0x4d, 0x1b, 0xa5, + 0xdd, 0xcc, 0x5e, 0xfe, 0x28, 0xdb, 0xc8, 0x18, 0xb5, 0xa9, 0xf4, 0x52, 0x5b, 0x72, 0xc8, 0x3e, + 0xac, 0xbb, 0x33, 0x7e, 0xe5, 0x0a, 0x23, 0x04, 0xda, 0x0c, 0x18, 0x6f, 0x54, 0x76, 0x73, 0x7b, + 0x79, 0xa3, 0x1e, 0x31, 0x04, 0x76, 0xc0, 0xb8, 0xc0, 0x06, 0xaf, 0x19, 0xf3, 0xcc, 0xb1, 0xeb, + 0x5c, 0x9a, 0x9c, 0xfa, 0x57, 0x8c, 0x37, 0xca, 0xbb, 0x99, 0xbd, 0x82, 0x51, 0x47, 0x46, 0xdb, + 0x75, 0x2e, 0x87, 0x48, 0x26, 0x9f, 0x02, 0xb9, 0xe6, 0xd3, 0x31, 0x42, 0x2d, 0xdf, 0x96, 0xc1, + 0x6a, 0x54, 0x11, 0xbc, 0x2e, 0x38, 0xed, 0x24, 0x83, 0x7c, 0x09, 0xdb, 0xe8, 0x1c, 0x6f, 0x76, + 0x31, 0xb5, 0xc6, 0x48, 0x34, 0x27, 0x8c, 0x4e, 0xa6, 0x96, 0xc3, 0x1a, 0x20, 0x6e, 0x6f, 0x6c, + 0x09, 0x40, 0x7f, 0xce, 0x3f, 0x0e, 0xd9, 0xe4, 0x3e, 0x14, 0xa6, 0xf4, 0x82, 0x4d, 0x1b, 0x0a, + 0xc6, 0x55, 0xfe, 0x20, 0x3b, 0x50, 0xb6, 0x1c, 0x8b, 0x5b, 0x94, 0xbb, 0x7e, 0xa3, 0x86, 0x9c, + 0x39, 0x41, 0xfb, 0x29, 0x0b, 0x55, 0x91, 0x2f, 0x1d, 0x67, 0x75, 0xba, 0x2c, 0x06, 0x2d, 0x7b, + 0x2b, 0x68, 0xb7, 0xc2, 0x91, 0xbb, 0x1d, 0x8e, 0x6d, 0x28, 0x4d, 0x69, 0xc0, 0xcd, 0x6b, 0xd7, + 0xc3, 0x0c, 0x51, 0x8c, 0xa2, 0xf8, 0x7d, 0xea, 0x7a, 0xe4, 0x7d, 0xa8, 0xb2, 0x1b, 0xce, 0x7c, + 0x87, 0x4e, 0x4d, 0xe1, 0x12, 0x4c, 0x8b, 0x92, 0xa1, 0x44, 0xc4, 0x53, 0x3e, 0x1d, 0x93, 0x3d, + 0x50, 0x63, 0x47, 0x46, 0x3e, 0x5f, 0x43, 0x37, 0xd6, 0x22, 0x37, 0x86, 0x2e, 0x8f, 0xfd, 0x50, + 0x5c, 0xe9, 0x87, 0xd2, 0xa2, 0x1f, 0xfe, 0x95, 0x01, 0x05, 0x13, 0x9c, 0x05, 0x9e, 0xeb, 0x04, + 0x8c, 0x10, 0xc8, 0x5a, 0x13, 0xf4, 0x42, 0x19, 0xf3, 0x25, 0x6b, 0x4d, 0x84, 0x09, 0xd6, 0xc4, + 0xbc, 0x78, 0xc3, 0x59, 0x80, 0x16, 0x2a, 0x46, 0xd1, 0x9a, 0x1c, 0x89, 0x9f, 0xe4, 0x31, 0x28, + 0x78, 0x3b, 0x3a, 0x99, 0xf8, 0x2c, 0x08, 0x64, 0x69, 0xe1, 0xc1, 0x8a, 0xa0, 0xb7, 0x24, 0x99, + 0x3c, 0x81, 0x8d, 0x24, 0xcc, 0x74, 0xbc, 0xc3, 0xd7, 0xc1, 0x35, 0xfa, 0xa3, 0x2c, 0xd3, 0x21, + 0x44, 0x76, 0x91, 0x41, 0x3e, 0x09, 0xb3, 0x27, 0xc2, 0x4b, 0x78, 0x01, 0xe1, 0x6a, 0x02, 0xde, + 0x47, 0xf4, 0x63, 0xa8, 0x05, 0xcc, 0x7f, 0xc5, 0x7c, 0xd3, 0x66, 0x41, 0x40, 0xaf, 0x18, 0x3a, + 0xa8, 0x6c, 0x54, 0x25, 0xf5, 0x5c, 0x12, 0x35, 0x15, 0x6a, 0xe7, 0xae, 0x63, 0x71, 0xd7, 0x0f, + 0x63, 0xae, 0xfd, 0x31, 0x0f, 0x20, 0xac, 0x1f, 0x70, 0xca, 0x67, 0xc1, 0xd2, 0x8e, 0x21, 0xbc, + 0x91, 0x5d, 0xe9, 0x8d, 0xca, 0xa2, 0x37, 0xf2, 0xfc, 0x8d, 0x27, 0xd3, 0xa0, 0x76, 0xb8, 0xfe, + 0x24, 0xec, 0x5d, 0x4f, 0x84, 0x8e, 0xe1, 0x1b, 0x8f, 0x19, 0xc8, 0x26, 0x7b, 0x50, 0x08, 0x38, + 0xe5, 0xb2, 0x63, 0xd4, 0x0e, 0x49, 0x0a, 0x27, 0xee, 0xc2, 0x0c, 0x09, 0x20, 0x5f, 0x43, 0xed, + 0x92, 0x5a, 0xd3, 0x99, 0xcf, 0x4c, 0x9f, 0xd1, 0xc0, 0x75, 0x30, 0x93, 0x6b, 0x87, 0x9b, 0xf1, + 0x91, 0x13, 0xc9, 0x36, 0x90, 0x6b, 0x54, 0x2f, 0x93, 0x3f, 0xc9, 0x87, 0x50, 0x0f, 0x43, 0x2d, + 0xea, 0x89, 0x5b, 0x76, 0xd4, 0x79, 0x6a, 0x73, 0xf2, 0xd0, 0xb2, 0xc5, 0x8d, 0x54, 0x4c, 0xd2, + 0x99, 0x37, 0xa1, 0x9c, 0x49, 0xa4, 0xec, 0x3f, 0x35, 0x41, 0x1f, 0x21, 0x19, 0x91, 0x8b, 0x01, + 0x2f, 0x2e, 0x0f, 0xf8, 0xf2, 0x00, 0x2a, 0x2b, 0x02, 0xb8, 0x22, 0x3d, 0xaa, 0xab, 0xd2, 0xe3, + 0x21, 0x54, 0xc6, 0x6e, 0xc0, 0x4d, 0x19, 0x5f, 0xcc, 0xea, 0x9c, 0x01, 0x82, 0x34, 0x40, 0x0a, + 0x79, 0x04, 0x0a, 0x02, 0x5c, 0x67, 0x7c, 0x4d, 0x2d, 0x07, 0x9b, 0x54, 0xce, 0xc0, 0x43, 0x3d, + 0x49, 0x12, 0xc5, 0x27, 0x21, 0x97, 0x97, 0x12, 0x03, 0xb2, 0xdf, 0x22, 0x26, 0xa4, 0xcd, 0x4b, + 0xaa, 0x9e, 0x28, 0x29, 0x8d, 0x80, 0x7a, 0x66, 0x05, 0x5c, 0x44, 0x2b, 0x88, 0x52, 0xe9, 0x67, + 0xb0, 0x9e, 0xa0, 0x85, 0xc5, 0xf4, 0x11, 0x14, 0x44, 0xf7, 0x08, 0x1a, 0x99, 0xdd, 0xdc, 0x5e, + 0xe5, 0x70, 0xe3, 0x56, 0xa0, 0x67, 0x81, 0x21, 0x11, 0xda, 0x23, 0xa8, 0x0b, 0x62, 0xc7, 0xb9, + 0x74, 0xa3, 0x8e, 0x54, 0x8b, 0x4b, 0x51, 0x11, 0x89, 0xa7, 0xd5, 0x40, 0x19, 0x32, 0xdf, 0x8e, + 0x55, 0xfe, 0x0a, 0xea, 0x1d, 0x27, 0xa4, 0x84, 0x0a, 0xff, 0x0f, 0xea, 0xb6, 0xe5, 0xc8, 0x96, + 0x45, 0x6d, 0x77, 0xe6, 0xf0, 0x30, 0xe0, 0x55, 0xdb, 0x72, 0x84, 0xfc, 0x16, 0x12, 0x11, 0x17, + 0xb5, 0xb6, 0x10, 0xb7, 0x16, 0xe2, 0x64, 0x77, 0x93, 0xb8, 0x67, 0xf9, 0x52, 0x46, 0xcd, 0x3e, + 0xcb, 0x97, 0xb2, 0x6a, 0xee, 0x59, 0xbe, 0x94, 0x53, 0xf3, 0xcf, 0xf2, 0xa5, 0xbc, 0x5a, 0x78, + 0x96, 0x2f, 0x15, 0xd5, 0x92, 0xf6, 0xd7, 0x0c, 0xa8, 0xbd, 0x19, 0xff, 0x9f, 0x5e, 0x01, 0x1f, + 0x46, 0xcb, 0x31, 0xc7, 0x53, 0xfe, 0xca, 0x9c, 0xb0, 0x29, 0xa7, 0x18, 0xee, 0x82, 0xa1, 0xd8, + 0x96, 0xd3, 0x9e, 0xf2, 0x57, 0xc7, 0x82, 0x16, 0x3d, 0x9f, 0x09, 0x54, 0x39, 0x44, 0xd1, 0x9b, + 0x18, 0xf5, 0x16, 0x73, 0x7e, 0x9f, 0x01, 0xe5, 0xdb, 0x99, 0xcb, 0xd9, 0xea, 0x27, 0x01, 0x13, + 0x6f, 0xde, 0x87, 0xb3, 0xa8, 0x03, 0xc6, 0xf3, 0x1e, 0x7c, 0xab, 0xa5, 0xe7, 0x96, 0xb4, 0xf4, + 0x3b, 0x1f, 0xbb, 0xfc, 0x9d, 0x8f, 0x9d, 0xf6, 0xdb, 0x8c, 0x88, 0x7a, 0x78, 0xcd, 0xd0, 0xe5, + 0xbb, 0xa0, 0x44, 0x8f, 0x94, 0x19, 0xd0, 0xe8, 0xc2, 0x10, 0xc8, 0x57, 0x6a, 0x40, 0x71, 0xca, + 0xc1, 0x02, 0x43, 0x8d, 0xc1, 0x75, 0x8c, 0x0c, 0xa7, 0x1c, 0xc1, 0xeb, 0x4b, 0x56, 0x78, 0xe0, + 0x5d, 0x80, 0x84, 0x2f, 0x0b, 0x68, 0x67, 0x79, 0x9c, 0x70, 0xa4, 0x74, 0x61, 0x5e, 0x2d, 0x68, + 0x7f, 0x93, 0x59, 0xf0, 0xdf, 0x5e, 0xe9, 0x03, 0xa8, 0xcd, 0x87, 0x1d, 0xc4, 0xc8, 0xf7, 0x55, + 0xf1, 0xa2, 0x69, 0x47, 0xa0, 0x3e, 0x0e, 0xfb, 0x88, 0x9c, 0x3b, 0xd2, 0xd7, 0xae, 0x0b, 0xce, + 0x40, 0x30, 0x42, 0x91, 0x38, 0x9f, 0x08, 0xbf, 0xd2, 0x37, 0x36, 0x73, 0xb8, 0x89, 0xc3, 0x9e, + 0x7c, 0x73, 0xeb, 0xe8, 0x4f, 0x49, 0x3f, 0x16, 0xb1, 0xbd, 0xdb, 0x40, 0xad, 0x0e, 0xd5, 0xa1, + 0xfb, 0x03, 0x73, 0xe2, 0x62, 0xfb, 0x0a, 0x6a, 0x11, 0x21, 0x34, 0x71, 0x1f, 0xd6, 0x38, 0x52, + 0xc2, 0xea, 0x9e, 0xb7, 0xf1, 0xb3, 0x80, 0x72, 0x04, 0x1b, 0x21, 0x42, 0xfb, 0x53, 0x16, 0xca, + 0x31, 0x55, 0x24, 0xc9, 0x05, 0x0d, 0x98, 0x69, 0xd3, 0x31, 0xf5, 0x5d, 0xd7, 0x09, 0x6b, 0x5c, + 0x11, 0xc4, 0xf3, 0x90, 0x26, 0x5a, 0x58, 0x64, 0xc7, 0x35, 0x0d, 0xae, 0xd1, 0x3b, 0x8a, 0x51, + 0x09, 0x69, 0xa7, 0x34, 0xb8, 0x26, 0x1f, 0x81, 0x1a, 0x41, 0x3c, 0x9f, 0x59, 0xb6, 0x78, 0xf9, + 0xe4, 0xfb, 0x5c, 0x0f, 0xe9, 0xfd, 0x90, 0x2c, 0x1a, 0xbc, 0x2c, 0x32, 0xd3, 0xa3, 0xd6, 0xc4, + 0xb4, 0x85, 0x17, 0xe5, 0xbc, 0x5a, 0x93, 0xf4, 0x3e, 0xb5, 0x26, 0xe7, 0x01, 0xe5, 0xe4, 0x33, + 0x78, 0x90, 0x18, 0x6a, 0x13, 0x70, 0x59, 0xc5, 0xc4, 0x8f, 0xa7, 0xda, 0xf8, 0xc8, 0x23, 0x50, + 0xc4, 0x8b, 0x61, 0x8e, 0x7d, 0x46, 0x39, 0x9b, 0x84, 0x75, 0x5c, 0x11, 0xb4, 0xb6, 0x24, 0x91, + 0x06, 0x14, 0xd9, 0x8d, 0x67, 0xf9, 0x6c, 0x82, 0x2f, 0x46, 0xc9, 0x88, 0x7e, 0x8a, 0xc3, 0x01, + 0x77, 0x7d, 0x7a, 0xc5, 0x4c, 0x87, 0xda, 0x2c, 0x1c, 0x51, 0x2a, 0x21, 0xad, 0x4b, 0x6d, 0xa6, + 0xbd, 0x03, 0xdb, 0x4f, 0x19, 0x3f, 0xb3, 0x5e, 0xce, 0xac, 0x89, 0xc5, 0xdf, 0xf4, 0xa9, 0x4f, + 0xe7, 0x5d, 0xf0, 0x2f, 0x05, 0xd8, 0x48, 0xb3, 0x18, 0x67, 0xbe, 0x78, 0x81, 0x0a, 0xfe, 0x6c, + 0xca, 0xa2, 0xe8, 0xcc, 0x5f, 0xcc, 0x18, 0x6c, 0xcc, 0xa6, 0xcc, 0x90, 0x20, 0xf2, 0x35, 0xec, + 0xcc, 0x53, 0xcc, 0x17, 0x6f, 0x60, 0x40, 0xb9, 0xe9, 0x31, 0xdf, 0x7c, 0x25, 0x5e, 0x7a, 0xf4, + 0x3e, 0x56, 0xa5, 0xcc, 0x36, 0x83, 0x72, 0x91, 0x71, 0x7d, 0xe6, 0x3f, 0x17, 0x6c, 0xf2, 0x21, + 0xa8, 0xc9, 0x51, 0xd1, 0xf4, 0x3c, 0x1b, 0x23, 0x91, 0x8f, 0xbb, 0x99, 0xf0, 0x97, 0x67, 0x93, + 0x4f, 0x41, 0xec, 0x07, 0x66, 0xca, 0xc3, 0x9e, 0x1d, 0x16, 0xbd, 0x90, 0x31, 0x5f, 0x1a, 0x04, + 0xfc, 0x4b, 0x68, 0x2e, 0x5f, 0x36, 0xf0, 0x54, 0x01, 0x4f, 0x6d, 0x2e, 0x59, 0x38, 0xc4, 0xd9, + 0xf4, 0x46, 0x21, 0x22, 0xb8, 0x86, 0xf8, 0xf9, 0x46, 0x21, 0x6a, 0xe6, 0x23, 0x58, 0x4f, 0x8d, + 0xb0, 0x08, 0x2c, 0x22, 0xb0, 0x96, 0x18, 0x63, 0xe3, 0xf2, 0x5a, 0x1c, 0xff, 0x4b, 0xcb, 0xc7, + 0xff, 0x27, 0xb0, 0x11, 0x0d, 0x2e, 0x17, 0x74, 0xfc, 0x83, 0x7b, 0x79, 0x69, 0x06, 0x6c, 0x8c, + 0x4d, 0x39, 0x6f, 0xac, 0x87, 0xac, 0x23, 0xc9, 0x19, 0xb0, 0x31, 0x69, 0x42, 0x89, 0xce, 0xb8, + 0x2b, 0x62, 0x84, 0x0f, 0x71, 0xc9, 0x88, 0x7f, 0x0b, 0x59, 0xd1, 0xdf, 0xe6, 0xc5, 0x6c, 0x72, + 0xc5, 0x64, 0xbb, 0xa8, 0x48, 0x59, 0x11, 0xeb, 0x08, 0x39, 0xe2, 0x9e, 0x5f, 0xc0, 0xf6, 0x2d, + 0x3c, 0xa7, 0x3e, 0xc7, 0x1b, 0x28, 0xd2, 0x67, 0x0b, 0xa7, 0x04, 0x5b, 0x5c, 0xe3, 0x63, 0x20, + 0x82, 0x63, 0x0a, 0x97, 0x58, 0x8e, 0x79, 0x39, 0xb5, 0xae, 0xae, 0x39, 0xce, 0x21, 0x79, 0xa3, + 0x2e, 0x38, 0xe7, 0xf4, 0xa6, 0xe3, 0x9c, 0x20, 0x79, 0xd9, 0x4b, 0x57, 0x0b, 0x63, 0xfe, 0xb6, + 0x97, 0xae, 0x9e, 0xca, 0x0d, 0x89, 0xd3, 0xfe, 0x9c, 0x81, 0x6a, 0x2a, 0x39, 0xb1, 0x49, 0xc9, + 0x3d, 0xcd, 0x0c, 0x27, 0x81, 0xbc, 0x51, 0x0e, 0x29, 0x9d, 0x09, 0x79, 0x12, 0x8e, 0x9b, 0x59, + 0x9c, 0x09, 0x9b, 0xcb, 0x33, 0x3c, 0x31, 0x77, 0x7e, 0x0a, 0xc4, 0x72, 0xc6, 0xae, 0x2d, 0x72, + 0x88, 0x5f, 0xfb, 0x2c, 0xb8, 0x76, 0xa7, 0x13, 0xcc, 0xd3, 0xaa, 0xb1, 0x1e, 0x71, 0x86, 0x11, + 0x43, 0xc0, 0xe3, 0xd5, 0x70, 0x0e, 0xcf, 0x4b, 0x78, 0xc4, 0x89, 0xe1, 0xda, 0x0b, 0xd8, 0x1e, + 0xac, 0xaa, 0x52, 0xf2, 0x15, 0x80, 0x17, 0xd7, 0x26, 0x5a, 0x52, 0x39, 0xdc, 0xb9, 0x7d, 0xe1, + 0x79, 0xfd, 0x1a, 0x09, 0xbc, 0xb6, 0x03, 0xcd, 0x65, 0xa2, 0x65, 0x23, 0xd6, 0x1e, 0xc0, 0xc6, + 0x60, 0x76, 0x75, 0xc5, 0x16, 0x26, 0xb2, 0xef, 0x41, 0x39, 0xb6, 0x82, 0x97, 0x33, 0x3a, 0xb5, + 0x2e, 0x2d, 0x36, 0x79, 0x9b, 0x33, 0x3f, 0x86, 0xb5, 0x70, 0xc4, 0x96, 0xee, 0x9c, 0x0f, 0x6b, + 0xad, 0x19, 0x77, 0xc3, 0xf9, 0x3a, 0x84, 0x68, 0x3f, 0x65, 0xe0, 0x7e, 0x5a, 0x67, 0xf8, 0x28, + 0x1c, 0x42, 0x29, 0x5a, 0xbe, 0xc3, 0xc6, 0xb3, 0x35, 0xb7, 0x32, 0xf5, 0x7d, 0xc2, 0x28, 0x86, + 0x9b, 0x38, 0xf9, 0x02, 0x94, 0x49, 0xe2, 0xa2, 0x8d, 0x2c, 0x9e, 0x7b, 0x10, 0x9f, 0x4b, 0x5a, + 0x61, 0xa4, 0xa0, 0xfb, 0x8f, 0xa1, 0x14, 0xed, 0x16, 0x44, 0x81, 0xd2, 0x59, 0xaf, 0xd7, 0x37, + 0x7b, 0xa3, 0xa1, 0x7a, 0x8f, 0x54, 0xa0, 0x88, 0xbf, 0x3a, 0x5d, 0x35, 0xb3, 0x1f, 0x40, 0x39, + 0x5e, 0x2d, 0x48, 0x15, 0xca, 0x9d, 0x6e, 0x67, 0xd8, 0x69, 0x0d, 0xf5, 0x63, 0xf5, 0x1e, 0x79, + 0x00, 0xeb, 0x7d, 0x43, 0xef, 0x9c, 0xb7, 0x9e, 0xea, 0xa6, 0xa1, 0x3f, 0xd7, 0x5b, 0x67, 0xfa, + 0xb1, 0x9a, 0x21, 0x04, 0x6a, 0xa7, 0xc3, 0xb3, 0xb6, 0xd9, 0x1f, 0x1d, 0x9d, 0x75, 0x06, 0xa7, + 0xfa, 0xb1, 0x9a, 0x15, 0x32, 0x07, 0xa3, 0x76, 0x5b, 0x1f, 0x0c, 0xd4, 0x1c, 0x01, 0x58, 0x3b, + 0x69, 0x75, 0x04, 0x38, 0x4f, 0x36, 0xa0, 0xde, 0xe9, 0x3e, 0xef, 0x75, 0xda, 0xba, 0x39, 0xd0, + 0x87, 0x43, 0x41, 0x2c, 0xec, 0xff, 0x3b, 0x03, 0xd5, 0xd4, 0x76, 0x42, 0xb6, 0x60, 0x43, 0x1c, + 0x19, 0x19, 0x42, 0x53, 0x6b, 0xd0, 0xeb, 0x9a, 0xdd, 0x5e, 0x57, 0x57, 0xef, 0x91, 0x77, 0x60, + 0x6b, 0x81, 0xd1, 0x3b, 0x39, 0x69, 0x9f, 0xb6, 0xc4, 0xe5, 0x49, 0x13, 0x36, 0x17, 0x98, 0xc3, + 0xce, 0xb9, 0x2e, 0xac, 0xcc, 0x92, 0x5d, 0xd8, 0x59, 0xe0, 0x0d, 0xbe, 0xd3, 0xf5, 0x7e, 0x8c, + 0xc8, 0x91, 0xc7, 0xf0, 0x68, 0x01, 0xd1, 0xe9, 0x0e, 0x46, 0x27, 0x27, 0x9d, 0x76, 0x47, 0xef, + 0x0e, 0xcd, 0xe7, 0xad, 0xb3, 0x91, 0xae, 0xe6, 0xc9, 0x0e, 0x34, 0x16, 0x95, 0xe8, 0xe7, 0xfd, + 0x9e, 0xd1, 0x32, 0x5e, 0xa8, 0x05, 0xf2, 0x3e, 0x3c, 0xbc, 0x25, 0xa4, 0xdd, 0x33, 0x0c, 0xbd, + 0x3d, 0x34, 0x5b, 0xe7, 0xbd, 0x51, 0x77, 0xa8, 0xae, 0xed, 0x1f, 0x88, 0x0d, 0x60, 0xa1, 0xf0, + 0x84, 0xcb, 0x46, 0xdd, 0x6f, 0xba, 0xbd, 0xef, 0xba, 0xea, 0x3d, 0xe1, 0xf9, 0xe1, 0xa9, 0xa1, + 0x0f, 0x4e, 0x7b, 0x67, 0xc7, 0x6a, 0x66, 0xff, 0x37, 0x39, 0x80, 0x79, 0x6e, 0x09, 0xef, 0xb4, + 0x46, 0xc3, 0x5e, 0xa4, 0x61, 0x7e, 0x4c, 0x83, 0xf7, 0x92, 0x8c, 0xa3, 0xd1, 0xf1, 0x53, 0x7d, + 0x68, 0x76, 0x7b, 0x43, 0x73, 0x30, 0x6c, 0x19, 0x43, 0x0c, 0x57, 0x13, 0x36, 0x93, 0x18, 0xe9, + 0x85, 0x13, 0x5d, 0x1f, 0xa8, 0x59, 0xf2, 0x1e, 0x34, 0x97, 0x9c, 0xd7, 0xcf, 0x5a, 0xfd, 0x81, + 0x7e, 0xac, 0xe6, 0xc8, 0x36, 0x3c, 0x48, 0xf2, 0x3b, 0x5d, 0xf3, 0xe4, 0xac, 0xf3, 0xf4, 0x74, + 0xa8, 0xe6, 0x49, 0x03, 0xee, 0xa7, 0xc5, 0xb6, 0x50, 0xaa, 0x5a, 0x58, 0x3c, 0x74, 0xde, 0xe9, + 0xea, 0x06, 0xb2, 0xd6, 0xc8, 0x26, 0x90, 0x24, 0xab, 0x6f, 0xe8, 0xfd, 0xd6, 0x0b, 0xb5, 0x48, + 0x1e, 0xc2, 0x3b, 0x49, 0x7a, 0xe4, 0xd1, 0xa3, 0x56, 0xfb, 0x9b, 0xde, 0xc9, 0x89, 0x5a, 0x5a, + 0xd4, 0x16, 0x67, 0x73, 0x79, 0xd1, 0x37, 0x51, 0x66, 0x83, 0x88, 0x5b, 0x8a, 0xd1, 0xf9, 0x76, + 0xd4, 0x39, 0xee, 0x0c, 0x5f, 0x98, 0xbd, 0x6f, 0xd4, 0x8a, 0x88, 0xdb, 0x12, 0xcb, 0x93, 0x09, + 0xa0, 0x2a, 0x87, 0xff, 0x28, 0xcb, 0x8f, 0x00, 0x6d, 0xfc, 0xec, 0x48, 0x0c, 0x28, 0x86, 0x85, + 0x4a, 0x56, 0x95, 0x6e, 0xf3, 0x41, 0x6a, 0x91, 0x8b, 0x1b, 0xd1, 0xd6, 0xaf, 0xff, 0xfe, 0xcf, + 0xdf, 0x65, 0xd7, 0x35, 0xe5, 0xe0, 0xd5, 0x67, 0x07, 0x02, 0x71, 0xe0, 0xce, 0xf8, 0x97, 0x99, + 0x7d, 0xd2, 0x83, 0x35, 0xf9, 0xb1, 0x89, 0x6c, 0xa6, 0x44, 0xc6, 0x5f, 0x9f, 0x56, 0x49, 0xdc, + 0x44, 0x89, 0xaa, 0x56, 0x89, 0x25, 0x5a, 0x8e, 0x10, 0xf8, 0x05, 0x14, 0xc3, 0x4f, 0x19, 0x89, + 0x4b, 0xa6, 0x3f, 0x6e, 0x34, 0x97, 0x6d, 0x9b, 0xff, 0x9f, 0x21, 0xdf, 0x43, 0x39, 0x5e, 0x54, + 0xc9, 0x76, 0xa2, 0x05, 0xa7, 0xdb, 0x67, 0xb3, 0xb9, 0x8c, 0x95, 0xbe, 0x16, 0xa9, 0xc5, 0xd7, + 0xc2, 0x25, 0x96, 0x8c, 0x64, 0x3b, 0x12, 0x4b, 0x2c, 0x69, 0xa4, 0xd4, 0x27, 0xf6, 0xda, 0xa5, + 0x17, 0xd3, 0x9a, 0x28, 0xf2, 0x3e, 0x21, 0x29, 0x91, 0x07, 0x3f, 0x5a, 0x93, 0x5f, 0x90, 0x9f, + 0x83, 0x12, 0x06, 0x00, 0x57, 0x4d, 0x32, 0x77, 0x56, 0x72, 0x1f, 0x6e, 0xce, 0x8d, 0x59, 0x5c, + 0x4a, 0x97, 0x48, 0x77, 0x67, 0xfc, 0x80, 0xa3, 0xb4, 0x8b, 0x58, 0x3a, 0xae, 0x30, 0x09, 0xe9, + 0xc9, 0x65, 0x30, 0x2d, 0x3d, 0xb5, 0xec, 0x68, 0xbb, 0x28, 0xbd, 0x49, 0x1a, 0x29, 0xe9, 0x2f, + 0x05, 0xe6, 0xe0, 0x47, 0x6a, 0x73, 0x61, 0x41, 0x4d, 0x4c, 0xb0, 0x18, 0xf2, 0x3b, 0x6d, 0x98, + 0x7b, 0x6d, 0x61, 0xb5, 0xd7, 0xb6, 0x51, 0xc9, 0x06, 0x59, 0x4f, 0xa4, 0x42, 0x6c, 0xc1, 0x5c, + 0xfa, 0x9d, 0x36, 0x24, 0xa5, 0xa7, 0x4d, 0x78, 0x88, 0xd2, 0xb7, 0xc9, 0x56, 0x52, 0x7a, 0xd2, + 0x82, 0x17, 0x50, 0x15, 0x3a, 0xa2, 0x1d, 0x26, 0x48, 0x64, 0x72, 0x6a, 0x51, 0x6a, 0x6e, 0xdd, + 0xa2, 0xa7, 0xab, 0x83, 0xd4, 0x51, 0x45, 0x40, 0xf9, 0x81, 0x5c, 0x8e, 0x08, 0x07, 0x72, 0x7b, + 0xbc, 0x27, 0x5a, 0x2c, 0x67, 0xe5, 0xec, 0xdf, 0xbc, 0x73, 0x82, 0xd0, 0x76, 0x50, 0xe1, 0x26, + 0xb9, 0x8f, 0x0a, 0x23, 0xc0, 0x81, 0x27, 0xe5, 0xff, 0x12, 0xc8, 0xe0, 0x2e, 0xad, 0x2b, 0x67, + 0x99, 0xe6, 0xfb, 0x77, 0x62, 0xd2, 0x0e, 0xd5, 0x96, 0x2a, 0x17, 0x25, 0xcc, 0x40, 0x49, 0x4e, + 0x10, 0x64, 0x6e, 0xcb, 0x92, 0x61, 0xa6, 0xf9, 0xee, 0x0a, 0x6e, 0xa8, 0xad, 0x81, 0xda, 0x08, + 0x51, 0x85, 0x36, 0x31, 0xa7, 0x1e, 0x04, 0x12, 0x76, 0xb1, 0x86, 0xff, 0x1f, 0xf9, 0xfc, 0x3f, + 0x01, 0x00, 0x00, 0xff, 0xff, 0xb9, 0xba, 0x11, 0xb0, 0x56, 0x19, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/looprpc/client.proto b/looprpc/client.proto index 6a78c7f..482de67 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -872,9 +872,102 @@ message SetLiquidityParamsResponse { message SuggestSwapsRequest { } +enum AutoReason { + AUTO_REASON_UNKNOWN = 0; + + /* + Budget not started indicates that we do not recommend any swaps because + the start time for our budget has not arrived yet. + */ + AUTO_REASON_BUDGET_NOT_STARTED = 1; + + /* + Sweep fees indicates that the estimated fees to sweep swaps are too high + right now. + */ + AUTO_REASON_SWEEP_FEES = 2; + + /* + Budget elapsed indicates that the autoloop budget for the period has been + elapsed. + */ + AUTO_REASON_BUDGET_ELAPSED = 3; + + /* + In flight indicates that the limit on in-flight automatically dispatched + swaps has already been reached. + */ + AUTO_REASON_IN_FLIGHT = 4; + + /* + Swap fee indicates that the server fee for a specific swap is too high. + */ + AUTO_REASON_SWAP_FEE = 5; + + /* + Miner fee indicates that the miner fee for a specific swap is to high. + */ + AUTO_REASON_MINER_FEE = 6; + + /* + Prepay indicates that the prepay fee for a specific swap is too high. + */ + AUTO_REASON_PREPAY = 7; + + /* + Failure backoff indicates that a swap has recently failed for this target, + and the backoff period has not yet passed. + */ + AUTO_REASON_FAILURE_BACKOFF = 8; + + /* + Loop out indicates that a loop out swap is currently utilizing the channel, + so it is not eligible. + */ + AUTO_REASON_LOOP_OUT = 9; + + /* + Loop In indicates that a loop in swap is currently in flight for the peer, + so it is not eligible. + */ + AUTO_REASON_LOOP_IN = 10; + + /* + Liquidity ok indicates that a target meets the liquidity balance expressed + in its rule, so no swap is needed. + */ + AUTO_REASON_LIQUIDITY_OK = 11; + + /* + Budget insufficient indicates that we cannot perform a swap because we do + not have enough pending budget available. This differs from budget elapsed, + because we still have some budget available, but we have allocated it to + other swaps. + */ + AUTO_REASON_BUDGET_INSUFFICIENT = 12; +} + +message Disqualified { + /* + The short channel ID of the channel that was excluded from our suggestions. + */ + uint64 channel_id = 1; + + /* + The reason that we excluded the channel from the our suggestions. + */ + AutoReason reason = 2; +} + message SuggestSwapsResponse { /* The set of recommended loop outs. */ repeated LoopOutRequest loop_out = 1; + + /* + Disqualified contains the set of channels that swaps are not recommended + for. + */ + repeated Disqualified disqualified = 2; } diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index 3a90098..ca9550c 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -395,6 +395,40 @@ } }, "definitions": { + "looprpcAutoReason": { + "type": "string", + "enum": [ + "AUTO_REASON_UNKNOWN", + "AUTO_REASON_BUDGET_NOT_STARTED", + "AUTO_REASON_SWEEP_FEES", + "AUTO_REASON_BUDGET_ELAPSED", + "AUTO_REASON_IN_FLIGHT", + "AUTO_REASON_SWAP_FEE", + "AUTO_REASON_MINER_FEE", + "AUTO_REASON_PREPAY", + "AUTO_REASON_FAILURE_BACKOFF", + "AUTO_REASON_LOOP_OUT", + "AUTO_REASON_LOOP_IN", + "AUTO_REASON_LIQUIDITY_OK", + "AUTO_REASON_BUDGET_INSUFFICIENT" + ], + "default": "AUTO_REASON_UNKNOWN", + "description": " - AUTO_REASON_BUDGET_NOT_STARTED: Budget not started indicates that we do not recommend any swaps because \nthe start time for our budget has not arrived yet.\n - AUTO_REASON_SWEEP_FEES: Sweep fees indicates that the estimated fees to sweep swaps are too high \nright now.\n - AUTO_REASON_BUDGET_ELAPSED: Budget elapsed indicates that the autoloop budget for the period has been \nelapsed.\n - AUTO_REASON_IN_FLIGHT: In flight indicates that the limit on in-flight automatically dispatched \nswaps has already been reached.\n - AUTO_REASON_SWAP_FEE: Swap fee indicates that the server fee for a specific swap is too high.\n - AUTO_REASON_MINER_FEE: Miner fee indicates that the miner fee for a specific swap is to high.\n - AUTO_REASON_PREPAY: Prepay indicates that the prepay fee for a specific swap is too high.\n - AUTO_REASON_FAILURE_BACKOFF: Failure backoff indicates that a swap has recently failed for this target,\nand the backoff period has not yet passed.\n - AUTO_REASON_LOOP_OUT: Loop out indicates that a loop out swap is currently utilizing the channel,\nso it is not eligible.\n - AUTO_REASON_LOOP_IN: Loop In indicates that a loop in swap is currently in flight for the peer, \nso it is not eligible.\n - AUTO_REASON_LIQUIDITY_OK: Liquidity ok indicates that a target meets the liquidity balance expressed \nin its rule, so no swap is needed.\n - AUTO_REASON_BUDGET_INSUFFICIENT: Budget insufficient indicates that we cannot perform a swap because we do \nnot have enough pending budget available. This differs from budget elapsed, \nbecause we still have some budget available, but we have allocated it to \nother swaps." + }, + "looprpcDisqualified": { + "type": "object", + "properties": { + "channel_id": { + "type": "string", + "format": "uint64", + "description": "The short channel ID of the channel that was excluded from our suggestions." + }, + "reason": { + "$ref": "#/definitions/looprpcAutoReason", + "description": "The reason that we excluded the channel from the our suggestions." + } + } + }, "looprpcFailureReason": { "type": "string", "enum": [ @@ -808,6 +842,13 @@ "$ref": "#/definitions/looprpcLoopOutRequest" }, "description": "The set of recommended loop outs." + }, + "disqualified": { + "type": "array", + "items": { + "$ref": "#/definitions/looprpcDisqualified" + }, + "description": "Disqualified contains the set of channels that swaps are not recommended\nfor." } } }, From cf50ffd43c85a46d9ab1b61a891f8bc8d2c0e4c9 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 8 Feb 2021 09:39:04 +0200 Subject: [PATCH 6/6] multi: add disqualified swaps to docs and release notes --- docs/autoloop.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ release_notes.md | 4 ++++ 2 files changed, 51 insertions(+) diff --git a/docs/autoloop.md b/docs/autoloop.md index ae9e244..63fc5bf 100644 --- a/docs/autoloop.md +++ b/docs/autoloop.md @@ -225,3 +225,50 @@ specified in the outgoing channel swap, and for loop in the channel's peer is specified as the last hop for an ongoing swap. This check is put in place to prevent the autolooper from interfering with swaps you have created yourself. +## Disqualified Swaps +There are various restrictions placed on the client's autoloop functionality. +If a channel is not eligible for a swap at present, or it does not need one +based on the current set of liquidity rules, it will be listed in the +`Disqualified` section of the output of the `SuggestSwaps` API. One of the +following reasons will be displayed: + +* Budget not started: if the start date for your budget is in the future, + no swaps will be executed until the start date is reached. See [budget](#budget) to + update. +* Budget elapsed: if the autolooper has elapsed the budget assigned to it for + fees, this reason will be returned. See [budget](#budget) to update. +* Sweep fees: this reason will be displayed if the estimated chain fee rate for + sweeping a loop out swap is higher than the current limit. See [sweep fees](#fee-market-awareness) + to update. +* In flight: there is a limit to the number of automatically dispatched swaps + that the client allows. If this limit has been reached, no further swaps + will be automatically dispatched until the in-flight swaps complete. See + [in flight limit](#in-flight-limit) to update. +* Budget insufficient: if there is not enough remaining budget for a swap, + including the amount currently reserved for in flight swaps, an insufficient + reason will be displayed. This differs from budget elapsed because there is + still budget remaining, just not enough to execute a specific swap. +* Swap fee: there is a limit placed on the fee that the client will pay to the + server for automatically dispatched swaps. The swap fee reason will be shown + if the fees advertised by the server are too high. See [swap fee](#swap-fee) + to update. +* Miner fee: if the estimated on-chain fees for a swap are too high, autoloop + will display a miner fee reason. See [miner fee](#miner-fee) to update. +* Prepay: if the no-show fee that the server will pay in the unlikely event + that the client fails to complete a swap is too high, a prepay reason will + be returned. See [no show fees](#no-show-fee) to update. +* Backoff: if an automatically dispatched swap has recently failed for a channel, + autoloop will backoff for a period before retrying. See [failure backoff](#failure-backoff) + to update. +* Loop out: if there is currently a loop out swap in-flight on a channel, it + will not be used for automated swaps. This issue will resolve itself once the + in-flight swap completes. +* Loop in: if there is currently a loop in swap in-flight for a peer, it will + not be used for automated swaps. This will resolve itself once the swap is + completed. +* Liquidity ok: if a channel's current liquidity balance is within the bound set + by the rule that it applies to, then a liquidity ok reason will be displayed + to indicate that no action is required for that channel. + +Further details for all of these reasons can be found in loopd's debug level +logs. diff --git a/release_notes.md b/release_notes.md index e0af837..ced6a24 100644 --- a/release_notes.md +++ b/release_notes.md @@ -28,6 +28,10 @@ This file tracks release notes for the loop client. swaps even if there are manually initiated swaps that are not limited to a single channel in progress. This change was made to allow autoloop to coexist with manual swaps. +* The `SuggestSwaps` endpoint has been updated to include reasons that indicate + why the Autolooper is not currently dispatching swaps for the set of rules + that the client is configured with. See the [autoloop documentation](docs/autoloop.md) for a + detailed explanations of these reasons. #### Breaking Changes * The `AutoOut`, `AutoOutBudgetSat` and `AutoOutBudgetStartSec` fields in the