From 965b99d4556754842128945d365c0d4b2cadb377 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 30 Nov 2021 13:18:32 +0200 Subject: [PATCH] liquidity: add existing loop in swaps to budget calculations --- liquidity/liquidity.go | 27 ++++- liquidity/liquidity_test.go | 196 +++++++++++++++++++++++++++++++++++- 2 files changed, 217 insertions(+), 6 deletions(-) diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 7e776ca..361736a 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -639,7 +639,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // Get a summary of our existing swaps so that we can check our autoloop // budget. - summary, err := m.checkExistingAutoLoops(ctx, loopOut) + summary, err := m.checkExistingAutoLoops(ctx, loopOut, loopIn) if err != nil { return nil, err } @@ -958,7 +958,8 @@ func (e *existingAutoLoopSummary) totalFees() btcutil.Amount { // total for our set of ongoing, automatically dispatched swaps as well as a // current in-flight count. func (m *Manager) checkExistingAutoLoops(ctx context.Context, - loopOuts []*loopdb.LoopOut) (*existingAutoLoopSummary, error) { + loopOuts []*loopdb.LoopOut, loopIns []*loopdb.LoopIn) ( + *existingAutoLoopSummary, error) { var summary existingAutoLoopSummary @@ -997,6 +998,28 @@ func (m *Manager) checkExistingAutoLoops(ctx context.Context, } } + for _, in := range loopIns { + if in.Contract.Label != labels.AutoloopLabel(swap.TypeIn) { + continue + } + + pending := in.State().State.Type() == loopdb.StateTypePending + inBudget := !in.LastUpdateTime().Before(m.params.AutoFeeStartDate) + + // If an autoloop is in a pending state, we always count it in + // our current budget, and record the worst-case fees for it, + // because we do not know how it will resolve. + if pending { + summary.inFlightCount++ + summary.pendingFees += worstCaseInFees( + in.Contract.MaxMinerFee, in.Contract.MaxSwapFee, + defaultLoopInSweepFee, + ) + } else if inBudget { + summary.spentFees += in.State().Cost.Total() + } + } + return &summary, nil } diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index d2c2f13..076d473 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -107,6 +107,13 @@ var ( OutgoingChanSet: loopdb.ChannelSet{999}, } + autoInContract = &loopdb.LoopInContract{ + SwapContract: loopdb.SwapContract{ + Label: labels.AutoloopLabel(swap.TypeIn), + InitiationTime: testBudgetStart, + }, + } + testRestrictions = NewRestrictions(1, 10000) // noneDisqualified can be used in tests where we don't have any @@ -1123,9 +1130,10 @@ func TestFeeBudget(t *testing.T) { // that are allowed. func TestInFlightLimit(t *testing.T) { tests := []struct { - name string - maxInFlight int - existingSwaps []*loopdb.LoopOut + name string + maxInFlight int + existingSwaps []*loopdb.LoopOut + existingInSwaps []*loopdb.LoopIn // peerRules will only be set (instead of test default values) // is it is non-nil. peerRules map[route.Vertex]*SwapRule @@ -1194,8 +1202,10 @@ func TestInFlightLimit(t *testing.T) { { Contract: autoOutContract, }, + }, + existingInSwaps: []*loopdb.LoopIn{ { - Contract: autoOutContract, + Contract: autoInContract, }, }, suggestions: &Suggestions{ @@ -1247,6 +1257,9 @@ func TestInFlightLimit(t *testing.T) { cfg.ListLoopOut = func() ([]*loopdb.LoopOut, error) { return testCase.existingSwaps, nil } + cfg.ListLoopIn = func() ([]*loopdb.LoopIn, error) { + return testCase.existingInSwaps, nil + } lnd.Channels = []lndclient.ChannelInfo{ channel1, channel2, @@ -1568,6 +1581,181 @@ func TestFeePercentage(t *testing.T) { } } +// TestBudgetWithLoopin tests that our autoloop budget accounts for loop in +// swaps that have been automatically dispatched. It tests out swaps that have +// already completed and those that are pending, inside and outside of our +// budget period to ensure that we account for all relevant swaps. +func TestBudgetWithLoopin(t *testing.T) { + var ( + budget btcutil.Amount = 10000 + + outsideBudget = testBudgetStart.Add(-5) + insideBudget = testBudgetStart.Add(5) + + contractOutsideBudget = &loopdb.LoopInContract{ + SwapContract: loopdb.SwapContract{ + InitiationTime: outsideBudget, + MaxSwapFee: budget, + }, + Label: labels.AutoloopLabel(swap.TypeIn), + } + + // Set our spend equal to our budget so we don't need to + // calculate exact costs. + eventOutsideBudget = &loopdb.LoopEvent{ + SwapStateData: loopdb.SwapStateData{ + Cost: loopdb.SwapCost{ + Server: budget, + }, + State: loopdb.StateSuccess, + }, + Time: outsideBudget, + } + + successWithinBudget = &loopdb.LoopEvent{ + SwapStateData: loopdb.SwapStateData{ + Cost: loopdb.SwapCost{ + Server: budget, + }, + State: loopdb.StateSuccess, + }, + Time: insideBudget, + } + + okQuote = &loop.LoopOutQuote{ + SwapFee: 15, + PrepayAmount: 30, + MinerFee: 1, + } + + rec = loop.OutRequest{ + Amount: 7500, + OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()}, + MaxMinerFee: scaleMinerFee(okQuote.MinerFee), + MaxSwapFee: okQuote.SwapFee, + MaxPrepayAmount: okQuote.PrepayAmount, + SweepConfTarget: defaultConfTarget, + Initiator: autoloopSwapInitiator, + } + + testPPM uint64 = 100000 + ) + + rec.MaxPrepayRoutingFee, rec.MaxSwapRoutingFee = testPPMFees( + testPPM, okQuote, 7500, + ) + + tests := []struct { + name string + + // loopIns is the set of loop in swaps that the client has + // performed. + loopIns []*loopdb.LoopIn + + // suggestions is the set of swaps that we expect to be + // suggested given our current traffic. + suggestions *Suggestions + }{ + { + name: "completed swap outside of budget", + loopIns: []*loopdb.LoopIn{ + { + Loop: loopdb.Loop{ + Events: []*loopdb.LoopEvent{ + eventOutsideBudget, + }, + }, + Contract: contractOutsideBudget, + }, + }, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + rec, + }, + DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, + }, + }, + { + name: "completed within budget", + loopIns: []*loopdb.LoopIn{ + { + Loop: loopdb.Loop{ + Events: []*loopdb.LoopEvent{ + successWithinBudget, + }, + }, + Contract: contractOutsideBudget, + }, + }, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonBudgetElapsed, + }, + DisqualifiedPeers: noPeersDisqualified, + }, + }, + { + name: "pending created before budget", + loopIns: []*loopdb.LoopIn{ + { + Contract: contractOutsideBudget, + }, + }, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonBudgetElapsed, + }, + DisqualifiedPeers: noPeersDisqualified, + }, + }, + } + + for _, testCase := range tests { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + cfg, lnd := newTestConfig() + + // Set our channel and rules so that we will need to + // swap 7500 sats and our fee limit is 10% of that + // amount (750 sats). + lnd.Channels = []lndclient.ChannelInfo{ + channel1, + } + + cfg.ListLoopIn = func() ([]*loopdb.LoopIn, error) { + return testCase.loopIns, nil + } + + cfg.LoopOutQuote = func(_ context.Context, + _ *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote, + error) { + + return okQuote, nil + } + + params := defaultParameters + params.AutoFeeBudget = budget + params.AutoFeeStartDate = testBudgetStart + + params.FeeLimit = NewFeePortion(testPPM) + params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ + chanID1: chanRule, + } + + // Allow more than one in flight swap, to ensure that + // we restrict based on budget, not in-flight. + params.MaxAutoInFlight = 2 + + testSuggestSwaps( + t, newSuggestSwapsSetup(cfg, lnd, params), + testCase.suggestions, nil, + ) + }) + } +} + // testSuggestSwapsSetup contains the elements that are used to create a // suggest swaps test. type testSuggestSwapsSetup struct {