Merge pull request #333 from carlaKC/autoloop-3-peerrules

autoloop: add peer level rules for aggregate liquidity management
pull/345/head
Carla Kirk-Cohen 3 years ago committed by GitHub
commit fd4214e68d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

@ -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,
@ -18,16 +19,24 @@ 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
}
// 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),
capacity: info.Capacity,
incoming: info.RemoteBalance,
outgoing: info.LocalBalance,
channels: []lnwire.ShortChannelID{
lnwire.NewShortChanIDFromInt(info.ChannelID),
},
pubkey: info.PubKeyBytes,
}
}

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

@ -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),
)
}
for peer, rule := range p.PeerRules {
ruleList = append(
ruleList, fmt.Sprintf("Peer: %v: %v", peer, rule),
)
}
return fmt.Sprintf("channel rules: %v, failure backoff: %v, sweep "+
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,12 +653,28 @@ 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),
}
}
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
@ -584,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
}
@ -601,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
}
@ -688,76 +795,78 @@ 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 []loop.OutRequest
disqualified = make(map[lnwire.ShortChannelID]Reason)
suggestions []swapSuggestion
resp = newSuggestions()
)
for _, channel := range channels {
balance := newBalances(channel)
rule, ok := m.params.ChannelRules[balance.channelID]
if !ok {
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
for peer, balances := range peerChannels {
rule, haveRule := m.params.PeerRules[peer]
if !haveRule {
continue
}
// We can have nil suggestions in the case where no action is
// required, so we skip over them.
suggestion := rule.suggestSwap(balance, restrictions)
if suggestion == nil {
disqualified[balance.channelID] = ReasonLiquidityOk
suggestion, err := m.suggestSwap(
ctx, traffic, balances, rule, restrictions, autoloop,
)
var reasonErr *reasonError
if errors.As(err, &reasonErr) {
resp.DisqualifiedPeers[peer] = reasonErr.reason
continue
}
// Get a quote for a swap of this amount.
quote, err := m.cfg.LoopOutQuote(
ctx, &loop.LoopOutQuoteRequest{
Amount: suggestion.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", suggestion, quote.SwapFee,
quote.MinerFee, quote.PrepayAmount)
suggestions = append(suggestions, suggestion)
}
for _, channel := range channels {
balance := newBalances(channel)
// Check that the estimated fees for the suggested swap are
// below the fee limits configured by the manager.
feeReason := m.checkFeeLimits(quote, suggestion.Amount)
if feeReason != ReasonNone {
disqualified[balance.channelID] = feeReason
channelID := lnwire.NewShortChanIDFromInt(channel.ChannelID)
rule, ok := m.params.ChannelRules[channelID]
if !ok {
continue
}
outRequest, err := m.makeLoopOutRequest(
ctx, suggestion, quote, autoloop,
suggestion, err := m.suggestSwap(
ctx, traffic, balance, rule, restrictions, autoloop,
)
var reasonErr *reasonError
if errors.As(err, &reasonErr) {
resp.DisqualifiedChans[channelID] = reasonErr.reason
continue
}
if err != nil {
return nil, err
}
suggestions = append(suggestions, outRequest)
}
// Finally, run through all possible swaps, excluding swaps that are
// not feasible due to fee or budget restrictions.
resp := &Suggestions{
DisqualifiedChans: disqualified,
suggestions = append(suggestions, suggestion)
}
// If we have no swaps to execute after we have applied all of our
@ -768,7 +877,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
@ -777,11 +886,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
}
}
@ -805,17 +917,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)
}
@ -824,6 +936,74 @@ 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) (swapSuggestion, error) {
// Check whether we can perform a swap.
err := traffic.maySwap(balance.pubkey, balance.channels)
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)
}
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
// 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
@ -871,22 +1051,23 @@ 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)
var chanSet loopdb.ChannelSet
for _, channel := range balance.channels {
chanSet = append(chanSet, channel.ToUint64())
}
request := loop.OutRequest{
Amount: suggestion.Amount,
OutgoingChanSet: loopdb.ChannelSet{
suggestion.Channel.ToUint64(),
},
Amount: amount,
OutgoingChanSet: chanSet,
MaxPrepayRoutingFee: prepayMaxFee,
MaxSwapRoutingFee: routeMaxFee,
MaxMinerFee: m.params.MaximumMinerFee,
@ -1095,31 +1276,33 @@ 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 {
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 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 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

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

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

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

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

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

@ -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,31 @@ 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 {
peer := peer
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 +654,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 +672,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 +784,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)
}

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

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

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

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

Loading…
Cancel
Save