Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lnrpc: deprecate QueryRoutes with more than one route #2497

Merged
merged 10 commits into from
Mar 13, 2019
229 changes: 229 additions & 0 deletions lnrpc/routerrpc/router_backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package routerrpc

import (
"encoding/hex"
"errors"
"fmt"

"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing"
context "golang.org/x/net/context"
)

// RouterBackend contains the backend implementation of the router rpc sub
// server calls.
type RouterBackend struct {
MaxPaymentMSat lnwire.MilliSatoshi
SelfNode routing.Vertex

FetchChannelCapacity func(chanID uint64) (btcutil.Amount, error)

FindRoutes func(source, target routing.Vertex,
amt lnwire.MilliSatoshi, restrictions *routing.RestrictParams,
numPaths uint32, finalExpiry ...uint16) (
[]*routing.Route, error)
}

// QueryRoutes attempts to query the daemons' Channel Router for a possible
// route to a target destination capable of carrying a specific amount of
// satoshis within the route's flow. The retuned route contains the full
// details required to craft and send an HTLC, also including the necessary
// information that should be present within the Sphinx packet encapsulated
// within the HTLC.
//
// TODO(roasbeef): should return a slice of routes in reality
// * create separate PR to send based on well formatted route
func (r *RouterBackend) QueryRoutes(ctx context.Context,
in *lnrpc.QueryRoutesRequest) (*lnrpc.QueryRoutesResponse, error) {

parsePubKey := func(key string) (routing.Vertex, error) {
pubKeyBytes, err := hex.DecodeString(key)
if err != nil {
return routing.Vertex{}, err
}

if len(pubKeyBytes) != 33 {
return routing.Vertex{},
errors.New("invalid key length")
}

var v routing.Vertex
copy(v[:], pubKeyBytes)

return v, nil
}

// Parse the hex-encoded source and target public keys into full public
// key objects we can properly manipulate.
targetPubKey, err := parsePubKey(in.PubKey)
if err != nil {
return nil, err
}

var sourcePubKey routing.Vertex
if in.SourcePubKey != "" {
var err error
sourcePubKey, err = parsePubKey(in.SourcePubKey)
if err != nil {
return nil, err
}
} else {
// If no source is specified, use self.
sourcePubKey = r.SelfNode
}

// Currently, within the bootstrap phase of the network, we limit the
// largest payment size allotted to (2^32) - 1 mSAT or 4.29 million
// satoshis.
amt := btcutil.Amount(in.Amt)
amtMSat := lnwire.NewMSatFromSatoshis(amt)
if amtMSat > r.MaxPaymentMSat {
return nil, fmt.Errorf("payment of %v is too large, max payment "+
"allowed is %v", amt, r.MaxPaymentMSat.ToSatoshis())
}

// Unmarshall restrictions from request.
feeLimit := calculateFeeLimit(in.FeeLimit, amtMSat)

ignoredNodes := make(map[routing.Vertex]struct{})
for _, ignorePubKey := range in.IgnoredNodes {
if len(ignorePubKey) != 33 {
return nil, fmt.Errorf("invalid ignore node pubkey")
}
var ignoreVertex routing.Vertex
copy(ignoreVertex[:], ignorePubKey)
ignoredNodes[ignoreVertex] = struct{}{}
}

ignoredEdges := make(map[routing.EdgeLocator]struct{})
for _, ignoredEdge := range in.IgnoredEdges {
locator := routing.EdgeLocator{
ChannelID: ignoredEdge.ChannelId,
}
if ignoredEdge.DirectionReverse {
locator.Direction = 1
}
ignoredEdges[locator] = struct{}{}
}

restrictions := &routing.RestrictParams{
FeeLimit: feeLimit,
IgnoredNodes: ignoredNodes,
IgnoredEdges: ignoredEdges,
}

// numRoutes will default to 10 if not specified explicitly.
numRoutesIn := uint32(in.NumRoutes)
if numRoutesIn == 0 {
numRoutesIn = 10
}

// Query the channel router for a possible path to the destination that
// can carry `in.Amt` satoshis _including_ the total fee required on
// the route.
var (
routes []*routing.Route
findErr error
)

if in.FinalCltvDelta == 0 {
routes, findErr = r.FindRoutes(
sourcePubKey, targetPubKey, amtMSat, restrictions, numRoutesIn,
)
} else {
routes, findErr = r.FindRoutes(
sourcePubKey, targetPubKey, amtMSat, restrictions, numRoutesIn,
uint16(in.FinalCltvDelta),
)
}
if findErr != nil {
return nil, findErr
}

// As the number of returned routes can be less than the number of
// requested routes, we'll clamp down the length of the response to the
// minimum of the two.
numRoutes := uint32(len(routes))
if numRoutesIn < numRoutes {
numRoutes = numRoutesIn
}

// For each valid route, we'll convert the result into the format
// required by the RPC system.
routeResp := &lnrpc.QueryRoutesResponse{
Routes: make([]*lnrpc.Route, 0, in.NumRoutes),
}
for i := uint32(0); i < numRoutes; i++ {
routeResp.Routes = append(
routeResp.Routes,
r.MarshallRoute(routes[i]),
)
}

return routeResp, nil
}

// calculateFeeLimit returns the fee limit in millisatoshis. If a percentage
// based fee limit has been requested, we'll factor in the ratio provided with
// the amount of the payment.
func calculateFeeLimit(feeLimit *lnrpc.FeeLimit,
amount lnwire.MilliSatoshi) lnwire.MilliSatoshi {

switch feeLimit.GetLimit().(type) {
case *lnrpc.FeeLimit_Fixed:
return lnwire.NewMSatFromSatoshis(
btcutil.Amount(feeLimit.GetFixed()),
)
case *lnrpc.FeeLimit_Percent:
return amount * lnwire.MilliSatoshi(feeLimit.GetPercent()) / 100
default:
// If a fee limit was not specified, we'll use the payment's
// amount as an upper bound in order to avoid payment attempts
// from incurring fees higher than the payment amount itself.
return amount
}
}

// MarshallRoute marshalls an internal route to an rpc route struct.
func (r *RouterBackend) MarshallRoute(route *routing.Route) *lnrpc.Route {
resp := &lnrpc.Route{
TotalTimeLock: route.TotalTimeLock,
TotalFees: int64(route.TotalFees.ToSatoshis()),
TotalFeesMsat: int64(route.TotalFees),
TotalAmt: int64(route.TotalAmount.ToSatoshis()),
TotalAmtMsat: int64(route.TotalAmount),
Hops: make([]*lnrpc.Hop, len(route.Hops)),
}
incomingAmt := route.TotalAmount
for i, hop := range route.Hops {
fee := route.HopFee(i)

// Channel capacity is not a defining property of a route. For
// backwards RPC compatibility, we retrieve it here from the
// graph.
chanCapacity, err := r.FetchChannelCapacity(hop.ChannelID)
if err != nil {
// If capacity cannot be retrieved, this may be a
// not-yet-received or private channel. Then report
// amount that is sent through the channel as capacity.
chanCapacity = incomingAmt.ToSatoshis()
}

resp.Hops[i] = &lnrpc.Hop{
ChanId: hop.ChannelID,
ChanCapacity: int64(chanCapacity),
AmtToForward: int64(hop.AmtToForward.ToSatoshis()),
AmtToForwardMsat: int64(hop.AmtToForward),
Fee: int64(fee.ToSatoshis()),
FeeMsat: int64(fee),
Expiry: uint32(hop.OutgoingTimeLock),
PubKey: hex.EncodeToString(
hop.PubKeyBytes[:]),
}
incomingAmt = hop.AmtToForward
}

return resp
}
126 changes: 126 additions & 0 deletions lnrpc/routerrpc/router_backend_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package routerrpc

import (
"bytes"
"context"
"encoding/hex"
"testing"

"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing"

"github.com/lightningnetwork/lnd/lnrpc"
)

const (
destKey = "0286098b97bc843372b4426d4b276cea9aa2f48f0428d6f5b66ae101befc14f8b4"
ignoreNodeKey = "02f274f48f3c0d590449a6776e3ce8825076ac376e470e992246eebc565ef8bb2a"
)

var (
sourceKey = routing.Vertex{1, 2, 3}
)

// TestQueryRoutes asserts that query routes rpc parameters are properly parsed
// and passed onto path finding.
func TestQueryRoutes(t *testing.T) {
ignoreNodeBytes, err := hex.DecodeString(ignoreNodeKey)
if err != nil {
t.Fatal(err)
}

var ignoreNodeVertex routing.Vertex
copy(ignoreNodeVertex[:], ignoreNodeBytes)

destNodeBytes, err := hex.DecodeString(destKey)
if err != nil {
t.Fatal(err)
}

request := &lnrpc.QueryRoutesRequest{
PubKey: destKey,
Amt: 100000,
NumRoutes: 1,
FinalCltvDelta: 100,
FeeLimit: &lnrpc.FeeLimit{
Limit: &lnrpc.FeeLimit_Fixed{
Fixed: 250,
},
},
IgnoredNodes: [][]byte{ignoreNodeBytes},
IgnoredEdges: []*lnrpc.EdgeLocator{&lnrpc.EdgeLocator{
ChannelId: 555,
DirectionReverse: true,
}},
}

route := &routing.Route{}

findRoutes := func(source, target routing.Vertex,
amt lnwire.MilliSatoshi, restrictions *routing.RestrictParams,
numPaths uint32, finalExpiry ...uint16) (
[]*routing.Route, error) {

if int64(amt) != request.Amt*1000 {
t.Fatal("unexpected amount")
}

if numPaths != 1 {
t.Fatal("unexpected number of routes")
}

if source != sourceKey {
t.Fatal("unexpected source key")
}

if !bytes.Equal(target[:], destNodeBytes) {
t.Fatal("unexpected target key")
}

if restrictions.FeeLimit != 250*1000 {
t.Fatal("unexpected fee limit")
}

if len(restrictions.IgnoredEdges) != 1 {
t.Fatal("unexpected ignored edges map size")
}

if _, ok := restrictions.IgnoredEdges[routing.EdgeLocator{
ChannelID: 555, Direction: 1,
}]; !ok {
t.Fatal("unexpected ignored edge")
}

if len(restrictions.IgnoredNodes) != 1 {
t.Fatal("unexpected ignored nodes map size")
}

if _, ok := restrictions.IgnoredNodes[ignoreNodeVertex]; !ok {
t.Fatal("unexpected ignored node")
}

return []*routing.Route{
route,
}, nil
}

backend := &RouterBackend{
MaxPaymentMSat: lnwire.NewMSatFromSatoshis(1000000),
FindRoutes: findRoutes,
SelfNode: routing.Vertex{1, 2, 3},
FetchChannelCapacity: func(chanID uint64) (
btcutil.Amount, error) {

return 1, nil
},
}

resp, err := backend.QueryRoutes(context.Background(), request)
if err != nil {
t.Fatal(err)
}
if len(resp.Routes) != 1 {
t.Fatal("expected a single route response")
}
}
Loading