Skip to content

Commit bfa7f66

Browse files
committed
liquidity: count static loop-ins
Teach the liquidity manager to include persisted static loop-ins in budget accounting, in-flight limits, and peer traffic backoff. This adds the static fee model used for conservative accounting and passes storage errors through the relevant planner helpers. The daemon wiring now exposes static loop-ins to liquidity so the manager can see the same ongoing swaps that the static-address subsystem persists, while easy autoloop keeps working with the new fallible traffic lookup path.
1 parent f25e3fd commit bfa7f66

10 files changed

Lines changed: 798 additions & 47 deletions

File tree

liquidity/easy_autoloop_exclusions_test.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ func TestEasyAutoloopExcludedPeers(t *testing.T) {
4747
)
4848

4949
// Picking a channel should not pick the excluded peer's channel.
50-
picked := c.manager.pickEasyAutoloopChannel(
51-
[]lndclient.ChannelInfo{ch1, ch2}, &params.ClientRestrictions,
52-
nil, nil, 1,
50+
picked, err := c.manager.pickEasyAutoloopChannel(
51+
t.Context(), []lndclient.ChannelInfo{ch1, ch2},
52+
&params.ClientRestrictions, nil, nil, 1,
5353
)
54+
require.NoError(t, err)
5455
require.NotNil(t, picked)
5556
require.Equal(
5657
t, ch2.ChannelID, picked.ChannelID,
@@ -92,21 +93,23 @@ func TestEasyAutoloopIncludeAllPeers(t *testing.T) {
9293
)
9394

9495
// With exclusion active, peer1 should not be picked.
95-
picked := c.manager.pickEasyAutoloopChannel(
96-
[]lndclient.ChannelInfo{ch1, ch2}, &params.ClientRestrictions,
97-
nil, nil, 1,
96+
picked, err := c.manager.pickEasyAutoloopChannel(
97+
t.Context(), []lndclient.ChannelInfo{ch1, ch2},
98+
&params.ClientRestrictions, nil, nil, 1,
9899
)
100+
require.NoError(t, err)
99101
require.NotNil(t, picked)
100102
require.Equal(t, ch2.ChannelID, picked.ChannelID)
101103

102104
// Simulate --includealleasypeers by clearing the exclusion list as the
103105
// CLI does before sending to the server.
104106
c.manager.params.EasyAutoloopExcludedPeers = nil
105107

106-
picked = c.manager.pickEasyAutoloopChannel(
107-
[]lndclient.ChannelInfo{ch1, ch2}, &params.ClientRestrictions,
108-
nil, nil, 1,
108+
picked, err = c.manager.pickEasyAutoloopChannel(
109+
t.Context(), []lndclient.ChannelInfo{ch1, ch2},
110+
&params.ClientRestrictions, nil, nil, 1,
109111
)
112+
require.NoError(t, err)
110113
require.NotNil(t, picked)
111114
require.Equal(
112115
t, ch1.ChannelID, picked.ChannelID,

liquidity/liquidity.go

Lines changed: 190 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ type Config struct {
221221
LoopOutTerms func(ctx context.Context,
222222
initiator string) (*loop.LoopOutTerms, error)
223223

224+
// ListStaticLoopIn returns all static-address loop-ins that liquidity
225+
// should consider for budget accounting, in-flight limits, and peer
226+
// traffic.
227+
ListStaticLoopIn func(context.Context) ([]*StaticLoopInInfo, error)
228+
224229
// GetAssetPrice returns the price of an asset in satoshis.
225230
GetAssetPrice func(ctx context.Context, assetId string,
226231
peerPubkey []byte, assetAmt uint64,
@@ -574,9 +579,19 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
574579
return err
575580
}
576581

582+
// Load the static loop-in snapshot once for the whole easy-autoloop
583+
// tick so budget and traffic checks cannot drift and do not need to hit
584+
// the store twice.
585+
staticLoopIns, err := m.loadStaticLoopIns(ctx)
586+
if err != nil {
587+
return err
588+
}
589+
577590
// Get a summary of our existing swaps so that we can check our autoloop
578591
// budget.
579-
summary := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
592+
summary := m.checkExistingAutoLoopsWithStatic(
593+
loopOut, loopIn, staticLoopIns,
594+
)
580595

581596
err = m.checkSummaryBudget(summary)
582597
if err != nil {
@@ -640,9 +655,13 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
640655
// Start building that swap.
641656
builder := newLoopOutBuilder(m.cfg)
642657

643-
channel := m.pickEasyAutoloopChannel(
644-
usableChannels, restrictions, loopOut, loopIn, 0,
658+
channel, err := m.pickEasyAutoloopChannelWithStatic(
659+
usableChannels, restrictions, loopOut, loopIn,
660+
staticLoopIns, 0,
645661
)
662+
if err != nil {
663+
return err
664+
}
646665
if channel == nil {
647666
return fmt.Errorf("no eligible channel for easy autoloop")
648667
}
@@ -721,9 +740,19 @@ func (m *Manager) dispatchBestAssetEasyAutoloopSwap(ctx context.Context,
721740
return err
722741
}
723742

743+
// Load the static loop-in snapshot once for the whole easy-autoloop
744+
// tick so budget and traffic checks cannot drift and do not need to hit
745+
// the store twice.
746+
staticLoopIns, err := m.loadStaticLoopIns(ctx)
747+
if err != nil {
748+
return err
749+
}
750+
724751
// Get a summary of our existing swaps so that we can check our autoloop
725752
// budget.
726-
summary := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
753+
summary := m.checkExistingAutoLoopsWithStatic(
754+
loopOut, loopIn, staticLoopIns,
755+
)
727756

728757
err = m.checkSummaryBudget(summary)
729758
if err != nil {
@@ -829,9 +858,13 @@ func (m *Manager) dispatchBestAssetEasyAutoloopSwap(ctx context.Context,
829858
// Start building that swap.
830859
builder := newLoopOutBuilder(m.cfg)
831860

832-
channel := m.pickEasyAutoloopChannel(
833-
usableChannels, restrictions, loopOut, loopIn, satsPerAsset,
861+
channel, err := m.pickEasyAutoloopChannelWithStatic(
862+
usableChannels, restrictions, loopOut, loopIn,
863+
staticLoopIns, satsPerAsset,
834864
)
865+
if err != nil {
866+
return err
867+
}
835868
if channel == nil {
836869
return fmt.Errorf("no eligible channel for easy autoloop")
837870
}
@@ -990,9 +1023,16 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
9901023
return nil, err
9911024
}
9921025

1026+
staticLoopIns, err := m.loadStaticLoopIns(ctx)
1027+
if err != nil {
1028+
return nil, err
1029+
}
1030+
9931031
// Get a summary of our existing swaps so that we can check our autoloop
9941032
// budget.
995-
summary := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
1033+
summary := m.checkExistingAutoLoopsWithStatic(
1034+
loopOut, loopIn, staticLoopIns,
1035+
)
9961036

9971037
err = m.checkSummaryBudget(summary)
9981038
if err != nil {
@@ -1037,7 +1077,9 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
10371077

10381078
// Get a summary of the channels and peers that are not eligible due
10391079
// to ongoing swaps.
1040-
traffic := m.currentSwapTraffic(loopOut, loopIn)
1080+
traffic := m.currentSwapTrafficWithStatic(
1081+
loopOut, loopIn, staticLoopIns,
1082+
)
10411083

10421084
var (
10431085
suggestions []swapSuggestion
@@ -1182,6 +1224,18 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
11821224
return resp, nil
11831225
}
11841226

1227+
// loadStaticLoopIns retrieves the static loop-ins that liquidity uses for
1228+
// shared accounting and traffic calculations.
1229+
func (m *Manager) loadStaticLoopIns(ctx context.Context) (
1230+
[]*StaticLoopInInfo, error) {
1231+
1232+
if m.cfg.ListStaticLoopIn == nil {
1233+
return nil, nil
1234+
}
1235+
1236+
return m.cfg.ListStaticLoopIn(ctx)
1237+
}
1238+
11851239
// suggestSwap checks whether we can currently perform a swap, and creates a
11861240
// swap request for the rule provided.
11871241
func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic,
@@ -1308,12 +1362,28 @@ func (e *existingAutoLoopSummary) totalFees() btcutil.Amount {
13081362
}
13091363

13101364
// checkExistingAutoLoops calculates the total amount that has been spent by
1311-
// automatically dispatched swaps that have completed, and the worst-case fee
1312-
// total for our set of ongoing, automatically dispatched swaps as well as a
1313-
// current in-flight count.
1314-
func (m *Manager) checkExistingAutoLoops(_ context.Context,
1365+
// automatically dispatched swaps that have completed, the worst-case fee total
1366+
// for our set of ongoing automatically dispatched swaps, and the current
1367+
// in-flight count.
1368+
func (m *Manager) checkExistingAutoLoops(ctx context.Context,
13151369
loopOuts []*loopdb.LoopOut,
1316-
loopIns []*loopdb.LoopIn) *existingAutoLoopSummary {
1370+
loopIns []*loopdb.LoopIn) (*existingAutoLoopSummary, error) {
1371+
1372+
staticLoopIns, err := m.loadStaticLoopIns(ctx)
1373+
if err != nil {
1374+
return nil, err
1375+
}
1376+
1377+
return m.checkExistingAutoLoopsWithStatic(
1378+
loopOuts, loopIns, staticLoopIns,
1379+
), nil
1380+
}
1381+
1382+
// checkExistingAutoLoopsWithStatic calculates our autoloop budget summary from
1383+
// the provided swap snapshots.
1384+
func (m *Manager) checkExistingAutoLoopsWithStatic(
1385+
loopOuts []*loopdb.LoopOut, loopIns []*loopdb.LoopIn,
1386+
staticLoopIns []*StaticLoopInInfo) *existingAutoLoopSummary {
13171387

13181388
var summary existingAutoLoopSummary
13191389

@@ -1370,14 +1440,72 @@ func (m *Manager) checkExistingAutoLoops(_ context.Context,
13701440
}
13711441
}
13721442

1443+
for _, in := range staticLoopIns {
1444+
if !isAutoloopLabel(in.Label) {
1445+
continue
1446+
}
1447+
1448+
inBudget := !in.LastUpdateTime.Before(
1449+
m.params.AutoloopBudgetLastRefresh,
1450+
)
1451+
1452+
switch {
1453+
case in.Pending:
1454+
summary.inFlightCount++
1455+
summary.pendingFees += staticLoopInWorstCaseFees(
1456+
in.NumDeposits, in.HasChange, in.QuotedSwapFee,
1457+
in.HtlcTxFeeRate, defaultLoopInSweepFee,
1458+
)
1459+
1460+
case !inBudget:
1461+
continue
1462+
1463+
case in.Failed:
1464+
// Static loop-in failure accounting stays pessimistic
1465+
// here. Once the swap is terminal we no longer know
1466+
// from liquidity's persisted view whether the timeout
1467+
// path actually confirmed, so we reserve the same
1468+
// worst-case fee shape we used while the swap was in
1469+
// flight.
1470+
// TODO: Persist real static-address swap costs,
1471+
// similar to loopdb.SwapCost, and use that exact
1472+
// terminal value here instead of the pessimistic
1473+
// worst-case estimate.
1474+
summary.spentFees += staticLoopInWorstCaseFees(
1475+
in.NumDeposits, in.HasChange, in.QuotedSwapFee,
1476+
in.HtlcTxFeeRate, defaultLoopInSweepFee,
1477+
)
1478+
1479+
default:
1480+
summary.spentFees += in.QuotedSwapFee
1481+
}
1482+
}
1483+
13731484
return &summary
13741485
}
13751486

13761487
// currentSwapTraffic examines our existing swaps and returns a summary of the
13771488
// current activity which can be used to determine whether we should perform
13781489
// any swaps.
1379-
func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut,
1380-
loopIn []*loopdb.LoopIn) *swapTraffic {
1490+
func (m *Manager) currentSwapTraffic(ctx context.Context,
1491+
loopOut []*loopdb.LoopOut,
1492+
loopIn []*loopdb.LoopIn) (*swapTraffic, error) {
1493+
1494+
staticLoopIns, err := m.loadStaticLoopIns(ctx)
1495+
if err != nil {
1496+
return nil, err
1497+
}
1498+
1499+
return m.currentSwapTrafficWithStatic(
1500+
loopOut, loopIn, staticLoopIns,
1501+
), nil
1502+
}
1503+
1504+
// currentSwapTrafficWithStatic builds the shared traffic view from the
1505+
// provided swap snapshots.
1506+
func (m *Manager) currentSwapTrafficWithStatic(loopOut []*loopdb.LoopOut,
1507+
loopIn []*loopdb.LoopIn,
1508+
staticLoopIns []*StaticLoopInInfo) *swapTraffic {
13811509

13821510
traffic := newSwapTraffic()
13831511

@@ -1408,9 +1536,7 @@ func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut,
14081536

14091537
if failedAt.After(failureCutoff) {
14101538
for _, id := range chanSet {
1411-
chanID := lnwire.NewShortChanIDFromInt(
1412-
id,
1413-
)
1539+
chanID := lnwire.NewShortChanIDFromInt(id)
14141540

14151541
traffic.failedLoopOut[chanID] = failedAt
14161542
}
@@ -1464,6 +1590,22 @@ func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut,
14641590
}
14651591
}
14661592

1593+
for _, in := range staticLoopIns {
1594+
if in.LastHop == nil {
1595+
continue
1596+
}
1597+
1598+
pubkey := *in.LastHop
1599+
1600+
switch {
1601+
case in.Pending && in.BlocksLoopIn:
1602+
traffic.ongoingLoopIn[pubkey] = true
1603+
1604+
case in.Failed && in.LastUpdateTime.After(failureCutoff):
1605+
traffic.failedLoopIn[pubkey] = in.LastUpdateTime
1606+
}
1607+
}
1608+
14671609
return traffic
14681610
}
14691611

@@ -1651,11 +1793,34 @@ func (m *Manager) waitForSwapPayment(ctx context.Context, swapHash lntypes.Hash,
16511793
// This function prioritizes channels with high local balance but also consults
16521794
// previous failures and ongoing swaps to avoid temporary channel failures or
16531795
// swap conflicts.
1654-
func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
1655-
restrictions *Restrictions, loopOut []*loopdb.LoopOut,
1656-
loopIn []*loopdb.LoopIn, satsPerAsset float64) *lndclient.ChannelInfo {
1796+
func (m *Manager) pickEasyAutoloopChannel(ctx context.Context,
1797+
channels []lndclient.ChannelInfo, restrictions *Restrictions,
1798+
loopOut []*loopdb.LoopOut, loopIn []*loopdb.LoopIn,
1799+
satsPerAsset float64) (*lndclient.ChannelInfo, error) {
16571800

1658-
traffic := m.currentSwapTraffic(loopOut, loopIn)
1801+
staticLoopIns, err := m.loadStaticLoopIns(ctx)
1802+
if err != nil {
1803+
return nil, err
1804+
}
1805+
1806+
return m.pickEasyAutoloopChannelWithStatic(
1807+
channels, restrictions, loopOut, loopIn, staticLoopIns,
1808+
satsPerAsset,
1809+
)
1810+
}
1811+
1812+
// pickEasyAutoloopChannelWithStatic picks an easy-autoloop channel using a
1813+
// shared static loop-in snapshot so callers can reuse one store load across
1814+
// budget and traffic checks within the same autoloop tick.
1815+
func (m *Manager) pickEasyAutoloopChannelWithStatic(
1816+
channels []lndclient.ChannelInfo, restrictions *Restrictions,
1817+
loopOut []*loopdb.LoopOut, loopIn []*loopdb.LoopIn,
1818+
staticLoopIns []*StaticLoopInInfo,
1819+
satsPerAsset float64) (*lndclient.ChannelInfo, error) {
1820+
1821+
traffic := m.currentSwapTrafficWithStatic(
1822+
loopOut, loopIn, staticLoopIns,
1823+
)
16591824

16601825
// Sort the candidate channels based on descending local balance. We
16611826
// want to prioritize picking a channel with the highest possible local
@@ -1722,13 +1887,13 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
17221887
"minimum is %v, skipping remaining channels",
17231888
channel.ChannelID, channel.LocalBalance,
17241889
restrictions.Minimum)
1725-
return nil
1890+
return nil, nil
17261891
}
17271892

1728-
return &channel
1893+
return &channel, nil
17291894
}
17301895

1731-
return nil
1896+
return nil, nil
17321897
}
17331898

17341899
func (m *Manager) numActiveStickyLoops() int {

0 commit comments

Comments
 (0)