-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
2 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |