Skip to content

Commit 989cc26

Browse files
committed
itest: add basic invoice acceptor integration test
This commit introduces a basic integration test for the invoice acceptor. The test covers scenarios where an invoice is settled with a payment that is less than the invoice amount, facilitated by the invoice settlement acceptor.
1 parent 91d56c7 commit 989cc26

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,10 @@ var allTestCases = []*lntest.TestCase{
422422
Name: "forward interceptor",
423423
TestFunc: testForwardInterceptorBasic,
424424
},
425+
{
426+
Name: "invoice acceptor basic",
427+
TestFunc: testInvoiceAcceptorBasic,
428+
},
425429
{
426430
Name: "zero conf channel open",
427431
TestFunc: testZeroConfChannelOpen,

itest/lnd_invoice_acceptor_test.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package itest
2+
3+
import (
4+
"time"
5+
6+
"github.com/btcsuite/btcd/btcutil"
7+
"github.com/lightningnetwork/lnd/chainreg"
8+
"github.com/lightningnetwork/lnd/lnrpc"
9+
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
10+
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
11+
"github.com/lightningnetwork/lnd/lntest"
12+
"github.com/lightningnetwork/lnd/lntest/node"
13+
"github.com/lightningnetwork/lnd/lntypes"
14+
"github.com/lightningnetwork/lnd/routing/route"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
// testInvoiceAcceptorBasic tests the basic functionality of the invoice
19+
// acceptor RPC server.
20+
func testInvoiceAcceptorBasic(ht *lntest.HarnessTest) {
21+
ts := newAcceptorTestScenario(ht)
22+
23+
alice, bob, carol := ts.alice, ts.bob, ts.carol
24+
25+
// Open and wait for channels.
26+
const chanAmt = btcutil.Amount(300000)
27+
p := lntest.OpenChannelParams{Amt: chanAmt}
28+
reqs := []*lntest.OpenChannelRequest{
29+
{Local: alice, Remote: bob, Param: p},
30+
{Local: bob, Remote: carol, Param: p},
31+
}
32+
resp := ht.OpenMultiChannelsAsync(reqs)
33+
cpAB, cpBC := resp[0], resp[1]
34+
35+
// Make sure Alice is aware of channel Bob=>Carol.
36+
ht.AssertTopologyChannelOpen(alice, cpBC)
37+
38+
// Initiate Carol's invoice acceptor.
39+
invoiceAcceptor, cancelInvoiceAcceptor := carol.RPC.InvoiceAcceptor()
40+
41+
// Prepare the test cases.
42+
testCases := ts.prepareTestCases()
43+
44+
for tcIdx, tc := range testCases {
45+
ht.Logf("Running test case: %d", tcIdx)
46+
47+
// Initiate a payment from Alice to Carol in a separate
48+
// goroutine. We use a separate goroutine to avoid blocking the
49+
// main goroutine where we will make use of the invoice
50+
// acceptor.
51+
sendPaymentDone := make(chan struct{})
52+
go func() {
53+
// Signal that all the payments have been sent.
54+
defer close(sendPaymentDone)
55+
56+
_ = ts.sendPayment(tc)
57+
}()
58+
59+
acceptorRequest := ht.ReceiveInvoiceAcceptor(invoiceAcceptor)
60+
61+
// Sanity check the acceptor request.
62+
require.EqualValues(ht, tc.invoiceAmountMsat,
63+
acceptorRequest.Invoice.ValueMsat)
64+
require.EqualValues(ht, tc.sendAmountMsat,
65+
acceptorRequest.ExitHtlcAmt)
66+
67+
preimage, err := lntypes.MakePreimage(tc.invoice.RPreimage)
68+
require.NoError(ht, err, "failed to parse invoice preimage")
69+
70+
// For all other packets we resolve according to the test case.
71+
err = invoiceAcceptor.Send(
72+
&invoicesrpc.InvoiceAcceptorResponse{
73+
Preimage: preimage[:],
74+
SkipAmountCheck: tc.skipAmtCheck,
75+
},
76+
)
77+
require.NoError(ht, err, "failed to send request")
78+
79+
ht.Log("Waiting for payment send to complete")
80+
select {
81+
case <-sendPaymentDone:
82+
ht.Log("Payment send attempt complete")
83+
case <-time.After(defaultTimeout):
84+
require.Fail(ht, "timeout waiting for payment send")
85+
}
86+
87+
ht.Log("Ensure invoice status is settled")
88+
require.Eventually(ht, func() bool {
89+
updatedInvoice := carol.RPC.LookupInvoice(
90+
tc.invoice.RHash,
91+
)
92+
93+
return updatedInvoice.State == tc.finalInvoiceState
94+
}, defaultTimeout, 1*time.Second)
95+
}
96+
97+
cancelInvoiceAcceptor()
98+
99+
// Finally, close channels.
100+
ht.CloseChannel(alice, cpAB)
101+
ht.CloseChannel(bob, cpBC)
102+
}
103+
104+
// acceptorTestCase is a helper struct to hold test case data.
105+
type acceptorTestCase struct {
106+
// invoiceAmountMsat is the amount of the invoice.
107+
invoiceAmountMsat int64
108+
109+
// sendAmountMsat is the amount that will be sent in the payment.
110+
sendAmountMsat int64
111+
112+
// skipAmtCheck is a flag that indicates whether the amount checks
113+
// should be skipped during the invoice settlement process.
114+
skipAmtCheck bool
115+
116+
// finalInvoiceState is the expected eventual final state of the
117+
// invoice.
118+
finalInvoiceState lnrpc.Invoice_InvoiceState
119+
120+
// payAddr is the payment address of the invoice.
121+
payAddr []byte
122+
123+
// invoice is the invoice that will be paid.
124+
invoice *lnrpc.Invoice
125+
}
126+
127+
// acceptorTestScenario is a helper struct to hold the test context and provides
128+
// helpful functionality.
129+
type acceptorTestScenario struct {
130+
ht *lntest.HarnessTest
131+
alice, bob, carol *node.HarnessNode
132+
}
133+
134+
// newAcceptorTestScenario initializes a new test scenario with three nodes and
135+
// connects them to have the following topology,
136+
//
137+
// Alice --> Bob --> Carol
138+
//
139+
// Among them, Alice and Bob are standby nodes and Carol is a new node.
140+
func newAcceptorTestScenario(
141+
ht *lntest.HarnessTest) *acceptorTestScenario {
142+
143+
alice, bob := ht.Alice, ht.Bob
144+
carol := ht.NewNode("carol", nil)
145+
146+
ht.EnsureConnected(alice, bob)
147+
ht.EnsureConnected(bob, carol)
148+
149+
return &acceptorTestScenario{
150+
ht: ht,
151+
alice: alice,
152+
bob: bob,
153+
carol: carol,
154+
}
155+
}
156+
157+
// prepareTestCases prepares test cases.
158+
func (c *acceptorTestScenario) prepareTestCases() []*acceptorTestCase {
159+
cases := []*acceptorTestCase{
160+
// Send a payment with amount less than the invoice amount.
161+
// Amount checking is skipped during the invoice settlement
162+
// process. The sent payment should eventually result in the
163+
// invoice being settled.
164+
{
165+
invoiceAmountMsat: 9000,
166+
sendAmountMsat: 1000,
167+
skipAmtCheck: true,
168+
finalInvoiceState: lnrpc.Invoice_SETTLED,
169+
},
170+
}
171+
172+
for _, t := range cases {
173+
inv := &lnrpc.Invoice{ValueMsat: t.invoiceAmountMsat}
174+
addResponse := c.carol.RPC.AddInvoice(inv)
175+
invoice := c.carol.RPC.LookupInvoice(addResponse.RHash)
176+
177+
// We'll need to also decode the returned invoice so we can
178+
// grab the payment address which is now required for ALL
179+
// payments.
180+
payReq := c.carol.RPC.DecodePayReq(invoice.PaymentRequest)
181+
182+
t.invoice = invoice
183+
t.payAddr = payReq.PaymentAddr
184+
}
185+
186+
return cases
187+
}
188+
189+
// buildRoute is a helper function to build a route with given hops.
190+
func (c *acceptorTestScenario) buildRoute(amtMsat int64,
191+
hops []*node.HarnessNode, payAddr []byte) *lnrpc.Route {
192+
193+
rpcHops := make([][]byte, 0, len(hops))
194+
for _, hop := range hops {
195+
k := hop.PubKeyStr
196+
pubkey, err := route.NewVertexFromStr(k)
197+
require.NoErrorf(c.ht, err, "error parsing %v: %v", k, err)
198+
rpcHops = append(rpcHops, pubkey[:])
199+
}
200+
201+
req := &routerrpc.BuildRouteRequest{
202+
AmtMsat: amtMsat,
203+
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
204+
HopPubkeys: rpcHops,
205+
PaymentAddr: payAddr,
206+
}
207+
208+
routeResp := c.alice.RPC.BuildRoute(req)
209+
210+
return routeResp.Route
211+
}
212+
213+
// sendPaymentAndAssertAction sends a payment from alice to carol.
214+
func (c *acceptorTestScenario) sendPayment(
215+
tc *acceptorTestCase) *lnrpc.HTLCAttempt {
216+
217+
// Build a route from alice to carol.
218+
aliceBobCarolRoute := c.buildRoute(
219+
tc.sendAmountMsat, []*node.HarnessNode{c.bob, c.carol},
220+
tc.payAddr,
221+
)
222+
223+
// Send the payment.
224+
sendReq := &routerrpc.SendToRouteRequest{
225+
PaymentHash: tc.invoice.RHash,
226+
Route: aliceBobCarolRoute,
227+
}
228+
229+
return c.alice.RPC.SendToRouteV2(sendReq)
230+
}

0 commit comments

Comments
 (0)