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.
pull/333/head
carla 3 years ago
parent d1f121cbc6
commit 3f46ae514b
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91

@ -4,6 +4,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/labels" "github.com/lightninglabs/loop/labels"
@ -12,6 +13,7 @@ import (
"github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
) )
// TestAutoLoopDisabled tests the case where we need to perform a swap, but // TestAutoLoopDisabled tests the case where we need to perform a swap, but
@ -266,6 +268,161 @@ func TestAutoLoopEnabled(t *testing.T) {
c.stop() 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 // existingSwapFromRequest is a helper function which returns the db
// representation of a loop out request with the event set provided. // representation of a loop out request with the event set provided.
func existingSwapFromRequest(request *loop.OutRequest, initTime time.Time, func existingSwapFromRequest(request *loop.OutRequest, initTime time.Time,

@ -19,8 +19,10 @@ type balances struct {
// outgoing is the local balance of the channel. // outgoing is the local balance of the channel.
outgoing btcutil.Amount outgoing btcutil.Amount
// channelID is the channel that has these balances. // channels is the channel that has these balances represent. This may
channelID lnwire.ShortChannelID // 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 is the public key of the peer we have this balances set with.
pubkey route.Vertex pubkey route.Vertex
@ -29,10 +31,12 @@ type balances struct {
// newBalances creates a balances struct from lndclient channel information. // newBalances creates a balances struct from lndclient channel information.
func newBalances(info lndclient.ChannelInfo) *balances { func newBalances(info lndclient.ChannelInfo) *balances {
return &balances{ return &balances{
capacity: info.Capacity, capacity: info.Capacity,
incoming: info.RemoteBalance, incoming: info.RemoteBalance,
outgoing: info.LocalBalance, outgoing: info.LocalBalance,
channelID: lnwire.NewShortChanIDFromInt(info.ChannelID), channels: []lnwire.ShortChannelID{
pubkey: info.PubKeyBytes, lnwire.NewShortChanIDFromInt(info.ChannelID),
},
pubkey: info.PubKeyBytes,
} }
} }

@ -124,6 +124,7 @@ var (
AutoFeeBudget: defaultBudget, AutoFeeBudget: defaultBudget,
MaxAutoInFlight: defaultMaxInFlight, MaxAutoInFlight: defaultMaxInFlight,
ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule), ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule),
PeerRules: make(map[route.Vertex]*ThresholdRule),
FailureBackOff: defaultFailureBackoff, FailureBackOff: defaultFailureBackoff,
SweepFeeRateLimit: defaultSweepFeeRateLimit, SweepFeeRateLimit: defaultSweepFeeRateLimit,
SweepConfTarget: loop.DefaultSweepConfTarget, SweepConfTarget: loop.DefaultSweepConfTarget,
@ -181,6 +182,11 @@ var (
// ErrNoRules is returned when no rules are set for swap suggestions. // ErrNoRules is returned when no rules are set for swap suggestions.
ErrNoRules = errors.New("no rules set for autoloop") 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 // Config contains the external functionality required to run the
@ -289,27 +295,41 @@ type Parameters struct {
ClientRestrictions Restrictions ClientRestrictions Restrictions
// ChannelRules maps a short channel ID to a rule that describes how we // 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 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. // String returns the string representation of our parameters.
func (p Parameters) String() string { 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 { for channel, rule := range p.ChannelRules {
channelRules = append( ruleList = append(
channelRules, fmt.Sprintf("%v: %v", channel, rule), 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: "+ "fee rate limit: %v, sweep conf target: %v, maximum prepay: "+
"%v, maximum miner fee: %v, maximum swap fee ppm: %v, maximum "+ "%v, maximum miner fee: %v, maximum swap fee ppm: %v, maximum "+
"routing fee ppm: %v, maximum prepay routing fee ppm: %v, "+ "routing fee ppm: %v, maximum prepay routing fee ppm: %v, "+
"auto budget: %v, budget start: %v, max auto in flight: %v, "+ "auto budget: %v, budget start: %v, max auto in flight: %v, "+
"minimum swap size=%v, maximum swap size=%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.SweepFeeRateLimit, p.SweepConfTarget, p.MaximumPrepay,
p.MaximumMinerFee, p.MaximumSwapFeePPM, p.MaximumMinerFee, p.MaximumSwapFeePPM,
p.MaximumRoutingFeePPM, p.MaximumPrepayRoutingFeePPM, p.MaximumRoutingFeePPM, p.MaximumPrepayRoutingFeePPM,
@ -317,9 +337,54 @@ func (p Parameters) String() string {
p.ClientRestrictions.Minimum, p.ClientRestrictions.Maximum) p.ClientRestrictions.Minimum, p.ClientRestrictions.Maximum)
} }
// validate checks whether a set of parameters is valid. It takes the minimum // haveRules returns a boolean indicating whether we have any rules configured.
// confirmations we allow for sweep confirmation target as a parameter. func (p Parameters) haveRules() bool {
func (p Parameters) validate(minConfs int32, server *Restrictions) error { 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 { for channel, rule := range p.ChannelRules {
if channel.ToUint64() == 0 { if channel.ToUint64() == 0 {
return ErrZeroChannelID 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 // 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 // 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. // 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 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 { if err != nil {
return err return err
} }
@ -510,6 +587,16 @@ func cloneParameters(params Parameters) Parameters {
paramCopy.ChannelRules[channel] = &ruleCopy 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 return paramCopy
} }
@ -566,11 +653,16 @@ type Suggestions struct {
// DisqualifiedChans maps the set of channels that we do not recommend // DisqualifiedChans maps the set of channels that we do not recommend
// swaps on to the reason that we did not recommend a swap. // swaps on to the reason that we did not recommend a swap.
DisqualifiedChans map[lnwire.ShortChannelID]Reason 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 { func newSuggestions() *Suggestions {
return &Suggestions{ return &Suggestions{
DisqualifiedChans: make(map[lnwire.ShortChannelID]Reason), 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 resp.DisqualifiedChans[id] = reason
} }
for peer := range m.params.PeerRules {
resp.DisqualifiedPeers[peer] = reason
}
return resp 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 // If we have no rules set, exit early to avoid unnecessary calls to
// lnd and the server. // lnd and the server.
if len(m.params.ChannelRules) == 0 { if !m.params.haveRules() {
return nil, ErrNoRules return nil, ErrNoRules
} }
@ -699,19 +795,59 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
return nil, err 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 // Get a summary of the channels and peers that are not eligible due
// to ongoing swaps. // to ongoing swaps.
traffic := m.currentSwapTraffic(loopOut, loopIn) traffic := m.currentSwapTraffic(loopOut, loopIn)
var ( var (
suggestions []swapSuggestion suggestions []swapSuggestion
disqualified = make(map[lnwire.ShortChannelID]Reason) 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 { for _, channel := range channels {
balance := newBalances(channel) balance := newBalances(channel)
rule, ok := m.params.ChannelRules[balance.channelID] channelID := lnwire.NewShortChanIDFromInt(channel.ChannelID)
rule, ok := m.params.ChannelRules[channelID]
if !ok { if !ok {
continue continue
} }
@ -722,7 +858,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
var reasonErr *reasonError var reasonErr *reasonError
if errors.As(err, &reasonErr) { if errors.As(err, &reasonErr) {
disqualified[balance.channelID] = reasonErr.reason resp.DisqualifiedChans[channelID] = reasonErr.reason
continue continue
} }
@ -733,12 +869,6 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
suggestions = append(suggestions, suggestion) 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 // If we have no swaps to execute after we have applied all of our
// limits, just return our set of disqualified swaps. // limits, just return our set of disqualified swaps.
if len(suggestions) == 0 { if len(suggestions) == 0 {
@ -813,7 +943,7 @@ func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic,
autoloop bool) (swapSuggestion, error) { autoloop bool) (swapSuggestion, error) {
// Check whether we can perform a swap. // Check whether we can perform a swap.
err := traffic.maySwap(balance.pubkey, balance.channelID) err := traffic.maySwap(balance.pubkey, balance.channels)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -930,11 +1060,14 @@ func (m *Manager) makeLoopOutRequest(ctx context.Context,
routeMaxFee := ppmToSat(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{ request := loop.OutRequest{
Amount: amount, Amount: amount,
OutgoingChanSet: loopdb.ChannelSet{ OutgoingChanSet: chanSet,
balance.channelID.ToUint64(),
},
MaxPrepayRoutingFee: prepayMaxFee, MaxPrepayRoutingFee: prepayMaxFee,
MaxSwapRoutingFee: routeMaxFee, MaxSwapRoutingFee: routeMaxFee,
MaxMinerFee: m.params.MaximumMinerFee, 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 // maySwap returns a boolean that indicates whether we may perform a swap for a
// peer and its set of channels. // peer and its set of channels.
func (s *swapTraffic) maySwap(peer route.Vertex, func (s *swapTraffic) maySwap(peer route.Vertex,
chanID lnwire.ShortChannelID) error { channels []lnwire.ShortChannelID) error {
lastFail, recentFail := s.failedLoopOut[chanID] for _, chanID := range channels {
if recentFail { lastFail, recentFail := s.failedLoopOut[chanID]
log.Debugf("Channel: %v not eligible for suggestions, was "+ if recentFail {
"part of a failed swap at: %v", chanID, lastFail) 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] { if s.ongoingLoopOut[chanID] {
log.Debugf("Channel: %v not eligible for suggestions, "+ log.Debugf("Channel: %v not eligible for suggestions, "+
"ongoing loop out utilizing channel", chanID) "ongoing loop out utilizing channel", chanID)
return newReasonError(ReasonLoopOut) return newReasonError(ReasonLoopOut)
}
} }
if s.ongoingLoopIn[peer] { if s.ongoingLoopIn[peer] {

@ -25,6 +25,7 @@ var (
chanID1 = lnwire.NewShortChanIDFromInt(1) chanID1 = lnwire.NewShortChanIDFromInt(1)
chanID2 = lnwire.NewShortChanIDFromInt(2) chanID2 = lnwire.NewShortChanIDFromInt(2)
chanID3 = lnwire.NewShortChanIDFromInt(3)
peer1 = route.Vertex{1} peer1 = route.Vertex{1}
peer2 = route.Vertex{2} peer2 = route.Vertex{2}
@ -111,6 +112,10 @@ var (
// noneDisqualified can be used in tests where we don't have any // noneDisqualified can be used in tests where we don't have any
// disqualified channels so that we can use require.Equal. // disqualified channels so that we can use require.Equal.
noneDisqualified = make(map[lnwire.ShortChannelID]Reason) 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. // newTestConfig creates a default test config.
@ -280,27 +285,36 @@ func TestRestrictedSuggestions(t *testing.T) {
defaultFailureBackoff * -3, defaultFailureBackoff * -3,
), ),
} }
chanRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
chanID2: chanRule,
}
) )
tests := []struct { tests := []struct {
name string name string
channels []lndclient.ChannelInfo channels []lndclient.ChannelInfo
loopOut []*loopdb.LoopOut loopOut []*loopdb.LoopOut
loopIn []*loopdb.LoopIn loopIn []*loopdb.LoopIn
expected *Suggestions chanRules map[lnwire.ShortChannelID]*ThresholdRule
peerRules map[route.Vertex]*ThresholdRule
expected *Suggestions
}{ }{
{ {
name: "no existing swaps", name: "no existing swaps",
channels: []lndclient.ChannelInfo{ channels: []lndclient.ChannelInfo{
channel1, channel1,
}, },
loopOut: nil, loopOut: nil,
loopIn: nil, loopIn: nil,
chanRules: chanRules,
expected: &Suggestions{ expected: &Suggestions{
OutSwaps: []loop.OutRequest{ OutSwaps: []loop.OutRequest{
chan1Rec, chan1Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -315,11 +329,13 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
chanRules: chanRules,
expected: &Suggestions{ expected: &Suggestions{
OutSwaps: []loop.OutRequest{ OutSwaps: []loop.OutRequest{
chan1Rec, chan1Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -334,11 +350,13 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
chanRules: chanRules,
expected: &Suggestions{ expected: &Suggestions{
OutSwaps: []loop.OutRequest{ OutSwaps: []loop.OutRequest{
chan1Rec, chan1Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -351,6 +369,7 @@ func TestRestrictedSuggestions(t *testing.T) {
Contract: chan1Out, Contract: chan1Out,
}, },
}, },
chanRules: chanRules,
expected: &Suggestions{ expected: &Suggestions{
OutSwaps: []loop.OutRequest{ OutSwaps: []loop.OutRequest{
chan2Rec, chan2Rec,
@ -358,6 +377,7 @@ func TestRestrictedSuggestions(t *testing.T) {
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonLoopOut, chanID1: ReasonLoopOut,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -372,6 +392,7 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
chanRules: chanRules,
expected: &Suggestions{ expected: &Suggestions{
OutSwaps: []loop.OutRequest{ OutSwaps: []loop.OutRequest{
chan1Rec, chan1Rec,
@ -379,6 +400,7 @@ func TestRestrictedSuggestions(t *testing.T) {
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID2: ReasonLoopIn, chanID2: ReasonLoopIn,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -396,10 +418,12 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
chanRules: chanRules,
expected: &Suggestions{ expected: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonFailureBackoff, chanID1: ReasonFailureBackoff,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -417,11 +441,13 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
chanRules: chanRules,
expected: &Suggestions{ expected: &Suggestions{
OutSwaps: []loop.OutRequest{ OutSwaps: []loop.OutRequest{
chan1Rec, chan1Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -439,10 +465,36 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
chanRules: chanRules,
expected: &Suggestions{ expected: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonLoopOut, 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 lnd.Channels = testCase.channels
params := defaultParameters params := defaultParameters
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{ if testCase.chanRules != nil {
chanID1: chanRule, params.ChannelRules = testCase.chanRules
chanID2: chanRule, }
if testCase.peerRules != nil {
params.PeerRules = testCase.peerRules
} }
testSuggestSwaps( testSuggestSwaps(
@ -493,6 +548,7 @@ func TestSweepFeeLimit(t *testing.T) {
chan1Rec, chan1Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -502,6 +558,7 @@ func TestSweepFeeLimit(t *testing.T) {
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonSweepFees, 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 // TestSuggestSwaps tests getting of swap suggestions based on the rules set for
// the liquidity manager and the current set of channel balances. // the liquidity manager and the current set of channel balances.
func TestSuggestSwaps(t *testing.T) { func TestSuggestSwaps(t *testing.T) {
singleChannel := []lndclient.ChannelInfo{
channel1,
}
tests := []struct { tests := []struct {
name string name string
channels []lndclient.ChannelInfo
rules map[lnwire.ShortChannelID]*ThresholdRule rules map[lnwire.ShortChannelID]*ThresholdRule
peerRules map[route.Vertex]*ThresholdRule
suggestions *Suggestions suggestions *Suggestions
err error err error
}{ }{
{ {
name: "no rules", name: "no rules",
rules: map[lnwire.ShortChannelID]*ThresholdRule{}, channels: singleChannel,
err: ErrNoRules, rules: map[lnwire.ShortChannelID]*ThresholdRule{},
err: ErrNoRules,
}, },
{ {
name: "loop out", name: "loop out",
channels: singleChannel,
rules: map[lnwire.ShortChannelID]*ThresholdRule{ rules: map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule, chanID1: chanRule,
}, },
@ -558,15 +623,76 @@ func TestSuggestSwaps(t *testing.T) {
chan1Rec, chan1Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
name: "no rule for channel", name: "no rule for channel",
channels: singleChannel,
rules: map[lnwire.ShortChannelID]*ThresholdRule{ rules: map[lnwire.ShortChannelID]*ThresholdRule{
chanID2: NewThresholdRule(10, 10), chanID2: NewThresholdRule(10, 10),
}, },
suggestions: &Suggestions{ suggestions: &Suggestions{
DisqualifiedChans: noneDisqualified, 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) { t.Run(testCase.name, func(t *testing.T) {
cfg, lnd := newTestConfig() cfg, lnd := newTestConfig()
lnd.Channels = []lndclient.ChannelInfo{ lnd.Channels = testCase.channels
channel1,
}
params := defaultParameters params := defaultParameters
params.ChannelRules = testCase.rules if testCase.rules != nil {
params.ChannelRules = testCase.rules
}
if testCase.peerRules != nil {
params.PeerRules = testCase.peerRules
}
testSuggestSwaps( testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params), t, newSuggestSwapsSetup(cfg, lnd, params),
@ -607,6 +737,7 @@ func TestFeeLimits(t *testing.T) {
chan1Rec, chan1Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -620,6 +751,7 @@ func TestFeeLimits(t *testing.T) {
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonPrepay, chanID1: ReasonPrepay,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -633,6 +765,7 @@ func TestFeeLimits(t *testing.T) {
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonMinerFee, chanID1: ReasonMinerFee,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -647,6 +780,7 @@ func TestFeeLimits(t *testing.T) {
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonSwapFee, chanID1: ReasonSwapFee,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
} }
@ -719,6 +853,7 @@ func TestFeeBudget(t *testing.T) {
chan1Rec, chan2Rec, chan1Rec, chan2Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -734,6 +869,7 @@ func TestFeeBudget(t *testing.T) {
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID2: ReasonBudgetInsufficient, chanID2: ReasonBudgetInsufficient,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -750,6 +886,7 @@ func TestFeeBudget(t *testing.T) {
chan1Rec, chan2Rec, chan1Rec, chan2Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -768,6 +905,7 @@ func TestFeeBudget(t *testing.T) {
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID2: ReasonBudgetInsufficient, chanID2: ReasonBudgetInsufficient,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -782,6 +920,7 @@ func TestFeeBudget(t *testing.T) {
chanID1: ReasonBudgetElapsed, chanID1: ReasonBudgetElapsed,
chanID2: ReasonBudgetElapsed, chanID2: ReasonBudgetElapsed,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
} }
@ -875,6 +1014,7 @@ func TestInFlightLimit(t *testing.T) {
chan1Rec, chan2Rec, chan1Rec, chan2Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -885,6 +1025,7 @@ func TestInFlightLimit(t *testing.T) {
chan1Rec, chan2Rec, chan1Rec, chan2Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -902,6 +1043,7 @@ func TestInFlightLimit(t *testing.T) {
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID2: ReasonInFlight, chanID2: ReasonInFlight,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -917,6 +1059,7 @@ func TestInFlightLimit(t *testing.T) {
chanID1: ReasonInFlight, chanID1: ReasonInFlight,
chanID2: ReasonInFlight, chanID2: ReasonInFlight,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -935,6 +1078,7 @@ func TestInFlightLimit(t *testing.T) {
chanID1: ReasonInFlight, chanID1: ReasonInFlight,
chanID2: ReasonInFlight, chanID2: ReasonInFlight,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
} }
@ -1026,6 +1170,7 @@ func TestSizeRestrictions(t *testing.T) {
chan1Rec, chan1Rec,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -1040,6 +1185,7 @@ func TestSizeRestrictions(t *testing.T) {
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonLiquidityOk, chanID1: ReasonLiquidityOk,
}, },
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {
@ -1055,6 +1201,7 @@ func TestSizeRestrictions(t *testing.T) {
outSwap, outSwap,
}, },
DisqualifiedChans: noneDisqualified, DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
}, },
}, },
{ {

Loading…
Cancel
Save