Skip to content

Commit 0273eac

Browse files
committed
itest: payment failure reason canceled
1 parent 653226d commit 0273eac

File tree

4 files changed

+171
-1
lines changed

4 files changed

+171
-1
lines changed

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ var allTestCases = []*lntest.TestCase{
193193
Name: "immediate payment after channel opened",
194194
TestFunc: testPaymentFollowingChannelOpen,
195195
},
196+
{
197+
Name: "payment failure reason canceled",
198+
TestFunc: testPaymentFailureReasonCanceled,
199+
},
196200
{
197201
Name: "invoice update subscription",
198202
TestFunc: testInvoiceSubscriptions,

itest/lnd_payment_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package itest
22

33
import (
4+
"context"
45
"crypto/sha256"
56
"encoding/hex"
67
"fmt"
@@ -13,7 +14,9 @@ import (
1314
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1415
"github.com/lightningnetwork/lnd/lntest"
1516
"github.com/lightningnetwork/lnd/lntest/node"
17+
"github.com/lightningnetwork/lnd/lntest/rpc"
1618
"github.com/lightningnetwork/lnd/lntest/wait"
19+
"github.com/lightningnetwork/lnd/lntypes"
1720
"github.com/stretchr/testify/require"
1821
)
1922

@@ -767,3 +770,133 @@ func assertChannelState(ht *lntest.HarnessTest, hn *node.HarnessNode,
767770
}, lntest.DefaultTimeout)
768771
require.NoError(ht, err, "timeout while chekcing for balance")
769772
}
773+
774+
// testPaymentFailureReasonCanceled ensures that the cancellation of a
775+
// SendPayment request results in the payment failure reason
776+
// FAILURE_REASON_CANCELED. This failure reason indicates that the context was
777+
// cancelled manually by the user. It does not interrupt the current payment
778+
// attempt, but will prevent any further payment attempts. The test steps are:
779+
// 1.) Alice pays Carol's invoice through Bob.
780+
// 2.) Bob intercepts the htlc, keeping the payment pending.
781+
// 3.) Alice cancels the payment context, the payment is still pending.
782+
// 4.) Bob fails OR resumes the intercepted HTLC.
783+
// 5.) Alice observes a failed OR succeeded payment with failure reason
784+
// FAILURE_REASON_CANCELED which suppresses further payment attempts.
785+
func testPaymentFailureReasonCanceled(ht *lntest.HarnessTest) {
786+
// Initialize the test context with 3 connected nodes.
787+
ts := newInterceptorTestScenario(ht)
788+
789+
alice, bob, carol := ts.alice, ts.bob, ts.carol
790+
791+
// Open and wait for channels.
792+
const chanAmt = btcutil.Amount(300000)
793+
p := lntest.OpenChannelParams{Amt: chanAmt}
794+
reqs := []*lntest.OpenChannelRequest{
795+
{Local: alice, Remote: bob, Param: p},
796+
{Local: bob, Remote: carol, Param: p},
797+
}
798+
resp := ht.OpenMultiChannelsAsync(reqs)
799+
cpAB, cpBC := resp[0], resp[1]
800+
801+
// Make sure Alice is aware of channel Bob=>Carol.
802+
ht.AssertTopologyChannelOpen(alice, cpBC)
803+
804+
// First we check that the payment is successful when bob resumes the
805+
// htlc even though the payment context was canceled before invoice
806+
// settlement.
807+
sendPaymentInterceptAndCancel(
808+
ht, ts, cpAB, routerrpc.ResolveHoldForwardAction_RESUME,
809+
lnrpc.Payment_SUCCEEDED,
810+
)
811+
812+
// Next we check that the context cancellation results in the expected
813+
// failure reason while the htlc is being held and failed after
814+
// cancellation.
815+
// Note that we'd have to reset Alice's mission control if we tested the
816+
// htlc fail case before the htlc resume case.
817+
sendPaymentInterceptAndCancel(
818+
ht, ts, cpAB, routerrpc.ResolveHoldForwardAction_FAIL,
819+
lnrpc.Payment_FAILED,
820+
)
821+
822+
// Finally, close channels.
823+
ht.CloseChannel(alice, cpAB)
824+
ht.CloseChannel(bob, cpBC)
825+
}
826+
827+
func sendPaymentInterceptAndCancel(ht *lntest.HarnessTest,
828+
ts *interceptorTestScenario, cpAB *lnrpc.ChannelPoint,
829+
interceptorAction routerrpc.ResolveHoldForwardAction,
830+
expectedPaymentStatus lnrpc.Payment_PaymentStatus) {
831+
832+
// Prepare the test cases.
833+
alice, bob, carol := ts.alice, ts.bob, ts.carol
834+
835+
// Connect the interceptor.
836+
interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor()
837+
838+
// Prepare the test cases.
839+
addResponse := carol.RPC.AddInvoice(&lnrpc.Invoice{
840+
ValueMsat: 1000,
841+
})
842+
invoice := carol.RPC.LookupInvoice(addResponse.RHash)
843+
844+
// We initiate a payment from Alice and define the payment context
845+
// cancellable.
846+
ctx, cancelPaymentContext := context.WithCancel(context.Background())
847+
var paymentStream rpc.PaymentClient
848+
go func() {
849+
req := &routerrpc.SendPaymentRequest{
850+
PaymentRequest: invoice.PaymentRequest,
851+
TimeoutSeconds: 60,
852+
FeeLimitSat: 100000,
853+
Cancelable: true,
854+
}
855+
856+
paymentStream = alice.RPC.SendPaymentWithContext(ctx, req)
857+
}()
858+
859+
// We start the htlc interceptor with a simple implementation that
860+
// saves all intercepted packets. These packets are held to simulate a
861+
// pending payment.
862+
packet := ht.ReceiveHtlcInterceptor(interceptor)
863+
864+
// Here we should wait for the channel to contain a pending htlc, and
865+
// also be shown as being active.
866+
ht.AssertIncomingHTLCActive(bob, cpAB, invoice.RHash)
867+
868+
// Ensure that Alice's payment is in-flight because Bob is holding the
869+
// htlc.
870+
ht.AssertPaymentStatusFromStream(paymentStream, lnrpc.Payment_IN_FLIGHT)
871+
872+
// Cancel the payment context. This should end the payment stream
873+
// context, but the payment should still be in state in-flight without a
874+
// failure reason.
875+
cancelPaymentContext()
876+
877+
var preimage lntypes.Preimage
878+
copy(preimage[:], invoice.RPreimage)
879+
payment := ht.AssertPaymentStatus(
880+
alice, preimage, lnrpc.Payment_IN_FLIGHT,
881+
)
882+
reasonNone := lnrpc.PaymentFailureReason_FAILURE_REASON_NONE
883+
require.Equal(ht, reasonNone, payment.FailureReason)
884+
885+
// Bob sends the interceptor action to the intercepted htlc.
886+
err := interceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
887+
IncomingCircuitKey: packet.IncomingCircuitKey,
888+
Action: interceptorAction,
889+
})
890+
require.NoError(ht, err, "failed to send request")
891+
892+
// Assert that the payment status is as expected.
893+
ht.AssertPaymentStatus(alice, preimage, expectedPaymentStatus)
894+
895+
// Since the payment context was cancelled, no further payment attempts
896+
// should've been made, and we observe FAILURE_REASON_CANCELED.
897+
expectedReason := lnrpc.PaymentFailureReason_FAILURE_REASON_CANCELED
898+
ht.AssertPaymentFailureReason(alice, preimage, expectedReason)
899+
900+
// Cancel the context, which will disconnect the above interceptor.
901+
cancelInterceptor()
902+
}

lntest/harness_assertion.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1639,6 +1639,24 @@ func (h *HarnessTest) AssertPaymentStatus(hn *node.HarnessNode,
16391639
return target
16401640
}
16411641

1642+
// AssertPaymentFailureReason asserts that the given node lists a payment with
1643+
// the given preimage which has the expected failure reason.
1644+
func (h *HarnessTest) AssertPaymentFailureReason(hn *node.HarnessNode,
1645+
preimage lntypes.Preimage, reason lnrpc.PaymentFailureReason) {
1646+
1647+
payHash := preimage.Hash()
1648+
err := wait.NoError(func() error {
1649+
p := h.findPayment(hn, payHash.String())
1650+
if reason == p.FailureReason {
1651+
return nil
1652+
}
1653+
1654+
return fmt.Errorf("payment: %v failure reason not match, "+
1655+
"want %s got %s", payHash, reason, p.Status)
1656+
}, DefaultTimeout)
1657+
require.NoError(h, err, "timeout checking payment failure reason")
1658+
}
1659+
16421660
// AssertActiveNodesSynced asserts all active nodes have synced to the chain.
16431661
func (h *HarnessTest) AssertActiveNodesSynced() {
16441662
for _, node := range h.manager.activeNodes {

lntest/rpc/router.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,29 @@ func (h *HarnessRPC) SendPayment(
3636
req *routerrpc.SendPaymentRequest) PaymentClient {
3737

3838
// SendPayment needs to have the context alive for the entire test case
39-
// as the router relies on the context to propagate HTLCs. Thus we use
39+
// as the router relies on the context to propagate HTLCs. Thus, we use
4040
// runCtx here instead of a timeout context.
4141
stream, err := h.Router.SendPaymentV2(h.runCtx, req)
4242
h.NoError(err, "SendPaymentV2")
4343

4444
return stream
4545
}
4646

47+
// SendPaymentWithContext sends a payment using the given node and payment
48+
// request and does so with the passed in context.
49+
func (h *HarnessRPC) SendPaymentWithContext(context context.Context,
50+
req *routerrpc.SendPaymentRequest) PaymentClient {
51+
52+
require.NotNil(h.T, context, "context must not be nil")
53+
54+
// SendPayment needs to have the context alive for the entire test case
55+
// as the router relies on the context to propagate HTLCs.
56+
stream, err := h.Router.SendPaymentV2(context, req)
57+
h.NoError(err, "SendPaymentV2")
58+
59+
return stream
60+
}
61+
4762
type HtlcEventsClient routerrpc.Router_SubscribeHtlcEventsClient
4863

4964
// SubscribeHtlcEvents makes a subscription to the HTLC events and returns a

0 commit comments

Comments
 (0)