diff --git a/routing/payment_lifecycle_test.go b/routing/payment_lifecycle_test.go index 8fee4cfb5..3f6dc1e71 100644 --- a/routing/payment_lifecycle_test.go +++ b/routing/payment_lifecycle_test.go @@ -1,17 +1,11 @@ package routing import ( - "crypto/rand" - "fmt" - "sync/atomic" "testing" "time" - "github.com/btcsuite/btcd/btcutil" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/channeldb" - "github.com/lightningnetwork/lnd/clock" - "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" @@ -19,783 +13,10 @@ import ( "github.com/stretchr/testify/require" ) -const stepTimeout = 5 * time.Second - var ( dummyErr = errors.New("dummy") ) -// createTestRoute builds a route a->b->c paying the given amt to c. -func createTestRoute(amt lnwire.MilliSatoshi, - aliasMap map[string]route.Vertex) (*route.Route, error) { - - hopFee := lnwire.NewMSatFromSatoshis(3) - hop1 := aliasMap["b"] - hop2 := aliasMap["c"] - hops := []*route.Hop{ - { - ChannelID: 1, - PubKeyBytes: hop1, - LegacyPayload: true, - AmtToForward: amt + hopFee, - }, - { - ChannelID: 2, - PubKeyBytes: hop2, - LegacyPayload: true, - AmtToForward: amt, - }, - } - - // We create a simple route that we will supply every time the router - // requests one. - return route.NewRouteFromHops( - amt+2*hopFee, 100, aliasMap["a"], hops, - ) -} - -// 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 - - // paymentErr is the error we expect our payment to fail with. This - // should be nil for tests with paymentSuccess steps and non-nil for - // payments with paymentError steps. - paymentErr error -} - -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" - - // routeRelease is a test step where we unblock pathfinding and - // allow it to respond to our test with a route. - routeRelease = "PaymentSession:release" - - // 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 GetAttemptResult method, and we will - // respond with a successful payment result. - getPaymentResultSuccess = "GetAttemptResult:success" - - // getPaymentResultTempFailure is a test step where we expect the - // router to call the GetAttemptResult method, and we will - // respond with a forwarding error, expecting the router to retry. - getPaymentResultTempFailure = "GetAttemptResult:temp-failure" - - // getPaymentResultTerminalFailure is a test step where we - // expect the router to call the GetAttemptResult method, and - // we will respond with a terminal error, expecting the router - // to stop making payment attempts. - getPaymentResultTerminalFailure = "GetAttemptResult: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 -// restart. -func TestRouterPaymentStateMachine(t *testing.T) { - t.Parallel() - - const startingBlockHeight = 101 - - // 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(t, true, testChannels, "a") - require.NoError(t, err, "unable to create graph") - - paymentAmt := lnwire.NewMSatFromSatoshis(1000) - - // We create a simple route that we will supply every time the router - // requests one. - rt, err := createTestRoute(paymentAmt, testGraph.aliasMap) - require.NoError(t, err, "unable to create route") - - tests := []paymentLifecycleTestCase{ - { - // Tests a normal payment flow that succeeds. - name: "single shot success", - - steps: []string{ - routerInitPayment, - routeRelease, - routerRegisterAttempt, - sendToSwitchSuccess, - getPaymentResultSuccess, - routerSettleAttempt, - paymentSuccess, - }, - routes: []*route.Route{rt}, - }, - { - // A payment flow with a failure on the first attempt, - // but that succeeds on the second attempt. - name: "single shot retry", - - steps: []string{ - routerInitPayment, - routeRelease, - routerRegisterAttempt, - sendToSwitchSuccess, - - // Make the first sent attempt fail. - getPaymentResultTempFailure, - routerFailAttempt, - - // The router should retry. - routeRelease, - routerRegisterAttempt, - sendToSwitchSuccess, - - // Make the second sent attempt succeed. - getPaymentResultSuccess, - routerSettleAttempt, - paymentSuccess, - }, - routes: []*route.Route{rt, rt}, - }, - { - // 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, - routeRelease, - routerRegisterAttempt, - - // Make the first sent attempt fail. - sendToSwitchResultFailure, - routerFailAttempt, - - // The router should retry. - routeRelease, - routerRegisterAttempt, - sendToSwitchSuccess, - - // Make the second sent attempt succeed. - getPaymentResultSuccess, - routerSettleAttempt, - 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. - name: "single shot route fails", - - steps: []string{ - routerInitPayment, - routeRelease, - routerRegisterAttempt, - sendToSwitchSuccess, - - // Make the first sent attempt fail. - getPaymentResultTempFailure, - routerFailAttempt, - - routeRelease, - - // Since there are no more routes to try, the - // payment should fail. - routerFailPayment, - paymentError, - }, - routes: []*route.Route{rt}, - paymentErr: channeldb.FailureReasonNoRoute, - }, - { - // We expect the payment to fail immediately if we have - // no routes to try. - name: "single shot no route", - - steps: []string{ - routerInitPayment, - routeRelease, - routerFailPayment, - paymentError, - }, - routes: []*route.Route{}, - paymentErr: channeldb.FailureReasonNoRoute, - }, - { - // 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. - name: "single shot resend", - - steps: []string{ - routerInitPayment, - routeRelease, - 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, - routerSettleAttempt, - - // 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 - // received payment result after a restart. - name: "single shot restart", - - steps: []string{ - routerInitPayment, - routeRelease, - 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, - routerSettleAttempt, - }, - routes: []*route.Route{rt}, - paymentErr: ErrRouterShuttingDown, - }, - { - // Tests that we are allowed to resend a payment after - // it has permanently failed. - name: "single shot resend fail", - - steps: []string{ - routerInitPayment, - routeRelease, - routerRegisterAttempt, - sendToSwitchSuccess, - - // Resending the payment at this stage should - // not be allowed. - resendPayment, - routerInitPayment, - resentPaymentError, - - // Make the first attempt fail. - getPaymentResultTempFailure, - routerFailAttempt, - - // Since we have no more routes to try, the - // original payment should fail. - routeRelease, - routerFailPayment, - paymentError, - - // Now resend the payment again. This should be - // allowed, since the payment has failed. - resendPayment, - routerInitPayment, - routeRelease, - routerRegisterAttempt, - sendToSwitchSuccess, - getPaymentResultSuccess, - routerSettleAttempt, - resentPaymentSuccess, - }, - routes: []*route.Route{rt}, - paymentErr: channeldb.FailureReasonNoRoute, - }, - } - - 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() - control.init = make(chan initArgs) - control.registerAttempt = make(chan registerAttemptArgs) - control.settleAttempt = make(chan settleAttemptArgs) - control.failAttempt = make(chan failAttemptArgs) - control.failPayment = make(chan failPaymentArgs) - control.fetchInFlight = make(chan struct{}) - - // 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) { - - chain := newMockChain(startingBlockHeight) - chainView := newMockChainView(chain) - - // We set uo the use the following channels and a mock Payer to - // synchronize with the interaction to the Switch. - sendResult := make(chan error) - paymentResult := make(chan *htlcswitch.PaymentResult) - - payer := &mockPayerOld{ - sendResult: sendResult, - paymentResult: paymentResult, - } - - router, err := New(Config{ - Graph: testGraph.graph, - Chain: chain, - ChainView: chainView, - Control: control, - SessionSource: &mockPaymentSessionSourceOld{}, - MissionControl: &mockMissionControlOld{}, - Payer: payer, - ChannelPruneExpiry: time.Hour * 24, - GraphPruneInterval: time.Hour * 2, - NextPaymentID: func() (uint64, error) { - next := atomic.AddUint64(&uniquePaymentID, 1) - return next, nil - }, - Clock: clock.NewTestClock(time.Unix(1, 0)), - IsAlias: func(scid lnwire.ShortChannelID) bool { - return false - }, - }) - 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. - errCh := make(chan error) - go func() { - close(errCh) - select { - case <-control.fetchInFlight: - return - case <-time.After(1 * time.Second): - errCh <- errors.New("router did not fetch in flight " + - "payments") - } - }() - - if err := router.Start(); err != nil { - t.Fatalf("unable to start router: %v", err) - } - - select { - case err := <-errCh: - if err != nil { - t.Fatalf("error in anonymous goroutine: %s", err) - } - case <-time.After(1 * time.Second): - t.Fatalf("did not fetch in flight payments at startup") - } - - return router, sendResult, paymentResult - } - - router, sendResult, getPaymentResult := setupRouter() - t.Cleanup(func() { - require.NoError(t, router.Stop()) - }) - - // Craft a LightningPayment struct. - var preImage lntypes.Preimage - if _, err := rand.Read(preImage[:]); err != nil { - t.Fatalf("unable to generate preimage") - } - - payHash := preImage.Hash() - - payment := LightningPayment{ - Target: testGraph.aliasMap["c"], - Amount: paymentAmt, - FeeLimit: noFeeLimit, - paymentHash: &payHash, - } - - // Setup our payment session source to block on release of - // routes. - routeChan := make(chan struct{}) - router.cfg.SessionSource = &mockPaymentSessionSourceOld{ - routes: test.routes, - routeRelease: routeChan, - } - - router.cfg.MissionControl = &mockMissionControlOld{} - - // 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 i, step := range test.steps { - i, step := i, step - - // fatal is a helper closure that wraps the step info. - fatal := func(err string, args ...interface{}) { - if args != nil { - err = fmt.Sprintf(err, args) - } - t.Fatalf( - "test case: %s failed on step [%v:%s], err: %s", - test.name, i, step, err, - ) - } - - switch step { - case routerInitPayment: - var args initArgs - select { - case args = <-control.init: - case <-time.After(stepTimeout): - fatal("no init payment with control") - } - - if args.c == nil { - fatal("expected non-nil CreationInfo") - } - - case routeRelease: - select { - case <-routeChan: - case <-time.After(stepTimeout): - fatal("no route requested") - } - - // 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): - fatal("attempt not registered with control") - } - - if args.a == nil { - fatal("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): - fatal("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): - fatal("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): - fatal("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): - fatal("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): - fatal("unable to send result") - } - - // In this step we expect the GetAttemptResult 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): - fatal("unable to send result") - } - - // In this state we expect the GetAttemptResult 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): - fatal("unable to get result") - } - - // In this state we expect the router to call the - // GetAttemptResult method, and we will respond with a - // terminal error, indicating 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): - fatal("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: - // On shutdown, the switch closes our result channel. - // Mimic this behavior in our mock. - close(getPaymentResult) - - if err := router.Stop(); err != nil { - fatal("unable to restart: %v", err) - } - - // In this step we manually start the router. - case startRouter: - router, sendResult, getPaymentResult = setupRouter() - - // In this state we expect to receive an error for the - // original payment made. - case paymentError: - require.Error(t, test.paymentErr, - "paymentError not set") - - select { - case err := <-paymentResult: - require.ErrorIs(t, err, test.paymentErr) - - case <-time.After(stepTimeout): - fatal("got no payment result") - } - - // In this state we expect the original payment to - // succeed. - case paymentSuccess: - require.Nil(t, test.paymentErr) - - select { - case err := <-paymentResult: - if err != nil { - t.Fatalf("did not expect "+ - "error %v", err) - } - - case <-time.After(stepTimeout): - fatal("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): - fatal("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): - fatal("got no payment result") - } - - default: - fatal("unknown step %v", step) - } - } - - select { - case <-done: - case <-time.After(testTimeout): - t.Fatalf("SendPayment didn't exit") - } -} - func makeSettledAttempt(total, fee int, preimage lntypes.Preimage) channeldb.HTLCAttempt { diff --git a/routing/router_test.go b/routing/router_test.go index eda49dd9d..79f0b026c 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -3400,649 +3400,6 @@ func createDummyLightningPayment(t *testing.T, } } -// TestSendMPPaymentSucceed tests that we can successfully send a MPPayment via -// router.SendPayment. This test mainly focuses on testing the logic of the -// method resumePayment is implemented as expected. -func TestSendMPPaymentSucceed(t *testing.T) { - const startingBlockHeight = 101 - - // Create mockers to initialize the router. - controlTower := &mockControlTower{} - sessionSource := &mockPaymentSessionSource{} - missionControl := &mockMissionControl{} - payer := &mockPaymentAttemptDispatcher{} - chain := newMockChain(startingBlockHeight) - chainView := newMockChainView(chain) - testGraph := createDummyTestGraph(t) - - // Define the behavior of the mockers to the point where we can - // successfully start the router. - controlTower.On("FetchInFlightPayments").Return( - []*channeldb.MPPayment{}, nil, - ) - payer.On("CleanStore", mock.Anything).Return(nil) - - // Create and start the router. - router, err := New(Config{ - Control: controlTower, - SessionSource: sessionSource, - MissionControl: missionControl, - Payer: payer, - - // TODO(yy): create new mocks for the chain and chainview. - Chain: chain, - ChainView: chainView, - - // TODO(yy): mock the graph once it's changed into interface. - Graph: testGraph.graph, - - Clock: clock.NewTestClock(time.Unix(1, 0)), - GraphPruneInterval: time.Hour * 2, - NextPaymentID: func() (uint64, error) { - next := atomic.AddUint64(&uniquePaymentID, 1) - return next, nil - }, - - IsAlias: func(scid lnwire.ShortChannelID) bool { - return false - }, - }) - require.NoError(t, err, "failed to create router") - - // Make sure the router can start and stop without error. - require.NoError(t, router.Start(), "router failed to start") - t.Cleanup(func() { - require.NoError(t, router.Stop(), "router failed to stop") - }) - - // Once the router is started, check that the mocked methods are called - // as expected. - controlTower.AssertExpectations(t) - payer.AssertExpectations(t) - - // Mock the methods to the point where we are inside the function - // resumePayment. - paymentAmt := lnwire.MilliSatoshi(10000) - req := createDummyLightningPayment( - t, testGraph.aliasMap["c"], paymentAmt, - ) - identifier := lntypes.Hash(req.Identifier()) - session := &mockPaymentSession{} - sessionSource.On("NewPaymentSession", req).Return(session, nil) - controlTower.On("InitPayment", identifier, mock.Anything).Return(nil) - // Mock the InFlightHTLCs. - var ( - htlcs []channeldb.HTLCAttempt - numAttempts atomic.Uint32 - settled atomic.Bool - numParts = uint32(4) - ) - - // Make a mock MPPayment. - payment := &mockMPPayment{} - payment.On("InFlightHTLCs").Return(htlcs). - On("GetState").Return(&channeldb.MPPaymentState{FeesPaid: 0}). - On("GetStatus").Return(channeldb.StatusInFlight) - controlTower.On("FetchPayment", identifier).Return(payment, nil).Once() - - // Mock FetchPayment to return the payment. - controlTower.On("FetchPayment", identifier).Return(payment, nil). - Run(func(args mock.Arguments) { - // When number of attempts made is less than 4, we will - // mock the payment's methods to allow the lifecycle to - // continue. - if numAttempts.Load() < numParts { - payment.On("AllowMoreAttempts").Return(true, nil).Once() - return - } - - if !settled.Load() { - fmt.Println("wait") - payment.On("AllowMoreAttempts").Return(false, nil).Once() - payment.On("NeedWaitAttempts").Return(true, nil).Once() - // We add another attempt to the counter to - // unblock next time. - return - } - - payment.On("AllowMoreAttempts").Return(false, nil). - On("NeedWaitAttempts").Return(false, nil) - }) - - // Mock SettleAttempt. - preimage := lntypes.Preimage{1, 2, 3} - settledAttempt := makeSettledAttempt( - int(paymentAmt/4), 0, preimage, - ) - - controlTower.On("SettleAttempt", - identifier, mock.Anything, mock.Anything, - ).Return(&settledAttempt, nil).Run(func(args mock.Arguments) { - // We want to at least wait for one settlement. - if numAttempts.Load() > 1 { - settled.Store(true) - } - }) - - // Create a route that can send 1/4 of the total amount. This value - // will be returned by calling RequestRoute. - shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap) - require.NoError(t, err, "failed to create route") - - session.On("RequestRoute", - mock.Anything, mock.Anything, mock.Anything, mock.Anything, - ).Return(shard, nil) - - // Make a new htlc attempt with zero fee and append it to the payment's - // HTLCs when calling RegisterAttempt. - controlTower.On("RegisterAttempt", - identifier, mock.Anything, - ).Return(nil).Run(func(args mock.Arguments) { - numAttempts.Add(1) - }) - - // Create a buffered chan and it will be returned by GetAttemptResult. - payer.resultChan = make(chan *htlcswitch.PaymentResult, 10) - payer.On("GetAttemptResult", - mock.Anything, identifier, mock.Anything, - ).Run(func(args mock.Arguments) { - // Before the mock method is returned, we send the result to - // the read-only chan. - payer.resultChan <- &htlcswitch.PaymentResult{} - }) - - // Simple mocking the rest. - payer.On("SendHTLC", - mock.Anything, mock.Anything, mock.Anything, - ).Return(nil) - - missionControl.On("ReportPaymentSuccess", - mock.Anything, mock.Anything, - ).Return(nil).Run(func(args mock.Arguments) { - }) - - controlTower.On("DeleteFailedAttempts", identifier).Return(nil) - - payment.On("TerminalInfo").Return(&settledAttempt, nil) - - // Call the actual method SendPayment on router. This is place inside a - // goroutine so we can set a timeout for the whole test, in case - // anything goes wrong and the test never finishes. - done := make(chan struct{}) - var p lntypes.Hash - go func() { - p, _, err = router.SendPayment(req) - close(done) - }() - - select { - case <-done: - case <-time.After(testTimeout): - t.Fatalf("SendPayment didn't exit") - } - - // Finally, validate the returned values and check that the mock - // methods are called as expected. - require.NoError(t, err, "send payment failed") - require.EqualValues(t, preimage, p, "preimage not match") - - // Note that we also implicitly check the methods such as FailAttempt, - // ReportPaymentFail, etc, are not called because we never mocked them - // in this test. If any of the unexpected methods was called, the test - // would fail. - controlTower.AssertExpectations(t) - payer.AssertExpectations(t) - sessionSource.AssertExpectations(t) - session.AssertExpectations(t) - missionControl.AssertExpectations(t) - payment.AssertExpectations(t) -} - -// TestSendMPPaymentSucceedOnExtraShards tests that we need extra attempts if -// there are failed ones,so that a payment is successfully sent. This test -// mainly focuses on testing the logic of the method resumePayment is -// implemented as expected. -func TestSendMPPaymentSucceedOnExtraShards(t *testing.T) { - const startingBlockHeight = 101 - - // Create mockers to initialize the router. - controlTower := &mockControlTower{} - sessionSource := &mockPaymentSessionSource{} - missionControl := &mockMissionControl{} - payer := &mockPaymentAttemptDispatcher{} - chain := newMockChain(startingBlockHeight) - chainView := newMockChainView(chain) - testGraph := createDummyTestGraph(t) - - // Define the behavior of the mockers to the point where we can - // successfully start the router. - controlTower.On("FetchInFlightPayments").Return( - []*channeldb.MPPayment{}, nil, - ) - payer.On("CleanStore", mock.Anything).Return(nil) - - // Create and start the router. - router, err := New(Config{ - Control: controlTower, - SessionSource: sessionSource, - MissionControl: missionControl, - Payer: payer, - - // TODO(yy): create new mocks for the chain and chainview. - Chain: chain, - ChainView: chainView, - - // TODO(yy): mock the graph once it's changed into interface. - Graph: testGraph.graph, - - Clock: clock.NewTestClock(time.Unix(1, 0)), - GraphPruneInterval: time.Hour * 2, - NextPaymentID: func() (uint64, error) { - next := atomic.AddUint64(&uniquePaymentID, 1) - return next, nil - }, - - IsAlias: func(scid lnwire.ShortChannelID) bool { - return false - }, - }) - require.NoError(t, err, "failed to create router") - - // Make sure the router can start and stop without error. - require.NoError(t, router.Start(), "router failed to start") - t.Cleanup(func() { - require.NoError(t, router.Stop(), "router failed to stop") - }) - - // Once the router is started, check that the mocked methods are called - // as expected. - controlTower.AssertExpectations(t) - payer.AssertExpectations(t) - - // Mock the methods to the point where we are inside the function - // resumePayment. - paymentAmt := lnwire.MilliSatoshi(20000) - req := createDummyLightningPayment( - t, testGraph.aliasMap["c"], paymentAmt, - ) - identifier := lntypes.Hash(req.Identifier()) - session := &mockPaymentSession{} - sessionSource.On("NewPaymentSession", req).Return(session, nil) - controlTower.On("InitPayment", identifier, mock.Anything).Return(nil) - - // Mock the InFlightHTLCs. - var ( - htlcs []channeldb.HTLCAttempt - numAttempts atomic.Uint32 - failAttemptCount atomic.Uint32 - settled atomic.Bool - ) - - // Make a mock MPPayment. - payment := &mockMPPayment{} - payment.On("InFlightHTLCs").Return(htlcs). - On("GetState").Return(&channeldb.MPPaymentState{FeesPaid: 0}). - On("GetStatus").Return(channeldb.StatusInFlight) - controlTower.On("FetchPayment", identifier).Return(payment, nil).Once() - - // Mock FetchPayment to return the payment. - controlTower.On("FetchPayment", identifier).Return(payment, nil). - Run(func(args mock.Arguments) { - // When number of attempts made is less than 4, we will - // mock the payment's methods to allow the lifecycle to - // continue. - attempts := numAttempts.Load() - if attempts < 6 { - payment.On("AllowMoreAttempts").Return(true, nil).Once() - return - } - - if !settled.Load() { - payment.On("AllowMoreAttempts").Return(false, nil).Once() - payment.On("NeedWaitAttempts").Return(true, nil).Once() - // We add another attempt to the counter to - // unblock next time. - numAttempts.Add(1) - return - } - - payment.On("AllowMoreAttempts").Return(false, nil). - On("NeedWaitAttempts").Return(false, nil) - }) - - // Create a route that can send 1/4 of the total amount. This value - // will be returned by calling RequestRoute. - shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap) - require.NoError(t, err, "failed to create route") - session.On("RequestRoute", - mock.Anything, mock.Anything, mock.Anything, mock.Anything, - ).Return(shard, nil) - - // Make a new htlc attempt with zero fee and append it to the payment's - // HTLCs when calling RegisterAttempt. - controlTower.On("RegisterAttempt", - identifier, mock.Anything, - ).Return(nil).Run(func(args mock.Arguments) { - // Increase the counter whenever an attempt is made. - numAttempts.Add(1) - }) - - // Create a buffered chan and it will be returned by GetAttemptResult. - payer.resultChan = make(chan *htlcswitch.PaymentResult, 10) - - // We use the failAttemptCount to track how many attempts we want to - // fail. Each time the following mock method is called, the count gets - // updated. - payer.On("GetAttemptResult", - mock.Anything, identifier, mock.Anything, - ).Run(func(args mock.Arguments) { - // Before the mock method is returned, we send the result to - // the read-only chan. - - // Update the counter. - failAttemptCount.Add(1) - - // We will make the first two attempts failed with temporary - // error. - if failAttemptCount.Load() <= 2 { - payer.resultChan <- &htlcswitch.PaymentResult{ - Error: htlcswitch.NewForwardingError( - &lnwire.FailTemporaryChannelFailure{}, - 1, - ), - } - return - } - - // Otherwise we will mark the attempt succeeded. - payer.resultChan <- &htlcswitch.PaymentResult{} - }) - - // Mock the FailAttempt method to fail one of the attempts. - var failedAttempt channeldb.HTLCAttempt - controlTower.On("FailAttempt", - identifier, mock.Anything, mock.Anything, - ).Return(&failedAttempt, nil) - - // Setup ReportPaymentFail to return nil reason and error so the - // payment won't fail. - missionControl.On("ReportPaymentFail", - mock.Anything, mock.Anything, mock.Anything, mock.Anything, - ).Return(nil, nil) - - // Simple mocking the rest. - payer.On("SendHTLC", - mock.Anything, mock.Anything, mock.Anything, - ).Return(nil) - missionControl.On("ReportPaymentSuccess", - mock.Anything, mock.Anything, - ).Return(nil) - - // Mock SettleAttempt by changing one of the HTLCs to be settled. - preimage := lntypes.Preimage{1, 2, 3} - settledAttempt := makeSettledAttempt( - int(paymentAmt/4), 0, preimage, - ) - controlTower.On("SettleAttempt", - identifier, mock.Anything, mock.Anything, - ).Return(&settledAttempt, nil).Run(func(args mock.Arguments) { - if numAttempts.Load() > 1 { - settled.Store(true) - } - }) - - controlTower.On("DeleteFailedAttempts", identifier).Return(nil) - - payment.On("TerminalInfo").Return(&settledAttempt, nil) - - // Call the actual method SendPayment on router. This is place inside a - // goroutine so we can set a timeout for the whole test, in case - // anything goes wrong and the test never finishes. - done := make(chan struct{}) - var p lntypes.Hash - go func() { - p, _, err = router.SendPayment(req) - close(done) - }() - - select { - case <-done: - case <-time.After(testTimeout): - t.Fatalf("SendPayment didn't exit") - } - - // Finally, validate the returned values and check that the mock - // methods are called as expected. - require.NoError(t, err, "send payment failed") - require.EqualValues(t, preimage, p, "preimage not match") - - controlTower.AssertExpectations(t) - payer.AssertExpectations(t) - sessionSource.AssertExpectations(t) - session.AssertExpectations(t) - missionControl.AssertExpectations(t) - payment.AssertExpectations(t) -} - -// TestSendMPPaymentFailed tests that when one of the shard fails with a -// terminal error, the router will stop attempting and the payment will fail. -// This test mainly focuses on testing the logic of the method resumePayment -// is implemented as expected. -func TestSendMPPaymentFailed(t *testing.T) { - const startingBlockHeight = 101 - - // Create mockers to initialize the router. - controlTower := &mockControlTower{} - sessionSource := &mockPaymentSessionSource{} - missionControl := &mockMissionControl{} - payer := &mockPaymentAttemptDispatcher{} - chain := newMockChain(startingBlockHeight) - chainView := newMockChainView(chain) - testGraph := createDummyTestGraph(t) - - // Define the behavior of the mockers to the point where we can - // successfully start the router. - controlTower.On("FetchInFlightPayments").Return( - []*channeldb.MPPayment{}, nil, - ) - payer.On("CleanStore", mock.Anything).Return(nil) - - // Create and start the router. - router, err := New(Config{ - Control: controlTower, - SessionSource: sessionSource, - MissionControl: missionControl, - Payer: payer, - - // TODO(yy): create new mocks for the chain and chainview. - Chain: chain, - ChainView: chainView, - - // TODO(yy): mock the graph once it's changed into interface. - Graph: testGraph.graph, - - Clock: clock.NewTestClock(time.Unix(1, 0)), - GraphPruneInterval: time.Hour * 2, - NextPaymentID: func() (uint64, error) { - next := atomic.AddUint64(&uniquePaymentID, 1) - return next, nil - }, - - IsAlias: func(scid lnwire.ShortChannelID) bool { - return false - }, - }) - require.NoError(t, err, "failed to create router") - - // Make sure the router can start and stop without error. - require.NoError(t, router.Start(), "router failed to start") - t.Cleanup(func() { - require.NoError(t, router.Stop(), "router failed to stop") - }) - - // Once the router is started, check that the mocked methods are called - // as expected. - controlTower.AssertExpectations(t) - payer.AssertExpectations(t) - - // Mock the methods to the point where we are inside the function - // resumePayment. - paymentAmt := lnwire.MilliSatoshi(10000) - req := createDummyLightningPayment( - t, testGraph.aliasMap["c"], paymentAmt, - ) - identifier := lntypes.Hash(req.Identifier()) - session := &mockPaymentSession{} - sessionSource.On("NewPaymentSession", req).Return(session, nil) - controlTower.On("InitPayment", identifier, mock.Anything).Return(nil) - - // Mock the InFlightHTLCs. - var ( - htlcs []channeldb.HTLCAttempt - numAttempts atomic.Uint32 - failAttemptCount atomic.Uint32 - failed atomic.Bool - numParts = uint32(4) - ) - - // Make a mock MPPayment. - payment := &mockMPPayment{} - payment.On("InFlightHTLCs").Return(htlcs).Once() - payment.On("GetState").Return(&channeldb.MPPaymentState{}). - On("GetStatus").Return(channeldb.StatusInFlight) - controlTower.On("FetchPayment", identifier).Return(payment, nil).Once() - - // Mock the sequential FetchPayment to return the payment. - controlTower.On("FetchPayment", identifier).Return(payment, nil).Run( - func(_ mock.Arguments) { - // We want to at least send out all parts in order to - // wait for them later. - if numAttempts.Load() < numParts { - payment.On("AllowMoreAttempts").Return(true, nil).Once() - return - } - - // Wait if the payment wasn't failed yet. - if !failed.Load() { - payment.On("AllowMoreAttempts").Return(false, nil).Once(). - On("NeedWaitAttempts").Return(true, nil).Once() - return - } - - payment.On("AllowMoreAttempts").Return(false, nil). - On("NeedWaitAttempts").Return(false, nil).Once() - }) - - // Create a route that can send 1/4 of the total amount. This value - // will be returned by calling RequestRoute. - shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap) - require.NoError(t, err, "failed to create route") - session.On("RequestRoute", - mock.Anything, mock.Anything, mock.Anything, mock.Anything, - ).Return(shard, nil) - - // Make a new htlc attempt with zero fee and append it to the payment's - // HTLCs when calling RegisterAttempt. - controlTower.On("RegisterAttempt", - identifier, mock.Anything, - ).Return(nil).Run(func(args mock.Arguments) { - numAttempts.Add(1) - }) - - // Create a buffered chan and it will be returned by GetAttemptResult. - payer.resultChan = make(chan *htlcswitch.PaymentResult, 10) - - // We use the failAttemptCount to track how many attempts we want to - // fail. Each time the following mock method is called, the count gets - // updated. - payer.On("GetAttemptResult", - mock.Anything, identifier, mock.Anything, - ).Run(func(_ mock.Arguments) { - // Before the mock method is returned, we send the result to - // the read-only chan. - - // Update the counter. - failAttemptCount.Add(1) - - // We fail the first attempt with terminal error. - if failAttemptCount.Load() == 1 { - payer.resultChan <- &htlcswitch.PaymentResult{ - Error: htlcswitch.NewForwardingError( - &lnwire.FailIncorrectDetails{}, - 1, - ), - } - return - } - - // We will make the rest attempts failed with temporary error. - payer.resultChan <- &htlcswitch.PaymentResult{ - Error: htlcswitch.NewForwardingError( - &lnwire.FailTemporaryChannelFailure{}, - 1, - ), - } - }) - - // Mock the FailAttempt method to fail one of the attempts. - var failedAttempt channeldb.HTLCAttempt - controlTower.On("FailAttempt", - identifier, mock.Anything, mock.Anything, - ).Return(&failedAttempt, nil) - - // Setup ReportPaymentFail to return nil reason and error so the - // payment won't fail. - failureReason := channeldb.FailureReasonPaymentDetails - missionControl.On("ReportPaymentFail", - mock.Anything, mock.Anything, mock.Anything, mock.Anything, - ).Return(&failureReason, nil) - - // Simple mocking the rest. - controlTower.On("FailPayment", - identifier, failureReason, - ).Return(nil).Run(func(_ mock.Arguments) { - failed.Store(true) - }) - - // Mock the payment to return the failure reason. - payer.On("SendHTLC", - mock.Anything, mock.Anything, mock.Anything, - ).Return(nil) - - payment.On("TerminalInfo").Return(nil, &failureReason) - - controlTower.On("DeleteFailedAttempts", identifier).Return(nil) - - // Call the actual method SendPayment on router. This is place inside a - // goroutine so we can set a timeout for the whole test, in case - // anything goes wrong and the test never finishes. - done := make(chan struct{}) - var p lntypes.Hash - go func() { - p, _, err = router.SendPayment(req) - close(done) - }() - - select { - case <-done: - case <-time.After(testTimeout): - t.Fatalf("SendPayment didn't exit") - } - - // Finally, validate the returned values and check that the mock - // methods are called as expected. - require.Error(t, err, "expected send payment error") - require.EqualValues(t, [32]byte{}, p, "preimage not match") - require.GreaterOrEqual(t, failAttemptCount.Load(), uint32(1)) - - controlTower.AssertExpectations(t) - payer.AssertExpectations(t) - sessionSource.AssertExpectations(t) - session.AssertExpectations(t) - missionControl.AssertExpectations(t) - payment.AssertExpectations(t) -} - // TestBlockDifferenceFix tests if when the router is behind on blocks, the // router catches up to the best block head. func TestBlockDifferenceFix(t *testing.T) {