diff --git a/itest/list_on_test.go b/itest/list_on_test.go index adedfd541..9294b4d16 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -193,6 +193,10 @@ var allTestCases = []*lntest.TestCase{ Name: "immediate payment after channel opened", TestFunc: testPaymentFollowingChannelOpen, }, + { + Name: "payment failure reason canceled", + TestFunc: testPaymentFailureReasonCanceled, + }, { Name: "invoice update subscription", TestFunc: testInvoiceSubscriptions, diff --git a/itest/lnd_payment_test.go b/itest/lnd_payment_test.go index 312978a95..cdd163605 100644 --- a/itest/lnd_payment_test.go +++ b/itest/lnd_payment_test.go @@ -1,6 +1,7 @@ package itest import ( + "context" "crypto/sha256" "encoding/hex" "fmt" @@ -13,7 +14,9 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntest/rpc" "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" ) @@ -767,3 +770,133 @@ func assertChannelState(ht *lntest.HarnessTest, hn *node.HarnessNode, }, lntest.DefaultTimeout) require.NoError(ht, err, "timeout while chekcing for balance") } + +// testPaymentFailureReasonCanceled ensures that the cancellation of a +// SendPayment request results in the payment failure reason +// FAILURE_REASON_CANCELED. This failure reason indicates that the context was +// cancelled manually by the user. It does not interrupt the current payment +// attempt, but will prevent any further payment attempts. The test steps are: +// 1.) Alice pays Carol's invoice through Bob. +// 2.) Bob intercepts the htlc, keeping the payment pending. +// 3.) Alice cancels the payment context, the payment is still pending. +// 4.) Bob fails OR resumes the intercepted HTLC. +// 5.) Alice observes a failed OR succeeded payment with failure reason +// FAILURE_REASON_CANCELED which suppresses further payment attempts. +func testPaymentFailureReasonCanceled(ht *lntest.HarnessTest) { + // Initialize the test context with 3 connected nodes. + ts := newInterceptorTestScenario(ht) + + alice, bob, carol := ts.alice, ts.bob, ts.carol + + // Open and wait for channels. + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} + reqs := []*lntest.OpenChannelRequest{ + {Local: alice, Remote: bob, Param: p}, + {Local: bob, Remote: carol, Param: p}, + } + resp := ht.OpenMultiChannelsAsync(reqs) + cpAB, cpBC := resp[0], resp[1] + + // Make sure Alice is aware of channel Bob=>Carol. + ht.AssertTopologyChannelOpen(alice, cpBC) + + // First we check that the payment is successful when bob resumes the + // htlc even though the payment context was canceled before invoice + // settlement. + sendPaymentInterceptAndCancel( + ht, ts, cpAB, routerrpc.ResolveHoldForwardAction_RESUME, + lnrpc.Payment_SUCCEEDED, + ) + + // Next we check that the context cancellation results in the expected + // failure reason while the htlc is being held and failed after + // cancellation. + // Note that we'd have to reset Alice's mission control if we tested the + // htlc fail case before the htlc resume case. + sendPaymentInterceptAndCancel( + ht, ts, cpAB, routerrpc.ResolveHoldForwardAction_FAIL, + lnrpc.Payment_FAILED, + ) + + // Finally, close channels. + ht.CloseChannel(alice, cpAB) + ht.CloseChannel(bob, cpBC) +} + +func sendPaymentInterceptAndCancel(ht *lntest.HarnessTest, + ts *interceptorTestScenario, cpAB *lnrpc.ChannelPoint, + interceptorAction routerrpc.ResolveHoldForwardAction, + expectedPaymentStatus lnrpc.Payment_PaymentStatus) { + + // Prepare the test cases. + alice, bob, carol := ts.alice, ts.bob, ts.carol + + // Connect the interceptor. + interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor() + + // Prepare the test cases. + addResponse := carol.RPC.AddInvoice(&lnrpc.Invoice{ + ValueMsat: 1000, + }) + invoice := carol.RPC.LookupInvoice(addResponse.RHash) + + // We initiate a payment from Alice and define the payment context + // cancellable. + ctx, cancelPaymentContext := context.WithCancel(context.Background()) + var paymentStream rpc.PaymentClient + go func() { + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitSat: 100000, + Cancelable: true, + } + + paymentStream = alice.RPC.SendPaymentWithContext(ctx, req) + }() + + // We start the htlc interceptor with a simple implementation that + // saves all intercepted packets. These packets are held to simulate a + // pending payment. + packet := ht.ReceiveHtlcInterceptor(interceptor) + + // Here we should wait for the channel to contain a pending htlc, and + // also be shown as being active. + ht.AssertIncomingHTLCActive(bob, cpAB, invoice.RHash) + + // Ensure that Alice's payment is in-flight because Bob is holding the + // htlc. + ht.AssertPaymentStatusFromStream(paymentStream, lnrpc.Payment_IN_FLIGHT) + + // Cancel the payment context. This should end the payment stream + // context, but the payment should still be in state in-flight without a + // failure reason. + cancelPaymentContext() + + var preimage lntypes.Preimage + copy(preimage[:], invoice.RPreimage) + payment := ht.AssertPaymentStatus( + alice, preimage, lnrpc.Payment_IN_FLIGHT, + ) + reasonNone := lnrpc.PaymentFailureReason_FAILURE_REASON_NONE + require.Equal(ht, reasonNone, payment.FailureReason) + + // Bob sends the interceptor action to the intercepted htlc. + err := interceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{ + IncomingCircuitKey: packet.IncomingCircuitKey, + Action: interceptorAction, + }) + require.NoError(ht, err, "failed to send request") + + // Assert that the payment status is as expected. + ht.AssertPaymentStatus(alice, preimage, expectedPaymentStatus) + + // Since the payment context was cancelled, no further payment attempts + // should've been made, and we observe FAILURE_REASON_CANCELED. + expectedReason := lnrpc.PaymentFailureReason_FAILURE_REASON_CANCELED + ht.AssertPaymentFailureReason(alice, preimage, expectedReason) + + // Cancel the context, which will disconnect the above interceptor. + cancelInterceptor() +} diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index 5ef2ec3df..615263373 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -1639,6 +1639,24 @@ func (h *HarnessTest) AssertPaymentStatus(hn *node.HarnessNode, return target } +// AssertPaymentFailureReason asserts that the given node lists a payment with +// the given preimage which has the expected failure reason. +func (h *HarnessTest) AssertPaymentFailureReason(hn *node.HarnessNode, + preimage lntypes.Preimage, reason lnrpc.PaymentFailureReason) { + + payHash := preimage.Hash() + err := wait.NoError(func() error { + p := h.findPayment(hn, payHash.String()) + if reason == p.FailureReason { + return nil + } + + return fmt.Errorf("payment: %v failure reason not match, "+ + "want %s got %s", payHash, reason, p.Status) + }, DefaultTimeout) + require.NoError(h, err, "timeout checking payment failure reason") +} + // AssertActiveNodesSynced asserts all active nodes have synced to the chain. func (h *HarnessTest) AssertActiveNodesSynced() { for _, node := range h.manager.activeNodes { diff --git a/lntest/rpc/router.go b/lntest/rpc/router.go index fbf44cb18..8bb8a92e3 100644 --- a/lntest/rpc/router.go +++ b/lntest/rpc/router.go @@ -36,7 +36,7 @@ func (h *HarnessRPC) SendPayment( req *routerrpc.SendPaymentRequest) PaymentClient { // SendPayment needs to have the context alive for the entire test case - // as the router relies on the context to propagate HTLCs. Thus we use + // as the router relies on the context to propagate HTLCs. Thus, we use // runCtx here instead of a timeout context. stream, err := h.Router.SendPaymentV2(h.runCtx, req) h.NoError(err, "SendPaymentV2") @@ -44,6 +44,21 @@ func (h *HarnessRPC) SendPayment( return stream } +// SendPaymentWithContext sends a payment using the given node and payment +// request and does so with the passed in context. +func (h *HarnessRPC) SendPaymentWithContext(context context.Context, + req *routerrpc.SendPaymentRequest) PaymentClient { + + require.NotNil(h.T, context, "context must not be nil") + + // SendPayment needs to have the context alive for the entire test case + // as the router relies on the context to propagate HTLCs. + stream, err := h.Router.SendPaymentV2(context, req) + h.NoError(err, "SendPaymentV2") + + return stream +} + type HtlcEventsClient routerrpc.Router_SubscribeHtlcEventsClient // SubscribeHtlcEvents makes a subscription to the HTLC events and returns a