mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
utils: use SelectHopHints from LND
This commit is contained in:
parent
db2fba6f0d
commit
31f7b5947a
2 changed files with 127 additions and 456 deletions
286
utils.go
286
utils.go
|
|
@ -2,202 +2,170 @@ package loop
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"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 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)
|
||||
// isPublicNode checks if a node is public, by simply checking if there's any
|
||||
// channels reported to the node.
|
||||
func isPublicNode(ctx context.Context, lnd *lndclient.LndServices,
|
||||
pubKey [33]byte) (bool, error) {
|
||||
|
||||
// GetNodeInfo doesn't report our private channels with the queried node
|
||||
// so we can use it to determine if the node is considered public.
|
||||
nodeInfo, err := lnd.Client.GetNodeInfo(
|
||||
ctx, pubKey, true,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return (nodeInfo.ChannelCount > 0), nil
|
||||
}
|
||||
|
||||
// fetchChannelEdgesByID fetches the edge info for the passed channel and
|
||||
// returns the channeldb structs filled with the data that is needed for
|
||||
// LND's SelectHopHints implementation.
|
||||
func fetchChannelEdgesByID(ctx context.Context, lnd *lndclient.LndServices,
|
||||
chanID uint64) (*channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy,
|
||||
*channeldb.ChannelEdgePolicy, error) {
|
||||
|
||||
chanInfo, err := lnd.Client.GetChanInfo(ctx, chanID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
edgeInfo := &channeldb.ChannelEdgeInfo{
|
||||
ChannelID: chanID,
|
||||
NodeKey1Bytes: chanInfo.Node1,
|
||||
NodeKey2Bytes: chanInfo.Node2,
|
||||
}
|
||||
|
||||
policy1 := &channeldb.ChannelEdgePolicy{
|
||||
FeeBaseMSat: lnwire.MilliSatoshi(
|
||||
chanInfo.Node1Policy.FeeBaseMsat,
|
||||
),
|
||||
FeeProportionalMillionths: lnwire.MilliSatoshi(
|
||||
chanInfo.Node1Policy.FeeRateMilliMsat,
|
||||
),
|
||||
TimeLockDelta: uint16(chanInfo.Node1Policy.TimeLockDelta),
|
||||
}
|
||||
|
||||
policy2 := &channeldb.ChannelEdgePolicy{
|
||||
FeeBaseMSat: lnwire.MilliSatoshi(
|
||||
chanInfo.Node2Policy.FeeBaseMsat,
|
||||
),
|
||||
FeeProportionalMillionths: lnwire.MilliSatoshi(
|
||||
chanInfo.Node2Policy.FeeRateMilliMsat,
|
||||
),
|
||||
TimeLockDelta: uint16(chanInfo.Node2Policy.TimeLockDelta),
|
||||
}
|
||||
|
||||
return edgeInfo, policy1, policy2, nil
|
||||
}
|
||||
|
||||
// parseOutPoint attempts to parse an outpoint from the passed in string.
|
||||
func parseOutPoint(s string) (*wire.OutPoint, error) {
|
||||
split := strings.Split(s, ":")
|
||||
if len(split) != 2 {
|
||||
return nil, fmt.Errorf("expecting outpoint to be in format "+
|
||||
"of txid:index: %s", s)
|
||||
}
|
||||
|
||||
index, err := strconv.ParseInt(split[1], 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode output index: %v", err)
|
||||
}
|
||||
|
||||
txid, err := chainhash.NewHashFromStr(split[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse hex string: %v", err)
|
||||
}
|
||||
|
||||
return &wire.OutPoint{
|
||||
Hash: *txid,
|
||||
Index: uint32(index),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SelectHopHints calls into LND's exposed SelectHopHints prefiltered to the
|
||||
// includeNodes map (unless it's empty).
|
||||
func SelectHopHints(ctx context.Context, lnd *lndclient.LndServices,
|
||||
amtMSat btcutil.Amount, numMaxHophints int,
|
||||
amt btcutil.Amount, numMaxHophints int,
|
||||
includeNodes map[route.Vertex]struct{}) ([][]zpay32.HopHint, error) {
|
||||
|
||||
cfg := &invoicesrpc.SelectHopHintsCfg{
|
||||
IsPublicNode: func(pubKey [33]byte) (bool, error) {
|
||||
return isPublicNode(ctx, lnd, pubKey)
|
||||
},
|
||||
FetchChannelEdgesByID: func(chanID uint64) (
|
||||
*channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy,
|
||||
*channeldb.ChannelEdgePolicy, error) {
|
||||
|
||||
return fetchChannelEdgesByID(ctx, lnd, chanID)
|
||||
},
|
||||
}
|
||||
// Fetch all active and public channels.
|
||||
openChannels, err := lnd.Client.ListChannels(ctx, false, false)
|
||||
channels, 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.
|
||||
openChannels := []*invoicesrpc.HopHintInfo{}
|
||||
for _, channel := range channels {
|
||||
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 {
|
||||
outPoint, err := parseOutPoint(channel.ChannelPoint)
|
||||
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(
|
||||
remotePubkey, 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
|
||||
openChannels = append(
|
||||
openChannels, &invoicesrpc.HopHintInfo{
|
||||
IsPublic: !channel.Private,
|
||||
IsActive: channel.Active,
|
||||
FundingOutpoint: *outPoint,
|
||||
RemotePubkey: remotePubkey,
|
||||
RemoteBalance: lnwire.MilliSatoshi(
|
||||
channel.RemoteBalance * 1000,
|
||||
),
|
||||
ShortChannelID: channel.ChannelID,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
routeHints := invoicesrpc.SelectHopHints(
|
||||
lnwire.MilliSatoshi(amt*1000), cfg, openChannels, numMaxHophints,
|
||||
)
|
||||
|
||||
// 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
|
||||
return routeHints, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue