From e229717025f31e19df3f9daacba76126d7bfbe37 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 4 May 2020 09:33:25 +0200 Subject: [PATCH 1/5] lnrpc: add failure detail none to link event rpc --- lnrpc/routerrpc/subscribe_events.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lnrpc/routerrpc/subscribe_events.go b/lnrpc/routerrpc/subscribe_events.go index 4454dbda..d44b54a7 100644 --- a/lnrpc/routerrpc/subscribe_events.go +++ b/lnrpc/routerrpc/subscribe_events.go @@ -120,15 +120,21 @@ func rpcFailReason(linkErr *htlcswitch.LinkError) (lnrpc.Failure_FailureCode, if err != nil { 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) { case invoices.FailResolutionResult: fd, err := rpcFailureResolution(failureDetail) - return wireErr.GetCode(), fd, err + return wireCode, fd, err case htlcswitch.OutgoingFailure: fd, err := rpcOutgoingFailure(failureDetail) - return wireErr.GetCode(), fd, err + return wireCode, fd, err default: return 0, 0, fmt.Errorf("unknown failure "+ From 578f6be0a1d3c1e012eb0bf99baa4f63995d6586 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 4 May 2020 09:47:01 +0200 Subject: [PATCH 2/5] lntest/test: move multi-hop payments test into own file --- lntest/itest/lnd_multi-hop-payments.go | 289 +++++++++++++++++++++++++ lntest/itest/lnd_test.go | 275 ----------------------- 2 files changed, 289 insertions(+), 275 deletions(-) create mode 100644 lntest/itest/lnd_multi-hop-payments.go diff --git a/lntest/itest/lnd_multi-hop-payments.go b/lntest/itest/lnd_multi-hop-payments.go new file mode 100644 index 00000000..fde4c8c9 --- /dev/null +++ b/lntest/itest/lnd_multi-hop-payments.go @@ -0,0 +1,289 @@ +// +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/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, + ) + + // 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) +} diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index b5e1eade..9d56f108 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -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 { name string From b608683c9bae3201d55a327b360c7462853a3fad Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 4 May 2020 09:52:15 +0200 Subject: [PATCH 3/5] lntest/test: test htlcnotifier stream for successful multi-hop payments --- lntest/itest/lnd_multi-hop-payments.go | 118 +++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/lntest/itest/lnd_multi-hop-payments.go b/lntest/itest/lnd_multi-hop-payments.go index fde4c8c9..39a48a08 100644 --- a/lntest/itest/lnd_multi-hop-payments.go +++ b/lntest/itest/lnd_multi-hop-payments.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" ) @@ -180,6 +181,39 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { 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) @@ -280,6 +314,29 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { } } + // 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) @@ -287,3 +344,64 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { 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 +} From 5a172330d322ce504552156e26affc7838dc3d53 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 4 May 2020 09:55:35 +0200 Subject: [PATCH 4/5] lntest/test: move multi-hop error propagation into its own file --- .../itest/lnd_multi-hop-error-propagation.go | 342 ++++++++++++++++++ lntest/itest/lnd_test.go | 326 ----------------- 2 files changed, 342 insertions(+), 326 deletions(-) create mode 100644 lntest/itest/lnd_multi-hop-error-propagation.go diff --git a/lntest/itest/lnd_multi-hop-error-propagation.go b/lntest/itest/lnd_multi-hop-error-propagation.go new file mode 100644 index 00000000..9e567b1e --- /dev/null +++ b/lntest/itest/lnd_multi-hop-error-propagation.go @@ -0,0 +1,342 @@ +// +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) + } + + // 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) +} diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 9d56f108..d6f93474 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -9106,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. // This means that the node will reject all forwarded HTLCs but can still // accept direct HTLCs as well as send HTLCs. From a93b55ddbf1d2493ca6f7fcf8e04a472cebd61a8 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 4 May 2020 10:12:55 +0200 Subject: [PATCH 5/5] lntest/test: add htlc events test to multi-hop error propagation --- .../itest/lnd_multi-hop-error-propagation.go | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/lntest/itest/lnd_multi-hop-error-propagation.go b/lntest/itest/lnd_multi-hop-error-propagation.go index 9e567b1e..d1689a99 100644 --- a/lntest/itest/lnd_multi-hop-error-propagation.go +++ b/lntest/itest/lnd_multi-hop-error-propagation.go @@ -146,6 +146,32 @@ out: 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 @@ -174,6 +200,18 @@ out: 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() @@ -205,6 +243,18 @@ out: 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() @@ -256,6 +306,16 @@ out: 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 } @@ -288,6 +348,16 @@ out: 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) @@ -327,6 +397,16 @@ out: 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. @@ -340,3 +420,23 @@ out: // 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) + } +}