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