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

View File

@ -8,6 +8,12 @@ var (
ErrNoPathFound = errors.New("unable to find a path to " +
"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
// capacity of one of the channels in the path is insufficient to carry
// the payment.
@ -18,6 +24,8 @@ var (
// the length of that path exceeds HopLimit.
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")
)

View File

@ -3,7 +3,6 @@ package routing
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"sort"
"sync"
@ -1221,43 +1220,70 @@ type LightningPayment struct {
// within the network to reach the destination. Additionally, the payment
// preimage will also be returned.
func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, *Route, error) {
var (
err error
preImage [32]byte
log.Tracef("Dispatching route for lightning payment: %v",
newLogClosure(func() string {
return spew.Sdump(payment)
}),
)
// Query the graph for a potential path to the destination node that
// 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)
// TODO(roasbeef): consult KSP cache before dispatching
// Generate the raw encoded sphinx packet to be included along with the
// htlcAdd message that we send directly to the switch.
sphinxPacket, err := generateSphinxPacket(route, payment.PaymentHash[:])
var (
sendError error
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 {
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)
// For each eligible path, we'll attempt to successfully send our
// target payment using the multi-hop route. We'll try each route
// serially until either once succeeds, or we've exhausted our set of
// available paths.
for _, route := range routes {
log.Tracef("Attempting to send payment %x, using route: %#v",
payment.PaymentHash, newLogClosure(func() string {
return spew.Sdump(route)
}),
)
// Attempt to send this payment through the network to complete the
// payment. If this attempt fails, then we'll bail our early.
firstHop := route.Hops[0].Channel.Node.PubKey
preImage, err = r.cfg.SendToSwitch(firstHop, htlcAdd)
if err != nil {
return preImage, nil, err
// Generate the raw encoded sphinx packet to be included along
// with the htlcAdd message that we send directly to the
// switch.
sphinxPacket, err := generateSphinxPacket(route, payment.PaymentHash[:])
if err != nil {
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
}

View File

@ -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)
}
}