mirror of https://github.com/lightninglabs/loop
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
246 lines
7.4 KiB
Go
246 lines
7.4 KiB
Go
package loop
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"github.com/lightningnetwork/lnd/zpay32"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
var (
|
|
// DefaultMaxHopHints is set to 20 as that is the default set in LND
|
|
DefaultMaxHopHints = 20
|
|
)
|
|
|
|
// chanCanBeHopHint checks whether the passed channel could be used as a private
|
|
// hophint.
|
|
func chanCanBeHopHint(chanInfo *lndclient.ChannelInfo) bool {
|
|
return chanInfo.Private && chanInfo.Active
|
|
}
|
|
|
|
// chanRemotePolicy selectes the correct remote routing policy.
|
|
func chanRemotePolicy(remotePub route.Vertex,
|
|
edgeInfo *lndclient.ChannelEdge) *lndclient.RoutingPolicy {
|
|
|
|
if remotePub == edgeInfo.Node1 {
|
|
return edgeInfo.Node1Policy
|
|
}
|
|
|
|
return edgeInfo.Node2Policy
|
|
}
|
|
|
|
// SelectHopHints is a direct port of the SelectHopHints found in lnd. It was
|
|
// reimplemented because the current implementation in LND relies on internals
|
|
// not externalized through the API. Hopefully in the future SelectHopHints
|
|
// will be refactored to allow for custom data sources. It iterates through all
|
|
// the active and public channels available and returns eligible channels.
|
|
// Eligibility requirements are simple: does the channel have enough liquidity
|
|
// to fulfill the request and is the node whitelisted (if specified)
|
|
func SelectHopHints(ctx context.Context, lnd *lndclient.LndServices,
|
|
amtMSat btcutil.Amount, numMaxHophints int,
|
|
includeNodes map[route.Vertex]struct{}) ([][]zpay32.HopHint, error) {
|
|
|
|
// Fetch all active and public channels.
|
|
openChannels, err := lnd.Client.ListChannels(ctx, false, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We'll add our hop hints in two passes, first we'll add all channels
|
|
// that are eligible to be hop hints, and also have a local balance
|
|
// above the payment amount.
|
|
var totalHintBandwidth btcutil.Amount
|
|
|
|
// chanInfoCache is a simple cache for any information we retrieve
|
|
// through GetChanInfo
|
|
chanInfoCache := make(map[uint64]*lndclient.ChannelEdge)
|
|
|
|
// skipCache is a simple cache which holds the indices of any channel
|
|
// that we should skip when doing the second round of channel selection.
|
|
skipCache := make(map[int]struct{})
|
|
|
|
hopHints := make([][]zpay32.HopHint, 0, numMaxHophints)
|
|
|
|
for i, channel := range openChannels {
|
|
channel := channel
|
|
|
|
// In this first pass, we'll ignore all channels in
|
|
// isolation that can't satisfy this payment.
|
|
|
|
// Skip public or inactive channels.
|
|
if !chanCanBeHopHint(&channel) {
|
|
log.Debugf("SelectHopHints: skipping ChannelID: %v, " +
|
|
"as is not eligible for a private hop hint")
|
|
skipCache[i] = struct{}{}
|
|
continue
|
|
}
|
|
|
|
// If includeNodes is set, we'll only add channels with peers in
|
|
// includeNodes. This is done to respect the last_hop parameter.
|
|
if len(includeNodes) > 0 {
|
|
if _, ok := includeNodes[channel.PubKeyBytes]; !ok {
|
|
skipCache[i] = struct{}{}
|
|
continue
|
|
}
|
|
}
|
|
|
|
// We want to prevent leaking private nodes, which we define as
|
|
// nodes with only private channels.
|
|
//
|
|
// GetNodeInfo will never return private channels, even if
|
|
// they're somehow known to us. If there are any channels
|
|
// returned, we can consider the node to be public.
|
|
nodeInfo, err := lnd.Client.GetNodeInfo(
|
|
ctx, channel.PubKeyBytes, true,
|
|
)
|
|
|
|
// If the error is node isn't found, just iterate. Otherwise,
|
|
// fail.
|
|
status, ok := status.FromError(err)
|
|
if ok && status.Code() == codes.NotFound {
|
|
log.Warnf("SelectHopHints: skipping ChannelID: %v, "+
|
|
"as peer (NodeID: %v) is not found: %v",
|
|
channel.ChannelID, channel.PubKeyBytes.String(),
|
|
err)
|
|
continue
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(nodeInfo.Channels) == 0 {
|
|
log.Infof(
|
|
"SelectHopHints: skipping ChannelID: %v as "+
|
|
"peer (NodeID: %v) appears to be private",
|
|
channel.ChannelID, channel.PubKeyBytes.String(),
|
|
)
|
|
|
|
// Skip this channel since the remote node is private.
|
|
skipCache[i] = struct{}{}
|
|
continue
|
|
}
|
|
|
|
// Retrieve extra info for each channel not available in
|
|
// listChannels.
|
|
chanInfo, err := lnd.Client.GetChanInfo(ctx, channel.ChannelID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Cache the GetChanInfo result since it might be useful
|
|
chanInfoCache[channel.ChannelID] = chanInfo
|
|
|
|
// Skip if channel can't forward payment
|
|
if channel.RemoteBalance < amtMSat {
|
|
log.Debugf(
|
|
"SelectHopHints: skipping ChannelID: %v, as "+
|
|
"the remote balance (%v sats) is "+
|
|
"insufficient", channel.ChannelID,
|
|
channel.RemoteBalance,
|
|
)
|
|
continue
|
|
}
|
|
|
|
// Now, we'll need to determine which is the correct policy.
|
|
policy := chanRemotePolicy(channel.PubKeyBytes, chanInfo)
|
|
if policy == nil {
|
|
continue
|
|
}
|
|
|
|
nodePubKey, err := btcec.ParsePubKey(
|
|
channel.PubKeyBytes[:], btcec.S256(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Now that we know this channel is usable, add it as a hop
|
|
// hint and the indices we'll use later.
|
|
hopHints = append(hopHints, []zpay32.HopHint{{
|
|
NodeID: nodePubKey,
|
|
ChannelID: channel.ChannelID,
|
|
FeeBaseMSat: uint32(policy.FeeBaseMsat),
|
|
FeeProportionalMillionths: uint32(
|
|
policy.FeeRateMilliMsat,
|
|
),
|
|
CLTVExpiryDelta: uint16(policy.TimeLockDelta),
|
|
}})
|
|
|
|
totalHintBandwidth += channel.RemoteBalance
|
|
|
|
// Mark the index to skip so we can skip it on the next
|
|
// iteration.
|
|
skipCache[i] = struct{}{}
|
|
}
|
|
|
|
// If we have enough hop hints at this point, then we'll exit early.
|
|
// Otherwise, we'll continue to add more that may help out mpp users.
|
|
if len(hopHints) >= numMaxHophints {
|
|
return hopHints, nil
|
|
}
|
|
|
|
// In this second pass we'll add channels, and we'll either stop when
|
|
// we have 20 hop hints, we've run through all the available channels,
|
|
// or if the sum of available bandwidth in the routing hints exceeds 2x
|
|
// the payment amount. We do 2x here to account for a margin of error
|
|
// if some of the selected channels no longer become operable.
|
|
hopHintFactor := btcutil.Amount(lnwire.MilliSatoshi(2))
|
|
|
|
for i := 0; i < len(openChannels); i++ {
|
|
// If we hit either of our early termination conditions, then
|
|
// we'll break the loop here.
|
|
if totalHintBandwidth > amtMSat*hopHintFactor ||
|
|
len(hopHints) >= numMaxHophints {
|
|
|
|
break
|
|
}
|
|
|
|
// Channels of private nodes, inactive, or public channels or
|
|
// those that have already been selected can be skipped in this
|
|
// iteration.
|
|
if _, ok := skipCache[i]; ok {
|
|
continue
|
|
}
|
|
|
|
channel := openChannels[i]
|
|
chanInfo := chanInfoCache[channel.ChannelID]
|
|
|
|
// Now, we'll need to determine which is the correct policy.
|
|
policy := chanRemotePolicy(channel.PubKeyBytes, chanInfo)
|
|
if policy == nil {
|
|
continue
|
|
}
|
|
|
|
nodeID, err := btcec.ParsePubKey(
|
|
channel.PubKeyBytes[:], btcec.S256())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Include the route hint in our set of options that will be
|
|
// used when creating the invoice.
|
|
hopHints = append(hopHints, []zpay32.HopHint{{
|
|
NodeID: nodeID,
|
|
ChannelID: channel.ChannelID,
|
|
FeeBaseMSat: uint32(policy.FeeBaseMsat),
|
|
FeeProportionalMillionths: uint32(
|
|
policy.FeeRateMilliMsat,
|
|
),
|
|
CLTVExpiryDelta: uint16(policy.TimeLockDelta),
|
|
}})
|
|
|
|
// As we've just added a new hop hint, we'll accumulate it's
|
|
// available balance now to update our tally.
|
|
//
|
|
// TODO(roasbeef): have a cut off based on min bandwidth?
|
|
totalHintBandwidth += channel.RemoteBalance
|
|
}
|
|
|
|
return hopHints, nil
|
|
}
|