Merge pull request #3749 from joostjager/extended-routing-failures
routing: local balance check before path finding
This commit is contained in:
9 files changed
+307
-186
No files matched your search
+1
-28
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user