You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
742 lines
26 KiB
742 lines
26 KiB
package routing |
|
|
|
import ( |
|
"container/heap" |
|
"fmt" |
|
"math" |
|
|
|
"github.com/coreos/bbolt" |
|
|
|
"github.com/lightningnetwork/lnd/channeldb" |
|
"github.com/lightningnetwork/lnd/lnwire" |
|
"github.com/lightningnetwork/lnd/routing/route" |
|
"github.com/lightningnetwork/lnd/tlv" |
|
) |
|
|
|
const ( |
|
// HopLimit is the maximum number hops that is permissible as a route. |
|
// Any potential paths found that lie above this limit will be rejected |
|
// with an error. This value is computed using the current fixed-size |
|
// packet length of the Sphinx construction. |
|
HopLimit = 20 |
|
|
|
// infinity is used as a starting distance in our shortest path search. |
|
infinity = math.MaxInt64 |
|
|
|
// RiskFactorBillionths controls the influence of time lock delta |
|
// of a channel on route selection. It is expressed as billionths |
|
// of msat per msat sent through the channel per time lock delta |
|
// block. See edgeWeight function below for more details. |
|
// The chosen value is based on the previous incorrect weight function |
|
// 1 + timelock + fee * fee. In this function, the fee penalty |
|
// diminishes the time lock penalty for all but the smallest amounts. |
|
// To not change the behaviour of path finding too drastically, a |
|
// relatively small value is chosen which is still big enough to give |
|
// some effect with smaller time lock values. The value may need |
|
// tweaking and/or be made configurable in the future. |
|
RiskFactorBillionths = 15 |
|
) |
|
|
|
// pathFinder defines the interface of a path finding algorithm. |
|
type pathFinder = func(g *graphParams, r *RestrictParams, |
|
cfg *PathFindingConfig, source, target route.Vertex, |
|
amt lnwire.MilliSatoshi) ([]*channeldb.ChannelEdgePolicy, error) |
|
|
|
var ( |
|
// DefaultPaymentAttemptPenalty is the virtual cost in path finding weight |
|
// units of executing a payment attempt that fails. It is used to trade |
|
// off potentially better routes against their probability of |
|
// succeeding. |
|
DefaultPaymentAttemptPenalty = lnwire.NewMSatFromSatoshis(100) |
|
|
|
// DefaultMinRouteProbability is the default minimum probability for routes |
|
// returned from findPath. |
|
DefaultMinRouteProbability = float64(0.01) |
|
|
|
// DefaultAprioriHopProbability is the default a priori probability for |
|
// a hop. |
|
DefaultAprioriHopProbability = float64(0.6) |
|
) |
|
|
|
// edgePolicyWithSource is a helper struct to keep track of the source node |
|
// of a channel edge. ChannelEdgePolicy only contains to destination node |
|
// of the edge. |
|
type edgePolicyWithSource struct { |
|
sourceNode route.Vertex |
|
edge *channeldb.ChannelEdgePolicy |
|
} |
|
|
|
// computeFee computes the fee to forward an HTLC of `amt` milli-satoshis over |
|
// the passed active payment channel. This value is currently computed as |
|
// specified in BOLT07, but will likely change in the near future. |
|
func computeFee(amt lnwire.MilliSatoshi, |
|
edge *channeldb.ChannelEdgePolicy) lnwire.MilliSatoshi { |
|
|
|
return edge.FeeBaseMSat + (amt*edge.FeeProportionalMillionths)/1000000 |
|
} |
|
|
|
// newRoute returns a fully valid route between the source and target that's |
|
// capable of supporting a payment of `amtToSend` after fees are fully |
|
// computed. If the route is too long, or the selected path cannot support the |
|
// fully payment including fees, then a non-nil error is returned. |
|
// |
|
// NOTE: The passed slice of ChannelHops MUST be sorted in forward order: from |
|
// the source to the target node of the path finding attempt. |
|
func newRoute(amtToSend lnwire.MilliSatoshi, sourceVertex route.Vertex, |
|
pathEdges []*channeldb.ChannelEdgePolicy, currentHeight uint32, |
|
finalCLTVDelta uint16, |
|
finalDestRecords []tlv.Record) (*route.Route, error) { |
|
|
|
var ( |
|
hops []*route.Hop |
|
|
|
// totalTimeLock will accumulate the cumulative time lock |
|
// across the entire route. This value represents how long the |
|
// sender will need to wait in the *worst* case. |
|
totalTimeLock = currentHeight |
|
|
|
// nextIncomingAmount is the amount that will need to flow into |
|
// the *next* hop. Since we're going to be walking the route |
|
// backwards below, this next hop gets closer and closer to the |
|
// sender of the payment. |
|
nextIncomingAmount lnwire.MilliSatoshi |
|
) |
|
|
|
pathLength := len(pathEdges) |
|
for i := pathLength - 1; i >= 0; i-- { |
|
// Now we'll start to calculate the items within the per-hop |
|
// payload for the hop this edge is leading to. |
|
edge := pathEdges[i] |
|
|
|
// If this is the last hop, then the hop payload will contain |
|
// the exact amount. In BOLT #4: Onion Routing |
|
// Protocol / "Payload for the Last Node", this is detailed. |
|
amtToForward := amtToSend |
|
|
|
// Fee is not part of the hop payload, but only used for |
|
// reporting through RPC. Set to zero for the final hop. |
|
fee := lnwire.MilliSatoshi(0) |
|
|
|
// If the current hop isn't the last hop, then add enough funds |
|
// to pay for transit over the next link. |
|
if i != len(pathEdges)-1 { |
|
// The amount that the current hop needs to forward is |
|
// equal to the incoming amount of the next hop. |
|
amtToForward = nextIncomingAmount |
|
|
|
// The fee that needs to be paid to the current hop is |
|
// based on the amount that this hop needs to forward |
|
// and its policy for the outgoing channel. This policy |
|
// is stored as part of the incoming channel of |
|
// the next hop. |
|
fee = computeFee(amtToForward, pathEdges[i+1]) |
|
} |
|
|
|
// If this is the last hop, then for verification purposes, the |
|
// value of the outgoing time-lock should be _exactly_ the |
|
// absolute time out they'd expect in the HTLC. |
|
var outgoingTimeLock uint32 |
|
if i == len(pathEdges)-1 { |
|
// As this is the last hop, we'll use the specified |
|
// final CLTV delta value instead of the value from the |
|
// last link in the route. |
|
totalTimeLock += uint32(finalCLTVDelta) |
|
|
|
outgoingTimeLock = currentHeight + uint32(finalCLTVDelta) |
|
} else { |
|
// Next, increment the total timelock of the entire |
|
// route such that each hops time lock increases as we |
|
// walk backwards in the route, using the delta of the |
|
// previous hop. |
|
delta := uint32(pathEdges[i+1].TimeLockDelta) |
|
totalTimeLock += delta |
|
|
|
// Otherwise, the value of the outgoing time-lock will |
|
// be the value of the time-lock for the _outgoing_ |
|
// HTLC, so we factor in their specified grace period |
|
// (time lock delta). |
|
outgoingTimeLock = totalTimeLock - delta |
|
} |
|
|
|
// Since we're traversing the path backwards atm, we prepend |
|
// each new hop such that, the final slice of hops will be in |
|
// the forwards order. |
|
currentHop := &route.Hop{ |
|
PubKeyBytes: edge.Node.PubKeyBytes, |
|
ChannelID: edge.ChannelID, |
|
AmtToForward: amtToForward, |
|
OutgoingTimeLock: outgoingTimeLock, |
|
LegacyPayload: true, |
|
} |
|
|
|
// We start out above by assuming that this node needs the |
|
// legacy payload, as if we don't have the full |
|
// NodeAnnouncement information for this node, then we can't |
|
// assume it knows the latest features. If we do have a feature |
|
// vector for this node, then we'll update the info now. |
|
if edge.Node.Features != nil { |
|
features := edge.Node.Features |
|
currentHop.LegacyPayload = !features.HasFeature( |
|
lnwire.TLVOnionPayloadOptional, |
|
) |
|
} |
|
|
|
// If this is the last hop, then we'll populate any TLV records |
|
// destined for it. |
|
if i == len(pathEdges)-1 && len(finalDestRecords) != 0 { |
|
currentHop.TLVRecords = finalDestRecords |
|
} |
|
|
|
hops = append([]*route.Hop{currentHop}, hops...) |
|
|
|
// Finally, we update the amount that needs to flow into the |
|
// *next* hop, which is the amount this hop needs to forward, |
|
// accounting for the fee that it takes. |
|
nextIncomingAmount = amtToForward + fee |
|
} |
|
|
|
// With the base routing data expressed as hops, build the full route |
|
newRoute, err := route.NewRouteFromHops( |
|
nextIncomingAmount, totalTimeLock, route.Vertex(sourceVertex), |
|
hops, |
|
) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
return newRoute, nil |
|
} |
|
|
|
// edgeWeight computes the weight of an edge. This value is used when searching |
|
// for the shortest path within the channel graph between two nodes. Weight is |
|
// is the fee itself plus a time lock penalty added to it. This benefits |
|
// channels with shorter time lock deltas and shorter (hops) routes in general. |
|
// RiskFactor controls the influence of time lock on route selection. This is |
|
// currently a fixed value, but might be configurable in the future. |
|
func edgeWeight(lockedAmt lnwire.MilliSatoshi, fee lnwire.MilliSatoshi, |
|
timeLockDelta uint16) int64 { |
|
// timeLockPenalty is the penalty for the time lock delta of this channel. |
|
// It is controlled by RiskFactorBillionths and scales proportional |
|
// to the amount that will pass through channel. Rationale is that it if |
|
// a twice as large amount gets locked up, it is twice as bad. |
|
timeLockPenalty := int64(lockedAmt) * int64(timeLockDelta) * |
|
RiskFactorBillionths / 1000000000 |
|
|
|
return int64(fee) + timeLockPenalty |
|
} |
|
|
|
// graphParams wraps the set of graph parameters passed to findPath. |
|
type graphParams struct { |
|
// tx can be set to an existing db transaction. If not set, a new |
|
// transaction will be started. |
|
tx *bbolt.Tx |
|
|
|
// graph is the ChannelGraph to be used during path finding. |
|
graph *channeldb.ChannelGraph |
|
|
|
// additionalEdges is an optional set of edges that should be |
|
// considered during path finding, that is not already found in the |
|
// channel graph. |
|
additionalEdges map[route.Vertex][]*channeldb.ChannelEdgePolicy |
|
|
|
// bandwidthHints is an optional map from channels to bandwidths that |
|
// can be populated if the caller has a better estimate of the current |
|
// channel bandwidth than what is found in the graph. If set, it will |
|
// override the capacities and disabled flags found in the graph for |
|
// local channels when doing path finding. In particular, it should be |
|
// set to the current available sending bandwidth for active local |
|
// channels, and 0 for inactive channels. |
|
bandwidthHints map[uint64]lnwire.MilliSatoshi |
|
} |
|
|
|
// RestrictParams wraps the set of restrictions passed to findPath that the |
|
// found path must adhere to. |
|
type RestrictParams struct { |
|
// ProbabilitySource is a callback that is expected to return the |
|
// success probability of traversing the channel from the node. |
|
ProbabilitySource func(route.Vertex, route.Vertex, |
|
lnwire.MilliSatoshi) float64 |
|
|
|
// FeeLimit is a maximum fee amount allowed to be used on the path from |
|
// the source to the target. |
|
FeeLimit lnwire.MilliSatoshi |
|
|
|
// OutgoingChannelID is the channel that needs to be taken to the first |
|
// hop. If nil, any channel may be used. |
|
OutgoingChannelID *uint64 |
|
|
|
// CltvLimit is the maximum time lock of the route excluding the final |
|
// ctlv. After path finding is complete, the caller needs to increase |
|
// all cltv expiry heights with the required final cltv delta. |
|
CltvLimit *uint32 |
|
|
|
// DestPayloadTLV should be set to true if we need to drop off a TLV |
|
// payload at the final hop in order to properly complete this payment |
|
// attempt. |
|
DestPayloadTLV bool |
|
} |
|
|
|
// PathFindingConfig defines global parameters that control the trade-off in |
|
// path finding between fees and probabiity. |
|
type PathFindingConfig struct { |
|
// PaymentAttemptPenalty is the virtual cost in path finding weight |
|
// units of executing a payment attempt that fails. It is used to trade |
|
// off potentially better routes against their probability of |
|
// succeeding. |
|
PaymentAttemptPenalty lnwire.MilliSatoshi |
|
|
|
// MinProbability defines the minimum success probability of the |
|
// returned route. |
|
MinProbability float64 |
|
} |
|
|
|
// findPath attempts to find a path from the source node within the |
|
// ChannelGraph to the target node that's capable of supporting a payment of |
|
// `amt` value. The current approach implemented is modified version of |
|
// Dijkstra's algorithm to find a single shortest path between the source node |
|
// and the destination. The distance metric used for edges is related to the |
|
// time-lock+fee costs along a particular edge. If a path is found, this |
|
// function returns a slice of ChannelHop structs which encoded the chosen path |
|
// from the target to the source. The search is performed backwards from |
|
// destination node back to source. This is to properly accumulate fees |
|
// that need to be paid along the path and accurately check the amount |
|
// to forward at every node against the available bandwidth. |
|
func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, |
|
source, target route.Vertex, amt lnwire.MilliSatoshi) ( |
|
[]*channeldb.ChannelEdgePolicy, error) { |
|
|
|
var err error |
|
tx := g.tx |
|
if tx == nil { |
|
tx, err = g.graph.Database().Begin(false) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer tx.Rollback() |
|
} |
|
|
|
// First we'll initialize an empty heap which'll help us to quickly |
|
// locate the next edge we should visit next during our graph |
|
// traversal. |
|
nodeHeap := newDistanceHeap() |
|
|
|
// For each node in the graph, we create an entry in the distance map |
|
// for the node set with a distance of "infinity". graph.ForEachNode |
|
// also returns the source node, so there is no need to add the source |
|
// node explicitly. |
|
distance := make(map[route.Vertex]nodeWithDist) |
|
if err := g.graph.ForEachNode(tx, func(_ *bbolt.Tx, |
|
node *channeldb.LightningNode) error { |
|
// TODO(roasbeef): with larger graph can just use disk seeks |
|
// with a visited map |
|
vertex := route.Vertex(node.PubKeyBytes) |
|
distance[vertex] = nodeWithDist{ |
|
dist: infinity, |
|
node: route.Vertex(node.PubKeyBytes), |
|
} |
|
|
|
// If we don't have any features for this node, then we can |
|
// stop here. |
|
if node.Features == nil || !r.DestPayloadTLV { |
|
return nil |
|
} |
|
|
|
// We only need to perform this check for the final node, so we |
|
// can exit here if this isn't them. |
|
if vertex != target { |
|
return nil |
|
} |
|
|
|
// If we have any records for the final hop, then we'll check |
|
// not to ensure that they are actually able to interpret them. |
|
supportsTLV := node.Features.HasFeature( |
|
lnwire.TLVOnionPayloadOptional, |
|
) |
|
if !supportsTLV { |
|
return fmt.Errorf("destination hop doesn't " + |
|
"understand new TLV paylods") |
|
} |
|
|
|
return nil |
|
}); err != nil { |
|
return nil, err |
|
} |
|
|
|
additionalEdgesWithSrc := make(map[route.Vertex][]*edgePolicyWithSource) |
|
for vertex, outgoingEdgePolicies := range g.additionalEdges { |
|
// We'll also include all the nodes found within the additional |
|
// edges that are not known to us yet in the distance map. |
|
distance[vertex] = nodeWithDist{ |
|
dist: infinity, |
|
node: vertex, |
|
} |
|
|
|
// Build reverse lookup to find incoming edges. Needed because |
|
// search is taken place from target to source. |
|
for _, outgoingEdgePolicy := range outgoingEdgePolicies { |
|
toVertex := outgoingEdgePolicy.Node.PubKeyBytes |
|
incomingEdgePolicy := &edgePolicyWithSource{ |
|
sourceNode: vertex, |
|
edge: outgoingEdgePolicy, |
|
} |
|
|
|
additionalEdgesWithSrc[toVertex] = |
|
append(additionalEdgesWithSrc[toVertex], |
|
incomingEdgePolicy) |
|
} |
|
} |
|
|
|
// We can't always assume that the end destination is publicly |
|
// advertised to the network and included in the graph.ForEachNode call |
|
// above, so we'll manually include the target node. The target node |
|
// charges no fee. Distance is set to 0, because this is the starting |
|
// point of the graph traversal. We are searching backwards to get the |
|
// fees first time right and correctly match channel bandwidth. |
|
distance[target] = nodeWithDist{ |
|
dist: 0, |
|
weight: 0, |
|
node: target, |
|
amountToReceive: amt, |
|
incomingCltv: 0, |
|
probability: 1, |
|
} |
|
|
|
// We'll use this map as a series of "next" hop pointers. So to get |
|
// from `Vertex` to the target node, we'll take the edge that it's |
|
// mapped to within `next`. |
|
next := make(map[route.Vertex]*channeldb.ChannelEdgePolicy) |
|
|
|
// processEdge is a helper closure that will be used to make sure edges |
|
// satisfy our specific requirements. |
|
processEdge := func(fromVertex route.Vertex, bandwidth lnwire.MilliSatoshi, |
|
edge *channeldb.ChannelEdgePolicy, toNode route.Vertex) { |
|
|
|
// If this is not a local channel and it is disabled, we will |
|
// skip it. |
|
// TODO(halseth): also ignore disable flags for non-local |
|
// channels if bandwidth hint is set? |
|
isSourceChan := fromVertex == source |
|
|
|
edgeFlags := edge.ChannelFlags |
|
isDisabled := edgeFlags&lnwire.ChanUpdateDisabled != 0 |
|
|
|
if !isSourceChan && isDisabled { |
|
return |
|
} |
|
|
|
// If we have an outgoing channel restriction and this is not |
|
// the specified channel, skip it. |
|
if isSourceChan && r.OutgoingChannelID != nil && |
|
*r.OutgoingChannelID != edge.ChannelID { |
|
|
|
return |
|
} |
|
|
|
// Calculate amount that the candidate node would have to sent |
|
// out. |
|
toNodeDist := distance[toNode] |
|
amountToSend := toNodeDist.amountToReceive |
|
|
|
// Request the success probability for this edge. |
|
edgeProbability := r.ProbabilitySource( |
|
fromVertex, toNode, amountToSend, |
|
) |
|
|
|
log.Tracef("path finding probability: fromnode=%v, tonode=%v, "+ |
|
"probability=%v", fromVertex, toNode, edgeProbability) |
|
|
|
// If the probability is zero, there is no point in trying. |
|
if edgeProbability == 0 { |
|
return |
|
} |
|
|
|
// If the estimated bandwidth of the channel edge is not able |
|
// to carry the amount that needs to be send, return. |
|
if bandwidth < amountToSend { |
|
return |
|
} |
|
|
|
// If the amountToSend is less than the minimum required |
|
// amount, return. |
|
if amountToSend < edge.MinHTLC { |
|
return |
|
} |
|
|
|
// If this edge was constructed from a hop hint, we won't have access to |
|
// its max HTLC. Therefore, only consider discarding this edge here if |
|
// the field is set. |
|
if edge.MaxHTLC != 0 && edge.MaxHTLC < amountToSend { |
|
return |
|
} |
|
|
|
// Compute fee that fromVertex is charging. It is based on the |
|
// amount that needs to be sent to the next node in the route. |
|
// |
|
// Source node has no predecessor to pay a fee. Therefore set |
|
// fee to zero, because it should not be included in the fee |
|
// limit check and edge weight. |
|
// |
|
// Also determine the time lock delta that will be added to the |
|
// route if fromVertex is selected. If fromVertex is the source |
|
// node, no additional timelock is required. |
|
var fee lnwire.MilliSatoshi |
|
var timeLockDelta uint16 |
|
if fromVertex != source { |
|
fee = computeFee(amountToSend, edge) |
|
timeLockDelta = edge.TimeLockDelta |
|
} |
|
|
|
incomingCltv := toNodeDist.incomingCltv + |
|
uint32(timeLockDelta) |
|
|
|
// Check that we have cltv limit and that we are within it. |
|
if r.CltvLimit != nil && incomingCltv > *r.CltvLimit { |
|
return |
|
} |
|
|
|
// amountToReceive is the amount that the node that is added to |
|
// the distance map needs to receive from a (to be found) |
|
// previous node in the route. That previous node will need to |
|
// pay the amount that this node forwards plus the fee it |
|
// charges. |
|
amountToReceive := amountToSend + fee |
|
|
|
// Check if accumulated fees would exceed fee limit when this |
|
// node would be added to the path. |
|
totalFee := amountToReceive - amt |
|
if totalFee > r.FeeLimit { |
|
return |
|
} |
|
|
|
// Calculate total probability of successfully reaching target |
|
// by multiplying the probabilities. Both this edge and the rest |
|
// of the route must succeed. |
|
probability := toNodeDist.probability * edgeProbability |
|
|
|
// If the probability is below the specified lower bound, we can |
|
// abandon this direction. Adding further nodes can only lower |
|
// the probability more. |
|
if probability < cfg.MinProbability { |
|
return |
|
} |
|
|
|
// By adding fromVertex in the route, there will be an extra |
|
// weight composed of the fee that this node will charge and |
|
// the amount that will be locked for timeLockDelta blocks in |
|
// the HTLC that is handed out to fromVertex. |
|
weight := edgeWeight(amountToReceive, fee, timeLockDelta) |
|
|
|
// Compute the tentative weight to this new channel/edge |
|
// which is the weight from our toNode to the target node |
|
// plus the weight of this edge. |
|
tempWeight := toNodeDist.weight + weight |
|
|
|
// Add an extra factor to the weight to take into account the |
|
// probability. |
|
tempDist := getProbabilityBasedDist( |
|
tempWeight, probability, |
|
int64(cfg.PaymentAttemptPenalty), |
|
) |
|
|
|
// If the current best route is better than this candidate |
|
// route, return. It is important to also return if the distance |
|
// is equal, because otherwise the algorithm could run into an |
|
// endless loop. |
|
if tempDist >= distance[fromVertex].dist { |
|
return |
|
} |
|
|
|
// Every edge should have a positive time lock delta. If we |
|
// encounter a zero delta, log a warning line. |
|
if edge.TimeLockDelta == 0 { |
|
log.Warnf("Channel %v has zero cltv delta", |
|
edge.ChannelID) |
|
} |
|
|
|
// All conditions are met and this new tentative distance is |
|
// better than the current best known distance to this node. |
|
// The new better distance is recorded, and also our "next hop" |
|
// map is populated with this edge. |
|
distance[fromVertex] = nodeWithDist{ |
|
dist: tempDist, |
|
weight: tempWeight, |
|
node: fromVertex, |
|
amountToReceive: amountToReceive, |
|
incomingCltv: incomingCltv, |
|
probability: probability, |
|
} |
|
|
|
next[fromVertex] = edge |
|
|
|
// Either push distance[fromVertex] onto the heap if the node |
|
// represented by fromVertex is not already on the heap OR adjust |
|
// its position within the heap via heap.Fix. |
|
nodeHeap.PushOrFix(distance[fromVertex]) |
|
} |
|
|
|
// TODO(roasbeef): also add path caching |
|
// * similar to route caching, but doesn't factor in the amount |
|
|
|
// To start, our target node will the sole item within our distance |
|
// heap. |
|
heap.Push(&nodeHeap, distance[target]) |
|
|
|
for nodeHeap.Len() != 0 { |
|
// Fetch the node within the smallest distance from our source |
|
// from the heap. |
|
partialPath := heap.Pop(&nodeHeap).(nodeWithDist) |
|
pivot := partialPath.node |
|
|
|
// If we've reached our source (or we don't have any incoming |
|
// edges), then we're done here and can exit the graph |
|
// traversal early. |
|
if pivot == source { |
|
break |
|
} |
|
|
|
cb := func(_ *bbolt.Tx, edgeInfo *channeldb.ChannelEdgeInfo, _, |
|
inEdge *channeldb.ChannelEdgePolicy) error { |
|
|
|
// If there is no edge policy for this candidate |
|
// node, skip. Note that we are searching backwards |
|
// so this node would have come prior to the pivot |
|
// node in the route. |
|
if inEdge == nil { |
|
return nil |
|
} |
|
|
|
// We'll query the lower layer to see if we can obtain |
|
// any more up to date information concerning the |
|
// bandwidth of this edge. |
|
edgeBandwidth, ok := g.bandwidthHints[edgeInfo.ChannelID] |
|
if !ok { |
|
// If we don't have a hint for this edge, then |
|
// we'll just use the known Capacity/MaxHTLC as |
|
// the available bandwidth. It's possible for |
|
// the capacity to be unknown when operating |
|
// under a light client. |
|
edgeBandwidth = inEdge.MaxHTLC |
|
if edgeBandwidth == 0 { |
|
edgeBandwidth = lnwire.NewMSatFromSatoshis( |
|
edgeInfo.Capacity, |
|
) |
|
} |
|
} |
|
|
|
// Before we can process the edge, we'll need to fetch |
|
// the node on the _other_ end of this channel as we |
|
// may later need to iterate over the incoming edges of |
|
// this node if we explore it further. |
|
chanSource, err := edgeInfo.OtherNodeKeyBytes(pivot[:]) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
// Check if this candidate node is better than what we |
|
// already have. |
|
processEdge(route.Vertex(chanSource), edgeBandwidth, inEdge, pivot) |
|
return nil |
|
} |
|
|
|
// Now that we've found the next potential step to take we'll |
|
// examine all the incoming edges (channels) from this node to |
|
// further our graph traversal. |
|
err := g.graph.ForEachNodeChannel(tx, pivot[:], cb) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// Then, we'll examine all the additional edges from the node |
|
// we're currently visiting. Since we don't know the capacity |
|
// of the private channel, we'll assume it was selected as a |
|
// routing hint due to having enough capacity for the payment |
|
// and use the payment amount as its capacity. |
|
bandWidth := partialPath.amountToReceive |
|
for _, reverseEdge := range additionalEdgesWithSrc[pivot] { |
|
processEdge(reverseEdge.sourceNode, bandWidth, |
|
reverseEdge.edge, pivot) |
|
} |
|
} |
|
|
|
// If the source node isn't found in the next hop map, then a path |
|
// doesn't exist, so we terminate in an error. |
|
if _, ok := next[source]; !ok { |
|
return nil, newErrf(ErrNoPathFound, "unable to find a path to "+ |
|
"destination") |
|
} |
|
|
|
// Use the nextHop map to unravel the forward path from source to |
|
// target. |
|
pathEdges := make([]*channeldb.ChannelEdgePolicy, 0, len(next)) |
|
currentNode := source |
|
for currentNode != target { // TODO(roasbeef): assumes no cycles |
|
// Determine the next hop forward using the next map. |
|
nextNode := next[currentNode] |
|
|
|
// Add the next hop to the list of path edges. |
|
pathEdges = append(pathEdges, nextNode) |
|
|
|
// Advance current node. |
|
currentNode = route.Vertex(nextNode.Node.PubKeyBytes) |
|
} |
|
|
|
// The route is invalid if it spans more than 20 hops. The current |
|
// Sphinx (onion routing) implementation can only encode up to 20 hops |
|
// as the entire packet is fixed size. If this route is more than 20 |
|
// hops, then it's invalid. |
|
numEdges := len(pathEdges) |
|
if numEdges > HopLimit { |
|
return nil, newErr(ErrMaxHopsExceeded, "potential path has "+ |
|
"too many hops") |
|
} |
|
|
|
log.Debugf("Found route: probability=%v, hops=%v, fee=%v\n", |
|
distance[source].probability, numEdges, |
|
distance[source].amountToReceive-amt) |
|
|
|
return pathEdges, nil |
|
} |
|
|
|
// getProbabilityBasedDist converts a weight into a distance that takes into |
|
// account the success probability and the (virtual) cost of a failed payment |
|
// attempt. |
|
// |
|
// Derivation: |
|
// |
|
// Suppose there are two routes A and B with fees Fa and Fb and success |
|
// probabilities Pa and Pb. |
|
// |
|
// Is the expected cost of trying route A first and then B lower than trying the |
|
// other way around? |
|
// |
|
// The expected cost of A-then-B is: Pa*Fa + (1-Pa)*Pb*(c+Fb) |
|
// |
|
// The expected cost of B-then-A is: Pb*Fb + (1-Pb)*Pa*(c+Fa) |
|
// |
|
// In these equations, the term representing the case where both A and B fail is |
|
// left out because its value would be the same in both cases. |
|
// |
|
// Pa*Fa + (1-Pa)*Pb*(c+Fb) < Pb*Fb + (1-Pb)*Pa*(c+Fa) |
|
// |
|
// Pa*Fa + Pb*c + Pb*Fb - Pa*Pb*c - Pa*Pb*Fb < Pb*Fb + Pa*c + Pa*Fa - Pa*Pb*c - Pa*Pb*Fa |
|
// |
|
// Removing terms that cancel out: |
|
// Pb*c - Pa*Pb*Fb < Pa*c - Pa*Pb*Fa |
|
// |
|
// Divide by Pa*Pb: |
|
// c/Pa - Fb < c/Pb - Fa |
|
// |
|
// Move terms around: |
|
// Fa + c/Pa < Fb + c/Pb |
|
// |
|
// So the value of F + c/P can be used to compare routes. |
|
func getProbabilityBasedDist(weight int64, probability float64, penalty int64) int64 { |
|
// Clamp probability to prevent overflow. |
|
const minProbability = 0.00001 |
|
|
|
if probability < minProbability { |
|
return infinity |
|
} |
|
|
|
return weight + int64(float64(penalty)/probability) |
|
}
|
|
|