diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 481fe5c..6586a00 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -516,12 +516,12 @@ func cloneParameters(params Parameters) Parameters { // autoloop gets a set of suggested swaps and dispatches them automatically if // we have automated looping enabled. func (m *Manager) autoloop(ctx context.Context) error { - swaps, err := m.SuggestSwaps(ctx, true) + suggestion, err := m.SuggestSwaps(ctx, true) if err != nil { return err } - for _, swap := range swaps { + for _, swap := range suggestion.OutSwaps { // If we don't actually have dispatch of swaps enabled, log // suggestions. if !m.params.Autoloop { @@ -557,6 +557,36 @@ func (m *Manager) ForceAutoLoop(ctx context.Context) error { } } +// Suggestions provides a set of suggested swaps, and the set of channels that +// were excluded from consideration. +type Suggestions struct { + // OutSwaps is the set of loop out swaps that we suggest executing. + OutSwaps []loop.OutRequest + + // DisqualifiedChans maps the set of channels that we do not recommend + // swaps on to the reason that we did not recommend a swap. + DisqualifiedChans map[lnwire.ShortChannelID]Reason +} + +func newSuggestions() *Suggestions { + return &Suggestions{ + DisqualifiedChans: make(map[lnwire.ShortChannelID]Reason), + } +} + +// singleReasonSuggestion is a helper function which returns a set of +// suggestions where all of our rules are disqualified due to a reason that +// applies to all of them (such as being out of budget). +func (m *Manager) singleReasonSuggestion(reason Reason) *Suggestions { + resp := newSuggestions() + + for id := range m.params.ChannelRules { + resp.DisqualifiedChans[id] = reason + } + + return resp +} + // SuggestSwaps returns a set of swap suggestions based on our current liquidity // balance for the set of rules configured for the manager, failing if there are // no rules set. It takes an autoloop boolean that indicates whether the @@ -564,7 +594,7 @@ func (m *Manager) ForceAutoLoop(ctx context.Context) error { // to determine the information we add to our swap suggestion and whether we // return any suggestions. func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( - []loop.OutRequest, error) { + *Suggestions, error) { m.paramsLock.Lock() defer m.paramsLock.Unlock() @@ -582,7 +612,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( log.Debugf("autoloop fee budget start time: %v is in "+ "the future", m.params.AutoFeeStartDate) - return nil, nil + return m.singleReasonSuggestion(ReasonBudgetNotStarted), nil } // Before we get any swap suggestions, we check what the current fee @@ -603,7 +633,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( satPerKwToSatPerVByte(estimate), satPerKwToSatPerVByte(m.params.SweepFeeRateLimit)) - return nil, nil + return m.singleReasonSuggestion(ReasonSweepFees), nil } // Get the current server side restrictions, combined with the client @@ -640,7 +670,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( m.params.AutoFeeBudget, summary.spentFees, summary.pendingFees) - return nil, nil + return m.singleReasonSuggestion(ReasonBudgetElapsed), nil } // If we have already reached our total allowed number of in flight @@ -649,7 +679,8 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( if allowedSwaps <= 0 { log.Debugf("%v autoloops allowed, %v in flight", m.params.MaxAutoInFlight, summary.inFlightCount) - return nil, nil + + return m.singleReasonSuggestion(ReasonInFlight), nil } channels, err := m.cfg.Lnd.Client.ListChannels(ctx) @@ -661,7 +692,10 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // to ongoing swaps. traffic := m.currentSwapTraffic(loopOut, loopIn) - var suggestions []loop.OutRequest + var ( + suggestions []loop.OutRequest + disqualified = make(map[lnwire.ShortChannelID]Reason) + ) for _, channel := range channels { balance := newBalances(channel) @@ -671,7 +705,11 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( continue } - if !traffic.maySwap(channel.PubKeyBytes, balance.channelID) { + // Check whether we can perform a swap, adding the channel to + // our set of disqualified swaps if it is not eligible. + reason := traffic.maySwap(channel.PubKeyBytes, balance.channelID) + if reason != ReasonNone { + disqualified[balance.channelID] = reason continue } @@ -679,6 +717,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // required, so we skip over them. suggestion := rule.suggestSwap(balance, restrictions) if suggestion == nil { + disqualified[balance.channelID] = ReasonLiquidityOk continue } @@ -700,11 +739,9 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // Check that the estimated fees for the suggested swap are // below the fee limits configured by the manager. - err = m.checkFeeLimits(quote, suggestion.Amount) - if err != nil { - log.Infof("suggestion: %v expected fees too high: %v", - suggestion, err) - + feeReason := m.checkFeeLimits(quote, suggestion.Amount) + if feeReason != ReasonNone { + disqualified[balance.channelID] = feeReason continue } @@ -717,10 +754,16 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( suggestions = append(suggestions, outRequest) } - // If we have no suggestions after we have applied all of our limits, - // just return. + // Finally, run through all possible swaps, excluding swaps that are + // not feasible due to fee or budget restrictions. + resp := &Suggestions{ + DisqualifiedChans: disqualified, + } + + // If we have no swaps to execute after we have applied all of our + // limits, just return our set of disqualified swaps. if len(suggestions) == 0 { - return nil, nil + return resp, nil } // Sort suggestions by amount in descending order. @@ -730,12 +773,38 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // Run through our suggested swaps in descending order of amount and // return all of the swaps which will fit within our remaining budget. - var ( - available = m.params.AutoFeeBudget - summary.totalFees() - inBudget []loop.OutRequest - ) + available := m.params.AutoFeeBudget - summary.totalFees() + + // setReason is a helper that adds a swap's channels to our disqualified + // list with the reason provided. + setReason := func(reason Reason, swap loop.OutRequest) { + for _, id := range swap.OutgoingChanSet { + chanID := lnwire.NewShortChanIDFromInt(id) + + resp.DisqualifiedChans[chanID] = reason + } + } for _, swap := range suggestions { + swap := swap + + // If we do not have enough funds available, or we hit our + // in flight limit, we record this value for the rest of the + // swaps. + var reason Reason + switch { + case available == 0: + reason = ReasonBudgetInsufficient + + case len(resp.OutSwaps) == allowedSwaps: + reason = ReasonInFlight + } + + if reason != ReasonNone { + setReason(reason, swap) + continue + } + fees := worstCaseOutFees( swap.MaxPrepayRoutingFee, swap.MaxSwapRoutingFee, swap.MaxSwapFee, swap.MaxMinerFee, swap.MaxPrepayAmount, @@ -746,17 +815,13 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // fall within the budget and decrement our available amount. if fees <= available { available -= fees - inBudget = append(inBudget, swap) - } - - // If we're out of budget, or we have hit the max number of - // swaps that we want to dispatch at one time, exit early. - if available == 0 || allowedSwaps == len(inBudget) { - break + resp.OutSwaps = append(resp.OutSwaps, swap) + } else { + setReason(ReasonBudgetInsufficient, swap) } } - return inBudget, nil + return resp, nil } // getSwapRestrictions queries the server for its latest swap size restrictions, @@ -1030,56 +1095,62 @@ func newSwapTraffic() *swapTraffic { // maySwap returns a boolean that indicates whether we may perform a swap for a // peer and its set of channels. func (s *swapTraffic) maySwap(peer route.Vertex, - chanID lnwire.ShortChannelID) bool { + chanID lnwire.ShortChannelID) Reason { lastFail, recentFail := s.failedLoopOut[chanID] if recentFail { log.Debugf("Channel: %v not eligible for suggestions, was "+ "part of a failed swap at: %v", chanID, lastFail) - return false + return ReasonFailureBackoff } if s.ongoingLoopOut[chanID] { log.Debugf("Channel: %v not eligible for suggestions, "+ "ongoing loop out utilizing channel", chanID) - return false + return ReasonLoopOut } if s.ongoingLoopIn[peer] { log.Debugf("Peer: %x not eligible for suggestions ongoing "+ "loop in utilizing peer", peer) - return false + return ReasonLoopIn } - return true + return ReasonNone } // checkFeeLimits takes a set of fees for a swap and checks whether they exceed // our swap limits. func (m *Manager) checkFeeLimits(quote *loop.LoopOutQuote, - swapAmt btcutil.Amount) error { + swapAmt btcutil.Amount) Reason { maxFee := ppmToSat(swapAmt, m.params.MaximumSwapFeePPM) if quote.SwapFee > maxFee { - return fmt.Errorf("quoted swap fee: %v > maximum swap fee: %v", + log.Debugf("quoted swap fee: %v > maximum swap fee: %v", quote.SwapFee, maxFee) + + return ReasonSwapFee } if quote.MinerFee > m.params.MaximumMinerFee { - return fmt.Errorf("quoted miner fee: %v > maximum miner "+ + log.Debugf("quoted miner fee: %v > maximum miner "+ "fee: %v", quote.MinerFee, m.params.MaximumMinerFee) + + return ReasonMinerFee } if quote.PrepayAmount > m.params.MaximumPrepay { - return fmt.Errorf("quoted prepay: %v > maximum prepay: %v", + log.Debugf("quoted prepay: %v > maximum prepay: %v", quote.PrepayAmount, m.params.MaximumPrepay) + + return ReasonPrepay } - return nil + return ReasonNone } // satPerKwToSatPerVByte converts sat per kWeight to sat per vByte. diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index 168eefc..557cf22 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -107,6 +107,10 @@ var ( } testRestrictions = NewRestrictions(1, 10000) + + // noneDisqualified can be used in tests where we don't have any + // disqualified channels so that we can use require.Equal. + noneDisqualified = make(map[lnwire.ShortChannelID]Reason) ) // newTestConfig creates a default test config. @@ -283,7 +287,7 @@ func TestRestrictedSuggestions(t *testing.T) { channels []lndclient.ChannelInfo loopOut []*loopdb.LoopOut loopIn []*loopdb.LoopIn - expected []loop.OutRequest + expected *Suggestions }{ { name: "no existing swaps", @@ -292,8 +296,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, loopOut: nil, loopIn: nil, - expected: []loop.OutRequest{ - chan1Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -308,8 +315,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: []loop.OutRequest{ - chan1Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -324,8 +334,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: []loop.OutRequest{ - chan1Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -338,8 +351,13 @@ func TestRestrictedSuggestions(t *testing.T) { Contract: chan1Out, }, }, - expected: []loop.OutRequest{ - chan2Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan2Rec, + }, + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonLoopOut, + }, }, }, { @@ -354,8 +372,13 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: []loop.OutRequest{ - chan1Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID2: ReasonLoopIn, + }, }, }, { @@ -373,7 +396,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: nil, + expected: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonFailureBackoff, + }, + }, }, { name: "swap failed before cutoff", @@ -390,8 +417,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: []loop.OutRequest{ - chan1Rec, + expected: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -409,7 +439,11 @@ func TestRestrictedSuggestions(t *testing.T) { }, }, }, - expected: nil, + expected: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonLoopOut, + }, + }, }, } @@ -447,21 +481,28 @@ func TestRestrictedSuggestions(t *testing.T) { // fee is above and below the configured limit. func TestSweepFeeLimit(t *testing.T) { tests := []struct { - name string - feeRate chainfee.SatPerKWeight - swaps []loop.OutRequest + name string + feeRate chainfee.SatPerKWeight + suggestions *Suggestions }{ { name: "fee estimate ok", feeRate: defaultSweepFeeRateLimit, - swaps: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { name: "fee estimate above limit", feeRate: defaultSweepFeeRateLimit + 1, - swaps: nil, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonSweepFees, + }, + }, }, } @@ -487,7 +528,7 @@ func TestSweepFeeLimit(t *testing.T) { testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - testCase.swaps, nil, + testCase.suggestions, nil, ) }) } @@ -497,10 +538,10 @@ func TestSweepFeeLimit(t *testing.T) { // the liquidity manager and the current set of channel balances. func TestSuggestSwaps(t *testing.T) { tests := []struct { - name string - rules map[lnwire.ShortChannelID]*ThresholdRule - swaps []loop.OutRequest - err error + name string + rules map[lnwire.ShortChannelID]*ThresholdRule + suggestions *Suggestions + err error }{ { name: "no rules", @@ -512,8 +553,11 @@ func TestSuggestSwaps(t *testing.T) { rules: map[lnwire.ShortChannelID]*ThresholdRule{ chanID1: chanRule, }, - swaps: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -521,7 +565,9 @@ func TestSuggestSwaps(t *testing.T) { rules: map[lnwire.ShortChannelID]*ThresholdRule{ chanID2: NewThresholdRule(10, 10), }, - swaps: nil, + suggestions: &Suggestions{ + DisqualifiedChans: noneDisqualified, + }, }, } @@ -540,7 +586,7 @@ func TestSuggestSwaps(t *testing.T) { testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - testCase.swaps, testCase.err, + testCase.suggestions, testCase.err, ) }) } @@ -549,15 +595,18 @@ func TestSuggestSwaps(t *testing.T) { // TestFeeLimits tests limiting of swap suggestions by fees. func TestFeeLimits(t *testing.T) { tests := []struct { - name string - quote *loop.LoopOutQuote - expected []loop.OutRequest + name string + quote *loop.LoopOutQuote + suggestions *Suggestions }{ { name: "fees ok", quote: testQuote, - expected: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -567,6 +616,11 @@ func TestFeeLimits(t *testing.T) { PrepayAmount: defaultMaximumPrepay + 1, MinerFee: 50, }, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonPrepay, + }, + }, }, { name: "insufficient miner fee", @@ -575,6 +629,11 @@ func TestFeeLimits(t *testing.T) { PrepayAmount: 100, MinerFee: defaultMaximumMinerFee + 1, }, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonMinerFee, + }, + }, }, { // Swap fee limited to 0.5% of 7500 = 37,5. @@ -584,6 +643,11 @@ func TestFeeLimits(t *testing.T) { PrepayAmount: 100, MinerFee: 500, }, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonSwapFee, + }, + }, }, } @@ -610,7 +674,7 @@ func TestFeeLimits(t *testing.T) { testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - testCase.expected, nil, + testCase.suggestions, nil, ) }) } @@ -641,8 +705,8 @@ func TestFeeBudget(t *testing.T) { // last update time to their total cost. existingSwaps map[time.Time]btcutil.Amount - // expectedSwaps is the set of swaps we expect to be suggested. - expectedSwaps []loop.OutRequest + // suggestions is the set of swaps we expect to be suggested. + suggestions *Suggestions }{ { // Two swaps will cost (78+5000)*2, set exactly 10156 @@ -650,8 +714,11 @@ func TestFeeBudget(t *testing.T) { name: "budget for 2 swaps, no existing", budget: 10156, maxMinerFee: 5000, - expectedSwaps: []loop.OutRequest{ - chan1Rec, chan2Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, chan2Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -660,8 +727,13 @@ func TestFeeBudget(t *testing.T) { name: "budget for 1 swaps, no existing", budget: 10155, maxMinerFee: 5000, - expectedSwaps: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID2: ReasonBudgetInsufficient, + }, }, }, { @@ -673,8 +745,11 @@ func TestFeeBudget(t *testing.T) { existingSwaps: map[time.Time]btcutil.Amount{ testBudgetStart.Add(time.Hour * -1): 200, }, - expectedSwaps: []loop.OutRequest{ - chan1Rec, chan2Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, chan2Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -686,8 +761,13 @@ func TestFeeBudget(t *testing.T) { existingSwaps: map[time.Time]btcutil.Amount{ testBudgetStart.Add(time.Hour): 500, }, - expectedSwaps: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID2: ReasonBudgetInsufficient, + }, }, }, { @@ -697,7 +777,12 @@ func TestFeeBudget(t *testing.T) { existingSwaps: map[time.Time]btcutil.Amount{ testBudgetStart.Add(time.Hour): 500, }, - expectedSwaps: nil, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonBudgetElapsed, + chanID2: ReasonBudgetElapsed, + }, + }, }, } @@ -760,14 +845,14 @@ func TestFeeBudget(t *testing.T) { // Set our custom max miner fee on each expected swap, // rather than having to create multiple vars for // different rates. - for i := range testCase.expectedSwaps { - testCase.expectedSwaps[i].MaxMinerFee = + for i := range testCase.suggestions.OutSwaps { + testCase.suggestions.OutSwaps[i].MaxMinerFee = testCase.maxMinerFee } testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - testCase.expectedSwaps, nil, + testCase.suggestions, nil, ) }) } @@ -780,20 +865,26 @@ func TestInFlightLimit(t *testing.T) { name string maxInFlight int existingSwaps []*loopdb.LoopOut - expectedSwaps []loop.OutRequest + suggestions *Suggestions }{ { name: "none in flight, extra space", maxInFlight: 3, - expectedSwaps: []loop.OutRequest{ - chan1Rec, chan2Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, chan2Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { name: "none in flight, exact match", maxInFlight: 2, - expectedSwaps: []loop.OutRequest{ - chan1Rec, chan2Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, chan2Rec, + }, + DisqualifiedChans: noneDisqualified, }, }, { @@ -804,8 +895,13 @@ func TestInFlightLimit(t *testing.T) { Contract: autoOutContract, }, }, - expectedSwaps: []loop.OutRequest{ - chan1Rec, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID2: ReasonInFlight, + }, }, }, { @@ -816,7 +912,12 @@ func TestInFlightLimit(t *testing.T) { Contract: autoOutContract, }, }, - expectedSwaps: nil, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonInFlight, + chanID2: ReasonInFlight, + }, + }, }, { name: "max swaps exceeded", @@ -829,7 +930,12 @@ func TestInFlightLimit(t *testing.T) { Contract: autoOutContract, }, }, - expectedSwaps: nil, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonInFlight, + chanID2: ReasonInFlight, + }, + }, }, } @@ -860,7 +966,7 @@ func TestInFlightLimit(t *testing.T) { testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - testCase.expectedSwaps, nil, + testCase.suggestions, nil, ) }) } @@ -875,13 +981,18 @@ func TestSizeRestrictions(t *testing.T) { } outSwap = loop.OutRequest{ + Amount: 7000, OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()}, MaxPrepayRoutingFee: prepayFee, - MaxMinerFee: defaultMaximumMinerFee, - MaxSwapFee: testQuote.SwapFee, - MaxPrepayAmount: testQuote.PrepayAmount, - SweepConfTarget: loop.DefaultSweepConfTarget, - Initiator: autoloopSwapInitiator, + MaxSwapRoutingFee: ppmToSat( + 7000, + defaultRoutingFeePPM, + ), + MaxMinerFee: defaultMaximumMinerFee, + MaxSwapFee: testQuote.SwapFee, + MaxPrepayAmount: testQuote.PrepayAmount, + SweepConfTarget: loop.DefaultSweepConfTarget, + Initiator: autoloopSwapInitiator, } ) @@ -896,8 +1007,8 @@ func TestSizeRestrictions(t *testing.T) { // endpoint. serverRestrictions []Restrictions - // expectedAmount is the amount that we expect for our swap. - expectedAmount btcutil.Amount + // suggestions is the set of suggestions we expect. + suggestions *Suggestions // expectedError is the error we expect. expectedError error @@ -910,7 +1021,12 @@ func TestSizeRestrictions(t *testing.T) { serverRestrictions: []Restrictions{ serverRestrictions, serverRestrictions, }, - expectedAmount: 7500, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + chan1Rec, + }, + DisqualifiedChans: noneDisqualified, + }, }, { name: "minimum more than server, no swap", @@ -920,7 +1036,11 @@ func TestSizeRestrictions(t *testing.T) { serverRestrictions: []Restrictions{ serverRestrictions, serverRestrictions, }, - expectedAmount: 0, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonLiquidityOk, + }, + }, }, { name: "maximum less than server, swap happens", @@ -930,7 +1050,12 @@ func TestSizeRestrictions(t *testing.T) { serverRestrictions: []Restrictions{ serverRestrictions, serverRestrictions, }, - expectedAmount: 7000, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + outSwap, + }, + DisqualifiedChans: noneDisqualified, + }, }, { // Originally, our client params are ok. But then the @@ -948,8 +1073,8 @@ func TestSizeRestrictions(t *testing.T) { Maximum: 6000, }, }, - expectedAmount: 0, - expectedError: ErrMaxExceedsServer, + suggestions: nil, + expectedError: ErrMaxExceedsServer, }, } @@ -981,25 +1106,9 @@ func TestSizeRestrictions(t *testing.T) { return &restrictions, nil } - - // If we expect a swap (non-zero amount), we add a - // swap to our set of expected swaps, and update amount - // and fee accordingly. - var expectedSwaps []loop.OutRequest - if testCase.expectedAmount != 0 { - outSwap.Amount = testCase.expectedAmount - - outSwap.MaxSwapRoutingFee = ppmToSat( - testCase.expectedAmount, - defaultRoutingFeePPM, - ) - - expectedSwaps = append(expectedSwaps, outSwap) - } - testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), - expectedSwaps, testCase.expectedError, + testCase.suggestions, testCase.expectedError, ) require.Equal( @@ -1034,7 +1143,7 @@ func newSuggestSwapsSetup(cfg *Config, lnd *test.LndMockServices, // use the default parameters and setup two channels (channel1 + channel2) with // chanRule set for each. func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup, - expected []loop.OutRequest, expectedErr error) { + expected *Suggestions, expectedErr error) { t.Parallel() diff --git a/liquidity/reasons.go b/liquidity/reasons.go new file mode 100644 index 0000000..d685f61 --- /dev/null +++ b/liquidity/reasons.go @@ -0,0 +1,62 @@ +package liquidity + +// Reason is an enum which represents the various reasons we have for not +// executing a swap. +type Reason uint8 + +const ( + // ReasonNone is the zero value reason, added so that this enum can + // align with the numeric values used in our protobufs and avoid + // ambiguity around default zero values. + ReasonNone Reason = iota + + // ReasonBudgetNotStarted indicates that we do not recommend any swaps + // because the start time for our budget has not arrived yet. + ReasonBudgetNotStarted + + // ReasonSweepFees indicates that the estimated fees to sweep swaps + // are too high right now. + ReasonSweepFees + + // ReasonBudgetElapsed indicates that the autoloop budget for the + // period has been elapsed. + ReasonBudgetElapsed + + // ReasonInFlight indicates that the limit on in-flight automatically + // dispatched swaps has already been reached. + ReasonInFlight + + // ReasonSwapFee indicates that the server fee for a specific swap is + // too high. + ReasonSwapFee + + // ReasonMinerFee indicates that the miner fee for a specific swap is + // to high. + ReasonMinerFee + + // ReasonPrepay indicates that the prepay fee for a specific swap is + // too high. + ReasonPrepay + + // ReasonFailureBackoff indicates that a swap has recently failed for + // this target, and the backoff period has not yet passed. + ReasonFailureBackoff + + // ReasonLoopOut indicates that a loop out swap is currently utilizing + // the channel, so it is not eligible. + ReasonLoopOut + + // ReasonLoopIn indicates that a loop in swap is currently in flight + // for the peer, so it is not eligible. + ReasonLoopIn + + // ReasonLiquidityOk indicates that a target meets the liquidity + // balance expressed in its rule, so no swap is needed. + ReasonLiquidityOk + + // ReasonBudgetInsufficient indicates that we cannot perform a swap + // because we do not have enough pending budget available. This differs + // from budget elapsed, because we still have some budget available, + // but we have allocated it to other swaps. + ReasonBudgetInsufficient +) diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index ff310fc..cbfe2da 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -704,7 +704,7 @@ func rpcToRule(rule *looprpc.LiquidityRule) (*liquidity.ThresholdRule, error) { func (s *swapClientServer) SuggestSwaps(ctx context.Context, _ *looprpc.SuggestSwapsRequest) (*looprpc.SuggestSwapsResponse, error) { - swaps, err := s.liquidityMgr.SuggestSwaps(ctx, false) + suggestions, err := s.liquidityMgr.SuggestSwaps(ctx, false) switch err { case liquidity.ErrNoRules: return nil, status.Error(codes.FailedPrecondition, err.Error()) @@ -717,7 +717,7 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context, var loopOut []*looprpc.LoopOutRequest - for _, swap := range swaps { + for _, swap := range suggestions.OutSwaps { loopOut = append(loopOut, &looprpc.LoopOutRequest{ Amt: int64(swap.Amount), OutgoingChanSet: swap.OutgoingChanSet,