diff --git a/routing/payment_lifecycle_test.go b/routing/payment_lifecycle_test.go index 70346966..d30ca02c 100644 --- a/routing/payment_lifecycle_test.go +++ b/routing/payment_lifecycle_test.go @@ -48,6 +48,101 @@ func createTestRoute(amt lnwire.MilliSatoshi, ) } +// paymentLifecycleTestCase contains the steps that we expect for a payment +// lifecycle test, and the routes that pathfinding should deliver. +type paymentLifecycleTestCase struct { + name string + + // 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" + + // routerSettleAttempt is a test step where we expect the + // router to call the SettleAttempt method on the control + // tower. + routerSettleAttempt = "Router:settle-attempt" + + // routerFailAttempt is a test step where we expect the router + // to call the FailAttempt method on the control tower. + routerFailAttempt = "Router:fail-attempt" + + // routerFailPayment is a test step where we expect the router + // to call the Fail method on the control tower. + routerFailPayment = "Router:fail-payment" + + // 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" + + // sendToSwitchResultFailure is a step where we expect the + // router to send the payment attempt to the switch, and we + // will respond with a forwarding error. This can happen when + // forwarding fail on our local links. + sendToSwitchResultFailure = "SendToSwitch:failure" + + // 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" + + // getPaymentResultTempFailure is a test step where we expect the + // router to call the GetPaymentResult method, and we will + // respond with a forwarding error, expecting the router to retry. + getPaymentResultTempFailure = "GetPaymentResult:temp-failure" + + // getPaymentResultTerminalFailure is a test step where we + // expect the router to call the GetPaymentResult method, and + // we will respond with a terminal error, expecting the router + // to stop making payment attempts. + getPaymentResultTerminalFailure = "GetPaymentResult:terminal-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 already 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" +) + // 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 @@ -95,102 +190,11 @@ func TestRouterPaymentStateMachine(t *testing.T) { 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" - - // routerSettleAttempt is a test step where we expect the - // router to call the SettleAttempt method on the control - // tower. - routerSettleAttempt = "Router:settle-attempt" - - // routerFailAttempt is a test step where we expect the router - // to call the FailAttempt method on the control tower. - routerFailAttempt = "Router:fail-attempt" - - // routerFailPayment is a test step where we expect the router - // to call the Fail method on the control tower. - routerFailPayment = "Router:fail-payment" - - // 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" - - // sendToSwitchResultFailure is a step where we expect the - // router to send the payment attempt to the switch, and we - // will respond with a forwarding error. This can happen when - // forwarding fail on our local links. - sendToSwitchResultFailure = "SendToSwitch:failure" - - // 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" - - // getPaymentResultTempFailure is a test step where we expect the - // router to call the GetPaymentResult method, and we will - // respond with a forwarding error, expecting the router to retry. - getPaymentResultTempFailure = "GetPaymentResult:temp-failure" - - // getPaymentResultTerminalFailure is a test step where we - // expect the router to call the GetPaymentResult method, and - // we will respond with a terminal error, expecting the router - // to stop making payment attempts. - getPaymentResultTerminalFailure = "GetPaymentResult:terminal-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 already 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 := []paymentLifecycleTestCase{ { // Tests a normal payment flow that succeeds. + name: "single shot success", + steps: []string{ routerInitPayment, routerRegisterAttempt, @@ -204,6 +208,8 @@ func TestRouterPaymentStateMachine(t *testing.T) { { // A payment flow with a failure on the first attempt, // but that succeeds on the second attempt. + name: "single shot retry", + steps: []string{ routerInitPayment, routerRegisterAttempt, @@ -228,6 +234,8 @@ func TestRouterPaymentStateMachine(t *testing.T) { // A payment flow with a forwarding failure first time // sending to the switch, but that succeeds on the // second attempt. + name: "single shot switch failure", + steps: []string{ routerInitPayment, routerRegisterAttempt, @@ -251,6 +259,8 @@ func TestRouterPaymentStateMachine(t *testing.T) { // A payment that fails on the first attempt, and has // only one route available to try. It will therefore // fail permanently. + name: "single shot route fails", + steps: []string{ routerInitPayment, routerRegisterAttempt, @@ -270,6 +280,8 @@ func TestRouterPaymentStateMachine(t *testing.T) { { // We expect the payment to fail immediately if we have // no routes to try. + name: "single shot no route", + steps: []string{ routerInitPayment, routerFailPayment, @@ -282,6 +294,8 @@ func TestRouterPaymentStateMachine(t *testing.T) { // the same payment after each step. This ensures that // the router don't attempt to resend a payment already // in flight. + name: "single shot resend", + steps: []string{ routerInitPayment, routerRegisterAttempt, @@ -322,6 +336,8 @@ func TestRouterPaymentStateMachine(t *testing.T) { { // Tests that the router is able to handle the // receieved payment result after a restart. + name: "single shot restart", + steps: []string{ routerInitPayment, routerRegisterAttempt, @@ -344,6 +360,8 @@ func TestRouterPaymentStateMachine(t *testing.T) { { // Tests that we are allowed to resend a payment after // it has permanently failed. + name: "single shot resend fail", + steps: []string{ routerInitPayment, routerRegisterAttempt, @@ -382,6 +400,8 @@ func TestRouterPaymentStateMachine(t *testing.T) { // ===================================== { // Tests a simple successful MP payment of 4 shards. + name: "MP success", + steps: []string{ routerInitPayment, @@ -422,6 +442,8 @@ func TestRouterPaymentStateMachine(t *testing.T) { { // An MP payment scenario where we need several extra // attempts before the payment finally settle. + name: "MP failed shards", + steps: []string{ routerInitPayment, @@ -474,6 +496,8 @@ func TestRouterPaymentStateMachine(t *testing.T) { // However the last shard settle, which means we get // the preimage and should consider the overall payment // a success. + name: "MP one shard success", + steps: []string{ routerInitPayment, @@ -513,6 +537,8 @@ func TestRouterPaymentStateMachine(t *testing.T) { { // An MP payment scenario a shard fail with a terminal // error, causing the router to stop attempting. + name: "MP terminal", + steps: []string{ routerInitPayment, @@ -554,6 +580,21 @@ func TestRouterPaymentStateMachine(t *testing.T) { }, } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + testPaymentLifecycle( + t, test, paymentAmt, startingBlockHeight, + testGraph, + ) + }) + } +} + +func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase, + paymentAmt lnwire.MilliSatoshi, startingBlockHeight uint32, + testGraph *testGraphInstance) { + // Create a mock control tower with channels set up, that we use to // synchronize and listen for events. control := makeMockControlTower() @@ -644,260 +685,258 @@ func TestRouterPaymentStateMachine(t *testing.T) { } }() - 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") - } + // Craft a LightningPayment struct. + var preImage lntypes.Preimage + if _, err := rand.Read(preImage[:]); err != nil { + t.Fatalf("unable to generate preimage") + } - payHash := preImage.Hash() + payHash := preImage.Hash() - payment := LightningPayment{ - Target: testGraph.aliasMap["c"], - Amount: paymentAmt, - FeeLimit: noFeeLimit, - PaymentHash: payHash, - } + payment := LightningPayment{ + Target: testGraph.aliasMap["c"], + Amount: paymentAmt, + FeeLimit: noFeeLimit, + PaymentHash: payHash, + } - router.cfg.SessionSource = &mockPaymentSessionSource{ - routes: test.routes, - } + router.cfg.SessionSource = &mockPaymentSessionSource{ + routes: test.routes, + } - router.cfg.MissionControl = &mockMissionControl{} + router.cfg.MissionControl = &mockMissionControl{} - // Send the payment. Since this is new payment hash, the - // information should be registered with the ControlTower. - paymentResult := make(chan error) - done := make(chan struct{}) - go func() { - _, _, err := router.SendPayment(&payment) - paymentResult <- err - close(done) - }() + // Send the payment. Since this is new payment hash, the + // information should be registered with the ControlTower. + paymentResult := make(chan error) + done := make(chan struct{}) + go func() { + _, _, err := router.SendPayment(&payment) + paymentResult <- err + close(done) + }() - var resendResult chan error - for _, step := range test.steps { - switch step { + var resendResult chan error + for _, step := range test.steps { + switch step { - case routerInitPayment: - var args initArgs - select { - case args = <-control.init: - case <-time.After(stepTimeout): - 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 registerAttemptArgs - select { - case args = <-control.registerAttempt: - case <-time.After(stepTimeout): - t.Fatalf("attempt 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 SettleAttempt method with the preimage. - case routerSettleAttempt: - select { - case <-control.settleAttempt: - case <-time.After(stepTimeout): - t.Fatalf("attempt settle not " + - "registered with control") - } - - // In this step we expect the router to call the - // ControlTower's FailAttempt method with a HTLC fail - // info. - case routerFailAttempt: - select { - case <-control.failAttempt: - case <-time.After(stepTimeout): - t.Fatalf("attempt fail 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 routerFailPayment: - select { - case <-control.failPayment: - case <-time.After(stepTimeout): - t.Fatalf("payment fail 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(stepTimeout): - t.Fatalf("unable to send result") - } - - // In this step we expect the SendToSwitch method to be - // called, and we respond with a forwarding error - case sendToSwitchResultFailure: - select { - case sendResult <- htlcswitch.NewForwardingError( - &lnwire.FailTemporaryChannelFailure{}, - 1, - ): - case <-time.After(stepTimeout): - 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(stepTimeout): - 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 getPaymentResultTempFailure: - failure := htlcswitch.NewForwardingError( - &lnwire.FailTemporaryChannelFailure{}, - 1, - ) - - select { - case getPaymentResult <- &htlcswitch.PaymentResult{ - Error: failure, - }: - case <-time.After(stepTimeout): - t.Fatalf("unable to get result") - } - - // In this state we expect the router to call the - // GetPaymentResult method, and we will respond with a - // terminal error, indiating the router should stop - // making payment attempts. - case getPaymentResultTerminalFailure: - failure := htlcswitch.NewForwardingError( - &lnwire.FailIncorrectDetails{}, - 1, - ) - - select { - case getPaymentResult <- &htlcswitch.PaymentResult{ - Error: failure, - }: - case <-time.After(stepTimeout): - 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 already 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(stepTimeout): - 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(stepTimeout): - 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 expect "+ - "error %v", err) - } - - case <-time.After(stepTimeout): - 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(stepTimeout): - 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(stepTimeout): - t.Fatalf("got no payment result") - } - - default: - t.Fatalf("unknown step %v", step) + case routerInitPayment: + var args initArgs + select { + case args = <-control.init: + case <-time.After(stepTimeout): + t.Fatalf("no init payment with control") } - } - select { - case <-done: - case <-time.After(testTimeout): - t.Fatalf("SendPayment didn't exit") + 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 registerAttemptArgs + select { + case args = <-control.registerAttempt: + case <-time.After(stepTimeout): + t.Fatalf("attempt 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 SettleAttempt method with the preimage. + case routerSettleAttempt: + select { + case <-control.settleAttempt: + case <-time.After(stepTimeout): + t.Fatalf("attempt settle not " + + "registered with control") + } + + // In this step we expect the router to call the + // ControlTower's FailAttempt method with a HTLC fail + // info. + case routerFailAttempt: + select { + case <-control.failAttempt: + case <-time.After(stepTimeout): + t.Fatalf("attempt fail 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 routerFailPayment: + select { + case <-control.failPayment: + case <-time.After(stepTimeout): + t.Fatalf("payment fail 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(stepTimeout): + t.Fatalf("unable to send result") + } + + // In this step we expect the SendToSwitch method to be + // called, and we respond with a forwarding error + case sendToSwitchResultFailure: + select { + case sendResult <- htlcswitch.NewForwardingError( + &lnwire.FailTemporaryChannelFailure{}, + 1, + ): + case <-time.After(stepTimeout): + 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(stepTimeout): + 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 getPaymentResultTempFailure: + failure := htlcswitch.NewForwardingError( + &lnwire.FailTemporaryChannelFailure{}, + 1, + ) + + select { + case getPaymentResult <- &htlcswitch.PaymentResult{ + Error: failure, + }: + case <-time.After(stepTimeout): + t.Fatalf("unable to get result") + } + + // In this state we expect the router to call the + // GetPaymentResult method, and we will respond with a + // terminal error, indiating the router should stop + // making payment attempts. + case getPaymentResultTerminalFailure: + failure := htlcswitch.NewForwardingError( + &lnwire.FailIncorrectDetails{}, + 1, + ) + + select { + case getPaymentResult <- &htlcswitch.PaymentResult{ + Error: failure, + }: + case <-time.After(stepTimeout): + 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 already 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(stepTimeout): + 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(stepTimeout): + 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 expect "+ + "error %v", err) + } + + case <-time.After(stepTimeout): + 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(stepTimeout): + 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(stepTimeout): + t.Fatalf("got no payment result") + } + + default: + t.Fatalf("unknown step %v", step) } } + + select { + case <-done: + case <-time.After(testTimeout): + t.Fatalf("SendPayment didn't exit") + } }