Skip to content

Commit 7c4d71b

Browse files
committed
liquidity: add reasons for autoloops not executing
1 parent b9b75c3 commit 7c4d71b

File tree

4 files changed

+373
-131
lines changed

4 files changed

+373
-131
lines changed

liquidity/liquidity.go

Lines changed: 110 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -516,12 +516,12 @@ func cloneParameters(params Parameters) Parameters {
516516
// autoloop gets a set of suggested swaps and dispatches them automatically if
517517
// we have automated looping enabled.
518518
func (m *Manager) autoloop(ctx context.Context) error {
519-
swaps, err := m.SuggestSwaps(ctx, true)
519+
suggestion, err := m.SuggestSwaps(ctx, true)
520520
if err != nil {
521521
return err
522522
}
523523

524-
for _, swap := range swaps {
524+
for _, swap := range suggestion.OutSwaps {
525525
// If we don't actually have dispatch of swaps enabled, log
526526
// suggestions.
527527
if !m.params.Autoloop {
@@ -557,14 +557,44 @@ func (m *Manager) ForceAutoLoop(ctx context.Context) error {
557557
}
558558
}
559559

560+
// Suggestions provides a set of suggested swaps, and the set of channels that
561+
// were excluded from consideration.
562+
type Suggestions struct {
563+
// OutSwaps is the set of loop out swaps that we suggest executing.
564+
OutSwaps []loop.OutRequest
565+
566+
// DisqualifiedChans maps the set of channels that we do not recommend
567+
// swaps on to the reason that we did not recommend a swap.
568+
DisqualifiedChans map[lnwire.ShortChannelID]Reason
569+
}
570+
571+
func newSuggestions() *Suggestions {
572+
return &Suggestions{
573+
DisqualifiedChans: make(map[lnwire.ShortChannelID]Reason),
574+
}
575+
}
576+
577+
// singleReasonSuggestion is a helper function which returns a set of
578+
// suggestions where all of our rules are disqualified due to a reason that
579+
// applies to all of them (such as being out of budget).
580+
func (m *Manager) singleReasonSuggestion(reason Reason) *Suggestions {
581+
resp := newSuggestions()
582+
583+
for id := range m.params.ChannelRules {
584+
resp.DisqualifiedChans[id] = reason
585+
}
586+
587+
return resp
588+
}
589+
560590
// SuggestSwaps returns a set of swap suggestions based on our current liquidity
561591
// balance for the set of rules configured for the manager, failing if there are
562592
// no rules set. It takes an autoloop boolean that indicates whether the
563593
// suggestions are being used for our internal autolooper. This boolean is used
564594
// to determine the information we add to our swap suggestion and whether we
565595
// return any suggestions.
566596
func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
567-
[]loop.OutRequest, error) {
597+
*Suggestions, error) {
568598

569599
m.paramsLock.Lock()
570600
defer m.paramsLock.Unlock()
@@ -582,7 +612,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
582612
log.Debugf("autoloop fee budget start time: %v is in "+
583613
"the future", m.params.AutoFeeStartDate)
584614

585-
return nil, nil
615+
return m.singleReasonSuggestion(ReasonBudgetNotStarted), nil
586616
}
587617

588618
// 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) (
603633
satPerKwToSatPerVByte(estimate),
604634
satPerKwToSatPerVByte(m.params.SweepFeeRateLimit))
605635

606-
return nil, nil
636+
return m.singleReasonSuggestion(ReasonSweepFees), nil
607637
}
608638

609639
// Get the current server side restrictions, combined with the client
@@ -640,7 +670,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
640670
m.params.AutoFeeBudget, summary.spentFees,
641671
summary.pendingFees)
642672

643-
return nil, nil
673+
return m.singleReasonSuggestion(ReasonBudgetElapsed), nil
644674
}
645675

646676
// 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) (
649679
if allowedSwaps <= 0 {
650680
log.Debugf("%v autoloops allowed, %v in flight",
651681
m.params.MaxAutoInFlight, summary.inFlightCount)
652-
return nil, nil
682+
683+
return m.singleReasonSuggestion(ReasonInFlight), nil
653684
}
654685

655686
channels, err := m.cfg.Lnd.Client.ListChannels(ctx)
@@ -661,7 +692,10 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
661692
// to ongoing swaps.
662693
traffic := m.currentSwapTraffic(loopOut, loopIn)
663694

664-
var suggestions []loop.OutRequest
695+
var (
696+
suggestions []loop.OutRequest
697+
disqualified = make(map[lnwire.ShortChannelID]Reason)
698+
)
665699

666700
for _, channel := range channels {
667701
balance := newBalances(channel)
@@ -671,14 +705,19 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
671705
continue
672706
}
673707

674-
if !traffic.maySwap(channel.PubKeyBytes, balance.channelID) {
708+
// Check whether we can perform a swap, adding the channel to
709+
// our set of disqualified swaps if it is not eligible.
710+
reason := traffic.maySwap(channel.PubKeyBytes, balance.channelID)
711+
if reason != ReasonNone {
712+
disqualified[balance.channelID] = reason
675713
continue
676714
}
677715

678716
// We can have nil suggestions in the case where no action is
679717
// required, so we skip over them.
680718
suggestion := rule.suggestSwap(balance, restrictions)
681719
if suggestion == nil {
720+
disqualified[balance.channelID] = ReasonLiquidityOk
682721
continue
683722
}
684723

@@ -700,11 +739,9 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
700739

701740
// Check that the estimated fees for the suggested swap are
702741
// below the fee limits configured by the manager.
703-
err = m.checkFeeLimits(quote, suggestion.Amount)
704-
if err != nil {
705-
log.Infof("suggestion: %v expected fees too high: %v",
706-
suggestion, err)
707-
742+
feeReason := m.checkFeeLimits(quote, suggestion.Amount)
743+
if feeReason != ReasonNone {
744+
disqualified[balance.channelID] = feeReason
708745
continue
709746
}
710747

@@ -717,10 +754,16 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
717754
suggestions = append(suggestions, outRequest)
718755
}
719756

720-
// If we have no suggestions after we have applied all of our limits,
721-
// just return.
757+
// Finally, run through all possible swaps, excluding swaps that are
758+
// not feasible due to fee or budget restrictions.
759+
resp := &Suggestions{
760+
DisqualifiedChans: disqualified,
761+
}
762+
763+
// If we have no swaps to execute after we have applied all of our
764+
// limits, just return our set of disqualified swaps.
722765
if len(suggestions) == 0 {
723-
return nil, nil
766+
return resp, nil
724767
}
725768

726769
// Sort suggestions by amount in descending order.
@@ -730,12 +773,38 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
730773

731774
// Run through our suggested swaps in descending order of amount and
732775
// return all of the swaps which will fit within our remaining budget.
733-
var (
734-
available = m.params.AutoFeeBudget - summary.totalFees()
735-
inBudget []loop.OutRequest
736-
)
776+
available := m.params.AutoFeeBudget - summary.totalFees()
777+
778+
// setReason is a helper that adds a swap's channels to our disqualified
779+
// list with the reason provided.
780+
setReason := func(reason Reason, swap loop.OutRequest) {
781+
for _, id := range swap.OutgoingChanSet {
782+
chanID := lnwire.NewShortChanIDFromInt(id)
783+
784+
resp.DisqualifiedChans[chanID] = reason
785+
}
786+
}
737787

738788
for _, swap := range suggestions {
789+
swap := swap
790+
791+
// If we do not have enough funds available, or we hit our
792+
// in flight limit, we record this value for the rest of the
793+
// swaps.
794+
var reason Reason
795+
switch {
796+
case available == 0:
797+
reason = ReasonBudgetInsufficient
798+
799+
case len(resp.OutSwaps) == allowedSwaps:
800+
reason = ReasonInFlight
801+
}
802+
803+
if reason != ReasonNone {
804+
setReason(reason, swap)
805+
continue
806+
}
807+
739808
fees := worstCaseOutFees(
740809
swap.MaxPrepayRoutingFee, swap.MaxSwapRoutingFee,
741810
swap.MaxSwapFee, swap.MaxMinerFee, swap.MaxPrepayAmount,
@@ -746,17 +815,13 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
746815
// fall within the budget and decrement our available amount.
747816
if fees <= available {
748817
available -= fees
749-
inBudget = append(inBudget, swap)
750-
}
751-
752-
// If we're out of budget, or we have hit the max number of
753-
// swaps that we want to dispatch at one time, exit early.
754-
if available == 0 || allowedSwaps == len(inBudget) {
755-
break
818+
resp.OutSwaps = append(resp.OutSwaps, swap)
819+
} else {
820+
setReason(ReasonBudgetInsufficient, swap)
756821
}
757822
}
758823

759-
return inBudget, nil
824+
return resp, nil
760825
}
761826

762827
// getSwapRestrictions queries the server for its latest swap size restrictions,
@@ -1030,56 +1095,62 @@ func newSwapTraffic() *swapTraffic {
10301095
// maySwap returns a boolean that indicates whether we may perform a swap for a
10311096
// peer and its set of channels.
10321097
func (s *swapTraffic) maySwap(peer route.Vertex,
1033-
chanID lnwire.ShortChannelID) bool {
1098+
chanID lnwire.ShortChannelID) Reason {
10341099

10351100
lastFail, recentFail := s.failedLoopOut[chanID]
10361101
if recentFail {
10371102
log.Debugf("Channel: %v not eligible for suggestions, was "+
10381103
"part of a failed swap at: %v", chanID, lastFail)
10391104

1040-
return false
1105+
return ReasonFailureBackoff
10411106
}
10421107

10431108
if s.ongoingLoopOut[chanID] {
10441109
log.Debugf("Channel: %v not eligible for suggestions, "+
10451110
"ongoing loop out utilizing channel", chanID)
10461111

1047-
return false
1112+
return ReasonLoopOut
10481113
}
10491114

10501115
if s.ongoingLoopIn[peer] {
10511116
log.Debugf("Peer: %x not eligible for suggestions ongoing "+
10521117
"loop in utilizing peer", peer)
10531118

1054-
return false
1119+
return ReasonLoopIn
10551120
}
10561121

1057-
return true
1122+
return ReasonNone
10581123
}
10591124

10601125
// checkFeeLimits takes a set of fees for a swap and checks whether they exceed
10611126
// our swap limits.
10621127
func (m *Manager) checkFeeLimits(quote *loop.LoopOutQuote,
1063-
swapAmt btcutil.Amount) error {
1128+
swapAmt btcutil.Amount) Reason {
10641129

10651130
maxFee := ppmToSat(swapAmt, m.params.MaximumSwapFeePPM)
10661131

10671132
if quote.SwapFee > maxFee {
1068-
return fmt.Errorf("quoted swap fee: %v > maximum swap fee: %v",
1133+
log.Debugf("quoted swap fee: %v > maximum swap fee: %v",
10691134
quote.SwapFee, maxFee)
1135+
1136+
return ReasonSwapFee
10701137
}
10711138

10721139
if quote.MinerFee > m.params.MaximumMinerFee {
1073-
return fmt.Errorf("quoted miner fee: %v > maximum miner "+
1140+
log.Debugf("quoted miner fee: %v > maximum miner "+
10741141
"fee: %v", quote.MinerFee, m.params.MaximumMinerFee)
1142+
1143+
return ReasonMinerFee
10751144
}
10761145

10771146
if quote.PrepayAmount > m.params.MaximumPrepay {
1078-
return fmt.Errorf("quoted prepay: %v > maximum prepay: %v",
1147+
log.Debugf("quoted prepay: %v > maximum prepay: %v",
10791148
quote.PrepayAmount, m.params.MaximumPrepay)
1149+
1150+
return ReasonPrepay
10801151
}
10811152

1082-
return nil
1153+
return ReasonNone
10831154
}
10841155

10851156
// satPerKwToSatPerVByte converts sat per kWeight to sat per vByte.

0 commit comments

Comments
 (0)