diff --git a/lntest/itest/lnd_amp_test.go b/lntest/itest/lnd_amp_test.go index d6c3b89a4..743923519 100644 --- a/lntest/itest/lnd_amp_test.go +++ b/lntest/itest/lnd_amp_test.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/amp" + "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" @@ -14,6 +15,129 @@ import ( "github.com/stretchr/testify/require" ) +// testSendPaymentAMP tests that we can send an AMP payment to a specified +// destination using SendPaymentV2. +func testSendPaymentAMP(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() + + // 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, net.Alice, + &routerrpc.SendPaymentRequest{ + Dest: net.Bob.PubKey[:], + Amt: int64(paymentAmt), + FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta, + MaxParts: 10, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + Amp: true, + }, + ) + + // 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) + } + + // Fetch Bob's invoices. + invoiceResp, err := net.Bob.ListInvoices( + ctxb, &lnrpc.ListInvoiceRequest{}, + ) + require.NoError(t.t, err) + + // There should only be one invoice. + require.Equal(t.t, 1, len(invoiceResp.Invoices)) + 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) + 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) + } +} + func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) { ctxb := context.Background() diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index 679c572ff..736f832d0 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -274,6 +274,11 @@ var allTestCases = []*testCase{ name: "sendtoroute amp", test: testSendToRouteAMP, }, + { + name: "sendpayment amp", + test: testSendPaymentAMP, + }, + { name: "send multi path payment", test: testSendMultiPathPayment,