From 31f7b5947aabdc43eadf5c7d37c075ed79102b48 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Thu, 27 Jan 2022 18:52:20 +0100 Subject: [PATCH] utils: use SelectHopHints from LND --- utils.go | 282 +++++++++++++++++++++-------------------------- utils_test.go | 297 -------------------------------------------------- 2 files changed, 125 insertions(+), 454 deletions(-) delete mode 100644 utils_test.go diff --git a/utils.go b/utils.go index 02f40dc..2b7c6fc 100644 --- a/utils.go +++ b/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) -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 } diff --git a/utils_test.go b/utils_test.go deleted file mode 100644 index d3bfbc6..0000000 --- a/utils_test.go +++ /dev/null @@ -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) - }) - } - -}