Skip to content

Commit c46b1a4

Browse files
authored
Merge pull request #8836 from hieblmi/payment-failure-reason-cancel
routing: add payment failure reason `FailureReasonCancel`
2 parents 4a3c4e4 + 808d958 commit c46b1a4

File tree

13 files changed

+596
-398
lines changed

13 files changed

+596
-398
lines changed

channeldb/payments.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ var (
104104
)
105105

106106
var (
107-
// ErrNoSequenceNumber is returned if we lookup a payment which does
107+
// ErrNoSequenceNumber is returned if we look up a payment which does
108108
// not have a sequence number.
109109
ErrNoSequenceNumber = errors.New("sequence number not found")
110110

@@ -147,18 +147,20 @@ const (
147147
// balance to complete the payment.
148148
FailureReasonInsufficientBalance FailureReason = 4
149149

150-
// TODO(halseth): cancel state.
150+
// FailureReasonCanceled indicates that the payment was canceled by the
151+
// user.
152+
FailureReasonCanceled FailureReason = 5
151153

152154
// TODO(joostjager): Add failure reasons for:
153155
// LocalLiquidityInsufficient, RemoteCapacityInsufficient.
154156
)
155157

156-
// Error returns a human readable error string for the FailureReason.
158+
// Error returns a human-readable error string for the FailureReason.
157159
func (r FailureReason) Error() string {
158160
return r.String()
159161
}
160162

161-
// String returns a human readable FailureReason.
163+
// String returns a human-readable FailureReason.
162164
func (r FailureReason) String() string {
163165
switch r {
164166
case FailureReasonTimeout:
@@ -171,6 +173,8 @@ func (r FailureReason) String() string {
171173
return "incorrect_payment_details"
172174
case FailureReasonInsufficientBalance:
173175
return "insufficient_balance"
176+
case FailureReasonCanceled:
177+
return "canceled"
174178
}
175179

176180
return "unknown"

docs/release-notes/release-notes-0.18.3.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ commitment when the channel was force closed.
114114
`--amp` flag when sending a payment specifying the payment request.
115115

116116
## Code Health
117+
118+
* [Added](https://github.com/lightningnetwork/lnd/pull/8836) a new failure
119+
reason `FailureReasonCanceled` to the list of payment failure reasons. It
120+
indicates that a payment was manually cancelled by the user.
121+
117122
## Breaking Changes
118123
## Performance Improvements
119124

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: 148 additions & 14 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,19 +14,21 @@ 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

20-
// testSendDirectPayment creates a topology Alice->Bob and then tests that
21-
// Alice can send a direct payment to Bob. This test modifies the fee estimator
22-
// to return floor fee rate(1 sat/vb).
23+
// testSendDirectPayment creates a topology Alice->Bob and then tests that Alice
24+
// can send a direct payment to Bob. This test modifies the fee estimator to
25+
// return floor fee rate(1 sat/vb).
2326
func testSendDirectPayment(ht *lntest.HarnessTest) {
2427
// Grab Alice and Bob's nodes for convenience.
2528
alice, bob := ht.Alice, ht.Bob
2629

2730
// Create a list of commitment types we want to test.
28-
commitTyes := []lnrpc.CommitmentType{
31+
commitmentTypes := []lnrpc.CommitmentType{
2932
lnrpc.CommitmentType_ANCHORS,
3033
lnrpc.CommitmentType_SIMPLE_TAPROOT,
3134
}
@@ -109,7 +112,7 @@ func testSendDirectPayment(ht *lntest.HarnessTest) {
109112
}
110113

111114
// Run the test cases.
112-
for _, ct := range commitTyes {
115+
for _, ct := range commitmentTypes {
113116
ht.Run(ct.String(), func(t *testing.T) {
114117
st := ht.Subtest(t)
115118

@@ -132,8 +135,9 @@ func testSendDirectPayment(ht *lntest.HarnessTest) {
132135
}
133136

134137
// Open private channel for taproot channels.
135-
params.Private = ct ==
136-
lnrpc.CommitmentType_SIMPLE_TAPROOT
138+
if ct == lnrpc.CommitmentType_SIMPLE_TAPROOT {
139+
params.Private = true
140+
}
137141

138142
testSendPayment(st, params)
139143
})
@@ -429,7 +433,7 @@ func runAsyncPayments(ht *lntest.HarnessTest, alice, bob *node.HarnessNode,
429433
// likely be lower, but we can't guarantee that any more HTLCs will
430434
// succeed due to the limited path diversity and inability of the router
431435
// to retry via another path.
432-
numInvoices := int(input.MaxHTLCNumber / 2)
436+
numInvoices := input.MaxHTLCNumber / 2
433437

434438
bobAmt := int64(numInvoices * paymentAmt)
435439
aliceAmt := info.LocalBalance - bobAmt
@@ -534,10 +538,10 @@ func testBidirectionalAsyncPayments(ht *lntest.HarnessTest) {
534538

535539
// We'll create a number of invoices equal the max number of HTLCs that
536540
// can be carried in one direction. The number on the commitment will
537-
// likely be lower, but we can't guarantee that any more HTLCs will
538-
// succeed due to the limited path diversity and inability of the router
539-
// to retry via another path.
540-
numInvoices := int(input.MaxHTLCNumber / 2)
541+
// likely be lower, but we can't guarantee that more HTLCs will succeed
542+
// due to the limited path diversity and inability of the router to
543+
// retry via another path.
544+
numInvoices := input.MaxHTLCNumber / 2
541545

542546
// Nodes should exchange the same amount of money and because of this
543547
// at the end balances should remain the same.
@@ -597,7 +601,7 @@ func testBidirectionalAsyncPayments(ht *lntest.HarnessTest) {
597601
assertChannelState(ht, alice, chanPoint, aliceAmt, bobAmt)
598602

599603
// Next query for Bob's and Alice's channel states, in order to confirm
600-
// that all payment have been successful transmitted.
604+
// that all payment have been successfully transmitted.
601605
assertChannelState(ht, bob, chanPoint, bobAmt, aliceAmt)
602606

603607
// Finally, immediately close the channel. This function will also
@@ -662,7 +666,7 @@ func testInvoiceSubscriptions(ht *lntest.HarnessTest) {
662666

663667
// Now that the set of invoices has been added, we'll re-register for
664668
// streaming invoice notifications for Bob, this time specifying the
665-
// add invoice of the last prior invoice.
669+
// add index of the last prior invoice.
666670
req = &lnrpc.InvoiceSubscription{AddIndex: lastAddIndex}
667671
bobInvoiceSubscription = bob.RPC.SubscribeInvoices(req)
668672

@@ -766,3 +770,133 @@ func assertChannelState(ht *lntest.HarnessTest, hn *node.HarnessNode,
766770
}, lntest.DefaultTimeout)
767771
require.NoError(ht, err, "timeout while chekcing for balance")
768772
}
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+
}

0 commit comments

Comments
 (0)