Skip to content

Commit 9db43a4

Browse files
committed
loop: Reimplement SelectHopHints from lnd
In this commit, instead of importing SelectHopHints from lnd, we chose to reimplement the functionality here, for the purpose of adding hop hints in case the user informs us the client node is considered private. In the future, we should modify the origin SelectHopHints to take closures for all it's data sources, so we can use it's logic external
1 parent 5504368 commit 9db43a4

File tree

4 files changed

+549
-0
lines changed

4 files changed

+549
-0
lines changed

test/lightning_client_mock.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/lightningnetwork/lnd/channeldb"
1515
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
1616
"github.com/lightningnetwork/lnd/lntypes"
17+
"github.com/lightningnetwork/lnd/routing/route"
1718
"github.com/lightningnetwork/lnd/zpay32"
1819
"golang.org/x/net/context"
1920
)
@@ -180,6 +181,53 @@ func (h *mockLightningClient) ListTransactions(
180181
return txs, nil
181182
}
182183

184+
// GetNodeInfo retrieves info on the node, and if includeChannels is True,
185+
// will return other channels the node may have with other peers
186+
func (h *mockLightningClient) GetNodeInfo(ctx context.Context,
187+
pubKeyBytes route.Vertex, includeChannels bool) (*lndclient.NodeInfo, error) {
188+
189+
nodeInfo := lndclient.NodeInfo{}
190+
191+
if !includeChannels {
192+
return nil, nil
193+
}
194+
195+
nodePubKey, err := route.NewVertexFromStr(h.lnd.NodePubkey)
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
// NodeInfo.Channels should only contain channels which: do not belong
201+
// to the queried node; are not private; have the provided vertex as a
202+
// participant
203+
for _, edge := range h.lnd.ChannelEdges {
204+
if (edge.Node1 == pubKeyBytes || edge.Node2 == pubKeyBytes) &&
205+
(edge.Node1 != nodePubKey || edge.Node2 != nodePubKey) {
206+
207+
for _, channel := range h.lnd.Channels {
208+
if channel.ChannelID == edge.ChannelID && !channel.Private {
209+
nodeInfo.Channels = append(nodeInfo.Channels, *edge)
210+
}
211+
}
212+
}
213+
}
214+
215+
nodeInfo.ChannelCount = len(nodeInfo.Channels)
216+
217+
return &nodeInfo, nil
218+
}
219+
220+
// GetChanInfo retrieves all the info the node has on the given channel
221+
func (h *mockLightningClient) GetChanInfo(ctx context.Context,
222+
channelID uint64) (*lndclient.ChannelEdge, error) {
223+
224+
var channelEdge *lndclient.ChannelEdge
225+
if channelEdge, ok := h.lnd.ChannelEdges[channelID]; ok {
226+
return channelEdge, nil
227+
}
228+
return channelEdge, fmt.Errorf("not found")
229+
}
230+
183231
// ListChannels retrieves all channels of the backing lnd node.
184232
func (h *mockLightningClient) ListChannels(ctx context.Context, _, _ bool) (
185233
[]lndclient.ChannelInfo, error) {

test/lnd_services_mock.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ type LndMockServices struct {
164164
Invoices map[lntypes.Hash]*lndclient.Invoice
165165

166166
Channels []lndclient.ChannelInfo
167+
ChannelEdges map[uint64]*lndclient.ChannelEdge
167168
ClosedChannels []lndclient.ClosedChannel
168169
ForwardingEvents []lndclient.ForwardingEvent
169170
Payments []lndclient.Payment

utils.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package loop
2+
3+
import (
4+
"context"
5+
6+
"github.com/btcsuite/btcd/btcec"
7+
"github.com/btcsuite/btcutil"
8+
"github.com/lightninglabs/lndclient"
9+
"github.com/lightningnetwork/lnd/lnwire"
10+
"github.com/lightningnetwork/lnd/routing/route"
11+
"github.com/lightningnetwork/lnd/zpay32"
12+
"google.golang.org/grpc/codes"
13+
"google.golang.org/grpc/status"
14+
)
15+
16+
var (
17+
// DefaultMaxHopHints is set to 20 as that is the default set in LND
18+
DefaultMaxHopHints = 20
19+
)
20+
21+
// SelectHopHints is a direct port of the SelectHopHints found in lnd. It was
22+
// reimplemented because the current implementation in LND relies on internals
23+
// not externalized through the API. Hopefully in the future SelectHopHints
24+
// will be refactored to allow for custom data sources. It iterates through all
25+
// the active and public channels available and returns eligible channels.
26+
// Eligibility requirements are simple: does the channel have enough liquidity
27+
// to fulfill the request and is the node whitelisted (if specified)
28+
func SelectHopHints(ctx context.Context, lnd *lndclient.LndServices,
29+
amtMSat btcutil.Amount, numMaxHophints int,
30+
includeNodes map[route.Vertex]struct{}) ([][]zpay32.HopHint, error) {
31+
32+
// Fetch all active and public channels.
33+
openChannels, err := lnd.Client.ListChannels(ctx, false, false)
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
// We'll add our hop hints in two passes, first we'll add all channels
39+
// that are eligible to be hop hints, and also have a local balance
40+
// above the payment amount.
41+
var totalHintBandwidth btcutil.Amount
42+
43+
// chanInfoCache is a simple cache for any information we retrieve
44+
// through GetChanInfo
45+
chanInfoCache := make(map[uint64]*lndclient.ChannelEdge)
46+
47+
// skipCache is a simple cache which holds the indice of any
48+
// channel we've added to final hopHints
49+
skipCache := make(map[int]struct{})
50+
51+
hopHints := make([][]zpay32.HopHint, 0, numMaxHophints)
52+
53+
for i, channel := range openChannels {
54+
// In this first pass, we'll ignore all channels in
55+
// isolation that can't satisfy this payment.
56+
57+
// Retrieve extra info for each channel not available in
58+
// listChannels
59+
chanInfo, err := lnd.Client.GetChanInfo(ctx, channel.ChannelID)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
// Cache the GetChanInfo result since it might be useful
65+
chanInfoCache[channel.ChannelID] = chanInfo
66+
67+
// Skip if channel can't forward payment
68+
if channel.RemoteBalance < amtMSat {
69+
log.Debugf(
70+
"Skipping ChannelID: %v for hints as "+
71+
"remote balance (%v sats) "+
72+
"insufficient appears to be private",
73+
channel.ChannelID, channel.RemoteBalance,
74+
)
75+
continue
76+
}
77+
// If includeNodes is set, we'll only add channels with peers in
78+
// includeNodes. This is done to respect the last_hop parameter.
79+
if len(includeNodes) > 0 {
80+
if _, ok := includeNodes[channel.PubKeyBytes]; !ok {
81+
continue
82+
}
83+
}
84+
85+
// Mark the index to skip so we can skip it on the next
86+
// iteration if needed. We'll skip all channels that make
87+
// it past this point as they'll likely belong to private
88+
// nodes or be selected.
89+
skipCache[i] = struct{}{}
90+
91+
// We want to prevent leaking private nodes, which we define as
92+
// nodes with only private channels.
93+
//
94+
// GetNodeInfo will never return private channels, even if
95+
// they're somehow known to us. If there are any channels
96+
// returned, we can consider the node to be public.
97+
nodeInfo, err := lnd.Client.GetNodeInfo(
98+
ctx, channel.PubKeyBytes, true,
99+
)
100+
101+
// If the error is node isn't found, just iterate. Otherwise,
102+
// fail.
103+
status, ok := status.FromError(err)
104+
if ok && status.Code() == codes.NotFound {
105+
log.Warnf("Skipping ChannelID: %v for hints as peer "+
106+
"(NodeID: %v) is not found: %v",
107+
channel.ChannelID, channel.PubKeyBytes.String(),
108+
err)
109+
continue
110+
} else if err != nil {
111+
return nil, err
112+
}
113+
114+
if len(nodeInfo.Channels) == 0 {
115+
log.Infof(
116+
"Skipping ChannelID: %v for hints as peer "+
117+
"(NodeID: %v) appears to be private",
118+
channel.ChannelID, channel.PubKeyBytes.String(),
119+
)
120+
continue
121+
}
122+
123+
nodeID, err := btcec.ParsePubKey(
124+
channel.PubKeyBytes[:], btcec.S256(),
125+
)
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
// Now that we now this channel use usable, add it as a hop
131+
// hint and the indexes we'll use later.
132+
hopHints = append(hopHints, []zpay32.HopHint{{
133+
NodeID: nodeID,
134+
ChannelID: channel.ChannelID,
135+
FeeBaseMSat: uint32(chanInfo.Node2Policy.FeeBaseMsat),
136+
FeeProportionalMillionths: uint32(
137+
chanInfo.Node2Policy.FeeRateMilliMsat,
138+
),
139+
CLTVExpiryDelta: uint16(
140+
chanInfo.Node2Policy.TimeLockDelta),
141+
}})
142+
143+
totalHintBandwidth += channel.RemoteBalance
144+
}
145+
146+
// If we have enough hop hints at this point, then we'll exit early.
147+
// Otherwise, we'll continue to add more that may help out mpp users.
148+
if len(hopHints) >= numMaxHophints {
149+
return hopHints, nil
150+
}
151+
152+
// In this second pass we'll add channels, and we'll either stop when
153+
// we have 20 hop hints, we've run through all the available channels,
154+
// or if the sum of available bandwidth in the routing hints exceeds 2x
155+
// the payment amount. We do 2x here to account for a margin of error
156+
// if some of the selected channels no longer become operable.
157+
hopHintFactor := btcutil.Amount(lnwire.MilliSatoshi(2))
158+
159+
for i := 0; i < len(openChannels); i++ {
160+
// If we hit either of our early termination conditions, then
161+
// we'll break the loop here.
162+
if totalHintBandwidth > amtMSat*hopHintFactor ||
163+
len(hopHints) >= numMaxHophints {
164+
165+
break
166+
}
167+
168+
// Skip the channel if we already selected it.
169+
if _, ok := skipCache[i]; ok {
170+
continue
171+
}
172+
173+
channel := openChannels[i]
174+
chanInfo := chanInfoCache[channel.ChannelID]
175+
176+
nodeID, err := btcec.ParsePubKey(
177+
channel.PubKeyBytes[:], btcec.S256())
178+
if err != nil {
179+
continue
180+
}
181+
182+
// Include the route hint in our set of options that will be
183+
// used when creating the invoice.
184+
hopHints = append(hopHints, []zpay32.HopHint{{
185+
NodeID: nodeID,
186+
ChannelID: channel.ChannelID,
187+
FeeBaseMSat: uint32(chanInfo.Node2Policy.FeeBaseMsat),
188+
FeeProportionalMillionths: uint32(
189+
chanInfo.Node2Policy.FeeRateMilliMsat,
190+
),
191+
CLTVExpiryDelta: uint16(
192+
chanInfo.Node2Policy.TimeLockDelta),
193+
}})
194+
195+
// As we've just added a new hop hint, we'll accumulate it's
196+
// available balance now to update our tally.
197+
//
198+
// TODO(roasbeef): have a cut off based on min bandwidth?
199+
totalHintBandwidth += channel.RemoteBalance
200+
}
201+
202+
return hopHints, nil
203+
}

0 commit comments

Comments
 (0)