From 9a9766775a4d3030015a89cfa5a9ba762a5a4151 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Thu, 4 May 2023 15:51:22 +0300 Subject: [PATCH] liquidity: add easy autoloop test --- liquidity/autoloop_test.go | 222 +++++++++++++++++++++++-- liquidity/autoloop_testcontext_test.go | 58 +++++++ 2 files changed, 262 insertions(+), 18 deletions(-) diff --git a/liquidity/autoloop_test.go b/liquidity/autoloop_test.go index b682c5c..ecab8a8 100644 --- a/liquidity/autoloop_test.go +++ b/liquidity/autoloop_test.go @@ -207,7 +207,7 @@ func TestAutoLoopEnabled(t *testing.T) { Events: []*loopdb.LoopEvent{ { SwapStateData: loopdb.SwapStateData{ - State: loopdb.StateInitiated, + State: loopdb.StateSuccess, }, }, }, @@ -472,7 +472,7 @@ func TestAutoloopAddress(t *testing.T) { Events: []*loopdb.LoopEvent{ { SwapStateData: loopdb.SwapStateData{ - State: loopdb.StateHtlcPublished, + State: loopdb.StateSuccess, }, }, }, @@ -647,7 +647,7 @@ func TestCompositeRules(t *testing.T) { Events: []*loopdb.LoopEvent{ { SwapStateData: loopdb.SwapStateData{ - State: loopdb.StateHtlcPublished, + State: loopdb.StateSuccess, }, }, }, @@ -984,7 +984,7 @@ func TestAutoloopBothTypes(t *testing.T) { Events: []*loopdb.LoopEvent{ { SwapStateData: loopdb.SwapStateData{ - State: loopdb.StateHtlcPublished, + State: loopdb.SwapState(loopdb.StateSuccess), }, }, }, @@ -1162,15 +1162,28 @@ func TestAutoLoopRecurringBudget(t *testing.T) { }, }, } + + singleLoopOut = &loopdb.LoopOut{ + Loop: loopdb.Loop{ + Events: []*loopdb.LoopEvent{ + { + SwapStateData: loopdb.SwapStateData{ + State: loopdb.SwapState(loopdb.StateSuccess), + }, + }, + }, + }, + } ) // 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, + minAmt: 1, + maxAmt: amt + 1, + quotesOut: quotes1, + expectedOut: loopOuts1, + existingOutSingle: singleLoopOut, } c.autoloop(step) @@ -1188,11 +1201,12 @@ func TestAutoLoopRecurringBudget(t *testing.T) { } step = &autoloopStep{ - minAmt: 1, - maxAmt: amt + 1, - quotesOut: quotes2, - existingOut: existing, - expectedOut: nil, + minAmt: 1, + maxAmt: amt + 1, + quotesOut: quotes2, + existingOut: existing, + expectedOut: nil, + existingOutSingle: singleLoopOut, } // Tick again, we should expect no loop outs because our budget would be // exceeded. @@ -1222,11 +1236,12 @@ func TestAutoLoopRecurringBudget(t *testing.T) { c.testClock.SetTime(testTime.Add(time.Hour * 25)) step = &autoloopStep{ - minAmt: 1, - maxAmt: amt + 1, - quotesOut: quotes2, - existingOut: existing2, - expectedOut: loopOuts2, + minAmt: 1, + maxAmt: amt + 1, + quotesOut: quotes2, + existingOut: existing2, + expectedOut: loopOuts2, + existingOutSingle: singleLoopOut, } // Tick again, we should expect a loop out to occur on the 2nd channel. @@ -1235,6 +1250,177 @@ func TestAutoLoopRecurringBudget(t *testing.T) { c.stop() } +// TestEasyAutoloop tests that the easy autoloop logic works as expected. This +// involves testing that channels are correctly selected and that the balance +// target is successfully met. +func TestEasyAutoloop(t *testing.T) { + defer test.Guard(t) + + // We need to change the default channels we use for tests so that they + // have different local balances in order to know which one is going to + // be selected by easy autoloop. + easyChannel1 := lndclient.ChannelInfo{ + Active: true, + ChannelID: chanID1.ToUint64(), + PubKeyBytes: peer1, + LocalBalance: 95000, + RemoteBalance: 0, + Capacity: 100000, + } + + easyChannel2 := lndclient.ChannelInfo{ + Active: true, + ChannelID: chanID1.ToUint64(), + PubKeyBytes: peer1, + LocalBalance: 75000, + RemoteBalance: 0, + Capacity: 100000, + } + + var ( + channels = []lndclient.ChannelInfo{ + easyChannel1, easyChannel2, + } + + params = Parameters{ + Autoloop: true, + AutoFeeBudget: 36000, + AutoFeeRefreshPeriod: time.Hour * 3, + AutoloopBudgetLastRefresh: testBudgetStart, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + SweepConfTarget: 10, + HtlcConfTarget: defaultHtlcConfTarget, + EasyAutoloop: true, + EasyAutoloopTarget: 75000, + FeeLimit: defaultFeePortion(), + } + ) + + c := newAutoloopTestCtx(t, params, channels, testRestrictions) + c.start() + + var ( + maxAmt = 50000 + + chan1Swap = &loop.OutRequest{ + Amount: btcutil.Amount(maxAmt), + OutgoingChanSet: loopdb.ChannelSet{easyChannel1.ChannelID}, + Label: labels.AutoloopLabel(swap.TypeOut), + Initiator: autoloopSwapInitiator, + } + + quotesOut1 = []quoteRequestResp{ + { + request: &loop.LoopOutQuoteRequest{ + Amount: btcutil.Amount(maxAmt), + }, + quote: &loop.LoopOutQuote{ + SwapFee: 1, + PrepayAmount: 1, + MinerFee: 1, + }, + }, + } + + loopOut1 = []loopOutRequestResp{ + { + request: chan1Swap, + response: &loop.LoopOutSwapInfo{ + SwapHash: lntypes.Hash{1}, + }, + }, + } + ) + + // We expected one max size swap to be dispatched on our channel with + // the biggest local balance. + step := &easyAutoloopStep{ + minAmt: 1, + maxAmt: 50000, + quotesOut: quotesOut1, + expectedOut: loopOut1, + } + + c.easyautoloop(step, false) + c.stop() + + // In order to reflect the change on the channel balances we create a + // new context and restart the autolooper. + easyChannel1.LocalBalance -= chan1Swap.Amount + channels = []lndclient.ChannelInfo{ + easyChannel1, easyChannel2, + } + + c = newAutoloopTestCtx(t, params, channels, testRestrictions) + c.start() + + var ( + amt2 = 45_000 + + chan2Swap = &loop.OutRequest{ + Amount: btcutil.Amount(amt2), + OutgoingChanSet: loopdb.ChannelSet{easyChannel2.ChannelID}, + Label: labels.AutoloopLabel(swap.TypeOut), + Initiator: autoloopSwapInitiator, + } + + quotesOut2 = []quoteRequestResp{ + { + request: &loop.LoopOutQuoteRequest{ + Amount: btcutil.Amount(amt2), + }, + quote: &loop.LoopOutQuote{ + SwapFee: 1, + PrepayAmount: 1, + MinerFee: 1, + }, + }, + } + + loopOut2 = []loopOutRequestResp{ + { + request: chan2Swap, + response: &loop.LoopOutSwapInfo{ + SwapHash: lntypes.Hash{1}, + }, + }, + } + ) + + // We expect a swap of size 45_000 to be dispatched in order to meet the + // defined target of 75_000. + step = &easyAutoloopStep{ + minAmt: 1, + maxAmt: 50000, + quotesOut: quotesOut2, + expectedOut: loopOut2, + } + + c.easyautoloop(step, false) + c.stop() + + // In order to reflect the change on the channel balances we create a + // new context and restart the autolooper. + easyChannel2.LocalBalance -= btcutil.Amount(amt2) + channels = []lndclient.ChannelInfo{ + easyChannel1, easyChannel2, + } + + c = newAutoloopTestCtx(t, params, channels, testRestrictions) + c.start() + + // We have met the target of 75_000 so we don't expect any action from + // easy autoloop. That's why we set noop to true in the call below. + step = &easyAutoloopStep{ + minAmt: 1, + maxAmt: 50000, + } + + c.easyautoloop(step, true) + 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, diff --git a/liquidity/autoloop_testcontext_test.go b/liquidity/autoloop_testcontext_test.go index 8ecf27e..bbaf2b5 100644 --- a/liquidity/autoloop_testcontext_test.go +++ b/liquidity/autoloop_testcontext_test.go @@ -19,6 +19,11 @@ import ( "github.com/stretchr/testify/require" ) +const ( + defaultEventuallyTimeout = time.Second * 45 + defaultEventuallyInterval = time.Millisecond * 100 +) + type autoloopTestCtx struct { t *testing.T manager *Manager @@ -272,6 +277,15 @@ type autoloopStep struct { keepDestAddr bool } +type easyAutoloopStep struct { + minAmt btcutil.Amount + maxAmt btcutil.Amount + existingOut []*loopdb.LoopOut + existingIn []*loopdb.LoopIn + quotesOut []quoteRequestResp + expectedOut []loopOutRequestResp +} + // autoloop walks our test context through the process of triggering our // autoloop functionality, providing mocked values as required. The set of // quotes provided indicates that we expect swap suggestions to be made (since @@ -325,6 +339,50 @@ func (c *autoloopTestCtx) autoloop(step *autoloopStep) { require.True(c.t, c.matchLoopOuts(step.expectedOut, step.keepDestAddr)) require.True(c.t, c.matchLoopIns(step.expectedIn)) + + require.Eventuallyf(c.t, func() bool { + return c.manager.numActiveStickyLoops() == 0 + }, defaultEventuallyTimeout, defaultEventuallyInterval, "failed to"+ + " wait for sticky loop counter") +} + +// easyautoloop walks our test context through the process of triggering our +// easy autoloop functionality, providing mocked values as required. The number +// of values needed to mock easy autoloop are less than standard autoloop as the +// goal of easy autoloop is to simplify its usage. +func (c *autoloopTestCtx) easyautoloop(step *easyAutoloopStep, noop bool) { + // Tick our autoloop ticker to force assessing whether we want to loop. + c.manager.cfg.AutoloopTicker.Force <- testTime + + // Provide the liquidity manager with our desired existing set of swaps. + c.loopOuts <- step.existingOut + c.loopIns <- step.existingIn + + // If easy autoloop is not meant to be triggered we skip sending the + // mock response for restrictions, as this is never called. + if !noop { + // Send a mocked response from the server with the swap size limits. + c.loopOutRestrictions <- NewRestrictions(step.minAmt, step.maxAmt) + } + + for _, expected := range step.quotesOut { + request := <-c.quoteRequest + require.Equal( + c.t, expected.request.Amount, request.Amount, + ) + + c.quotes <- expected.quote + } + + for _, expected := range step.expectedOut { + actual := <-c.outRequest + + require.Equal(c.t, expected.request.Amount, actual.Amount) + require.Equal( + c.t, expected.request.OutgoingChanSet, + actual.OutgoingChanSet, + ) + } } // matchLoopOuts checks that the actual loop out requests we got match the