Merge pull request #3749 from joostjager/extended-routing-failures

routing: local balance check before path finding
This commit is contained in:
Joost Jager authored and GitHub committed 2019-12-04 12:17:17 +01:00
commit 883f9e5f9a
9 files changed
+307 -186

No files matched your search

+1 -28
View File
@@ -7,28 +7,10 @@ import "github.com/go-errors/errors"
type errorCode uint8
const (
// ErrNoPathFound is returned when a path to the target destination
// does not exist in the graph.
ErrNoPathFound errorCode = iota
// ErrNoRouteFound is returned when the router is unable to find a
// valid route to the target destination after fees and time-lock
// limitations are factored in.
ErrNoRouteFound
// ErrInsufficientCapacity is returned when a path if found, yet the
// capacity of one of the channels in the path is insufficient to carry
// the payment.
ErrInsufficientCapacity
// ErrMaxHopsExceeded is returned when a candidate path is found, but
// the length of that path exceeds HopLimit.
ErrMaxHopsExceeded
// ErrTargetNotInNetwork is returned when the target of a path-finding
// or payment attempt isn't known to be within the current version of
// the channel graph.
ErrTargetNotInNetwork
ErrTargetNotInNetwork errorCode = iota
// ErrOutdated is returned when the routing update already have
// been applied, or a newer update is already known.
@@ -39,18 +21,9 @@ const (
// announcement was given for node not found in any channel.
ErrIgnored
// ErrRejected is returned if the update is for a channel ID that was
// previously added to the reject cache because of an invalid update
// was attempted to be processed.
ErrRejected
// ErrPaymentAttemptTimeout is an error that indicates that a payment
// attempt timed out before we were able to successfully route an HTLC.
ErrPaymentAttemptTimeout
// ErrFeeLimitExceeded is returned when the total fees of a route exceed
// the user-specified fee limit.
ErrFeeLimitExceeded
)
// routerError is a structure that represent the error inside the routing package,
+85 -10
View File
@@ -2,13 +2,13 @@ package routing
import (
"container/heap"
"errors"
"fmt"
"math"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
@@ -63,6 +63,23 @@ var (
// DefaultAprioriHopProbability is the default a priori probability for
// a hop.
DefaultAprioriHopProbability = float64(0.6)
// errNoTlvPayload is returned when the destination hop does not support
// a tlv payload.
errNoTlvPayload = errors.New("destination hop doesn't " +
"understand new TLV payloads")
// errNoPathFound is returned when a path to the target destination does
// not exist in the graph.
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
// channels have enough balance for the payment.
errInsufficientBalance = errors.New("insufficient local balance")
)
// edgePolicyWithSource is a helper struct to keep track of the source node
@@ -292,6 +309,50 @@ type PathFindingConfig struct {
MinProbability float64
}
// getMaxOutgoingAmt returns the maximum available balance in any of the
// channels of the given node.
func getMaxOutgoingAmt(node route.Vertex, outgoingChan *uint64,
g *graphParams, tx *bbolt.Tx) (lnwire.MilliSatoshi, error) {
var max lnwire.MilliSatoshi
cb := func(_ *bbolt.Tx, edgeInfo *channeldb.ChannelEdgeInfo, outEdge,
_ *channeldb.ChannelEdgePolicy) error {
if outEdge == nil {
return nil
}
chanID := outEdge.ChannelID
// Enforce outgoing channel restriction.
if outgoingChan != nil && chanID != *outgoingChan {
return nil
}
bandwidth, ok := g.bandwidthHints[chanID]
// If the bandwidth is not available for whatever reason, don't
// fail the pathfinding early.
if !ok {
max = lnwire.MaxMilliSatoshi
return nil
}
if bandwidth > max {
max = bandwidth
}
return nil
}
// Iterate over all channels of the to node.
err := g.graph.ForEachNodeChannel(tx, node[:], cb)
if err != nil {
return 0, err
}
return max, err
}
// 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
@@ -319,7 +380,13 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
"time=%v", nodesVisited, edgesExpanded, timeElapsed)
}()
var err error
// Get source node outside of the pathfinding tx, to prevent a deadlock.
selfNode, err := g.graph.SourceNode()
if err != nil {
return nil, err
}
self := selfNode.PubKeyBytes
tx := g.tx
if tx == nil {
tx, err = g.graph.Database().Begin(false)
@@ -347,12 +414,23 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
lnwire.TLVOnionPayloadOptional,
)
if !supportsTLV {
return nil, fmt.Errorf("destination hop doesn't " +
"understand new TLV paylods")
return nil, errNoTlvPayload
}
}
}
// If we are routing from ourselves, check that we have enough local
// balance available.
if source == self {
max, err := getMaxOutgoingAmt(self, r.OutgoingChannelID, g, tx)
if err != nil {
return nil, err
}
if max < amt {
return nil, errInsufficientBalance
}
}
// 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.
@@ -363,7 +441,6 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
additionalEdgesWithSrc := make(map[route.Vertex][]*edgePolicyWithSource)
for vertex, outgoingEdgePolicies := range g.additionalEdges {
// Build reverse lookup to find incoming edges. Needed because
// search is taken place from target to source.
for _, outgoingEdgePolicy := range outgoingEdgePolicies {
@@ -552,7 +629,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
pivot := partialPath.node
// Create unified policies for all incoming connections.
u := newUnifiedPolicies(source, pivot, r.OutgoingChannelID)
u := newUnifiedPolicies(self, pivot, r.OutgoingChannelID)
err := u.addGraphPolicies(g.graph, tx)
if err != nil {
@@ -622,8 +699,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
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")
return nil, errNoPathFound
}
// Add the next hop to the list of path edges.
@@ -646,8 +722,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// hops, then it's invalid.
numEdges := len(pathEdges)
if numEdges > HopLimit {
return nil, newErr(ErrMaxHopsExceeded, "potential path has "+
"too many hops")
return nil, errMaxHopsExceeded
}
log.Debugf("Found route: probability=%v, hops=%v, fee=%v\n",
+44 -16
View File
@@ -1269,7 +1269,7 @@ func TestPathNotAvailable(t *testing.T) {
noRestrictions, testPathFindingConfig,
sourceNode.PubKeyBytes, unknownNode, 100,
)
if !IsError(err, ErrNoPathFound) {
if err != errNoPathFound {
t.Fatalf("path shouldn't have been found: %v", err)
}
}
@@ -1306,7 +1306,7 @@ func TestPathInsufficientCapacity(t *testing.T) {
noRestrictions, testPathFindingConfig,
sourceNode.PubKeyBytes, target, payAmt,
)
if !IsError(err, ErrNoPathFound) {
if err != errNoPathFound {
t.Fatalf("graph shouldn't be able to support payment: %v", err)
}
}
@@ -1339,7 +1339,7 @@ func TestRouteFailMinHTLC(t *testing.T) {
noRestrictions, testPathFindingConfig,
sourceNode.PubKeyBytes, target, payAmt,
)
if !IsError(err, ErrNoPathFound) {
if err != errNoPathFound {
t.Fatalf("graph shouldn't be able to support payment: %v", err)
}
}
@@ -1403,7 +1403,7 @@ func TestRouteFailMaxHTLC(t *testing.T) {
// We'll now attempt to route through that edge with a payment above
// 100k msat, which should fail.
_, err = ctx.findPath(target, payAmt)
if !IsError(err, ErrNoPathFound) {
if err != errNoPathFound {
t.Fatalf("graph shouldn't be able to support payment: %v", err)
}
}
@@ -1491,7 +1491,7 @@ func TestRouteFailDisabledEdge(t *testing.T) {
noRestrictions, testPathFindingConfig,
sourceNode.PubKeyBytes, target, payAmt,
)
if !IsError(err, ErrNoPathFound) {
if err != errNoPathFound {
t.Fatalf("graph shouldn't be able to support payment: %v", err)
}
}
@@ -1549,7 +1549,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) {
noRestrictions, testPathFindingConfig,
sourceNode.PubKeyBytes, target, payAmt,
)
if !IsError(err, ErrNoPathFound) {
if err != errNoPathFound {
t.Fatalf("graph shouldn't be able to support payment: %v", err)
}
@@ -1971,7 +1971,7 @@ func testCltvLimit(t *testing.T, limit uint32, expectedChannel uint64) {
path, err := ctx.findPath(target, paymentAmt)
if expectedChannel == 0 {
// Finish test if we expect no route.
if IsError(err, ErrNoPathFound) {
if err == errNoPathFound {
return
}
t.Fatal("expected no path to be found")
@@ -2137,7 +2137,7 @@ func testProbabilityRouting(t *testing.T, p10, p11, p20, minProbability float64,
path, err := ctx.findPath(target, paymentAmt)
if expectedChan == 0 {
if err == nil || !IsError(err, ErrNoPathFound) {
if err != errNoPathFound {
t.Fatalf("expected no path found, but got %v", err)
}
return
@@ -2337,6 +2337,37 @@ func TestRouteToSelf(t *testing.T) {
ctx.assertPath(path, []uint64{1, 3, 2})
}
// TestInsufficientBalance tests that a dedicated error is returned for
// insufficient local balance.
func TestInsufficientBalance(t *testing.T) {
t.Parallel()
testChannels := []*testChannel{
symmetricTestChannel("source", "target", 100000, &testChannelPolicy{
Expiry: 144,
FeeBaseMsat: 500,
}, 1),
}
ctx := newPathFindingTestContext(t, testChannels, "source")
defer ctx.cleanup()
paymentAmt := lnwire.NewMSatFromSatoshis(100)
target := ctx.keyFromAlias("target")
ctx.graphParams.bandwidthHints = map[uint64]lnwire.MilliSatoshi{
1: lnwire.NewMSatFromSatoshis(50),
}
// Find the best path to self. We expect this to be source->a->source,
// because a charges the lowest forwarding fee.
_, err := ctx.findPath(target, paymentAmt)
if err != errInsufficientBalance {
t.Fatalf("expected insufficient balance error, but got: %v",
err)
}
}
type pathFindingTestContext struct {
t *testing.T
graphParams graphParams
@@ -2365,16 +2396,13 @@ func newPathFindingTestContext(t *testing.T, testChannels []*testChannel,
t: t,
testGraphInstance: testGraphInstance,
source: route.Vertex(sourceNode.PubKeyBytes),
pathFindingConfig: *testPathFindingConfig,
graphParams: graphParams{
graph: testGraphInstance.graph,
},
restrictParams: *noRestrictions,
}
ctx.pathFindingConfig = *testPathFindingConfig
ctx.graphParams.graph = testGraphInstance.graph
ctx.restrictParams.FeeLimit = noFeeLimit
ctx.restrictParams.ProbabilitySource = noProbabilitySource
ctx.restrictParams.CltvLimit = math.MaxUint32
return ctx
}
+20 -1
View File
@@ -186,6 +186,22 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) {
}
// errorToPaymentFailure takes a path finding error and converts it into a
// payment-level failure.
func errorToPaymentFailure(err error) channeldb.FailureReason {
switch err {
case errNoTlvPayload, errNoPathFound, errMaxHopsExceeded,
errPrebuiltRouteTried:
return channeldb.FailureReasonNoRoute
case errInsufficientBalance:
return channeldb.FailureReasonInsufficientBalance
}
return channeldb.FailureReasonError
}
// createNewPaymentAttempt creates and stores a new payment attempt to the
// database.
func (p *paymentLifecycle) createNewPaymentAttempt() (lnwire.ShortChannelID,
@@ -230,11 +246,14 @@ func (p *paymentLifecycle) createNewPaymentAttempt() (lnwire.ShortChannelID,
log.Warnf("Failed to find route for payment %x: %v",
p.payment.PaymentHash, err)
// Convert error to payment-level failure.
failure := errorToPaymentFailure(err)
// If we're unable to successfully make a payment using
// any of the routes we've found, then mark the payment
// as permanently failed.
saveErr := p.router.cfg.Control.Fail(
p.payment.PaymentHash, channeldb.FailureReasonNoRoute,
p.payment.PaymentHash, failure,
)
if saveErr != nil {
return lnwire.ShortChannelID{}, nil, saveErr
+8 -2
View File
@@ -1,7 +1,7 @@
package routing
import (
"fmt"
"errors"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwire"
@@ -12,6 +12,12 @@ import (
// to prevent an HTLC being failed if some blocks are mined while it's in-flight.
const BlockPadding uint16 = 3
var (
// errPrebuiltRouteTried is returned when the single pre-built route
// failed and there is nothing more we can do.
errPrebuiltRouteTried = errors.New("pre-built route already tried")
)
// PaymentSession is used during SendPayment attempts to provide routes to
// attempt. It also defines methods to give the PaymentSession additional
// information learned during the previous attempts.
@@ -66,7 +72,7 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
// If the pre-built route has been tried already, the payment session is
// over.
case p.preBuiltRoute != nil:
return nil, fmt.Errorf("pre-built route already tried")
return nil, errPrebuiltRouteTried
}
// Add BlockPadding to the finalCltvDelta so that the receiving node