Skip to content

Commit

Permalink
Merge pull request #3749 from joostjager/extended-routing-failures
Browse files Browse the repository at this point in the history
routing: local balance check before path finding
  • Loading branch information
joostjager committed Dec 4, 2019
2 parents e28c5a1 + 9ae014e commit 883f9e5
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 186 deletions.
6 changes: 6 additions & 0 deletions channeldb/payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ const (
// or the final cltv delta or amount is incorrect.
FailureReasonPaymentDetails FailureReason = 3

// FailureReasonInsufficientBalance indicates that we didn't have enough
// balance to complete the payment.
FailureReasonInsufficientBalance FailureReason = 4

// TODO(halseth): cancel state.

// TODO(joostjager): Add failure reasons for:
Expand All @@ -122,6 +126,8 @@ func (r FailureReason) String() string {
return "error"
case FailureReasonPaymentDetails:
return "incorrect_payment_details"
case FailureReasonInsufficientBalance:
return "insufficient_balance"
}

return "unknown"
Expand Down
264 changes: 135 additions & 129 deletions lnrpc/routerrpc/router.pb.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions lnrpc/routerrpc/router.proto
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ enum PaymentState {
invalid final cltv delta)
*/
FAILED_INCORRECT_PAYMENT_DETAILS = 5;

/**
Insufficient local balance.
*/
FAILED_INSUFFICIENT_BALANCE = 6;
}


Expand Down
3 changes: 3 additions & 0 deletions lnrpc/routerrpc/router_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,9 @@ func marshallFailureReason(reason channeldb.FailureReason) (

case channeldb.FailureReasonPaymentDetails:
return PaymentState_FAILED_INCORRECT_PAYMENT_DETAILS, nil

case channeldb.FailureReasonInsufficientBalance:
return PaymentState_FAILED_INSUFFICIENT_BALANCE, nil
}

return 0, errors.New("unknown failure reason")
Expand Down
29 changes: 1 addition & 28 deletions routing/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand Down
95 changes: 85 additions & 10 deletions routing/pathfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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",
Expand Down
60 changes: 44 additions & 16 deletions routing/pathfind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
Loading

0 comments on commit 883f9e5

Please sign in to comment.