From 3b896babc62cd424e8a1c50cbeb6f63610a35c72 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Mon, 20 Feb 2023 23:53:59 +0200 Subject: [PATCH] liquidity: add test for recurring budget --- liquidity/autoloop_test.go | 205 +++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/liquidity/autoloop_test.go b/liquidity/autoloop_test.go index b70a7d9..bcc07a5 100644 --- a/liquidity/autoloop_test.go +++ b/liquidity/autoloop_test.go @@ -966,6 +966,211 @@ func TestAutoloopBothTypes(t *testing.T) { c.stop() } +// TestAutoLoopRecurringBudget tests that the autolooper will perform swaps that +// respect the fee budget, and that it will refresh the budget based on the +// defined refresh period. +func TestAutoLoopRecurringBudget(t *testing.T) { + defer test.Guard(t)() + + var ( + channels = []lndclient.ChannelInfo{ + channel1, channel2, + } + + swapFeePPM uint64 = 1000 + routeFeePPM uint64 = 1000 + prepayFeePPM uint64 = 1000 + prepayAmount = btcutil.Amount(20000) + maxMiner = btcutil.Amount(20000) + + params = Parameters{ + Autoloop: true, + AutoFeeBudget: 36000, + AutoFeeRefreshPeriod: time.Hour * 3, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + SweepConfTarget: 10, + FeeLimit: NewFeeCategoryLimit( + swapFeePPM, routeFeePPM, prepayFeePPM, maxMiner, + prepayAmount, 20000, + ), + ChannelRules: map[lnwire.ShortChannelID]*SwapRule{ + chanID1: chanRule, + chanID2: chanRule, + }, + HtlcConfTarget: defaultHtlcConfTarget, + } + ) + + c := newAutoloopTestCtx(t, params, channels, testRestrictions) + c.start() + + // Calculate our maximum allowed fees and create quotes that fall within + // our budget. + var ( + amt = chan1Rec.Amount + + maxSwapFee = ppmToSat(amt, swapFeePPM) + + // Create a quote that is within our limits. We do not set miner + // fee because this value is not actually set by the server. + quote1 = &loop.LoopOutQuote{ + SwapFee: maxSwapFee, + PrepayAmount: prepayAmount - 10, + MinerFee: maxMiner - 10, + } + + quote2 = &loop.LoopOutQuote{ + SwapFee: maxSwapFee, + PrepayAmount: prepayAmount - 20, + MinerFee: maxMiner - 10, + } + + quoteRequest = &loop.LoopOutQuoteRequest{ + Amount: amt, + SweepConfTarget: params.SweepConfTarget, + } + + quotes1 = []quoteRequestResp{ + { + request: quoteRequest, + quote: quote1, + }, + { + request: quoteRequest, + quote: quote2, + }, + } + + quotes2 = []quoteRequestResp{ + { + request: quoteRequest, + quote: quote2, + }, + } + + maxRouteFee = ppmToSat(amt, routeFeePPM) + + chan1Swap = &loop.OutRequest{ + Amount: amt, + MaxSwapRoutingFee: maxRouteFee, + MaxPrepayRoutingFee: ppmToSat( + quote1.PrepayAmount, prepayFeePPM, + ), + MaxSwapFee: quote1.SwapFee, + MaxPrepayAmount: quote1.PrepayAmount, + MaxMinerFee: maxMiner, + SweepConfTarget: params.SweepConfTarget, + OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()}, + Label: labels.AutoloopLabel(swap.TypeOut), + Initiator: autoloopSwapInitiator, + } + + chan2Swap = &loop.OutRequest{ + Amount: amt, + MaxSwapRoutingFee: maxRouteFee, + MaxPrepayRoutingFee: ppmToSat( + quote2.PrepayAmount, routeFeePPM, + ), + MaxSwapFee: quote2.SwapFee, + MaxPrepayAmount: quote2.PrepayAmount, + MaxMinerFee: maxMiner, + SweepConfTarget: params.SweepConfTarget, + OutgoingChanSet: loopdb.ChannelSet{chanID2.ToUint64()}, + Label: labels.AutoloopLabel(swap.TypeOut), + Initiator: autoloopSwapInitiator, + } + + loopOuts1 = []loopOutRequestResp{ + { + request: chan1Swap, + response: &loop.LoopOutSwapInfo{ + SwapHash: lntypes.Hash{1}, + }, + }, + } + + loopOuts2 = []loopOutRequestResp{ + { + request: chan2Swap, + response: &loop.LoopOutSwapInfo{ + SwapHash: lntypes.Hash{1}, + }, + }, + } + ) + + // Tick our autolooper with no existing swaps, we expect a loop out + // swap to be dispatched on first channel. + step := &autoloopStep{ + minAmt: 1, + maxAmt: amt + 1, + quotesOut: quotes1, + expectedOut: loopOuts1, + } + c.autoloop(step) + + existing := []*loopdb.LoopOut{ + existingSwapFromRequest( + chan1Swap, testTime, []*loopdb.LoopEvent{ + { + SwapStateData: loopdb.SwapStateData{ + State: loopdb.StateInitiated, + }, + Time: testTime, + }, + }, + ), + } + + step = &autoloopStep{ + minAmt: 1, + maxAmt: amt + 1, + quotesOut: quotes2, + existingOut: existing, + expectedOut: nil, + } + // Tick again, we should expect no loop outs because our budget would be + // exceeded. + c.autoloop(step) + + // Create the existing entry for the first swap, marking its last update + // with success and a specific timestamp. + existing2 := []*loopdb.LoopOut{ + existingSwapFromRequest( + chan1Swap, testTime, []*loopdb.LoopEvent{ + { + SwapStateData: loopdb.SwapStateData{ + State: loopdb.StateSuccess, + }, + Time: testTime, + }, + }, + ), + } + + // Apply the balance shifts on the channels in order to get the correct + // recommendations on next tick. + c.lnd.Channels[0].LocalBalance = 2500 + c.lnd.Channels[0].RemoteBalance = 7500 + + // Advance time to the future, causing a budget refresh. + c.testClock.SetTime(testTime.Add(time.Hour * 25)) + + step = &autoloopStep{ + minAmt: 1, + maxAmt: amt + 1, + quotesOut: quotes2, + existingOut: existing2, + expectedOut: loopOuts2, + } + + // Tick again, we should expect a loop out to occur on the 2nd channel. + c.autoloop(step) + + c.stop() +} + // existingSwapFromRequest is a helper function which returns the db // representation of a loop out request with the event set provided. func existingSwapFromRequest(request *loop.OutRequest, initTime time.Time,