Merge pull request #4234 from carlaKC/htlcnotifier-unknownfailuredetail
routerrpc+lntest: add nil check for failure detail and add itest coverage
This commit is contained in:
commit
4a90d55789
@ -120,15 +120,21 @@ func rpcFailReason(linkErr *htlcswitch.LinkError) (lnrpc.Failure_FailureCode,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
wireCode := wireErr.GetCode()
|
||||||
|
|
||||||
|
// If the link has no failure detail, return with failure detail none.
|
||||||
|
if linkErr.FailureDetail == nil {
|
||||||
|
return wireCode, FailureDetail_NO_DETAIL, nil
|
||||||
|
}
|
||||||
|
|
||||||
switch failureDetail := linkErr.FailureDetail.(type) {
|
switch failureDetail := linkErr.FailureDetail.(type) {
|
||||||
case invoices.FailResolutionResult:
|
case invoices.FailResolutionResult:
|
||||||
fd, err := rpcFailureResolution(failureDetail)
|
fd, err := rpcFailureResolution(failureDetail)
|
||||||
return wireErr.GetCode(), fd, err
|
return wireCode, fd, err
|
||||||
|
|
||||||
case htlcswitch.OutgoingFailure:
|
case htlcswitch.OutgoingFailure:
|
||||||
fd, err := rpcOutgoingFailure(failureDetail)
|
fd, err := rpcOutgoingFailure(failureDetail)
|
||||||
return wireErr.GetCode(), fd, err
|
return wireCode, fd, err
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return 0, 0, fmt.Errorf("unknown failure "+
|
return 0, 0, fmt.Errorf("unknown failure "+
|
||||||
|
442
lntest/itest/lnd_multi-hop-error-propagation.go
Normal file
442
lntest/itest/lnd_multi-hop-error-propagation.go
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
// +build rpctest
|
||||||
|
|
||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testHtlcErrorPropagation(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
// In this test we wish to exercise the daemon's correct parsing,
|
||||||
|
// handling, and propagation of errors that occur while processing a
|
||||||
|
// multi-hop payment.
|
||||||
|
const chanAmt = lnd.MaxBtcFundingAmount
|
||||||
|
|
||||||
|
// First establish a channel with a capacity of 0.5 BTC between Alice
|
||||||
|
// and Bob.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAlice := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPointAlice); err != nil {
|
||||||
|
t.Fatalf("channel not seen by alice before timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cType, err := channelCommitType(net.Alice, chanPointAlice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get channel type: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commitFee := cType.calcStaticFee(0)
|
||||||
|
assertBaseBalance := func() {
|
||||||
|
balReq := &lnrpc.ChannelBalanceRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
aliceBal, err := net.Alice.ChannelBalance(ctxt, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get channel balance: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
bobBal, err := net.Bob.ChannelBalance(ctxt, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get channel balance: %v", err)
|
||||||
|
}
|
||||||
|
if aliceBal.Balance != int64(chanAmt-commitFee) {
|
||||||
|
t.Fatalf("alice has an incorrect balance: expected %v got %v",
|
||||||
|
int64(chanAmt-commitFee), aliceBal)
|
||||||
|
}
|
||||||
|
if bobBal.Balance != int64(chanAmt-commitFee) {
|
||||||
|
t.Fatalf("bob has an incorrect balance: expected %v got %v",
|
||||||
|
int64(chanAmt-commitFee), bobBal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we'd like to test some multi-hop failure scenarios, we'll
|
||||||
|
// introduce another node into our test network: Carol.
|
||||||
|
carol, err := net.NewNode("Carol", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create new nodes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll create a connection from Bob to Carol, and open a
|
||||||
|
// channel between them so we have the topology: Alice -> Bob -> Carol.
|
||||||
|
// The channel created will be of lower capacity that the one created
|
||||||
|
// above.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, net.Bob, carol); err != nil {
|
||||||
|
t.Fatalf("unable to connect bob to carol: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
const bobChanAmt = lnd.MaxBtcFundingAmount
|
||||||
|
chanPointBob := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Bob, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure that Alice has Carol in her routing table before proceeding.
|
||||||
|
nodeInfoReq := &lnrpc.NodeInfoRequest{
|
||||||
|
PubKey: carol.PubKeyStr,
|
||||||
|
}
|
||||||
|
checkTableTimeout := time.After(time.Second * 10)
|
||||||
|
checkTableTicker := time.NewTicker(100 * time.Millisecond)
|
||||||
|
defer checkTableTicker.Stop()
|
||||||
|
|
||||||
|
out:
|
||||||
|
// TODO(roasbeef): make into async hook for node announcements
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-checkTableTicker.C:
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
_, err := net.Alice.GetNodeInfo(ctxt, nodeInfoReq)
|
||||||
|
if err != nil && strings.Contains(err.Error(),
|
||||||
|
"unable to find") {
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
break out
|
||||||
|
case <-checkTableTimeout:
|
||||||
|
t.Fatalf("carol's node announcement didn't propagate within " +
|
||||||
|
"the timeout period")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the channels, open we can now start to test our multi-hop error
|
||||||
|
// scenarios. First, we'll generate an invoice from carol that we'll
|
||||||
|
// use to test some error cases.
|
||||||
|
const payAmt = 10000
|
||||||
|
invoiceReq := &lnrpc.Invoice{
|
||||||
|
Memo: "kek99",
|
||||||
|
Value: payAmt,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
carolInvoice, err := carol.AddInvoice(ctxt, invoiceReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate carol invoice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
carolPayReq, err := carol.DecodePayReq(ctxb,
|
||||||
|
&lnrpc.PayReqString{
|
||||||
|
PayReq: carolInvoice.PaymentRequest,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to decode generated payment request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before we send the payment, ensure that the announcement of the new
|
||||||
|
// channel has been processed by Alice.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPointBob); err != nil {
|
||||||
|
t.Fatalf("channel not seen by alice before timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before we start sending payments, subscribe to htlc events for each
|
||||||
|
// node.
|
||||||
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
aliceEvents, err := net.Alice.RouterClient.SubscribeHtlcEvents(
|
||||||
|
ctxt, &routerrpc.SubscribeHtlcEventsRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not subscribe events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bobEvents, err := net.Bob.RouterClient.SubscribeHtlcEvents(
|
||||||
|
ctxt, &routerrpc.SubscribeHtlcEventsRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not subscribe events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
carolEvents, err := carol.RouterClient.SubscribeHtlcEvents(
|
||||||
|
ctxt, &routerrpc.SubscribeHtlcEventsRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not subscribe events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the first scenario, we'll test the cancellation of an HTLC with
|
||||||
|
// an unknown payment hash.
|
||||||
|
// TODO(roasbeef): return failure response rather than failing entire
|
||||||
|
// stream on payment error.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
sendReq := &lnrpc.SendRequest{
|
||||||
|
PaymentHashString: hex.EncodeToString(makeFakePayHash(t)),
|
||||||
|
DestString: hex.EncodeToString(carol.PubKey[:]),
|
||||||
|
Amt: payAmt,
|
||||||
|
FinalCltvDelta: int32(carolPayReq.CltvExpiry),
|
||||||
|
}
|
||||||
|
resp, err := net.Alice.SendPaymentSync(ctxt, sendReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The payment should have resulted in an error since we sent it with the
|
||||||
|
// wrong payment hash.
|
||||||
|
if resp.PaymentError == "" {
|
||||||
|
t.Fatalf("payment should have been rejected due to invalid " +
|
||||||
|
"payment hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertLastHTLCError(
|
||||||
|
t, net.Alice,
|
||||||
|
lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We expect alice and bob to each have one forward and one forward
|
||||||
|
// fail event at this stage.
|
||||||
|
assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_SEND, aliceEvents)
|
||||||
|
assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_FORWARD, bobEvents)
|
||||||
|
|
||||||
|
// Carol should have a link failure because the htlc failed on her
|
||||||
|
// incoming link.
|
||||||
|
assertLinkFailure(
|
||||||
|
t, routerrpc.HtlcEvent_RECEIVE,
|
||||||
|
routerrpc.FailureDetail_UNKNOWN_INVOICE, carolEvents,
|
||||||
|
)
|
||||||
|
|
||||||
|
// The balances of all parties should be the same as initially since
|
||||||
|
// the HTLC was canceled.
|
||||||
|
assertBaseBalance()
|
||||||
|
|
||||||
|
// Next, we'll test the case of a recognized payHash but, an incorrect
|
||||||
|
// value on the extended HTLC.
|
||||||
|
htlcAmt := lnwire.NewMSatFromSatoshis(1000)
|
||||||
|
sendReq = &lnrpc.SendRequest{
|
||||||
|
PaymentHashString: hex.EncodeToString(carolInvoice.RHash),
|
||||||
|
DestString: hex.EncodeToString(carol.PubKey[:]),
|
||||||
|
Amt: int64(htlcAmt.ToSatoshis()), // 10k satoshis are expected.
|
||||||
|
FinalCltvDelta: int32(carolPayReq.CltvExpiry),
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err = net.Alice.SendPaymentSync(ctxt, sendReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The payment should fail with an error since we sent 1k satoshis isn't of
|
||||||
|
// 10k as was requested.
|
||||||
|
if resp.PaymentError == "" {
|
||||||
|
t.Fatalf("payment should have been rejected due to wrong " +
|
||||||
|
"HTLC amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertLastHTLCError(
|
||||||
|
t, net.Alice,
|
||||||
|
lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We expect alice and bob to each have one forward and one forward
|
||||||
|
// fail event at this stage.
|
||||||
|
assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_SEND, aliceEvents)
|
||||||
|
assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_FORWARD, bobEvents)
|
||||||
|
|
||||||
|
// Carol should have a link failure because the htlc failed on her
|
||||||
|
// incoming link.
|
||||||
|
assertLinkFailure(
|
||||||
|
t, routerrpc.HtlcEvent_RECEIVE,
|
||||||
|
routerrpc.FailureDetail_INVOICE_UNDERPAID, carolEvents,
|
||||||
|
)
|
||||||
|
|
||||||
|
// The balances of all parties should be the same as initially since
|
||||||
|
// the HTLC was canceled.
|
||||||
|
assertBaseBalance()
|
||||||
|
|
||||||
|
// Next we'll test an error that occurs mid-route due to an outgoing
|
||||||
|
// link having insufficient capacity. In order to do so, we'll first
|
||||||
|
// need to unbalance the link connecting Bob<->Carol.
|
||||||
|
ctx, cancel := context.WithCancel(ctxb)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
bobPayStream, err := net.Bob.SendPayment(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create payment stream: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To do so, we'll push most of the funds in the channel over to
|
||||||
|
// Alice's side, leaving on 10k satoshis of available balance for bob.
|
||||||
|
// There's a max payment amount, so we'll have to do this
|
||||||
|
// incrementally.
|
||||||
|
chanReserve := int64(chanAmt / 100)
|
||||||
|
amtToSend := int64(chanAmt) - chanReserve - 20000
|
||||||
|
amtSent := int64(0)
|
||||||
|
for amtSent != amtToSend {
|
||||||
|
// We'll send in chunks of the max payment amount. If we're
|
||||||
|
// about to send too much, then we'll only send the amount
|
||||||
|
// remaining.
|
||||||
|
toSend := int64(lnd.MaxPaymentMSat.ToSatoshis())
|
||||||
|
if toSend+amtSent > amtToSend {
|
||||||
|
toSend = amtToSend - amtSent
|
||||||
|
}
|
||||||
|
|
||||||
|
invoiceReq = &lnrpc.Invoice{
|
||||||
|
Value: toSend,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
carolInvoice2, err := carol.AddInvoice(ctxt, invoiceReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate carol invoice: %v", err)
|
||||||
|
}
|
||||||
|
if err := bobPayStream.Send(&lnrpc.SendRequest{
|
||||||
|
PaymentRequest: carolInvoice2.PaymentRequest,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp, err := bobPayStream.Recv(); err != nil {
|
||||||
|
t.Fatalf("payment stream has been closed: %v", err)
|
||||||
|
} else if resp.PaymentError != "" {
|
||||||
|
t.Fatalf("bob's payment failed: %v", resp.PaymentError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each send bob makes, we need to check that bob has a
|
||||||
|
// forward and settle event for his send, and carol has a
|
||||||
|
// settle event for her receive.
|
||||||
|
assertHtlcEvents(
|
||||||
|
t, 1, 0, 1, routerrpc.HtlcEvent_SEND, bobEvents,
|
||||||
|
)
|
||||||
|
assertHtlcEvents(
|
||||||
|
t, 0, 0, 1, routerrpc.HtlcEvent_RECEIVE, carolEvents,
|
||||||
|
)
|
||||||
|
|
||||||
|
amtSent += toSend
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, Alice has 50mil satoshis on her side of the channel,
|
||||||
|
// but Bob only has 10k available on his side of the channel. So a
|
||||||
|
// payment from Alice to Carol worth 100k satoshis should fail.
|
||||||
|
invoiceReq = &lnrpc.Invoice{
|
||||||
|
Value: 100000,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
carolInvoice3, err := carol.AddInvoice(ctxt, invoiceReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate carol invoice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendReq = &lnrpc.SendRequest{
|
||||||
|
PaymentRequest: carolInvoice3.PaymentRequest,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err = net.Alice.SendPaymentSync(ctxt, sendReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
|
}
|
||||||
|
if resp.PaymentError == "" {
|
||||||
|
t.Fatalf("payment should fail due to insufficient "+
|
||||||
|
"capacity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertLastHTLCError(
|
||||||
|
t, net.Alice, lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Alice should have a forwarding event and a forwarding failure.
|
||||||
|
assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_SEND, aliceEvents)
|
||||||
|
|
||||||
|
// Bob should have a link failure because the htlc failed on his
|
||||||
|
// outgoing link.
|
||||||
|
assertLinkFailure(
|
||||||
|
t, routerrpc.HtlcEvent_FORWARD,
|
||||||
|
routerrpc.FailureDetail_INSUFFICIENT_BALANCE, bobEvents,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generate new invoice to not pay same invoice twice.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
carolInvoice, err = carol.AddInvoice(ctxt, invoiceReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate carol invoice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For our final test, we'll ensure that if a target link isn't
|
||||||
|
// available for what ever reason then the payment fails accordingly.
|
||||||
|
//
|
||||||
|
// We'll attempt to complete the original invoice we created with Carol
|
||||||
|
// above, but before we do so, Carol will go offline, resulting in a
|
||||||
|
// failed payment.
|
||||||
|
shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
// Reset mission control to forget the temporary channel failure above.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
_, err = net.Alice.RouterClient.ResetMissionControl(
|
||||||
|
ctxt, &routerrpc.ResetMissionControlRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to reset mission control: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendReq = &lnrpc.SendRequest{
|
||||||
|
PaymentRequest: carolInvoice.PaymentRequest,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err = net.Alice.SendPaymentSync(ctxt, sendReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.PaymentError == "" {
|
||||||
|
t.Fatalf("payment should have failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertLastHTLCError(t, net.Alice, lnrpc.Failure_UNKNOWN_NEXT_PEER)
|
||||||
|
|
||||||
|
// Alice should have a forwarding event and subsequent fail.
|
||||||
|
assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_SEND, aliceEvents)
|
||||||
|
|
||||||
|
// Bob should have a link failure because he could not find the next
|
||||||
|
// peer.
|
||||||
|
assertLinkFailure(
|
||||||
|
t, routerrpc.HtlcEvent_FORWARD,
|
||||||
|
routerrpc.FailureDetail_NO_DETAIL, bobEvents,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Finally, immediately close the channel. This function will also
|
||||||
|
// block until the channel is closed and will additionally assert the
|
||||||
|
// relevant channel closing post conditions.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
||||||
|
|
||||||
|
// Force close Bob's final channel.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBob, true)
|
||||||
|
|
||||||
|
// Cleanup by mining the force close and sweep transaction.
|
||||||
|
cleanupForceClose(t, net, net.Bob, chanPointBob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertLinkFailure checks that the stream provided has a single link failure
|
||||||
|
// the the failure detail provided.
|
||||||
|
func assertLinkFailure(t *harnessTest,
|
||||||
|
eventType routerrpc.HtlcEvent_EventType,
|
||||||
|
failureDetail routerrpc.FailureDetail,
|
||||||
|
client routerrpc.Router_SubscribeHtlcEventsClient) {
|
||||||
|
|
||||||
|
event := assertEventAndType(t, eventType, client)
|
||||||
|
|
||||||
|
linkFail, ok := event.Event.(*routerrpc.HtlcEvent_LinkFailEvent)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected forwarding failure, got: %T", linkFail)
|
||||||
|
}
|
||||||
|
|
||||||
|
if linkFail.LinkFailEvent.FailureDetail != failureDetail {
|
||||||
|
t.Fatalf("expected: %v, got: %v", failureDetail,
|
||||||
|
linkFail.LinkFailEvent.FailureDetail)
|
||||||
|
}
|
||||||
|
}
|
407
lntest/itest/lnd_multi-hop-payments.go
Normal file
407
lntest/itest/lnd_multi-hop-payments.go
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
// +build rpctest
|
||||||
|
|
||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
const chanAmt = btcutil.Amount(100000)
|
||||||
|
var networkChans []*lnrpc.ChannelPoint
|
||||||
|
|
||||||
|
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
||||||
|
// being the sole funder of the channel.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAlice := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointAlice)
|
||||||
|
|
||||||
|
aliceChanTXID, err := lnd.GetChanPointFundingTxid(chanPointAlice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
aliceFundPoint := wire.OutPoint{
|
||||||
|
Hash: *aliceChanTXID,
|
||||||
|
Index: chanPointAlice.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// As preliminary setup, we'll create two new nodes: Carol and Dave,
|
||||||
|
// such that we now have a 4 node, 3 channel topology. Dave will make a
|
||||||
|
// channel with Alice, and Carol with Dave. After this setup, the
|
||||||
|
// network topology should now look like:
|
||||||
|
// Carol -> Dave -> Alice -> Bob
|
||||||
|
//
|
||||||
|
// First, we'll create Dave and establish a channel to Alice. Dave will
|
||||||
|
// be running an older node that requires the legacy onion payload.
|
||||||
|
daveArgs := []string{"--protocol.legacyonion"}
|
||||||
|
dave, err := net.NewNode("Dave", daveArgs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create new nodes: %v", err)
|
||||||
|
}
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, dave, net.Alice); err != nil {
|
||||||
|
t.Fatalf("unable to connect dave to alice: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, dave)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send coins to dave: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointDave := openChannelAndAssert(
|
||||||
|
ctxt, t, net, dave, net.Alice,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointDave)
|
||||||
|
daveChanTXID, err := lnd.GetChanPointFundingTxid(chanPointDave)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
daveFundPoint := wire.OutPoint{
|
||||||
|
Hash: *daveChanTXID,
|
||||||
|
Index: chanPointDave.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll create Carol and establish a channel to from her to
|
||||||
|
// Dave.
|
||||||
|
carol, err := net.NewNode("Carol", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create new nodes: %v", err)
|
||||||
|
}
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to dave: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send coins to carol: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, carol, dave,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointCarol)
|
||||||
|
|
||||||
|
carolChanTXID, err := lnd.GetChanPointFundingTxid(chanPointCarol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
carolFundPoint := wire.OutPoint{
|
||||||
|
Hash: *carolChanTXID,
|
||||||
|
Index: chanPointCarol.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all nodes to have seen all channels.
|
||||||
|
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave}
|
||||||
|
nodeNames := []string{"Alice", "Bob", "Carol", "Dave"}
|
||||||
|
for _, chanPoint := range networkChans {
|
||||||
|
for i, node := range nodes {
|
||||||
|
txid, err := lnd.GetChanPointFundingTxid(chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
point := wire.OutPoint{
|
||||||
|
Hash: *txid,
|
||||||
|
Index: chanPoint.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s(%d): timeout waiting for "+
|
||||||
|
"channel(%s) open: %v", nodeNames[i],
|
||||||
|
node.NodeID, point, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 5 invoices for Bob, which expect a payment from Carol for 1k
|
||||||
|
// satoshis with a different preimage each time.
|
||||||
|
const numPayments = 5
|
||||||
|
const paymentAmt = 1000
|
||||||
|
payReqs, _, _, err := createPayReqs(
|
||||||
|
net.Bob, paymentAmt, numPayments,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create pay reqs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll wait for all parties to recognize the new channels within the
|
||||||
|
// network.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = dave.WaitForNetworkChannelOpen(ctxt, chanPointDave)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dave didn't advertise his channel: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointCarol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("carol didn't advertise her channel in time: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 50)
|
||||||
|
|
||||||
|
// Set the fee policies of the Alice -> Bob and the Dave -> Alice
|
||||||
|
// channel edges to relatively large non default values. This makes it
|
||||||
|
// possible to pick up more subtle fee calculation errors.
|
||||||
|
maxHtlc := uint64(calculateMaxHtlc(chanAmt))
|
||||||
|
updateChannelPolicy(
|
||||||
|
t, net.Alice, chanPointAlice, 1000, 100000,
|
||||||
|
lnd.DefaultBitcoinTimeLockDelta, maxHtlc, carol,
|
||||||
|
)
|
||||||
|
|
||||||
|
updateChannelPolicy(
|
||||||
|
t, dave, chanPointDave, 5000, 150000,
|
||||||
|
lnd.DefaultBitcoinTimeLockDelta, maxHtlc, carol,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Before we start sending payments, subscribe to htlc events for each
|
||||||
|
// node.
|
||||||
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
aliceEvents, err := net.Alice.RouterClient.SubscribeHtlcEvents(
|
||||||
|
ctxt, &routerrpc.SubscribeHtlcEventsRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not subscribe events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bobEvents, err := net.Bob.RouterClient.SubscribeHtlcEvents(
|
||||||
|
ctxt, &routerrpc.SubscribeHtlcEventsRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not subscribe events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
carolEvents, err := carol.RouterClient.SubscribeHtlcEvents(
|
||||||
|
ctxt, &routerrpc.SubscribeHtlcEventsRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not subscribe events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
daveEvents, err := dave.RouterClient.SubscribeHtlcEvents(
|
||||||
|
ctxt, &routerrpc.SubscribeHtlcEventsRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not subscribe events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using Carol as the source, pay to the 5 invoices from Bob created
|
||||||
|
// above.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = completePaymentRequests(ctxt, carol, payReqs, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payments: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When asserting the amount of satoshis moved, we'll factor in the
|
||||||
|
// default base fee, as we didn't modify the fee structure when
|
||||||
|
// creating the seed nodes in the network.
|
||||||
|
const baseFee = 1
|
||||||
|
|
||||||
|
// At this point all the channels within our proto network should be
|
||||||
|
// shifted by 5k satoshis in the direction of Bob, the sink within the
|
||||||
|
// payment flow generated above. The order of asserts corresponds to
|
||||||
|
// increasing of time is needed to embed the HTLC in commitment
|
||||||
|
// transaction, in channel Carol->David->Alice->Bob, order is Bob,
|
||||||
|
// Alice, David, Carol.
|
||||||
|
|
||||||
|
// The final node bob expects to get paid five times 1000 sat.
|
||||||
|
expectedAmountPaidAtoB := int64(5 * 1000)
|
||||||
|
|
||||||
|
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Bob,
|
||||||
|
aliceFundPoint, int64(0), expectedAmountPaidAtoB)
|
||||||
|
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Alice,
|
||||||
|
aliceFundPoint, expectedAmountPaidAtoB, int64(0))
|
||||||
|
|
||||||
|
// To forward a payment of 1000 sat, Alice is charging a fee of
|
||||||
|
// 1 sat + 10% = 101 sat.
|
||||||
|
const expectedFeeAlice = 5 * 101
|
||||||
|
|
||||||
|
// Dave needs to pay what Alice pays plus Alice's fee.
|
||||||
|
expectedAmountPaidDtoA := expectedAmountPaidAtoB + expectedFeeAlice
|
||||||
|
|
||||||
|
assertAmountPaid(t, "Dave(local) => Alice(remote)", net.Alice,
|
||||||
|
daveFundPoint, int64(0), expectedAmountPaidDtoA)
|
||||||
|
assertAmountPaid(t, "Dave(local) => Alice(remote)", dave,
|
||||||
|
daveFundPoint, expectedAmountPaidDtoA, int64(0))
|
||||||
|
|
||||||
|
// To forward a payment of 1101 sat, Dave is charging a fee of
|
||||||
|
// 5 sat + 15% = 170.15 sat. This is rounded down in rpcserver to 170.
|
||||||
|
const expectedFeeDave = 5 * 170
|
||||||
|
|
||||||
|
// Carol needs to pay what Dave pays plus Dave's fee.
|
||||||
|
expectedAmountPaidCtoD := expectedAmountPaidDtoA + expectedFeeDave
|
||||||
|
|
||||||
|
assertAmountPaid(t, "Carol(local) => Dave(remote)", dave,
|
||||||
|
carolFundPoint, int64(0), expectedAmountPaidCtoD)
|
||||||
|
assertAmountPaid(t, "Carol(local) => Dave(remote)", carol,
|
||||||
|
carolFundPoint, expectedAmountPaidCtoD, int64(0))
|
||||||
|
|
||||||
|
// Now that we know all the balances have been settled out properly,
|
||||||
|
// we'll ensure that our internal record keeping for completed circuits
|
||||||
|
// was properly updated.
|
||||||
|
|
||||||
|
// First, check that the FeeReport response shows the proper fees
|
||||||
|
// accrued over each time range. Dave should've earned 170 satoshi for
|
||||||
|
// each of the forwarded payments.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
feeReport, err := dave.FeeReport(ctxt, &lnrpc.FeeReportRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for fee report: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if feeReport.DayFeeSum != uint64(expectedFeeDave) {
|
||||||
|
t.Fatalf("fee mismatch: expected %v, got %v", expectedFeeDave,
|
||||||
|
feeReport.DayFeeSum)
|
||||||
|
}
|
||||||
|
if feeReport.WeekFeeSum != uint64(expectedFeeDave) {
|
||||||
|
t.Fatalf("fee mismatch: expected %v, got %v", expectedFeeDave,
|
||||||
|
feeReport.WeekFeeSum)
|
||||||
|
}
|
||||||
|
if feeReport.MonthFeeSum != uint64(expectedFeeDave) {
|
||||||
|
t.Fatalf("fee mismatch: expected %v, got %v", expectedFeeDave,
|
||||||
|
feeReport.MonthFeeSum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, ensure that if we issue the vanilla query for the forwarding
|
||||||
|
// history, it returns 5 values, and each entry is formatted properly.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
fwdingHistory, err := dave.ForwardingHistory(
|
||||||
|
ctxt, &lnrpc.ForwardingHistoryRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for fee report: %v", err)
|
||||||
|
}
|
||||||
|
if len(fwdingHistory.ForwardingEvents) != 5 {
|
||||||
|
t.Fatalf("wrong number of forwarding event: expected %v, "+
|
||||||
|
"got %v", 5, len(fwdingHistory.ForwardingEvents))
|
||||||
|
}
|
||||||
|
expectedForwardingFee := uint64(expectedFeeDave / numPayments)
|
||||||
|
for _, event := range fwdingHistory.ForwardingEvents {
|
||||||
|
// Each event should show a fee of 170 satoshi.
|
||||||
|
if event.Fee != expectedForwardingFee {
|
||||||
|
t.Fatalf("fee mismatch: expected %v, got %v",
|
||||||
|
expectedForwardingFee, event.Fee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect Carol to have successful forwards and settles for
|
||||||
|
// her sends.
|
||||||
|
assertHtlcEvents(
|
||||||
|
t, numPayments, 0, numPayments, routerrpc.HtlcEvent_SEND,
|
||||||
|
carolEvents,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dave and Alice should both have forwards and settles for
|
||||||
|
// their role as forwarding nodes.
|
||||||
|
assertHtlcEvents(
|
||||||
|
t, numPayments, 0, numPayments, routerrpc.HtlcEvent_FORWARD,
|
||||||
|
daveEvents,
|
||||||
|
)
|
||||||
|
assertHtlcEvents(
|
||||||
|
t, numPayments, 0, numPayments, routerrpc.HtlcEvent_FORWARD,
|
||||||
|
aliceEvents,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bob should only have settle events for his receives.
|
||||||
|
assertHtlcEvents(
|
||||||
|
t, 0, 0, numPayments, routerrpc.HtlcEvent_RECEIVE, bobEvents,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, dave, chanPointDave, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertHtlcEvents consumes events from a client and ensures that they are of
|
||||||
|
// the expected type and contain the expected number of forwards, forward
|
||||||
|
// failures and settles.
|
||||||
|
func assertHtlcEvents(t *harnessTest, fwdCount, fwdFailCount, settleCount int,
|
||||||
|
userType routerrpc.HtlcEvent_EventType,
|
||||||
|
client routerrpc.Router_SubscribeHtlcEventsClient) {
|
||||||
|
|
||||||
|
var forwards, forwardFails, settles int
|
||||||
|
|
||||||
|
numEvents := fwdCount + fwdFailCount + settleCount
|
||||||
|
for i := 0; i < numEvents; i++ {
|
||||||
|
event := assertEventAndType(t, userType, client)
|
||||||
|
|
||||||
|
switch event.Event.(type) {
|
||||||
|
case *routerrpc.HtlcEvent_ForwardEvent:
|
||||||
|
forwards++
|
||||||
|
|
||||||
|
case *routerrpc.HtlcEvent_ForwardFailEvent:
|
||||||
|
forwardFails++
|
||||||
|
|
||||||
|
case *routerrpc.HtlcEvent_SettleEvent:
|
||||||
|
settles++
|
||||||
|
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected event: %T", event.Event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if forwards != fwdCount {
|
||||||
|
t.Fatalf("expected: %v forwards, got: %v", fwdCount, forwards)
|
||||||
|
}
|
||||||
|
|
||||||
|
if forwardFails != fwdFailCount {
|
||||||
|
t.Fatalf("expected: %v forward fails, got: %v", fwdFailCount,
|
||||||
|
forwardFails)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settles != settleCount {
|
||||||
|
t.Fatalf("expected: %v settles, got: %v", settleCount, settles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertEventAndType reads an event from the stream provided and ensures that
|
||||||
|
// it is associated with the correct user related type - a user initiated send,
|
||||||
|
// a receive to our node or a forward through our node. Note that this event
|
||||||
|
// type is different from the htlc event type (forward, link failure etc).
|
||||||
|
func assertEventAndType(t *harnessTest, eventType routerrpc.HtlcEvent_EventType,
|
||||||
|
client routerrpc.Router_SubscribeHtlcEventsClient) *routerrpc.HtlcEvent {
|
||||||
|
event, err := client.Recv()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not get event")
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.EventType != eventType {
|
||||||
|
t.Fatalf("expected: %v, got: %v", eventType,
|
||||||
|
event.EventType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
@ -4504,281 +4504,6 @@ func updateChannelPolicy(t *harnessTest, node *lntest.HarnessNode,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
const chanAmt = btcutil.Amount(100000)
|
|
||||||
var networkChans []*lnrpc.ChannelPoint
|
|
||||||
|
|
||||||
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
|
||||||
// being the sole funder of the channel.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAlice := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointAlice)
|
|
||||||
|
|
||||||
aliceChanTXID, err := lnd.GetChanPointFundingTxid(chanPointAlice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
aliceFundPoint := wire.OutPoint{
|
|
||||||
Hash: *aliceChanTXID,
|
|
||||||
Index: chanPointAlice.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// As preliminary setup, we'll create two new nodes: Carol and Dave,
|
|
||||||
// such that we now have a 4 node, 3 channel topology. Dave will make a
|
|
||||||
// channel with Alice, and Carol with Dave. After this setup, the
|
|
||||||
// network topology should now look like:
|
|
||||||
// Carol -> Dave -> Alice -> Bob
|
|
||||||
//
|
|
||||||
// First, we'll create Dave and establish a channel to Alice. Dave will
|
|
||||||
// be running an older node that requires the legacy onion payload.
|
|
||||||
daveArgs := []string{"--protocol.legacyonion"}
|
|
||||||
dave, err := net.NewNode("Dave", daveArgs)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create new nodes: %v", err)
|
|
||||||
}
|
|
||||||
defer shutdownAndAssert(net, t, dave)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, dave, net.Alice); err != nil {
|
|
||||||
t.Fatalf("unable to connect dave to alice: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, dave)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send coins to dave: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointDave := openChannelAndAssert(
|
|
||||||
ctxt, t, net, dave, net.Alice,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointDave)
|
|
||||||
daveChanTXID, err := lnd.GetChanPointFundingTxid(chanPointDave)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
daveFundPoint := wire.OutPoint{
|
|
||||||
Hash: *daveChanTXID,
|
|
||||||
Index: chanPointDave.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we'll create Carol and establish a channel to from her to
|
|
||||||
// Dave.
|
|
||||||
carol, err := net.NewNode("Carol", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create new nodes: %v", err)
|
|
||||||
}
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
|
|
||||||
t.Fatalf("unable to connect carol to dave: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, carol)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send coins to carol: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, carol, dave,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointCarol)
|
|
||||||
|
|
||||||
carolChanTXID, err := lnd.GetChanPointFundingTxid(chanPointCarol)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
carolFundPoint := wire.OutPoint{
|
|
||||||
Hash: *carolChanTXID,
|
|
||||||
Index: chanPointCarol.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all nodes to have seen all channels.
|
|
||||||
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave}
|
|
||||||
nodeNames := []string{"Alice", "Bob", "Carol", "Dave"}
|
|
||||||
for _, chanPoint := range networkChans {
|
|
||||||
for i, node := range nodes {
|
|
||||||
txid, err := lnd.GetChanPointFundingTxid(chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
point := wire.OutPoint{
|
|
||||||
Hash: *txid,
|
|
||||||
Index: chanPoint.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s(%d): timeout waiting for "+
|
|
||||||
"channel(%s) open: %v", nodeNames[i],
|
|
||||||
node.NodeID, point, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create 5 invoices for Bob, which expect a payment from Carol for 1k
|
|
||||||
// satoshis with a different preimage each time.
|
|
||||||
const numPayments = 5
|
|
||||||
const paymentAmt = 1000
|
|
||||||
payReqs, _, _, err := createPayReqs(
|
|
||||||
net.Bob, paymentAmt, numPayments,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create pay reqs: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll wait for all parties to recognize the new channels within the
|
|
||||||
// network.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = dave.WaitForNetworkChannelOpen(ctxt, chanPointDave)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("dave didn't advertise his channel: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointCarol)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("carol didn't advertise her channel in time: %v",
|
|
||||||
err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 50)
|
|
||||||
|
|
||||||
// Set the fee policies of the Alice -> Bob and the Dave -> Alice
|
|
||||||
// channel edges to relatively large non default values. This makes it
|
|
||||||
// possible to pick up more subtle fee calculation errors.
|
|
||||||
maxHtlc := uint64(calculateMaxHtlc(chanAmt))
|
|
||||||
updateChannelPolicy(
|
|
||||||
t, net.Alice, chanPointAlice, 1000, 100000,
|
|
||||||
lnd.DefaultBitcoinTimeLockDelta, maxHtlc, carol,
|
|
||||||
)
|
|
||||||
|
|
||||||
updateChannelPolicy(
|
|
||||||
t, dave, chanPointDave, 5000, 150000,
|
|
||||||
lnd.DefaultBitcoinTimeLockDelta, maxHtlc, carol,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Using Carol as the source, pay to the 5 invoices from Bob created
|
|
||||||
// above.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = completePaymentRequests(ctxt, carol, payReqs, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payments: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When asserting the amount of satoshis moved, we'll factor in the
|
|
||||||
// default base fee, as we didn't modify the fee structure when
|
|
||||||
// creating the seed nodes in the network.
|
|
||||||
const baseFee = 1
|
|
||||||
|
|
||||||
// At this point all the channels within our proto network should be
|
|
||||||
// shifted by 5k satoshis in the direction of Bob, the sink within the
|
|
||||||
// payment flow generated above. The order of asserts corresponds to
|
|
||||||
// increasing of time is needed to embed the HTLC in commitment
|
|
||||||
// transaction, in channel Carol->David->Alice->Bob, order is Bob,
|
|
||||||
// Alice, David, Carol.
|
|
||||||
|
|
||||||
// The final node bob expects to get paid five times 1000 sat.
|
|
||||||
expectedAmountPaidAtoB := int64(5 * 1000)
|
|
||||||
|
|
||||||
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Bob,
|
|
||||||
aliceFundPoint, int64(0), expectedAmountPaidAtoB)
|
|
||||||
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Alice,
|
|
||||||
aliceFundPoint, expectedAmountPaidAtoB, int64(0))
|
|
||||||
|
|
||||||
// To forward a payment of 1000 sat, Alice is charging a fee of
|
|
||||||
// 1 sat + 10% = 101 sat.
|
|
||||||
const expectedFeeAlice = 5 * 101
|
|
||||||
|
|
||||||
// Dave needs to pay what Alice pays plus Alice's fee.
|
|
||||||
expectedAmountPaidDtoA := expectedAmountPaidAtoB + expectedFeeAlice
|
|
||||||
|
|
||||||
assertAmountPaid(t, "Dave(local) => Alice(remote)", net.Alice,
|
|
||||||
daveFundPoint, int64(0), expectedAmountPaidDtoA)
|
|
||||||
assertAmountPaid(t, "Dave(local) => Alice(remote)", dave,
|
|
||||||
daveFundPoint, expectedAmountPaidDtoA, int64(0))
|
|
||||||
|
|
||||||
// To forward a payment of 1101 sat, Dave is charging a fee of
|
|
||||||
// 5 sat + 15% = 170.15 sat. This is rounded down in rpcserver to 170.
|
|
||||||
const expectedFeeDave = 5 * 170
|
|
||||||
|
|
||||||
// Carol needs to pay what Dave pays plus Dave's fee.
|
|
||||||
expectedAmountPaidCtoD := expectedAmountPaidDtoA + expectedFeeDave
|
|
||||||
|
|
||||||
assertAmountPaid(t, "Carol(local) => Dave(remote)", dave,
|
|
||||||
carolFundPoint, int64(0), expectedAmountPaidCtoD)
|
|
||||||
assertAmountPaid(t, "Carol(local) => Dave(remote)", carol,
|
|
||||||
carolFundPoint, expectedAmountPaidCtoD, int64(0))
|
|
||||||
|
|
||||||
// Now that we know all the balances have been settled out properly,
|
|
||||||
// we'll ensure that our internal record keeping for completed circuits
|
|
||||||
// was properly updated.
|
|
||||||
|
|
||||||
// First, check that the FeeReport response shows the proper fees
|
|
||||||
// accrued over each time range. Dave should've earned 170 satoshi for
|
|
||||||
// each of the forwarded payments.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
feeReport, err := dave.FeeReport(ctxt, &lnrpc.FeeReportRequest{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for fee report: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if feeReport.DayFeeSum != uint64(expectedFeeDave) {
|
|
||||||
t.Fatalf("fee mismatch: expected %v, got %v", expectedFeeDave,
|
|
||||||
feeReport.DayFeeSum)
|
|
||||||
}
|
|
||||||
if feeReport.WeekFeeSum != uint64(expectedFeeDave) {
|
|
||||||
t.Fatalf("fee mismatch: expected %v, got %v", expectedFeeDave,
|
|
||||||
feeReport.WeekFeeSum)
|
|
||||||
}
|
|
||||||
if feeReport.MonthFeeSum != uint64(expectedFeeDave) {
|
|
||||||
t.Fatalf("fee mismatch: expected %v, got %v", expectedFeeDave,
|
|
||||||
feeReport.MonthFeeSum)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, ensure that if we issue the vanilla query for the forwarding
|
|
||||||
// history, it returns 5 values, and each entry is formatted properly.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
fwdingHistory, err := dave.ForwardingHistory(
|
|
||||||
ctxt, &lnrpc.ForwardingHistoryRequest{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for fee report: %v", err)
|
|
||||||
}
|
|
||||||
if len(fwdingHistory.ForwardingEvents) != 5 {
|
|
||||||
t.Fatalf("wrong number of forwarding event: expected %v, "+
|
|
||||||
"got %v", 5, len(fwdingHistory.ForwardingEvents))
|
|
||||||
}
|
|
||||||
expectedForwardingFee := uint64(expectedFeeDave / numPayments)
|
|
||||||
for _, event := range fwdingHistory.ForwardingEvents {
|
|
||||||
// Each event should show a fee of 170 satoshi.
|
|
||||||
if event.Fee != expectedForwardingFee {
|
|
||||||
t.Fatalf("fee mismatch: expected %v, got %v",
|
|
||||||
expectedForwardingFee, event.Fee)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, dave, chanPointDave, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
type singleHopSendToRouteCase struct {
|
type singleHopSendToRouteCase struct {
|
||||||
name string
|
name string
|
||||||
|
|
||||||
@ -9381,332 +9106,6 @@ func assertNodeNumChannels(t *harnessTest, node *lntest.HarnessNode,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testHtlcErrorPropagation(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
// In this test we wish to exercise the daemon's correct parsing,
|
|
||||||
// handling, and propagation of errors that occur while processing a
|
|
||||||
// multi-hop payment.
|
|
||||||
const chanAmt = lnd.MaxBtcFundingAmount
|
|
||||||
|
|
||||||
// First establish a channel with a capacity of 0.5 BTC between Alice
|
|
||||||
// and Bob.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAlice := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPointAlice); err != nil {
|
|
||||||
t.Fatalf("channel not seen by alice before timeout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cType, err := channelCommitType(net.Alice, chanPointAlice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get channel type: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
commitFee := cType.calcStaticFee(0)
|
|
||||||
assertBaseBalance := func() {
|
|
||||||
balReq := &lnrpc.ChannelBalanceRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
aliceBal, err := net.Alice.ChannelBalance(ctxt, balReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get channel balance: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
bobBal, err := net.Bob.ChannelBalance(ctxt, balReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get channel balance: %v", err)
|
|
||||||
}
|
|
||||||
if aliceBal.Balance != int64(chanAmt-commitFee) {
|
|
||||||
t.Fatalf("alice has an incorrect balance: expected %v got %v",
|
|
||||||
int64(chanAmt-commitFee), aliceBal)
|
|
||||||
}
|
|
||||||
if bobBal.Balance != int64(chanAmt-commitFee) {
|
|
||||||
t.Fatalf("bob has an incorrect balance: expected %v got %v",
|
|
||||||
int64(chanAmt-commitFee), bobBal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we'd like to test some multi-hop failure scenarios, we'll
|
|
||||||
// introduce another node into our test network: Carol.
|
|
||||||
carol, err := net.NewNode("Carol", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create new nodes: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we'll create a connection from Bob to Carol, and open a
|
|
||||||
// channel between them so we have the topology: Alice -> Bob -> Carol.
|
|
||||||
// The channel created will be of lower capacity that the one created
|
|
||||||
// above.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, net.Bob, carol); err != nil {
|
|
||||||
t.Fatalf("unable to connect bob to carol: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
const bobChanAmt = lnd.MaxBtcFundingAmount
|
|
||||||
chanPointBob := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Bob, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure that Alice has Carol in her routing table before proceeding.
|
|
||||||
nodeInfoReq := &lnrpc.NodeInfoRequest{
|
|
||||||
PubKey: carol.PubKeyStr,
|
|
||||||
}
|
|
||||||
checkTableTimeout := time.After(time.Second * 10)
|
|
||||||
checkTableTicker := time.NewTicker(100 * time.Millisecond)
|
|
||||||
defer checkTableTicker.Stop()
|
|
||||||
|
|
||||||
out:
|
|
||||||
// TODO(roasbeef): make into async hook for node announcements
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-checkTableTicker.C:
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
_, err := net.Alice.GetNodeInfo(ctxt, nodeInfoReq)
|
|
||||||
if err != nil && strings.Contains(err.Error(),
|
|
||||||
"unable to find") {
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
break out
|
|
||||||
case <-checkTableTimeout:
|
|
||||||
t.Fatalf("carol's node announcement didn't propagate within " +
|
|
||||||
"the timeout period")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the channels, open we can now start to test our multi-hop error
|
|
||||||
// scenarios. First, we'll generate an invoice from carol that we'll
|
|
||||||
// use to test some error cases.
|
|
||||||
const payAmt = 10000
|
|
||||||
invoiceReq := &lnrpc.Invoice{
|
|
||||||
Memo: "kek99",
|
|
||||||
Value: payAmt,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
carolInvoice, err := carol.AddInvoice(ctxt, invoiceReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate carol invoice: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
carolPayReq, err := carol.DecodePayReq(ctxb,
|
|
||||||
&lnrpc.PayReqString{
|
|
||||||
PayReq: carolInvoice.PaymentRequest,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to decode generated payment request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Before we send the payment, ensure that the announcement of the new
|
|
||||||
// channel has been processed by Alice.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPointBob); err != nil {
|
|
||||||
t.Fatalf("channel not seen by alice before timeout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the first scenario, we'll test the cancellation of an HTLC with
|
|
||||||
// an unknown payment hash.
|
|
||||||
// TODO(roasbeef): return failure response rather than failing entire
|
|
||||||
// stream on payment error.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
sendReq := &lnrpc.SendRequest{
|
|
||||||
PaymentHashString: hex.EncodeToString(makeFakePayHash(t)),
|
|
||||||
DestString: hex.EncodeToString(carol.PubKey[:]),
|
|
||||||
Amt: payAmt,
|
|
||||||
FinalCltvDelta: int32(carolPayReq.CltvExpiry),
|
|
||||||
}
|
|
||||||
resp, err := net.Alice.SendPaymentSync(ctxt, sendReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The payment should have resulted in an error since we sent it with the
|
|
||||||
// wrong payment hash.
|
|
||||||
if resp.PaymentError == "" {
|
|
||||||
t.Fatalf("payment should have been rejected due to invalid " +
|
|
||||||
"payment hash")
|
|
||||||
}
|
|
||||||
|
|
||||||
assertLastHTLCError(
|
|
||||||
t, net.Alice,
|
|
||||||
lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
|
|
||||||
)
|
|
||||||
|
|
||||||
// The balances of all parties should be the same as initially since
|
|
||||||
// the HTLC was canceled.
|
|
||||||
assertBaseBalance()
|
|
||||||
|
|
||||||
// Next, we'll test the case of a recognized payHash but, an incorrect
|
|
||||||
// value on the extended HTLC.
|
|
||||||
htlcAmt := lnwire.NewMSatFromSatoshis(1000)
|
|
||||||
sendReq = &lnrpc.SendRequest{
|
|
||||||
PaymentHashString: hex.EncodeToString(carolInvoice.RHash),
|
|
||||||
DestString: hex.EncodeToString(carol.PubKey[:]),
|
|
||||||
Amt: int64(htlcAmt.ToSatoshis()), // 10k satoshis are expected.
|
|
||||||
FinalCltvDelta: int32(carolPayReq.CltvExpiry),
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err = net.Alice.SendPaymentSync(ctxt, sendReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The payment should fail with an error since we sent 1k satoshis isn't of
|
|
||||||
// 10k as was requested.
|
|
||||||
if resp.PaymentError == "" {
|
|
||||||
t.Fatalf("payment should have been rejected due to wrong " +
|
|
||||||
"HTLC amount")
|
|
||||||
}
|
|
||||||
|
|
||||||
assertLastHTLCError(
|
|
||||||
t, net.Alice,
|
|
||||||
lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
|
|
||||||
)
|
|
||||||
|
|
||||||
// The balances of all parties should be the same as initially since
|
|
||||||
// the HTLC was canceled.
|
|
||||||
assertBaseBalance()
|
|
||||||
|
|
||||||
// Next we'll test an error that occurs mid-route due to an outgoing
|
|
||||||
// link having insufficient capacity. In order to do so, we'll first
|
|
||||||
// need to unbalance the link connecting Bob<->Carol.
|
|
||||||
ctx, cancel := context.WithCancel(ctxb)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
bobPayStream, err := net.Bob.SendPayment(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create payment stream: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// To do so, we'll push most of the funds in the channel over to
|
|
||||||
// Alice's side, leaving on 10k satoshis of available balance for bob.
|
|
||||||
// There's a max payment amount, so we'll have to do this
|
|
||||||
// incrementally.
|
|
||||||
chanReserve := int64(chanAmt / 100)
|
|
||||||
amtToSend := int64(chanAmt) - chanReserve - 20000
|
|
||||||
amtSent := int64(0)
|
|
||||||
for amtSent != amtToSend {
|
|
||||||
// We'll send in chunks of the max payment amount. If we're
|
|
||||||
// about to send too much, then we'll only send the amount
|
|
||||||
// remaining.
|
|
||||||
toSend := int64(lnd.MaxPaymentMSat.ToSatoshis())
|
|
||||||
if toSend+amtSent > amtToSend {
|
|
||||||
toSend = amtToSend - amtSent
|
|
||||||
}
|
|
||||||
|
|
||||||
invoiceReq = &lnrpc.Invoice{
|
|
||||||
Value: toSend,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
carolInvoice2, err := carol.AddInvoice(ctxt, invoiceReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate carol invoice: %v", err)
|
|
||||||
}
|
|
||||||
if err := bobPayStream.Send(&lnrpc.SendRequest{
|
|
||||||
PaymentRequest: carolInvoice2.PaymentRequest,
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp, err := bobPayStream.Recv(); err != nil {
|
|
||||||
t.Fatalf("payment stream has been closed: %v", err)
|
|
||||||
} else if resp.PaymentError != "" {
|
|
||||||
t.Fatalf("bob's payment failed: %v", resp.PaymentError)
|
|
||||||
}
|
|
||||||
|
|
||||||
amtSent += toSend
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, Alice has 50mil satoshis on her side of the channel,
|
|
||||||
// but Bob only has 10k available on his side of the channel. So a
|
|
||||||
// payment from Alice to Carol worth 100k satoshis should fail.
|
|
||||||
invoiceReq = &lnrpc.Invoice{
|
|
||||||
Value: 100000,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
carolInvoice3, err := carol.AddInvoice(ctxt, invoiceReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate carol invoice: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendReq = &lnrpc.SendRequest{
|
|
||||||
PaymentRequest: carolInvoice3.PaymentRequest,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err = net.Alice.SendPaymentSync(ctxt, sendReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
if resp.PaymentError == "" {
|
|
||||||
t.Fatalf("payment should fail due to insufficient "+
|
|
||||||
"capacity: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertLastHTLCError(
|
|
||||||
t, net.Alice, lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generate new invoice to not pay same invoice twice.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
carolInvoice, err = carol.AddInvoice(ctxt, invoiceReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate carol invoice: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For our final test, we'll ensure that if a target link isn't
|
|
||||||
// available for what ever reason then the payment fails accordingly.
|
|
||||||
//
|
|
||||||
// We'll attempt to complete the original invoice we created with Carol
|
|
||||||
// above, but before we do so, Carol will go offline, resulting in a
|
|
||||||
// failed payment.
|
|
||||||
shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
// Reset mission control to forget the temporary channel failure above.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
_, err = net.Alice.RouterClient.ResetMissionControl(
|
|
||||||
ctxt, &routerrpc.ResetMissionControlRequest{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to reset mission control: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendReq = &lnrpc.SendRequest{
|
|
||||||
PaymentRequest: carolInvoice.PaymentRequest,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err = net.Alice.SendPaymentSync(ctxt, sendReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.PaymentError == "" {
|
|
||||||
t.Fatalf("payment should have failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
assertLastHTLCError(t, net.Alice, lnrpc.Failure_UNKNOWN_NEXT_PEER)
|
|
||||||
|
|
||||||
// Finally, immediately close the channel. This function will also
|
|
||||||
// block until the channel is closed and will additionally assert the
|
|
||||||
// relevant channel closing post conditions.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
|
||||||
|
|
||||||
// Force close Bob's final channel.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBob, true)
|
|
||||||
|
|
||||||
// Cleanup by mining the force close and sweep transaction.
|
|
||||||
cleanupForceClose(t, net, net.Bob, chanPointBob)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testRejectHTLC tests that a node can be created with the flag --rejecthtlc.
|
// testRejectHTLC tests that a node can be created with the flag --rejecthtlc.
|
||||||
// This means that the node will reject all forwarded HTLCs but can still
|
// This means that the node will reject all forwarded HTLCs but can still
|
||||||
// accept direct HTLCs as well as send HTLCs.
|
// accept direct HTLCs as well as send HTLCs.
|
||||||
|
Loading…
Reference in New Issue
Block a user