Skip to content

Commit fd4214e

Browse files
authored
Merge pull request #333 from carlaKC/autoloop-3-peerrules
autoloop: add peer level rules for aggregate liquidity management
2 parents 2ecdd24 + b393102 commit fd4214e

16 files changed

+1083
-373
lines changed

cmd/loop/liquidity.go

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package main
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
78
"strconv"
89

910
"github.com/lightninglabs/loop/liquidity"
1011
"github.com/lightninglabs/loop/looprpc"
12+
"github.com/lightningnetwork/lnd/routing/route"
1113
"github.com/urfave/cli"
1214
"google.golang.org/grpc/codes"
1315
"google.golang.org/grpc/status"
@@ -42,9 +44,9 @@ func getParams(ctx *cli.Context) error {
4244

4345
var setLiquidityRuleCommand = cli.Command{
4446
Name: "setrule",
45-
Usage: "set liquidity manager rule for a channel",
46-
Description: "Update or remove the liquidity rule for a channel.",
47-
ArgsUsage: "shortchanid",
47+
Usage: "set liquidity manager rule for a channel/peer",
48+
Description: "Update or remove the liquidity rule for a channel/peer.",
49+
ArgsUsage: "{shortchanid | peerpubkey}",
4850
Flags: []cli.Flag{
4951
cli.IntFlag{
5052
Name: "incoming_threshold",
@@ -58,8 +60,9 @@ var setLiquidityRuleCommand = cli.Command{
5860
"that we do not want to drop below.",
5961
},
6062
cli.BoolFlag{
61-
Name: "clear",
62-
Usage: "remove the rule currently set for the channel.",
63+
Name: "clear",
64+
Usage: "remove the rule currently set for the " +
65+
"channel/peer.",
6366
},
6467
},
6568
Action: setRule,
@@ -68,13 +71,22 @@ var setLiquidityRuleCommand = cli.Command{
6871
func setRule(ctx *cli.Context) error {
6972
// We require that a channel ID is set for this rule update.
7073
if ctx.NArg() != 1 {
71-
return fmt.Errorf("please set a channel id for the rule " +
72-
"update")
74+
return fmt.Errorf("please set a channel id or peer pubkey " +
75+
"for the rule update")
7376
}
7477

78+
var (
79+
pubkey route.Vertex
80+
pubkeyRule bool
81+
)
7582
chanID, err := strconv.ParseUint(ctx.Args().First(), 10, 64)
7683
if err != nil {
77-
return fmt.Errorf("could not parse channel ID: %v", err)
84+
pubkey, err = route.NewVertexFromStr(ctx.Args().First())
85+
if err != nil {
86+
return fmt.Errorf("please provide a valid pubkey: "+
87+
"%v, or short channel ID", err)
88+
}
89+
pubkeyRule = true
7890
}
7991

8092
client, cleanup, err := getClient(ctx)
@@ -101,11 +113,20 @@ func setRule(ctx *cli.Context) error {
101113
)
102114

103115
// Run through our current set of rules and check whether we have a rule
104-
// currently set for this channel. We also track a slice containing all
105-
// of the rules we currently have set for other channels, because we
106-
// want to leave these rules untouched.
116+
// currently set for this channel or peer. We also track a slice
117+
// containing all of the rules we currently have set for other channels,
118+
// and peers because we want to leave these rules untouched.
107119
for _, rule := range params.Rules {
108-
if rule.ChannelId == chanID {
120+
var (
121+
channelRuleSet = rule.ChannelId != 0 &&
122+
rule.ChannelId == chanID
123+
124+
peerRuleSet = rule.Pubkey != nil && bytes.Equal(
125+
rule.Pubkey, pubkey[:],
126+
)
127+
)
128+
129+
if channelRuleSet || peerRuleSet {
109130
ruleSet = true
110131
} else {
111132
otherRules = append(otherRules, rule)
@@ -149,6 +170,10 @@ func setRule(ctx *cli.Context) error {
149170
Type: looprpc.LiquidityRuleType_THRESHOLD,
150171
}
151172

173+
if pubkeyRule {
174+
newRule.Pubkey = pubkey[:]
175+
}
176+
152177
if inboundSet {
153178
newRule.IncomingThreshold = uint32(
154179
ctx.Int("incoming_threshold"),

docs/autoloop.md

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,35 @@ Note that autoloop parameters and rules are not persisted, so must be set on
2323
restart. We recommend running loopd with `--debuglevel=debug` when using this
2424
feature.
2525

26-
### Channel Thresholds
27-
To setup the autolooper to dispatch swaps on your behalf, you need to tell it
28-
which channels you would like it to perform swaps on, and the liquidity balance
29-
you would like on each channel. Desired liqudity balance is expressed using
30-
threshold incoming and outgoing percentages of channel capacity. The incoming
31-
threshold you specify indicates the minimum percentage of your channel capacity
32-
that you would like in incoming capacity. The outgoing thresold allows you to
33-
reserve a percentage of your balance for outgoing capacity, but may be set to
34-
zero if you are only concerned with incoming capcity.
35-
36-
The autolooper will perform swaps that push your incoming channel capacity to
37-
at least the incoming threshold you specify, while reserving at least the
38-
outgoing capacity threshold. Rules can be set as follows:
26+
### Liquidity Targets
27+
Autoloop can be configured to manage liquidity for individual channels, or for
28+
a peer as a whole. Peer-level liquidity management will examine the liquidity
29+
balance of all the channels you have with a peer. This differs from channel-level
30+
liquidity, where each channel's individual balance is checked. Note that if you
31+
set a liquidity rule for a peer, you cannot also set a specific rule for one of
32+
its channels.
33+
34+
### Liqudity Thresholds
35+
To setup the autolooper to dispatch swaps on your behalf, you need to set the
36+
liquidity balance you would like for each channel or peer. Desired liquidity
37+
balance is expressed using threshold incoming and outgoing percentages of
38+
capacity. The incoming threshold you specify indicates the minimum percentage
39+
of your capacity that you would like in incoming capacity. The outgoing
40+
threshold allows you to reserve a percentage of your balance for outgoing
41+
capacity, but may be set to zero if you are only concerned with incoming
42+
capacity.
43+
44+
The autolooper will perform swaps that push your incoming capacity to at least
45+
the incoming threshold you specify, while reserving at least the outgoing
46+
capacity threshold. Rules can be set as follows:
3947

4048
```
41-
loop setrule {short channel id} --incoming_threshold={minimum % incoming} --outgoing_threshold={minimum % outgoing}
49+
loop setrule {short channel id/ peer pubkey} --incoming_threshold={minimum % incoming} --outgoing_threshold={minimum % outgoing}
4250
```
4351

44-
To remove a channel from consideration, its rule can simply be cleared:
52+
To remove a rule from consideration, its rule can simply be cleared:
4553
```
46-
loop setrule {short channel id} --clear
54+
loop setrule {short channel id/ peer pubkey} --clear
4755
```
4856

4957
## Fees

liquidity/autoloop_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"testing"
55
"time"
66

7+
"github.com/btcsuite/btcutil"
78
"github.com/lightninglabs/lndclient"
89
"github.com/lightninglabs/loop"
910
"github.com/lightninglabs/loop/labels"
@@ -12,6 +13,7 @@ import (
1213
"github.com/lightninglabs/loop/test"
1314
"github.com/lightningnetwork/lnd/lntypes"
1415
"github.com/lightningnetwork/lnd/lnwire"
16+
"github.com/lightningnetwork/lnd/routing/route"
1517
)
1618

1719
// TestAutoLoopDisabled tests the case where we need to perform a swap, but
@@ -266,6 +268,161 @@ func TestAutoLoopEnabled(t *testing.T) {
266268
c.stop()
267269
}
268270

271+
// TestCompositeRules tests the case where we have rules set on a per peer
272+
// and per channel basis, and perform swaps for both targets.
273+
func TestCompositeRules(t *testing.T) {
274+
defer test.Guard(t)()
275+
276+
// Setup our channels so that we have two channels with peer 2, and
277+
// a single channel with peer 1.
278+
channel3 := lndclient.ChannelInfo{
279+
ChannelID: chanID3.ToUint64(),
280+
PubKeyBytes: peer2,
281+
LocalBalance: 10000,
282+
RemoteBalance: 0,
283+
Capacity: 10000,
284+
}
285+
286+
channels := []lndclient.ChannelInfo{
287+
channel1, channel2, channel3,
288+
}
289+
290+
// Create a set of parameters with autoloop enabled, set our budget to
291+
// a value that will easily accommodate our two swaps.
292+
params := Parameters{
293+
Autoloop: true,
294+
AutoFeeBudget: 100000,
295+
AutoFeeStartDate: testTime,
296+
MaxAutoInFlight: 2,
297+
FailureBackOff: time.Hour,
298+
SweepFeeRateLimit: 20000,
299+
SweepConfTarget: 10,
300+
MaximumPrepay: 20000,
301+
MaximumSwapFeePPM: 1000,
302+
MaximumRoutingFeePPM: 1000,
303+
MaximumPrepayRoutingFeePPM: 1000,
304+
MaximumMinerFee: 20000,
305+
ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{
306+
chanID1: chanRule,
307+
},
308+
PeerRules: map[route.Vertex]*ThresholdRule{
309+
peer2: chanRule,
310+
},
311+
}
312+
313+
c := newAutoloopTestCtx(t, params, channels, testRestrictions)
314+
c.start()
315+
316+
// Calculate our maximum allowed fees and create quotes that fall within
317+
// our budget.
318+
var (
319+
// Create a quote for our peer level swap that is within
320+
// our budget, with an amount which would balance the peer
321+
/// across all of its channels.
322+
peerAmount = btcutil.Amount(15000)
323+
maxPeerSwapFee = ppmToSat(peerAmount, params.MaximumSwapFeePPM)
324+
325+
peerSwapQuote = &loop.LoopOutQuote{
326+
SwapFee: maxPeerSwapFee,
327+
PrepayAmount: params.MaximumPrepay - 20,
328+
}
329+
330+
peerSwapQuoteRequest = &loop.LoopOutQuoteRequest{
331+
Amount: peerAmount,
332+
SweepConfTarget: params.SweepConfTarget,
333+
}
334+
335+
maxPeerRouteFee = ppmToSat(
336+
peerAmount, params.MaximumRoutingFeePPM,
337+
)
338+
339+
peerSwap = &loop.OutRequest{
340+
Amount: peerAmount,
341+
MaxSwapRoutingFee: maxPeerRouteFee,
342+
MaxPrepayRoutingFee: ppmToSat(
343+
peerSwapQuote.PrepayAmount,
344+
params.MaximumPrepayRoutingFeePPM,
345+
),
346+
MaxSwapFee: peerSwapQuote.SwapFee,
347+
MaxPrepayAmount: peerSwapQuote.PrepayAmount,
348+
MaxMinerFee: params.MaximumMinerFee,
349+
SweepConfTarget: params.SweepConfTarget,
350+
OutgoingChanSet: loopdb.ChannelSet{
351+
chanID2.ToUint64(), chanID3.ToUint64(),
352+
},
353+
Label: labels.AutoloopLabel(swap.TypeOut),
354+
Initiator: autoloopSwapInitiator,
355+
}
356+
// Create a quote for our single channel swap that is within
357+
// our budget.
358+
chanAmount = chan1Rec.Amount
359+
maxChanSwapFee = ppmToSat(chanAmount, params.MaximumSwapFeePPM)
360+
361+
channelSwapQuote = &loop.LoopOutQuote{
362+
SwapFee: maxChanSwapFee,
363+
PrepayAmount: params.MaximumPrepay - 10,
364+
}
365+
366+
chanSwapQuoteRequest = &loop.LoopOutQuoteRequest{
367+
Amount: chanAmount,
368+
SweepConfTarget: params.SweepConfTarget,
369+
}
370+
371+
maxChanRouteFee = ppmToSat(
372+
chanAmount, params.MaximumRoutingFeePPM,
373+
)
374+
375+
chanSwap = &loop.OutRequest{
376+
Amount: chanAmount,
377+
MaxSwapRoutingFee: maxChanRouteFee,
378+
MaxPrepayRoutingFee: ppmToSat(
379+
channelSwapQuote.PrepayAmount,
380+
params.MaximumPrepayRoutingFeePPM,
381+
),
382+
MaxSwapFee: channelSwapQuote.SwapFee,
383+
MaxPrepayAmount: channelSwapQuote.PrepayAmount,
384+
MaxMinerFee: params.MaximumMinerFee,
385+
SweepConfTarget: params.SweepConfTarget,
386+
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
387+
Label: labels.AutoloopLabel(swap.TypeOut),
388+
Initiator: autoloopSwapInitiator,
389+
}
390+
quotes = []quoteRequestResp{
391+
{
392+
request: peerSwapQuoteRequest,
393+
quote: peerSwapQuote,
394+
},
395+
{
396+
request: chanSwapQuoteRequest,
397+
quote: channelSwapQuote,
398+
},
399+
}
400+
401+
loopOuts = []loopOutRequestResp{
402+
{
403+
request: peerSwap,
404+
response: &loop.LoopOutSwapInfo{
405+
SwapHash: lntypes.Hash{2},
406+
},
407+
},
408+
{
409+
request: chanSwap,
410+
response: &loop.LoopOutSwapInfo{
411+
SwapHash: lntypes.Hash{1},
412+
},
413+
},
414+
}
415+
)
416+
417+
// Tick our autolooper with no existing swaps, we expect a loop out
418+
// swap to be dispatched for each of our rules. We set our server side
419+
// maximum to be greater than the swap amount for our peer swap (which
420+
// is the larger of the two swaps).
421+
c.autoloop(1, peerAmount+1, nil, quotes, loopOuts)
422+
423+
c.stop()
424+
}
425+
269426
// existingSwapFromRequest is a helper function which returns the db
270427
// representation of a loop out request with the event set provided.
271428
func existingSwapFromRequest(request *loop.OutRequest, initTime time.Time,

liquidity/balances.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"github.com/btcsuite/btcutil"
55
"github.com/lightninglabs/lndclient"
66
"github.com/lightningnetwork/lnd/lnwire"
7+
"github.com/lightningnetwork/lnd/routing/route"
78
)
89

910
// balances summarizes the state of the balances of a channel. Channel reserve,
@@ -18,16 +19,24 @@ type balances struct {
1819
// outgoing is the local balance of the channel.
1920
outgoing btcutil.Amount
2021

21-
// channelID is the channel that has these balances.
22-
channelID lnwire.ShortChannelID
22+
// channels is the channel that has these balances represent. This may
23+
// be more than one channel in the case where we are examining a peer's
24+
// liquidity as a whole.
25+
channels []lnwire.ShortChannelID
26+
27+
// pubkey is the public key of the peer we have this balances set with.
28+
pubkey route.Vertex
2329
}
2430

2531
// newBalances creates a balances struct from lndclient channel information.
2632
func newBalances(info lndclient.ChannelInfo) *balances {
2733
return &balances{
28-
capacity: info.Capacity,
29-
incoming: info.RemoteBalance,
30-
outgoing: info.LocalBalance,
31-
channelID: lnwire.NewShortChanIDFromInt(info.ChannelID),
34+
capacity: info.Capacity,
35+
incoming: info.RemoteBalance,
36+
outgoing: info.LocalBalance,
37+
channels: []lnwire.ShortChannelID{
38+
lnwire.NewShortChanIDFromInt(info.ChannelID),
39+
},
40+
pubkey: info.PubKeyBytes,
3241
}
3342
}

0 commit comments

Comments
 (0)