mirror of
https://github.com/lightninglabs/loop
synced 2024-11-09 19:10:47 +00:00
3be5a37cd3
This failure became normal recently: === RUN TestAutoLoopInEnabled autoloop_testcontext_test.go:318: Error Trace: /home/runner/work/loop/loop/liquidity/autoloop_testcontext_test.go:318 /home/runner/work/loop/loop/liquidity/autoloop_test.go:804 Error: Not equal: expected: 80000 actual : 160000 Test: TestAutoLoopInEnabled autoloop_testcontext_test.go:318: Error Trace: /home/runner/work/loop/loop/liquidity/autoloop_testcontext_test.go:318 /home/runner/work/loop/loop/liquidity/autoloop_test.go:804 Error: Not equal: expected: 160000 actual : 80000 Test: TestAutoLoopInEnabled autoloop_testcontext_test.go:343: Error Trace: /home/runner/work/loop/loop/liquidity/autoloop_testcontext_test.go:343 /home/runner/work/loop/loop/liquidity/autoloop_test.go:804 Error: Should be true Test: TestAutoLoopInEnabled The root cause is them the order of items in c.quoteRequestIn depends on the order of loopInBuilder.buildSwap calls, which depends on the order of channel handling in Manager.SuggestSwaps, which depends on the order of map traversal, which is not determitistic. Since in the test all the amounts are different, I used amount as a key and put the expected calls into a map using amount as a key. When I extract an item from c.quoteRequestIn channel, I find the corresponding item in the map and remove it. All other logic is preserved.
518 lines
15 KiB
Go
518 lines
15 KiB
Go
package liquidity
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightninglabs/loop"
|
|
"github.com/lightninglabs/loop/loopdb"
|
|
"github.com/lightninglabs/loop/swap"
|
|
"github.com/lightninglabs/loop/test"
|
|
"github.com/lightningnetwork/lnd/clock"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/ticker"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
defaultEventuallyTimeout = time.Second * 45
|
|
defaultEventuallyInterval = time.Millisecond * 100
|
|
)
|
|
|
|
type autoloopTestCtx struct {
|
|
t *testing.T
|
|
manager *Manager
|
|
lnd *test.LndMockServices
|
|
testClock *clock.TestClock
|
|
|
|
// quoteRequests is a channel that requests for quotes are pushed into.
|
|
quoteRequest chan *loop.LoopOutQuoteRequest
|
|
|
|
// quotes is a channel that we get loop out quote requests on.
|
|
quotes chan *loop.LoopOutQuote
|
|
|
|
// quoteRequestIn is a channel that requests for loop in quotes are
|
|
// pushed into.
|
|
quoteRequestIn chan *loop.LoopInQuoteRequest
|
|
|
|
// quotesIn is a channel that we get loop in quote responses on.
|
|
quotesIn chan *loop.LoopInQuote
|
|
|
|
// loopOutRestrictions is a channel that we get the server's
|
|
// restrictions on.
|
|
loopOutRestrictions chan *Restrictions
|
|
|
|
// loopInRestrictions is a channel that we get the server's
|
|
// loop in restrictions on.
|
|
loopInRestrictions chan *Restrictions
|
|
|
|
// loopOuts is a channel that we get existing loop out swaps on.
|
|
loopOuts chan []*loopdb.LoopOut
|
|
|
|
// loopOutSingle is the single loop out returned from fetching a single
|
|
// swap from store.
|
|
loopOutSingle *loopdb.LoopOut
|
|
|
|
// loopIns is a channel that we get existing loop in swaps on.
|
|
loopIns chan []*loopdb.LoopIn
|
|
|
|
// loopInSingle is the single loop in returned from fetching a single
|
|
// swap from store.
|
|
loopInSingle *loopdb.LoopIn
|
|
|
|
// restrictions is a channel that we get swap restrictions on.
|
|
restrictions chan *Restrictions
|
|
|
|
// outRequest is a channel that requests to dispatch loop outs are
|
|
// pushed into.
|
|
outRequest chan *loop.OutRequest
|
|
|
|
// loopOut is a channel that we return loop out responses on.
|
|
loopOut chan *loop.LoopOutSwapInfo
|
|
|
|
// inRequest is a channel that requests to dispatch loop in swaps are
|
|
// pushed into.
|
|
inRequest chan *loop.LoopInRequest
|
|
|
|
// loopIn is a channel that we return loop in responses on.
|
|
loopIn chan *loop.LoopInSwapInfo
|
|
|
|
// errChan is a channel that we send run errors into.
|
|
errChan chan error
|
|
|
|
// cancelCtx cancels the context that our liquidity manager is run with.
|
|
// This can be used to cleanly shutdown the test. Note that this will be
|
|
// nil until the test context has been started.
|
|
cancelCtx func()
|
|
}
|
|
|
|
// newAutoloopTestCtx creates a test context with custom liquidity manager
|
|
// parameters and lnd channels.
|
|
func newAutoloopTestCtx(t *testing.T, parameters Parameters,
|
|
channels []lndclient.ChannelInfo,
|
|
server *Restrictions) *autoloopTestCtx {
|
|
|
|
// Create a mock lnd and set our expected fee rate for sweeps to our
|
|
// sweep fee rate limit value.
|
|
lnd := test.NewMockLnd()
|
|
|
|
categories, ok := parameters.FeeLimit.(*FeeCategoryLimit)
|
|
if ok {
|
|
lnd.SetFeeEstimate(
|
|
parameters.SweepConfTarget, categories.SweepFeeRateLimit,
|
|
)
|
|
}
|
|
|
|
testCtx := &autoloopTestCtx{
|
|
t: t,
|
|
testClock: clock.NewTestClock(testTime),
|
|
lnd: lnd,
|
|
|
|
quoteRequest: make(chan *loop.LoopOutQuoteRequest),
|
|
quotes: make(chan *loop.LoopOutQuote),
|
|
quoteRequestIn: make(chan *loop.LoopInQuoteRequest),
|
|
quotesIn: make(chan *loop.LoopInQuote),
|
|
loopOutRestrictions: make(chan *Restrictions),
|
|
loopInRestrictions: make(chan *Restrictions),
|
|
loopOuts: make(chan []*loopdb.LoopOut),
|
|
loopIns: make(chan []*loopdb.LoopIn),
|
|
restrictions: make(chan *Restrictions),
|
|
outRequest: make(chan *loop.OutRequest),
|
|
loopOut: make(chan *loop.LoopOutSwapInfo),
|
|
inRequest: make(chan *loop.LoopInRequest),
|
|
loopIn: make(chan *loop.LoopInSwapInfo),
|
|
errChan: make(chan error, 1),
|
|
}
|
|
|
|
// Set lnd's channels to equal the set of channels we want for our
|
|
// test.
|
|
testCtx.lnd.Channels = channels
|
|
|
|
cfg := &Config{
|
|
AutoloopTicker: ticker.NewForce(DefaultAutoloopTicker),
|
|
Restrictions: func(_ context.Context, swapType swap.Type, initiator string) (*Restrictions,
|
|
error) {
|
|
|
|
if swapType == swap.TypeOut {
|
|
return <-testCtx.loopOutRestrictions, nil
|
|
}
|
|
|
|
return <-testCtx.loopInRestrictions, nil
|
|
},
|
|
ListLoopOut: func(context.Context) ([]*loopdb.LoopOut, error) {
|
|
return <-testCtx.loopOuts, nil
|
|
},
|
|
GetLoopOut: func(ctx context.Context,
|
|
hash lntypes.Hash) (*loopdb.LoopOut, error) {
|
|
|
|
return testCtx.loopOutSingle, nil
|
|
},
|
|
ListLoopIn: func(context.Context) ([]*loopdb.LoopIn, error) {
|
|
return <-testCtx.loopIns, nil
|
|
},
|
|
LoopOutQuote: func(_ context.Context,
|
|
req *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote,
|
|
error) {
|
|
|
|
testCtx.quoteRequest <- req
|
|
|
|
return <-testCtx.quotes, nil
|
|
},
|
|
LoopOut: func(_ context.Context,
|
|
req *loop.OutRequest) (*loop.LoopOutSwapInfo,
|
|
error) {
|
|
|
|
testCtx.outRequest <- req
|
|
|
|
return <-testCtx.loopOut, nil
|
|
},
|
|
LoopInQuote: func(_ context.Context,
|
|
req *loop.LoopInQuoteRequest) (*loop.LoopInQuote, error) {
|
|
|
|
testCtx.quoteRequestIn <- req
|
|
|
|
return <-testCtx.quotesIn, nil
|
|
},
|
|
LoopIn: func(_ context.Context,
|
|
req *loop.LoopInRequest) (*loop.LoopInSwapInfo, error) {
|
|
|
|
testCtx.inRequest <- req
|
|
|
|
return <-testCtx.loopIn, nil
|
|
},
|
|
MinimumConfirmations: loop.DefaultSweepConfTarget,
|
|
Lnd: &testCtx.lnd.LndServices,
|
|
Clock: testCtx.testClock,
|
|
PutLiquidityParams: func(_ context.Context, _ []byte) error {
|
|
return nil
|
|
},
|
|
FetchLiquidityParams: func(context.Context) ([]byte, error) {
|
|
return nil, nil
|
|
},
|
|
}
|
|
|
|
// SetParameters needs to make a call to our mocked restrictions call,
|
|
// which will block, so we push our test values in a goroutine.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
testCtx.loopOutRestrictions <- server
|
|
close(done)
|
|
}()
|
|
|
|
// Create a manager with our test config and set our starting set of
|
|
// parameters.
|
|
testCtx.manager = NewManager(cfg)
|
|
err := testCtx.manager.setParameters(context.Background(), parameters)
|
|
assert.NoError(t, err)
|
|
// Override the payments check interval for the tests in order to not
|
|
// timeout.
|
|
testCtx.manager.params.CustomPaymentCheckInterval =
|
|
150 * time.Millisecond
|
|
<-done
|
|
return testCtx
|
|
}
|
|
|
|
// start starts our liquidity manager's run loop in a goroutine. Tests should
|
|
// be run with test.Guard() to ensure that this does not leak.
|
|
func (c *autoloopTestCtx) start() {
|
|
ctx := context.Background()
|
|
ctx, c.cancelCtx = context.WithCancel(ctx)
|
|
|
|
go func() {
|
|
c.errChan <- c.manager.Run(ctx)
|
|
}()
|
|
}
|
|
|
|
// stop shuts down our test context and asserts that we have exited with a
|
|
// context-cancelled error.
|
|
func (c *autoloopTestCtx) stop() {
|
|
c.cancelCtx()
|
|
assert.Equal(c.t, context.Canceled, <-c.errChan)
|
|
}
|
|
|
|
// quoteRequestResp pairs an expected swap quote request with the response we
|
|
// would like to provide the liquidity manager with.
|
|
type quoteRequestResp struct {
|
|
request *loop.LoopOutQuoteRequest
|
|
quote *loop.LoopOutQuote
|
|
}
|
|
|
|
// loopOutRequestResp pairs an expected loop out request with the response we
|
|
// would like the server to respond with.
|
|
type loopOutRequestResp struct {
|
|
request *loop.OutRequest
|
|
response *loop.LoopOutSwapInfo
|
|
}
|
|
|
|
// quoteInRequestResp pairs an expected loop in quote request with the response
|
|
// we would like to provide the manager with.
|
|
type quoteInRequestResp struct {
|
|
request *loop.LoopInQuoteRequest
|
|
quote *loop.LoopInQuote
|
|
}
|
|
|
|
// loopInRequestResp pairs and expected loop in request with the response we
|
|
// would like the mocked server to respond with.
|
|
type loopInRequestResp struct {
|
|
request *loop.LoopInRequest
|
|
response *loop.LoopInSwapInfo
|
|
}
|
|
|
|
// autoloopStep contains all of the information to required to step
|
|
// through an autoloop tick.
|
|
type autoloopStep struct {
|
|
minAmt btcutil.Amount
|
|
maxAmt btcutil.Amount
|
|
existingOut []*loopdb.LoopOut
|
|
existingOutSingle *loopdb.LoopOut
|
|
existingIn []*loopdb.LoopIn
|
|
existingInSingle *loopdb.LoopIn
|
|
quotesOut []quoteRequestResp
|
|
quotesIn []quoteInRequestResp
|
|
expectedOut []loopOutRequestResp
|
|
expectedIn []loopInRequestResp
|
|
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
|
|
// we will query for a quote for each suggested swap). The set of expected
|
|
// swaps indicates whether we expect any of these swap suggestions to actually
|
|
// be dispatched by the autolooper.
|
|
func (c *autoloopTestCtx) autoloop(step *autoloopStep) {
|
|
// Tick our autoloop ticker to force assessing whether we want to loop.
|
|
c.manager.cfg.AutoloopTicker.Force <- testTime
|
|
|
|
// Send a mocked response from the server with the swap size limits.
|
|
c.loopOutRestrictions <- NewRestrictions(step.minAmt, step.maxAmt)
|
|
c.loopInRestrictions <- NewRestrictions(step.minAmt, step.maxAmt)
|
|
|
|
// Provide the liquidity manager with our desired existing set of swaps.
|
|
c.loopOuts <- step.existingOut
|
|
c.loopIns <- step.existingIn
|
|
|
|
c.loopOutSingle = step.existingOutSingle
|
|
c.loopInSingle = step.existingInSingle
|
|
|
|
// Assert that we query the server for a quote for each of our
|
|
// recommended swaps. Note that this differs from our set of expected
|
|
// swaps because we may get quotes for suggested swaps but then just
|
|
// log them. The order in c.quoteRequestIn is not deterministic,
|
|
// it depends on the order of map traversal (map peerChannels in
|
|
// method Manager.SuggestSwaps). So receive from the channel an item
|
|
// and then find a corresponding expected item, using amount as a key.
|
|
amt2expected := make(map[btcutil.Amount]quoteInRequestResp)
|
|
for _, expected := range step.quotesIn {
|
|
// Make sure all amounts are unique.
|
|
require.NotContains(c.t, amt2expected, expected.request.Amount)
|
|
|
|
amt2expected[expected.request.Amount] = expected
|
|
}
|
|
|
|
for i := 0; i < len(step.quotesIn); i++ {
|
|
request := <-c.quoteRequestIn
|
|
|
|
// Get the expected item, using amount as a key.
|
|
expected, has := amt2expected[request.Amount]
|
|
require.True(c.t, has)
|
|
delete(amt2expected, request.Amount)
|
|
|
|
assert.Equal(
|
|
c.t, expected.request.Amount, request.Amount,
|
|
)
|
|
|
|
assert.Equal(
|
|
c.t, expected.request.HtlcConfTarget,
|
|
request.HtlcConfTarget,
|
|
)
|
|
|
|
c.quotesIn <- expected.quote
|
|
}
|
|
|
|
for _, expected := range step.quotesOut {
|
|
request := <-c.quoteRequest
|
|
assert.Equal(
|
|
c.t, expected.request.Amount, request.Amount,
|
|
)
|
|
assert.Equal(
|
|
c.t, expected.request.SweepConfTarget,
|
|
request.SweepConfTarget,
|
|
)
|
|
c.quotes <- expected.quote
|
|
}
|
|
|
|
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")
|
|
|
|
// Since we're checking if any false-positive swaps were dispatched we
|
|
// need to give some time to autoloop to possibly dispatch them.
|
|
select {
|
|
case <-c.outRequest:
|
|
c.t.Fatal("expected no more loopout requests")
|
|
|
|
case <-c.inRequest:
|
|
c.t.Fatal("expected no more loopin requests")
|
|
|
|
case <-c.quoteRequestIn:
|
|
c.t.Fatal("expected no more loopout quote requests")
|
|
|
|
case <-c.quoteRequest:
|
|
c.t.Fatal("expected no more loopin quote requests")
|
|
|
|
case <-time.After(500 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
)
|
|
if expected.request.DestAddr != nil {
|
|
require.Equal(
|
|
c.t, expected.request.DestAddr, actual.DestAddr,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Since we're checking if any false-positive swaps were dispatched we
|
|
// need to give some time to autoloop to possibly dispatch them.
|
|
select {
|
|
case <-c.outRequest:
|
|
c.t.Fatal("expected no more loopout requests")
|
|
|
|
case <-c.inRequest:
|
|
c.t.Fatal("expected no more loopin requests")
|
|
|
|
case <-c.quoteRequestIn:
|
|
c.t.Fatal("expected no more loopout quote requests")
|
|
|
|
case <-c.quoteRequest:
|
|
c.t.Fatal("expected no more loopin quote requests")
|
|
|
|
case <-time.After(500 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
// matchLoopOuts checks that the actual loop out requests we got match the
|
|
// expected ones. The argument keepDestAddr is used to indicate whether we keep
|
|
// the actual loops destination address for the comparison. This is useful
|
|
// because we don't want to compare the destination address generated by the
|
|
// wallet mock. We want to compare the destination address when testing the
|
|
// autoloop DestAddr parameter for loop outs.
|
|
func (c *autoloopTestCtx) matchLoopOuts(swaps []loopOutRequestResp,
|
|
keepDestAddr bool) bool {
|
|
|
|
swapsCopy := make([]loopOutRequestResp, len(swaps))
|
|
copy(swapsCopy, swaps)
|
|
|
|
length := len(swapsCopy)
|
|
|
|
for i := 0; i < length; i++ {
|
|
actual := <-c.outRequest
|
|
|
|
if !keepDestAddr {
|
|
actual.DestAddr = nil
|
|
}
|
|
|
|
inner:
|
|
for index, swap := range swapsCopy {
|
|
equal := reflect.DeepEqual(swap.request, actual)
|
|
|
|
if equal {
|
|
c.loopOut <- swap.response
|
|
|
|
swapsCopy = append(
|
|
swapsCopy[:index],
|
|
swapsCopy[index+1:]...,
|
|
)
|
|
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
|
|
return len(swapsCopy) == 0
|
|
}
|
|
|
|
// matchLoopIns checks that the actual loop in requests we got match the
|
|
// expected ones.
|
|
func (c *autoloopTestCtx) matchLoopIns(
|
|
swaps []loopInRequestResp) bool {
|
|
|
|
swapsCopy := make([]loopInRequestResp, len(swaps))
|
|
copy(swapsCopy, swaps)
|
|
|
|
for i := 0; i < len(swapsCopy); i++ {
|
|
actual := <-c.inRequest
|
|
|
|
inner:
|
|
for i, swap := range swapsCopy {
|
|
equal := reflect.DeepEqual(swap.request, actual)
|
|
|
|
if equal {
|
|
c.loopIn <- swap.response
|
|
|
|
swapsCopy = append(
|
|
swapsCopy[:i], swapsCopy[i+1:]...,
|
|
)
|
|
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
|
|
return len(swapsCopy) == 0
|
|
}
|