mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 18:10:34 +01:00
7dfe4018ce
This commit was previously split into the following parts to ease review: - 2d746f68: replace imports - 4008f0fd: use ecdsa.Signature - 849e33d1: remove btcec.S256() - b8f6ebbd: use v2 library correctly - fa80bca9: bump go modules
801 lines
27 KiB
Go
801 lines
27 KiB
Go
package itest
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/lightningnetwork/lnd/amp"
|
|
"github.com/lightningnetwork/lnd/chainreg"
|
|
"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/lntypes"
|
|
"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) {
|
|
t.t.Run("native payaddr", func(t *testing.T) {
|
|
tt := newHarnessTest(t, net)
|
|
testSendPaymentAMPInvoiceCase(net, tt, false)
|
|
})
|
|
t.t.Run("external payaddr", func(t *testing.T) {
|
|
tt := newHarnessTest(t, net)
|
|
testSendPaymentAMPInvoiceCase(net, tt, true)
|
|
})
|
|
}
|
|
|
|
func testSendPaymentAMPInvoiceCase(net *lntest.NetworkHarness, t *harnessTest,
|
|
useExternalPayAddr bool) {
|
|
|
|
ctxb := context.Background()
|
|
|
|
ctx := newMppTestContext(t, net)
|
|
defer ctx.shutdownNodes()
|
|
|
|
// Subscribe to bob's invoices. Do this early in the test to make sure
|
|
// that the subscription has actually been completed when we add an
|
|
// invoice. Otherwise the notification will be missed.
|
|
req := &lnrpc.InvoiceSubscription{}
|
|
ctxc, cancelSubscription := context.WithCancel(ctxb)
|
|
bobInvoiceSubscription, err := ctx.bob.SubscribeInvoices(ctxc, req)
|
|
require.NoError(t.t, err)
|
|
defer cancelSubscription()
|
|
|
|
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()
|
|
|
|
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)
|
|
}
|
|
|
|
// Generate an external payment address when attempting to pseudo-reuse
|
|
// an AMP invoice. When using an external payment address, we'll also
|
|
// expect an extra invoice to appear in the ListInvoices response, since
|
|
// a new invoice will be JIT inserted under a different payment address
|
|
// than the one in the invoice.
|
|
var (
|
|
expNumInvoices = 1
|
|
externalPayAddr []byte
|
|
)
|
|
if useExternalPayAddr {
|
|
expNumInvoices = 2
|
|
externalPayAddr = make([]byte, 32)
|
|
_, err = rand.Read(externalPayAddr)
|
|
require.NoError(t.t, err)
|
|
}
|
|
|
|
payment := sendAndAssertSuccess(
|
|
t, ctx.alice, &routerrpc.SendPaymentRequest{
|
|
PaymentRequest: addInvoiceResp.PaymentRequest,
|
|
PaymentAddr: externalPayAddr,
|
|
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)
|
|
}
|
|
|
|
// When an external payment address is supplied, we'll get an extra
|
|
// notification for the JIT inserted invoice, since it differs from the
|
|
// original.
|
|
if useExternalPayAddr {
|
|
_, err = bobInvoiceSubscription.Recv()
|
|
require.NoError(t.t, err)
|
|
}
|
|
|
|
// 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 received via the subscription.
|
|
invoiceResp, err := ctx.bob.ListInvoices(
|
|
ctxb, &lnrpc.ListInvoiceRequest{},
|
|
)
|
|
require.NoError(t.t, err)
|
|
require.Equal(t.t, expNumInvoices, len(invoiceResp.Invoices))
|
|
assertInvoiceEqual(t.t, rpcInvoice, invoiceResp.Invoices[expNumInvoices-1])
|
|
|
|
// 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)
|
|
}
|
|
|
|
// The set ID we extract above should be shown in the final settled
|
|
// state.
|
|
ampState := rpcInvoice.AmpInvoiceState[hex.EncodeToString(setID)]
|
|
require.Equal(t.t, lnrpc.InvoiceHTLCState_SETTLED, ampState.State)
|
|
}
|
|
|
|
// testSendPaymentAMPInvoiceRepeat tests that it's possible to pay an AMP
|
|
// invoice multiple times by having the client generate a new setID each time.
|
|
func testSendPaymentAMPInvoiceRepeat(net *lntest.NetworkHarness,
|
|
t *harnessTest) {
|
|
|
|
// In this basic test, we'll only need two nodes as we want to
|
|
// primarily test the recurring payment feature. So we'll re-use the
|
|
carol := net.NewNode(t.t, "Carol", nil)
|
|
defer shutdownAndAssert(net, t, carol)
|
|
|
|
// Send Carol enough coins to be able to open a channel to Dave.
|
|
net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol)
|
|
|
|
dave := net.NewNode(t.t, "Dave", nil)
|
|
defer shutdownAndAssert(net, t, dave)
|
|
|
|
// Before we start the test, we'll ensure both sides are connected to
|
|
// the funding flow can properly be executed.
|
|
net.EnsureConnected(t.t, carol, dave)
|
|
|
|
// Set up an invoice subscription so we can be notified when Dave
|
|
// receives his repeated payments.
|
|
req := &lnrpc.InvoiceSubscription{}
|
|
ctxb := context.Background()
|
|
ctxc, cancelSubscription := context.WithCancel(ctxb)
|
|
invSubscription, err := dave.SubscribeInvoices(ctxc, req)
|
|
require.NoError(t.t, err)
|
|
defer cancelSubscription()
|
|
|
|
// Establish a channel between Carol and Dave.
|
|
chanAmt := btcutil.Amount(100_000)
|
|
chanPoint := openChannelAndAssert(
|
|
t, net, carol, dave,
|
|
lntest.OpenChannelParams{
|
|
Amt: chanAmt,
|
|
},
|
|
)
|
|
err = carol.WaitForNetworkChannelOpen(chanPoint)
|
|
require.NoError(t.t, err, "carol didn't report channel")
|
|
err = dave.WaitForNetworkChannelOpen(chanPoint)
|
|
require.NoError(t.t, err, "dave didn't report channel")
|
|
|
|
// Create an AMP invoice of a trivial amount, that we'll pay repeatedly
|
|
// in this integration test.
|
|
paymentAmt := 10000
|
|
addInvoiceResp, err := dave.AddInvoice(ctxb, &lnrpc.Invoice{
|
|
Value: int64(paymentAmt),
|
|
IsAmp: true,
|
|
})
|
|
require.NoError(t.t, err)
|
|
|
|
// We should get an initial notification that the HTLC has been added.
|
|
rpcInvoice, err := invSubscription.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))
|
|
|
|
// Now we'll use Carol to pay the invoice that Dave created.
|
|
_ = sendAndAssertSuccess(
|
|
t, carol, &routerrpc.SendPaymentRequest{
|
|
PaymentRequest: addInvoiceResp.PaymentRequest,
|
|
TimeoutSeconds: 60,
|
|
FeeLimitMsat: noFeeLimitMsat,
|
|
},
|
|
)
|
|
|
|
// Dave should get a notification that the invoice has been settled.
|
|
invoiceNtfn, err := invSubscription.Recv()
|
|
require.NoError(t.t, err)
|
|
|
|
// The notification should signal that the invoice is now settled, and
|
|
// should also include the set ID, and show the proper amount paid.
|
|
require.True(t.t, invoiceNtfn.Settled) // nolint:staticcheck
|
|
require.Equal(t.t, lnrpc.Invoice_SETTLED, invoiceNtfn.State)
|
|
require.Equal(t.t, paymentAmt, int(invoiceNtfn.AmtPaidSat))
|
|
require.Equal(t.t, 1, len(invoiceNtfn.AmpInvoiceState))
|
|
var firstSetID []byte
|
|
for setIDStr, ampState := range invoiceNtfn.AmpInvoiceState {
|
|
firstSetID, _ = hex.DecodeString(setIDStr)
|
|
require.Equal(t.t, lnrpc.InvoiceHTLCState_SETTLED, ampState.State)
|
|
}
|
|
|
|
// Pay the invoice again, we should get another notification that Dave
|
|
// has received another payment.
|
|
_ = sendAndAssertSuccess(
|
|
t, carol, &routerrpc.SendPaymentRequest{
|
|
PaymentRequest: addInvoiceResp.PaymentRequest,
|
|
TimeoutSeconds: 60,
|
|
FeeLimitMsat: noFeeLimitMsat,
|
|
},
|
|
)
|
|
|
|
// Dave should get another notification.
|
|
invoiceNtfn, err = invSubscription.Recv()
|
|
require.NoError(t.t, err)
|
|
|
|
// The invoice should still be shown as settled, and also include the
|
|
// information about this newly generated setID, showing 2x the amount
|
|
// paid.
|
|
require.True(t.t, invoiceNtfn.Settled) // nolint:staticcheck
|
|
require.Equal(t.t, paymentAmt*2, int(invoiceNtfn.AmtPaidSat))
|
|
|
|
var secondSetID []byte
|
|
for setIDStr, ampState := range invoiceNtfn.AmpInvoiceState {
|
|
secondSetID, _ = hex.DecodeString(setIDStr)
|
|
require.Equal(t.t, lnrpc.InvoiceHTLCState_SETTLED, ampState.State)
|
|
}
|
|
|
|
// The returned invoice should only include a single HTLC since we
|
|
// return the "projected" sub-invoice for a given setID.
|
|
require.Equal(t.t, 1, len(invoiceNtfn.Htlcs))
|
|
|
|
// However the AMP state index should show that there've been two
|
|
// repeated payments to this invoice so far.
|
|
require.Equal(t.t, 2, len(invoiceNtfn.AmpInvoiceState))
|
|
|
|
// Now we'll look up the invoice using the new LookupInvoice2 RPC call
|
|
// by the set ID of each of the invoices.
|
|
subInvoice1, err := dave.LookupInvoiceV2(ctxb, &invoicesrpc.LookupInvoiceMsg{
|
|
InvoiceRef: &invoicesrpc.LookupInvoiceMsg_SetId{
|
|
SetId: firstSetID,
|
|
},
|
|
LookupModifier: invoicesrpc.LookupModifier_HTLC_SET_ONLY,
|
|
})
|
|
require.Nil(t.t, err)
|
|
subInvoice2, err := dave.LookupInvoiceV2(ctxb, &invoicesrpc.LookupInvoiceMsg{
|
|
InvoiceRef: &invoicesrpc.LookupInvoiceMsg_SetId{
|
|
SetId: secondSetID,
|
|
},
|
|
LookupModifier: invoicesrpc.LookupModifier_HTLC_SET_ONLY,
|
|
})
|
|
require.Nil(t.t, err)
|
|
|
|
// Each invoice should only show a single HTLC present, as we passed
|
|
// the HTLC set only modifier.
|
|
require.Equal(t.t, 1, len(subInvoice1.Htlcs))
|
|
require.Equal(t.t, 1, len(subInvoice2.Htlcs))
|
|
|
|
// If we look up the same invoice, by its payment address, but now with
|
|
// the HTLC blank modifier, then none of them should be returned.
|
|
rootInvoice, err := dave.LookupInvoiceV2(ctxb, &invoicesrpc.LookupInvoiceMsg{
|
|
InvoiceRef: &invoicesrpc.LookupInvoiceMsg_PaymentAddr{
|
|
PaymentAddr: addInvoiceResp.PaymentAddr,
|
|
},
|
|
LookupModifier: invoicesrpc.LookupModifier_HTLC_SET_BLANK,
|
|
})
|
|
require.Nil(t.t, err)
|
|
require.Equal(t.t, 0, len(rootInvoice.Htlcs))
|
|
|
|
// If we look up the same invoice, by its payment address, but without
|
|
// that modified, then we should get all the relevant HTLCs.
|
|
rootInvoice, err = dave.LookupInvoiceV2(ctxb,
|
|
&invoicesrpc.LookupInvoiceMsg{
|
|
InvoiceRef: &invoicesrpc.LookupInvoiceMsg_PaymentAddr{
|
|
PaymentAddr: addInvoiceResp.PaymentAddr,
|
|
},
|
|
})
|
|
require.Nil(t.t, err)
|
|
require.Equal(t.t, 2, len(rootInvoice.Htlcs))
|
|
|
|
// Finally, we'll test that if we subscribe for notifications of
|
|
// settled invoices, we get a backlog, which includes the invoice we
|
|
// settled last (since you can only fetch from index 1 onwards), and
|
|
// only the relevant set of HTLCs.
|
|
req = &lnrpc.InvoiceSubscription{
|
|
SettleIndex: 1,
|
|
}
|
|
ctxc, cancelSubscription2 := context.WithCancel(ctxb)
|
|
invSub2, err := dave.SubscribeInvoices(ctxc, req)
|
|
require.NoError(t.t, err)
|
|
defer cancelSubscription2()
|
|
|
|
// The first invoice we get back should match the state of the invoice
|
|
// after our second payment: amt updated, but only a single HTLC shown
|
|
// through.
|
|
backlogInv, _ := invSub2.Recv()
|
|
require.Equal(t.t, 1, len(backlogInv.Htlcs))
|
|
require.Equal(t.t, 2, len(backlogInv.AmpInvoiceState))
|
|
require.True(t.t, backlogInv.Settled) // nolint:staticcheck
|
|
require.Equal(t.t, paymentAmt*2, int(backlogInv.AmtPaidSat))
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
payment := sendAndAssertSuccess(
|
|
t, ctx.alice, &routerrpc.SendPaymentRequest{
|
|
Dest: ctx.bob.PubKey[:],
|
|
Amt: int64(paymentAmt),
|
|
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
|
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 := ctx.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) // 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)
|
|
}
|
|
|
|
// The set ID we extract above should be shown in the final settled
|
|
// state.
|
|
ampState := rpcInvoice.AmpInvoiceState[hex.EncodeToString(setID)]
|
|
require.Equal(t.t, lnrpc.InvoiceHTLCState_SETTLED, ampState.State)
|
|
}
|
|
|
|
func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) {
|
|
ctxb := context.Background()
|
|
|
|
ctx := newMppTestContext(t, net)
|
|
defer ctx.shutdownNodes()
|
|
|
|
const (
|
|
paymentAmt = btcutil.Amount(300000)
|
|
numShards = 3
|
|
shardAmt = paymentAmt / numShards
|
|
chanAmt = shardAmt * 3 / 2
|
|
)
|
|
|
|
// Set up a network with three different paths Alice <-> Bob.
|
|
// _ Eve _
|
|
// / \
|
|
// Alice -- Carol ---- Bob
|
|
// \ /
|
|
// \__ Dave ____/
|
|
//
|
|
ctx.openChannel(ctx.carol, ctx.bob, chanAmt)
|
|
ctx.openChannel(ctx.dave, ctx.bob, chanAmt)
|
|
ctx.openChannel(ctx.alice, ctx.dave, chanAmt)
|
|
ctx.openChannel(ctx.eve, ctx.bob, chanAmt)
|
|
ctx.openChannel(ctx.carol, ctx.eve, chanAmt)
|
|
|
|
// Since the channel Alice-> Carol will have to carry two
|
|
// shards, we make it larger.
|
|
ctx.openChannel(ctx.alice, ctx.carol, chanAmt+shardAmt)
|
|
|
|
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()
|
|
|
|
// We'll send shards along three routes from Alice.
|
|
sendRoutes := [numShards][]*lntest.HarnessNode{
|
|
{ctx.carol, ctx.bob},
|
|
{ctx.dave, ctx.bob},
|
|
{ctx.carol, ctx.eve, ctx.bob},
|
|
}
|
|
|
|
payAddr := make([]byte, 32)
|
|
_, err = rand.Read(payAddr)
|
|
require.NoError(t.t, err)
|
|
|
|
setID := make([]byte, 32)
|
|
_, err = rand.Read(setID)
|
|
require.NoError(t.t, err)
|
|
|
|
var sharer amp.Sharer
|
|
sharer, err = amp.NewSeedSharer()
|
|
require.NoError(t.t, err)
|
|
|
|
childPreimages := make(map[lntypes.Preimage]uint32)
|
|
responses := make(chan *lnrpc.HTLCAttempt, len(sendRoutes))
|
|
|
|
// Define a closure for sending each of the three shards.
|
|
sendShard := func(i int, hops []*lntest.HarnessNode) {
|
|
// Build a route for the specified hops.
|
|
r, err := ctx.buildRoute(ctxb, shardAmt, ctx.alice, hops)
|
|
if err != nil {
|
|
t.Fatalf("unable to build route: %v", err)
|
|
}
|
|
|
|
// Set the MPP records to indicate this is a payment shard.
|
|
hop := r.Hops[len(r.Hops)-1]
|
|
hop.TlvPayload = true
|
|
hop.MppRecord = &lnrpc.MPPRecord{
|
|
PaymentAddr: payAddr,
|
|
TotalAmtMsat: int64(paymentAmt * 1000),
|
|
}
|
|
|
|
var child *amp.Child
|
|
if i < len(sendRoutes)-1 {
|
|
var left amp.Sharer
|
|
left, sharer, err = sharer.Split()
|
|
require.NoError(t.t, err)
|
|
|
|
child = left.Child(uint32(i))
|
|
} else {
|
|
child = sharer.Child(uint32(i))
|
|
}
|
|
childPreimages[child.Preimage] = child.Index
|
|
|
|
hop.AmpRecord = &lnrpc.AMPRecord{
|
|
RootShare: child.Share[:],
|
|
SetId: setID,
|
|
ChildIndex: child.Index,
|
|
}
|
|
|
|
// Send the shard.
|
|
sendReq := &routerrpc.SendToRouteRequest{
|
|
PaymentHash: child.Hash[:],
|
|
Route: r,
|
|
}
|
|
|
|
// We'll send all shards in their own goroutine, since SendToRoute will
|
|
// block as long as the payment is in flight.
|
|
go func() {
|
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
resp, err := ctx.alice.RouterClient.SendToRouteV2(ctxt, sendReq)
|
|
if err != nil {
|
|
t.Fatalf("unable to send payment: %v", err)
|
|
}
|
|
|
|
responses <- resp
|
|
}()
|
|
}
|
|
|
|
// Send the first shard, this cause Bob to JIT add an invoice.
|
|
sendShard(0, sendRoutes[0])
|
|
|
|
// 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, payAddr, rpcInvoice.PaymentAddr)
|
|
|
|
require.Equal(t.t, 0, len(rpcInvoice.Htlcs))
|
|
|
|
sendShard(1, sendRoutes[1])
|
|
sendShard(2, sendRoutes[2])
|
|
|
|
// Assert that all of the child preimages are unique.
|
|
require.Equal(t.t, len(sendRoutes), len(childPreimages))
|
|
|
|
// Make a copy of the childPreimages map for validating the resulting
|
|
// invoice.
|
|
childPreimagesCopy := make(map[lntypes.Preimage]uint32)
|
|
for preimage, childIndex := range childPreimages {
|
|
childPreimagesCopy[preimage] = childIndex
|
|
}
|
|
|
|
// Wait for all responses to be back, and check that they all
|
|
// succeeded.
|
|
for range sendRoutes {
|
|
var resp *lnrpc.HTLCAttempt
|
|
select {
|
|
case resp = <-responses:
|
|
case <-time.After(defaultTimeout):
|
|
t.Fatalf("response not received")
|
|
}
|
|
|
|
if resp.Failure != nil {
|
|
t.Fatalf("received payment failure : %v", resp.Failure)
|
|
}
|
|
|
|
preimage, err := lntypes.MakePreimage(resp.Preimage)
|
|
require.NoError(t.t, err)
|
|
|
|
// Assert that the response includes one of our child preimages.
|
|
_, ok := childPreimages[preimage]
|
|
require.True(t.t, ok)
|
|
|
|
// Remove this preimage from out set so that we ensure all
|
|
// responses have a unique child preimage.
|
|
delete(childPreimages, preimage)
|
|
}
|
|
childPreimages = childPreimagesCopy
|
|
|
|
// 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 received 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)
|
|
require.Equal(t.t, payAddr, rpcInvoice.PaymentAddr)
|
|
|
|
// Finally, assert that the proper set id is recorded for each htlc, and
|
|
// that the preimage hash pair is valid.
|
|
require.Equal(t.t, numShards, len(rpcInvoice.Htlcs))
|
|
for _, htlc := range rpcInvoice.Htlcs {
|
|
require.NotNil(t.t, htlc.Amp)
|
|
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)
|
|
|
|
// Assert that the HTLC includes one of our child preimages.
|
|
childIndex, ok := childPreimages[childPreimage]
|
|
require.True(t.t, ok)
|
|
|
|
// Assert that the correct child index is reflected.
|
|
require.Equal(t.t, childIndex, htlc.Amp.ChildIndex)
|
|
|
|
// Remove this preimage from our set so that we ensure all HTLCs
|
|
// have a unique child preimage.
|
|
delete(childPreimages, childPreimage)
|
|
}
|
|
}
|
|
|
|
// assertInvoiceEqual asserts that two lnrpc.Invoices are equivalent. A custom
|
|
// comparison function is defined for these tests, since proto message returned
|
|
// from unary and streaming RPCs (as of protobuf 1.23.0 and grpc 1.29.1) aren't
|
|
// consistent with the private fields set on the messages. As a result, we avoid
|
|
// using require.Equal and test only the actual data members.
|
|
func assertInvoiceEqual(t *testing.T, a, b *lnrpc.Invoice) {
|
|
t.Helper()
|
|
|
|
// Ensure the HTLCs are sorted properly before attempting to compare.
|
|
sort.Slice(a.Htlcs, func(i, j int) bool {
|
|
return a.Htlcs[i].ChanId < a.Htlcs[j].ChanId
|
|
})
|
|
sort.Slice(b.Htlcs, func(i, j int) bool {
|
|
return b.Htlcs[i].ChanId < b.Htlcs[j].ChanId
|
|
})
|
|
|
|
require.Equal(t, a.Memo, b.Memo)
|
|
require.Equal(t, a.RPreimage, b.RPreimage)
|
|
require.Equal(t, a.RHash, b.RHash)
|
|
require.Equal(t, a.Value, b.Value)
|
|
require.Equal(t, a.ValueMsat, b.ValueMsat)
|
|
require.Equal(t, a.CreationDate, b.CreationDate)
|
|
require.Equal(t, a.SettleDate, b.SettleDate)
|
|
require.Equal(t, a.PaymentRequest, b.PaymentRequest)
|
|
require.Equal(t, a.DescriptionHash, b.DescriptionHash)
|
|
require.Equal(t, a.Expiry, b.Expiry)
|
|
require.Equal(t, a.FallbackAddr, b.FallbackAddr)
|
|
require.Equal(t, a.CltvExpiry, b.CltvExpiry)
|
|
require.Equal(t, a.RouteHints, b.RouteHints)
|
|
require.Equal(t, a.Private, b.Private)
|
|
require.Equal(t, a.AddIndex, b.AddIndex)
|
|
require.Equal(t, a.SettleIndex, b.SettleIndex)
|
|
require.Equal(t, a.AmtPaidSat, b.AmtPaidSat)
|
|
require.Equal(t, a.AmtPaidMsat, b.AmtPaidMsat)
|
|
require.Equal(t, a.State, b.State)
|
|
require.Equal(t, a.Features, b.Features)
|
|
require.Equal(t, a.IsKeysend, b.IsKeysend)
|
|
require.Equal(t, a.PaymentAddr, b.PaymentAddr)
|
|
require.Equal(t, a.IsAmp, b.IsAmp)
|
|
|
|
require.Equal(t, len(a.Htlcs), len(b.Htlcs))
|
|
for i := range a.Htlcs {
|
|
htlcA, htlcB := a.Htlcs[i], b.Htlcs[i]
|
|
require.Equal(t, htlcA.ChanId, htlcB.ChanId)
|
|
require.Equal(t, htlcA.HtlcIndex, htlcB.HtlcIndex)
|
|
require.Equal(t, htlcA.AmtMsat, htlcB.AmtMsat)
|
|
require.Equal(t, htlcA.AcceptHeight, htlcB.AcceptHeight)
|
|
require.Equal(t, htlcA.AcceptTime, htlcB.AcceptTime)
|
|
require.Equal(t, htlcA.ResolveTime, htlcB.ResolveTime)
|
|
require.Equal(t, htlcA.ExpiryHeight, htlcB.ExpiryHeight)
|
|
require.Equal(t, htlcA.State, htlcB.State)
|
|
require.Equal(t, htlcA.CustomRecords, htlcB.CustomRecords)
|
|
require.Equal(t, htlcA.MppTotalAmtMsat, htlcB.MppTotalAmtMsat)
|
|
require.Equal(t, htlcA.Amp, htlcB.Amp)
|
|
}
|
|
}
|