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.
loop/utils.go

204 lines
6.3 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
)
// 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 indice of any
// channel we've added to final hopHints
skipCache := make(map[int]struct{})
hopHints := make([][]zpay32.HopHint, 0, numMaxHophints)
for i, channel := range openChannels {
// In this first pass, we'll ignore all channels in
// isolation that can't satisfy this payment.
// 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(
"Skipping ChannelID: %v for hints as "+
"remote balance (%v sats) "+
"insufficient appears to be private",
channel.ChannelID, channel.RemoteBalance,
)
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 {
continue
}
}
// Mark the index to skip so we can skip it on the next
// iteration if needed. We'll skip all channels that make
// it past this point as they'll likely belong to private
// nodes or be selected.
skipCache[i] = struct{}{}
// 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("Skipping ChannelID: %v for hints 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(
"Skipping ChannelID: %v for hints as peer "+
"(NodeID: %v) appears to be private",
channel.ChannelID, channel.PubKeyBytes.String(),
)
continue
}
nodeID, err := btcec.ParsePubKey(
channel.PubKeyBytes[:], btcec.S256(),
)
if err != nil {
return nil, err
}
// Now that we now this channel use usable, add it as a hop
// hint and the indexes we'll use later.
hopHints = append(hopHints, []zpay32.HopHint{{
NodeID: nodeID,
ChannelID: channel.ChannelID,
FeeBaseMSat: uint32(chanInfo.Node2Policy.FeeBaseMsat),
FeeProportionalMillionths: uint32(
chanInfo.Node2Policy.FeeRateMilliMsat,
),
CLTVExpiryDelta: uint16(
chanInfo.Node2Policy.TimeLockDelta),
}})
totalHintBandwidth += channel.RemoteBalance
}
// 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
}
// Skip the channel if we already selected it.
if _, ok := skipCache[i]; ok {
continue
}
channel := openChannels[i]
chanInfo := chanInfoCache[channel.ChannelID]
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(chanInfo.Node2Policy.FeeBaseMsat),
FeeProportionalMillionths: uint32(
chanInfo.Node2Policy.FeeRateMilliMsat,
),
CLTVExpiryDelta: uint16(
chanInfo.Node2Policy.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
}