From 9c35946cab1143f8824f756df25c79a1925dd6e4 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 16 Feb 2021 13:31:48 +0200 Subject: [PATCH 1/7] liquidity: simplify suggest swap to return swap amount --- liquidity/liquidity.go | 24 +++++++++++----------- liquidity/suggestions.go | 34 -------------------------------- liquidity/threshold_rule.go | 18 +++++++---------- liquidity/threshold_rule_test.go | 12 +++++------ 4 files changed, 24 insertions(+), 64 deletions(-) delete mode 100644 liquidity/suggestions.go diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 6586a00..30153a0 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -713,10 +713,10 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( continue } - // We can have nil suggestions in the case where no action is + // We can have zero amount in the case where no action is // required, so we skip over them. - suggestion := rule.suggestSwap(balance, restrictions) - if suggestion == nil { + amount := rule.swapAmount(balance, restrictions) + if amount == 0 { disqualified[balance.channelID] = ReasonLiquidityOk continue } @@ -724,7 +724,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // Get a quote for a swap of this amount. quote, err := m.cfg.LoopOutQuote( ctx, &loop.LoopOutQuoteRequest{ - Amount: suggestion.Amount, + Amount: amount, SweepConfTarget: m.params.SweepConfTarget, SwapPublicationDeadline: m.cfg.Clock.Now(), }, @@ -734,19 +734,19 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( } log.Debugf("quote for suggestion: %v, swap fee: %v, "+ - "miner fee: %v, prepay: %v", suggestion, quote.SwapFee, + "miner fee: %v, prepay: %v", amount, quote.SwapFee, quote.MinerFee, quote.PrepayAmount) // Check that the estimated fees for the suggested swap are // below the fee limits configured by the manager. - feeReason := m.checkFeeLimits(quote, suggestion.Amount) + feeReason := m.checkFeeLimits(quote, amount) if feeReason != ReasonNone { disqualified[balance.channelID] = feeReason continue } outRequest, err := m.makeLoopOutRequest( - ctx, suggestion, quote, autoloop, + ctx, amount, balance, quote, autoloop, ) if err != nil { return nil, err @@ -871,21 +871,19 @@ func (m *Manager) getSwapRestrictions(ctx context.Context, swapType swap.Type) ( // dispatched, and decides whether we set a sweep address (we don't bother for // non-auto requests, because the client api will set it anyway). func (m *Manager) makeLoopOutRequest(ctx context.Context, - suggestion *LoopOutRecommendation, quote *loop.LoopOutQuote, + amount btcutil.Amount, balance *balances, quote *loop.LoopOutQuote, autoloop bool) (loop.OutRequest, error) { prepayMaxFee := ppmToSat( quote.PrepayAmount, m.params.MaximumPrepayRoutingFeePPM, ) - routeMaxFee := ppmToSat( - suggestion.Amount, m.params.MaximumRoutingFeePPM, - ) + routeMaxFee := ppmToSat(amount, m.params.MaximumRoutingFeePPM) request := loop.OutRequest{ - Amount: suggestion.Amount, + Amount: amount, OutgoingChanSet: loopdb.ChannelSet{ - suggestion.Channel.ToUint64(), + balance.channelID.ToUint64(), }, MaxPrepayRoutingFee: prepayMaxFee, MaxSwapRoutingFee: routeMaxFee, diff --git a/liquidity/suggestions.go b/liquidity/suggestions.go deleted file mode 100644 index 3f2bfc0..0000000 --- a/liquidity/suggestions.go +++ /dev/null @@ -1,34 +0,0 @@ -package liquidity - -import ( - "fmt" - - "github.com/btcsuite/btcutil" - "github.com/lightningnetwork/lnd/lnwire" -) - -// LoopOutRecommendation contains the information required to recommend a loop -// out. -type LoopOutRecommendation struct { - // Amount is the total amount to swap. - Amount btcutil.Amount - - // Channel is the target outgoing channel. - Channel lnwire.ShortChannelID -} - -// String returns a string representation of a loop out recommendation. -func (l *LoopOutRecommendation) String() string { - return fmt.Sprintf("loop out: %v over %v", l.Amount, - l.Channel.ToUint64()) -} - -// newLoopOutRecommendation creates a new loop out swap. -func newLoopOutRecommendation(amount btcutil.Amount, - channelID lnwire.ShortChannelID) *LoopOutRecommendation { - - return &LoopOutRecommendation{ - Amount: amount, - Channel: channelID, - } -} diff --git a/liquidity/threshold_rule.go b/liquidity/threshold_rule.go index 559449d..47ef735 100644 --- a/liquidity/threshold_rule.go +++ b/liquidity/threshold_rule.go @@ -62,10 +62,10 @@ func (r *ThresholdRule) validate() error { return nil } -// suggestSwap suggests a swap based on the liquidity thresholds configured, -// returning nil if no swap is recommended. -func (r *ThresholdRule) suggestSwap(channel *balances, - outRestrictions *Restrictions) *LoopOutRecommendation { +// swapAmount suggests a swap based on the liquidity thresholds configured, +// returning zero if no swap is recommended. +func (r *ThresholdRule) swapAmount(channel *balances, + outRestrictions *Restrictions) btcutil.Amount { // Examine our total balance and required ratios to decide whether we // need to swap. @@ -76,17 +76,13 @@ func (r *ThresholdRule) suggestSwap(channel *balances, // Limit our swap amount by the minimum/maximum thresholds set. switch { case amount < outRestrictions.Minimum: - return nil + return 0 case amount > outRestrictions.Maximum: - return newLoopOutRecommendation( - outRestrictions.Maximum, channel.channelID, - ) + return outRestrictions.Maximum default: - return newLoopOutRecommendation( - amount, channel.channelID, - ) + return amount } } diff --git a/liquidity/threshold_rule_test.go b/liquidity/threshold_rule_test.go index ea6aa1e..b90c6c6 100644 --- a/liquidity/threshold_rule_test.go +++ b/liquidity/threshold_rule_test.go @@ -184,7 +184,7 @@ func TestSuggestSwap(t *testing.T) { rule *ThresholdRule channel *balances outRestrictions *Restrictions - swap *LoopOutRecommendation + swap btcutil.Amount }{ { name: "liquidity ok", @@ -205,7 +205,7 @@ func TestSuggestSwap(t *testing.T) { incoming: 0, outgoing: 100, }, - swap: &LoopOutRecommendation{Amount: 50}, + swap: 50, }, { name: "amount below minimum", @@ -216,7 +216,7 @@ func TestSuggestSwap(t *testing.T) { incoming: 0, outgoing: 100, }, - swap: nil, + swap: 0, }, { name: "amount above maximum", @@ -227,7 +227,7 @@ func TestSuggestSwap(t *testing.T) { incoming: 0, outgoing: 100, }, - swap: &LoopOutRecommendation{Amount: 20}, + swap: 20, }, { name: "loop in", @@ -238,7 +238,7 @@ func TestSuggestSwap(t *testing.T) { incoming: 100, outgoing: 0, }, - swap: nil, + swap: 0, }, } @@ -246,7 +246,7 @@ func TestSuggestSwap(t *testing.T) { test := test t.Run(test.name, func(t *testing.T) { - swap := test.rule.suggestSwap( + swap := test.rule.swapAmount( test.channel, test.outRestrictions, ) require.Equal(t, test.swap, swap) From c6e816ad95e1bdef207a53cfaa59528b9a9fe86d Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 16 Feb 2021 13:31:49 +0200 Subject: [PATCH 2/7] liquidity: move swap creation into separate function --- liquidity/balances.go | 5 ++ liquidity/liquidity.go | 117 +++++++++++++++++++++++++---------------- liquidity/reasons.go | 66 +++++++++++++++++++++++ 3 files changed, 144 insertions(+), 44 deletions(-) diff --git a/liquidity/balances.go b/liquidity/balances.go index 051cff5..d729e02 100644 --- a/liquidity/balances.go +++ b/liquidity/balances.go @@ -4,6 +4,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" ) // balances summarizes the state of the balances of a channel. Channel reserve, @@ -20,6 +21,9 @@ type balances struct { // channelID is the channel that has these balances. channelID lnwire.ShortChannelID + + // pubkey is the public key of the peer we have this balances set with. + pubkey route.Vertex } // newBalances creates a balances struct from lndclient channel information. @@ -29,5 +33,6 @@ func newBalances(info lndclient.ChannelInfo) *balances { incoming: info.RemoteBalance, outgoing: info.LocalBalance, channelID: lnwire.NewShortChanIDFromInt(info.ChannelID), + pubkey: info.PubKeyBytes, } } diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 30153a0..bf65014 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -705,53 +705,21 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( continue } - // 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 - } - - // We can have zero amount in the case where no action is - // required, so we skip over them. - amount := rule.swapAmount(balance, restrictions) - if amount == 0 { - disqualified[balance.channelID] = ReasonLiquidityOk - continue - } - - // Get a quote for a swap of this amount. - quote, err := m.cfg.LoopOutQuote( - ctx, &loop.LoopOutQuoteRequest{ - Amount: amount, - SweepConfTarget: m.params.SweepConfTarget, - SwapPublicationDeadline: m.cfg.Clock.Now(), - }, + suggestion, err := m.suggestSwap( + ctx, traffic, balance, rule, restrictions, autoloop, ) - if err != nil { - return nil, err - } - log.Debugf("quote for suggestion: %v, swap fee: %v, "+ - "miner fee: %v, prepay: %v", amount, quote.SwapFee, - quote.MinerFee, quote.PrepayAmount) - - // Check that the estimated fees for the suggested swap are - // below the fee limits configured by the manager. - feeReason := m.checkFeeLimits(quote, amount) - if feeReason != ReasonNone { - disqualified[balance.channelID] = feeReason + var reasonErr *reasonError + if errors.As(err, &reasonErr) { + disqualified[balance.channelID] = reasonErr.reason continue } - outRequest, err := m.makeLoopOutRequest( - ctx, amount, balance, quote, autoloop, - ) if err != nil { return nil, err } - suggestions = append(suggestions, outRequest) + + suggestions = append(suggestions, *suggestion) } // Finally, run through all possible swaps, excluding swaps that are @@ -824,6 +792,67 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( return resp, nil } +// suggestSwap checks whether we can currently perform a swap, and creates a +// swap request for the rule provided. +func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic, + balance *balances, rule *ThresholdRule, restrictions *Restrictions, + autoloop bool) (*loop.OutRequest, error) { + + // Check whether we can perform a swap. + err := traffic.maySwap(balance.pubkey, balance.channelID) + if err != nil { + return nil, err + } + + // We can have nil suggestions in the case where no action is + // required, so we skip over them. + amount := rule.swapAmount(balance, restrictions) + if amount == 0 { + return nil, newReasonError(ReasonLiquidityOk) + } + + return m.loopOutSwap(ctx, amount, balance, autoloop) +} + +// loopOutSwap creates a loop out swap with the amount provided for the balance +// described by the balance set provided. A reason that indicates whether we +// can swap is returned. If this value is not ReasonNone, there is no possible +// swap and the loop out request returned will be nil. +func (m *Manager) loopOutSwap(ctx context.Context, amount btcutil.Amount, + balance *balances, autoloop bool) (*loop.OutRequest, error) { + + quote, err := m.cfg.LoopOutQuote( + ctx, &loop.LoopOutQuoteRequest{ + Amount: amount, + SweepConfTarget: m.params.SweepConfTarget, + SwapPublicationDeadline: m.cfg.Clock.Now(), + }, + ) + if err != nil { + return nil, err + } + + log.Debugf("quote for suggestion: %v, swap fee: %v, "+ + "miner fee: %v, prepay: %v", amount, quote.SwapFee, + quote.MinerFee, quote.PrepayAmount) + + // Check that the estimated fees for the suggested swap are + // below the fee limits configured by the manager. + feeReason := m.checkFeeLimits(quote, amount) + if feeReason != ReasonNone { + return nil, newReasonError(feeReason) + } + + outRequest, err := m.makeLoopOutRequest( + ctx, amount, balance, quote, autoloop, + ) + if err != nil { + return nil, err + } + + return &outRequest, nil +} + // getSwapRestrictions queries the server for its latest swap size restrictions, // validates client restrictions (if present) against these values and merges // the client's custom requirements with the server's limits to produce a single @@ -1093,31 +1122,31 @@ 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) Reason { + chanID lnwire.ShortChannelID) error { 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 ReasonFailureBackoff + return newReasonError(ReasonFailureBackoff) } if s.ongoingLoopOut[chanID] { log.Debugf("Channel: %v not eligible for suggestions, "+ "ongoing loop out utilizing channel", chanID) - return ReasonLoopOut + return newReasonError(ReasonLoopOut) } if s.ongoingLoopIn[peer] { log.Debugf("Peer: %x not eligible for suggestions ongoing "+ "loop in utilizing peer", peer) - return ReasonLoopIn + return newReasonError(ReasonLoopIn) } - return ReasonNone + return nil } // checkFeeLimits takes a set of fees for a swap and checks whether they exceed diff --git a/liquidity/reasons.go b/liquidity/reasons.go index d685f61..c97616f 100644 --- a/liquidity/reasons.go +++ b/liquidity/reasons.go @@ -1,5 +1,7 @@ package liquidity +import "fmt" + // Reason is an enum which represents the various reasons we have for not // executing a swap. type Reason uint8 @@ -60,3 +62,67 @@ const ( // but we have allocated it to other swaps. ReasonBudgetInsufficient ) + +// String returns a string representation of a reason. +func (r Reason) String() string { + switch r { + case ReasonNone: + return "none" + + case ReasonBudgetNotStarted: + return "budget not started" + + case ReasonSweepFees: + return "sweep fees to high" + + case ReasonBudgetElapsed: + return "budget elapsed" + + case ReasonInFlight: + return "autoloops already in flight" + + case ReasonSwapFee: + return "swap server fee to high" + + case ReasonMinerFee: + return "miner fee to high" + + case ReasonPrepay: + return "prepayment too high" + + case ReasonFailureBackoff: + return "backing off due to failure" + + case ReasonLoopOut: + return "loop out using channel" + + case ReasonLoopIn: + return "loop in using peer" + + case ReasonLiquidityOk: + return "liquidity balance ok" + + case ReasonBudgetInsufficient: + return "budget insufficient" + + default: + return "unknown" + } +} + +// reasonError is an error type which embeds our reasons for not performing +// swaps. +type reasonError struct { + reason Reason +} + +func newReasonError(r Reason) *reasonError { + return &reasonError{ + reason: r, + } +} + +// Error returns an error string for a reason error. +func (r *reasonError) Error() string { + return fmt.Sprintf("swap reason: %v", r.reason) +} From d1f121cbc6fc78c0f7838ff6e0a7de46d190a330 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 16 Feb 2021 13:31:50 +0200 Subject: [PATCH 3/7] liquidity: move swap suggestions behind interface --- liquidity/interface.go | 47 ++++++++++++++++++++++++++++++++++++++++ liquidity/liquidity.go | 49 ++++++++++++++++++++++++++++++------------ 2 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 liquidity/interface.go diff --git a/liquidity/interface.go b/liquidity/interface.go new file mode 100644 index 0000000..a2386ed --- /dev/null +++ b/liquidity/interface.go @@ -0,0 +1,47 @@ +package liquidity + +import ( + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop" + "github.com/lightningnetwork/lnd/lnwire" +) + +// swapSuggestion is an interface implemented by suggested swaps for our +// different swap types. This interface is used to allow us to handle different +// swap types with the same autoloop logic. +type swapSuggestion interface { + // fees returns the highest possible fee amount we could pay for a swap + // in satoshis. + fees() btcutil.Amount + + // amount returns the swap amount in satoshis. + amount() btcutil.Amount + + // channels returns the set of channels involved in the swap. + channels() []lnwire.ShortChannelID +} + +type loopOutSwapSuggestion struct { + loop.OutRequest +} + +func (l *loopOutSwapSuggestion) amount() btcutil.Amount { + return l.Amount +} + +func (l *loopOutSwapSuggestion) fees() btcutil.Amount { + return worstCaseOutFees( + l.MaxPrepayRoutingFee, l.MaxSwapRoutingFee, l.MaxSwapFee, + l.MaxMinerFee, l.MaxPrepayAmount, + ) +} + +func (l *loopOutSwapSuggestion) channels() []lnwire.ShortChannelID { + channels := make([]lnwire.ShortChannelID, len(l.OutgoingChanSet)) + + for i, id := range l.OutgoingChanSet { + channels[i] = lnwire.NewShortChanIDFromInt(id) + } + + return channels +} diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index bf65014..c0007e7 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -574,6 +574,17 @@ func newSuggestions() *Suggestions { } } +func (s *Suggestions) addSwap(swap swapSuggestion) error { + out, ok := swap.(*loopOutSwapSuggestion) + if !ok { + return fmt.Errorf("unexpected swap type: %T", swap) + } + + s.OutSwaps = append(s.OutSwaps, out.OutRequest) + + return nil +} + // 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). @@ -693,7 +704,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( traffic := m.currentSwapTraffic(loopOut, loopIn) var ( - suggestions []loop.OutRequest + suggestions []swapSuggestion disqualified = make(map[lnwire.ShortChannelID]Reason) ) @@ -719,7 +730,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( return nil, err } - suggestions = append(suggestions, *suggestion) + suggestions = append(suggestions, suggestion) } // Finally, run through all possible swaps, excluding swaps that are @@ -736,7 +747,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // Sort suggestions by amount in descending order. sort.SliceStable(suggestions, func(i, j int) bool { - return suggestions[i].Amount > suggestions[j].Amount + return suggestions[i].amount() > suggestions[j].amount() }) // Run through our suggested swaps in descending order of amount and @@ -745,11 +756,14 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // 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) + setReason := func(reason Reason, swap swapSuggestion) { + for _, channel := range swap.channels() { + _, ok := m.params.ChannelRules[channel] + if !ok { + continue + } - resp.DisqualifiedChans[chanID] = reason + resp.DisqualifiedChans[channel] = reason } } @@ -773,17 +787,17 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( continue } - fees := worstCaseOutFees( - swap.MaxPrepayRoutingFee, swap.MaxSwapRoutingFee, - swap.MaxSwapFee, swap.MaxMinerFee, swap.MaxPrepayAmount, - ) + fees := swap.fees() // If the maximum fee we expect our swap to use is less than the // amount we have available, we add it to our set of swaps that // fall within the budget and decrement our available amount. if fees <= available { available -= fees - resp.OutSwaps = append(resp.OutSwaps, swap) + + if err := resp.addSwap(swap); err != nil { + return nil, err + } } else { setReason(ReasonBudgetInsufficient, swap) } @@ -796,7 +810,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // swap request for the rule provided. func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic, balance *balances, rule *ThresholdRule, restrictions *Restrictions, - autoloop bool) (*loop.OutRequest, error) { + autoloop bool) (swapSuggestion, error) { // Check whether we can perform a swap. err := traffic.maySwap(balance.pubkey, balance.channelID) @@ -811,7 +825,14 @@ func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic, return nil, newReasonError(ReasonLiquidityOk) } - return m.loopOutSwap(ctx, amount, balance, autoloop) + swap, err := m.loopOutSwap(ctx, amount, balance, autoloop) + if err != nil { + return nil, err + } + + return &loopOutSwapSuggestion{ + OutRequest: *swap, + }, nil } // loopOutSwap creates a loop out swap with the amount provided for the balance From 3f46ae514b4dff1c982b1ff458896de05bd18bb9 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 16 Feb 2021 13:31:51 +0200 Subject: [PATCH 4/7] liquidity: add peer-level liquidity rules to allow aggregate management We add 'peer-level' rules to allow assessment of liquidity on a per-peer level, rather than on an individual channel basis. No overlap is allowed with the existing set of channel rules because this could lead to contradictory rules. --- liquidity/autoloop_test.go | 157 +++++++++++++++++++++++++++ liquidity/balances.go | 18 ++-- liquidity/liquidity.go | 209 +++++++++++++++++++++++++++++------- liquidity/liquidity_test.go | 185 +++++++++++++++++++++++++++---- 4 files changed, 506 insertions(+), 63 deletions(-) diff --git a/liquidity/autoloop_test.go b/liquidity/autoloop_test.go index 4295542..c609615 100644 --- a/liquidity/autoloop_test.go +++ b/liquidity/autoloop_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/btcsuite/btcutil" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/labels" @@ -12,6 +13,7 @@ import ( "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" ) // TestAutoLoopDisabled tests the case where we need to perform a swap, but @@ -266,6 +268,161 @@ func TestAutoLoopEnabled(t *testing.T) { c.stop() } +// TestCompositeRules tests the case where we have rules set on a per peer +// and per channel basis, and perform swaps for both targets. +func TestCompositeRules(t *testing.T) { + defer test.Guard(t)() + + // Setup our channels so that we have two channels with peer 2, and + // a single channel with peer 1. + channel3 := lndclient.ChannelInfo{ + ChannelID: chanID3.ToUint64(), + PubKeyBytes: peer2, + LocalBalance: 10000, + RemoteBalance: 0, + Capacity: 10000, + } + + channels := []lndclient.ChannelInfo{ + channel1, channel2, channel3, + } + + // Create a set of parameters with autoloop enabled, set our budget to + // a value that will easily accommodate our two swaps. + params := Parameters{ + Autoloop: true, + AutoFeeBudget: 100000, + AutoFeeStartDate: testTime, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + SweepFeeRateLimit: 20000, + SweepConfTarget: 10, + MaximumPrepay: 20000, + MaximumSwapFeePPM: 1000, + MaximumRoutingFeePPM: 1000, + MaximumPrepayRoutingFeePPM: 1000, + MaximumMinerFee: 20000, + ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{ + chanID1: chanRule, + }, + PeerRules: map[route.Vertex]*ThresholdRule{ + peer2: chanRule, + }, + } + + c := newAutoloopTestCtx(t, params, channels, testRestrictions) + c.start() + + // Calculate our maximum allowed fees and create quotes that fall within + // our budget. + var ( + // Create a quote for our peer level swap that is within + // our budget, with an amount which would balance the peer + /// across all of its channels. + peerAmount = btcutil.Amount(15000) + maxPeerSwapFee = ppmToSat(peerAmount, params.MaximumSwapFeePPM) + + peerSwapQuote = &loop.LoopOutQuote{ + SwapFee: maxPeerSwapFee, + PrepayAmount: params.MaximumPrepay - 20, + } + + peerSwapQuoteRequest = &loop.LoopOutQuoteRequest{ + Amount: peerAmount, + SweepConfTarget: params.SweepConfTarget, + } + + maxPeerRouteFee = ppmToSat( + peerAmount, params.MaximumRoutingFeePPM, + ) + + peerSwap = &loop.OutRequest{ + Amount: peerAmount, + MaxSwapRoutingFee: maxPeerRouteFee, + MaxPrepayRoutingFee: ppmToSat( + peerSwapQuote.PrepayAmount, + params.MaximumPrepayRoutingFeePPM, + ), + MaxSwapFee: peerSwapQuote.SwapFee, + MaxPrepayAmount: peerSwapQuote.PrepayAmount, + MaxMinerFee: params.MaximumMinerFee, + SweepConfTarget: params.SweepConfTarget, + OutgoingChanSet: loopdb.ChannelSet{ + chanID2.ToUint64(), chanID3.ToUint64(), + }, + Label: labels.AutoloopLabel(swap.TypeOut), + Initiator: autoloopSwapInitiator, + } + // Create a quote for our single channel swap that is within + // our budget. + chanAmount = chan1Rec.Amount + maxChanSwapFee = ppmToSat(chanAmount, params.MaximumSwapFeePPM) + + channelSwapQuote = &loop.LoopOutQuote{ + SwapFee: maxChanSwapFee, + PrepayAmount: params.MaximumPrepay - 10, + } + + chanSwapQuoteRequest = &loop.LoopOutQuoteRequest{ + Amount: chanAmount, + SweepConfTarget: params.SweepConfTarget, + } + + maxChanRouteFee = ppmToSat( + chanAmount, params.MaximumRoutingFeePPM, + ) + + chanSwap = &loop.OutRequest{ + Amount: chanAmount, + MaxSwapRoutingFee: maxChanRouteFee, + MaxPrepayRoutingFee: ppmToSat( + channelSwapQuote.PrepayAmount, + params.MaximumPrepayRoutingFeePPM, + ), + MaxSwapFee: channelSwapQuote.SwapFee, + MaxPrepayAmount: channelSwapQuote.PrepayAmount, + MaxMinerFee: params.MaximumMinerFee, + SweepConfTarget: params.SweepConfTarget, + OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()}, + Label: labels.AutoloopLabel(swap.TypeOut), + Initiator: autoloopSwapInitiator, + } + quotes = []quoteRequestResp{ + { + request: peerSwapQuoteRequest, + quote: peerSwapQuote, + }, + { + request: chanSwapQuoteRequest, + quote: channelSwapQuote, + }, + } + + loopOuts = []loopOutRequestResp{ + { + request: peerSwap, + response: &loop.LoopOutSwapInfo{ + SwapHash: lntypes.Hash{2}, + }, + }, + { + request: chanSwap, + response: &loop.LoopOutSwapInfo{ + SwapHash: lntypes.Hash{1}, + }, + }, + } + ) + + // Tick our autolooper with no existing swaps, we expect a loop out + // swap to be dispatched for each of our rules. We set our server side + // maximum to be greater than the swap amount for our peer swap (which + // is the larger of the two swaps). + c.autoloop(1, peerAmount+1, nil, quotes, loopOuts) + + c.stop() +} + // existingSwapFromRequest is a helper function which returns the db // representation of a loop out request with the event set provided. func existingSwapFromRequest(request *loop.OutRequest, initTime time.Time, diff --git a/liquidity/balances.go b/liquidity/balances.go index d729e02..f66aad0 100644 --- a/liquidity/balances.go +++ b/liquidity/balances.go @@ -19,8 +19,10 @@ type balances struct { // outgoing is the local balance of the channel. outgoing btcutil.Amount - // channelID is the channel that has these balances. - channelID lnwire.ShortChannelID + // channels is the channel that has these balances represent. This may + // be more than one channel in the case where we are examining a peer's + // liquidity as a whole. + channels []lnwire.ShortChannelID // pubkey is the public key of the peer we have this balances set with. pubkey route.Vertex @@ -29,10 +31,12 @@ type balances struct { // newBalances creates a balances struct from lndclient channel information. func newBalances(info lndclient.ChannelInfo) *balances { return &balances{ - capacity: info.Capacity, - incoming: info.RemoteBalance, - outgoing: info.LocalBalance, - channelID: lnwire.NewShortChanIDFromInt(info.ChannelID), - pubkey: info.PubKeyBytes, + capacity: info.Capacity, + incoming: info.RemoteBalance, + outgoing: info.LocalBalance, + channels: []lnwire.ShortChannelID{ + lnwire.NewShortChanIDFromInt(info.ChannelID), + }, + pubkey: info.PubKeyBytes, } } diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index c0007e7..278e9df 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -124,6 +124,7 @@ var ( AutoFeeBudget: defaultBudget, MaxAutoInFlight: defaultMaxInFlight, ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule), + PeerRules: make(map[route.Vertex]*ThresholdRule), FailureBackOff: defaultFailureBackoff, SweepFeeRateLimit: defaultSweepFeeRateLimit, SweepConfTarget: loop.DefaultSweepConfTarget, @@ -181,6 +182,11 @@ var ( // ErrNoRules is returned when no rules are set for swap suggestions. ErrNoRules = errors.New("no rules set for autoloop") + + // ErrExclusiveRules is returned when a set of rules that may not be + // set together are specified. + ErrExclusiveRules = errors.New("channel and peer rules must be " + + "exclusive") ) // Config contains the external functionality required to run the @@ -289,27 +295,41 @@ type Parameters struct { ClientRestrictions Restrictions // ChannelRules maps a short channel ID to a rule that describes how we - // would like liquidity to be managed. + // would like liquidity to be managed. These rules and PeerRules are + // exclusively set to prevent overlap between peer and channel rules. ChannelRules map[lnwire.ShortChannelID]*ThresholdRule + + // PeerRules maps a peer's pubkey to a rule that applies to all the + // channels that we have with the peer collectively. These rules and + // ChannelRules are exclusively set to prevent overlap between peer + // and channel rules map to avoid ambiguity. + PeerRules map[route.Vertex]*ThresholdRule } // String returns the string representation of our parameters. func (p Parameters) String() string { - channelRules := make([]string, 0, len(p.ChannelRules)) + ruleList := make([]string, 0, len(p.ChannelRules)+len(p.PeerRules)) for channel, rule := range p.ChannelRules { - channelRules = append( - channelRules, fmt.Sprintf("%v: %v", channel, rule), + ruleList = append( + ruleList, fmt.Sprintf("Channel: %v: %v", channel, rule), ) } - return fmt.Sprintf("channel rules: %v, failure backoff: %v, sweep "+ + for peer, rule := range p.PeerRules { + ruleList = append( + ruleList, fmt.Sprintf("Peer: %v: %v", peer, rule), + ) + + } + + return fmt.Sprintf("rules: %v, failure backoff: %v, sweep "+ "fee rate limit: %v, sweep conf target: %v, maximum prepay: "+ "%v, maximum miner fee: %v, maximum swap fee ppm: %v, maximum "+ "routing fee ppm: %v, maximum prepay routing fee ppm: %v, "+ "auto budget: %v, budget start: %v, max auto in flight: %v, "+ "minimum swap size=%v, maximum swap size=%v", - strings.Join(channelRules, ","), p.FailureBackOff, + strings.Join(ruleList, ","), p.FailureBackOff, p.SweepFeeRateLimit, p.SweepConfTarget, p.MaximumPrepay, p.MaximumMinerFee, p.MaximumSwapFeePPM, p.MaximumRoutingFeePPM, p.MaximumPrepayRoutingFeePPM, @@ -317,9 +337,54 @@ func (p Parameters) String() string { p.ClientRestrictions.Minimum, p.ClientRestrictions.Maximum) } -// validate checks whether a set of parameters is valid. It takes the minimum -// confirmations we allow for sweep confirmation target as a parameter. -func (p Parameters) validate(minConfs int32, server *Restrictions) error { +// haveRules returns a boolean indicating whether we have any rules configured. +func (p Parameters) haveRules() bool { + if len(p.ChannelRules) != 0 { + return true + } + + if len(p.PeerRules) != 0 { + return true + } + + return false +} + +// validate checks whether a set of parameters is valid. Our set of currently +// open channels are required to check that there is no overlap between the +// rules set on a per-peer level, and those set for specific channels. We can't +// allow both, because then we're trying to cater for two separate liquidity +// goals on the same channel. Since we use short channel ID, we don't need to +// worry about pending channels (users would need to work very hard to get the +// short channel ID for a pending channel). Likewise, we don't care about closed +// channels, since there is no action that may occur on them, and we want to +// allow peer-level rules to be set once a channel which had a specific rule +// has been closed. It takes the minimum confirmations we allow for sweep +// confirmation target as a parameter. +// TODO(carla): prune channels that have been closed from rules. +func (p Parameters) validate(minConfs int32, openChans []lndclient.ChannelInfo, + server *Restrictions) error { + + // First, we check that the rules on a per peer and per channel do not + // overlap, since this could lead to contractions. + for _, channel := range openChans { + // If we don't have a rule for the peer, there's no way we have + // an overlap between this peer and the channel. + _, ok := p.PeerRules[channel.PubKeyBytes] + if !ok { + continue + } + + shortID := lnwire.NewShortChanIDFromInt(channel.ChannelID) + _, ok = p.ChannelRules[shortID] + if ok { + log.Debugf("Rules for peer: %v and its channel: %v "+ + "can't both be set", channel.PubKeyBytes, shortID) + + return ErrExclusiveRules + } + } + for channel, rule := range p.ChannelRules { if channel.ToUint64() == 0 { return ErrZeroChannelID @@ -331,6 +396,13 @@ func (p Parameters) validate(minConfs int32, server *Restrictions) error { } } + for peer, rule := range p.PeerRules { + if err := rule.validate(); err != nil { + return fmt.Errorf("peer: %v has invalid rule: %v", + peer, err) + } + } + // Check that our sweep limit is above our minimum fee rate. We use // absolute fee floor rather than kw floor because we will allow users // to specify fee rate is sat/vByte and want to allow 1 sat/vByte. @@ -483,7 +555,12 @@ func (m *Manager) SetParameters(ctx context.Context, params Parameters) error { return err } - err = params.validate(m.cfg.MinimumConfirmations, restrictions) + channels, err := m.cfg.Lnd.Client.ListChannels(ctx) + if err != nil { + return err + } + + err = params.validate(m.cfg.MinimumConfirmations, channels, restrictions) if err != nil { return err } @@ -510,6 +587,16 @@ func cloneParameters(params Parameters) Parameters { paramCopy.ChannelRules[channel] = &ruleCopy } + paramCopy.PeerRules = make( + map[route.Vertex]*ThresholdRule, + len(params.PeerRules), + ) + + for peer, rule := range params.PeerRules { + ruleCopy := *rule + paramCopy.PeerRules[peer] = &ruleCopy + } + return paramCopy } @@ -566,11 +653,16 @@ type Suggestions struct { // 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 + + // Disqualified peers maps the set of peers that we do not recommend + // swaps for to the reason that they were excluded. + DisqualifiedPeers map[route.Vertex]Reason } func newSuggestions() *Suggestions { return &Suggestions{ DisqualifiedChans: make(map[lnwire.ShortChannelID]Reason), + DisqualifiedPeers: make(map[route.Vertex]Reason), } } @@ -595,6 +687,10 @@ func (m *Manager) singleReasonSuggestion(reason Reason) *Suggestions { resp.DisqualifiedChans[id] = reason } + for peer := range m.params.PeerRules { + resp.DisqualifiedPeers[peer] = reason + } + return resp } @@ -612,7 +708,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 { + if !m.params.haveRules() { return nil, ErrNoRules } @@ -699,19 +795,59 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( return nil, err } + peerChannels := make(map[route.Vertex]*balances) + for _, channel := range channels { + bal, ok := peerChannels[channel.PubKeyBytes] + if !ok { + bal = &balances{} + } + + chanID := lnwire.NewShortChanIDFromInt(channel.ChannelID) + bal.channels = append(bal.channels, chanID) + bal.capacity += channel.Capacity + bal.incoming += channel.RemoteBalance + bal.outgoing += channel.LocalBalance + bal.pubkey = channel.PubKeyBytes + + peerChannels[channel.PubKeyBytes] = bal + } + // Get a summary of the channels and peers that are not eligible due // to ongoing swaps. traffic := m.currentSwapTraffic(loopOut, loopIn) var ( - suggestions []swapSuggestion - disqualified = make(map[lnwire.ShortChannelID]Reason) + suggestions []swapSuggestion + resp = newSuggestions() ) + for peer, balances := range peerChannels { + rule, haveRule := m.params.PeerRules[peer] + if !haveRule { + continue + } + + suggestion, err := m.suggestSwap( + ctx, traffic, balances, rule, restrictions, autoloop, + ) + var reasonErr *reasonError + if errors.As(err, &reasonErr) { + resp.DisqualifiedPeers[peer] = reasonErr.reason + continue + } + + if err != nil { + return nil, err + } + + suggestions = append(suggestions, suggestion) + } + for _, channel := range channels { balance := newBalances(channel) - rule, ok := m.params.ChannelRules[balance.channelID] + channelID := lnwire.NewShortChanIDFromInt(channel.ChannelID) + rule, ok := m.params.ChannelRules[channelID] if !ok { continue } @@ -722,7 +858,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( var reasonErr *reasonError if errors.As(err, &reasonErr) { - disqualified[balance.channelID] = reasonErr.reason + resp.DisqualifiedChans[channelID] = reasonErr.reason continue } @@ -733,12 +869,6 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( suggestions = append(suggestions, suggestion) } - // 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 { @@ -813,7 +943,7 @@ func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic, autoloop bool) (swapSuggestion, error) { // Check whether we can perform a swap. - err := traffic.maySwap(balance.pubkey, balance.channelID) + err := traffic.maySwap(balance.pubkey, balance.channels) if err != nil { return nil, err } @@ -930,11 +1060,14 @@ func (m *Manager) makeLoopOutRequest(ctx context.Context, routeMaxFee := ppmToSat(amount, m.params.MaximumRoutingFeePPM) + var chanSet loopdb.ChannelSet + for _, channel := range balance.channels { + chanSet = append(chanSet, channel.ToUint64()) + } + request := loop.OutRequest{ - Amount: amount, - OutgoingChanSet: loopdb.ChannelSet{ - balance.channelID.ToUint64(), - }, + Amount: amount, + OutgoingChanSet: chanSet, MaxPrepayRoutingFee: prepayMaxFee, MaxSwapRoutingFee: routeMaxFee, MaxMinerFee: m.params.MaximumMinerFee, @@ -1143,21 +1276,23 @@ 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) error { + channels []lnwire.ShortChannelID) error { - 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) + for _, chanID := range channels { + 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 newReasonError(ReasonFailureBackoff) - } + return newReasonError(ReasonFailureBackoff) + } - if s.ongoingLoopOut[chanID] { - log.Debugf("Channel: %v not eligible for suggestions, "+ - "ongoing loop out utilizing channel", chanID) + if s.ongoingLoopOut[chanID] { + log.Debugf("Channel: %v not eligible for suggestions, "+ + "ongoing loop out utilizing channel", chanID) - return newReasonError(ReasonLoopOut) + return newReasonError(ReasonLoopOut) + } } if s.ongoingLoopIn[peer] { diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index 557cf22..c014f71 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -25,6 +25,7 @@ var ( chanID1 = lnwire.NewShortChanIDFromInt(1) chanID2 = lnwire.NewShortChanIDFromInt(2) + chanID3 = lnwire.NewShortChanIDFromInt(3) peer1 = route.Vertex{1} peer2 = route.Vertex{2} @@ -111,6 +112,10 @@ var ( // 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) + + // noPeersDisqualified can be used in tests where we don't have any + // disqualified peers so that we can use require.Equal. + noPeersDisqualified = make(map[route.Vertex]Reason) ) // newTestConfig creates a default test config. @@ -280,27 +285,36 @@ func TestRestrictedSuggestions(t *testing.T) { defaultFailureBackoff * -3, ), } + + chanRules = map[lnwire.ShortChannelID]*ThresholdRule{ + chanID1: chanRule, + chanID2: chanRule, + } ) tests := []struct { - name string - channels []lndclient.ChannelInfo - loopOut []*loopdb.LoopOut - loopIn []*loopdb.LoopIn - expected *Suggestions + name string + channels []lndclient.ChannelInfo + loopOut []*loopdb.LoopOut + loopIn []*loopdb.LoopIn + chanRules map[lnwire.ShortChannelID]*ThresholdRule + peerRules map[route.Vertex]*ThresholdRule + expected *Suggestions }{ { name: "no existing swaps", channels: []lndclient.ChannelInfo{ channel1, }, - loopOut: nil, - loopIn: nil, + loopOut: nil, + loopIn: nil, + chanRules: chanRules, expected: &Suggestions{ OutSwaps: []loop.OutRequest{ chan1Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -315,11 +329,13 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, + chanRules: chanRules, expected: &Suggestions{ OutSwaps: []loop.OutRequest{ chan1Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -334,11 +350,13 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, + chanRules: chanRules, expected: &Suggestions{ OutSwaps: []loop.OutRequest{ chan1Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -351,6 +369,7 @@ func TestRestrictedSuggestions(t *testing.T) { Contract: chan1Out, }, }, + chanRules: chanRules, expected: &Suggestions{ OutSwaps: []loop.OutRequest{ chan2Rec, @@ -358,6 +377,7 @@ func TestRestrictedSuggestions(t *testing.T) { DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID1: ReasonLoopOut, }, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -372,6 +392,7 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, + chanRules: chanRules, expected: &Suggestions{ OutSwaps: []loop.OutRequest{ chan1Rec, @@ -379,6 +400,7 @@ func TestRestrictedSuggestions(t *testing.T) { DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID2: ReasonLoopIn, }, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -396,10 +418,12 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, + chanRules: chanRules, expected: &Suggestions{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID1: ReasonFailureBackoff, }, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -417,11 +441,13 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, + chanRules: chanRules, expected: &Suggestions{ OutSwaps: []loop.OutRequest{ chan1Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -439,10 +465,36 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, + chanRules: chanRules, expected: &Suggestions{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID1: ReasonLoopOut, }, + DisqualifiedPeers: noPeersDisqualified, + }, + }, + { + name: "existing on peer's channel", + channels: []lndclient.ChannelInfo{ + channel1, + { + ChannelID: chanID3.ToUint64(), + PubKeyBytes: peer1, + }, + }, + loopOut: []*loopdb.LoopOut{ + { + Contract: chan1Out, + }, + }, + peerRules: map[route.Vertex]*ThresholdRule{ + peer1: NewThresholdRule(0, 50), + }, + expected: &Suggestions{ + DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: map[route.Vertex]Reason{ + peer1: ReasonLoopOut, + }, }, }, } @@ -464,9 +516,12 @@ func TestRestrictedSuggestions(t *testing.T) { lnd.Channels = testCase.channels params := defaultParameters - params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{ - chanID1: chanRule, - chanID2: chanRule, + if testCase.chanRules != nil { + params.ChannelRules = testCase.chanRules + } + + if testCase.peerRules != nil { + params.PeerRules = testCase.peerRules } testSuggestSwaps( @@ -493,6 +548,7 @@ func TestSweepFeeLimit(t *testing.T) { chan1Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -502,6 +558,7 @@ func TestSweepFeeLimit(t *testing.T) { DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID1: ReasonSweepFees, }, + DisqualifiedPeers: noPeersDisqualified, }, }, } @@ -537,19 +594,27 @@ func TestSweepFeeLimit(t *testing.T) { // TestSuggestSwaps tests getting of swap suggestions based on the rules set for // the liquidity manager and the current set of channel balances. func TestSuggestSwaps(t *testing.T) { + singleChannel := []lndclient.ChannelInfo{ + channel1, + } + tests := []struct { name string + channels []lndclient.ChannelInfo rules map[lnwire.ShortChannelID]*ThresholdRule + peerRules map[route.Vertex]*ThresholdRule suggestions *Suggestions err error }{ { - name: "no rules", - rules: map[lnwire.ShortChannelID]*ThresholdRule{}, - err: ErrNoRules, + name: "no rules", + channels: singleChannel, + rules: map[lnwire.ShortChannelID]*ThresholdRule{}, + err: ErrNoRules, }, { - name: "loop out", + name: "loop out", + channels: singleChannel, rules: map[lnwire.ShortChannelID]*ThresholdRule{ chanID1: chanRule, }, @@ -558,15 +623,76 @@ func TestSuggestSwaps(t *testing.T) { chan1Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { - name: "no rule for channel", + name: "no rule for channel", + channels: singleChannel, rules: map[lnwire.ShortChannelID]*ThresholdRule{ chanID2: NewThresholdRule(10, 10), }, suggestions: &Suggestions{ DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, + }, + }, + { + name: "multiple peer rules", + channels: []lndclient.ChannelInfo{ + { + PubKeyBytes: peer1, + ChannelID: chanID1.ToUint64(), + Capacity: 20000, + LocalBalance: 8000, + RemoteBalance: 12000, + }, + { + PubKeyBytes: peer1, + ChannelID: chanID2.ToUint64(), + Capacity: 10000, + LocalBalance: 9000, + RemoteBalance: 1000, + }, + { + PubKeyBytes: peer2, + ChannelID: chanID3.ToUint64(), + Capacity: 5000, + LocalBalance: 2000, + RemoteBalance: 3000, + }, + }, + peerRules: map[route.Vertex]*ThresholdRule{ + peer1: NewThresholdRule(80, 0), + peer2: NewThresholdRule(40, 50), + }, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + { + Amount: 10000, + OutgoingChanSet: loopdb.ChannelSet{ + chanID1.ToUint64(), + chanID2.ToUint64(), + }, + MaxPrepayRoutingFee: ppmToSat( + testQuote.PrepayAmount, + defaultPrepayRoutingFeePPM, + ), + MaxSwapRoutingFee: ppmToSat( + 10000, + defaultRoutingFeePPM, + ), + MaxMinerFee: defaultMaximumMinerFee, + MaxSwapFee: testQuote.SwapFee, + MaxPrepayAmount: testQuote.PrepayAmount, + SweepConfTarget: loop.DefaultSweepConfTarget, + Initiator: autoloopSwapInitiator, + }, + }, + DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: map[route.Vertex]Reason{ + peer2: ReasonLiquidityOk, + }, }, }, } @@ -577,12 +703,16 @@ func TestSuggestSwaps(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { cfg, lnd := newTestConfig() - lnd.Channels = []lndclient.ChannelInfo{ - channel1, - } + lnd.Channels = testCase.channels params := defaultParameters - params.ChannelRules = testCase.rules + if testCase.rules != nil { + params.ChannelRules = testCase.rules + } + + if testCase.peerRules != nil { + params.PeerRules = testCase.peerRules + } testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), @@ -607,6 +737,7 @@ func TestFeeLimits(t *testing.T) { chan1Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -620,6 +751,7 @@ func TestFeeLimits(t *testing.T) { DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID1: ReasonPrepay, }, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -633,6 +765,7 @@ func TestFeeLimits(t *testing.T) { DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID1: ReasonMinerFee, }, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -647,6 +780,7 @@ func TestFeeLimits(t *testing.T) { DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID1: ReasonSwapFee, }, + DisqualifiedPeers: noPeersDisqualified, }, }, } @@ -719,6 +853,7 @@ func TestFeeBudget(t *testing.T) { chan1Rec, chan2Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -734,6 +869,7 @@ func TestFeeBudget(t *testing.T) { DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID2: ReasonBudgetInsufficient, }, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -750,6 +886,7 @@ func TestFeeBudget(t *testing.T) { chan1Rec, chan2Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -768,6 +905,7 @@ func TestFeeBudget(t *testing.T) { DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID2: ReasonBudgetInsufficient, }, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -782,6 +920,7 @@ func TestFeeBudget(t *testing.T) { chanID1: ReasonBudgetElapsed, chanID2: ReasonBudgetElapsed, }, + DisqualifiedPeers: noPeersDisqualified, }, }, } @@ -875,6 +1014,7 @@ func TestInFlightLimit(t *testing.T) { chan1Rec, chan2Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -885,6 +1025,7 @@ func TestInFlightLimit(t *testing.T) { chan1Rec, chan2Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -902,6 +1043,7 @@ func TestInFlightLimit(t *testing.T) { DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID2: ReasonInFlight, }, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -917,6 +1059,7 @@ func TestInFlightLimit(t *testing.T) { chanID1: ReasonInFlight, chanID2: ReasonInFlight, }, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -935,6 +1078,7 @@ func TestInFlightLimit(t *testing.T) { chanID1: ReasonInFlight, chanID2: ReasonInFlight, }, + DisqualifiedPeers: noPeersDisqualified, }, }, } @@ -1026,6 +1170,7 @@ func TestSizeRestrictions(t *testing.T) { chan1Rec, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -1040,6 +1185,7 @@ func TestSizeRestrictions(t *testing.T) { DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID1: ReasonLiquidityOk, }, + DisqualifiedPeers: noPeersDisqualified, }, }, { @@ -1055,6 +1201,7 @@ func TestSizeRestrictions(t *testing.T) { outSwap, }, DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, }, }, { From 949e76bb2a0e80517cc19f94c71e822337c21c7f Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 16 Feb 2021 13:31:51 +0200 Subject: [PATCH 5/7] looprpc: add peer level rules to rpc --- loopd/swapclient_server.go | 95 +++++++--- looprpc/client.pb.go | 347 +++++++++++++++++++----------------- looprpc/client.proto | 12 ++ looprpc/client.swagger.json | 12 +- 4 files changed, 283 insertions(+), 183 deletions(-) diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index a4b9764..83fc521 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -574,6 +574,8 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context, satPerByte := cfg.SweepFeeRateLimit.FeePerKVByte() / 1000 + totalRules := len(cfg.ChannelRules) + len(cfg.PeerRules) + rpcCfg := &looprpc.LiquidityParameters{ MaxMinerFeeSat: uint64(cfg.MaximumMinerFee), MaxSwapFeePpm: uint64(cfg.MaximumSwapFeePPM), @@ -587,7 +589,7 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context, AutoloopBudgetSat: uint64(cfg.AutoFeeBudget), AutoMaxInFlight: uint64(cfg.MaxAutoInFlight), Rules: make( - []*looprpc.LiquidityRule, 0, len(cfg.ChannelRules), + []*looprpc.LiquidityRule, 0, totalRules, ), MinSwapAmount: uint64(cfg.ClientRestrictions.Minimum), MaxSwapAmount: uint64(cfg.ClientRestrictions.Maximum), @@ -602,19 +604,30 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context, } for channel, rule := range cfg.ChannelRules { - rpcRule := &looprpc.LiquidityRule{ - ChannelId: channel.ToUint64(), - Type: looprpc.LiquidityRuleType_THRESHOLD, - IncomingThreshold: uint32(rule.MinimumIncoming), - OutgoingThreshold: uint32(rule.MinimumOutgoing), - } + rpcRule := newRPCRule(channel.ToUint64(), nil, rule) + rpcCfg.Rules = append(rpcCfg.Rules, rpcRule) + } + for peer, rule := range cfg.PeerRules { + rpcRule := newRPCRule(0, peer[:], rule) rpcCfg.Rules = append(rpcCfg.Rules, rpcRule) } return rpcCfg, nil } +func newRPCRule(channelID uint64, peer []byte, + rule *liquidity.ThresholdRule) *looprpc.LiquidityRule { + + return &looprpc.LiquidityRule{ + ChannelId: channelID, + Pubkey: peer, + Type: looprpc.LiquidityRuleType_THRESHOLD, + IncomingThreshold: uint32(rule.MinimumIncoming), + OutgoingThreshold: uint32(rule.MinimumOutgoing), + } +} + // SetLiquidityParams attempts to set our current liquidity manager's // parameters. func (s *swapClientServer) SetLiquidityParams(ctx context.Context, @@ -640,7 +653,9 @@ func (s *swapClientServer) SetLiquidityParams(ctx context.Context, MaxAutoInFlight: int(in.Parameters.AutoMaxInFlight), ChannelRules: make( map[lnwire.ShortChannelID]*liquidity.ThresholdRule, - len(in.Parameters.Rules), + ), + PeerRules: make( + map[route.Vertex]*liquidity.ThresholdRule, ), ClientRestrictions: liquidity.Restrictions{ Minimum: btcutil.Amount(in.Parameters.MinSwapAmount), @@ -656,22 +671,47 @@ func (s *swapClientServer) SetLiquidityParams(ctx context.Context, } for _, rule := range in.Parameters.Rules { - var ( - shortID = lnwire.NewShortChanIDFromInt(rule.ChannelId) - err error - ) - - // Make sure that there are not multiple rules set for a single - // channel. - if _, ok := params.ChannelRules[shortID]; ok { - return nil, fmt.Errorf("multiple rules set for "+ - "channel: %v", shortID) - } + peerRule := rule.Pubkey != nil + chanRule := rule.ChannelId != 0 - params.ChannelRules[shortID], err = rpcToRule(rule) + liquidityRule, err := rpcToRule(rule) if err != nil { return nil, err } + + switch { + case peerRule && chanRule: + return nil, fmt.Errorf("cannot set channel: %v and "+ + "peer: %v fields in rule", rule.ChannelId, + rule.Pubkey) + + case peerRule: + pubkey, err := route.NewVertexFromBytes(rule.Pubkey) + if err != nil { + return nil, err + } + + if _, ok := params.PeerRules[pubkey]; ok { + return nil, fmt.Errorf("multiple rules set "+ + "for peer: %v", pubkey) + } + + params.PeerRules[pubkey] = liquidityRule + + case chanRule: + shortID := lnwire.NewShortChanIDFromInt(rule.ChannelId) + + if _, ok := params.ChannelRules[shortID]; ok { + return nil, fmt.Errorf("multiple rules set "+ + "for channel: %v", shortID) + } + + params.ChannelRules[shortID] = liquidityRule + + default: + return nil, errors.New("please set channel id or " + + "pubkey for rule") + } } if err := s.liquidityMgr.SetParameters(ctx, params); err != nil { @@ -743,6 +783,21 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context, Reason: autoloopReason, ChannelId: id.ToUint64(), } + + disqualified = append(disqualified, exclChan) + } + + for pubkey, reason := range suggestions.DisqualifiedPeers { + autoloopReason, err := rpcAutoloopReason(reason) + if err != nil { + return nil, err + } + + exclChan := &looprpc.Disqualified{ + Reason: autoloopReason, + Pubkey: pubkey[:], + } + disqualified = append(disqualified, exclChan) } diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index 7e72f33..8025431 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -1868,8 +1868,13 @@ func (m *LiquidityParameters) GetMaxSwapAmount() uint64 { type LiquidityRule struct { // //The short channel ID of the channel that this rule should be applied to. + //This field may not be set when the pubkey field is set. ChannelId uint64 `protobuf:"varint,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` // + //The public key of the peer that this rule should be applied to. This field + //may not be set when the channel id field is set. + Pubkey []byte `protobuf:"bytes,5,opt,name=pubkey,proto3" json:"pubkey,omitempty"` + // //Type indicates the type of rule that this message rule represents. Setting //this value will determine which fields are used in the message. The comments //on each field in this message will be prefixed with the LiquidityRuleType @@ -1920,6 +1925,13 @@ func (m *LiquidityRule) GetChannelId() uint64 { return 0 } +func (m *LiquidityRule) GetPubkey() []byte { + if m != nil { + return m.Pubkey + } + return nil +} + func (m *LiquidityRule) GetType() LiquidityRuleType { if m != nil { return m.Type @@ -2052,6 +2064,9 @@ 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 public key of the peer that was excluded from our suggestions. + Pubkey []byte `protobuf:"bytes,3,opt,name=pubkey,proto3" json:"pubkey,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:"-"` @@ -2091,6 +2106,13 @@ func (m *Disqualified) GetChannelId() uint64 { return 0 } +func (m *Disqualified) GetPubkey() []byte { + if m != nil { + return m.Pubkey + } + return nil +} + func (m *Disqualified) GetReason() AutoReason { if m != nil { return m.Reason @@ -2186,170 +2208,171 @@ func init() { func init() { proto.RegisterFile("client.proto", fileDescriptor_014de31d7ac8c57c) } var fileDescriptor_014de31d7ac8c57c = []byte{ - // 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, + // 2624 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x59, 0x4f, 0x73, 0xe3, 0xc6, + 0xb1, 0x5f, 0xfe, 0x13, 0xc9, 0x26, 0x48, 0x42, 0xa3, 0x5d, 0x89, 0xa2, 0x65, 0xaf, 0x16, 0xf6, + 0x3e, 0xcb, 0xb2, 0xbd, 0x7a, 0x96, 0x4f, 0x76, 0xd9, 0xaf, 0x8a, 0xa2, 0xa0, 0x15, 0xd7, 0x12, + 0x49, 0x83, 0xe4, 0xba, 0xf6, 0xd5, 0xab, 0x42, 0x8d, 0xc8, 0x91, 0x84, 0x32, 0xf1, 0x67, 0x81, + 0xe1, 0xae, 0x54, 0xae, 0x97, 0x54, 0xa5, 0xe2, 0x73, 0x0e, 0xf9, 0x06, 0xb9, 0xe7, 0x96, 0x5b, + 0x3e, 0x40, 0x2e, 0x39, 0x25, 0xb9, 0xe5, 0x9a, 0x4b, 0x0e, 0xf9, 0x0e, 0xa9, 0xe9, 0x01, 0x40, + 0x80, 0x22, 0xe5, 0xe4, 0x90, 0x9b, 0xd8, 0xfd, 0x9b, 0x9e, 0xe9, 0xff, 0xdd, 0x10, 0x28, 0xe3, + 0xa9, 0xc5, 0x1c, 0xfe, 0xcc, 0xf3, 0x5d, 0xee, 0x92, 0xe2, 0xd4, 0x75, 0x3d, 0xdf, 0x1b, 0x37, + 0x77, 0xae, 0x5c, 0xf7, 0x6a, 0xca, 0x0e, 0xa8, 0x67, 0x1d, 0x50, 0xc7, 0x71, 0x39, 0xe5, 0x96, + 0xeb, 0x04, 0x12, 0xa6, 0xfd, 0x36, 0x0f, 0xb5, 0x33, 0xd7, 0xf5, 0x7a, 0x33, 0x6e, 0xb0, 0xd7, + 0x33, 0x16, 0x70, 0xa2, 0x42, 0x8e, 0xda, 0xbc, 0x91, 0xd9, 0xcd, 0xec, 0xe5, 0x0c, 0xf1, 0x27, + 0x21, 0x90, 0x9f, 0xb0, 0x80, 0x37, 0xb2, 0xbb, 0x99, 0xbd, 0xb2, 0x81, 0x7f, 0x93, 0x03, 0x78, + 0x68, 0xd3, 0x1b, 0x33, 0x78, 0x4b, 0x3d, 0xd3, 0x77, 0x67, 0xdc, 0x72, 0xae, 0xcc, 0x4b, 0xc6, + 0x1a, 0x39, 0x3c, 0xb6, 0x6e, 0xd3, 0x9b, 0xc1, 0x5b, 0xea, 0x19, 0x92, 0x73, 0xc2, 0x18, 0xf9, + 0x1c, 0x36, 0xc5, 0x01, 0xcf, 0x67, 0x1e, 0xbd, 0x4d, 0x1d, 0xc9, 0xe3, 0x91, 0x0d, 0x9b, 0xde, + 0xf4, 0x91, 0x99, 0x38, 0xb4, 0x0b, 0x4a, 0x7c, 0x8b, 0x80, 0x16, 0x10, 0x0a, 0xa1, 0x74, 0x81, + 0xf8, 0x00, 0x6a, 0x09, 0xb1, 0xe2, 0xe1, 0x6b, 0x88, 0x51, 0x62, 0x71, 0x2d, 0x9b, 0x13, 0x0d, + 0xaa, 0x02, 0x65, 0x5b, 0x0e, 0xf3, 0x51, 0x50, 0x11, 0x41, 0x15, 0x9b, 0xde, 0x9c, 0x0b, 0x9a, + 0x90, 0xf4, 0x09, 0xa8, 0xc2, 0x66, 0xa6, 0x3b, 0xe3, 0xe6, 0xf8, 0x9a, 0x3a, 0x0e, 0x9b, 0x36, + 0x4a, 0xbb, 0x99, 0xbd, 0xfc, 0x51, 0xb6, 0x91, 0x31, 0x6a, 0x53, 0x69, 0xa5, 0xb6, 0xe4, 0x90, + 0x7d, 0x58, 0x77, 0x67, 0xfc, 0xca, 0x15, 0x4a, 0x08, 0xb4, 0x19, 0x30, 0xde, 0xa8, 0xec, 0xe6, + 0xf6, 0xf2, 0x46, 0x3d, 0x62, 0x08, 0xec, 0x80, 0x71, 0x81, 0x0d, 0xde, 0x32, 0xe6, 0x99, 0x63, + 0xd7, 0xb9, 0x34, 0x39, 0xf5, 0xaf, 0x18, 0x6f, 0x94, 0x77, 0x33, 0x7b, 0x05, 0xa3, 0x8e, 0x8c, + 0xb6, 0xeb, 0x5c, 0x0e, 0x91, 0x4c, 0x3e, 0x05, 0x72, 0xcd, 0xa7, 0x63, 0x84, 0x5a, 0xbe, 0x2d, + 0x9d, 0xd5, 0xa8, 0x22, 0x78, 0x5d, 0x70, 0xda, 0x49, 0x06, 0xf9, 0x12, 0xb6, 0xd1, 0x38, 0xde, + 0xec, 0x62, 0x6a, 0x8d, 0x91, 0x68, 0x4e, 0x18, 0x9d, 0x4c, 0x2d, 0x87, 0x35, 0x40, 0xbc, 0xde, + 0xd8, 0x12, 0x80, 0xfe, 0x9c, 0x7f, 0x1c, 0xb2, 0xc9, 0x43, 0x28, 0x4c, 0xe9, 0x05, 0x9b, 0x36, + 0x14, 0xf4, 0xab, 0xfc, 0x41, 0x76, 0xa0, 0x6c, 0x39, 0x16, 0xb7, 0x28, 0x77, 0xfd, 0x46, 0x0d, + 0x39, 0x73, 0x82, 0xf6, 0x63, 0x16, 0xaa, 0x22, 0x5e, 0x3a, 0xce, 0xea, 0x70, 0x59, 0x74, 0x5a, + 0xf6, 0x8e, 0xd3, 0xee, 0xb8, 0x23, 0x77, 0xd7, 0x1d, 0xdb, 0x50, 0x9a, 0xd2, 0x80, 0x9b, 0xd7, + 0xae, 0x87, 0x11, 0xa2, 0x18, 0x45, 0xf1, 0xfb, 0xd4, 0xf5, 0xc8, 0xfb, 0x50, 0x65, 0x37, 0x9c, + 0xf9, 0x0e, 0x9d, 0x9a, 0xc2, 0x24, 0x18, 0x16, 0x25, 0x43, 0x89, 0x88, 0xa7, 0x7c, 0x3a, 0x26, + 0x7b, 0xa0, 0xc6, 0x86, 0x8c, 0x6c, 0xbe, 0x86, 0x66, 0xac, 0x45, 0x66, 0x0c, 0x4d, 0x1e, 0xdb, + 0xa1, 0xb8, 0xd2, 0x0e, 0xa5, 0x45, 0x3b, 0xfc, 0x3d, 0x03, 0x0a, 0x06, 0x38, 0x0b, 0x3c, 0xd7, + 0x09, 0x18, 0x21, 0x90, 0xb5, 0x26, 0x68, 0x85, 0x32, 0xc6, 0x4b, 0xd6, 0x9a, 0x08, 0x15, 0xac, + 0x89, 0x79, 0x71, 0xcb, 0x59, 0x80, 0x1a, 0x2a, 0x46, 0xd1, 0x9a, 0x1c, 0x89, 0x9f, 0xe4, 0x29, + 0x28, 0xf8, 0x3a, 0x3a, 0x99, 0xf8, 0x2c, 0x08, 0x64, 0x6a, 0xe1, 0xc1, 0x8a, 0xa0, 0xb7, 0x24, + 0x99, 0x3c, 0x83, 0x8d, 0x24, 0xcc, 0x74, 0xbc, 0xc3, 0xb7, 0xc1, 0x35, 0xda, 0xa3, 0x2c, 0xc3, + 0x21, 0x44, 0x76, 0x91, 0x41, 0x3e, 0x09, 0xa3, 0x27, 0xc2, 0x4b, 0x78, 0x01, 0xe1, 0x6a, 0x02, + 0xde, 0x47, 0xf4, 0x53, 0xa8, 0x05, 0xcc, 0x7f, 0xc3, 0x7c, 0xd3, 0x66, 0x41, 0x40, 0xaf, 0x18, + 0x1a, 0xa8, 0x6c, 0x54, 0x25, 0xf5, 0x5c, 0x12, 0x35, 0x15, 0x6a, 0xe7, 0xae, 0x63, 0x71, 0xd7, + 0x0f, 0x7d, 0xae, 0xfd, 0x2e, 0x0f, 0x20, 0xb4, 0x1f, 0x70, 0xca, 0x67, 0xc1, 0xd2, 0x8a, 0x21, + 0xac, 0x91, 0x5d, 0x69, 0x8d, 0xca, 0xa2, 0x35, 0xf2, 0xfc, 0xd6, 0x93, 0x61, 0x50, 0x3b, 0x5c, + 0x7f, 0x16, 0xd6, 0xae, 0x67, 0xe2, 0x8e, 0xe1, 0xad, 0xc7, 0x0c, 0x64, 0x93, 0x3d, 0x28, 0x04, + 0x9c, 0x72, 0x59, 0x31, 0x6a, 0x87, 0x24, 0x85, 0x13, 0x6f, 0x61, 0x86, 0x04, 0x90, 0xaf, 0xa1, + 0x76, 0x49, 0xad, 0xe9, 0xcc, 0x67, 0xa6, 0xcf, 0x68, 0xe0, 0x3a, 0x18, 0xc9, 0xb5, 0xc3, 0xcd, + 0xf8, 0xc8, 0x89, 0x64, 0x1b, 0xc8, 0x35, 0xaa, 0x97, 0xc9, 0x9f, 0xe4, 0x43, 0xa8, 0x87, 0xae, + 0x16, 0xf9, 0xc4, 0x2d, 0x3b, 0xaa, 0x3c, 0xb5, 0x39, 0x79, 0x68, 0xd9, 0xe2, 0x45, 0x2a, 0x06, + 0xe9, 0xcc, 0x9b, 0x50, 0xce, 0x24, 0x52, 0xd6, 0x9f, 0x9a, 0xa0, 0x8f, 0x90, 0x8c, 0xc8, 0x45, + 0x87, 0x17, 0x97, 0x3b, 0x7c, 0xb9, 0x03, 0x95, 0x15, 0x0e, 0x5c, 0x11, 0x1e, 0xd5, 0x55, 0xe1, + 0xf1, 0x18, 0x2a, 0x63, 0x37, 0xe0, 0xa6, 0xf4, 0x2f, 0x46, 0x75, 0xce, 0x00, 0x41, 0x1a, 0x20, + 0x85, 0x3c, 0x01, 0x05, 0x01, 0xae, 0x33, 0xbe, 0xa6, 0x96, 0x83, 0x45, 0x2a, 0x67, 0xe0, 0xa1, + 0x9e, 0x24, 0x89, 0xe4, 0x93, 0x90, 0xcb, 0x4b, 0x89, 0x01, 0x59, 0x6f, 0x11, 0x13, 0xd2, 0xe6, + 0x29, 0x55, 0x4f, 0xa4, 0x94, 0x46, 0x40, 0x3d, 0xb3, 0x02, 0x2e, 0xbc, 0x15, 0x44, 0xa1, 0xf4, + 0x3f, 0xb0, 0x9e, 0xa0, 0x85, 0xc9, 0xf4, 0x11, 0x14, 0x44, 0xf5, 0x08, 0x1a, 0x99, 0xdd, 0xdc, + 0x5e, 0xe5, 0x70, 0xe3, 0x8e, 0xa3, 0x67, 0x81, 0x21, 0x11, 0xda, 0x13, 0xa8, 0x0b, 0x62, 0xc7, + 0xb9, 0x74, 0xa3, 0x8a, 0x54, 0x8b, 0x53, 0x51, 0x11, 0x81, 0xa7, 0xd5, 0x40, 0x19, 0x32, 0xdf, + 0x8e, 0xaf, 0xfc, 0x39, 0xd4, 0x3b, 0x4e, 0x48, 0x09, 0x2f, 0xfc, 0x2f, 0xa8, 0xdb, 0x96, 0x23, + 0x4b, 0x16, 0xb5, 0xdd, 0x99, 0xc3, 0x43, 0x87, 0x57, 0x6d, 0xcb, 0x11, 0xf2, 0x5b, 0x48, 0x44, + 0x5c, 0x54, 0xda, 0x42, 0xdc, 0x5a, 0x88, 0x93, 0xd5, 0x4d, 0xe2, 0x5e, 0xe4, 0x4b, 0x19, 0x35, + 0xfb, 0x22, 0x5f, 0xca, 0xaa, 0xb9, 0x17, 0xf9, 0x52, 0x4e, 0xcd, 0xbf, 0xc8, 0x97, 0xf2, 0x6a, + 0xe1, 0x45, 0xbe, 0x54, 0x54, 0x4b, 0xda, 0x1f, 0x33, 0xa0, 0xf6, 0x66, 0xfc, 0x3f, 0xfa, 0x04, + 0x6c, 0x8c, 0x96, 0x63, 0x8e, 0xa7, 0xfc, 0x8d, 0x39, 0x61, 0x53, 0x4e, 0xd1, 0xdd, 0x05, 0x43, + 0xb1, 0x2d, 0xa7, 0x3d, 0xe5, 0x6f, 0x8e, 0x05, 0x2d, 0x6a, 0x9f, 0x09, 0x54, 0x39, 0x44, 0xd1, + 0x9b, 0x18, 0xf5, 0x13, 0xea, 0xfc, 0x26, 0x03, 0xca, 0xb7, 0x33, 0x97, 0xb3, 0xd5, 0x2d, 0x01, + 0x03, 0x6f, 0x5e, 0x87, 0xb3, 0x78, 0x07, 0x8c, 0xe7, 0x35, 0xf8, 0x4e, 0x49, 0xcf, 0x2d, 0x29, + 0xe9, 0xf7, 0x36, 0xbb, 0xfc, 0xbd, 0xcd, 0x4e, 0xfb, 0x55, 0x46, 0x78, 0x3d, 0x7c, 0x66, 0x68, + 0xf2, 0x5d, 0x50, 0xa2, 0x26, 0x65, 0x06, 0x34, 0x7a, 0x30, 0x04, 0xb2, 0x4b, 0x0d, 0x28, 0x4e, + 0x39, 0x98, 0x60, 0x78, 0x63, 0x70, 0x1d, 0x23, 0xc3, 0x29, 0x47, 0xf0, 0xfa, 0x92, 0x15, 0x1e, + 0x78, 0x17, 0x20, 0x61, 0xcb, 0x02, 0xea, 0x59, 0x1e, 0x27, 0x0c, 0x29, 0x4d, 0x98, 0x57, 0x0b, + 0xda, 0x9f, 0x64, 0x14, 0xfc, 0xbb, 0x4f, 0xfa, 0x00, 0x6a, 0xf3, 0x61, 0x07, 0x31, 0xb2, 0xbf, + 0x2a, 0x5e, 0x34, 0xed, 0x08, 0xd4, 0xc7, 0x61, 0x1d, 0x91, 0x73, 0x47, 0xfa, 0xd9, 0x75, 0xc1, + 0x19, 0x08, 0x46, 0x28, 0x12, 0xe7, 0x13, 0x61, 0x57, 0x7a, 0x6b, 0x33, 0x87, 0x9b, 0x38, 0xec, + 0xc9, 0x9e, 0x5b, 0x47, 0x7b, 0x4a, 0xfa, 0xb1, 0xf0, 0xed, 0xfd, 0x0a, 0x6a, 0x75, 0xa8, 0x0e, + 0xdd, 0xef, 0x99, 0x13, 0x27, 0xdb, 0x57, 0x50, 0x8b, 0x08, 0xa1, 0x8a, 0xfb, 0xb0, 0xc6, 0x91, + 0x12, 0x66, 0xf7, 0xbc, 0x8c, 0x9f, 0x05, 0x94, 0x23, 0xd8, 0x08, 0x11, 0xda, 0xef, 0xb3, 0x50, + 0x8e, 0xa9, 0x22, 0x48, 0x2e, 0x68, 0xc0, 0x4c, 0x9b, 0x8e, 0xa9, 0xef, 0xba, 0x4e, 0x98, 0xe3, + 0x8a, 0x20, 0x9e, 0x87, 0x34, 0x51, 0xc2, 0x22, 0x3d, 0xae, 0x69, 0x70, 0x8d, 0xd6, 0x51, 0x8c, + 0x4a, 0x48, 0x3b, 0xa5, 0xc1, 0x35, 0xf9, 0x08, 0xd4, 0x08, 0xe2, 0xf9, 0xcc, 0xb2, 0x45, 0xe7, + 0x93, 0xfd, 0xb9, 0x1e, 0xd2, 0xfb, 0x21, 0x59, 0x14, 0x78, 0x99, 0x64, 0xa6, 0x47, 0xad, 0x89, + 0x69, 0x0b, 0x2b, 0xca, 0x79, 0xb5, 0x26, 0xe9, 0x7d, 0x6a, 0x4d, 0xce, 0x03, 0xca, 0xc9, 0x67, + 0xf0, 0x28, 0x31, 0xd4, 0x26, 0xe0, 0x32, 0x8b, 0x89, 0x1f, 0x4f, 0xb5, 0xf1, 0x91, 0x27, 0xa0, + 0x88, 0x8e, 0x61, 0x8e, 0x7d, 0x46, 0x39, 0x9b, 0x84, 0x79, 0x5c, 0x11, 0xb4, 0xb6, 0x24, 0x91, + 0x06, 0x14, 0xd9, 0x8d, 0x67, 0xf9, 0x6c, 0x82, 0x1d, 0xa3, 0x64, 0x44, 0x3f, 0xc5, 0xe1, 0x80, + 0xbb, 0x3e, 0xbd, 0x62, 0xa6, 0x43, 0x6d, 0x16, 0x8e, 0x28, 0x95, 0x90, 0xd6, 0xa5, 0x36, 0xd3, + 0xde, 0x81, 0xed, 0xe7, 0x8c, 0x9f, 0x59, 0xaf, 0x67, 0xd6, 0xc4, 0xe2, 0xb7, 0x7d, 0xea, 0xd3, + 0x79, 0x15, 0xfc, 0x43, 0x01, 0x36, 0xd2, 0x2c, 0xc6, 0x99, 0x2f, 0x3a, 0x50, 0xc1, 0x9f, 0x4d, + 0x59, 0xe4, 0x9d, 0x79, 0xc7, 0x8c, 0xc1, 0xc6, 0x6c, 0xca, 0x0c, 0x09, 0x22, 0x5f, 0xc3, 0xce, + 0x3c, 0xc4, 0x7c, 0xd1, 0x03, 0x03, 0xca, 0x4d, 0x8f, 0xf9, 0xe6, 0x1b, 0xd1, 0xe9, 0xd1, 0xfa, + 0x98, 0x95, 0x32, 0xda, 0x0c, 0xca, 0x45, 0xc4, 0xf5, 0x99, 0xff, 0x52, 0xb0, 0xc9, 0x87, 0xa0, + 0x26, 0x47, 0x45, 0xd3, 0xf3, 0x6c, 0xf4, 0x44, 0x3e, 0xae, 0x66, 0xc2, 0x5e, 0x9e, 0x4d, 0x3e, + 0x05, 0xb1, 0x1f, 0x98, 0x29, 0x0b, 0x7b, 0x76, 0x98, 0xf4, 0x42, 0xc6, 0x7c, 0x69, 0x10, 0xf0, + 0x2f, 0xa1, 0xb9, 0x7c, 0xd9, 0xc0, 0x53, 0x05, 0x3c, 0xb5, 0xb9, 0x64, 0xe1, 0x10, 0x67, 0xd3, + 0x1b, 0x85, 0xf0, 0xe0, 0x1a, 0xe2, 0xe7, 0x1b, 0x85, 0xc8, 0x99, 0x8f, 0x60, 0x3d, 0x35, 0xc2, + 0x22, 0xb0, 0x88, 0xc0, 0x5a, 0x62, 0x8c, 0x8d, 0xd3, 0x6b, 0x71, 0xfc, 0x2f, 0x2d, 0x1f, 0xff, + 0x9f, 0xc1, 0x46, 0x34, 0xb8, 0x5c, 0xd0, 0xf1, 0xf7, 0xee, 0xe5, 0xa5, 0x19, 0xb0, 0x31, 0x16, + 0xe5, 0xbc, 0xb1, 0x1e, 0xb2, 0x8e, 0x24, 0x67, 0xc0, 0xc6, 0xa4, 0x09, 0x25, 0x3a, 0xe3, 0xae, + 0xf0, 0x11, 0x36, 0xe2, 0x92, 0x11, 0xff, 0x16, 0xb2, 0xa2, 0xbf, 0xcd, 0x8b, 0xd9, 0xe4, 0x8a, + 0xc9, 0x72, 0x51, 0x91, 0xb2, 0x22, 0xd6, 0x11, 0x72, 0xc4, 0x3b, 0xbf, 0x80, 0xed, 0x3b, 0x78, + 0x4e, 0x7d, 0x8e, 0x2f, 0x50, 0xa4, 0xcd, 0x16, 0x4e, 0x09, 0xb6, 0x78, 0xc6, 0xc7, 0x40, 0x04, + 0xc7, 0x14, 0x26, 0xb1, 0x1c, 0xf3, 0x72, 0x6a, 0x5d, 0x5d, 0x73, 0x9c, 0x43, 0xf2, 0x46, 0x5d, + 0x70, 0xce, 0xe9, 0x4d, 0xc7, 0x39, 0x41, 0xf2, 0xb2, 0x4e, 0x57, 0x0b, 0x7d, 0xfe, 0x53, 0x9d, + 0xae, 0x9e, 0x8a, 0x0d, 0x89, 0xd3, 0xfe, 0x92, 0x81, 0x6a, 0x2a, 0x38, 0xb1, 0x48, 0xc9, 0x3d, + 0xcd, 0x0c, 0x27, 0x81, 0xbc, 0x51, 0x0e, 0x29, 0x9d, 0x09, 0xd9, 0x84, 0x35, 0x6f, 0x76, 0xf1, + 0x3d, 0xbb, 0xc5, 0x48, 0x50, 0x8c, 0xf0, 0x17, 0x79, 0x16, 0x8e, 0xa1, 0x59, 0x9c, 0x15, 0x9b, + 0xcb, 0x23, 0x3f, 0x31, 0x8f, 0x7e, 0x0a, 0xc4, 0x72, 0xc6, 0xae, 0x2d, 0x62, 0x8b, 0x5f, 0xfb, + 0x2c, 0xb8, 0x76, 0xa7, 0x13, 0x8c, 0xdf, 0xaa, 0xb1, 0x1e, 0x71, 0x86, 0x11, 0x43, 0xc0, 0xe3, + 0x95, 0x71, 0x0e, 0xcf, 0x4b, 0x78, 0xc4, 0x89, 0xe1, 0xda, 0x2b, 0xd8, 0x1e, 0xac, 0xca, 0x5e, + 0xf2, 0x15, 0x80, 0x17, 0xe7, 0x2c, 0x6a, 0x58, 0x39, 0xdc, 0xb9, 0xfb, 0xe0, 0x79, 0x5e, 0x1b, + 0x09, 0xbc, 0xb6, 0x03, 0xcd, 0x65, 0xa2, 0x65, 0x81, 0xd6, 0x1e, 0xc1, 0xc6, 0x60, 0x76, 0x75, + 0xc5, 0x16, 0x26, 0x35, 0x1f, 0x94, 0x63, 0x2b, 0x78, 0x3d, 0xa3, 0x53, 0xeb, 0xd2, 0x62, 0x93, + 0x7f, 0xdd, 0xc8, 0xb9, 0x94, 0x91, 0x3f, 0x86, 0xb5, 0x70, 0x24, 0x97, 0x66, 0x9e, 0x0f, 0x77, + 0xad, 0x19, 0x77, 0xc3, 0x79, 0x3c, 0x84, 0x68, 0x3f, 0x66, 0xe0, 0x61, 0xfa, 0x2d, 0x61, 0x13, + 0x39, 0x84, 0x52, 0xb4, 0xac, 0x87, 0x85, 0x6a, 0x6b, 0xae, 0x7d, 0xea, 0x7b, 0x86, 0x51, 0x0c, + 0x37, 0x77, 0xf2, 0x05, 0x28, 0x93, 0x84, 0x02, 0x8d, 0x2c, 0x9e, 0x7b, 0x14, 0x9f, 0x4b, 0x6a, + 0x67, 0xa4, 0xa0, 0xfb, 0x4f, 0xa1, 0x14, 0xed, 0x22, 0x44, 0x81, 0xd2, 0x59, 0xaf, 0xd7, 0x37, + 0x7b, 0xa3, 0xa1, 0xfa, 0x80, 0x54, 0xa0, 0x88, 0xbf, 0x3a, 0x5d, 0x35, 0xb3, 0x1f, 0x40, 0x39, + 0x5e, 0x45, 0x48, 0x15, 0xca, 0x9d, 0x6e, 0x67, 0xd8, 0x69, 0x0d, 0xf5, 0x63, 0xf5, 0x01, 0x79, + 0x04, 0xeb, 0x7d, 0x43, 0xef, 0x9c, 0xb7, 0x9e, 0xeb, 0xa6, 0xa1, 0xbf, 0xd4, 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, + 0x69, 0x75, 0x04, 0x38, 0x4f, 0x36, 0xa0, 0xde, 0xe9, 0xbe, 0xec, 0x75, 0xda, 0xba, 0x39, 0xd0, + 0x87, 0x43, 0x41, 0x2c, 0xec, 0xff, 0x23, 0x03, 0xd5, 0xd4, 0x36, 0x43, 0xb6, 0x60, 0x43, 0x1c, + 0x19, 0x19, 0xe2, 0xa6, 0xd6, 0xa0, 0xd7, 0x35, 0xbb, 0xbd, 0xae, 0xae, 0x3e, 0x20, 0xef, 0xc0, + 0xd6, 0x02, 0xa3, 0x77, 0x72, 0xd2, 0x3e, 0x6d, 0x89, 0xc7, 0x93, 0x26, 0x6c, 0x2e, 0x30, 0x87, + 0x9d, 0x73, 0x5d, 0x68, 0x99, 0x25, 0xbb, 0xb0, 0xb3, 0xc0, 0x1b, 0x7c, 0xa7, 0xeb, 0xfd, 0x18, + 0x91, 0x23, 0x4f, 0xe1, 0xc9, 0x02, 0xa2, 0xd3, 0x1d, 0x8c, 0x4e, 0x4e, 0x3a, 0xed, 0x8e, 0xde, + 0x1d, 0x9a, 0x2f, 0x5b, 0x67, 0x23, 0x5d, 0xcd, 0x93, 0x1d, 0x68, 0x2c, 0x5e, 0xa2, 0x9f, 0xf7, + 0x7b, 0x46, 0xcb, 0x78, 0xa5, 0x16, 0xc8, 0xfb, 0xf0, 0xf8, 0x8e, 0x90, 0x76, 0xcf, 0x30, 0xf4, + 0xf6, 0xd0, 0x6c, 0x9d, 0xf7, 0x46, 0xdd, 0xa1, 0xba, 0xb6, 0x7f, 0x20, 0x36, 0x86, 0x85, 0x84, + 0x14, 0x26, 0x1b, 0x75, 0xbf, 0xe9, 0xf6, 0xbe, 0xeb, 0xaa, 0x0f, 0x84, 0xe5, 0x87, 0xa7, 0x86, + 0x3e, 0x38, 0xed, 0x9d, 0x1d, 0xab, 0x99, 0xfd, 0x5f, 0xe6, 0x00, 0xe6, 0xb1, 0x25, 0xac, 0xd3, + 0x1a, 0x0d, 0x7b, 0xd1, 0x0d, 0xf3, 0x63, 0x1a, 0xbc, 0x97, 0x64, 0x1c, 0x8d, 0x8e, 0x9f, 0xeb, + 0x43, 0xb3, 0xdb, 0x1b, 0x9a, 0x83, 0x61, 0xcb, 0x18, 0xa2, 0xbb, 0x9a, 0xb0, 0x99, 0xc4, 0x48, + 0x2b, 0x9c, 0xe8, 0xfa, 0x40, 0xcd, 0x92, 0xf7, 0xa0, 0xb9, 0xe4, 0xbc, 0x7e, 0xd6, 0xea, 0x0f, + 0xf4, 0x63, 0x35, 0x47, 0xb6, 0xe1, 0x51, 0x92, 0xdf, 0xe9, 0x9a, 0x27, 0x67, 0x9d, 0xe7, 0xa7, + 0x43, 0x35, 0x4f, 0x1a, 0xf0, 0x30, 0x2d, 0xb6, 0x85, 0x52, 0xd5, 0xc2, 0xe2, 0xa1, 0xf3, 0x4e, + 0x57, 0x37, 0x90, 0xb5, 0x46, 0x36, 0x81, 0x24, 0x59, 0x7d, 0x43, 0xef, 0xb7, 0x5e, 0xa9, 0x45, + 0xf2, 0x18, 0xde, 0x49, 0xd2, 0x23, 0x8b, 0x1e, 0xb5, 0xda, 0xdf, 0xf4, 0x4e, 0x4e, 0xd4, 0xd2, + 0xe2, 0x6d, 0x71, 0x34, 0x97, 0x17, 0x6d, 0x13, 0x45, 0x36, 0x08, 0xbf, 0xa5, 0x18, 0x9d, 0x6f, + 0x47, 0x9d, 0xe3, 0xce, 0xf0, 0x95, 0xd9, 0xfb, 0x46, 0xad, 0x08, 0xbf, 0x2d, 0xd1, 0x3c, 0x19, + 0x00, 0xaa, 0x72, 0xf8, 0xd7, 0xb2, 0xfc, 0x68, 0xd0, 0xc6, 0xcf, 0x94, 0xc4, 0x80, 0x62, 0x98, + 0xa8, 0x64, 0x55, 0xea, 0x36, 0x1f, 0xa5, 0x16, 0xbf, 0xb8, 0x40, 0x6d, 0xfd, 0xe2, 0xcf, 0x7f, + 0xfb, 0x75, 0x76, 0x5d, 0x53, 0x0e, 0xde, 0x7c, 0x76, 0x20, 0x10, 0x07, 0xee, 0x8c, 0x7f, 0x99, + 0xd9, 0x27, 0x3d, 0x58, 0x93, 0x1f, 0xa7, 0xc8, 0x66, 0x4a, 0x64, 0xfc, 0xb5, 0x6a, 0x95, 0xc4, + 0x4d, 0x94, 0xa8, 0x6a, 0x95, 0x58, 0xa2, 0xe5, 0x08, 0x81, 0x5f, 0x40, 0x31, 0xfc, 0xf4, 0x91, + 0x78, 0x64, 0xfa, 0x63, 0x48, 0x73, 0xd9, 0x76, 0xfa, 0xdf, 0x19, 0xf2, 0xbf, 0x50, 0x8e, 0x17, + 0x5b, 0xb2, 0x9d, 0x28, 0xcd, 0xe9, 0xb2, 0xda, 0x6c, 0x2e, 0x63, 0xa5, 0x9f, 0x45, 0x6a, 0xf1, + 0xb3, 0x70, 0xe9, 0x25, 0x23, 0x59, 0x8e, 0xc4, 0xd2, 0x4b, 0x1a, 0xa9, 0xeb, 0x13, 0x7b, 0xf0, + 0xd2, 0x87, 0x69, 0x4d, 0x14, 0xf9, 0x90, 0x90, 0x94, 0xc8, 0x83, 0x1f, 0xac, 0xc9, 0xff, 0x93, + 0xff, 0x03, 0x25, 0x74, 0x00, 0xae, 0xa6, 0x64, 0x6e, 0xac, 0xe4, 0xfe, 0xdc, 0x9c, 0x2b, 0xb3, + 0xb8, 0xc4, 0x2e, 0x91, 0xee, 0xce, 0xf8, 0x01, 0x47, 0x69, 0x17, 0xb1, 0x74, 0x5c, 0x79, 0x12, + 0xd2, 0x93, 0xcb, 0x63, 0x5a, 0x7a, 0x6a, 0x39, 0xd2, 0x76, 0x51, 0x7a, 0x93, 0x34, 0x52, 0xd2, + 0x5f, 0x0b, 0xcc, 0xc1, 0x0f, 0xd4, 0xe6, 0x42, 0x83, 0x9a, 0x98, 0x78, 0xd1, 0xe5, 0xf7, 0xea, + 0x30, 0xb7, 0xda, 0xc2, 0xa7, 0x00, 0x6d, 0x1b, 0x2f, 0xd9, 0x20, 0xeb, 0x89, 0x50, 0x88, 0x35, + 0x98, 0x4b, 0xbf, 0x57, 0x87, 0xa4, 0xf4, 0xb4, 0x0a, 0x8f, 0x51, 0xfa, 0x36, 0xd9, 0x4a, 0x4a, + 0x4f, 0x6a, 0xf0, 0x0a, 0xaa, 0xe2, 0x8e, 0x68, 0xe7, 0x09, 0x12, 0x91, 0x9c, 0x5a, 0xac, 0x9a, + 0x5b, 0x77, 0xe8, 0xe9, 0xec, 0x20, 0x75, 0xbc, 0x22, 0xa0, 0xfc, 0x40, 0x2e, 0x53, 0x84, 0x03, + 0xb9, 0xbb, 0x0e, 0x10, 0x2d, 0x96, 0xb3, 0x72, 0x57, 0x68, 0xde, 0x3b, 0x59, 0x68, 0x3b, 0x78, + 0xe1, 0x26, 0x79, 0x88, 0x17, 0x46, 0x80, 0x03, 0x4f, 0xca, 0xff, 0x19, 0x90, 0xc1, 0x7d, 0xb7, + 0xae, 0x9c, 0x71, 0x9a, 0xef, 0xdf, 0x8b, 0x49, 0x1b, 0x54, 0x5b, 0x7a, 0xb9, 0x48, 0x61, 0x06, + 0x4a, 0x72, 0x82, 0x20, 0x73, 0x5d, 0x96, 0x0c, 0x39, 0xcd, 0x77, 0x57, 0x70, 0xc3, 0xdb, 0x1a, + 0x78, 0x1b, 0x21, 0xaa, 0xb8, 0x4d, 0xcc, 0xb5, 0x07, 0x81, 0x84, 0x5d, 0xac, 0xe1, 0xff, 0x53, + 0x3e, 0xff, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe6, 0x11, 0x1a, 0x17, 0x86, 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 482de67..cb39798 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -832,9 +832,16 @@ enum LiquidityRuleType { message LiquidityRule { /* The short channel ID of the channel that this rule should be applied to. + This field may not be set when the pubkey field is set. */ uint64 channel_id = 1; + /* + The public key of the peer that this rule should be applied to. This field + may not be set when the channel id field is set. + */ + bytes pubkey = 5; + /* Type indicates the type of rule that this message rule represents. Setting this value will determine which fields are used in the message. The comments @@ -952,6 +959,11 @@ message Disqualified { The short channel ID of the channel that was excluded from our suggestions. */ uint64 channel_id = 1; + + /* + The public key of the peer that was excluded from our suggestions. + */ + bytes pubkey = 3; /* The reason that we excluded the channel from the our suggestions. diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index ca9550c..b402ac1 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -423,6 +423,11 @@ "format": "uint64", "description": "The short channel ID of the channel that was excluded from our suggestions." }, + "pubkey": { + "type": "string", + "format": "byte", + "description": "The public key of the peer that was excluded from our suggestions." + }, "reason": { "$ref": "#/definitions/looprpcAutoReason", "description": "The reason that we excluded the channel from the our suggestions." @@ -566,7 +571,12 @@ "channel_id": { "type": "string", "format": "uint64", - "description": "The short channel ID of the channel that this rule should be applied to." + "description": "The short channel ID of the channel that this rule should be applied to.\nThis field may not be set when the pubkey field is set." + }, + "pubkey": { + "type": "string", + "format": "byte", + "description": "The public key of the peer that this rule should be applied to. This field\nmay not be set when the channel id field is set." }, "type": { "$ref": "#/definitions/looprpcLiquidityRuleType", From b9aae4f8f914e41149d91376d3df110b4a62243e Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 16 Feb 2021 13:31:52 +0200 Subject: [PATCH 6/7] loop: add peer rules to set rule command --- cmd/loop/liquidity.go | 49 ++++++++++++++++++++++++++++---------- loopd/swapclient_server.go | 1 + 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/cmd/loop/liquidity.go b/cmd/loop/liquidity.go index f3b823a..63b491a 100644 --- a/cmd/loop/liquidity.go +++ b/cmd/loop/liquidity.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "errors" "fmt" @@ -8,6 +9,7 @@ import ( "github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/looprpc" + "github.com/lightningnetwork/lnd/routing/route" "github.com/urfave/cli" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -42,9 +44,9 @@ func getParams(ctx *cli.Context) error { var setLiquidityRuleCommand = cli.Command{ Name: "setrule", - Usage: "set liquidity manager rule for a channel", - Description: "Update or remove the liquidity rule for a channel.", - ArgsUsage: "shortchanid", + Usage: "set liquidity manager rule for a channel/peer", + Description: "Update or remove the liquidity rule for a channel/peer.", + ArgsUsage: "{shortchanid | peerpubkey}", Flags: []cli.Flag{ cli.IntFlag{ Name: "incoming_threshold", @@ -58,8 +60,9 @@ var setLiquidityRuleCommand = cli.Command{ "that we do not want to drop below.", }, cli.BoolFlag{ - Name: "clear", - Usage: "remove the rule currently set for the channel.", + Name: "clear", + Usage: "remove the rule currently set for the " + + "channel/peer.", }, }, Action: setRule, @@ -68,13 +71,22 @@ var setLiquidityRuleCommand = cli.Command{ func setRule(ctx *cli.Context) error { // We require that a channel ID is set for this rule update. if ctx.NArg() != 1 { - return fmt.Errorf("please set a channel id for the rule " + - "update") + return fmt.Errorf("please set a channel id or peer pubkey " + + "for the rule update") } + var ( + pubkey route.Vertex + pubkeyRule bool + ) chanID, err := strconv.ParseUint(ctx.Args().First(), 10, 64) if err != nil { - return fmt.Errorf("could not parse channel ID: %v", err) + pubkey, err = route.NewVertexFromStr(ctx.Args().First()) + if err != nil { + return fmt.Errorf("please provide a valid pubkey: "+ + "%v, or short channel ID", err) + } + pubkeyRule = true } client, cleanup, err := getClient(ctx) @@ -101,11 +113,20 @@ func setRule(ctx *cli.Context) error { ) // Run through our current set of rules and check whether we have a rule - // currently set for this channel. We also track a slice containing all - // of the rules we currently have set for other channels, because we - // want to leave these rules untouched. + // currently set for this channel or peer. We also track a slice + // containing all of the rules we currently have set for other channels, + // and peers because we want to leave these rules untouched. for _, rule := range params.Rules { - if rule.ChannelId == chanID { + var ( + channelRuleSet = rule.ChannelId != 0 && + rule.ChannelId == chanID + + peerRuleSet = rule.Pubkey != nil && bytes.Equal( + rule.Pubkey, pubkey[:], + ) + ) + + if channelRuleSet || peerRuleSet { ruleSet = true } else { otherRules = append(otherRules, rule) @@ -149,6 +170,10 @@ func setRule(ctx *cli.Context) error { Type: looprpc.LiquidityRuleType_THRESHOLD, } + if pubkeyRule { + newRule.Pubkey = pubkey[:] + } + if inboundSet { newRule.IncomingThreshold = uint32( ctx.Int("incoming_threshold"), diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 83fc521..d0e1e32 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -609,6 +609,7 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context, } for peer, rule := range cfg.PeerRules { + peer := peer rpcRule := newRPCRule(0, peer[:], rule) rpcCfg.Rules = append(rpcCfg.Rules, rpcRule) } From b39310269666e0ee689f20556dfadd238c9084b5 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 16 Feb 2021 13:31:53 +0200 Subject: [PATCH 7/7] multi: update docs and release notes to include peer level management --- docs/autoloop.md | 40 ++++++++++++++++++++++++---------------- release_notes.md | 5 +++++ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/docs/autoloop.md b/docs/autoloop.md index 63fc5bf..bc09324 100644 --- a/docs/autoloop.md +++ b/docs/autoloop.md @@ -23,27 +23,35 @@ Note that autoloop parameters and rules are not persisted, so must be set on restart. We recommend running loopd with `--debuglevel=debug` when using this feature. -### Channel Thresholds -To setup the autolooper to dispatch swaps on your behalf, you need to tell it -which channels you would like it to perform swaps on, and the liquidity balance -you would like on each channel. Desired liqudity balance is expressed using -threshold incoming and outgoing percentages of channel capacity. The incoming -threshold you specify indicates the minimum percentage of your channel capacity -that you would like in incoming capacity. The outgoing thresold allows you to -reserve a percentage of your balance for outgoing capacity, but may be set to -zero if you are only concerned with incoming capcity. - -The autolooper will perform swaps that push your incoming channel capacity to -at least the incoming threshold you specify, while reserving at least the -outgoing capacity threshold. Rules can be set as follows: +### Liquidity Targets +Autoloop can be configured to manage liquidity for individual channels, or for +a peer as a whole. Peer-level liquidity management will examine the liquidity +balance of all the channels you have with a peer. This differs from channel-level +liquidity, where each channel's individual balance is checked. Note that if you +set a liquidity rule for a peer, you cannot also set a specific rule for one of +its channels. + +### Liqudity Thresholds +To setup the autolooper to dispatch swaps on your behalf, you need to set the +liquidity balance you would like for each channel or peer. Desired liquidity +balance is expressed using threshold incoming and outgoing percentages of +capacity. The incoming threshold you specify indicates the minimum percentage +of your capacity that you would like in incoming capacity. The outgoing +threshold allows you to reserve a percentage of your balance for outgoing +capacity, but may be set to zero if you are only concerned with incoming +capacity. + +The autolooper will perform swaps that push your incoming capacity to at least +the incoming threshold you specify, while reserving at least the outgoing +capacity threshold. Rules can be set as follows: ``` -loop setrule {short channel id} --incoming_threshold={minimum % incoming} --outgoing_threshold={minimum % outgoing} +loop setrule {short channel id/ peer pubkey} --incoming_threshold={minimum % incoming} --outgoing_threshold={minimum % outgoing} ``` -To remove a channel from consideration, its rule can simply be cleared: +To remove a rule from consideration, its rule can simply be cleared: ``` -loop setrule {short channel id} --clear +loop setrule {short channel id/ peer pubkey} --clear ``` ## Fees diff --git a/release_notes.md b/release_notes.md index 9a87269..aaab42e 100644 --- a/release_notes.md +++ b/release_notes.md @@ -15,6 +15,11 @@ This file tracks release notes for the loop client. ## Next release #### New Features +* Autoloop can now be configured on a per-peer basis, rather than only on an + individual channel level. This change allows desired liquidity thresholds + to be set for an individual peer, rather than a specific channel, and + leverages multi-loop-out to more efficiently manage liquidity. To configure + peer-level rules, provide the 'setrule' command with the peer's pubkey. #### Breaking Changes