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:
parent
9818f662cf
commit
b126298b2b
@ -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")
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user