Skip to content

Commit 941716d

Browse files
committed
lntest+itest: add new test testPaymentHTLCTimeout
This commit adds a new test case to validate that when an HTLC has timed out, the corresponding payment is marked as failed.
1 parent 0928ba0 commit 941716d

File tree

3 files changed

+332
-3
lines changed

3 files changed

+332
-3
lines changed

itest/list_on_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,4 +654,12 @@ var allTestCases = []*lntest.TestCase{
654654
Name: "coop close with external delivery",
655655
TestFunc: testCoopCloseWithExternalDelivery,
656656
},
657+
{
658+
Name: "payment failed htlc local swept",
659+
TestFunc: testPaymentFailedHTLCLocalSwept,
660+
},
661+
{
662+
Name: "payment succeeded htlc remote swept",
663+
TestFunc: testPaymentSucceededHTLCRemoteSwept,
664+
},
657665
}

itest/lnd_payment_test.go

Lines changed: 314 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import (
1010

1111
"github.com/btcsuite/btcd/btcutil"
1212
"github.com/lightningnetwork/lnd/input"
13+
"github.com/lightningnetwork/lnd/lncfg"
1314
"github.com/lightningnetwork/lnd/lnrpc"
15+
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
1416
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1517
"github.com/lightningnetwork/lnd/lntest"
1618
"github.com/lightningnetwork/lnd/lntest/node"
@@ -20,9 +22,318 @@ import (
2022
"github.com/stretchr/testify/require"
2123
)
2224

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).
25+
// testPaymentSucceededHTLCRemoteSwept checks that when an outgoing HTLC is
26+
// timed out and is swept by the remote via the direct preimage spend path, the
27+
// payment will be marked as succeeded. This test creates a topology from Alice
28+
// -> Bob, and let Alice send payments to Bob. Bob then goes offline, such that
29+
// Alice's outgoing HTLC will time out. Once the force close transaction is
30+
// broadcast by Alice, she then goes offline and Bob comes back online to take
31+
// her outgoing HTLC. And Alice should mark this payment as succeeded after she
32+
// comes back online again.
33+
func testPaymentSucceededHTLCRemoteSwept(ht *lntest.HarnessTest) {
34+
// Set the feerate to be 10 sat/vb.
35+
ht.SetFeeEstimate(2500)
36+
37+
// Open a channel with 100k satoshis between Alice and Bob with Alice
38+
// being the sole funder of the channel.
39+
chanAmt := btcutil.Amount(100_000)
40+
openChannelParams := lntest.OpenChannelParams{
41+
Amt: chanAmt,
42+
}
43+
44+
// Create a two hop network: Alice -> Bob.
45+
chanPoints, nodes := createSimpleNetwork(ht, nil, 2, openChannelParams)
46+
chanPoint := chanPoints[0]
47+
alice, bob := nodes[0], nodes[1]
48+
49+
// We now create two payments, one above dust and the other below dust,
50+
// and we should see different behavior in terms of when the payment
51+
// will be marked as failed due to the HTLC timeout.
52+
//
53+
// First, create random preimages.
54+
preimage := ht.RandomPreimage()
55+
dustPreimage := ht.RandomPreimage()
56+
57+
// Get the preimage hashes.
58+
payHash := preimage.Hash()
59+
dustPayHash := dustPreimage.Hash()
60+
61+
// Create an hold invoice for Bob which expects a payment of 10k
62+
// satoshis from Alice.
63+
const paymentAmt = 10_000
64+
req := &invoicesrpc.AddHoldInvoiceRequest{
65+
Value: paymentAmt,
66+
Hash: payHash[:],
67+
// Use a small CLTV value so we can mine fewer blocks.
68+
CltvExpiry: finalCltvDelta,
69+
}
70+
invoice := bob.RPC.AddHoldInvoice(req)
71+
72+
// Create another hold invoice for Bob which expects a payment of 1k
73+
// satoshis from Alice.
74+
const dustAmt = 1000
75+
req = &invoicesrpc.AddHoldInvoiceRequest{
76+
Value: dustAmt,
77+
Hash: dustPayHash[:],
78+
// Use a small CLTV value so we can mine fewer blocks.
79+
CltvExpiry: finalCltvDelta,
80+
}
81+
dustInvoice := bob.RPC.AddHoldInvoice(req)
82+
83+
// Alice now sends both payments to Bob.
84+
payReq := &routerrpc.SendPaymentRequest{
85+
PaymentRequest: invoice.PaymentRequest,
86+
TimeoutSeconds: 3600,
87+
}
88+
dustPayReq := &routerrpc.SendPaymentRequest{
89+
PaymentRequest: dustInvoice.PaymentRequest,
90+
TimeoutSeconds: 3600,
91+
}
92+
93+
// We expect the payment to stay in-flight from both streams.
94+
ht.SendPaymentAssertInflight(alice, payReq)
95+
ht.SendPaymentAssertInflight(alice, dustPayReq)
96+
97+
// We also check the payments are marked as IN_FLIGHT in Alice's
98+
// database.
99+
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_IN_FLIGHT)
100+
ht.AssertPaymentStatus(alice, dustPreimage, lnrpc.Payment_IN_FLIGHT)
101+
102+
// Bob should have two incoming HTLC.
103+
ht.AssertIncomingHTLCActive(bob, chanPoint, payHash[:])
104+
ht.AssertIncomingHTLCActive(bob, chanPoint, dustPayHash[:])
105+
106+
// Alice should have two outgoing HTLCs.
107+
ht.AssertOutgoingHTLCActive(alice, chanPoint, payHash[:])
108+
ht.AssertOutgoingHTLCActive(alice, chanPoint, dustPayHash[:])
109+
110+
// Let Bob go offline.
111+
restartBob := ht.SuspendNode(bob)
112+
113+
// Alice force closes the channel, which puts her commitment tx into
114+
// the mempool.
115+
ht.CloseChannelAssertPending(alice, chanPoint, true)
116+
117+
// We now let Alice go offline to avoid her sweeping her outgoing htlc.
118+
restartAlice := ht.SuspendNode(alice)
119+
120+
// Mine one block to confirm Alice's force closing tx.
121+
ht.MineBlocksAndAssertNumTxes(1, 1)
122+
123+
// Restart Bob to settle the invoice and sweep the htlc output.
124+
require.NoError(ht, restartBob())
125+
126+
// Bob now settles the invoices, since his link with Alice is broken,
127+
// Alice won't know the preimages.
128+
bob.RPC.SettleInvoice(preimage[:])
129+
bob.RPC.SettleInvoice(dustPreimage[:])
130+
131+
// Once Bob comes back up, he should find the force closing transaction
132+
// from Alice and try to sweep the non-dust outgoing htlc via the
133+
// direct preimage spend.
134+
ht.AssertNumPendingSweeps(bob, 1)
135+
136+
// Mine a block to trigger the sweep.
137+
//
138+
// TODO(yy): remove it once `blockbeat` is implemented.
139+
ht.MineEmptyBlocks(1)
140+
141+
// Mine Bob's sweeping tx.
142+
ht.MineBlocksAndAssertNumTxes(1, 1)
143+
144+
// Let Alice come back up. Since the channel is now closed, we expect
145+
// different behaviors based on whether the HTLC is a dust.
146+
// - For dust payment, it should be failed now as the HTLC won't go
147+
// onchain.
148+
// - For non-dust payment, it should be marked as succeeded since her
149+
// outgoing htlc is swept by Bob.
150+
require.NoError(ht, restartAlice())
151+
152+
// Since Alice is restarted, we need to track the payments again.
153+
payStream := alice.RPC.TrackPaymentV2(payHash[:])
154+
dustPayStream := alice.RPC.TrackPaymentV2(dustPayHash[:])
155+
156+
// Check that the dust payment is failed in both the stream and DB.
157+
ht.AssertPaymentStatus(alice, dustPreimage, lnrpc.Payment_FAILED)
158+
ht.AssertPaymentStatusFromStream(dustPayStream, lnrpc.Payment_FAILED)
159+
160+
// We expect the non-dust payment to marked as succeeded in Alice's
161+
// database and also from her stream.
162+
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED)
163+
ht.AssertPaymentStatusFromStream(payStream, lnrpc.Payment_SUCCEEDED)
164+
}
165+
166+
// testPaymentFailedHTLCLocalSwept checks that when an outgoing HTLC is timed
167+
// out and claimed onchain via the timeout path, the payment will be marked as
168+
// failed. This test creates a topology from Alice -> Bob, and let Alice send
169+
// payments to Bob. Bob then goes offline, such that Alice's outgoing HTLC will
170+
// time out. Alice will also be restarted to make sure resumed payments are
171+
// also marked as failed.
172+
func testPaymentFailedHTLCLocalSwept(ht *lntest.HarnessTest) {
173+
success := ht.Run("fail payment", func(t *testing.T) {
174+
st := ht.Subtest(t)
175+
runTestPaymentHTLCTimeout(st, false)
176+
})
177+
if !success {
178+
return
179+
}
180+
181+
ht.Run("fail resumed payment", func(t *testing.T) {
182+
st := ht.Subtest(t)
183+
runTestPaymentHTLCTimeout(st, true)
184+
})
185+
}
186+
187+
// runTestPaymentHTLCTimeout is the helper function that actually runs the
188+
// testPaymentFailedHTLCLocalSwept.
189+
func runTestPaymentHTLCTimeout(ht *lntest.HarnessTest, restartAlice bool) {
190+
// Set the feerate to be 10 sat/vb.
191+
ht.SetFeeEstimate(2500)
192+
193+
// Open a channel with 100k satoshis between Alice and Bob with Alice
194+
// being the sole funder of the channel.
195+
chanAmt := btcutil.Amount(100_000)
196+
openChannelParams := lntest.OpenChannelParams{
197+
Amt: chanAmt,
198+
}
199+
200+
// Create a two hop network: Alice -> Bob.
201+
chanPoints, nodes := createSimpleNetwork(ht, nil, 2, openChannelParams)
202+
chanPoint := chanPoints[0]
203+
alice, bob := nodes[0], nodes[1]
204+
205+
// We now create two payments, one above dust and the other below dust,
206+
// and we should see different behavior in terms of when the payment
207+
// will be marked as failed due to the HTLC timeout.
208+
//
209+
// First, create random preimages.
210+
preimage := ht.RandomPreimage()
211+
dustPreimage := ht.RandomPreimage()
212+
213+
// Get the preimage hashes.
214+
payHash := preimage.Hash()
215+
dustPayHash := dustPreimage.Hash()
216+
217+
// Create an hold invoice for Bob which expects a payment of 10k
218+
// satoshis from Alice.
219+
const paymentAmt = 20_000
220+
req := &invoicesrpc.AddHoldInvoiceRequest{
221+
Value: paymentAmt,
222+
Hash: payHash[:],
223+
// Use a small CLTV value so we can mine fewer blocks.
224+
CltvExpiry: finalCltvDelta,
225+
}
226+
invoice := bob.RPC.AddHoldInvoice(req)
227+
228+
// Create another hold invoice for Bob which expects a payment of 1k
229+
// satoshis from Alice.
230+
const dustAmt = 1000
231+
req = &invoicesrpc.AddHoldInvoiceRequest{
232+
Value: dustAmt,
233+
Hash: dustPayHash[:],
234+
// Use a small CLTV value so we can mine fewer blocks.
235+
CltvExpiry: finalCltvDelta,
236+
}
237+
dustInvoice := bob.RPC.AddHoldInvoice(req)
238+
239+
// Alice now sends both the payments to Bob.
240+
payReq := &routerrpc.SendPaymentRequest{
241+
PaymentRequest: invoice.PaymentRequest,
242+
TimeoutSeconds: 3600,
243+
}
244+
dustPayReq := &routerrpc.SendPaymentRequest{
245+
PaymentRequest: dustInvoice.PaymentRequest,
246+
TimeoutSeconds: 3600,
247+
}
248+
249+
// We expect the payment to stay in-flight from both streams.
250+
ht.SendPaymentAssertInflight(alice, payReq)
251+
ht.SendPaymentAssertInflight(alice, dustPayReq)
252+
253+
// We also check the payments are marked as IN_FLIGHT in Alice's
254+
// database.
255+
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_IN_FLIGHT)
256+
ht.AssertPaymentStatus(alice, dustPreimage, lnrpc.Payment_IN_FLIGHT)
257+
258+
// Bob should have two incoming HTLC.
259+
ht.AssertIncomingHTLCActive(bob, chanPoint, payHash[:])
260+
ht.AssertIncomingHTLCActive(bob, chanPoint, dustPayHash[:])
261+
262+
// Alice should have two outgoing HTLCs.
263+
ht.AssertOutgoingHTLCActive(alice, chanPoint, payHash[:])
264+
ht.AssertOutgoingHTLCActive(alice, chanPoint, dustPayHash[:])
265+
266+
// Let Bob go offline.
267+
ht.Shutdown(bob)
268+
269+
// We'll now mine enough blocks to trigger Alice to broadcast her
270+
// commitment transaction due to the fact that the HTLC is about to
271+
// timeout. With the default outgoing broadcast delta of zero, this
272+
// will be the same height as the htlc expiry height.
273+
numBlocks := padCLTV(
274+
uint32(req.CltvExpiry - lncfg.DefaultOutgoingBroadcastDelta),
275+
)
276+
ht.MineBlocks(int(numBlocks))
277+
278+
// Restart Alice if requested.
279+
if restartAlice {
280+
// Restart Alice to test the resumed payment is canceled.
281+
ht.RestartNode(alice)
282+
}
283+
284+
// We now subscribe to the payment status.
285+
payStream := alice.RPC.TrackPaymentV2(payHash[:])
286+
dustPayStream := alice.RPC.TrackPaymentV2(dustPayHash[:])
287+
288+
// Mine a block to confirm Alice's closing transaction.
289+
ht.MineBlocksAndAssertNumTxes(1, 1)
290+
291+
// Now the channel is closed, we expect different behaviors based on
292+
// whether the HTLC is a dust. For dust payment, it should be failed
293+
// now as the HTLC won't go onchain. For non-dust payment, it should
294+
// still be inflight. It won't be marked as failed unless the outgoing
295+
// HTLC is resolved onchain.
296+
//
297+
// NOTE: it's possible for Bob to race against Alice using the
298+
// preimage path. If Bob successfully claims the HTLC, Alice should
299+
// mark the non-dust payment as succeeded.
300+
//
301+
// Check that the dust payment is failed in both the stream and DB.
302+
ht.AssertPaymentStatus(alice, dustPreimage, lnrpc.Payment_FAILED)
303+
ht.AssertPaymentStatusFromStream(dustPayStream, lnrpc.Payment_FAILED)
304+
305+
// Check that the non-dust payment is still in-flight.
306+
//
307+
// NOTE: we don't check the payment status from the stream here as
308+
// there's no new status being sent.
309+
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_IN_FLIGHT)
310+
311+
// We now have two possible cases for the non-dust payment:
312+
// - Bob stays offline, and Alice will sweep her outgoing HTLC, which
313+
// makes the payment failed.
314+
// - Bob comes back online, and claims the HTLC on Alice's commitment
315+
// via direct preimage spend, hence racing against Alice onchain. If
316+
// he succeeds, Alice should mark the payment as succeeded.
317+
//
318+
// TODO(yy): test the second case once we have the RPC to clean
319+
// mempool.
320+
321+
// Since Alice's force close transaction has been confirmed, she should
322+
// sweep her outgoing HTLC in next block.
323+
ht.MineBlocksAndAssertNumTxes(1, 1)
324+
325+
// Cleanup the channel.
326+
ht.CleanupForceClose(alice)
327+
328+
// We expect the non-dust payment to marked as failed in Alice's
329+
// database and also from her stream.
330+
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_FAILED)
331+
ht.AssertPaymentStatusFromStream(payStream, lnrpc.Payment_FAILED)
332+
}
333+
334+
// testSendDirectPayment creates a topology Alice->Bob and then tests that
335+
// Alice can send a direct payment to Bob. This test modifies the fee estimator
336+
// to return floor fee rate(1 sat/vb).
26337
func testSendDirectPayment(ht *lntest.HarnessTest) {
27338
// Grab Alice and Bob's nodes for convenience.
28339
alice, bob := ht.Alice, ht.Bob

lntest/harness.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,8 @@ func (h *HarnessTest) resetStandbyNodes(t *testing.T) {
399399
// config for the coming test. This will also inherit the
400400
// test's running context.
401401
h.RestartNodeWithExtraArgs(hn, hn.Cfg.OriginalExtraArgs)
402+
403+
hn.AddToLogf("Finished test case %v", h.manager.currentTestCase)
402404
}
403405
}
404406

@@ -1771,6 +1773,14 @@ func (h *HarnessTest) SendPaymentAssertSettled(hn *node.HarnessNode,
17711773
return h.SendPaymentAndAssertStatus(hn, req, lnrpc.Payment_SUCCEEDED)
17721774
}
17731775

1776+
// SendPaymentAssertInflight sends a payment from the passed node and asserts
1777+
// the payment is inflight.
1778+
func (h *HarnessTest) SendPaymentAssertInflight(hn *node.HarnessNode,
1779+
req *routerrpc.SendPaymentRequest) *lnrpc.Payment {
1780+
1781+
return h.SendPaymentAndAssertStatus(hn, req, lnrpc.Payment_IN_FLIGHT)
1782+
}
1783+
17741784
// OpenChannelRequest is used to open a channel using the method
17751785
// OpenMultiChannelsAsync.
17761786
type OpenChannelRequest struct {

0 commit comments

Comments
 (0)