diff --git a/routing/errors.go b/routing/errors.go index 0dcf2c4fc..c7c8fa371 100644 --- a/routing/errors.go +++ b/routing/errors.go @@ -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") ) diff --git a/routing/router.go b/routing/router.go index 07adf0f50..ace1646cd 100644 --- a/routing/router.go +++ b/routing/router.go @@ -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 } diff --git a/routing/router_test.go b/routing/router_test.go index 5ee592fa5..abd70a4fd 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -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) + } +}