Skip to content

Commit

Permalink
itest: add basic invoice acceptor integration test
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ffranr committed May 3, 2024
1 parent 91d56c7 commit 989cc26
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 0 deletions.
4 changes: 4 additions & 0 deletions itest/list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,10 @@ var allTestCases = []*lntest.TestCase{
Name: "forward interceptor",
TestFunc: testForwardInterceptorBasic,
},
{
Name: "invoice acceptor basic",
TestFunc: testInvoiceAcceptorBasic,
},
{
Name: "zero conf channel open",
TestFunc: testZeroConfChannelOpen,
Expand Down
230 changes: 230 additions & 0 deletions itest/lnd_invoice_acceptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package itest

import (
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)

// testInvoiceAcceptorBasic tests the basic functionality of the invoice
// acceptor RPC server.
func testInvoiceAcceptorBasic(ht *lntest.HarnessTest) {
ts := newAcceptorTestScenario(ht)

alice, bob, carol := ts.alice, ts.bob, ts.carol

// Open and wait for channels.
const chanAmt = btcutil.Amount(300000)
p := lntest.OpenChannelParams{Amt: chanAmt}
reqs := []*lntest.OpenChannelRequest{
{Local: alice, Remote: bob, Param: p},
{Local: bob, Remote: carol, Param: p},
}
resp := ht.OpenMultiChannelsAsync(reqs)
cpAB, cpBC := resp[0], resp[1]

// Make sure Alice is aware of channel Bob=>Carol.
ht.AssertTopologyChannelOpen(alice, cpBC)

// Initiate Carol's invoice acceptor.
invoiceAcceptor, cancelInvoiceAcceptor := carol.RPC.InvoiceAcceptor()

// Prepare the test cases.
testCases := ts.prepareTestCases()

for tcIdx, tc := range testCases {
ht.Logf("Running test case: %d", tcIdx)

// Initiate a payment from Alice to Carol in a separate
// goroutine. We use a separate goroutine to avoid blocking the
// main goroutine where we will make use of the invoice
// acceptor.
sendPaymentDone := make(chan struct{})
go func() {
// Signal that all the payments have been sent.
defer close(sendPaymentDone)

_ = ts.sendPayment(tc)
}()

acceptorRequest := ht.ReceiveInvoiceAcceptor(invoiceAcceptor)

// Sanity check the acceptor request.
require.EqualValues(ht, tc.invoiceAmountMsat,
acceptorRequest.Invoice.ValueMsat)
require.EqualValues(ht, tc.sendAmountMsat,
acceptorRequest.ExitHtlcAmt)

preimage, err := lntypes.MakePreimage(tc.invoice.RPreimage)
require.NoError(ht, err, "failed to parse invoice preimage")

// For all other packets we resolve according to the test case.
err = invoiceAcceptor.Send(
&invoicesrpc.InvoiceAcceptorResponse{
Preimage: preimage[:],
SkipAmountCheck: tc.skipAmtCheck,
},
)
require.NoError(ht, err, "failed to send request")

ht.Log("Waiting for payment send to complete")
select {
case <-sendPaymentDone:
ht.Log("Payment send attempt complete")
case <-time.After(defaultTimeout):
require.Fail(ht, "timeout waiting for payment send")
}

ht.Log("Ensure invoice status is settled")
require.Eventually(ht, func() bool {
updatedInvoice := carol.RPC.LookupInvoice(
tc.invoice.RHash,
)

return updatedInvoice.State == tc.finalInvoiceState
}, defaultTimeout, 1*time.Second)
}

cancelInvoiceAcceptor()

// Finally, close channels.
ht.CloseChannel(alice, cpAB)
ht.CloseChannel(bob, cpBC)
}

// acceptorTestCase is a helper struct to hold test case data.
type acceptorTestCase struct {
// invoiceAmountMsat is the amount of the invoice.
invoiceAmountMsat int64

// sendAmountMsat is the amount that will be sent in the payment.
sendAmountMsat int64

// skipAmtCheck is a flag that indicates whether the amount checks
// should be skipped during the invoice settlement process.
skipAmtCheck bool

// finalInvoiceState is the expected eventual final state of the
// invoice.
finalInvoiceState lnrpc.Invoice_InvoiceState

// payAddr is the payment address of the invoice.
payAddr []byte

// invoice is the invoice that will be paid.
invoice *lnrpc.Invoice
}

// acceptorTestScenario is a helper struct to hold the test context and provides
// helpful functionality.
type acceptorTestScenario struct {
ht *lntest.HarnessTest
alice, bob, carol *node.HarnessNode
}

// newAcceptorTestScenario initializes a new test scenario with three nodes and
// connects them to have the following topology,
//
// Alice --> Bob --> Carol
//
// Among them, Alice and Bob are standby nodes and Carol is a new node.
func newAcceptorTestScenario(
ht *lntest.HarnessTest) *acceptorTestScenario {

alice, bob := ht.Alice, ht.Bob
carol := ht.NewNode("carol", nil)

ht.EnsureConnected(alice, bob)
ht.EnsureConnected(bob, carol)

return &acceptorTestScenario{
ht: ht,
alice: alice,
bob: bob,
carol: carol,
}
}

// prepareTestCases prepares test cases.
func (c *acceptorTestScenario) prepareTestCases() []*acceptorTestCase {
cases := []*acceptorTestCase{
// Send a payment with amount less than the invoice amount.
// Amount checking is skipped during the invoice settlement
// process. The sent payment should eventually result in the
// invoice being settled.
{
invoiceAmountMsat: 9000,
sendAmountMsat: 1000,
skipAmtCheck: true,
finalInvoiceState: lnrpc.Invoice_SETTLED,
},
}

for _, t := range cases {
inv := &lnrpc.Invoice{ValueMsat: t.invoiceAmountMsat}
addResponse := c.carol.RPC.AddInvoice(inv)
invoice := c.carol.RPC.LookupInvoice(addResponse.RHash)

// We'll need to also decode the returned invoice so we can
// grab the payment address which is now required for ALL
// payments.
payReq := c.carol.RPC.DecodePayReq(invoice.PaymentRequest)

t.invoice = invoice
t.payAddr = payReq.PaymentAddr
}

return cases
}

// buildRoute is a helper function to build a route with given hops.
func (c *acceptorTestScenario) buildRoute(amtMsat int64,
hops []*node.HarnessNode, payAddr []byte) *lnrpc.Route {

rpcHops := make([][]byte, 0, len(hops))
for _, hop := range hops {
k := hop.PubKeyStr
pubkey, err := route.NewVertexFromStr(k)
require.NoErrorf(c.ht, err, "error parsing %v: %v", k, err)
rpcHops = append(rpcHops, pubkey[:])
}

req := &routerrpc.BuildRouteRequest{
AmtMsat: amtMsat,
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
HopPubkeys: rpcHops,
PaymentAddr: payAddr,
}

routeResp := c.alice.RPC.BuildRoute(req)

return routeResp.Route
}

// sendPaymentAndAssertAction sends a payment from alice to carol.
func (c *acceptorTestScenario) sendPayment(
tc *acceptorTestCase) *lnrpc.HTLCAttempt {

// Build a route from alice to carol.
aliceBobCarolRoute := c.buildRoute(
tc.sendAmountMsat, []*node.HarnessNode{c.bob, c.carol},
tc.payAddr,
)

// Send the payment.
sendReq := &routerrpc.SendToRouteRequest{
PaymentHash: tc.invoice.RHash,
Route: aliceBobCarolRoute,
}

return c.alice.RPC.SendToRouteV2(sendReq)
}

0 comments on commit 989cc26

Please sign in to comment.