lnd.xprv/lnrpc/routerrpc/router_backend.go
Joost Jager 99a920f2a3
routerrpc: report route success probability
The intention of adding this field is to make probability based path
finding more transparent and help understand why a route is returned.
2019-09-05 08:35:04 +02:00

693 lines
19 KiB
Go

package routerrpc
import (
"encoding/hex"
"errors"
"fmt"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tlv"
"github.com/lightningnetwork/lnd/zpay32"
context "golang.org/x/net/context"
)
// RouterBackend contains the backend implementation of the router rpc sub
// server calls.
type RouterBackend struct {
// MaxPaymentMSat is the largest payment permitted by the backend.
MaxPaymentMSat lnwire.MilliSatoshi
// SelfNode is the vertex of the node sending the payment.
SelfNode route.Vertex
// FetchChannelCapacity is a closure that we'll use the fetch the total
// capacity of a channel to populate in responses.
FetchChannelCapacity func(chanID uint64) (btcutil.Amount, error)
// FetchChannelEndpoints returns the pubkeys of both endpoints of the
// given channel id.
FetchChannelEndpoints func(chanID uint64) (route.Vertex,
route.Vertex, error)
// FindRoutes is a closure that abstracts away how we locate/query for
// routes.
FindRoute func(source, target route.Vertex,
amt lnwire.MilliSatoshi, restrictions *routing.RestrictParams,
destTlvRecords []tlv.Record,
finalExpiry ...uint16) (*route.Route, error)
MissionControl MissionControl
// ActiveNetParams are the network parameters of the primary network
// that the route is operating on. This is necessary so we can ensure
// that we receive payment requests that send to destinations on our
// network.
ActiveNetParams *chaincfg.Params
// Tower is the ControlTower instance that is used to track pending
// payments.
Tower routing.ControlTower
}
// MissionControl defines the mission control dependencies of routerrpc.
type MissionControl interface {
// GetProbability is expected to return the success probability of a
// payment from fromNode to toNode.
GetProbability(fromNode, toNode route.Vertex,
amt lnwire.MilliSatoshi) float64
// ResetHistory resets the history of MissionControl returning it to a
// state as if no payment attempts have been made.
ResetHistory() error
// GetHistorySnapshot takes a snapshot from the current mission control
// state and actual probability estimates.
GetHistorySnapshot() *routing.MissionControlSnapshot
}
// 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) (route.Vertex, error) {
pubKeyBytes, err := hex.DecodeString(key)
if err != nil {
return route.Vertex{}, err
}
return route.NewVertexFromBytes(pubKeyBytes)
}
// 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 route.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[route.Vertex]struct{})
for _, ignorePubKey := range in.IgnoredNodes {
ignoreVertex, err := route.NewVertexFromBytes(ignorePubKey)
if err != nil {
return nil, err
}
ignoredNodes[ignoreVertex] = struct{}{}
}
ignoredPairs := make(map[routing.DirectedNodePair]struct{})
// Convert deprecated ignoredEdges to pairs.
for _, ignoredEdge := range in.IgnoredEdges {
pair, err := r.rpcEdgeToPair(ignoredEdge)
if err != nil {
log.Warnf("Ignore channel %v skipped: %v",
ignoredEdge.ChannelId, err)
continue
}
ignoredPairs[pair] = struct{}{}
}
// Add ignored pairs to set.
for _, ignorePair := range in.IgnoredPairs {
from, err := route.NewVertexFromBytes(ignorePair.From)
if err != nil {
return nil, err
}
to, err := route.NewVertexFromBytes(ignorePair.To)
if err != nil {
return nil, err
}
pair := routing.NewDirectedNodePair(from, to)
ignoredPairs[pair] = struct{}{}
}
restrictions := &routing.RestrictParams{
FeeLimit: feeLimit,
ProbabilitySource: func(fromNode, toNode route.Vertex,
amt lnwire.MilliSatoshi) float64 {
if _, ok := ignoredNodes[fromNode]; ok {
return 0
}
pair := routing.DirectedNodePair{
From: fromNode,
To: toNode,
}
if _, ok := ignoredPairs[pair]; ok {
return 0
}
if !in.UseMissionControl {
return 1
}
return r.MissionControl.GetProbability(
fromNode, toNode, amt,
)
},
DestPayloadTLV: len(in.DestTlv) != 0,
}
// If we have any TLV records destined for the final hop, then we'll
// attempt to decode them now into a form that the router can more
// easily manipulate.
destTlvRecords, err := tlv.MapToRecords(in.DestTlv)
if err != nil {
return nil, err
}
// 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 (
route *route.Route
findErr error
)
if in.FinalCltvDelta == 0 {
route, findErr = r.FindRoute(
sourcePubKey, targetPubKey, amtMSat, restrictions,
destTlvRecords,
)
} else {
route, findErr = r.FindRoute(
sourcePubKey, targetPubKey, amtMSat, restrictions,
destTlvRecords, uint16(in.FinalCltvDelta),
)
}
if findErr != nil {
return nil, findErr
}
// For each valid route, we'll convert the result into the format
// required by the RPC system.
rpcRoute, err := r.MarshallRoute(route)
if err != nil {
return nil, err
}
// Calculate route success probability. Do not rely on a probability
// that could have been returned from path finding, because mission
// control may have been disabled in the provided ProbabilitySource.
successProb := r.getSuccessProbability(route)
routeResp := &lnrpc.QueryRoutesResponse{
Routes: []*lnrpc.Route{rpcRoute},
SuccessProb: successProb,
}
return routeResp, nil
}
// getSuccessProbability returns the success probability for the given route
// based on the current state of mission control.
func (r *RouterBackend) getSuccessProbability(rt *route.Route) float64 {
fromNode := rt.SourcePubKey
amtToFwd := rt.TotalAmount
successProb := 1.0
for _, hop := range rt.Hops {
toNode := hop.PubKeyBytes
probability := r.MissionControl.GetProbability(
fromNode, toNode, amtToFwd,
)
successProb *= probability
amtToFwd = hop.AmtToForward
fromNode = toNode
}
return successProb
}
// rpcEdgeToPair looks up the provided channel and returns the channel endpoints
// as a directed pair.
func (r *RouterBackend) rpcEdgeToPair(e *lnrpc.EdgeLocator) (
routing.DirectedNodePair, error) {
a, b, err := r.FetchChannelEndpoints(e.ChannelId)
if err != nil {
return routing.DirectedNodePair{}, err
}
var pair routing.DirectedNodePair
if e.DirectionReverse {
pair.From, pair.To = b, a
} else {
pair.From, pair.To = a, b
}
return pair, 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 *route.Route) (*lnrpc.Route, error) {
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()
}
tlvMap, err := tlv.RecordsToMap(hop.TLVRecords)
if err != nil {
return nil, err
}
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[:],
),
TlvRecords: tlvMap,
TlvPayload: !hop.LegacyPayload,
}
incomingAmt = hop.AmtToForward
}
return resp, nil
}
// UnmarshallHopByChannelLookup unmarshalls an rpc hop for which the pub key is
// not known. This function will query the channel graph with channel id to
// retrieve both endpoints and determine the hop pubkey using the previous hop
// pubkey. If the channel is unknown, an error is returned.
func (r *RouterBackend) UnmarshallHopByChannelLookup(hop *lnrpc.Hop,
prevPubKeyBytes [33]byte) (*route.Hop, error) {
// Discard edge policies, because they may be nil.
node1, node2, err := r.FetchChannelEndpoints(hop.ChanId)
if err != nil {
return nil, err
}
var pubKeyBytes [33]byte
switch {
case prevPubKeyBytes == node1:
pubKeyBytes = node2
case prevPubKeyBytes == node2:
pubKeyBytes = node1
default:
return nil, fmt.Errorf("channel edge does not match expected node")
}
tlvRecords, err := tlv.MapToRecords(hop.TlvRecords)
if err != nil {
return nil, err
}
return &route.Hop{
OutgoingTimeLock: hop.Expiry,
AmtToForward: lnwire.MilliSatoshi(hop.AmtToForwardMsat),
PubKeyBytes: pubKeyBytes,
ChannelID: hop.ChanId,
TLVRecords: tlvRecords,
LegacyPayload: !hop.TlvPayload,
}, nil
}
// UnmarshallKnownPubkeyHop unmarshalls an rpc hop that contains the hop pubkey.
// The channel graph doesn't need to be queried because all information required
// for sending the payment is present.
func UnmarshallKnownPubkeyHop(hop *lnrpc.Hop) (*route.Hop, error) {
pubKey, err := hex.DecodeString(hop.PubKey)
if err != nil {
return nil, fmt.Errorf("cannot decode pubkey %s", hop.PubKey)
}
var pubKeyBytes [33]byte
copy(pubKeyBytes[:], pubKey)
var tlvRecords []tlv.Record
if hop.TlvRecords != nil {
tlvRecords, err = tlv.MapToRecords(hop.TlvRecords)
if err != nil {
return nil, err
}
}
return &route.Hop{
OutgoingTimeLock: hop.Expiry,
AmtToForward: lnwire.MilliSatoshi(hop.AmtToForwardMsat),
PubKeyBytes: pubKeyBytes,
ChannelID: hop.ChanId,
TLVRecords: tlvRecords,
LegacyPayload: !hop.TlvPayload,
}, nil
}
// UnmarshallHop unmarshalls an rpc hop that may or may not contain a node
// pubkey.
func (r *RouterBackend) UnmarshallHop(hop *lnrpc.Hop,
prevNodePubKey [33]byte) (*route.Hop, error) {
if hop.PubKey == "" {
// If no pub key is given of the hop, the local channel
// graph needs to be queried to complete the information
// necessary for routing.
return r.UnmarshallHopByChannelLookup(hop, prevNodePubKey)
}
return UnmarshallKnownPubkeyHop(hop)
}
// UnmarshallRoute unmarshalls an rpc route. For hops that don't specify a
// pubkey, the channel graph is queried.
func (r *RouterBackend) UnmarshallRoute(rpcroute *lnrpc.Route) (
*route.Route, error) {
prevNodePubKey := r.SelfNode
hops := make([]*route.Hop, len(rpcroute.Hops))
for i, hop := range rpcroute.Hops {
routeHop, err := r.UnmarshallHop(hop, prevNodePubKey)
if err != nil {
return nil, err
}
hops[i] = routeHop
prevNodePubKey = routeHop.PubKeyBytes
}
route, err := route.NewRouteFromHops(
lnwire.MilliSatoshi(rpcroute.TotalAmtMsat),
rpcroute.TotalTimeLock,
r.SelfNode,
hops,
)
if err != nil {
return nil, err
}
return route, nil
}
// extractIntentFromSendRequest attempts to parse the SendRequest details
// required to dispatch a client from the information presented by an RPC
// client.
func (r *RouterBackend) extractIntentFromSendRequest(
rpcPayReq *SendPaymentRequest) (*routing.LightningPayment, error) {
payIntent := &routing.LightningPayment{}
// Pass along an outgoing channel restriction if specified.
if rpcPayReq.OutgoingChanId != 0 {
payIntent.OutgoingChannelID = &rpcPayReq.OutgoingChanId
}
// Take cltv limit from request if set.
if rpcPayReq.CltvLimit != 0 {
cltvLimit := uint32(rpcPayReq.CltvLimit)
payIntent.CltvLimit = &cltvLimit
}
// Take fee limit from request.
payIntent.FeeLimit = lnwire.NewMSatFromSatoshis(
btcutil.Amount(rpcPayReq.FeeLimitSat),
)
// Set payment attempt timeout.
if rpcPayReq.TimeoutSeconds == 0 {
return nil, errors.New("timeout_seconds must be specified")
}
if len(rpcPayReq.DestTlv) != 0 {
var err error
payIntent.FinalDestRecords, err = tlv.MapToRecords(
rpcPayReq.DestTlv,
)
if err != nil {
return nil, err
}
}
payIntent.PayAttemptTimeout = time.Second *
time.Duration(rpcPayReq.TimeoutSeconds)
// Route hints.
routeHints, err := unmarshallRouteHints(
rpcPayReq.RouteHints,
)
if err != nil {
return nil, err
}
payIntent.RouteHints = routeHints
// If the payment request field isn't blank, then the details of the
// invoice are encoded entirely within the encoded payReq. So we'll
// attempt to decode it, populating the payment accordingly.
if rpcPayReq.PaymentRequest != "" {
switch {
case len(rpcPayReq.Dest) > 0:
return nil, errors.New("dest and payment_request " +
"cannot appear together")
case len(rpcPayReq.PaymentHash) > 0:
return nil, errors.New("dest and payment_hash " +
"cannot appear together")
case rpcPayReq.FinalCltvDelta != 0:
return nil, errors.New("dest and final_cltv_delta " +
"cannot appear together")
}
payReq, err := zpay32.Decode(
rpcPayReq.PaymentRequest, r.ActiveNetParams,
)
if err != nil {
return nil, err
}
// Next, we'll ensure that this payreq hasn't already expired.
err = ValidatePayReqExpiry(payReq)
if err != nil {
return nil, err
}
// If the amount was not included in the invoice, then we let
// the payee specify the amount of satoshis they wish to send.
// We override the amount to pay with the amount provided from
// the payment request.
if payReq.MilliSat == nil {
if rpcPayReq.Amt == 0 {
return nil, errors.New("amount must be " +
"specified when paying a zero amount " +
"invoice")
}
payIntent.Amount = lnwire.NewMSatFromSatoshis(
btcutil.Amount(rpcPayReq.Amt),
)
} else {
if rpcPayReq.Amt != 0 {
return nil, errors.New("amount must not be " +
"specified when paying a non-zero " +
" amount invoice")
}
payIntent.Amount = *payReq.MilliSat
}
copy(payIntent.PaymentHash[:], payReq.PaymentHash[:])
destKey := payReq.Destination.SerializeCompressed()
copy(payIntent.Target[:], destKey)
payIntent.FinalCLTVDelta = uint16(payReq.MinFinalCLTVExpiry())
payIntent.RouteHints = append(
payIntent.RouteHints, payReq.RouteHints...,
)
} else {
// Otherwise, If the payment request field was not specified
// (and a custom route wasn't specified), construct the payment
// from the other fields.
// Payment destination.
target, err := route.NewVertexFromBytes(rpcPayReq.Dest)
if err != nil {
return nil, err
}
payIntent.Target = target
// Final payment CLTV delta.
if rpcPayReq.FinalCltvDelta != 0 {
payIntent.FinalCLTVDelta =
uint16(rpcPayReq.FinalCltvDelta)
} else {
payIntent.FinalCLTVDelta = zpay32.DefaultFinalCLTVDelta
}
// Amount.
if rpcPayReq.Amt == 0 {
return nil, errors.New("amount must be specified")
}
payIntent.Amount = lnwire.NewMSatFromSatoshis(
btcutil.Amount(rpcPayReq.Amt),
)
// Payment hash.
copy(payIntent.PaymentHash[:], rpcPayReq.PaymentHash)
}
// 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.
if payIntent.Amount > r.MaxPaymentMSat {
// In this case, we'll send an error to the caller, but
// continue our loop for the next payment.
return payIntent, fmt.Errorf("payment of %v is too large, "+
"max payment allowed is %v", payIntent.Amount,
r.MaxPaymentMSat)
}
return payIntent, nil
}
// unmarshallRouteHints unmarshalls a list of route hints.
func unmarshallRouteHints(rpcRouteHints []*lnrpc.RouteHint) (
[][]zpay32.HopHint, error) {
routeHints := make([][]zpay32.HopHint, 0, len(rpcRouteHints))
for _, rpcRouteHint := range rpcRouteHints {
routeHint := make(
[]zpay32.HopHint, 0, len(rpcRouteHint.HopHints),
)
for _, rpcHint := range rpcRouteHint.HopHints {
hint, err := unmarshallHopHint(rpcHint)
if err != nil {
return nil, err
}
routeHint = append(routeHint, hint)
}
routeHints = append(routeHints, routeHint)
}
return routeHints, nil
}
// unmarshallHopHint unmarshalls a single hop hint.
func unmarshallHopHint(rpcHint *lnrpc.HopHint) (zpay32.HopHint, error) {
pubBytes, err := hex.DecodeString(rpcHint.NodeId)
if err != nil {
return zpay32.HopHint{}, err
}
pubkey, err := btcec.ParsePubKey(pubBytes, btcec.S256())
if err != nil {
return zpay32.HopHint{}, err
}
return zpay32.HopHint{
NodeID: pubkey,
ChannelID: rpcHint.ChanId,
FeeBaseMSat: rpcHint.FeeBaseMsat,
FeeProportionalMillionths: rpcHint.FeeProportionalMillionths,
CLTVExpiryDelta: uint16(rpcHint.CltvExpiryDelta),
}, nil
}
// ValidatePayReqExpiry checks if the passed payment request has expired. In
// the case it has expired, an error will be returned.
func ValidatePayReqExpiry(payReq *zpay32.Invoice) error {
expiry := payReq.Expiry()
validUntil := payReq.Timestamp.Add(expiry)
if time.Now().After(validUntil) {
return fmt.Errorf("invoice expired. Valid until %v", validUntil)
}
return nil
}