Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preexisting, but errorCode should be exported if ErrTargetNotInNetwork is also exported for godocs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be made into a trivial follow up


// 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 @@ -540,7 +617,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 @@ -610,8 +687,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 @@ -634,8 +710,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 @@ -2271,6 +2271,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 @@ -2299,16 +2330,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