Skip to content

Commit 965b99d

Browse files
committed
liquidity: add existing loop in swaps to budget calculations
1 parent 0f0be28 commit 965b99d

File tree

2 files changed

+217
-6
lines changed

2 files changed

+217
-6
lines changed

liquidity/liquidity.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
639639

640640
// Get a summary of our existing swaps so that we can check our autoloop
641641
// budget.
642-
summary, err := m.checkExistingAutoLoops(ctx, loopOut)
642+
summary, err := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
643643
if err != nil {
644644
return nil, err
645645
}
@@ -958,7 +958,8 @@ func (e *existingAutoLoopSummary) totalFees() btcutil.Amount {
958958
// total for our set of ongoing, automatically dispatched swaps as well as a
959959
// current in-flight count.
960960
func (m *Manager) checkExistingAutoLoops(ctx context.Context,
961-
loopOuts []*loopdb.LoopOut) (*existingAutoLoopSummary, error) {
961+
loopOuts []*loopdb.LoopOut, loopIns []*loopdb.LoopIn) (
962+
*existingAutoLoopSummary, error) {
962963

963964
var summary existingAutoLoopSummary
964965

@@ -997,6 +998,28 @@ func (m *Manager) checkExistingAutoLoops(ctx context.Context,
997998
}
998999
}
9991000

1001+
for _, in := range loopIns {
1002+
if in.Contract.Label != labels.AutoloopLabel(swap.TypeIn) {
1003+
continue
1004+
}
1005+
1006+
pending := in.State().State.Type() == loopdb.StateTypePending
1007+
inBudget := !in.LastUpdateTime().Before(m.params.AutoFeeStartDate)
1008+
1009+
// If an autoloop is in a pending state, we always count it in
1010+
// our current budget, and record the worst-case fees for it,
1011+
// because we do not know how it will resolve.
1012+
if pending {
1013+
summary.inFlightCount++
1014+
summary.pendingFees += worstCaseInFees(
1015+
in.Contract.MaxMinerFee, in.Contract.MaxSwapFee,
1016+
defaultLoopInSweepFee,
1017+
)
1018+
} else if inBudget {
1019+
summary.spentFees += in.State().Cost.Total()
1020+
}
1021+
}
1022+
10001023
return &summary, nil
10011024
}
10021025

liquidity/liquidity_test.go

Lines changed: 192 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ var (
107107
OutgoingChanSet: loopdb.ChannelSet{999},
108108
}
109109

110+
autoInContract = &loopdb.LoopInContract{
111+
SwapContract: loopdb.SwapContract{
112+
Label: labels.AutoloopLabel(swap.TypeIn),
113+
InitiationTime: testBudgetStart,
114+
},
115+
}
116+
110117
testRestrictions = NewRestrictions(1, 10000)
111118

112119
// noneDisqualified can be used in tests where we don't have any
@@ -1123,9 +1130,10 @@ func TestFeeBudget(t *testing.T) {
11231130
// that are allowed.
11241131
func TestInFlightLimit(t *testing.T) {
11251132
tests := []struct {
1126-
name string
1127-
maxInFlight int
1128-
existingSwaps []*loopdb.LoopOut
1133+
name string
1134+
maxInFlight int
1135+
existingSwaps []*loopdb.LoopOut
1136+
existingInSwaps []*loopdb.LoopIn
11291137
// peerRules will only be set (instead of test default values)
11301138
// is it is non-nil.
11311139
peerRules map[route.Vertex]*SwapRule
@@ -1194,8 +1202,10 @@ func TestInFlightLimit(t *testing.T) {
11941202
{
11951203
Contract: autoOutContract,
11961204
},
1205+
},
1206+
existingInSwaps: []*loopdb.LoopIn{
11971207
{
1198-
Contract: autoOutContract,
1208+
Contract: autoInContract,
11991209
},
12001210
},
12011211
suggestions: &Suggestions{
@@ -1247,6 +1257,9 @@ func TestInFlightLimit(t *testing.T) {
12471257
cfg.ListLoopOut = func() ([]*loopdb.LoopOut, error) {
12481258
return testCase.existingSwaps, nil
12491259
}
1260+
cfg.ListLoopIn = func() ([]*loopdb.LoopIn, error) {
1261+
return testCase.existingInSwaps, nil
1262+
}
12501263

12511264
lnd.Channels = []lndclient.ChannelInfo{
12521265
channel1, channel2,
@@ -1568,6 +1581,181 @@ func TestFeePercentage(t *testing.T) {
15681581
}
15691582
}
15701583

1584+
// TestBudgetWithLoopin tests that our autoloop budget accounts for loop in
1585+
// swaps that have been automatically dispatched. It tests out swaps that have
1586+
// already completed and those that are pending, inside and outside of our
1587+
// budget period to ensure that we account for all relevant swaps.
1588+
func TestBudgetWithLoopin(t *testing.T) {
1589+
var (
1590+
budget btcutil.Amount = 10000
1591+
1592+
outsideBudget = testBudgetStart.Add(-5)
1593+
insideBudget = testBudgetStart.Add(5)
1594+
1595+
contractOutsideBudget = &loopdb.LoopInContract{
1596+
SwapContract: loopdb.SwapContract{
1597+
InitiationTime: outsideBudget,
1598+
MaxSwapFee: budget,
1599+
},
1600+
Label: labels.AutoloopLabel(swap.TypeIn),
1601+
}
1602+
1603+
// Set our spend equal to our budget so we don't need to
1604+
// calculate exact costs.
1605+
eventOutsideBudget = &loopdb.LoopEvent{
1606+
SwapStateData: loopdb.SwapStateData{
1607+
Cost: loopdb.SwapCost{
1608+
Server: budget,
1609+
},
1610+
State: loopdb.StateSuccess,
1611+
},
1612+
Time: outsideBudget,
1613+
}
1614+
1615+
successWithinBudget = &loopdb.LoopEvent{
1616+
SwapStateData: loopdb.SwapStateData{
1617+
Cost: loopdb.SwapCost{
1618+
Server: budget,
1619+
},
1620+
State: loopdb.StateSuccess,
1621+
},
1622+
Time: insideBudget,
1623+
}
1624+
1625+
okQuote = &loop.LoopOutQuote{
1626+
SwapFee: 15,
1627+
PrepayAmount: 30,
1628+
MinerFee: 1,
1629+
}
1630+
1631+
rec = loop.OutRequest{
1632+
Amount: 7500,
1633+
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
1634+
MaxMinerFee: scaleMinerFee(okQuote.MinerFee),
1635+
MaxSwapFee: okQuote.SwapFee,
1636+
MaxPrepayAmount: okQuote.PrepayAmount,
1637+
SweepConfTarget: defaultConfTarget,
1638+
Initiator: autoloopSwapInitiator,
1639+
}
1640+
1641+
testPPM uint64 = 100000
1642+
)
1643+
1644+
rec.MaxPrepayRoutingFee, rec.MaxSwapRoutingFee = testPPMFees(
1645+
testPPM, okQuote, 7500,
1646+
)
1647+
1648+
tests := []struct {
1649+
name string
1650+
1651+
// loopIns is the set of loop in swaps that the client has
1652+
// performed.
1653+
loopIns []*loopdb.LoopIn
1654+
1655+
// suggestions is the set of swaps that we expect to be
1656+
// suggested given our current traffic.
1657+
suggestions *Suggestions
1658+
}{
1659+
{
1660+
name: "completed swap outside of budget",
1661+
loopIns: []*loopdb.LoopIn{
1662+
{
1663+
Loop: loopdb.Loop{
1664+
Events: []*loopdb.LoopEvent{
1665+
eventOutsideBudget,
1666+
},
1667+
},
1668+
Contract: contractOutsideBudget,
1669+
},
1670+
},
1671+
suggestions: &Suggestions{
1672+
OutSwaps: []loop.OutRequest{
1673+
rec,
1674+
},
1675+
DisqualifiedChans: noneDisqualified,
1676+
DisqualifiedPeers: noPeersDisqualified,
1677+
},
1678+
},
1679+
{
1680+
name: "completed within budget",
1681+
loopIns: []*loopdb.LoopIn{
1682+
{
1683+
Loop: loopdb.Loop{
1684+
Events: []*loopdb.LoopEvent{
1685+
successWithinBudget,
1686+
},
1687+
},
1688+
Contract: contractOutsideBudget,
1689+
},
1690+
},
1691+
suggestions: &Suggestions{
1692+
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
1693+
chanID1: ReasonBudgetElapsed,
1694+
},
1695+
DisqualifiedPeers: noPeersDisqualified,
1696+
},
1697+
},
1698+
{
1699+
name: "pending created before budget",
1700+
loopIns: []*loopdb.LoopIn{
1701+
{
1702+
Contract: contractOutsideBudget,
1703+
},
1704+
},
1705+
suggestions: &Suggestions{
1706+
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
1707+
chanID1: ReasonBudgetElapsed,
1708+
},
1709+
DisqualifiedPeers: noPeersDisqualified,
1710+
},
1711+
},
1712+
}
1713+
1714+
for _, testCase := range tests {
1715+
testCase := testCase
1716+
1717+
t.Run(testCase.name, func(t *testing.T) {
1718+
cfg, lnd := newTestConfig()
1719+
1720+
// Set our channel and rules so that we will need to
1721+
// swap 7500 sats and our fee limit is 10% of that
1722+
// amount (750 sats).
1723+
lnd.Channels = []lndclient.ChannelInfo{
1724+
channel1,
1725+
}
1726+
1727+
cfg.ListLoopIn = func() ([]*loopdb.LoopIn, error) {
1728+
return testCase.loopIns, nil
1729+
}
1730+
1731+
cfg.LoopOutQuote = func(_ context.Context,
1732+
_ *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote,
1733+
error) {
1734+
1735+
return okQuote, nil
1736+
}
1737+
1738+
params := defaultParameters
1739+
params.AutoFeeBudget = budget
1740+
params.AutoFeeStartDate = testBudgetStart
1741+
1742+
params.FeeLimit = NewFeePortion(testPPM)
1743+
params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
1744+
chanID1: chanRule,
1745+
}
1746+
1747+
// Allow more than one in flight swap, to ensure that
1748+
// we restrict based on budget, not in-flight.
1749+
params.MaxAutoInFlight = 2
1750+
1751+
testSuggestSwaps(
1752+
t, newSuggestSwapsSetup(cfg, lnd, params),
1753+
testCase.suggestions, nil,
1754+
)
1755+
})
1756+
}
1757+
}
1758+
15711759
// testSuggestSwapsSetup contains the elements that are used to create a
15721760
// suggest swaps test.
15731761
type testSuggestSwapsSetup struct {

0 commit comments

Comments
 (0)