Skip to content

Commit c0d2b53

Browse files
ffranrguggero
authored andcommitted
itest: add basic invoice HTLC modifier integration test
This commit introduces a basic integration test for the invoice HTLC modifier. The test covers scenarios where an invoice is settled with a payment that is less than the invoice amount, facilitated by the invoice HTLC modifier.
1 parent 08429c6 commit c0d2b53

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,10 @@ var allTestCases = []*lntest.TestCase{
434434
Name: "forward interceptor wire records",
435435
TestFunc: testForwardInterceptorWireRecords,
436436
},
437+
{
438+
Name: "invoice HTLC modifier basic",
439+
TestFunc: testInvoiceHtlcModifierBasic,
440+
},
437441
{
438442
Name: "zero conf channel open",
439443
TestFunc: testZeroConfChannelOpen,

itest/lnd_invoice_acceptor_test.go

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

0 commit comments

Comments
 (0)