Skip to content

Commit 4c58d0c

Browse files
authored
Merge pull request #418 from carlaKC/autoloop-builder
liquidity: add swap builder interface
2 parents 7ac6e26 + 8bd7ee3 commit 4c58d0c

File tree

4 files changed

+274
-196
lines changed

4 files changed

+274
-196
lines changed

liquidity/interface.go

Lines changed: 29 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package liquidity
22

33
import (
4+
"context"
5+
46
"github.com/btcsuite/btcutil"
57
"github.com/lightninglabs/loop"
8+
"github.com/lightninglabs/loop/swap"
69
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
710
"github.com/lightningnetwork/lnd/lnwire"
811
"github.com/lightningnetwork/lnd/routing/route"
@@ -31,6 +34,32 @@ type FeeLimit interface {
3134
btcutil.Amount, btcutil.Amount, btcutil.Amount)
3235
}
3336

37+
// swapBuilder is an interface used to build our different swap types.
38+
type swapBuilder interface {
39+
// swapType returns the swap type that the builder is responsible for
40+
// creating.
41+
swapType() swap.Type
42+
43+
// maySwap checks whether we can currently execute a swap, examining
44+
// the current on-chain fee conditions against relevant to our swap
45+
// type against our fee restrictions.
46+
maySwap(ctx context.Context, params Parameters) error
47+
48+
// inUse examines our current swap traffic to determine whether we
49+
// should suggest the builder's type of swap for the peer and channels
50+
// suggested.
51+
inUse(traffic *swapTraffic, peer route.Vertex,
52+
channels []lnwire.ShortChannelID) error
53+
54+
// buildSwap creates a swap for the target peer/channels provided. The
55+
// autoloop boolean indicates whether this swap will actually be
56+
// executed, because there are some calls we can leave out if this swap
57+
// is just for a dry run.
58+
buildSwap(ctx context.Context, peer route.Vertex,
59+
channels []lnwire.ShortChannelID, amount btcutil.Amount,
60+
autoloop bool, params Parameters) (swapSuggestion, error)
61+
}
62+
3463
// swapSuggestion is an interface implemented by suggested swaps for our
3564
// different swap types. This interface is used to allow us to handle different
3665
// swap types with the same autoloop logic.
@@ -50,55 +79,3 @@ type swapSuggestion interface {
5079
// can be looked up.
5180
peers(knownChans map[uint64]route.Vertex) []route.Vertex
5281
}
53-
54-
// Compile-time assertion that loopOutSwapSuggestion satisfies the
55-
// swapSuggestion interface.
56-
var _ swapSuggestion = (*loopOutSwapSuggestion)(nil)
57-
58-
type loopOutSwapSuggestion struct {
59-
loop.OutRequest
60-
}
61-
62-
func (l *loopOutSwapSuggestion) amount() btcutil.Amount {
63-
return l.Amount
64-
}
65-
66-
func (l *loopOutSwapSuggestion) fees() btcutil.Amount {
67-
return worstCaseOutFees(
68-
l.MaxPrepayRoutingFee, l.MaxSwapRoutingFee, l.MaxSwapFee,
69-
l.MaxMinerFee, l.MaxPrepayAmount,
70-
)
71-
}
72-
73-
func (l *loopOutSwapSuggestion) channels() []lnwire.ShortChannelID {
74-
channels := make([]lnwire.ShortChannelID, len(l.OutgoingChanSet))
75-
76-
for i, id := range l.OutgoingChanSet {
77-
channels[i] = lnwire.NewShortChanIDFromInt(id)
78-
}
79-
80-
return channels
81-
}
82-
83-
// peers returns the set of peers that the loop out swap is restricted to.
84-
func (l *loopOutSwapSuggestion) peers(
85-
knownChans map[uint64]route.Vertex) []route.Vertex {
86-
87-
peers := make(map[route.Vertex]struct{}, len(knownChans))
88-
89-
for _, channel := range l.OutgoingChanSet {
90-
peer, ok := knownChans[channel]
91-
if !ok {
92-
log.Warnf("peer for channel: %v unknown", channel)
93-
}
94-
95-
peers[peer] = struct{}{}
96-
}
97-
98-
peerList := make([]route.Vertex, 0, len(peers))
99-
for peer := range peers {
100-
peerList = append(peerList, peer)
101-
}
102-
103-
return peerList
104-
}

liquidity/liquidity.go

Lines changed: 17 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,10 @@ type Manager struct {
386386
// current liquidity balance.
387387
cfg *Config
388388

389+
// builder is the swap builder responsible for creating swaps of our
390+
// chosen type for us.
391+
builder swapBuilder
392+
389393
// params is the set of parameters we are currently using. These may be
390394
// updated at runtime.
391395
params Parameters
@@ -424,8 +428,9 @@ func (m *Manager) Run(ctx context.Context) error {
424428
// NewManager creates a liquidity manager which has no rules set.
425429
func NewManager(cfg *Config) *Manager {
426430
return &Manager{
427-
cfg: cfg,
428-
params: defaultParameters,
431+
cfg: cfg,
432+
params: defaultParameters,
433+
builder: newLoopOutBuilder(cfg),
429434
}
430435
}
431436

@@ -616,14 +621,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
616621
// estimate is to sweep within our target number of confirmations. If
617622
// This fee exceeds the fee limit we have set, we will not suggest any
618623
// swaps at present.
619-
estimate, err := m.cfg.Lnd.WalletKit.EstimateFee(
620-
ctx, m.params.SweepConfTarget,
621-
)
622-
if err != nil {
623-
return nil, err
624-
}
625-
626-
if err := m.params.FeeLimit.mayLoopOut(estimate); err != nil {
624+
if err := m.builder.maySwap(ctx, m.params); err != nil {
627625
var reasonErr *reasonError
628626
if errors.As(err, &reasonErr) {
629627
return m.singleReasonSuggestion(reasonErr.reason), nil
@@ -635,7 +633,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
635633

636634
// Get the current server side restrictions, combined with the client
637635
// set restrictions, if any.
638-
restrictions, err := m.getSwapRestrictions(ctx, swap.TypeOut)
636+
restrictions, err := m.getSwapRestrictions(ctx, m.builder.swapType())
639637
if err != nil {
640638
return nil, err
641639
}
@@ -846,65 +844,24 @@ func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic,
846844
balance *balances, rule *ThresholdRule, restrictions *Restrictions,
847845
autoloop bool) (swapSuggestion, error) {
848846

849-
// Check whether we can perform a swap.
850-
err := traffic.maySwap(balance.pubkey, balance.channels)
847+
// First, check whether this peer/channel combination is already in use
848+
// for our swap.
849+
err := m.builder.inUse(traffic, balance.pubkey, balance.channels)
851850
if err != nil {
852851
return nil, err
853852
}
854853

855-
// We can have nil suggestions in the case where no action is
856-
// required, so we skip over them.
854+
// Next, get the amount that we need to swap for this entity, skipping
855+
// over it if no change in liquidity is required.
857856
amount := rule.swapAmount(balance, restrictions)
858857
if amount == 0 {
859858
return nil, newReasonError(ReasonLiquidityOk)
860859
}
861860

862-
swap, err := m.loopOutSwap(ctx, amount, balance, autoloop)
863-
if err != nil {
864-
return nil, err
865-
}
866-
867-
return &loopOutSwapSuggestion{
868-
OutRequest: *swap,
869-
}, nil
870-
}
871-
872-
// loopOutSwap creates a loop out swap with the amount provided for the balance
873-
// described by the balance set provided. A reason that indicates whether we
874-
// can swap is returned. If this value is not ReasonNone, there is no possible
875-
// swap and the loop out request returned will be nil.
876-
func (m *Manager) loopOutSwap(ctx context.Context, amount btcutil.Amount,
877-
balance *balances, autoloop bool) (*loop.OutRequest, error) {
878-
879-
quote, err := m.cfg.LoopOutQuote(
880-
ctx, &loop.LoopOutQuoteRequest{
881-
Amount: amount,
882-
SweepConfTarget: m.params.SweepConfTarget,
883-
SwapPublicationDeadline: m.cfg.Clock.Now(),
884-
},
885-
)
886-
if err != nil {
887-
return nil, err
888-
}
889-
890-
log.Debugf("quote for suggestion: %v, swap fee: %v, "+
891-
"miner fee: %v, prepay: %v", amount, quote.SwapFee,
892-
quote.MinerFee, quote.PrepayAmount)
893-
894-
// Check that the estimated fees for the suggested swap are
895-
// below the fee limits configured by the manager.
896-
if err := m.params.FeeLimit.loopOutLimits(amount, quote); err != nil {
897-
return nil, err
898-
}
899-
900-
outRequest, err := m.makeLoopOutRequest(
901-
ctx, amount, balance, quote, autoloop,
861+
return m.builder.buildSwap(
862+
ctx, balance.pubkey, balance.channels, amount, autoloop,
863+
m.params,
902864
)
903-
if err != nil {
904-
return nil, err
905-
}
906-
907-
return &outRequest, nil
908865
}
909866

910867
// getSwapRestrictions queries the server for its latest swap size restrictions,
@@ -942,58 +899,6 @@ func (m *Manager) getSwapRestrictions(ctx context.Context, swapType swap.Type) (
942899
return restrictions, nil
943900
}
944901

945-
// makeLoopOutRequest creates a loop out request from a suggestion. Since we
946-
// do not get any information about our off-chain routing fees when we request
947-
// a quote, we just set our prepay and route maximum fees directly from the
948-
// amounts we expect to route. The estimation we use elsewhere is the repo is
949-
// route-independent, which is a very poor estimation so we don't bother with
950-
// checking against this inaccurate constant. We use the exact prepay amount
951-
// and swap fee given to us by the server, but use our maximum miner fee anyway
952-
// to give us some leeway when performing the swap. We take an auto-out which
953-
// determines whether we set a label identifying this swap as automatically
954-
// dispatched, and decides whether we set a sweep address (we don't bother for
955-
// non-auto requests, because the client api will set it anyway).
956-
func (m *Manager) makeLoopOutRequest(ctx context.Context,
957-
amount btcutil.Amount, balance *balances, quote *loop.LoopOutQuote,
958-
autoloop bool) (loop.OutRequest, error) {
959-
960-
prepayMaxFee, routeMaxFee, minerFee := m.params.FeeLimit.loopOutFees(
961-
amount, quote,
962-
)
963-
964-
var chanSet loopdb.ChannelSet
965-
for _, channel := range balance.channels {
966-
chanSet = append(chanSet, channel.ToUint64())
967-
}
968-
969-
// Create a request with our calculated routing fees. We can use the
970-
// swap fee, prepay amount and miner fee from the quote because we have
971-
// already validated them.
972-
request := loop.OutRequest{
973-
Amount: amount,
974-
OutgoingChanSet: chanSet,
975-
MaxPrepayRoutingFee: prepayMaxFee,
976-
MaxSwapRoutingFee: routeMaxFee,
977-
MaxMinerFee: minerFee,
978-
MaxSwapFee: quote.SwapFee,
979-
MaxPrepayAmount: quote.PrepayAmount,
980-
SweepConfTarget: m.params.SweepConfTarget,
981-
Initiator: autoloopSwapInitiator,
982-
}
983-
984-
if autoloop {
985-
request.Label = labels.AutoloopLabel(swap.TypeOut)
986-
987-
addr, err := m.cfg.Lnd.WalletKit.NextAddr(ctx)
988-
if err != nil {
989-
return loop.OutRequest{}, err
990-
}
991-
request.DestAddr = addr
992-
}
993-
994-
return request, nil
995-
}
996-
997902
// worstCaseOutFees calculates the largest possible fees for a loop out swap,
998903
// comparing the fees for a successful swap to the cost when the client pays
999904
// the prepay because they failed to sweep the on chain htlc. This is unlikely,
@@ -1177,38 +1082,6 @@ func newSwapTraffic() *swapTraffic {
11771082
}
11781083
}
11791084

1180-
// maySwap returns a boolean that indicates whether we may perform a swap for a
1181-
// peer and its set of channels.
1182-
func (s *swapTraffic) maySwap(peer route.Vertex,
1183-
channels []lnwire.ShortChannelID) error {
1184-
1185-
for _, chanID := range channels {
1186-
lastFail, recentFail := s.failedLoopOut[chanID]
1187-
if recentFail {
1188-
log.Debugf("Channel: %v not eligible for suggestions, was "+
1189-
"part of a failed swap at: %v", chanID, lastFail)
1190-
1191-
return newReasonError(ReasonFailureBackoff)
1192-
}
1193-
1194-
if s.ongoingLoopOut[chanID] {
1195-
log.Debugf("Channel: %v not eligible for suggestions, "+
1196-
"ongoing loop out utilizing channel", chanID)
1197-
1198-
return newReasonError(ReasonLoopOut)
1199-
}
1200-
}
1201-
1202-
if s.ongoingLoopIn[peer] {
1203-
log.Debugf("Peer: %x not eligible for suggestions ongoing "+
1204-
"loop in utilizing peer", peer)
1205-
1206-
return newReasonError(ReasonLoopIn)
1207-
}
1208-
1209-
return nil
1210-
}
1211-
12121085
// satPerKwToSatPerVByte converts sat per kWeight to sat per vByte.
12131086
func satPerKwToSatPerVByte(satPerKw chainfee.SatPerKWeight) int64 {
12141087
return int64(satPerKw.FeePerKVByte() / 1000)

liquidity/loopout.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package liquidity
2+
3+
import (
4+
"github.com/btcsuite/btcutil"
5+
"github.com/lightninglabs/loop"
6+
"github.com/lightningnetwork/lnd/lnwire"
7+
"github.com/lightningnetwork/lnd/routing/route"
8+
)
9+
10+
// Compile-time assertion that loopOutSwapSuggestion satisfies the
11+
// swapSuggestion interface.
12+
var _ swapSuggestion = (*loopOutSwapSuggestion)(nil)
13+
14+
// loopOutSwapSuggestion is an implementation of the swapSuggestion interface
15+
// implemented to allow us to abstract away from the details of a specific
16+
// swap.
17+
type loopOutSwapSuggestion struct {
18+
loop.OutRequest
19+
}
20+
21+
// amount returns the amount being swapped.
22+
func (l *loopOutSwapSuggestion) amount() btcutil.Amount {
23+
return l.Amount
24+
}
25+
26+
// fees returns the maximum fees we could possibly pay for this swap.
27+
func (l *loopOutSwapSuggestion) fees() btcutil.Amount {
28+
return worstCaseOutFees(
29+
l.MaxPrepayRoutingFee, l.MaxSwapRoutingFee, l.MaxSwapFee,
30+
l.MaxMinerFee, l.MaxPrepayAmount,
31+
)
32+
}
33+
34+
// channels returns the set of channels the loop out swap is restricted to.
35+
func (l *loopOutSwapSuggestion) channels() []lnwire.ShortChannelID {
36+
channels := make([]lnwire.ShortChannelID, len(l.OutgoingChanSet))
37+
38+
for i, id := range l.OutgoingChanSet {
39+
channels[i] = lnwire.NewShortChanIDFromInt(id)
40+
}
41+
42+
return channels
43+
}
44+
45+
// peers returns the set of peers that the loop out swap is restricted to.
46+
func (l *loopOutSwapSuggestion) peers(
47+
knownChans map[uint64]route.Vertex) []route.Vertex {
48+
49+
peers := make(map[route.Vertex]struct{}, len(knownChans))
50+
51+
for _, channel := range l.OutgoingChanSet {
52+
peer, ok := knownChans[channel]
53+
if !ok {
54+
log.Warnf("peer for channel: %v unknown", channel)
55+
}
56+
57+
peers[peer] = struct{}{}
58+
}
59+
60+
peerList := make([]route.Vertex, 0, len(peers))
61+
for peer := range peers {
62+
peerList = append(peerList, peer)
63+
}
64+
65+
return peerList
66+
}

0 commit comments

Comments
 (0)