utils: use SelectHopHints from LND

pull/455/head
Andras Banki-Horvath 2 years ago
parent db2fba6f0d
commit 31f7b5947a
No known key found for this signature in database
GPG Key ID: 80E5375C094198D8

@ -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)
func SelectHopHints(ctx context.Context, lnd *lndclient.LndServices,
amtMSat btcutil.Amount, numMaxHophints int,
includeNodes map[route.Vertex]struct{}) ([][]zpay32.HopHint, error) {
// 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,
)
// Fetch all active and public channels.
openChannels, err := lnd.Client.ListChannels(ctx, false, false)
if err != nil {
return nil, err
return false, 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
return (nodeInfo.ChannelCount > 0), nil
}
// chanInfoCache is a simple cache for any information we retrieve
// through GetChanInfo
chanInfoCache := make(map[uint64]*lndclient.ChannelEdge)
// 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) {
// skipCache is a simple cache which holds the indice of any
// channel we've added to final hopHints
skipCache := make(map[int]struct{})
chanInfo, err := lnd.Client.GetChanInfo(ctx, chanID)
if err != nil {
return nil, nil, nil, err
}
hopHints := make([][]zpay32.HopHint, 0, numMaxHophints)
edgeInfo := &channeldb.ChannelEdgeInfo{
ChannelID: chanID,
NodeKey1Bytes: chanInfo.Node1,
NodeKey2Bytes: chanInfo.Node2,
}
for i, channel := range openChannels {
// In this first pass, we'll ignore all channels in
// isolation that can't satisfy this payment.
policy1 := &channeldb.ChannelEdgePolicy{
FeeBaseMSat: lnwire.MilliSatoshi(
chanInfo.Node1Policy.FeeBaseMsat,
),
FeeProportionalMillionths: lnwire.MilliSatoshi(
chanInfo.Node1Policy.FeeRateMilliMsat,
),
TimeLockDelta: uint16(chanInfo.Node1Policy.TimeLockDelta),
}
// Retrieve extra info for each channel not available in
// listChannels
chanInfo, err := lnd.Client.GetChanInfo(ctx, channel.ChannelID)
if err != nil {
return nil, err
}
policy2 := &channeldb.ChannelEdgePolicy{
FeeBaseMSat: lnwire.MilliSatoshi(
chanInfo.Node2Policy.FeeBaseMsat,
),
FeeProportionalMillionths: lnwire.MilliSatoshi(
chanInfo.Node2Policy.FeeRateMilliMsat,
),
TimeLockDelta: uint16(chanInfo.Node2Policy.TimeLockDelta),
}
// 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.
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,
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.
channels, err := lnd.Client.ListChannels(ctx, false, false)
if err != nil {
return nil, err
}
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
}
// 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
openChannels = append(
openChannels, &invoicesrpc.HopHintInfo{
IsPublic: !channel.Private,
IsActive: channel.Active,
FundingOutpoint: *outPoint,
RemotePubkey: remotePubkey,
RemoteBalance: lnwire.MilliSatoshi(
channel.RemoteBalance * 1000,
),
ShortChannelID: channel.ChannelID,
},
)
}
// 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
}
routeHints := invoicesrpc.SelectHopHints(
lnwire.MilliSatoshi(amt*1000), cfg, openChannels, numMaxHophints,
)
return hopHints, nil
return routeHints, nil
}

@ -1,297 +0,0 @@
package loop
import (
"context"
"encoding/hex"
"math/big"
"testing"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient"
mock_lnd "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/stretchr/testify/require"
)
var (
chanID1 = lnwire.NewShortChanIDFromInt(1)
chanID2 = lnwire.NewShortChanIDFromInt(2)
chanID3 = lnwire.NewShortChanIDFromInt(3)
chanID4 = lnwire.NewShortChanIDFromInt(4)
// To generate a nodeID we'll have to perform a few steps.
//
// Step 1: We generate the corresponding Y value to an
// arbitrary X value on the secp25k1 curve. This
// is done outside this function, and the output
// is converted to string with big.Int.Text(16)
// and converted to []bytes. btcec.decompressPoint
// is used here to generate this Y value
//
// Step 2: Construct a btcec.PublicKey object with the
// aforementioned values
//
// Step 3: Convert the pubkey to a Vertex by passing a
// compressed pubkey. This compression looses the
// Y value as it can be inferred.
//
// The Vertex object mainly contains the X value information,
// and has the underlying []bytes type. We generate the Y
// value information ourselves as that is returned in the
// hophints, and we must ensure it's accuracy
// Generate origin NodeID
originYBytes, _ = hex.DecodeString(
"bde70df51939b94c9c24979fa7dd04ebd9b" +
"3572da7802290438af2a681895441",
)
pubKeyOrigin = &btcec.PublicKey{
X: big.NewInt(0),
Y: new(big.Int).SetBytes(originYBytes),
Curve: btcec.S256(),
}
origin, _ = route.NewVertexFromBytes(pubKeyOrigin.SerializeCompressed())
// Generate peer1 NodeID
pubKey1YBytes, _ = hex.DecodeString(
"598ec453728e0ffe0ae2f5e174243cf58f2" +
"a3f2c83d2457b43036db568b11093",
)
pubKeyPeer1 = &btcec.PublicKey{
X: big.NewInt(4),
Y: new(big.Int).SetBytes(pubKey1YBytes),
Curve: btcec.S256(),
}
peer1, _ = route.NewVertexFromBytes(pubKeyPeer1.SerializeCompressed())
// Generate peer2 NodeID
pubKey2YBytes, _ = hex.DecodeString(
"bde70df51939b94c9c24979fa7dd04ebd" +
"9b3572da7802290438af2a681895441",
)
pubKeyPeer2 = &btcec.PublicKey{
X: big.NewInt(1),
Y: new(big.Int).SetBytes(pubKey2YBytes),
Curve: btcec.S256(),
}
peer2, _ = route.NewVertexFromBytes(pubKeyPeer2.SerializeCompressed())
// Construct channel1 which will be returned my listChannels and
// channelEdge1 which will be returned by getChanInfo
chan1Capacity = btcutil.Amount(10000)
channel1 = lndclient.ChannelInfo{
Active: true,
ChannelID: chanID1.ToUint64(),
PubKeyBytes: peer1,
LocalBalance: 10000,
RemoteBalance: 0,
Capacity: chan1Capacity,
}
channelEdge1 = lndclient.ChannelEdge{
ChannelID: chanID1.ToUint64(),
ChannelPoint: "b121f1d368b8f60648970bc36b37e7b9700d" +
"ed098c60b027e42e9c648e297502:0",
Capacity: chan1Capacity,
Node1: origin,
Node2: peer1,
Node1Policy: &lndclient.RoutingPolicy{
FeeBaseMsat: 0,
FeeRateMilliMsat: 0,
TimeLockDelta: 144,
},
Node2Policy: &lndclient.RoutingPolicy{
FeeBaseMsat: 0,
FeeRateMilliMsat: 0,
TimeLockDelta: 144,
},
}
// Construct channel2 which will be returned my listChannels and
// channelEdge2 which will be returned by getChanInfo
chan2Capacity = btcutil.Amount(10000)
channel2 = lndclient.ChannelInfo{
Active: true,
ChannelID: chanID1.ToUint64(),
PubKeyBytes: peer2,
LocalBalance: 0,
RemoteBalance: 10000,
Capacity: chan1Capacity,
}
channelEdge2 = lndclient.ChannelEdge{
ChannelID: chanID2.ToUint64(),
ChannelPoint: "b121f1d368b8f60648970bc36b37e7b9700d" +
"ed098c60b027e42e9c648e297502:0",
Capacity: chan2Capacity,
Node1: origin,
Node2: peer2,
Node1Policy: &lndclient.RoutingPolicy{
FeeBaseMsat: 0,
FeeRateMilliMsat: 0,
TimeLockDelta: 144,
},
Node2Policy: &lndclient.RoutingPolicy{
FeeBaseMsat: 0,
FeeRateMilliMsat: 0,
TimeLockDelta: 144,
},
}
// Construct channel3 which will be returned my listChannels and
// channelEdge3 which will be returned by getChanInfo
chan3Capacity = btcutil.Amount(10000)
channel3 = lndclient.ChannelInfo{
Active: true,
ChannelID: chanID3.ToUint64(),
PubKeyBytes: peer2,
LocalBalance: 10000,
RemoteBalance: 0,
Capacity: chan1Capacity,
}
channelEdge3 = lndclient.ChannelEdge{
ChannelID: chanID3.ToUint64(),
ChannelPoint: "b121f1d368b8f60648970bc36b37e7b9700d" +
"ed098c60b027e42e9c648e297502:0",
Capacity: chan3Capacity,
Node1: origin,
Node2: peer2,
Node1Policy: &lndclient.RoutingPolicy{
FeeBaseMsat: 0,
FeeRateMilliMsat: 0,
TimeLockDelta: 144,
},
Node2Policy: &lndclient.RoutingPolicy{
FeeBaseMsat: 0,
FeeRateMilliMsat: 0,
TimeLockDelta: 144,
},
}
// Construct channel4 which will be returned my listChannels and
// channelEdge4 which will be returned by getChanInfo
chan4Capacity = btcutil.Amount(10000)
channel4 = lndclient.ChannelInfo{
Active: true,
ChannelID: chanID4.ToUint64(),
PubKeyBytes: peer2,
LocalBalance: 10000,
RemoteBalance: 0,
Capacity: chan4Capacity,
}
channelEdge4 = lndclient.ChannelEdge{
ChannelID: chanID4.ToUint64(),
ChannelPoint: "6fe4408bba52c0a0ee15365e107105de" +
"fabfc70c497556af69351c4cfbc167b:0",
Capacity: chan1Capacity,
Node1: origin,
Node2: peer2,
Node1Policy: &lndclient.RoutingPolicy{
FeeBaseMsat: 0,
FeeRateMilliMsat: 0,
TimeLockDelta: 144,
},
Node2Policy: &lndclient.RoutingPolicy{
FeeBaseMsat: 0,
FeeRateMilliMsat: 0,
TimeLockDelta: 144,
},
}
)
func TestSelectHopHints(t *testing.T) {
tests := []struct {
name string
channels []lndclient.ChannelInfo
channelEdges map[uint64]*lndclient.ChannelEdge
expectedHopHints [][]zpay32.HopHint
amtMSat btcutil.Amount
numMaxHophints int
includeNodes map[route.Vertex]struct{}
expectedError error
}{
// 3 inputs set assumes the host node has 3 channels to chose
// from. Only channel 2 with peer 2 is ideal, however we should
// still include the other 2 after in the order they were
// provided just in case
{
name: "3 inputs set",
channels: []lndclient.ChannelInfo{
channel2,
channel3,
channel4,
},
channelEdges: map[uint64]*lndclient.ChannelEdge{
channel2.ChannelID: &channelEdge2,
channel3.ChannelID: &channelEdge3,
channel4.ChannelID: &channelEdge4,
},
expectedHopHints: [][]zpay32.HopHint{
{{
NodeID: pubKeyPeer2,
ChannelID: channel2.ChannelID,
FeeBaseMSat: 0,
FeeProportionalMillionths: 0,
CLTVExpiryDelta: 144,
}},
{{
NodeID: pubKeyPeer2,
ChannelID: channel3.ChannelID,
FeeBaseMSat: 0,
FeeProportionalMillionths: 0,
CLTVExpiryDelta: 144,
}},
{{
NodeID: pubKeyPeer2,
ChannelID: channel4.ChannelID,
FeeBaseMSat: 0,
FeeProportionalMillionths: 0,
CLTVExpiryDelta: 144,
}},
},
amtMSat: chan1Capacity,
numMaxHophints: 20,
includeNodes: make(map[route.Vertex]struct{}),
expectedError: nil,
},
{
name: "invalid set",
channels: []lndclient.ChannelInfo{
channel1,
},
channelEdges: map[uint64]*lndclient.ChannelEdge{
channel1.ChannelID: &channelEdge1,
},
expectedHopHints: [][]zpay32.HopHint{
{{
NodeID: pubKeyPeer1,
ChannelID: channel1.ChannelID,
FeeBaseMSat: 0,
FeeProportionalMillionths: 0,
CLTVExpiryDelta: 144,
}},
}, amtMSat: chan1Capacity,
numMaxHophints: 20,
includeNodes: make(map[route.Vertex]struct{}),
expectedError: nil,
},
}
for _, test := range tests {
test := test
ctx := context.Background()
lnd := mock_lnd.NewMockLnd()
lnd.Channels = test.channels
lnd.ChannelEdges = test.channelEdges
t.Run(test.name, func(t *testing.T) {
hopHints, err := SelectHopHints(
ctx, &lnd.LndServices, test.amtMSat,
test.numMaxHophints, test.includeNodes,
)
require.Equal(t, test.expectedError, err)
require.Equal(t, test.expectedHopHints, hopHints)
})
}
}
Loading…
Cancel
Save