@@ -2,202 +2,170 @@ package loop
22
33import (
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
1621var (
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