routing: modify findPath to take into account additional edges

In this commit, we modify our path finding algorithm to take an
additional set of edges that are currently not known to us that are
used to temporarily extend our graph with during a payment session.
These edges should assist the sender of a payment in successfully
constructing a path to the destination.

These edges should usually represent private channels, as they are not
publicly advertised to the network for routing.
This commit is contained in:
Wilmer Paulino 2018-03-27 00:14:10 -04:00
parent 7965247db1
commit 4598df054e
No known key found for this signature in database
GPG Key ID: 6DF57B9F9514972F
4 changed files with 142 additions and 92 deletions

@ -282,8 +282,11 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
// Taking into account this prune view, we'll attempt to locate a path
// to our destination, respecting the recommendations from
// missionControl.
path, err := findPath(nil, p.mc.graph, p.mc.selfNode, payment.Target,
pruneView.vertexes, pruneView.edges, payment.Amount)
path, err := findPath(
nil, p.mc.graph, p.additionalEdges, p.mc.selfNode,
payment.Target, pruneView.vertexes, pruneView.edges,
payment.Amount,
)
if err != nil {
return nil, err
}

@ -455,6 +455,7 @@ func edgeWeight(amt lnwire.MilliSatoshi, e *channeldb.ChannelEdgePolicy) int64 {
// function returns a slice of ChannelHop structs which encoded the chosen path
// from the target to the source.
func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph,
additionalEdges map[Vertex][]*channeldb.ChannelEdgePolicy,
sourceNode *channeldb.LightningNode, target *btcec.PublicKey,
ignoredNodes map[Vertex]struct{}, ignoredEdges map[uint64]struct{},
amt lnwire.MilliSatoshi) ([]*ChannelHop, error) {
@ -473,9 +474,8 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph,
// traversal.
var nodeHeap distanceHeap
// For each node/Vertex the graph we create an entry in the distance
// map for the node set with a distance of "infinity". We also mark
// add the node to our set of unvisited nodes.
// For each node in the graph, we create an entry in the distance
// map for the node set with a distance of "infinity".
distance := make(map[Vertex]nodeWithDist)
if err := graph.ForEachNode(tx, func(_ *bolt.Tx, node *channeldb.LightningNode) error {
// TODO(roasbeef): with larger graph can just use disk seeks
@ -489,6 +489,86 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph,
return nil, err
}
// We'll also include all the nodes found within the additional edges
// that are not known to us yet in the distance map.
for vertex := range additionalEdges {
node := &channeldb.LightningNode{PubKeyBytes: vertex}
distance[vertex] = nodeWithDist{
dist: infinity,
node: node,
}
}
// 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.
targetVertex := NewVertex(target)
targetNode := &channeldb.LightningNode{PubKeyBytes: targetVertex}
distance[targetVertex] = nodeWithDist{
dist: infinity,
node: targetNode,
}
// We'll use this map as a series of "previous" hop pointers. So to get
// to `Vertex` we'll take the edge that it's mapped to within `prev`.
prev := make(map[Vertex]edgeWithPrev)
// processEdge is a helper closure that will be used to make sure edges
// satisfy our specific requirements.
processEdge := func(edge *channeldb.ChannelEdgePolicy,
capacity btcutil.Amount, pivot Vertex) {
v := Vertex(edge.Node.PubKeyBytes)
// If the edge is currently disabled, then we'll stop here, as
// we shouldn't attempt to route through it.
edgeFlags := lnwire.ChanUpdateFlag(edge.Flags)
if edgeFlags&lnwire.ChanUpdateDisabled != 0 {
return
}
// If this vertex or edge has been black listed, then we'll skip
// exploring this edge.
if _, ok := ignoredNodes[v]; ok {
return
}
if _, ok := ignoredEdges[edge.ChannelID]; ok {
return
}
// Compute the tentative distance to this new channel/edge which
// is the distance to our pivot node plus the weight of this
// edge.
tempDist := distance[pivot].dist + edgeWeight(amt, edge)
// If this new tentative distance is better than the current
// best known distance to this node, then we record the new
// better distance, and also populate our "next hop" map with
// this edge. We'll also shave off irrelevant edges by adding
// the sufficient capacity of an edge and clearing their
// min-htlc amount to our relaxation condition.
if tempDist < distance[v].dist && capacity >= amt.ToSatoshis() &&
amt >= edge.MinHTLC && edge.TimeLockDelta != 0 {
distance[v] = nodeWithDist{
dist: tempDist,
node: edge.Node,
}
prev[v] = edgeWithPrev{
edge: &ChannelHop{
ChannelEdgePolicy: edge,
Capacity: capacity,
},
prevNode: pivot,
}
// Add this new node to our heap as we'd like to further
// explore down this edge.
heap.Push(&nodeHeap, distance[v])
}
}
// TODO(roasbeef): also add path caching
// * similar to route caching, but doesn't factor in the amount
@ -505,11 +585,6 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph,
// heap.
heap.Push(&nodeHeap, distance[sourceVertex])
targetBytes := target.SerializeCompressed()
// We'll use this map as a series of "previous" hop pointers. So to get
// to `Vertex` we'll take the edge that it's mapped to within `prev`.
prev := make(map[Vertex]edgeWithPrev)
for nodeHeap.Len() != 0 {
// Fetch the node within the smallest distance from our source
// from the heap.
@ -519,7 +594,7 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph,
// If we've reached our target (or we don't have any outgoing
// edges), then we're done here and can exit the graph
// traversal early.
if bytes.Equal(bestNode.PubKeyBytes[:], targetBytes) {
if bytes.Equal(bestNode.PubKeyBytes[:], targetVertex[:]) {
break
}
@ -529,65 +604,9 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph,
pivot := Vertex(bestNode.PubKeyBytes)
err := bestNode.ForEachChannel(tx, func(tx *bolt.Tx,
edgeInfo *channeldb.ChannelEdgeInfo,
outEdge, inEdge *channeldb.ChannelEdgePolicy) error {
outEdge, _ *channeldb.ChannelEdgePolicy) error {
v := Vertex(outEdge.Node.PubKeyBytes)
// If the outgoing edge is currently disabled, then
// we'll stop here, as we shouldn't attempt to route
// through it.
edgeFlags := lnwire.ChanUpdateFlag(outEdge.Flags)
if edgeFlags&lnwire.ChanUpdateDisabled == lnwire.ChanUpdateDisabled {
return nil
}
// If this Vertex or edge has been black listed, then
// we'll skip exploring this edge during this
// iteration.
if _, ok := ignoredNodes[v]; ok {
return nil
}
if _, ok := ignoredEdges[outEdge.ChannelID]; ok {
return nil
}
// Compute the tentative distance to this new
// channel/edge which is the distance to our current
// pivot node plus the weight of this edge.
tempDist := distance[pivot].dist + edgeWeight(amt, outEdge)
// If this new tentative distance is better than the
// current best known distance to this node, then we
// record the new better distance, and also populate
// our "next hop" map with this edge. We'll also shave
// off irrelevant edges by adding the sufficient
// capacity of an edge and clearing their min-htlc
// amount to our relaxation condition.
if tempDist < distance[v].dist &&
edgeInfo.Capacity >= amt.ToSatoshis() &&
amt >= outEdge.MinHTLC &&
outEdge.TimeLockDelta != 0 {
distance[v] = nodeWithDist{
dist: tempDist,
node: outEdge.Node,
}
prev[v] = edgeWithPrev{
// We'll use the *incoming* edge here
// as we need to use the routing policy
// specified by the node this channel
// connects to.
edge: &ChannelHop{
ChannelEdgePolicy: outEdge,
Capacity: edgeInfo.Capacity,
},
prevNode: bestNode.PubKeyBytes,
}
// Add this new node to our heap as we'd like
// to further explore down this edge.
heap.Push(&nodeHeap, distance[v])
}
processEdge(outEdge, edgeInfo.Capacity, pivot)
// TODO(roasbeef): return min HTLC as error in end?
@ -596,6 +615,15 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph,
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.
for _, edge := range additionalEdges[bestNode.PubKeyBytes] {
processEdge(edge, amt.ToSatoshis(), pivot)
}
}
// If the target node isn't found in the prev hop map, then a path
@ -669,7 +697,8 @@ func findPaths(tx *bolt.Tx, graph *channeldb.ChannelGraph,
// selfNode) to the target destination that's capable of carrying amt
// satoshis along the path before fees are calculated.
startingPath, err := findPath(
tx, graph, source, target, ignoredVertexes, ignoredEdges, amt,
tx, graph, nil, source, target, ignoredVertexes, ignoredEdges,
amt,
)
if err != nil {
log.Errorf("Unable to find path: %v", err)
@ -742,8 +771,8 @@ func findPaths(tx *bolt.Tx, graph *channeldb.ChannelGraph,
// root path removed, we'll attempt to find another
// shortest path from the spur node to the destination.
spurPath, err := findPath(
tx, graph, spurNode, target, ignoredVertexes,
ignoredEdges, amt,
tx, graph, nil, spurNode, target,
ignoredVertexes, ignoredEdges, amt,
)
// If we weren't able to find a path, we'll continue to

@ -311,8 +311,10 @@ func TestBasicGraphPathFinding(t *testing.T) {
paymentAmt := lnwire.NewMSatFromSatoshis(100)
target := aliases["sophon"]
path, err := findPath(nil, graph, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt)
path, err := findPath(
nil, graph, nil, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt,
)
if err != nil {
t.Fatalf("unable to find path: %v", err)
}
@ -451,8 +453,10 @@ func TestBasicGraphPathFinding(t *testing.T) {
// exist two possible paths in the graph, but the shorter (1 hop) path
// should be selected.
target = aliases["luoji"]
path, err = findPath(nil, graph, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt)
path, err = findPath(
nil, graph, nil, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt,
)
if err != nil {
t.Fatalf("unable to find route: %v", err)
}
@ -572,8 +576,10 @@ func TestNewRoutePathTooLong(t *testing.T) {
// We start by confirming that routing a payment 20 hops away is possible.
// Alice should be able to find a valid route to ursula.
target := aliases["ursula"]
_, err = findPath(nil, graph, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt)
_, err = findPath(
nil, graph, nil, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt,
)
if err != nil {
t.Fatalf("path should have been found")
}
@ -581,8 +587,10 @@ func TestNewRoutePathTooLong(t *testing.T) {
// Vincent is 21 hops away from Alice, and thus no valid route should be
// presented to Alice.
target = aliases["vincent"]
path, err := findPath(nil, graph, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt)
path, err := findPath(
nil, graph, nil, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt,
)
if err == nil {
t.Fatalf("should not have been able to find path, supposed to be "+
"greater than 20 hops, found route with %v hops",
@ -621,8 +629,10 @@ func TestPathNotAvailable(t *testing.T) {
t.Fatalf("unable to parse pubkey: %v", err)
}
_, err = findPath(nil, graph, sourceNode, unknownNode, ignoredVertexes,
ignoredEdges, 100)
_, err = findPath(
nil, graph, nil, sourceNode, unknownNode, ignoredVertexes,
ignoredEdges, 100,
)
if !IsError(err, ErrNoPathFound) {
t.Fatalf("path shouldn't have been found: %v", err)
}
@ -655,8 +665,10 @@ func TestPathInsufficientCapacity(t *testing.T) {
target := aliases["sophon"]
payAmt := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
_, err = findPath(nil, graph, sourceNode, target, ignoredVertexes,
ignoredEdges, payAmt)
_, err = findPath(
nil, graph, nil, sourceNode, target, ignoredVertexes,
ignoredEdges, payAmt,
)
if !IsError(err, ErrNoPathFound) {
t.Fatalf("graph shouldn't be able to support payment: %v", err)
}
@ -683,8 +695,10 @@ func TestRouteFailMinHTLC(t *testing.T) {
// attempt should fail.
target := aliases["songoku"]
payAmt := lnwire.MilliSatoshi(10)
_, err = findPath(nil, graph, sourceNode, target, ignoredVertexes,
ignoredEdges, payAmt)
_, err = findPath(
nil, graph, nil, sourceNode, target, ignoredVertexes,
ignoredEdges, payAmt,
)
if !IsError(err, ErrNoPathFound) {
t.Fatalf("graph shouldn't be able to support payment: %v", err)
}
@ -711,8 +725,10 @@ func TestRouteFailDisabledEdge(t *testing.T) {
// succeed without issue, and return a single path.
target := aliases["songoku"]
payAmt := lnwire.NewMSatFromSatoshis(10000)
_, err = findPath(nil, graph, sourceNode, target, ignoredVertexes,
ignoredEdges, payAmt)
_, err = findPath(
nil, graph, nil, sourceNode, target, ignoredVertexes,
ignoredEdges, payAmt,
)
if err != nil {
t.Fatalf("unable to find path: %v", err)
}
@ -730,8 +746,10 @@ func TestRouteFailDisabledEdge(t *testing.T) {
// Now, if we attempt to route through that edge, we should get a
// failure as it is no longer eligible.
_, err = findPath(nil, graph, sourceNode, target, ignoredVertexes,
ignoredEdges, payAmt)
_, err = findPath(
nil, graph, nil, sourceNode, target, ignoredVertexes,
ignoredEdges, payAmt,
)
if !IsError(err, ErrNoPathFound) {
t.Fatalf("graph shouldn't be able to support payment: %v", err)
}

@ -1602,8 +1602,8 @@ func TestFindPathFeeWeighting(t *testing.T) {
// the edge weighting, we should select the direct path over the 2 hop
// path even though the direct path has a higher potential time lock.
path, err := findPath(
nil, ctx.graph, sourceNode, target, ignoreVertex, ignoreEdge,
amt,
nil, ctx.graph, nil, sourceNode, target, ignoreVertex,
ignoreEdge, amt,
)
if err != nil {
t.Fatalf("unable to find path: %v", err)