Merge pull request #3418 from champo/routing_is_fast_now

routing, channeldb: several optimizations for path finding
This commit is contained in:
Joost Jager 2019-10-25 10:54:43 +02:00 committed by GitHub
commit 5ae4f0eae4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 82 additions and 95 deletions

@ -3,6 +3,7 @@ package routing
import ( import (
"container/heap" "container/heap"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
) )
@ -35,12 +36,15 @@ type nodeWithDist struct {
// Includes the routing fees and a virtual cost factor to account for // Includes the routing fees and a virtual cost factor to account for
// time locks. // time locks.
weight int64 weight int64
// nextHop is the edge this route comes from.
nextHop *channeldb.ChannelEdgePolicy
} }
// distanceHeap is a min-distance heap that's used within our path finding // distanceHeap is a min-distance heap that's used within our path finding
// algorithm to keep track of the "closest" node to our source node. // algorithm to keep track of the "closest" node to our source node.
type distanceHeap struct { type distanceHeap struct {
nodes []nodeWithDist nodes []*nodeWithDist
// pubkeyIndices maps public keys of nodes to their respective index in // pubkeyIndices maps public keys of nodes to their respective index in
// the heap. This is used as a way to avoid db lookups by using heap.Fix // the heap. This is used as a way to avoid db lookups by using heap.Fix
@ -50,9 +54,10 @@ type distanceHeap struct {
// newDistanceHeap initializes a new distance heap. This is required because // newDistanceHeap initializes a new distance heap. This is required because
// we must initialize the pubkeyIndices map for path-finding optimizations. // we must initialize the pubkeyIndices map for path-finding optimizations.
func newDistanceHeap() distanceHeap { func newDistanceHeap(numNodes int) distanceHeap {
distHeap := distanceHeap{ distHeap := distanceHeap{
pubkeyIndices: make(map[route.Vertex]int), pubkeyIndices: make(map[route.Vertex]int, numNodes),
nodes: make([]*nodeWithDist, 0, numNodes),
} }
return distHeap return distHeap
@ -84,7 +89,7 @@ func (d *distanceHeap) Swap(i, j int) {
// //
// NOTE: This is part of the heap.Interface implementation. // NOTE: This is part of the heap.Interface implementation.
func (d *distanceHeap) Push(x interface{}) { func (d *distanceHeap) Push(x interface{}) {
n := x.(nodeWithDist) n := x.(*nodeWithDist)
d.nodes = append(d.nodes, n) d.nodes = append(d.nodes, n)
d.pubkeyIndices[n.node] = len(d.nodes) - 1 d.pubkeyIndices[n.node] = len(d.nodes) - 1
} }
@ -96,6 +101,7 @@ func (d *distanceHeap) Push(x interface{}) {
func (d *distanceHeap) Pop() interface{} { func (d *distanceHeap) Pop() interface{} {
n := len(d.nodes) n := len(d.nodes)
x := d.nodes[n-1] x := d.nodes[n-1]
d.nodes[n-1] = nil
d.nodes = d.nodes[0 : n-1] d.nodes = d.nodes[0 : n-1]
delete(d.pubkeyIndices, x.node) delete(d.pubkeyIndices, x.node)
return x return x
@ -106,7 +112,7 @@ func (d *distanceHeap) Pop() interface{} {
// modify its position and reorder the heap. If the vertex does not already // modify its position and reorder the heap. If the vertex does not already
// exist in the heap, then it is pushed onto the heap. Otherwise, we will end // exist in the heap, then it is pushed onto the heap. Otherwise, we will end
// up performing more db lookups on the same node in the pathfinding algorithm. // up performing more db lookups on the same node in the pathfinding algorithm.
func (d *distanceHeap) PushOrFix(dist nodeWithDist) { func (d *distanceHeap) PushOrFix(dist *nodeWithDist) {
index, ok := d.pubkeyIndices[dist.node] index, ok := d.pubkeyIndices[dist.node]
if !ok { if !ok {
heap.Push(d, dist) heap.Push(d, dist)

@ -17,19 +17,19 @@ func TestHeapOrdering(t *testing.T) {
// First, create a blank heap, we'll use this to push on randomly // First, create a blank heap, we'll use this to push on randomly
// generated items. // generated items.
nodeHeap := newDistanceHeap() nodeHeap := newDistanceHeap(0)
prand.Seed(1) prand.Seed(1)
// Create 100 random entries adding them to the heap created above, but // Create 100 random entries adding them to the heap created above, but
// also a list that we'll sort with the entries. // also a list that we'll sort with the entries.
const numEntries = 100 const numEntries = 100
sortedEntries := make([]nodeWithDist, 0, numEntries) sortedEntries := make([]*nodeWithDist, 0, numEntries)
for i := 0; i < numEntries; i++ { for i := 0; i < numEntries; i++ {
var pubKey [33]byte var pubKey [33]byte
prand.Read(pubKey[:]) prand.Read(pubKey[:])
entry := nodeWithDist{ entry := &nodeWithDist{
node: route.Vertex(pubKey), node: route.Vertex(pubKey),
dist: prand.Int63(), dist: prand.Int63(),
} }
@ -55,9 +55,9 @@ func TestHeapOrdering(t *testing.T) {
// One by one, pop of all the entries from the heap, they should come // One by one, pop of all the entries from the heap, they should come
// out in sorted order. // out in sorted order.
var poppedEntries []nodeWithDist var poppedEntries []*nodeWithDist
for nodeHeap.Len() != 0 { for nodeHeap.Len() != 0 {
e := heap.Pop(&nodeHeap).(nodeWithDist) e := heap.Pop(&nodeHeap).(*nodeWithDist)
poppedEntries = append(poppedEntries, e) poppedEntries = append(poppedEntries, e)
} }

@ -6,6 +6,7 @@ import (
"math" "math"
"time" "time"
"github.com/btcsuite/btcd/btcec"
"github.com/coreos/bbolt" "github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
@ -36,6 +37,11 @@ const (
// some effect with smaller time lock values. The value may need // some effect with smaller time lock values. The value may need
// tweaking and/or be made configurable in the future. // tweaking and/or be made configurable in the future.
RiskFactorBillionths = 15 RiskFactorBillionths = 15
// estimatedNodeCount is used to preallocate the path finding structures
// to avoid resizing and copies. It should be number on the same order as
// the number of active nodes in the network.
estimatedNodeCount = 10000
) )
// pathFinder defines the interface of a path finding algorithm. // pathFinder defines the interface of a path finding algorithm.
@ -319,61 +325,40 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
defer tx.Rollback() defer tx.Rollback()
} }
if r.DestPayloadTLV {
// Check if the target has TLV enabled
targetKey, err := btcec.ParsePubKey(target[:], btcec.S256())
if err != nil {
return nil, err
}
targetNode, err := g.graph.FetchLightningNode(targetKey)
if err != nil {
return nil, err
}
if targetNode.Features != nil {
supportsTLV := targetNode.Features.HasFeature(
lnwire.TLVOnionPayloadOptional,
)
if !supportsTLV {
return nil, fmt.Errorf("destination hop doesn't " +
"understand new TLV paylods")
}
}
}
// First we'll initialize an empty heap which'll help us to quickly // First we'll initialize an empty heap which'll help us to quickly
// locate the next edge we should visit next during our graph // locate the next edge we should visit next during our graph
// traversal. // traversal.
nodeHeap := newDistanceHeap() nodeHeap := newDistanceHeap(estimatedNodeCount)
// For each node in the graph, we create an entry in the distance map // Holds the current best distance for a given node.
// for the node set with a distance of "infinity". graph.ForEachNode distance := make(map[route.Vertex]*nodeWithDist, estimatedNodeCount)
// 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) additionalEdgesWithSrc := make(map[route.Vertex][]*edgePolicyWithSource)
for vertex, outgoingEdgePolicies := range g.additionalEdges { 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 // Build reverse lookup to find incoming edges. Needed because
// search is taken place from target to source. // search is taken place from target to source.
@ -391,12 +376,12 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
} }
// We can't always assume that the end destination is publicly // We can't always assume that the end destination is publicly
// advertised to the network and included in the graph.ForEachNode call // advertised to the network so we'll manually include the target node.
// above, so we'll manually include the target node. The target node // The target node charges no fee. Distance is set to 0, because this
// charges no fee. Distance is set to 0, because this is the starting // is the starting point of the graph traversal. We are searching
// point of the graph traversal. We are searching backwards to get the // backwards to get the fees first time right and correctly match
// fees first time right and correctly match channel bandwidth. // channel bandwidth.
distance[target] = nodeWithDist{ distance[target] = &nodeWithDist{
dist: 0, dist: 0,
weight: 0, weight: 0,
node: target, node: target,
@ -405,15 +390,10 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
probability: 1, 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 // processEdge is a helper closure that will be used to make sure edges
// satisfy our specific requirements. // satisfy our specific requirements.
processEdge := func(fromVertex route.Vertex, bandwidth lnwire.MilliSatoshi, processEdge := func(fromVertex route.Vertex, bandwidth lnwire.MilliSatoshi,
edge *channeldb.ChannelEdgePolicy, toNode route.Vertex) { edge *channeldb.ChannelEdgePolicy, toNodeDist *nodeWithDist) {
edgesExpanded++ edgesExpanded++
@ -440,16 +420,18 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// Calculate amount that the candidate node would have to sent // Calculate amount that the candidate node would have to sent
// out. // out.
toNodeDist := distance[toNode]
amountToSend := toNodeDist.amountToReceive amountToSend := toNodeDist.amountToReceive
// Request the success probability for this edge. // Request the success probability for this edge.
edgeProbability := r.ProbabilitySource( edgeProbability := r.ProbabilitySource(
fromVertex, toNode, amountToSend, fromVertex, toNodeDist.node, amountToSend,
) )
log.Tracef("path finding probability: fromnode=%v, tonode=%v, "+ log.Trace(newLogClosure(func() string {
"probability=%v", fromVertex, toNode, edgeProbability) return fmt.Sprintf("path finding probability: fromnode=%v,"+
" tonode=%v, probability=%v", fromVertex, toNodeDist.node,
edgeProbability)
}))
// If the probability is zero, there is no point in trying. // If the probability is zero, there is no point in trying.
if edgeProbability == 0 { if edgeProbability == 0 {
@ -548,7 +530,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// route, return. It is important to also return if the distance // route, return. It is important to also return if the distance
// is equal, because otherwise the algorithm could run into an // is equal, because otherwise the algorithm could run into an
// endless loop. // endless loop.
if tempDist >= distance[fromVertex].dist { current, ok := distance[fromVertex]
if ok && tempDist >= current.dist {
return return
} }
@ -563,21 +546,21 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// better than the current best known distance to this node. // better than the current best known distance to this node.
// The new better distance is recorded, and also our "next hop" // The new better distance is recorded, and also our "next hop"
// map is populated with this edge. // map is populated with this edge.
distance[fromVertex] = nodeWithDist{ withDist := &nodeWithDist{
dist: tempDist, dist: tempDist,
weight: tempWeight, weight: tempWeight,
node: fromVertex, node: fromVertex,
amountToReceive: amountToReceive, amountToReceive: amountToReceive,
incomingCltv: incomingCltv, incomingCltv: incomingCltv,
probability: probability, probability: probability,
nextHop: edge,
} }
distance[fromVertex] = withDist
next[fromVertex] = edge // Either push withDist onto the heap if the node
// Either push distance[fromVertex] onto the heap if the node
// represented by fromVertex is not already on the heap OR adjust // represented by fromVertex is not already on the heap OR adjust
// its position within the heap via heap.Fix. // its position within the heap via heap.Fix.
nodeHeap.PushOrFix(distance[fromVertex]) nodeHeap.PushOrFix(withDist)
} }
// TODO(roasbeef): also add path caching // TODO(roasbeef): also add path caching
@ -592,7 +575,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// Fetch the node within the smallest distance from our source // Fetch the node within the smallest distance from our source
// from the heap. // from the heap.
partialPath := heap.Pop(&nodeHeap).(nodeWithDist) partialPath := heap.Pop(&nodeHeap).(*nodeWithDist)
pivot := partialPath.node pivot := partialPath.node
// If we've reached our source (or we don't have any incoming // If we've reached our source (or we don't have any incoming
@ -642,7 +625,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// Check if this candidate node is better than what we // Check if this candidate node is better than what we
// already have. // already have.
processEdge(route.Vertex(chanSource), edgeBandwidth, inEdge, pivot) processEdge(chanSource, edgeBandwidth, inEdge, partialPath)
return nil return nil
} }
@ -662,30 +645,28 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
bandWidth := partialPath.amountToReceive bandWidth := partialPath.amountToReceive
for _, reverseEdge := range additionalEdgesWithSrc[pivot] { for _, reverseEdge := range additionalEdgesWithSrc[pivot] {
processEdge(reverseEdge.sourceNode, bandWidth, processEdge(reverseEdge.sourceNode, bandWidth,
reverseEdge.edge, pivot) reverseEdge.edge, partialPath)
} }
} }
// If the source node isn't found in the next hop map, then a path // Use the distance map to unravel the forward path from source to
// 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. // target.
pathEdges := make([]*channeldb.ChannelEdgePolicy, 0, len(next)) var pathEdges []*channeldb.ChannelEdgePolicy
currentNode := source currentNode := source
for currentNode != target { // TODO(roasbeef): assumes no cycles for currentNode != target { // TODO(roasbeef): assumes no cycles
// Determine the next hop forward using the next map. // Determine the next hop forward using the next map.
nextNode := next[currentNode] currentNodeWithDist, ok := distance[currentNode]
if !ok {
// If the node doesnt have a next hop it means we didn't find a path.
return nil, newErrf(ErrNoPathFound, "unable to find a "+
"path to destination")
}
// Add the next hop to the list of path edges. // Add the next hop to the list of path edges.
pathEdges = append(pathEdges, nextNode) pathEdges = append(pathEdges, currentNodeWithDist.nextHop)
// Advance current node. // Advance current node.
currentNode = route.Vertex(nextNode.Node.PubKeyBytes) currentNode = currentNodeWithDist.nextHop.Node.PubKeyBytes
} }
// The route is invalid if it spans more than 20 hops. The current // The route is invalid if it spans more than 20 hops. The current