mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
9a972e1b0c
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.
359 lines
10 KiB
Go
359 lines
10 KiB
Go
package itest
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/lightningnetwork/lnd/chainreg"
|
|
"github.com/lightningnetwork/lnd/invoices"
|
|
"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/lnwire"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// testInvoiceHtlcModifierBasic tests the basic functionality of the invoice
|
|
// HTLC modifier RPC server.
|
|
func testInvoiceHtlcModifierBasic(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 HTLC modifier.
|
|
invoiceModifier, cancelModifier := carol.RPC.InvoiceHtlcModifier()
|
|
|
|
// We need to wait a bit to make sure the gRPC stream is established
|
|
// correctly.
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Make sure we get an error if we try to register a second modifier and
|
|
// then try to use it (the error won't be returned on connect, only on
|
|
// the first _read_ interaction on the stream).
|
|
mod2, err := carol.RPC.Invoice.HtlcModifier(context.Background())
|
|
require.NoError(ht, err)
|
|
_, err = mod2.Recv()
|
|
require.ErrorContains(
|
|
ht, err,
|
|
invoices.ErrInterceptorClientAlreadyConnected.Error(),
|
|
)
|
|
|
|
// We also add a normal (forwarding) HTLC interceptor at Bob, so we can
|
|
// test that custom wire messages on the HTLC are forwarded correctly to
|
|
// the invoice HTLC interceptor.
|
|
bobInterceptor, bobInterceptorCancel := bob.RPC.HtlcInterceptor()
|
|
|
|
// 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)
|
|
}()
|
|
|
|
// First, intercept the HTLC at Bob.
|
|
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
|
|
err := bobInterceptor.Send(
|
|
&routerrpc.ForwardHtlcInterceptResponse{
|
|
IncomingCircuitKey: packet.IncomingCircuitKey,
|
|
OutAmountMsat: packet.OutgoingAmountMsat,
|
|
OutWireCustomRecords: tc.lastHopCustomRecords,
|
|
Action: actionResumeModify,
|
|
},
|
|
)
|
|
require.NoError(ht, err, "failed to send request")
|
|
|
|
modifierRequest := ht.ReceiveInvoiceHtlcModification(
|
|
invoiceModifier,
|
|
)
|
|
|
|
// Sanity check the modifier request.
|
|
require.EqualValues(
|
|
ht, tc.invoiceAmountMsat,
|
|
modifierRequest.Invoice.ValueMsat,
|
|
)
|
|
require.EqualValues(
|
|
ht, tc.sendAmountMsat, modifierRequest.ExitHtlcAmt,
|
|
)
|
|
require.Equal(
|
|
ht, tc.lastHopCustomRecords,
|
|
modifierRequest.ExitHtlcWireCustomRecords,
|
|
)
|
|
|
|
// For all other packets we resolve according to the test case.
|
|
amtPaid := uint64(tc.invoiceAmountMsat)
|
|
err = invoiceModifier.Send(
|
|
&invoicesrpc.HtlcModifyResponse{
|
|
CircuitKey: modifierRequest.ExitHtlcCircuitKey,
|
|
AmtPaid: &amtPaid,
|
|
},
|
|
)
|
|
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)
|
|
|
|
updatedInvoice := carol.RPC.LookupInvoice(
|
|
tc.invoice.RHash,
|
|
)
|
|
|
|
require.Len(ht, updatedInvoice.Htlcs, 1)
|
|
require.Equal(
|
|
ht, tc.lastHopCustomRecords,
|
|
updatedInvoice.Htlcs[0].CustomRecords,
|
|
)
|
|
|
|
// Make sure the custom channel data contains the encoded
|
|
// version of the custom records.
|
|
customRecords := lnwire.CustomRecords(
|
|
updatedInvoice.Htlcs[0].CustomRecords,
|
|
)
|
|
encodedRecords, err := customRecords.Serialize()
|
|
require.NoError(ht, err)
|
|
|
|
require.Equal(
|
|
ht, encodedRecords,
|
|
updatedInvoice.Htlcs[0].CustomChannelData,
|
|
)
|
|
}
|
|
|
|
// We don't need the HTLC interceptor at Bob anymore.
|
|
bobInterceptorCancel()
|
|
|
|
// After the normal test cases, we test that we can shut down Carol
|
|
// while an HTLC interception is going on. We 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{})
|
|
tc := &acceptorTestCase{
|
|
invoiceAmountMsat: 9000,
|
|
sendAmountMsat: 9000,
|
|
}
|
|
ts.createInvoice(tc)
|
|
|
|
go func() {
|
|
// Signal that all the payments have been sent.
|
|
defer close(sendPaymentDone)
|
|
|
|
_ = ts.sendPayment(tc)
|
|
}()
|
|
|
|
modifierRequest := ht.ReceiveInvoiceHtlcModification(invoiceModifier)
|
|
|
|
// Sanity check the modifier request.
|
|
require.EqualValues(
|
|
ht, tc.invoiceAmountMsat, modifierRequest.Invoice.ValueMsat,
|
|
)
|
|
require.EqualValues(
|
|
ht, tc.sendAmountMsat, modifierRequest.ExitHtlcAmt,
|
|
)
|
|
|
|
// We don't send a response to the modifier, but instead shut down and
|
|
// restart Carol.
|
|
restart := ht.SuspendNode(carol)
|
|
require.NoError(ht, restart())
|
|
|
|
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")
|
|
}
|
|
|
|
cancelModifier()
|
|
|
|
// 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
|
|
|
|
// lastHopCustomRecords is a map of custom records that will be added
|
|
// to the last hop of the payment, by an HTLC interceptor at Bob.
|
|
lastHopCustomRecords map[uint64][]byte
|
|
|
|
// 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,
|
|
finalInvoiceState: lnrpc.Invoice_SETTLED,
|
|
},
|
|
{
|
|
invoiceAmountMsat: 9000,
|
|
sendAmountMsat: 1000,
|
|
finalInvoiceState: lnrpc.Invoice_SETTLED,
|
|
lastHopCustomRecords: map[uint64][]byte{
|
|
lnwire.MinCustomRecordsTlvType: {1, 2, 3},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, t := range cases {
|
|
c.createInvoice(t)
|
|
}
|
|
|
|
return cases
|
|
}
|
|
|
|
// createInvoice creates an invoice for the given test case.
|
|
func (c *acceptorTestScenario) createInvoice(tc *acceptorTestCase) {
|
|
inv := &lnrpc.Invoice{ValueMsat: tc.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)
|
|
|
|
tc.invoice = invoice
|
|
tc.payAddr = payReq.PaymentAddr
|
|
}
|
|
|
|
// 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,
|
|
)
|
|
|
|
// We need to cheat a bit. We are attempting to pay an invoice with
|
|
// amount X with an HTLC of amount Y that is less than X. And then we
|
|
// use the invoice HTLC interceptor to simulate the HTLC actually
|
|
// carrying amount X (even though the actual HTLC transaction output
|
|
// only has amount Y). But in order for the invoice to be settled, we
|
|
// need to make sure that the MPP total amount record in the last hop
|
|
// is set to the invoice amount. This would also be the case in a normal
|
|
// MPP payment, where each shard only pays a fraction of the invoice.
|
|
aliceBobCarolRoute.Hops[1].MppRecord.TotalAmtMsat = tc.invoiceAmountMsat
|
|
|
|
// Send the payment.
|
|
sendReq := &routerrpc.SendToRouteRequest{
|
|
PaymentHash: tc.invoice.RHash,
|
|
Route: aliceBobCarolRoute,
|
|
}
|
|
|
|
return c.alice.RPC.SendToRouteV2(sendReq)
|
|
}
|