Skip to content

Commit 31f7b59

Browse files
committed
utils: use SelectHopHints from LND
1 parent db2fba6 commit 31f7b59

File tree

2 files changed

+125
-454
lines changed

2 files changed

+125
-454
lines changed

utils.go

Lines changed: 125 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -2,202 +2,170 @@ package loop
22

33
import (
44
"context"
5+
"fmt"
6+
"strconv"
7+
"strings"
58

69
"github.com/btcsuite/btcd/btcec"
10+
"github.com/btcsuite/btcd/chaincfg/chainhash"
11+
"github.com/btcsuite/btcd/wire"
712
"github.com/btcsuite/btcutil"
813
"github.com/lightninglabs/lndclient"
14+
"github.com/lightningnetwork/lnd/channeldb"
15+
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
916
"github.com/lightningnetwork/lnd/lnwire"
1017
"github.com/lightningnetwork/lnd/routing/route"
1118
"github.com/lightningnetwork/lnd/zpay32"
12-
"google.golang.org/grpc/codes"
13-
"google.golang.org/grpc/status"
1419
)
1520

1621
var (
17-
// DefaultMaxHopHints is set to 20 as that is the default set in LND
22+
// DefaultMaxHopHints is set to 20 as that is the default set in LND.
1823
DefaultMaxHopHints = 20
1924
)
2025

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) {
26+
// isPublicNode checks if a node is public, by simply checking if there's any
27+
// channels reported to the node.
28+
func isPublicNode(ctx context.Context, lnd *lndclient.LndServices,
29+
pubKey [33]byte) (bool, error) {
30+
31+
// GetNodeInfo doesn't report our private channels with the queried node
32+
// so we can use it to determine if the node is considered public.
33+
nodeInfo, err := lnd.Client.GetNodeInfo(
34+
ctx, pubKey, true,
35+
)
3136

32-
// Fetch all active and public channels.
33-
openChannels, err := lnd.Client.ListChannels(ctx, false, false)
3437
if err != nil {
35-
return nil, err
38+
return false, err
3639
}
3740

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
41+
return (nodeInfo.ChannelCount > 0), nil
42+
}
4243

43-
// chanInfoCache is a simple cache for any information we retrieve
44-
// through GetChanInfo
45-
chanInfoCache := make(map[uint64]*lndclient.ChannelEdge)
44+
// fetchChannelEdgesByID fetches the edge info for the passed channel and
45+
// returns the channeldb structs filled with the data that is needed for
46+
// LND's SelectHopHints implementation.
47+
func fetchChannelEdgesByID(ctx context.Context, lnd *lndclient.LndServices,
48+
chanID uint64) (*channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy,
49+
*channeldb.ChannelEdgePolicy, error) {
4650

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{})
51+
chanInfo, err := lnd.Client.GetChanInfo(ctx, chanID)
52+
if err != nil {
53+
return nil, nil, nil, err
54+
}
5055

51-
hopHints := make([][]zpay32.HopHint, 0, numMaxHophints)
56+
edgeInfo := &channeldb.ChannelEdgeInfo{
57+
ChannelID: chanID,
58+
NodeKey1Bytes: chanInfo.Node1,
59+
NodeKey2Bytes: chanInfo.Node2,
60+
}
5261

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.
62+
policy1 := &channeldb.ChannelEdgePolicy{
63+
FeeBaseMSat: lnwire.MilliSatoshi(
64+
chanInfo.Node1Policy.FeeBaseMsat,
65+
),
66+
FeeProportionalMillionths: lnwire.MilliSatoshi(
67+
chanInfo.Node1Policy.FeeRateMilliMsat,
68+
),
69+
TimeLockDelta: uint16(chanInfo.Node1Policy.TimeLockDelta),
70+
}
5671

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-
}
72+
policy2 := &channeldb.ChannelEdgePolicy{
73+
FeeBaseMSat: lnwire.MilliSatoshi(
74+
chanInfo.Node2Policy.FeeBaseMsat,
75+
),
76+
FeeProportionalMillionths: lnwire.MilliSatoshi(
77+
chanInfo.Node2Policy.FeeRateMilliMsat,
78+
),
79+
TimeLockDelta: uint16(chanInfo.Node2Policy.TimeLockDelta),
80+
}
6381

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.
82+
return edgeInfo, policy1, policy2, nil
83+
}
84+
85+
// parseOutPoint attempts to parse an outpoint from the passed in string.
86+
func parseOutPoint(s string) (*wire.OutPoint, error) {
87+
split := strings.Split(s, ":")
88+
if len(split) != 2 {
89+
return nil, fmt.Errorf("expecting outpoint to be in format "+
90+
"of txid:index: %s", s)
91+
}
92+
93+
index, err := strconv.ParseInt(split[1], 10, 32)
94+
if err != nil {
95+
return nil, fmt.Errorf("unable to decode output index: %v", err)
96+
}
97+
98+
txid, err := chainhash.NewHashFromStr(split[0])
99+
if err != nil {
100+
return nil, fmt.Errorf("unable to parse hex string: %v", err)
101+
}
102+
103+
return &wire.OutPoint{
104+
Hash: *txid,
105+
Index: uint32(index),
106+
}, nil
107+
}
108+
109+
// SelectHopHints calls into LND's exposed SelectHopHints prefiltered to the
110+
// includeNodes map (unless it's empty).
111+
func SelectHopHints(ctx context.Context, lnd *lndclient.LndServices,
112+
amt btcutil.Amount, numMaxHophints int,
113+
includeNodes map[route.Vertex]struct{}) ([][]zpay32.HopHint, error) {
114+
115+
cfg := &invoicesrpc.SelectHopHintsCfg{
116+
IsPublicNode: func(pubKey [33]byte) (bool, error) {
117+
return isPublicNode(ctx, lnd, pubKey)
118+
},
119+
FetchChannelEdgesByID: func(chanID uint64) (
120+
*channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy,
121+
*channeldb.ChannelEdgePolicy, error) {
122+
123+
return fetchChannelEdgesByID(ctx, lnd, chanID)
124+
},
125+
}
126+
// Fetch all active and public channels.
127+
channels, err := lnd.Client.ListChannels(ctx, false, false)
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
openChannels := []*invoicesrpc.HopHintInfo{}
133+
for _, channel := range channels {
79134
if len(includeNodes) > 0 {
80135
if _, ok := includeNodes[channel.PubKeyBytes]; !ok {
81136
continue
82137
}
83138
}
84139

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 {
140+
outPoint, err := parseOutPoint(channel.ChannelPoint)
141+
if err != nil {
111142
return nil, err
112143
}
113144

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(
145+
remotePubkey, err := btcec.ParsePubKey(
124146
channel.PubKeyBytes[:], btcec.S256(),
125147
)
126148
if err != nil {
127149
return nil, err
128150
}
129151

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
152+
openChannels = append(
153+
openChannels, &invoicesrpc.HopHintInfo{
154+
IsPublic: !channel.Private,
155+
IsActive: channel.Active,
156+
FundingOutpoint: *outPoint,
157+
RemotePubkey: remotePubkey,
158+
RemoteBalance: lnwire.MilliSatoshi(
159+
channel.RemoteBalance * 1000,
160+
),
161+
ShortChannelID: channel.ChannelID,
162+
},
163+
)
150164
}
151165

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-
}
166+
routeHints := invoicesrpc.SelectHopHints(
167+
lnwire.MilliSatoshi(amt*1000), cfg, openChannels, numMaxHophints,
168+
)
201169

202-
return hopHints, nil
170+
return routeHints, nil
203171
}

0 commit comments

Comments
 (0)