|
package itest |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"encoding/hex" |
|
"time" |
|
|
|
"github.com/btcsuite/btcutil" |
|
"github.com/davecgh/go-spew/spew" |
|
"github.com/lightningnetwork/lnd/lnrpc" |
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc" |
|
"github.com/lightningnetwork/lnd/lntest" |
|
"github.com/lightningnetwork/lnd/lntest/wait" |
|
"github.com/lightningnetwork/lnd/lntypes" |
|
"github.com/lightningnetwork/lnd/lnwire" |
|
"github.com/lightningnetwork/lnd/record" |
|
"github.com/stretchr/testify/require" |
|
) |
|
|
|
func testSingleHopInvoice(net *lntest.NetworkHarness, t *harnessTest) { |
|
ctxb := context.Background() |
|
|
|
// Open a channel with 100k satoshis between Alice and Bob with Alice being |
|
// the sole funder of the channel. |
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout) |
|
chanAmt := btcutil.Amount(100000) |
|
chanPoint := openChannelAndAssert( |
|
ctxt, t, net, net.Alice, net.Bob, |
|
lntest.OpenChannelParams{ |
|
Amt: chanAmt, |
|
}, |
|
) |
|
|
|
// Now that the channel is open, create an invoice for Bob which |
|
// expects a payment of 1000 satoshis from Alice paid via a particular |
|
// preimage. |
|
const paymentAmt = 1000 |
|
preimage := bytes.Repeat([]byte("A"), 32) |
|
invoice := &lnrpc.Invoice{ |
|
Memo: "testing", |
|
RPreimage: preimage, |
|
Value: paymentAmt, |
|
} |
|
invoiceResp, err := net.Bob.AddInvoice(ctxb, invoice) |
|
if err != nil { |
|
t.Fatalf("unable to add invoice: %v", err) |
|
} |
|
|
|
// Wait for Alice to recognize and advertise the new channel generated |
|
// above. |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint) |
|
if err != nil { |
|
t.Fatalf("alice didn't advertise channel before "+ |
|
"timeout: %v", err) |
|
} |
|
err = net.Bob.WaitForNetworkChannelOpen(ctxt, chanPoint) |
|
if err != nil { |
|
t.Fatalf("bob didn't advertise channel before "+ |
|
"timeout: %v", err) |
|
} |
|
|
|
// With the invoice for Bob added, send a payment towards Alice paying |
|
// to the above generated invoice. |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
resp := sendAndAssertSuccess( |
|
ctxt, t, net.Alice, |
|
&routerrpc.SendPaymentRequest{ |
|
PaymentRequest: invoiceResp.PaymentRequest, |
|
TimeoutSeconds: 60, |
|
FeeLimitMsat: noFeeLimitMsat, |
|
}, |
|
) |
|
if hex.EncodeToString(preimage) != resp.PaymentPreimage { |
|
t.Fatalf("preimage mismatch: expected %v, got %v", preimage, |
|
resp.PaymentPreimage) |
|
} |
|
|
|
// Bob's invoice should now be found and marked as settled. |
|
payHash := &lnrpc.PaymentHash{ |
|
RHash: invoiceResp.RHash, |
|
} |
|
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout) |
|
dbInvoice, err := net.Bob.LookupInvoice(ctxt, payHash) |
|
if err != nil { |
|
t.Fatalf("unable to lookup invoice: %v", err) |
|
} |
|
if !dbInvoice.Settled { // nolint:staticcheck |
|
t.Fatalf("bob's invoice should be marked as settled: %v", |
|
spew.Sdump(dbInvoice)) |
|
} |
|
|
|
// With the payment completed all balance related stats should be |
|
// properly updated. |
|
err = wait.NoError( |
|
assertAmountSent(paymentAmt, net.Alice, net.Bob), |
|
3*time.Second, |
|
) |
|
if err != nil { |
|
t.Fatalf(err.Error()) |
|
} |
|
|
|
|
|
// Till now alice -> bob channel opening, sending payment |
|
// and generating invoice is tested properly, now opening the channel from Bob to |
|
// Carol and in the similar fashion sending payment and |
|
// generating invoice. |
|
|
|
// Creating Carol and opening a channel from |
|
// Bob to Carol (bob -> carol) |
|
Carol := net.NewNode(t.t, "Carol", nil) |
|
defer shutdownAndAssert(net, t, Carol) |
|
|
|
// establishing a connection |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
net.ConnectNodes(ctxt, t.t, net.Bob, Carol) |
|
|
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout) |
|
chanPointCarol := openChannelAndAssert( |
|
ctxt, t, net, net.Bob, Carol, |
|
lntest.OpenChannelParams{ |
|
Amt: chanAmt, |
|
}, |
|
) |
|
|
|
// After opening a channel, creating an invoice for Carol which |
|
// expects a payment of 1000 satoshis from Bob paid via a particular |
|
// preimage, using the same paymentAmt which is used in transaction |
|
// between Alice and Bob |
|
|
|
invoiceCarol := &lnrpc.Invoice{ |
|
Memo: "testing", |
|
RPreimage: preimage, |
|
Value: paymentAmt, |
|
} |
|
|
|
invoiceRespCarol, err := Carol.AddInvoice(ctxb, invoiceCarol) |
|
if err != nil { |
|
t.Fatalf("unable to add invoice: %v", err) |
|
} |
|
|
|
|
|
// Wait for Bob and Carol to recognize and advertise the new channel generated |
|
// between Bob and Carol |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
err = net.Bob.WaitForNetworkChannelOpen(ctxt, chanPointCarol) |
|
if err != nil { |
|
t.Fatalf("bob didn't advertise channel before "+ |
|
"timeout: %v", err) |
|
} |
|
err = Carol.WaitForNetworkChannelOpen(ctxt, chanPointCarol) |
|
if err != nil { |
|
t.Fatalf("carol didn't advertise channel before "+ |
|
"timeout: %v", err) |
|
} |
|
|
|
|
|
// With the invoice for Carol added, send a payment request to Bob paying |
|
// to the above generated invoice and assert that the |
|
// payment completes successfully. |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
respCarol := sendAndAssertSuccess( |
|
ctxt, t, net.Bob, |
|
&routerrpc.SendPaymentRequest{ |
|
PaymentRequest: invoiceRespCarol.PaymentRequest, |
|
TimeoutSeconds: 60, |
|
FeeLimitMsat: noFeeLimitMsat, |
|
}, |
|
) |
|
if hex.EncodeToString(preimage) != respCarol.PaymentPreimage { |
|
t.Fatalf("preimage mismatch: expected %v, got %v", preimage, |
|
respCarol.PaymentPreimage) |
|
} |
|
|
|
|
|
// Carols's invoice should now be found and marked as settled. |
|
payHash = &lnrpc.PaymentHash{ |
|
RHash: invoiceRespCarol.RHash, |
|
} |
|
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout) |
|
dbInvoice, err = Carol.LookupInvoice(ctxt, payHash) |
|
if err != nil { |
|
t.Fatalf("unable to lookup invoice: %v", err) |
|
} |
|
if !dbInvoice.Settled { |
|
t.Fatalf("carol's invoice should be marked as settled: %v", |
|
spew.Sdump(dbInvoice)) |
|
} |
|
|
|
|
|
// With the payment completed all balance related stats should be |
|
// properly updated. |
|
err = wait.NoError( |
|
assertAmountSent(paymentAmt, net.Bob, Carol), |
|
3*time.Second, |
|
) |
|
if err != nil { |
|
t.Fatalf(err.Error()) |
|
} |
|
|
|
// Checking the transfer of total 1000 Satoshis following the transaction history as |
|
// Alice -> Bob -> Carol, i.e all the 1000 Satoshis are sent from Alice to Carol via Bob |
|
err = wait.NoError( |
|
assertAmountSent(paymentAmt, net.Alice, Carol), |
|
3*time.Second, |
|
) |
|
if err != nil { |
|
t.Fatalf(err.Error()) |
|
} |
|
|
|
// Create another invoice for Bob, this time leaving off the preimage |
|
// to one will be randomly generated. We'll test the proper |
|
// encoding/decoding of the zpay32 payment requests. |
|
invoice = &lnrpc.Invoice{ |
|
Memo: "test3", |
|
Value: paymentAmt, |
|
} |
|
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout) |
|
invoiceResp, err = net.Bob.AddInvoice(ctxt, invoice) |
|
if err != nil { |
|
t.Fatalf("unable to add invoice: %v", err) |
|
} |
|
|
|
// Next send another payment, but this time using a zpay32 encoded |
|
// invoice rather than manually specifying the payment details. |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
sendAndAssertSuccess( |
|
ctxt, t, net.Alice, |
|
&routerrpc.SendPaymentRequest{ |
|
PaymentRequest: invoiceResp.PaymentRequest, |
|
TimeoutSeconds: 60, |
|
FeeLimitMsat: noFeeLimitMsat, |
|
}, |
|
) |
|
|
|
// As done in the above test for transaction between alice and bob, |
|
// creating another invoice for Carol, this time leaving off the preimage |
|
// to one will be randomly generated. Testing the proper |
|
// encoding/decoding of the zpay32 payment requests. |
|
invoiceCarol = &lnrpc.Invoice{ |
|
Memo: "test3", |
|
Value: paymentAmt, |
|
} |
|
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout) |
|
invoiceRespCarol, err = Carol.AddInvoice(ctxt, invoice) |
|
if err != nil { |
|
t.Fatalf("unable to add invoice: %v", err) |
|
} |
|
|
|
// Next send another payment, but this time using a zpay32 encoded |
|
// invoice rather than manually specifying the payment details. |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
sendAndAssertSuccess( |
|
ctxt, t, net.Bob, |
|
&routerrpc.SendPaymentRequest{ |
|
PaymentRequest: invoiceRespCarol.PaymentRequest, |
|
TimeoutSeconds: 60, |
|
FeeLimitMsat: noFeeLimitMsat, |
|
}, |
|
) |
|
|
|
// The second payment should also have succeeded, with the balances |
|
// being update accordingly. |
|
err = wait.NoError( |
|
assertAmountSent(2*paymentAmt, net.Bob, Carol), |
|
3*time.Second, |
|
) |
|
if err != nil { |
|
t.Fatalf(err.Error()) |
|
} |
|
|
|
// Checking the transfer of total 2000 Satoshis following the transaction history as |
|
// Alice -> Bob -> Carol, i.e all the 2000 Satoshis are being sent from Alice to Carol via Bob |
|
err = wait.NoError( |
|
assertAmountSent(2*paymentAmt, net.Alice, Carol), |
|
3*time.Second, |
|
) |
|
if err != nil { |
|
t.Fatalf(err.Error()) |
|
} |
|
|
|
// Next send a keysend payment. |
|
keySendPreimage := lntypes.Preimage{3, 4, 5, 11} |
|
keySendHash := keySendPreimage.Hash() |
|
|
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
sendAndAssertSuccess( |
|
ctxt, t, net.Alice, |
|
&routerrpc.SendPaymentRequest{ |
|
Dest: net.Bob.PubKey[:], |
|
Amt: paymentAmt, |
|
FinalCltvDelta: 40, |
|
PaymentHash: keySendHash[:], |
|
DestCustomRecords: map[uint64][]byte{ |
|
record.KeySendType: keySendPreimage[:], |
|
}, |
|
TimeoutSeconds: 60, |
|
FeeLimitMsat: noFeeLimitMsat, |
|
}, |
|
) |
|
|
|
// Similarly, sending a keysend payment. |
|
|
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
sendAndAssertSuccess( |
|
ctxt, t, net.Bob, |
|
&routerrpc.SendPaymentRequest{ |
|
Dest: Carol.PubKey[:], |
|
Amt: paymentAmt, |
|
FinalCltvDelta: 40, |
|
PaymentHash: keySendHash[:], |
|
DestCustomRecords: map[uint64][]byte{ |
|
record.KeySendType: keySendPreimage[:], |
|
}, |
|
TimeoutSeconds: 60, |
|
FeeLimitMsat: noFeeLimitMsat, |
|
}, |
|
) |
|
|
|
// The keysend payment should also have succeeded, with the balances |
|
// being update accordingly. |
|
err = wait.NoError( |
|
assertAmountSent(3*paymentAmt, net.Bob, Carol), |
|
3*time.Second, |
|
) |
|
if err != nil { |
|
t.Fatalf(err.Error()) |
|
} |
|
|
|
// Checking the transfer of total 3000 Satoshis following the transaction history as |
|
// Alice -> Bob -> Carol, i.e all the 3000 Satoshis are being sent from Alice to Carol via Bob |
|
err = wait.NoError( |
|
assertAmountSent(3*paymentAmt, net.Alice, Carol), |
|
3*time.Second, |
|
) |
|
if err != nil { |
|
t.Fatalf(err.Error()) |
|
} |
|
|
|
// Assert that the invoice has the proper AMP fields set, since the |
|
// legacy keysend payment should have been promoted into an AMP payment |
|
// internally. |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
keysendInvoice, err := net.Bob.LookupInvoice( |
|
ctxt, &lnrpc.PaymentHash{ |
|
RHash: keySendHash[:], |
|
}, |
|
) |
|
require.NoError(t.t, err) |
|
require.Equal(t.t, 1, len(keysendInvoice.Htlcs)) |
|
htlc := keysendInvoice.Htlcs[0] |
|
require.Equal(t.t, uint64(0), htlc.MppTotalAmtMsat) |
|
require.Nil(t.t, htlc.Amp) |
|
|
|
// Now create an invoice and specify routing hints. |
|
// We will test that the routing hints are encoded properly. |
|
hintChannel := lnwire.ShortChannelID{BlockHeight: 10} |
|
bobPubKey := hex.EncodeToString(net.Bob.PubKey[:]) |
|
hints := []*lnrpc.RouteHint{ |
|
{ |
|
HopHints: []*lnrpc.HopHint{ |
|
{ |
|
NodeId: bobPubKey, |
|
ChanId: hintChannel.ToUint64(), |
|
FeeBaseMsat: 1, |
|
FeeProportionalMillionths: 1000000, |
|
CltvExpiryDelta: 20, |
|
}, |
|
}, |
|
}, |
|
} |
|
|
|
invoice = &lnrpc.Invoice{ |
|
Memo: "hints", |
|
Value: paymentAmt, |
|
RouteHints: hints, |
|
} |
|
|
|
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout) |
|
invoiceResp, err = net.Bob.AddInvoice(ctxt, invoice) |
|
if err != nil { |
|
t.Fatalf("unable to add invoice: %v", err) |
|
} |
|
payreq, err := net.Bob.DecodePayReq(ctxt, &lnrpc.PayReqString{PayReq: invoiceResp.PaymentRequest}) |
|
if err != nil { |
|
t.Fatalf("failed to decode payment request %v", err) |
|
} |
|
if len(payreq.RouteHints) != 1 { |
|
t.Fatalf("expected one routing hint") |
|
} |
|
routingHint := payreq.RouteHints[0] |
|
if len(routingHint.HopHints) != 1 { |
|
t.Fatalf("expected one hop hint") |
|
} |
|
hopHint := routingHint.HopHints[0] |
|
if hopHint.FeeProportionalMillionths != 1000000 { |
|
t.Fatalf("wrong FeeProportionalMillionths %v", |
|
hopHint.FeeProportionalMillionths) |
|
} |
|
if hopHint.NodeId != bobPubKey { |
|
t.Fatalf("wrong NodeId %v", |
|
hopHint.NodeId) |
|
} |
|
if hopHint.ChanId != hintChannel.ToUint64() { |
|
t.Fatalf("wrong ChanId %v", |
|
hopHint.ChanId) |
|
} |
|
if hopHint.FeeBaseMsat != 1 { |
|
t.Fatalf("wrong FeeBaseMsat %v", |
|
hopHint.FeeBaseMsat) |
|
} |
|
if hopHint.CltvExpiryDelta != 20 { |
|
t.Fatalf("wrong CltvExpiryDelta %v", |
|
hopHint.CltvExpiryDelta) |
|
} |
|
|
|
|
|
// In the same way, asserting that the invoice has the proper AMP fields set, since the |
|
// legacy keysend payment should have been promoted into an AMP payment |
|
// internally. |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
keysendInvoice, err = Carol.LookupInvoice( |
|
ctxt, &lnrpc.PaymentHash{ |
|
RHash: keySendHash[:], |
|
}, |
|
) |
|
require.NoError(t.t, err) |
|
require.Equal(t.t, 1, len(keysendInvoice.Htlcs)) |
|
htlc = keysendInvoice.Htlcs[0] |
|
require.Equal(t.t, uint64(0), htlc.MppTotalAmtMsat) |
|
require.Nil(t.t, htlc.Amp) |
|
|
|
// Now create an invoice and specify routing hints. |
|
// We will test that the routing hints are encoded properly. |
|
hintChannel = lnwire.ShortChannelID{BlockHeight: 10} |
|
carolPubKey := hex.EncodeToString(Carol.PubKey[:]) |
|
hints = []*lnrpc.RouteHint{ |
|
{ |
|
HopHints: []*lnrpc.HopHint{ |
|
{ |
|
NodeId: carolPubKey, |
|
ChanId: hintChannel.ToUint64(), |
|
FeeBaseMsat: 1, |
|
FeeProportionalMillionths: 1000000, |
|
CltvExpiryDelta: 20, |
|
}, |
|
}, |
|
}, |
|
} |
|
|
|
invoice = &lnrpc.Invoice{ |
|
Memo: "hints", |
|
Value: paymentAmt, |
|
RouteHints: hints, |
|
} |
|
|
|
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout) |
|
invoiceRespCarol, err = Carol.AddInvoice(ctxt, invoice) |
|
if err != nil { |
|
t.Fatalf("unable to add invoice: %v", err) |
|
} |
|
payreq, err = Carol.DecodePayReq(ctxt, &lnrpc.PayReqString{PayReq: invoiceRespCarol.PaymentRequest}) |
|
if err != nil { |
|
t.Fatalf("failed to decode payment request %v", err) |
|
} |
|
if len(payreq.RouteHints) != 1 { |
|
t.Fatalf("expected one routing hint") |
|
} |
|
routingHint = payreq.RouteHints[0] |
|
if len(routingHint.HopHints) != 1 { |
|
t.Fatalf("expected one hop hint") |
|
} |
|
hopHint = routingHint.HopHints[0] |
|
if hopHint.FeeProportionalMillionths != 1000000 { |
|
t.Fatalf("wrong FeeProportionalMillionths %v", |
|
hopHint.FeeProportionalMillionths) |
|
} |
|
if hopHint.NodeId != carolPubKey { |
|
t.Fatalf("wrong NodeId %v", |
|
hopHint.NodeId) |
|
} |
|
if hopHint.ChanId != hintChannel.ToUint64() { |
|
t.Fatalf("wrong ChanId %v", |
|
hopHint.ChanId) |
|
} |
|
if hopHint.FeeBaseMsat != 1 { |
|
t.Fatalf("wrong FeeBaseMsat %v", |
|
hopHint.FeeBaseMsat) |
|
} |
|
if hopHint.CltvExpiryDelta != 20 { |
|
t.Fatalf("wrong CltvExpiryDelta %v", |
|
hopHint.CltvExpiryDelta) |
|
} |
|
|
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout) |
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPoint, false) |
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout) |
|
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointCarol, false) |
|
|
|
} |