Skip to content

Commit 2e6adea

Browse files
authored
Merge pull request #433 from carlaKC/419-swaptype
liquidity: add swap type and loop in fee logic
2 parents 06c4c34 + 8113c34 commit 2e6adea

File tree

13 files changed

+839
-173
lines changed

13 files changed

+839
-173
lines changed

cmd/loop/liquidity.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ func setRule(ctx *cli.Context) error {
168168
newRule := &looprpc.LiquidityRule{
169169
ChannelId: chanID,
170170
Type: looprpc.LiquidityRuleType_THRESHOLD,
171+
SwapType: looprpc.SwapType_LOOP_OUT,
171172
}
172173

173174
if pubkeyRule {

liquidity/autoloop_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func TestAutoLoopDisabled(t *testing.T) {
2727
}
2828

2929
params := defaultParameters
30-
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
30+
params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
3131
chanID1: chanRule,
3232
}
3333

@@ -95,7 +95,7 @@ func TestAutoLoopEnabled(t *testing.T) {
9595
swapFeePPM, routeFeePPM, prepayFeePPM, maxMiner,
9696
prepayAmount, 20000,
9797
),
98-
ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{
98+
ChannelRules: map[lnwire.ShortChannelID]*SwapRule{
9999
chanID1: chanRule,
100100
chanID2: chanRule,
101101
},
@@ -312,10 +312,10 @@ func TestCompositeRules(t *testing.T) {
312312
MaxAutoInFlight: 2,
313313
FailureBackOff: time.Hour,
314314
SweepConfTarget: 10,
315-
ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{
315+
ChannelRules: map[lnwire.ShortChannelID]*SwapRule{
316316
chanID1: chanRule,
317317
},
318-
PeerRules: map[route.Vertex]*ThresholdRule{
318+
PeerRules: map[route.Vertex]*SwapRule{
319319
peer2: chanRule,
320320
},
321321
}

liquidity/fees.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,27 @@ func (f *FeeCategoryLimit) loopOutLimits(amount btcutil.Amount,
223223
return nil
224224
}
225225

226+
func (f *FeeCategoryLimit) loopInLimits(amount btcutil.Amount,
227+
quote *loop.LoopInQuote) error {
228+
229+
maxServerFee := ppmToSat(amount, f.MaximumSwapFeePPM)
230+
if quote.SwapFee > maxServerFee {
231+
log.Debugf("quoted swap fee: %v > maximum swap fee: %v",
232+
quote.SwapFee, maxServerFee)
233+
234+
return newReasonError(ReasonSwapFee)
235+
}
236+
237+
if quote.MinerFee > f.MaximumMinerFee {
238+
log.Debugf("quoted miner fee: %v > maximum miner "+
239+
"fee: %v", quote.MinerFee, f.MaximumMinerFee)
240+
241+
return newReasonError(ReasonMinerFee)
242+
}
243+
244+
return nil
245+
}
246+
226247
// loopOutFees returns the prepay and routing and miner fees we are willing to
227248
// pay for a loop out swap.
228249
func (f *FeeCategoryLimit) loopOutFees(amount btcutil.Amount,
@@ -384,3 +405,42 @@ func splitOffChain(available, prepayAmt,
384405
func scaleMinerFee(estimate btcutil.Amount) btcutil.Amount {
385406
return estimate * btcutil.Amount(minerMultiplier)
386407
}
408+
409+
func (f *FeePortion) loopInLimits(amount btcutil.Amount,
410+
quote *loop.LoopInQuote) error {
411+
412+
// Calculate the total amount that this swap may spend in fees, as a
413+
// portion of the swap amount.
414+
totalFeeSpend := ppmToSat(amount, f.PartsPerMillion)
415+
416+
// Check individual fee components so that we can give more specific
417+
// feedback.
418+
if quote.MinerFee > totalFeeSpend {
419+
log.Debugf("miner fee: %v greater than fee limit: %v, at "+
420+
"%v ppm", quote.MinerFee, totalFeeSpend,
421+
f.PartsPerMillion)
422+
423+
return newReasonError(ReasonMinerFee)
424+
}
425+
426+
if quote.SwapFee > totalFeeSpend {
427+
log.Debugf("swap fee: %v greater than fee limit: %v, at "+
428+
"%v ppm", quote.SwapFee, totalFeeSpend,
429+
f.PartsPerMillion)
430+
431+
return newReasonError(ReasonSwapFee)
432+
}
433+
434+
fees := worstCaseInFees(
435+
quote.MinerFee, quote.SwapFee, defaultLoopInSweepFee,
436+
)
437+
438+
if fees > totalFeeSpend {
439+
log.Debugf("total fees for swap: %v > fee limit: %v, at "+
440+
"%v ppm", fees, totalFeeSpend, f.PartsPerMillion)
441+
442+
return newReasonError(ReasonFeePPMInsufficient)
443+
}
444+
445+
return nil
446+
}

liquidity/interface.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ type FeeLimit interface {
3232
// a swap amount and quote.
3333
loopOutFees(amount btcutil.Amount, quote *loop.LoopOutQuote) (
3434
btcutil.Amount, btcutil.Amount, btcutil.Amount)
35+
36+
// loopInLimits checks whether the quote provided is within our fee
37+
// limits for the swap amount.
38+
loopInLimits(amount btcutil.Amount,
39+
quote *loop.LoopInQuote) error
3540
}
3641

3742
// swapBuilder is an interface used to build our different swap types.

liquidity/liquidity.go

Lines changed: 89 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ const (
8181
// autoloopSwapInitiator is the value we send in the initiator field of
8282
// a swap request when issuing an automatic swap.
8383
autoloopSwapInitiator = "autoloop"
84+
85+
// We use a static fee rate to estimate our sweep fee, because we
86+
// can't realistically estimate what our fee estimate will be by the
87+
// time we reach timeout. We set this to a high estimate so that we can
88+
// account for worst-case fees, (1250 * 4 / 1000) = 50 sat/byte.
89+
defaultLoopInSweepFee = chainfee.SatPerKWeight(1250)
8490
)
8591

8692
var (
@@ -97,8 +103,8 @@ var (
97103
defaultParameters = Parameters{
98104
AutoFeeBudget: defaultBudget,
99105
MaxAutoInFlight: defaultMaxInFlight,
100-
ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule),
101-
PeerRules: make(map[route.Vertex]*ThresholdRule),
106+
ChannelRules: make(map[lnwire.ShortChannelID]*SwapRule),
107+
PeerRules: make(map[route.Vertex]*SwapRule),
102108
FailureBackOff: defaultFailureBackoff,
103109
SweepConfTarget: defaultConfTarget,
104110
FeeLimit: defaultFeePortion(),
@@ -216,13 +222,13 @@ type Parameters struct {
216222
// ChannelRules maps a short channel ID to a rule that describes how we
217223
// would like liquidity to be managed. These rules and PeerRules are
218224
// exclusively set to prevent overlap between peer and channel rules.
219-
ChannelRules map[lnwire.ShortChannelID]*ThresholdRule
225+
ChannelRules map[lnwire.ShortChannelID]*SwapRule
220226

221227
// PeerRules maps a peer's pubkey to a rule that applies to all the
222228
// channels that we have with the peer collectively. These rules and
223229
// ChannelRules are exclusively set to prevent overlap between peer
224230
// and channel rules map to avoid ambiguity.
225-
PeerRules map[route.Vertex]*ThresholdRule
231+
PeerRules map[route.Vertex]*SwapRule
226232
}
227233

228234
// String returns the string representation of our parameters.
@@ -386,10 +392,6 @@ type Manager struct {
386392
// current liquidity balance.
387393
cfg *Config
388394

389-
// builder is the swap builder responsible for creating swaps of our
390-
// chosen type for us.
391-
builder swapBuilder
392-
393395
// params is the set of parameters we are currently using. These may be
394396
// updated at runtime.
395397
params Parameters
@@ -428,9 +430,8 @@ func (m *Manager) Run(ctx context.Context) error {
428430
// NewManager creates a liquidity manager which has no rules set.
429431
func NewManager(cfg *Config) *Manager {
430432
return &Manager{
431-
cfg: cfg,
432-
params: defaultParameters,
433-
builder: newLoopOutBuilder(cfg),
433+
cfg: cfg,
434+
params: defaultParameters,
434435
}
435436
}
436437

@@ -473,7 +474,7 @@ func (m *Manager) SetParameters(ctx context.Context, params Parameters) error {
473474
func cloneParameters(params Parameters) Parameters {
474475
paramCopy := params
475476
paramCopy.ChannelRules = make(
476-
map[lnwire.ShortChannelID]*ThresholdRule,
477+
map[lnwire.ShortChannelID]*SwapRule,
477478
len(params.ChannelRules),
478479
)
479480

@@ -483,7 +484,7 @@ func cloneParameters(params Parameters) Parameters {
483484
}
484485

485486
paramCopy.PeerRules = make(
486-
map[route.Vertex]*ThresholdRule,
487+
map[route.Vertex]*SwapRule,
487488
len(params.PeerRules),
488489
)
489490

@@ -617,23 +618,8 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
617618
return m.singleReasonSuggestion(ReasonBudgetNotStarted), nil
618619
}
619620

620-
// Before we get any swap suggestions, we check what the current fee
621-
// estimate is to sweep within our target number of confirmations. If
622-
// This fee exceeds the fee limit we have set, we will not suggest any
623-
// swaps at present.
624-
if err := m.builder.maySwap(ctx, m.params); err != nil {
625-
var reasonErr *reasonError
626-
if errors.As(err, &reasonErr) {
627-
return m.singleReasonSuggestion(reasonErr.reason), nil
628-
629-
}
630-
631-
return nil, err
632-
}
633-
634-
// Get the current server side restrictions, combined with the client
635-
// set restrictions, if any.
636-
restrictions, err := m.getSwapRestrictions(ctx, m.builder.swapType())
621+
// Get restrictions placed on swaps by the server.
622+
outRestrictions, err := m.getSwapRestrictions(ctx, swap.TypeOut)
637623
if err != nil {
638624
return nil, err
639625
}
@@ -653,7 +639,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
653639

654640
// Get a summary of our existing swaps so that we can check our autoloop
655641
// budget.
656-
summary, err := m.checkExistingAutoLoops(ctx, loopOut)
642+
summary, err := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
657643
if err != nil {
658644
return nil, err
659645
}
@@ -721,7 +707,8 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
721707
}
722708

723709
suggestion, err := m.suggestSwap(
724-
ctx, traffic, balances, rule, restrictions, autoloop,
710+
ctx, traffic, balances, rule, outRestrictions,
711+
autoloop,
725712
)
726713
var reasonErr *reasonError
727714
if errors.As(err, &reasonErr) {
@@ -746,7 +733,8 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
746733
}
747734

748735
suggestion, err := m.suggestSwap(
749-
ctx, traffic, balance, rule, restrictions, autoloop,
736+
ctx, traffic, balance, rule, outRestrictions,
737+
autoloop,
750738
)
751739

752740
var reasonErr *reasonError
@@ -841,12 +829,34 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
841829
// suggestSwap checks whether we can currently perform a swap, and creates a
842830
// swap request for the rule provided.
843831
func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic,
844-
balance *balances, rule *ThresholdRule, restrictions *Restrictions,
832+
balance *balances, rule *SwapRule, outRestrictions *Restrictions,
845833
autoloop bool) (swapSuggestion, error) {
846834

835+
var (
836+
builder swapBuilder
837+
restrictions *Restrictions
838+
)
839+
840+
switch rule.Type {
841+
case swap.TypeOut:
842+
builder = newLoopOutBuilder(m.cfg)
843+
restrictions = outRestrictions
844+
845+
default:
846+
return nil, fmt.Errorf("unsupported swap type: %v", rule.Type)
847+
}
848+
849+
// Before we get any swap suggestions, we check what the current fee
850+
// estimate is to sweep within our target number of confirmations. If
851+
// This fee exceeds the fee limit we have set, we will not suggest any
852+
// swaps at present.
853+
if err := builder.maySwap(ctx, m.params); err != nil {
854+
return nil, err
855+
}
856+
847857
// First, check whether this peer/channel combination is already in use
848858
// for our swap.
849-
err := m.builder.inUse(traffic, balance.pubkey, balance.channels)
859+
err := builder.inUse(traffic, balance.pubkey, balance.channels)
850860
if err != nil {
851861
return nil, err
852862
}
@@ -858,7 +868,7 @@ func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic,
858868
return nil, newReasonError(ReasonLiquidityOk)
859869
}
860870

861-
return m.builder.buildSwap(
871+
return builder.buildSwap(
862872
ctx, balance.pubkey, balance.channels, amount, autoloop,
863873
m.params,
864874
)
@@ -948,7 +958,8 @@ func (e *existingAutoLoopSummary) totalFees() btcutil.Amount {
948958
// total for our set of ongoing, automatically dispatched swaps as well as a
949959
// current in-flight count.
950960
func (m *Manager) checkExistingAutoLoops(ctx context.Context,
951-
loopOuts []*loopdb.LoopOut) (*existingAutoLoopSummary, error) {
961+
loopOuts []*loopdb.LoopOut, loopIns []*loopdb.LoopIn) (
962+
*existingAutoLoopSummary, error) {
952963

953964
var summary existingAutoLoopSummary
954965

@@ -987,6 +998,28 @@ func (m *Manager) checkExistingAutoLoops(ctx context.Context,
987998
}
988999
}
9891000

1001+
for _, in := range loopIns {
1002+
if in.Contract.Label != labels.AutoloopLabel(swap.TypeIn) {
1003+
continue
1004+
}
1005+
1006+
pending := in.State().State.Type() == loopdb.StateTypePending
1007+
inBudget := !in.LastUpdateTime().Before(m.params.AutoFeeStartDate)
1008+
1009+
// If an autoloop is in a pending state, we always count it in
1010+
// our current budget, and record the worst-case fees for it,
1011+
// because we do not know how it will resolve.
1012+
if pending {
1013+
summary.inFlightCount++
1014+
summary.pendingFees += worstCaseInFees(
1015+
in.Contract.MaxMinerFee, in.Contract.MaxSwapFee,
1016+
defaultLoopInSweepFee,
1017+
)
1018+
} else if inBudget {
1019+
summary.spentFees += in.State().Cost.Total()
1020+
}
1021+
}
1022+
9901023
return &summary, nil
9911024
}
9921025

@@ -1051,17 +1084,28 @@ func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut,
10511084
}
10521085

10531086
for _, in := range loopIn {
1054-
// Skip completed swaps, they can't affect our channel balances.
1055-
if in.State().State.Type() != loopdb.StateTypePending {
1056-
continue
1057-
}
1058-
10591087
// Skip over swaps that may come through any peer.
10601088
if in.Contract.LastHop == nil {
10611089
continue
10621090
}
10631091

1064-
traffic.ongoingLoopIn[*in.Contract.LastHop] = true
1092+
pubkey := *in.Contract.LastHop
1093+
1094+
switch {
1095+
// Include any pending swaps in our ongoing set of swaps.
1096+
case in.State().State.Type() == loopdb.StateTypePending:
1097+
traffic.ongoingLoopIn[pubkey] = true
1098+
1099+
// If a swap failed with an on-chain timeout, the server could
1100+
// not route to us. We add it to our backoff list so that
1101+
// there's some time for routing conditions to improve.
1102+
case in.State().State == loopdb.StateFailTimeout:
1103+
failedAt := in.LastUpdate().Time
1104+
1105+
if failedAt.After(failureCutoff) {
1106+
traffic.failedLoopIn[pubkey] = failedAt
1107+
}
1108+
}
10651109
}
10661110

10671111
return traffic
@@ -1072,13 +1116,15 @@ type swapTraffic struct {
10721116
ongoingLoopOut map[lnwire.ShortChannelID]bool
10731117
ongoingLoopIn map[route.Vertex]bool
10741118
failedLoopOut map[lnwire.ShortChannelID]time.Time
1119+
failedLoopIn map[route.Vertex]time.Time
10751120
}
10761121

10771122
func newSwapTraffic() *swapTraffic {
10781123
return &swapTraffic{
10791124
ongoingLoopOut: make(map[lnwire.ShortChannelID]bool),
10801125
ongoingLoopIn: make(map[route.Vertex]bool),
10811126
failedLoopOut: make(map[lnwire.ShortChannelID]time.Time),
1127+
failedLoopIn: make(map[route.Vertex]time.Time),
10821128
}
10831129
}
10841130

0 commit comments

Comments
 (0)