Skip to content

Commit fd17580

Browse files
committed
liquidity: add max in flight limit to swap suggestions
To allow control over the rate at which we dispatch autoloops, we add a limit to the number of in flight autoloops we allow.
1 parent 692620d commit fd17580

File tree

2 files changed

+137
-5
lines changed

2 files changed

+137
-5
lines changed

liquidity/liquidity.go

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ const (
8888
// defaultSweepFeeRateLimit is the default limit we place on estimated
8989
// sweep fees, (750 * 4 /1000 = 3 sat/vByte).
9090
defaultSweepFeeRateLimit = chainfee.SatPerKWeight(750)
91+
92+
// defaultMaxInFlight is the default number of in-flight automatically
93+
// dispatched swaps we allow. Note that this does not enable automated
94+
// swaps itself (because we want non-zero values to be expressed in
95+
// suggestions as a dry-run).
96+
defaultMaxInFlight = 2
9197
)
9298

9399
var (
@@ -106,6 +112,7 @@ var (
106112
// liquidity manger with.
107113
defaultParameters = Parameters{
108114
AutoFeeBudget: defaultBudget,
115+
MaxAutoInFlight: defaultMaxInFlight,
109116
ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule),
110117
FailureBackOff: defaultFailureBackoff,
111118
SweepFeeRateLimit: defaultSweepFeeRateLimit,
@@ -143,6 +150,9 @@ var (
143150

144151
// ErrNegativeBudget is returned if a negative swap budget is set.
145152
ErrNegativeBudget = errors.New("swap budget must be >= 0")
153+
154+
// ErrZeroInFlight is returned is a zero in flight swaps value is set.
155+
ErrZeroInFlight = errors.New("max in flight swaps must be >=0")
146156
)
147157

148158
// Config contains the external functionality required to run the
@@ -187,6 +197,10 @@ type Parameters struct {
187197
// dispatched swaps in our current budget, inclusive.
188198
AutoFeeStartDate time.Time
189199

200+
// MaxAutoInFlight is the maximum number of in-flight automatically
201+
// dispatched swaps we allow.
202+
MaxAutoInFlight int
203+
190204
// FailureBackOff is the amount of time that we require passes after a
191205
// channel has been part of a failed loop out swap before we suggest
192206
// using it again.
@@ -248,12 +262,12 @@ func (p Parameters) String() string {
248262
"fee rate limit: %v, sweep conf target: %v, maximum prepay: "+
249263
"%v, maximum miner fee: %v, maximum swap fee ppm: %v, maximum "+
250264
"routing fee ppm: %v, maximum prepay routing fee ppm: %v, "+
251-
"auto budget: %v, budget start: %v",
265+
"auto budget: %v, budget start: %v, max auto in flight: %v",
252266
strings.Join(channelRules, ","), p.FailureBackOff,
253267
p.SweepFeeRateLimit, p.SweepConfTarget, p.MaximumPrepay,
254268
p.MaximumMinerFee, p.MaximumSwapFeePPM,
255269
p.MaximumRoutingFeePPM, p.MaximumPrepayRoutingFeePPM,
256-
p.AutoFeeBudget, p.AutoFeeStartDate)
270+
p.AutoFeeBudget, p.AutoFeeStartDate, p.MaxAutoInFlight)
257271
}
258272

259273
// validate checks whether a set of parameters is valid. It takes the minimum
@@ -308,6 +322,10 @@ func (p Parameters) validate(minConfs int32) error {
308322
return ErrNegativeBudget
309323
}
310324

325+
if p.MaxAutoInFlight <= 0 {
326+
return ErrZeroInFlight
327+
}
328+
311329
return nil
312330
}
313331

@@ -456,6 +474,15 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
456474
return nil, nil
457475
}
458476

477+
// If we have already reached our total allowed number of in flight
478+
// swaps, we do not suggest any more at the moment.
479+
allowedSwaps := m.params.MaxAutoInFlight - summary.inFlightCount
480+
if allowedSwaps <= 0 {
481+
log.Debugf("%v autoloops allowed, %v in flight",
482+
m.params.MaxAutoInFlight, summary.inFlightCount)
483+
return nil, nil
484+
}
485+
459486
eligible, err := m.getEligibleChannels(ctx, loopOut, loopIn)
460487
if err != nil {
461488
return nil, err
@@ -541,8 +568,9 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
541568
inBudget = append(inBudget, swap)
542569
}
543570

544-
// If we're out of budget, exit early.
545-
if available == 0 {
571+
// If we're out of budget, or we have hit the max number of
572+
// swaps that we want to dispatch at one time, exit early.
573+
if available == 0 || allowedSwaps == len(inBudget) {
546574
break
547575
}
548576
}
@@ -612,6 +640,13 @@ type existingAutoLoopSummary struct {
612640
// pendingFees is the worst-case amount of fees we could spend on in
613641
// flight autoloops.
614642
pendingFees btcutil.Amount
643+
644+
// inFlightCount is the total number of automated swaps that are
645+
// currently in flight. Note that this may race with swap completion,
646+
// but not with initiation of new automated swaps, this is ok, because
647+
// it can only lead to dispatching fewer swaps than we could have (not
648+
// too many).
649+
inFlightCount int
615650
}
616651

617652
// totalFees returns the total amount of fees that automatically dispatched
@@ -622,7 +657,8 @@ func (e *existingAutoLoopSummary) totalFees() btcutil.Amount {
622657

623658
// checkExistingAutoLoops calculates the total amount that has been spent by
624659
// automatically dispatched swaps that have completed, and the worst-case fee
625-
// total for our set of ongoing, automatically dispatched swaps.
660+
// total for our set of ongoing, automatically dispatched swaps as well as a
661+
// current in-flight count.
626662
func (m *Manager) checkExistingAutoLoops(ctx context.Context,
627663
loopOuts []*loopdb.LoopOut) (*existingAutoLoopSummary, error) {
628664

@@ -642,6 +678,8 @@ func (m *Manager) checkExistingAutoLoops(ctx context.Context,
642678
// for the swap provided that the swap completed after our
643679
// budget start date.
644680
if out.State().State.Type() == loopdb.StateTypePending {
681+
summary.inFlightCount++
682+
645683
prepay, err := m.cfg.Lnd.Client.DecodePaymentRequest(
646684
ctx, out.Contract.PrepayInvoice,
647685
)

liquidity/liquidity_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@ func TestFeeBudget(t *testing.T) {
690690
params.AutoFeeStartDate = testBudgetStart
691691
params.AutoFeeBudget = testCase.budget
692692
params.MaximumMinerFee = testCase.maxMinerFee
693+
params.MaxAutoInFlight = 2
693694

694695
// Set our custom max miner fee on each expected swap,
695696
// rather than having to create multiple vars for
@@ -707,6 +708,99 @@ func TestFeeBudget(t *testing.T) {
707708
}
708709
}
709710

711+
// TestInFlightLimit tests the limit we place on the number of in-flight swaps
712+
// that are allowed.
713+
func TestInFlightLimit(t *testing.T) {
714+
tests := []struct {
715+
name string
716+
maxInFlight int
717+
existingSwaps []*loopdb.LoopOut
718+
expectedSwaps []loop.OutRequest
719+
}{
720+
{
721+
name: "none in flight, extra space",
722+
maxInFlight: 3,
723+
expectedSwaps: []loop.OutRequest{
724+
chan1Rec, chan2Rec,
725+
},
726+
},
727+
{
728+
name: "none in flight, exact match",
729+
maxInFlight: 2,
730+
expectedSwaps: []loop.OutRequest{
731+
chan1Rec, chan2Rec,
732+
},
733+
},
734+
{
735+
name: "one in flight, one allowed",
736+
maxInFlight: 2,
737+
existingSwaps: []*loopdb.LoopOut{
738+
{
739+
Contract: autoOutContract,
740+
},
741+
},
742+
expectedSwaps: []loop.OutRequest{
743+
chan1Rec,
744+
},
745+
},
746+
{
747+
name: "max in flight",
748+
maxInFlight: 1,
749+
existingSwaps: []*loopdb.LoopOut{
750+
{
751+
Contract: autoOutContract,
752+
},
753+
},
754+
expectedSwaps: nil,
755+
},
756+
{
757+
name: "max swaps exceeded",
758+
maxInFlight: 1,
759+
existingSwaps: []*loopdb.LoopOut{
760+
{
761+
Contract: autoOutContract,
762+
},
763+
{
764+
Contract: autoOutContract,
765+
},
766+
},
767+
expectedSwaps: nil,
768+
},
769+
}
770+
771+
for _, testCase := range tests {
772+
testCase := testCase
773+
774+
t.Run(testCase.name, func(t *testing.T) {
775+
cfg, lnd := newTestConfig()
776+
cfg.ListLoopOut = func() ([]*loopdb.LoopOut, error) {
777+
return testCase.existingSwaps, nil
778+
}
779+
780+
lnd.Channels = []lndclient.ChannelInfo{
781+
channel1, channel2,
782+
}
783+
784+
params := defaultParameters
785+
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
786+
chanID1: chanRule,
787+
chanID2: chanRule,
788+
}
789+
params.MaxAutoInFlight = testCase.maxInFlight
790+
791+
// By default we only have budget for one swap, increase
792+
// our budget so that we could recommend more than one
793+
// swap at a time.
794+
params.AutoFeeBudget = defaultBudget * 2
795+
796+
testSuggestSwaps(
797+
t, newSuggestSwapsSetup(cfg, lnd, params),
798+
testCase.expectedSwaps,
799+
)
800+
})
801+
}
802+
}
803+
710804
// testSuggestSwapsSetup contains the elements that are used to create a
711805
// suggest swaps test.
712806
type testSuggestSwapsSetup struct {

0 commit comments

Comments
 (0)