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) }