|
| 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