routing: limit routing info size during pathfinding

Also the max hop count check can be removed, because the real bound is
the payload size. By moving the check inside the search loop, we now
also backtrack when we hit the limit.
This commit is contained in:
Joost Jager 2019-12-16 14:22:42 +01:00
parent 513341516e
commit b760b25229
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7
4 changed files with 68 additions and 24 deletions

@ -40,6 +40,10 @@ type nodeWithDist struct {
// nextHop is the edge this route comes from. // nextHop is the edge this route comes from.
nextHop *channeldb.ChannelEdgePolicy nextHop *channeldb.ChannelEdgePolicy
// routingInfoSize is the total size requirement for the payloads field
// in the onion packet from this hop towards the final destination.
routingInfoSize uint64
} }
// 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

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/coreos/bbolt" "github.com/coreos/bbolt"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/feature" "github.com/lightningnetwork/lnd/feature"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@ -16,12 +17,6 @@ import (
) )
const ( 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 is used as a starting distance in our shortest path search.
infinity = math.MaxInt64 infinity = math.MaxInt64
@ -79,10 +74,6 @@ var (
// not exist in the graph. // not exist in the graph.
errNoPathFound = errors.New("unable to find a path to destination") errNoPathFound = errors.New("unable to find a path to destination")
// errMaxHopsExceeded is returned when a candidate path is found, but
// the length of that path exceeds HopLimit.
errMaxHopsExceeded = errors.New("potential path has too many hops")
// errInsufficientLocalBalance is returned when none of the local // errInsufficientLocalBalance is returned when none of the local
// channels have enough balance for the payment. // channels have enough balance for the payment.
errInsufficientBalance = errors.New("insufficient local balance") errInsufficientBalance = errors.New("insufficient local balance")
@ -529,6 +520,23 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
} }
} }
// Build a preliminary destination hop structure to obtain the payload
// size.
var mpp *record.MPP
if r.PaymentAddr != nil {
mpp = record.NewMPP(amt, *r.PaymentAddr)
}
finalHop := route.Hop{
AmtToForward: amt,
OutgoingTimeLock: uint32(finalHtlcExpiry),
CustomRecords: r.DestCustomRecords,
LegacyPayload: !features.HasFeature(
lnwire.TLVOnionPayloadOptional,
),
MPP: mpp,
}
// 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 so we'll manually include the target node. // advertised to the network so we'll manually include the target node.
// The target node charges no fee. Distance is set to 0, because this is // The target node charges no fee. Distance is set to 0, because this is
@ -545,6 +553,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
amountToReceive: amt, amountToReceive: amt,
incomingCltv: finalHtlcExpiry, incomingCltv: finalHtlcExpiry,
probability: 1, probability: 1,
routingInfoSize: finalHop.PayloadSize(0),
} }
// Calculate the absolute cltv limit. Use uint64 to prevent an overflow // Calculate the absolute cltv limit. Use uint64 to prevent an overflow
@ -554,6 +563,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// 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, processEdge := func(fromVertex route.Vertex,
fromFeatures *lnwire.FeatureVector,
edge *channeldb.ChannelEdgePolicy, toNodeDist *nodeWithDist) { edge *channeldb.ChannelEdgePolicy, toNodeDist *nodeWithDist) {
edgesExpanded++ edgesExpanded++
@ -674,6 +684,34 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
edge.ChannelID) edge.ChannelID)
} }
// Calculate the total routing info size if this hop were to be
// included. If we are coming from the source hop, the payload
// size is zero, because the original htlc isn't in the onion
// blob.
var payloadSize uint64
if fromVertex != source {
supportsTlv := fromFeatures.HasFeature(
lnwire.TLVOnionPayloadOptional,
)
hop := route.Hop{
AmtToForward: amountToSend,
OutgoingTimeLock: uint32(
toNodeDist.incomingCltv,
),
LegacyPayload: !supportsTlv,
}
payloadSize = hop.PayloadSize(edge.ChannelID)
}
routingInfoSize := toNodeDist.routingInfoSize + payloadSize
// Skip paths that would exceed the maximum routing info size.
if routingInfoSize > sphinx.MaxPayloadSize {
return
}
// All conditions are met and this new tentative distance is // All conditions are met and this new tentative distance is
// 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"
@ -686,6 +724,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
incomingCltv: incomingCltv, incomingCltv: incomingCltv,
probability: probability, probability: probability,
nextHop: edge, nextHop: edge,
routingInfoSize: routingInfoSize,
} }
distance[fromVertex] = withDist distance[fromVertex] = withDist
@ -796,7 +835,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(fromNode, policy, partialPath) processEdge(fromNode, fromFeatures, policy, partialPath)
} }
if nodeHeap.Len() == 0 { if nodeHeap.Len() == 0 {
@ -854,17 +893,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// findPath, and avoid using ChannelEdgePolicy altogether. // findPath, and avoid using ChannelEdgePolicy altogether.
pathEdges[len(pathEdges)-1].Node.Features = features pathEdges[len(pathEdges)-1].Node.Features = features
// 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, errMaxHopsExceeded
}
log.Debugf("Found route: probability=%v, hops=%v, fee=%v\n", log.Debugf("Found route: probability=%v, hops=%v, fee=%v\n",
distance[source].probability, numEdges, distance[source].probability, len(pathEdges),
distance[source].amountToReceive-amt) distance[source].amountToReceive-amt)
return pathEdges, nil return pathEdges, nil

@ -1393,8 +1393,19 @@ func TestNewRoutePathTooLong(t *testing.T) {
// Assert that finding a 21 hop route fails. // Assert that finding a 21 hop route fails.
node21 := ctx.keyFromAlias("node-21") node21 := ctx.keyFromAlias("node-21")
_, err = ctx.findPath(node21, payAmt) _, err = ctx.findPath(node21, payAmt)
if err != errMaxHopsExceeded { if err != errNoPathFound {
t.Fatalf("expected route too long, but got %v", err) t.Fatalf("not route error expected, but got %v", err)
}
// Assert that we can't find a 20 hop route if custom records make it
// exceed the maximum payload size.
ctx.restrictParams.DestFeatures = tlvFeatures
ctx.restrictParams.DestCustomRecords = map[uint64][]byte{
100000: bytes.Repeat([]byte{1}, 100),
}
_, err = ctx.findPath(node20, payAmt)
if err != errNoPathFound {
t.Fatalf("not route error expected, but got %v", err)
} }
} }

@ -196,7 +196,6 @@ func errorToPaymentFailure(err error) channeldb.FailureReason {
errNoTlvPayload, errNoTlvPayload,
errNoPaymentAddr, errNoPaymentAddr,
errNoPathFound, errNoPathFound,
errMaxHopsExceeded,
errPrebuiltRouteTried: errPrebuiltRouteTried:
return channeldb.FailureReasonNoRoute return channeldb.FailureReasonNoRoute