mirror of
https://github.com/lightninglabs/loop
synced 2024-11-16 00:12:52 +00:00
9db43a4dbd
In this commit, instead of importing SelectHopHints from lnd, we chose to reimplement the functionality here, for the purpose of adding hop hints in case the user informs us the client node is considered private. In the future, we should modify the origin SelectHopHints to take closures for all it's data sources, so we can use it's logic external
204 lines
6.3 KiB
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
|
|
}
|