diff --git a/liquidity/autoloop_test.go b/liquidity/autoloop_test.go index c8b3521..7b8b64c 100644 --- a/liquidity/autoloop_test.go +++ b/liquidity/autoloop_test.go @@ -471,6 +471,349 @@ func TestCompositeRules(t *testing.T) { c.stop() } +// TestAutoLoopInEnabled tests dispatch of autoloop in swaps. +func TestAutoLoopInEnabled(t *testing.T) { + defer test.Guard(t)() + + var ( + chan1 = lndclient.ChannelInfo{ + ChannelID: chanID1.ToUint64(), + PubKeyBytes: peer1, + Capacity: 100000, + RemoteBalance: 100000, + LocalBalance: 0, + } + + chan2 = lndclient.ChannelInfo{ + ChannelID: chanID2.ToUint64(), + PubKeyBytes: peer2, + Capacity: 200000, + RemoteBalance: 200000, + LocalBalance: 0, + } + + channels = []lndclient.ChannelInfo{ + chan1, chan2, + } + + // Create a rule which will loop in, with no inbound liquidity + // reserve. + rule = &SwapRule{ + ThresholdRule: NewThresholdRule(0, 60), + Type: swap.TypeIn, + } + + // Under these rules, we'll have the following recommended + // swaps: + peer1ExpectedAmt btcutil.Amount = 80000 + peer2ExpectedAmt btcutil.Amount = 160000 + + // Set our per-swap budget to 5% of swap amount. + swapFeePPM uint64 = 50000 + + htlcConfTarget int32 = 10 + + // Calculate the maximum amount we'll pay for each swap and + // set our budget to be able to accommodate both. + peer1MaxFee = ppmToSat(peer1ExpectedAmt, swapFeePPM) + peer2MaxFee = ppmToSat(peer2ExpectedAmt, swapFeePPM) + + params = Parameters{ + Autoloop: true, + AutoFeeBudget: peer1MaxFee + peer2MaxFee + 1, + AutoFeeStartDate: testTime, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + FeeLimit: NewFeePortion(swapFeePPM), + ChannelRules: make(map[lnwire.ShortChannelID]*SwapRule), + PeerRules: map[route.Vertex]*SwapRule{ + peer1: rule, + peer2: rule, + }, + HtlcConfTarget: htlcConfTarget, + SweepConfTarget: loop.DefaultSweepConfTarget, + } + ) + c := newAutoloopTestCtx(t, params, channels, testRestrictions) + c.start() + + // Calculate our maximum allowed fees and create quotes that fall within + // our budget. + var ( + quote1 = &loop.LoopInQuote{ + SwapFee: peer1MaxFee / 4, + MinerFee: peer1MaxFee / 8, + } + + quote2Unaffordable = &loop.LoopInQuote{ + SwapFee: peer2MaxFee * 2, + MinerFee: peer2MaxFee * 2, + } + + quoteRequest1 = &loop.LoopInQuoteRequest{ + Amount: peer1ExpectedAmt, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer1, + } + + quoteRequest2 = &loop.LoopInQuoteRequest{ + Amount: peer2ExpectedAmt, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer2, + } + + peer1Swap = &loop.LoopInRequest{ + Amount: peer1ExpectedAmt, + MaxSwapFee: quote1.SwapFee, + MaxMinerFee: quote1.MinerFee, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer1, + ExternalHtlc: false, + Label: labels.AutoloopLabel(swap.TypeIn), + Initiator: autoloopSwapInitiator, + } + ) + + // Tick our autolooper with no existing swaps. Both of our peers + // require swaps, but one of our peer's quotes is too expensive. + step := &autoloopStep{ + minAmt: 1, + maxAmt: peer2ExpectedAmt + 1, + quotesIn: []quoteInRequestResp{ + { + request: quoteRequest1, + quote: quote1, + }, + { + request: quoteRequest2, + quote: quote2Unaffordable, + }, + }, + expectedIn: []loopInRequestResp{ + { + request: peer1Swap, + response: &loop.LoopInSwapInfo{ + SwapHash: lntypes.Hash{1}, + }, + }, + }, + } + c.autoloop(step) + + // Now, we tick again with our first swap in progress. This time, we + // provide a quote for our second swap which is more affordable, so we + // expect it to be dispatched. + + var ( + quote2Affordable = &loop.LoopInQuote{ + SwapFee: peer2MaxFee / 8, + MinerFee: peer2MaxFee / 2, + } + + peer2Swap = &loop.LoopInRequest{ + Amount: peer2ExpectedAmt, + MaxSwapFee: quote2Affordable.SwapFee, + MaxMinerFee: quote2Affordable.MinerFee, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer2, + ExternalHtlc: false, + Label: labels.AutoloopLabel(swap.TypeIn), + Initiator: autoloopSwapInitiator, + } + + existing = []*loopdb.LoopIn{ + existingInFromRequest(peer1Swap, testTime, nil), + } + ) + + step = &autoloopStep{ + minAmt: 1, + maxAmt: peer2ExpectedAmt + 1, + quotesIn: []quoteInRequestResp{ + { + request: quoteRequest2, + quote: quote2Affordable, + }, + }, + existingIn: existing, + expectedIn: []loopInRequestResp{ + { + request: peer2Swap, + response: &loop.LoopInSwapInfo{ + SwapHash: lntypes.Hash{2}, + }, + }, + }, + } + c.autoloop(step) + + c.stop() +} + +// TestAutoloopBothTypes tests dispatching of a loop out and loop in swap at the +// same time. +func TestAutoloopBothTypes(t *testing.T) { + defer test.Guard(t)() + + var ( + chan1 = lndclient.ChannelInfo{ + ChannelID: chanID1.ToUint64(), + PubKeyBytes: peer1, + Capacity: 1000000, + LocalBalance: 1000000, + } + chan2 = lndclient.ChannelInfo{ + ChannelID: chanID2.ToUint64(), + PubKeyBytes: peer2, + Capacity: 200000, + RemoteBalance: 200000, + LocalBalance: 0, + } + + channels = []lndclient.ChannelInfo{ + chan1, chan2, + } + + // Create a rule which will loop out, with no outbound liquidity + // reserve. + outRule = &SwapRule{ + ThresholdRule: NewThresholdRule(40, 0), + Type: swap.TypeOut, + } + + // Create a rule which will loop in, with no inbound liquidity + // reserve. + inRule = &SwapRule{ + ThresholdRule: NewThresholdRule(0, 60), + Type: swap.TypeIn, + } + + // Under this rule, we expect a loop in swap. + loopOutAmt btcutil.Amount = 700000 + loopInAmount btcutil.Amount = 160000 + + // Set our per-swap budget to 5% of swap amount. + swapFeePPM uint64 = 50000 + + htlcConfTarget int32 = 10 + + // Calculate the maximum amount we'll pay for our loop in. + loopOutMaxFee = ppmToSat(loopOutAmt, swapFeePPM) + loopInMaxFee = ppmToSat(loopInAmount, swapFeePPM) + + params = Parameters{ + Autoloop: true, + AutoFeeBudget: loopOutMaxFee + loopInMaxFee + 1, + AutoFeeStartDate: testTime, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + FeeLimit: NewFeePortion(swapFeePPM), + ChannelRules: map[lnwire.ShortChannelID]*SwapRule{ + chanID1: outRule, + }, + PeerRules: map[route.Vertex]*SwapRule{ + peer2: inRule, + }, + HtlcConfTarget: htlcConfTarget, + SweepConfTarget: loop.DefaultSweepConfTarget, + } + ) + c := newAutoloopTestCtx(t, params, channels, testRestrictions) + c.start() + + // Calculate our maximum allowed fees and create quotes that fall within + // our budget. + var ( + loopOutQuote = &loop.LoopOutQuote{ + SwapFee: loopOutMaxFee / 4, + PrepayAmount: loopOutMaxFee / 4, + } + + loopOutQuoteReq = &loop.LoopOutQuoteRequest{ + Amount: loopOutAmt, + SweepConfTarget: params.SweepConfTarget, + SwapPublicationDeadline: testTime, + } + + prepayMaxFee, routeMaxFee, + minerFee = params.FeeLimit.loopOutFees( + loopOutAmt, loopOutQuote, + ) + + loopOutSwap = &loop.OutRequest{ + Amount: loopOutAmt, + MaxSwapRoutingFee: routeMaxFee, + MaxPrepayRoutingFee: prepayMaxFee, + MaxSwapFee: loopOutQuote.SwapFee, + MaxPrepayAmount: loopOutQuote.PrepayAmount, + MaxMinerFee: minerFee, + SweepConfTarget: params.SweepConfTarget, + OutgoingChanSet: loopdb.ChannelSet{ + chanID1.ToUint64(), + }, + Label: labels.AutoloopLabel(swap.TypeOut), + Initiator: autoloopSwapInitiator, + } + + loopinQuote = &loop.LoopInQuote{ + SwapFee: loopInMaxFee / 4, + MinerFee: loopInMaxFee / 8, + } + + loopInQuoteReq = &loop.LoopInQuoteRequest{ + Amount: loopInAmount, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer2, + } + + loopInSwap = &loop.LoopInRequest{ + Amount: loopInAmount, + MaxSwapFee: loopinQuote.SwapFee, + MaxMinerFee: loopinQuote.MinerFee, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer2, + ExternalHtlc: false, + Label: labels.AutoloopLabel(swap.TypeIn), + Initiator: autoloopSwapInitiator, + } + ) + + step := &autoloopStep{ + minAmt: 1, + maxAmt: loopOutAmt + 1, + quotesOut: []quoteRequestResp{ + { + request: loopOutQuoteReq, + quote: loopOutQuote, + }, + }, + quotesIn: []quoteInRequestResp{ + { + request: loopInQuoteReq, + quote: loopinQuote, + }, + }, + expectedOut: []loopOutRequestResp{ + { + request: loopOutSwap, + response: &loop.LoopOutSwapInfo{ + SwapHash: lntypes.Hash{1}, + }, + }, + }, + expectedIn: []loopInRequestResp{ + { + request: loopInSwap, + response: &loop.LoopInSwapInfo{ + SwapHash: lntypes.Hash{2}, + }, + }, + }, + } + 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, @@ -496,3 +839,24 @@ func existingSwapFromRequest(request *loop.OutRequest, initTime time.Time, }, } } + +func existingInFromRequest(in *loop.LoopInRequest, initTime time.Time, + events []*loopdb.LoopEvent) *loopdb.LoopIn { + + return &loopdb.LoopIn{ + Loop: loopdb.Loop{ + Events: events, + }, + Contract: &loopdb.LoopInContract{ + SwapContract: loopdb.SwapContract{ + MaxSwapFee: in.MaxSwapFee, + MaxMinerFee: in.MaxMinerFee, + InitiationTime: initTime, + Label: in.Label, + }, + HtlcConfTarget: in.HtlcConfTarget, + LastHop: in.LastHop, + ExternalHtlc: in.ExternalHtlc, + }, + } +}