diff --git a/lntest/itest/lnd_amp_test.go b/lntest/itest/lnd_amp_test.go index 407a76641..1dc147aa4 100644 --- a/lntest/itest/lnd_amp_test.go +++ b/lntest/itest/lnd_amp_test.go @@ -17,6 +17,152 @@ import ( "github.com/stretchr/testify/require" ) +// testSendPaymentAMPInvoice tests that we can send an AMP payment to a +// specified AMP invoice using SendPaymentV2. +func testSendPaymentAMPInvoice(net *lntest.NetworkHarness, t *harnessTest) { + ctxb := context.Background() + + ctx := newMppTestContext(t, net) + defer ctx.shutdownNodes() + + const paymentAmt = btcutil.Amount(300000) + + // Set up a network with three different paths Alice <-> Bob. Channel + // capacities are set such that the payment can only succeed if (at + // least) three paths are used. + // + // _ Eve _ + // / \ + // Alice -- Carol ---- Bob + // \ / + // \__ Dave ____/ + // + ctx.openChannel(ctx.carol, ctx.bob, 135000) + ctx.openChannel(ctx.alice, ctx.carol, 235000) + ctx.openChannel(ctx.dave, ctx.bob, 135000) + ctx.openChannel(ctx.alice, ctx.dave, 135000) + ctx.openChannel(ctx.eve, ctx.bob, 135000) + ctx.openChannel(ctx.carol, ctx.eve, 135000) + + defer ctx.closeChannels() + + ctx.waitForChannels() + + // Subscribe to bob's invoices. + req := &lnrpc.InvoiceSubscription{} + ctxc, cancelSubscription := context.WithCancel(ctxb) + bobInvoiceSubscription, err := ctx.bob.SubscribeInvoices(ctxc, req) + require.NoError(t.t, err) + defer cancelSubscription() + + addInvoiceResp, err := ctx.bob.AddInvoice(context.Background(), &lnrpc.Invoice{ + Value: int64(paymentAmt), + IsAmp: true, + }) + require.NoError(t.t, err) + + // Ensure we get a notification of the invoice being added by Bob. + rpcInvoice, err := bobInvoiceSubscription.Recv() + require.NoError(t.t, err) + + require.False(t.t, rpcInvoice.Settled) // nolint:staticcheck + require.Equal(t.t, lnrpc.Invoice_OPEN, rpcInvoice.State) + require.Equal(t.t, int64(0), rpcInvoice.AmtPaidSat) + require.Equal(t.t, int64(0), rpcInvoice.AmtPaidMsat) + + require.Equal(t.t, 0, len(rpcInvoice.Htlcs)) + + // Increase Dave's fee to make the test deterministic. Otherwise it + // would be unpredictable whether pathfinding would go through Charlie + // or Dave for the first shard. + _, err = ctx.dave.UpdateChannelPolicy( + context.Background(), + &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_Global{Global: true}, + BaseFeeMsat: 500000, + FeeRate: 0.001, + TimeLockDelta: 40, + }, + ) + if err != nil { + t.Fatalf("dave policy update: %v", err) + } + + ctxt, _ := context.WithTimeout(context.Background(), 4*defaultTimeout) + payment := sendAndAssertSuccess( + ctxt, t, ctx.alice, + &routerrpc.SendPaymentRequest{ + PaymentRequest: addInvoiceResp.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }, + ) + + // Check that Alice split the payment in at least three shards. Because + // the hand-off of the htlc to the link is asynchronous (via a mailbox), + // there is some non-determinism in the process. Depending on whether + // the new pathfinding round is started before or after the htlc is + // locked into the channel, different sharding may occur. Therefore we + // can only check if the number of shards isn't below the theoretical + // minimum. + succeeded := 0 + for _, htlc := range payment.Htlcs { + if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED { + succeeded++ + } + } + + const minExpectedShards = 3 + if succeeded < minExpectedShards { + t.Fatalf("expected at least %v shards, but got %v", + minExpectedShards, succeeded) + } + + // There should now be a settle event for the invoice. + rpcInvoice, err = bobInvoiceSubscription.Recv() + require.NoError(t.t, err) + + // Also fetch Bob's invoice from ListInvoices and assert it is equal to + // the one recevied via the subscription. + invoiceResp, err := ctx.bob.ListInvoices( + ctxb, &lnrpc.ListInvoiceRequest{}, + ) + require.NoError(t.t, err) + require.Equal(t.t, 1, len(invoiceResp.Invoices)) + assertInvoiceEqual(t.t, rpcInvoice, invoiceResp.Invoices[0]) + + // Assert that the invoice is settled for the total payment amount and + // has the correct payment address. + require.True(t.t, rpcInvoice.Settled) // nolint:staticcheck + require.Equal(t.t, lnrpc.Invoice_SETTLED, rpcInvoice.State) + require.Equal(t.t, int64(paymentAmt), rpcInvoice.AmtPaidSat) + require.Equal(t.t, int64(paymentAmt*1000), rpcInvoice.AmtPaidMsat) + + // Finally, assert that the same set id is recorded for each htlc, and + // that the preimage hash pair is valid. + var setID []byte + require.Equal(t.t, succeeded, len(rpcInvoice.Htlcs)) + for _, htlc := range rpcInvoice.Htlcs { + require.NotNil(t.t, htlc.Amp) + if setID == nil { + setID = make([]byte, 32) + copy(setID, htlc.Amp.SetId) + } + require.Equal(t.t, setID, htlc.Amp.SetId) + + // Parse the child hash and child preimage, and assert they are + // well-formed. + childHash, err := lntypes.MakeHash(htlc.Amp.Hash) + require.NoError(t.t, err) + childPreimage, err := lntypes.MakePreimage(htlc.Amp.Preimage) + require.NoError(t.t, err) + + // Assert that the preimage actually matches the hashes. + validPreimage := childPreimage.Matches(childHash) + require.True(t.t, validPreimage) + } +} + // testSendPaymentAMP tests that we can send an AMP payment to a specified // destination using SendPaymentV2. func testSendPaymentAMP(net *lntest.NetworkHarness, t *harnessTest) { diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index 15a660f02..d5ecc34a3 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -278,7 +278,10 @@ var allTestCases = []*testCase{ name: "sendpayment amp", test: testSendPaymentAMP, }, - + { + name: "sendpayment amp invoice", + test: testSendPaymentAMPInvoice, + }, { name: "send multi path payment", test: testSendMultiPathPayment,