routing: implement route failure fallback in SendPayment

This commit adds payment route failure fallback to SendPayment. By
this, we mean that we now take all the possible routes found during
path finding and try them in series. Either a route fails and we move
onto the next one, or the route is successful and we terminate early.

With this commit, sending payments using lnd is now much more robust as
if there exists an eligible route with sufficient capacity, it will be
utilized.
This commit is contained in:
Olaoluwa Osuntokun 2017-03-20 18:58:21 -07:00
parent 9818f662cf
commit b126298b2b
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2
3 changed files with 129 additions and 31 deletions

@ -8,6 +8,12 @@ var (
ErrNoPathFound = errors.New("unable to find a path to " + ErrNoPathFound = errors.New("unable to find a path to " +
"destination") "destination")
// 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 = errors.New("unable to find a eligible route to " +
"the destination")
// ErrInsufficientCapacity is returned when a path if found, yet the // ErrInsufficientCapacity is returned when a path if found, yet the
// capacity of one of the channels in the path is insufficient to carry // capacity of one of the channels in the path is insufficient to carry
// the payment. // the payment.
@ -18,6 +24,8 @@ var (
// the length of that path exceeds HopLimit. // the length of that path exceeds HopLimit.
ErrMaxHopsExceeded = errors.New("potential path has too many hops") ErrMaxHopsExceeded = errors.New("potential path has too many hops")
// ErrTargetNotInNetwork is returned when a // 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 = errors.New("target not found") ErrTargetNotInNetwork = errors.New("target not found")
) )

@ -3,7 +3,6 @@ package routing
import ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"sort" "sort"
"sync" "sync"
@ -1221,43 +1220,70 @@ type LightningPayment struct {
// within the network to reach the destination. Additionally, the payment // within the network to reach the destination. Additionally, the payment
// preimage will also be returned. // preimage will also be returned.
func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, *Route, error) { func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, *Route, error) {
var ( log.Tracef("Dispatching route for lightning payment: %v",
err error newLogClosure(func() string {
preImage [32]byte return spew.Sdump(payment)
}),
) )
// Query the graph for a potential path to the destination node that // TODO(roasbeef): consult KSP cache before dispatching
// can support our payment amount. If a path is ultimately unavailable,
// then an error will be returned.
route, err := r.FindRoute(payment.Target, payment.Amount)
if err != nil {
return preImage, nil, err
}
log.Tracef("Selected route for payment: %#v", route)
// Generate the raw encoded sphinx packet to be included along with the var (
// htlcAdd message that we send directly to the switch. sendError error
sphinxPacket, err := generateSphinxPacket(route, payment.PaymentHash[:]) preImage [32]byte
)
// Query the graph for a set of potential routes to the destination
// node that can support our payment amount. If no such routes can be
// found then an error will be returned.
routes, err := r.FindRoutes(payment.Target, payment.Amount)
if err != nil { if err != nil {
return preImage, nil, err return preImage, nil, err
} }
// Craft an HTLC packet to send to the layer 2 switch. The metadata // For each eligible path, we'll attempt to successfully send our
// within this packet will be used to route the payment through the // target payment using the multi-hop route. We'll try each route
// network, starting with the first-hop. // serially until either once succeeds, or we've exhausted our set of
htlcAdd := &lnwire.UpdateAddHTLC{ // available paths.
Amount: route.TotalAmount, for _, route := range routes {
PaymentHash: payment.PaymentHash, log.Tracef("Attempting to send payment %x, using route: %#v",
} payment.PaymentHash, newLogClosure(func() string {
copy(htlcAdd.OnionBlob[:], sphinxPacket) return spew.Sdump(route)
}),
)
// Attempt to send this payment through the network to complete the // Generate the raw encoded sphinx packet to be included along
// payment. If this attempt fails, then we'll bail our early. // with the htlcAdd message that we send directly to the
firstHop := route.Hops[0].Channel.Node.PubKey // switch.
preImage, err = r.cfg.SendToSwitch(firstHop, htlcAdd) sphinxPacket, err := generateSphinxPacket(route, payment.PaymentHash[:])
if err != nil { if err != nil {
return preImage, nil, err return preImage, nil, err
}
// Craft an HTLC packet to send to the layer 2 switch. The
// metadata within this packet will be used to route the
// payment through the network, starting with the first-hop.
htlcAdd := &lnwire.UpdateAddHTLC{
Amount: route.TotalAmount,
PaymentHash: payment.PaymentHash,
}
copy(htlcAdd.OnionBlob[:], sphinxPacket)
// Attempt to send this payment through the network to complete
// the payment. If this attempt fails, then we'll continue on
// to the next available route.
firstHop := route.Hops[0].Channel.Node.PubKey
preImage, sendError = r.cfg.SendToSwitch(firstHop, htlcAdd)
if sendError != nil {
log.Errorf("Attempt to send payment %x failed: %v",
payment.PaymentHash, err)
continue
}
return preImage, route, nil
} }
return preImage, route, nil // If we're unable to successfully make a payment using any of the
// routes we've found, then return an error.
return [32]byte{}, nil, sendError
} }

@ -146,3 +146,67 @@ func TestFindRoutesFeeSorting(t *testing.T) {
} }
} }
// TestSendPaymentRouteFailureFallback tests that when sending a payment, if
// one of the target routes is seen as unavailable, then the next route in the
// queue is used instead. This process should continue until either a payment
// succeeds, or all routes have been exhausted.
func TestSendPaymentRouteFailureFallback(t *testing.T) {
const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtx(startingBlockHeight, basicGraphFilePath)
defer cleanUp()
if err != nil {
t.Fatalf("unable to create router: %v", err)
}
// Craft a LightningPayment struct that'll send a payment from roasbeef
// to luo ji for 100 satoshis.
var payHash [32]byte
payment := LightningPayment{
Target: ctx.aliases["luoji"],
Amount: btcutil.Amount(1000),
PaymentHash: payHash,
}
var preImage [32]byte
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
// We'll modify the SendToSwitch method that's been set within the
// router's configuration to ignore the path that has luo ji as the
// first hop. This should force the router to instead take the
// available two hop path (through satoshi).
ctx.router.cfg.SendToSwitch = func(n *btcec.PublicKey,
_ *lnwire.UpdateAddHTLC) ([32]byte, error) {
if ctx.aliases["luoji"].IsEqual(n) {
return [32]byte{}, errors.New("send error")
}
return preImage, nil
}
// Send off the payment request to the router, route through satoshi
// should've been selected as a fall back and succeeded correctly.
paymentPreImage, route, err := ctx.router.SendPayment(&payment)
if err != nil {
t.Fatalf("unable to send payment: %v", err)
}
// The route selected should have two hops
if len(route.Hops) != 2 {
t.Fatalf("incorrect route length: expected %v got %v", 2,
len(route.Hops))
}
// The preimage should match up with the once created above.
if !bytes.Equal(paymentPreImage[:], preImage[:]) {
t.Fatalf("incorrect preimage used: expected %x got %x",
preImage[:], paymentPreImage[:])
}
// The route should have satoshi as the first hop.
if route.Hops[0].Channel.Node.Alias != "satoshi" {
t.Fatalf("route should go through satoshi as first hop, "+
"instead passes through: %v",
route.Hops[0].Channel.Node.Alias)
}
}