routing: weight findPath more heavily towards fees during edge relaxation

In this commit, we modify the edgeWeight function that’s used within
the findPath method to weight fees more heavily than the time lock
value at an edge. We do this in order to greedily prefer lower fees
during path finding. This is a simple stop gap in place of more complex
weighting parameters that will be investigated later.

We also modify the edge distance to use an int64 rather than a float.
Finally an additional test has been added in order to excessive this
new change. Before the commit, the test was failing as we preferred the
route with lower total time lock.
This commit is contained in:
Olaoluwa Osuntokun 2018-02-12 16:27:30 -08:00
parent ad0f5b31f6
commit 528aa67df7
No known key found for this signature in database
GPG Key ID: 964EA263DD637C21
5 changed files with 87 additions and 12 deletions

View File

@ -7,7 +7,7 @@ import "github.com/lightningnetwork/lnd/channeldb"
type nodeWithDist struct { type nodeWithDist struct {
// dist is the distance to this node from the source node in our // dist is the distance to this node from the source node in our
// current context. // current context.
dist float64 dist int64
// node is the vertex itself. This pointer can be used to explore all // node is the vertex itself. This pointer can be used to explore all
// the outgoing edges (channels) emanating from a node. // the outgoing edges (channels) emanating from a node.

View File

@ -25,7 +25,7 @@ const (
HopLimit = 20 HopLimit = 20
// infinity is used as a starting distance in our shortest path search. // infinity is used as a starting distance in our shortest path search.
infinity = math.MaxFloat64 infinity = math.MaxInt64
) )
// ChannelHop is an intermediate hop within the network with a greater // ChannelHop is an intermediate hop within the network with a greater
@ -74,7 +74,9 @@ type Hop struct {
// computeFee computes the fee to forward an HTLC of `amt` milli-satoshis over // computeFee computes the fee to forward an HTLC of `amt` milli-satoshis over
// the passed active payment channel. This value is currently computed as // the passed active payment channel. This value is currently computed as
// specified in BOLT07, but will likely change in the near future. // specified in BOLT07, but will likely change in the near future.
func computeFee(amt lnwire.MilliSatoshi, edge *ChannelHop) lnwire.MilliSatoshi { func computeFee(amt lnwire.MilliSatoshi,
edge *channeldb.ChannelEdgePolicy) lnwire.MilliSatoshi {
return edge.FeeBaseMSat + (amt*edge.FeeProportionalMillionths)/1000000 return edge.FeeBaseMSat + (amt*edge.FeeProportionalMillionths)/1000000
} }
@ -293,7 +295,7 @@ func newRoute(amtToSend lnwire.MilliSatoshi, sourceVertex Vertex,
// amount of satoshis incoming into this hop to // amount of satoshis incoming into this hop to
// properly pay the required fees. // properly pay the required fees.
prevAmount := prevHop.AmtToForward prevAmount := prevHop.AmtToForward
fee = computeFee(prevAmount, prevEdge) fee = computeFee(prevAmount, prevEdge.ChannelEdgePolicy)
// With the fee computed, we increment the total amount // With the fee computed, we increment the total amount
// as we need to pay this fee. This value represents // as we need to pay this fee. This value represents
@ -398,12 +400,28 @@ type edgeWithPrev struct {
// edgeWeight computes the weight of an edge. This value is used when searching // edgeWeight computes the weight of an edge. This value is used when searching
// for the shortest path within the channel graph between two nodes. Currently // for the shortest path within the channel graph between two nodes. Currently
// this is just 1 + the cltv delta value required at this hop, this value // a component is just 1 + the cltv delta value required at this hop, this
// should be tuned with experimental and empirical data. // value should be tuned with experimental and empirical data. We'll also
// factor in the "pure fee" through this hop, using the square of this fee as
// part of the weighting. The goal here is to bias more heavily towards fee
// ranking, and fallback to a time-lock based value in the case of a fee tie.
// //
// TODO(roasbeef): compute robust weight metric // TODO(roasbeef): compute robust weight metric
func edgeWeight(e *channeldb.ChannelEdgePolicy) float64 { func edgeWeight(amt lnwire.MilliSatoshi, e *channeldb.ChannelEdgePolicy) int64 {
return float64(1 + e.TimeLockDelta) // First, we'll compute the "pure" fee through this hop. We say pure,
// as this may not be what's ultimately paid as fees are properly
// calculated backwards, while we're going in the reverse direction.
pureFee := computeFee(amt, e)
// We'll then square the fee itself in order to more heavily weight our
// edge selection to bias towards lower fees.
feeWeight := int64(pureFee * pureFee)
// The final component is then 1 plus the timelock delta.
timeWeight := int64(1 + e.TimeLockDelta)
// The final weighting is: fee^2 + time_lock_delta.
return feeWeight + timeWeight
} }
// findPath attempts to find a path from the source node within the // findPath attempts to find a path from the source node within the
@ -514,7 +532,7 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph,
// Compute the tentative distance to this new // Compute the tentative distance to this new
// channel/edge which is the distance to our current // channel/edge which is the distance to our current
// pivot node plus the weight of this edge. // pivot node plus the weight of this edge.
tempDist := distance[pivot].dist + edgeWeight(outEdge) tempDist := distance[pivot].dist + edgeWeight(amt, outEdge)
// If this new tentative distance is better than the // If this new tentative distance is better than the
// current best known distance to this node, then we // current best known distance to this node, then we

View File

@ -390,7 +390,9 @@ func TestBasicGraphPathFinding(t *testing.T) {
// Additionally, we'll ensure that the amount to forward, and fees // Additionally, we'll ensure that the amount to forward, and fees
// computed for each hop are correct. // computed for each hop are correct.
firstHopFee := computeFee(paymentAmt, route.Hops[1].Channel) firstHopFee := computeFee(
paymentAmt, route.Hops[1].Channel.ChannelEdgePolicy,
)
if route.Hops[0].Fee != firstHopFee { if route.Hops[0].Fee != firstHopFee {
t.Fatalf("first hop fee incorrect: expected %v, got %v", t.Fatalf("first hop fee incorrect: expected %v, got %v",
firstHopFee, route.Hops[0].Fee) firstHopFee, route.Hops[0].Fee)
@ -507,7 +509,9 @@ func TestKShortestPathFinding(t *testing.T) {
paymentAmt := lnwire.NewMSatFromSatoshis(100) paymentAmt := lnwire.NewMSatFromSatoshis(100)
target := aliases["luoji"] target := aliases["luoji"]
paths, err := findPaths(nil, graph, sourceNode, target, paymentAmt) paths, err := findPaths(
nil, graph, sourceNode, target, paymentAmt, 100,
)
if err != nil { if err != nil {
t.Fatalf("unable to find paths between roasbeef and "+ t.Fatalf("unable to find paths between roasbeef and "+
"luo ji: %v", err) "luo ji: %v", err)

View File

@ -1334,3 +1334,56 @@ func TestRouterChansClosedOfflinePruneGraph(t *testing.T) {
t.Fatalf("channel was found in graph but shouldn't have been") t.Fatalf("channel was found in graph but shouldn't have been")
} }
} }
// TestFindPathFeeWeighting tests that the findPath method will properly prefer
// routes with lower fees over routes with lower time lock values. This is
// meant to exercise the fact that the internal findPath method ranks edges
// with the square of the total fee in order bias towards lower fees.
func TestFindPathFeeWeighting(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtx(startingBlockHeight, basicGraphFilePath)
defer cleanUp()
if err != nil {
t.Fatalf("unable to create router: %v", err)
}
var preImage [32]byte
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
sourceNode, err := ctx.graph.SourceNode()
if err != nil {
t.Fatalf("unable to fetch source node: %v", err)
}
ignoreVertex := make(map[Vertex]struct{})
ignoreEdge := make(map[uint64]struct{})
amt := lnwire.MilliSatoshi(100)
target := ctx.aliases["luoji"]
if target == nil {
t.Fatalf("unable to find target node")
}
// We'll now attempt a path finding attempt using this set up. Due to
// 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,
)
if err != nil {
t.Fatalf("unable to find path: %v", err)
}
// The route that was chosen should be exactly one hop, and should be
// directly to luoji.
if len(path) != 1 {
t.Fatalf("expected path length of 1, instead was: %v", len(path))
}
if path[0].Node.Alias != "luoji" {
t.Fatalf("wrong node: %v", path[0].Node.Alias)
}
}

View File

@ -135,7 +135,7 @@
"channel_id": 689530843, "channel_id": 689530843,
"channel_point": "25376aa6cb81913ad30416bd22d4083241bd6d68e811d0284d3c3a17795c458a:0", "channel_point": "25376aa6cb81913ad30416bd22d4083241bd6d68e811d0284d3c3a17795c458a:0",
"flags": 0, "flags": 0,
"expiry": 1, "expiry": 10,
"min_htlc": 1, "min_htlc": 1,
"fee_base_msat": 10, "fee_base_msat": 10,
"fee_rate": 1000, "fee_rate": 1000,