Skip to content

Commit 692620d

Browse files
committed
liquidity: add fee budget to swap suggestions
Add a dated fee budget to our swap suggestions. This budget only applies to automatically dispatched swaps (which will be added in later commits). We choose to apply the fee budget to our suggestions so that they perfectly replicate what the autolooper would do. The budget has a start date so that it can be refreshed once it has been used up over a period (rather than having to endlessly increase it).
1 parent bda6d36 commit 692620d

File tree

6 files changed

+386
-4
lines changed

6 files changed

+386
-4
lines changed

go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2+
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e h1:F2x1bq7RaNCIuqYpswggh1+c1JmwdnkHNC9wy1KDip0=
23
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo=
34
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
45
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
6+
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e h1:n+DcnTNkQnHlwpsrHoQtkrJIO7CBx029fw6oR4vIob4=
57
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ=
8+
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 h1:MG93+PZYs9PyEsj/n5/haQu2gK0h4tUtSy9ejtMwWa0=
69
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc=
710
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
11+
github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2 h1:2be4ykKKov3M1yISM2E8gnGXZ/N2SsPawfnGiXxaYEU=
812
github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
913
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
1014
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
@@ -131,7 +135,9 @@ github.com/grpc-ecosystem/grpc-gateway v1.14.3 h1:OCJlWkOUoTnl0neNGlf4fUm3TmbEtg
131135
github.com/grpc-ecosystem/grpc-gateway v1.14.3/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0=
132136
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
133137
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
138+
github.com/jackpal/gateway v1.0.5 h1:qzXWUJfuMdlLMtt0a3Dgt+xkWQiA5itDEITVJtuSwMc=
134139
github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
140+
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad h1:heFfj7z0pGsNCekUlsFhO2jstxO4b5iQ665LjwM5mDc=
135141
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
136142
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
137143
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@@ -253,6 +259,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
253259
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
254260
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
255261
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
262+
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E=
256263
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY=
257264
github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
258265
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=

labels/labels.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package labels
22

33
import (
44
"errors"
5+
"fmt"
56
"strings"
67
)
78

@@ -12,6 +13,10 @@ const (
1213
// Reserved is used as a prefix to separate labels that are created by
1314
// loopd from those created by users.
1415
Reserved = "[reserved]"
16+
17+
// autoOut is the label used for loop out swaps that are automatically
18+
// dispatched.
19+
autoOut = "autoloop-out"
1520
)
1621

1722
var (
@@ -23,6 +28,12 @@ var (
2328
ErrReservedPrefix = errors.New("label contains reserved prefix")
2429
)
2530

31+
// AutoOutLabel returns a label with the reserved prefix that identifies
32+
// automatically dispatched loop outs.
33+
func AutoOutLabel() string {
34+
return fmt.Sprintf("%v: %v", Reserved, autoOut)
35+
}
36+
2637
// Validate checks that a label is of appropriate length and is not in our list
2738
// of reserved labels.
2839
func Validate(label string) error {

liquidity/liquidity.go

Lines changed: 186 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,17 @@ import (
3636
"context"
3737
"errors"
3838
"fmt"
39+
"sort"
3940
"strings"
4041
"sync"
4142
"time"
4243

4344
"github.com/btcsuite/btcutil"
4445
"github.com/lightninglabs/lndclient"
4546
"github.com/lightninglabs/loop"
47+
"github.com/lightninglabs/loop/labels"
4648
"github.com/lightninglabs/loop/loopdb"
49+
"github.com/lightningnetwork/lnd"
4750
"github.com/lightningnetwork/lnd/clock"
4851
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
4952
"github.com/lightningnetwork/lnd/lnwire"
@@ -88,9 +91,21 @@ const (
8891
)
8992

9093
var (
94+
// defaultBudget is the default autoloop budget we set. This budget will
95+
// only be used for automatically dispatched swaps if autoloop is
96+
// explicitly enabled, so we are happy to set a non-zero value here. The
97+
// amount chosen simply uses the current defaults to provide budget for
98+
// a single swap. We don't have a swap amount to calculate our maximum
99+
// routing fee, so we use 0.16 BTC for now.
100+
defaultBudget = defaultMaximumMinerFee +
101+
ppmToSat(lnd.MaxBtcFundingAmount, defaultSwapFeePPM) +
102+
ppmToSat(defaultMaximumPrepay, defaultPrepayRoutingFeePPM) +
103+
ppmToSat(lnd.MaxBtcFundingAmount, defaultRoutingFeePPM)
104+
91105
// defaultParameters contains the default parameters that we start our
92106
// liquidity manger with.
93107
defaultParameters = Parameters{
108+
AutoFeeBudget: defaultBudget,
94109
ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule),
95110
FailureBackOff: defaultFailureBackoff,
96111
SweepFeeRateLimit: defaultSweepFeeRateLimit,
@@ -125,6 +140,9 @@ var (
125140

126141
// ErrZeroPrepay is returned if a zero maximum prepay is set.
127142
ErrZeroPrepay = errors.New("maximum prepay must be non-zero")
143+
144+
// ErrNegativeBudget is returned if a negative swap budget is set.
145+
ErrNegativeBudget = errors.New("swap budget must be >= 0")
128146
)
129147

130148
// Config contains the external functionality required to run the
@@ -159,6 +177,16 @@ type Config struct {
159177
// Parameters is a set of parameters provided by the user which guide
160178
// how we assess liquidity.
161179
type Parameters struct {
180+
// AutoFeeBudget is the total amount we allow to be spent on
181+
// automatically dispatched swaps. Once this budget has been used, we
182+
// will stop dispatching swaps until the budget is increased or the
183+
// start date is moved.
184+
AutoFeeBudget btcutil.Amount
185+
186+
// AutoFeeStartDate is the date from which we will include automatically
187+
// dispatched swaps in our current budget, inclusive.
188+
AutoFeeStartDate time.Time
189+
162190
// FailureBackOff is the amount of time that we require passes after a
163191
// channel has been part of a failed loop out swap before we suggest
164192
// using it again.
@@ -219,12 +247,13 @@ func (p Parameters) String() string {
219247
return fmt.Sprintf("channel rules: %v, failure backoff: %v, sweep "+
220248
"fee rate limit: %v, sweep conf target: %v, maximum prepay: "+
221249
"%v, maximum miner fee: %v, maximum swap fee ppm: %v, maximum "+
222-
"routing fee ppm: %v, maximum prepay routing fee ppm: %v",
250+
"routing fee ppm: %v, maximum prepay routing fee ppm: %v, "+
251+
"auto budget: %v, budget start: %v",
223252
strings.Join(channelRules, ","), p.FailureBackOff,
224253
p.SweepFeeRateLimit, p.SweepConfTarget, p.MaximumPrepay,
225254
p.MaximumMinerFee, p.MaximumSwapFeePPM,
226255
p.MaximumRoutingFeePPM, p.MaximumPrepayRoutingFeePPM,
227-
)
256+
p.AutoFeeBudget, p.AutoFeeStartDate)
228257
}
229258

230259
// validate checks whether a set of parameters is valid. It takes the minimum
@@ -275,6 +304,10 @@ func (p Parameters) validate(minConfs int32) error {
275304
return ErrZeroMinerFee
276305
}
277306

307+
if p.AutoFeeBudget < 0 {
308+
return ErrNegativeBudget
309+
}
310+
278311
return nil
279312
}
280313

@@ -356,6 +389,16 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
356389
return nil, nil
357390
}
358391

392+
// If our start date is in the future, we interpret this as meaning that
393+
// we should start using our budget at this date. This means that we
394+
// have no budget for the present, so we just return.
395+
if m.params.AutoFeeStartDate.After(m.cfg.Clock.Now()) {
396+
log.Debugf("autoloop fee budget start time: %v is in "+
397+
"the future", m.params.AutoFeeStartDate)
398+
399+
return nil, nil
400+
}
401+
359402
// Before we get any swap suggestions, we check what the current fee
360403
// estimate is to sweep within our target number of confirmations. If
361404
// This fee exceeds the fee limit we have set, we will not suggest any
@@ -396,6 +439,23 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
396439
return nil, err
397440
}
398441

442+
// Get a summary of our existing swaps so that we can check our autoloop
443+
// budget.
444+
summary, err := m.checkExistingAutoLoops(ctx, loopOut)
445+
if err != nil {
446+
return nil, err
447+
}
448+
449+
if summary.totalFees() >= m.params.AutoFeeBudget {
450+
log.Debugf("autoloop fee budget: %v exhausted, %v spent on "+
451+
"completed swaps, %v reserved for ongoing swaps "+
452+
"(upper limit)",
453+
m.params.AutoFeeBudget, summary.spentFees,
454+
summary.pendingFees)
455+
456+
return nil, nil
457+
}
458+
399459
eligible, err := m.getEligibleChannels(ctx, loopOut, loopIn)
400460
if err != nil {
401461
return nil, err
@@ -449,7 +509,45 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
449509
suggestions = append(suggestions, outRequest)
450510
}
451511

452-
return suggestions, nil
512+
// If we have no suggestions after we have applied all of our limits,
513+
// just return.
514+
if len(suggestions) == 0 {
515+
return nil, nil
516+
}
517+
518+
// Sort suggestions by amount in descending order.
519+
sort.SliceStable(suggestions, func(i, j int) bool {
520+
return suggestions[i].Amount > suggestions[j].Amount
521+
})
522+
523+
// Run through our suggested swaps in descending order of amount and
524+
// return all of the swaps which will fit within our remaining budget.
525+
var (
526+
available = m.params.AutoFeeBudget - summary.totalFees()
527+
inBudget []loop.OutRequest
528+
)
529+
530+
for _, swap := range suggestions {
531+
fees := worstCaseOutFees(
532+
swap.MaxPrepayRoutingFee, swap.MaxSwapRoutingFee,
533+
swap.MaxSwapFee, swap.MaxMinerFee, swap.MaxPrepayAmount,
534+
)
535+
536+
// If the maximum fee we expect our swap to use is less than the
537+
// amount we have available, we add it to our set of swaps that
538+
// fall within the budget and decrement our available amount.
539+
if fees <= available {
540+
available -= fees
541+
inBudget = append(inBudget, swap)
542+
}
543+
544+
// If we're out of budget, exit early.
545+
if available == 0 {
546+
break
547+
}
548+
}
549+
550+
return inBudget, nil
453551
}
454552

455553
// makeLoopOutRequest creates a loop out request from a suggestion. Since we
@@ -485,6 +583,87 @@ func (m *Manager) makeLoopOutRequest(suggestion *LoopOutRecommendation,
485583
}
486584
}
487585

586+
// worstCaseOutFees calculates the largest possible fees for a loop out swap,
587+
// comparing the fees for a successful swap to the cost when the client pays
588+
// the prepay because they failed to sweep the on chain htlc. This is unlikely,
589+
// because we expect clients to be online to sweep, but we want to account for
590+
// every outcome so we include it.
591+
func worstCaseOutFees(prepayRouting, swapRouting, swapFee, minerFee,
592+
prepayAmount btcutil.Amount) btcutil.Amount {
593+
594+
var (
595+
successFees = prepayRouting + minerFee + swapFee + swapRouting
596+
noShowFees = prepayRouting + prepayAmount
597+
)
598+
599+
if noShowFees > successFees {
600+
return noShowFees
601+
}
602+
603+
return successFees
604+
}
605+
606+
// existingAutoLoopSummary provides a summary of the existing autoloops which
607+
// were dispatched during our current budget period.
608+
type existingAutoLoopSummary struct {
609+
// spentFees is the amount we have spent on completed swaps.
610+
spentFees btcutil.Amount
611+
612+
// pendingFees is the worst-case amount of fees we could spend on in
613+
// flight autoloops.
614+
pendingFees btcutil.Amount
615+
}
616+
617+
// totalFees returns the total amount of fees that automatically dispatched
618+
// swaps may consume.
619+
func (e *existingAutoLoopSummary) totalFees() btcutil.Amount {
620+
return e.spentFees + e.pendingFees
621+
}
622+
623+
// checkExistingAutoLoops calculates the total amount that has been spent by
624+
// automatically dispatched swaps that have completed, and the worst-case fee
625+
// total for our set of ongoing, automatically dispatched swaps.
626+
func (m *Manager) checkExistingAutoLoops(ctx context.Context,
627+
loopOuts []*loopdb.LoopOut) (*existingAutoLoopSummary, error) {
628+
629+
var summary existingAutoLoopSummary
630+
631+
for _, out := range loopOuts {
632+
if out.Contract.Label != labels.AutoOutLabel() {
633+
continue
634+
}
635+
636+
// If we have a pending swap, we are uncertain of the fees that
637+
// it will end up paying. We use the worst-case estimate based
638+
// on the maximum values we set for each fee category. This will
639+
// likely over-estimate our fees (because we probably won't
640+
// spend our maximum miner amount). If a swap is not pending,
641+
// it has succeeded or failed so we just record our actual fees
642+
// for the swap provided that the swap completed after our
643+
// budget start date.
644+
if out.State().State.Type() == loopdb.StateTypePending {
645+
prepay, err := m.cfg.Lnd.Client.DecodePaymentRequest(
646+
ctx, out.Contract.PrepayInvoice,
647+
)
648+
if err != nil {
649+
return nil, err
650+
}
651+
652+
summary.pendingFees += worstCaseOutFees(
653+
out.Contract.MaxPrepayRoutingFee,
654+
out.Contract.MaxSwapRoutingFee,
655+
out.Contract.MaxSwapFee,
656+
out.Contract.MaxMinerFee,
657+
mSatToSatoshis(prepay.Value),
658+
)
659+
} else if !out.LastUpdateTime().Before(m.params.AutoFeeStartDate) {
660+
summary.spentFees += out.State().Cost.Total()
661+
}
662+
}
663+
664+
return &summary, nil
665+
}
666+
488667
// getEligibleChannels takes lists of our existing loop out and in swaps, and
489668
// gets a list of channels that are not currently being utilized for a swap.
490669
// If an unrestricted swap is ongoing, we return an empty set of channels
@@ -653,3 +832,7 @@ func satPerKwToSatPerVByte(satPerKw chainfee.SatPerKWeight) int64 {
653832
func ppmToSat(amount btcutil.Amount, ppm int) btcutil.Amount {
654833
return btcutil.Amount(uint64(amount) * uint64(ppm) / FeeBase)
655834
}
835+
836+
func mSatToSatoshis(amount lnwire.MilliSatoshi) btcutil.Amount {
837+
return btcutil.Amount(amount / 1000)
838+
}

0 commit comments

Comments
 (0)