diff --git a/routing/mock_test.go b/routing/mock_test.go index 03aa2923c..b84954283 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -1,8 +1,15 @@ package routing import ( + "fmt" + "sync" + + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/htlcswitch" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/zpay32" ) type mockPaymentAttemptDispatcher struct { @@ -62,3 +69,217 @@ func (m *mockPaymentAttemptDispatcher) setPaymentResult( m.onPayment = f } + +type mockPaymentSessionSource struct { + routes []*route.Route +} + +var _ PaymentSessionSource = (*mockPaymentSessionSource)(nil) + +func (m *mockPaymentSessionSource) NewPaymentSession(routeHints [][]zpay32.HopHint, + target route.Vertex) (PaymentSession, error) { + + return &mockPaymentSession{m.routes}, nil +} + +func (m *mockPaymentSessionSource) NewPaymentSessionForRoute( + preBuiltRoute *route.Route) PaymentSession { + return nil +} + +func (m *mockPaymentSessionSource) NewPaymentSessionEmpty() PaymentSession { + return &mockPaymentSession{} +} + +type mockPaymentSession struct { + routes []*route.Route +} + +var _ PaymentSession = (*mockPaymentSession)(nil) + +func (m *mockPaymentSession) RequestRoute(payment *LightningPayment, + height uint32, finalCltvDelta uint16) (*route.Route, error) { + + if len(m.routes) == 0 { + return nil, fmt.Errorf("no routes") + } + + r := m.routes[0] + m.routes = m.routes[1:] + + return r, nil +} + +func (m *mockPaymentSession) ReportVertexFailure(v route.Vertex) {} + +func (m *mockPaymentSession) ReportEdgeFailure(e *EdgeLocator) {} + +func (m *mockPaymentSession) ReportEdgePolicyFailure(errSource route.Vertex, failedEdge *EdgeLocator) { +} + +type mockPayer struct { + sendResult chan error + paymentResultErr chan error + paymentResult chan *htlcswitch.PaymentResult + quit chan struct{} +} + +var _ PaymentAttemptDispatcher = (*mockPayer)(nil) + +func (m *mockPayer) SendHTLC(_ lnwire.ShortChannelID, + paymentID uint64, + _ *lnwire.UpdateAddHTLC) error { + + select { + case res := <-m.sendResult: + return res + case <-m.quit: + return fmt.Errorf("test quitting") + } + +} + +func (m *mockPayer) GetPaymentResult(paymentID uint64, _ htlcswitch.ErrorDecrypter) ( + <-chan *htlcswitch.PaymentResult, error) { + + select { + case res := <-m.paymentResult: + resChan := make(chan *htlcswitch.PaymentResult, 1) + resChan <- res + return resChan, nil + case err := <-m.paymentResultErr: + return nil, err + case <-m.quit: + return nil, fmt.Errorf("test quitting") + } +} + +type initArgs struct { + c *channeldb.PaymentCreationInfo +} + +type registerArgs struct { + a *channeldb.PaymentAttemptInfo +} + +type successArgs struct { + preimg lntypes.Preimage +} + +type failArgs struct { +} + +type mockControlTower struct { + inflights map[lntypes.Hash]channeldb.InFlightPayment + successful map[lntypes.Hash]struct{} + + init chan initArgs + register chan registerArgs + success chan successArgs + fail chan failArgs + fetchInFlight chan struct{} + + sync.Mutex +} + +var _ channeldb.ControlTower = (*mockControlTower)(nil) + +func makeMockControlTower() *mockControlTower { + return &mockControlTower{ + inflights: make(map[lntypes.Hash]channeldb.InFlightPayment), + successful: make(map[lntypes.Hash]struct{}), + } +} + +func (m *mockControlTower) InitPayment(phash lntypes.Hash, + c *channeldb.PaymentCreationInfo) error { + + m.Lock() + defer m.Unlock() + + if m.init != nil { + m.init <- initArgs{c} + } + + if _, ok := m.successful[phash]; ok { + return fmt.Errorf("already successful") + } + + _, ok := m.inflights[phash] + if ok { + return fmt.Errorf("in flight") + } + + m.inflights[phash] = channeldb.InFlightPayment{ + Info: c, + } + + return nil +} + +func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash, + a *channeldb.PaymentAttemptInfo) error { + + m.Lock() + defer m.Unlock() + + if m.register != nil { + m.register <- registerArgs{a} + } + + p, ok := m.inflights[phash] + if !ok { + return fmt.Errorf("not in flight") + } + + p.Attempt = a + m.inflights[phash] = p + + return nil +} + +func (m *mockControlTower) Success(phash lntypes.Hash, + preimg lntypes.Preimage) error { + + m.Lock() + defer m.Unlock() + + if m.success != nil { + m.success <- successArgs{preimg} + } + + delete(m.inflights, phash) + m.successful[phash] = struct{}{} + return nil +} + +func (m *mockControlTower) Fail(phash lntypes.Hash) error { + + m.Lock() + defer m.Unlock() + + if m.fail != nil { + m.fail <- failArgs{} + } + + delete(m.inflights, phash) + return nil +} + +func (m *mockControlTower) FetchInFlightPayments() ( + []*channeldb.InFlightPayment, error) { + + m.Lock() + defer m.Unlock() + + if m.fetchInFlight != nil { + m.fetchInFlight <- struct{}{} + } + + var fl []*channeldb.InFlightPayment + for _, ifl := range m.inflights { + fl = append(fl, &ifl) + } + + return fl, nil +} diff --git a/routing/router_test.go b/routing/router_test.go index 0380475da..d1cafddd6 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -731,7 +731,9 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) { }) // Once again, Roasbeef should route around Goku since they disagree - // w.r.t to the block height, and instead go through Pham Nuwen. + // w.r.t to the block height, and instead go through Pham Nuwen. We + // flip a bit in the payment hash to allow resending this payment. + payment.PaymentHash[1] ^= 1 paymentPreImage, rt, err = ctx.router.SendPayment(&payment) if err != nil { t.Fatalf("unable to send payment: %v", err) @@ -898,6 +900,8 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { return preImage, nil }) + // We flip a bit in the payment hash to allow resending this payment. + payment.PaymentHash[1] ^= 1 paymentPreImage, rt, err = ctx.router.SendPayment(&payment) if err != nil { t.Fatalf("unable to send payment: %v", err) @@ -2507,35 +2511,588 @@ func assertChannelsPruned(t *testing.T, graph *channeldb.ChannelGraph, } } -type mockControlTower struct{} +// TestRouterPaymentStateMachine tests that the router interacts as expected +// with the ControlTower during a payment lifecycle, such that it payment +// attempts are not sent twice to the switch, and results are handled after a +// restart. +func TestRouterPaymentStateMachine(t *testing.T) { + t.Parallel() -var _ channeldb.ControlTower = (*mockControlTower)(nil) + const startingBlockHeight = 101 -func makeMockControlTower() *mockControlTower { - return &mockControlTower{} -} - -func (m *mockControlTower) InitPayment(lntypes.Hash, - *channeldb.PaymentCreationInfo) error { - return nil -} - -func (m *mockControlTower) RegisterAttempt(lntypes.Hash, - *channeldb.PaymentAttemptInfo) error { - return nil -} - -func (m *mockControlTower) Success(paymentHash lntypes.Hash, - preimg lntypes.Preimage) error { - return nil -} - -func (m *mockControlTower) Fail(paymentHash lntypes.Hash) error { - return nil -} - -func (m *mockControlTower) FetchInFlightPayments() ( - []*channeldb.InFlightPayment, error) { - - return nil, nil + // Setup two simple channels such that we can mock sending along this + // route. + chanCapSat := btcutil.Amount(100000) + testChannels := []*testChannel{ + symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{ + Expiry: 144, + FeeRate: 400, + MinHTLC: 1, + MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat), + }, 1), + symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{ + Expiry: 144, + FeeRate: 400, + MinHTLC: 1, + MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat), + }, 2), + } + + testGraph, err := createTestGraphFromChannels(testChannels) + if err != nil { + t.Fatalf("unable to create graph: %v", err) + } + defer testGraph.cleanUp() + + hop1 := testGraph.aliasMap["b"] + hop2 := testGraph.aliasMap["c"] + hops := []*route.Hop{ + { + ChannelID: 1, + PubKeyBytes: hop1, + }, + { + ChannelID: 2, + PubKeyBytes: hop2, + }, + } + + // We create a simple route that we will supply every time the router + // requests one. + rt, err := route.NewRouteFromHops( + lnwire.MilliSatoshi(10000), 100, testGraph.aliasMap["a"], hops, + ) + if err != nil { + t.Fatalf("unable to create route: %v", err) + } + + // A payment state machine test case consists of several ordered steps, + // that we use for driving the scenario. + type testCase struct { + // steps is a list of steps to perform during the testcase. + steps []string + + // routes is the sequence of routes we will provide to the + // router when it requests a new route. + routes []*route.Route + } + + const ( + // routerInitPayment is a test step where we expect the router + // to call the InitPayment method on the control tower. + routerInitPayment = "Router:init-payment" + + // routerRegisterAttempt is a test step where we expect the + // router to call the RegisterAttempt method on the control + // tower. + routerRegisterAttempt = "Router:register-attempt" + + // routerSuccess is a test step where we expect the router to + // call the Success method on the control tower. + routerSuccess = "Router:success" + + // routerFail is a test step where we expect the router to call + // the Fail method on the control tower. + routerFail = "Router:fail" + + // sendToSwitchSuccess is a step where we expect the router to + // call send the payment attempt to the switch, and we will + // respond with a non-error, indicating that the payment + // attempt was successfully forwarded. + sendToSwitchSuccess = "SendToSwitch:success" + + // getPaymentResultSuccess is a test step where we expect the + // router to call the GetPaymentResult method, and we will + // respond with a successful payment result. + getPaymentResultSuccess = "GetPaymentResult:success" + + // getPaymentResultFailure is a test step where we expect the + // router to call the GetPaymentResult method, and we will + // respond with a forwarding error. + getPaymentResultFailure = "GetPaymentResult:failure" + + // resendPayment is a test step where we manually try to resend + // the same payment, making sure the router responds with an + // error indicating that it is alreayd in flight. + resendPayment = "ResendPayment" + + // startRouter is a step where we manually start the router, + // used to test that it automatically will resume payments at + // startup. + startRouter = "StartRouter" + + // stopRouter is a test step where we manually make the router + // shut down. + stopRouter = "StopRouter" + + // paymentSuccess is a step where assert that we receive a + // successful result for the original payment made. + paymentSuccess = "PaymentSuccess" + + // paymentError is a step where assert that we receive an error + // for the original payment made. + paymentError = "PaymentError" + + // resentPaymentSuccess is a step where assert that we receive + // a successful result for a payment that was resent. + resentPaymentSuccess = "ResentPaymentSuccess" + + // resentPaymentError is a step where assert that we receive an + // error for a payment that was resent. + resentPaymentError = "ResentPaymentError" + ) + + tests := []testCase{ + { + // Tests a normal payment flow that succeeds. + steps: []string{ + routerInitPayment, + routerRegisterAttempt, + sendToSwitchSuccess, + getPaymentResultSuccess, + routerSuccess, + paymentSuccess, + }, + routes: []*route.Route{rt}, + }, + { + // A payment flow with a failure on the first attempt, + // but that succeeds on the second attempt. + steps: []string{ + routerInitPayment, + routerRegisterAttempt, + sendToSwitchSuccess, + + // Make the first sent attempt fail. + getPaymentResultFailure, + + // The router should retry. + routerRegisterAttempt, + sendToSwitchSuccess, + + // Make the second sent attempt succeed. + getPaymentResultSuccess, + routerSuccess, + paymentSuccess, + }, + routes: []*route.Route{rt, rt}, + }, + { + // A payment that fails on the first attempt, and has + // only one route available to try. It will therefore + // fail permanently. + steps: []string{ + routerInitPayment, + routerRegisterAttempt, + sendToSwitchSuccess, + + // Make the first sent attempt fail. + getPaymentResultFailure, + + // Since there are no more routes to try, the + // payment should fail. + routerFail, + paymentError, + }, + routes: []*route.Route{rt}, + }, + { + // We expect the payment to fail immediately if we have + // no routes to try. + steps: []string{ + routerInitPayment, + routerFail, + paymentError, + }, + routes: []*route.Route{}, + }, + { + // A normal payment flow, where we attempt to resend + // the same payment after each step. This ensures that + // the router don't attempt to resend a payment already + // in flight. + steps: []string{ + routerInitPayment, + routerRegisterAttempt, + + // Manually resend the payment, the router + // should attempt to init with the control + // tower, but fail since it is already in + // flight. + resendPayment, + routerInitPayment, + resentPaymentError, + + // The original payment should proceed as + // normal. + sendToSwitchSuccess, + + // Again resend the payment and assert it's not + // allowed. + resendPayment, + routerInitPayment, + resentPaymentError, + + // Notify about a success for the original + // payment. + getPaymentResultSuccess, + routerSuccess, + + // Now that the original payment finished, + // resend it again to ensure this is not + // allowed. + resendPayment, + routerInitPayment, + resentPaymentError, + paymentSuccess, + }, + routes: []*route.Route{rt}, + }, + { + // Tests that the router is able to handle the + // receieved payment result after a restart. + steps: []string{ + routerInitPayment, + routerRegisterAttempt, + sendToSwitchSuccess, + + // Shut down the router. The original caller + // should get notified about this. + stopRouter, + paymentError, + + // Start the router again, and ensure the + // router registers the success with the + // control tower. + startRouter, + getPaymentResultSuccess, + routerSuccess, + }, + routes: []*route.Route{rt}, + }, + { + // Tests that we are allowed to resend a payment after + // it has permanently failed. + steps: []string{ + routerInitPayment, + routerRegisterAttempt, + sendToSwitchSuccess, + + // Resending the payment at this stage should + // not be allowed. + resendPayment, + routerInitPayment, + resentPaymentError, + + // Make the first attempt fail. + getPaymentResultFailure, + routerFail, + + // Since we have no more routes to try, the + // original payment should fail. + paymentError, + + // Now resend the payment again. This should be + // allowed, since the payment has failed. + resendPayment, + routerInitPayment, + routerRegisterAttempt, + sendToSwitchSuccess, + getPaymentResultSuccess, + routerSuccess, + resentPaymentSuccess, + }, + routes: []*route.Route{rt}, + }, + } + + // Create a mock control tower with channels set up, that we use to + // synchronize and listen for events. + control := makeMockControlTower() + control.init = make(chan initArgs) + control.register = make(chan registerArgs) + control.success = make(chan successArgs) + control.fail = make(chan failArgs) + control.fetchInFlight = make(chan struct{}) + + quit := make(chan struct{}) + defer close(quit) + + // setupRouter is a helper method that creates and starts the router in + // the desired configuration for this test. + setupRouter := func() (*ChannelRouter, chan error, + chan *htlcswitch.PaymentResult, chan error) { + + chain := newMockChain(startingBlockHeight) + chainView := newMockChainView(chain) + + // We set uo the use the following channels and a mock Payer to + // synchonize with the interaction to the Switch. + sendResult := make(chan error) + paymentResultErr := make(chan error) + paymentResult := make(chan *htlcswitch.PaymentResult) + + payer := &mockPayer{ + sendResult: sendResult, + paymentResult: paymentResult, + paymentResultErr: paymentResultErr, + } + + router, err := New(Config{ + Graph: testGraph.graph, + Chain: chain, + ChainView: chainView, + Control: control, + MissionControl: &mockPaymentSessionSource{}, + Payer: payer, + ChannelPruneExpiry: time.Hour * 24, + GraphPruneInterval: time.Hour * 2, + QueryBandwidth: func(e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi { + return lnwire.NewMSatFromSatoshis(e.Capacity) + }, + NextPaymentID: func() (uint64, error) { + next := atomic.AddUint64(&uniquePaymentID, 1) + return next, nil + }, + }) + if err != nil { + t.Fatalf("unable to create router %v", err) + } + + // On startup, the router should fetch all pending payments + // from the ControlTower, so assert that here. + didFetch := make(chan struct{}) + go func() { + select { + case <-control.fetchInFlight: + close(didFetch) + case <-time.After(1 * time.Second): + t.Fatalf("router did not fetch in flight " + + "payments") + } + }() + + if err := router.Start(); err != nil { + t.Fatalf("unable to start router: %v", err) + } + + select { + case <-didFetch: + case <-time.After(1 * time.Second): + t.Fatalf("did not fetch in flight payments at startup") + } + + return router, sendResult, paymentResult, paymentResultErr + } + + router, sendResult, getPaymentResult, getPaymentResultErr := setupRouter() + defer router.Stop() + + for _, test := range tests { + // Craft a LightningPayment struct. + var preImage lntypes.Preimage + if _, err := rand.Read(preImage[:]); err != nil { + t.Fatalf("unable to generate preimage") + } + + payHash := preImage.Hash() + + paymentAmt := lnwire.NewMSatFromSatoshis(1000) + payment := LightningPayment{ + Target: testGraph.aliasMap["c"], + Amount: paymentAmt, + FeeLimit: noFeeLimit, + PaymentHash: payHash, + } + + errSource, err := btcec.ParsePubKey(hop1[:], btcec.S256()) + if err != nil { + t.Fatalf("unable to fetch source node pub: %v", err) + } + + copy(preImage[:], bytes.Repeat([]byte{9}, 32)) + + router.cfg.MissionControl = &mockPaymentSessionSource{ + routes: test.routes, + } + + // Send the payment. Since this is new payment hash, the + // information should be registered with the ControlTower. + paymentResult := make(chan error) + go func() { + _, _, err := router.SendPayment(&payment) + paymentResult <- err + }() + + var resendResult chan error + for _, step := range test.steps { + switch step { + + case routerInitPayment: + var args initArgs + select { + case args = <-control.init: + case <-time.After(1 * time.Second): + t.Fatalf("no init payment with control") + } + + if args.c == nil { + t.Fatalf("expected non-nil CreationInfo") + } + + // In this step we expect the router to make a call to + // register a new attempt with the ControlTower. + case routerRegisterAttempt: + var args registerArgs + select { + case args = <-control.register: + case <-time.After(1 * time.Second): + t.Fatalf("not registered with control") + } + + if args.a == nil { + t.Fatalf("expected non-nil AttemptInfo") + } + + // In this step we expect the router to call the + // ControlTower's Succcess method with the preimage. + case routerSuccess: + select { + case _ = <-control.success: + case <-time.After(1 * time.Second): + t.Fatalf("not registered with control") + } + + // In this step we expect the router to call the + // ControlTower's Fail method, to indicate that the + // payment failed. + case routerFail: + select { + case _ = <-control.fail: + case <-time.After(1 * time.Second): + t.Fatalf("not registered with control") + } + + // In this step we expect the SendToSwitch method to be + // called, and we respond with a nil-error. + case sendToSwitchSuccess: + select { + case sendResult <- nil: + case <-time.After(1 * time.Second): + t.Fatalf("unable to send result") + } + + // In this step we expect the GetPaymentResult method + // to be called, and we respond with the preimage to + // complete the payment. + case getPaymentResultSuccess: + select { + case getPaymentResult <- &htlcswitch.PaymentResult{ + Preimage: preImage, + }: + case <-time.After(1 * time.Second): + t.Fatalf("unable to send result") + } + + // In this state we expect the GetPaymentResult method + // to be called, and we respond with a forwarding + // error, indicating that the router should retry. + case getPaymentResultFailure: + select { + case getPaymentResult <- &htlcswitch.PaymentResult{ + Error: &htlcswitch.ForwardingError{ + ErrorSource: errSource, + FailureMessage: &lnwire.FailTemporaryChannelFailure{}, + }, + }: + case <-time.After(1 * time.Second): + t.Fatalf("unable to get result") + } + + // In this step we manually try to resend the same + // payment, making sure the router responds with an + // error indicating that it is alreayd in flight. + case resendPayment: + resendResult = make(chan error) + go func() { + _, _, err := router.SendPayment(&payment) + resendResult <- err + }() + + // In this step we manually stop the router. + case stopRouter: + select { + case getPaymentResultErr <- fmt.Errorf( + "shutting down"): + case <-time.After(1 * time.Second): + t.Fatalf("unable to send payment " + + "result error") + } + + if err := router.Stop(); err != nil { + t.Fatalf("unable to restart: %v", err) + } + + // In this step we manually start the router. + case startRouter: + router, sendResult, getPaymentResult, + getPaymentResultErr = setupRouter() + + // In this state we expect to receive an error for the + // original payment made. + case paymentError: + select { + case err := <-paymentResult: + if err == nil { + t.Fatalf("expected error") + } + + case <-time.After(1 * time.Second): + t.Fatalf("got no payment result") + } + + // In this state we expect the original payment to + // succeed. + case paymentSuccess: + select { + case err := <-paymentResult: + if err != nil { + t.Fatalf("did not expecte error %v", err) + } + + case <-time.After(1 * time.Second): + t.Fatalf("got no payment result") + } + + // In this state we expect to receive an error for the + // resent payment made. + case resentPaymentError: + select { + case err := <-resendResult: + if err == nil { + t.Fatalf("expected error") + } + + case <-time.After(1 * time.Second): + t.Fatalf("got no payment result") + } + + // In this state we expect the resent payment to + // succeed. + case resentPaymentSuccess: + select { + case err := <-resendResult: + if err != nil { + t.Fatalf("did not expect error %v", err) + } + + case <-time.After(1 * time.Second): + t.Fatalf("got no payment result") + } + + default: + t.Fatalf("unknown step %v", step) + } + } + } }