package itest import ( "encoding/hex" "fmt" "testing" "time" "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) type singleHopSendToRouteCase struct { name string // streaming tests streaming SendToRoute if true, otherwise tests // synchronous SenToRoute. streaming bool // routerrpc submits the request to the routerrpc subserver if true, // otherwise submits to the main rpc server. routerrpc bool } var singleHopSendToRouteCases = []singleHopSendToRouteCase{ { name: "regular main sync", }, { name: "regular main stream", streaming: true, }, { name: "regular routerrpc sync", routerrpc: true, }, { name: "mpp main sync", }, { name: "mpp main stream", streaming: true, }, { name: "mpp routerrpc sync", routerrpc: true, }, } // testSingleHopSendToRoute tests that payments are properly processed through a // provided route with a single hop. We'll create the following network // topology: // // Carol --100k--> Dave // // We'll query the daemon for routes from Carol to Dave and then send payments // by feeding the route back into the various SendToRoute RPC methods. Here we // test all three SendToRoute endpoints, forcing each to perform both a regular // payment and an MPP payment. func testSingleHopSendToRoute(ht *lntest.HarnessTest) { for _, test := range singleHopSendToRouteCases { test := test ht.Run(test.name, func(t1 *testing.T) { st := ht.Subtest(t1) testSingleHopSendToRouteCase(st, test) }) } } func testSingleHopSendToRouteCase(ht *lntest.HarnessTest, test singleHopSendToRouteCase) { const chanAmt = btcutil.Amount(100000) const paymentAmtSat = 1000 const numPayments = 5 const amountPaid = int64(numPayments * paymentAmtSat) // Create Carol and Dave, then establish a channel between them. Carol // is the sole funder of the channel with 100k satoshis. The network // topology should look like: // Carol -> 100k -> Dave carol := ht.NewNode("Carol", nil) dave := ht.NewNode("Dave", nil) ht.ConnectNodes(carol, dave) ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) // Open a channel with 100k satoshis between Carol and Dave with Carol // being the sole funder of the channel. chanPointCarol := ht.OpenChannel( carol, dave, lntest.OpenChannelParams{Amt: chanAmt}, ) defer ht.CloseChannel(carol, chanPointCarol) // Create invoices for Dave, which expect a payment from Carol. payReqs, rHashes, _ := ht.CreatePayReqs( dave, paymentAmtSat, numPayments, ) // Reconstruct payment addresses. var payAddrs [][]byte for _, payReq := range payReqs { resp := dave.RPC.DecodePayReq(payReq) payAddrs = append(payAddrs, resp.PaymentAddr) } // Assert Carol and Dave are synced to the chain before proceeding, to // ensure the queried route will have a valid final CLTV once the HTLC // reaches Dave. _, minerHeight := ht.Miner.GetBestBlock() ht.WaitForNodeBlockHeight(carol, minerHeight) ht.WaitForNodeBlockHeight(dave, minerHeight) // Query for routes to pay from Carol to Dave using the default CLTV // config. routesReq := &lnrpc.QueryRoutesRequest{ PubKey: dave.PubKeyStr, Amt: paymentAmtSat, } routes := carol.RPC.QueryRoutes(routesReq) // There should only be one route to try, so take the first item. r := routes.Routes[0] // Construct a closure that will set MPP fields on the route, which // allows us to test MPP payments. setMPPFields := func(i int) { hop := r.Hops[len(r.Hops)-1] hop.TlvPayload = true hop.MppRecord = &lnrpc.MPPRecord{ PaymentAddr: payAddrs[i], TotalAmtMsat: paymentAmtSat * 1000, } } // Construct closures for each of the payment types covered: // - main rpc server sync // - main rpc server streaming // - routerrpc server sync sendToRouteSync := func() { for i, rHash := range rHashes { setMPPFields(i) sendReq := &lnrpc.SendToRouteRequest{ PaymentHash: rHash, Route: r, } resp := carol.RPC.SendToRouteSync(sendReq) require.Emptyf(ht, resp.PaymentError, "received payment error from %s: %v", carol.Name(), resp.PaymentError) } } sendToRouteStream := func() { alicePayStream := carol.RPC.SendToRoute() for i, rHash := range rHashes { setMPPFields(i) sendReq := &lnrpc.SendToRouteRequest{ PaymentHash: rHash, Route: routes.Routes[0], } err := alicePayStream.Send(sendReq) require.NoError(ht, err, "unable to send payment") resp, err := ht.ReceiveSendToRouteUpdate(alicePayStream) require.NoError(ht, err, "unable to receive stream") require.Emptyf(ht, resp.PaymentError, "received payment error from %s: %v", carol.Name(), resp.PaymentError) } } sendToRouteRouterRPC := func() { for i, rHash := range rHashes { setMPPFields(i) sendReq := &routerrpc.SendToRouteRequest{ PaymentHash: rHash, Route: r, } resp := carol.RPC.SendToRouteV2(sendReq) require.Nilf(ht, resp.Failure, "received payment "+ "error from %s", carol.Name()) } } // Using Carol as the node as the source, send the payments // synchronously via the routerrpc's SendToRoute, or via the main RPC // server's SendToRoute streaming or sync calls. switch { case !test.routerrpc && test.streaming: sendToRouteStream() case !test.routerrpc && !test.streaming: sendToRouteSync() case test.routerrpc && !test.streaming: sendToRouteRouterRPC() default: require.Fail(ht, "routerrpc does not support "+ "streaming send_to_route") } // Verify that the payment's from Carol's PoV have the correct payment // hash and amount. payments := ht.AssertNumPayments(carol, numPayments) for i, p := range payments { // Assert that the payment hashes for each payment match up. rHashHex := hex.EncodeToString(rHashes[i]) require.Equalf(ht, rHashHex, p.PaymentHash, "incorrect payment hash for payment %d", i) // Assert that each payment has no invoice since the payment // was completed using SendToRoute. require.Emptyf(ht, p.PaymentRequest, "incorrect payment request for payment: %d", i) // Assert the payment amount is correct. require.EqualValues(ht, paymentAmtSat, p.ValueSat, "incorrect payment amt for payment %d, ", i) // Assert exactly one htlc was made. require.Lenf(ht, p.Htlcs, 1, "expected 1 htlc for payment %d", i) // Assert the htlc's route is populated. htlc := p.Htlcs[0] require.NotNilf(ht, htlc.Route, "expected route for payment %d", i) // Assert the hop has exactly one hop. require.Lenf(ht, htlc.Route.Hops, 1, "expected 1 hop for payment %d", i) // If this is an MPP test, assert the MPP record's fields are // properly populated. Otherwise the hop should not have an MPP // record. hop := htlc.Route.Hops[0] require.NotNilf(ht, hop.MppRecord, "expected mpp record for mpp payment %d", i) require.EqualValues(ht, paymentAmtSat*1000, hop.MppRecord.TotalAmtMsat, "incorrect mpp total msat for payment %d", i) expAddr := payAddrs[i] require.Equal(ht, expAddr, hop.MppRecord.PaymentAddr, "incorrect mpp payment addr for payment %d ", i) } // Verify that the invoices's from Dave's PoV have the correct payment // hash and amount. invoices := ht.AssertNumInvoices(dave, numPayments) for i, inv := range invoices { // Assert that the payment hashes match up. require.Equal(ht, rHashes[i], inv.RHash, "incorrect payment hash for invoice %d", i) // Assert that the amount paid to the invoice is correct. require.EqualValues(ht, paymentAmtSat, inv.AmtPaidSat, "incorrect payment amt for invoice %d, ", i) } // At this point all the channels within our proto network should be // shifted by 5k satoshis in the direction of Dave, 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->Dave, order is Dave and then Carol. ht.AssertAmountPaid("Carol(local) => Dave(remote)", dave, chanPointCarol, int64(0), amountPaid) ht.AssertAmountPaid("Carol(local) => Dave(remote)", carol, chanPointCarol, amountPaid, int64(0)) } // testMultiHopSendToRoute tests that payments are properly processed // through a provided route. We'll create the following network topology: // // Alice --100k--> Bob --100k--> Carol // // We'll query the daemon for routes from Alice to Carol and then // send payments through the routes. func testMultiHopSendToRoute(ht *lntest.HarnessTest) { ht.Run("with cache", func(tt *testing.T) { st := ht.Subtest(tt) runMultiHopSendToRoute(st, true) }) if *dbBackendFlag == "bbolt" { ht.Run("without cache", func(tt *testing.T) { st := ht.Subtest(tt) runMultiHopSendToRoute(st, false) }) } } // runMultiHopSendToRoute tests that payments are properly processed // through a provided route. We'll create the following network topology: // // Alice --100k--> Bob --100k--> Carol // // We'll query the daemon for routes from Alice to Carol and then // send payments through the routes. func runMultiHopSendToRoute(ht *lntest.HarnessTest, useGraphCache bool) { var opts []string if !useGraphCache { opts = append(opts, "--db.no-graph-cache") } alice, bob := ht.Alice, ht.Bob ht.RestartNodeWithExtraArgs(alice, opts) ht.EnsureConnected(alice, bob) const chanAmt = btcutil.Amount(100000) // Open a channel with 100k satoshis between Alice and Bob with Alice // being the sole funder of the channel. chanPointAlice := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{Amt: chanAmt}, ) defer ht.CloseChannel(alice, chanPointAlice) // Create Carol and establish a channel from Bob. Bob is the sole // funder of the channel with 100k satoshis. The network topology // should look like: // Alice -> Bob -> Carol carol := ht.NewNode("Carol", nil) ht.ConnectNodes(carol, bob) chanPointBob := ht.OpenChannel( bob, carol, lntest.OpenChannelParams{Amt: chanAmt}, ) defer ht.CloseChannel(carol, chanPointBob) // Make sure Alice knows the channel between Bob and Carol. ht.AssertTopologyChannelOpen(alice, chanPointBob) // Create 5 invoices for Carol, which expect a payment from Alice for // 1k satoshis with a different preimage each time. const ( numPayments = 5 paymentAmt = 1000 ) _, rHashes, invoices := ht.CreatePayReqs(carol, paymentAmt, numPayments) // Construct a route from Alice to Carol for each of the invoices // created above. We set FinalCltvDelta to 40 since by default // QueryRoutes returns the last hop with a final cltv delta of 9 where // as the default in htlcswitch is 40. routesReq := &lnrpc.QueryRoutesRequest{ PubKey: carol.PubKeyStr, Amt: paymentAmt, FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta, } routes := alice.RPC.QueryRoutes(routesReq) // Using Alice as the source, pay to the 5 invoices from Carol created // above. for i, rHash := range rHashes { // Manually set the MPP payload a new for each payment since // the payment addr will change with each invoice, although we // can re-use the route itself. route := routes.Routes[0] route.Hops[len(route.Hops)-1].TlvPayload = true route.Hops[len(route.Hops)-1].MppRecord = &lnrpc.MPPRecord{ PaymentAddr: invoices[i].PaymentAddr, TotalAmtMsat: int64( lnwire.NewMSatFromSatoshis(paymentAmt), ), } sendReq := &routerrpc.SendToRouteRequest{ PaymentHash: rHash, Route: route, } resp := alice.RPC.SendToRouteV2(sendReq) require.Nil(ht, resp.Failure, "received payment error") } // 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 Carol, 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 Alice->Bob->Carol, order is Carol, Bob, // Alice. const amountPaid = int64(5000) ht.AssertAmountPaid("Bob(local) => Carol(remote)", carol, chanPointBob, int64(0), amountPaid) ht.AssertAmountPaid("Bob(local) => Carol(remote)", bob, chanPointBob, amountPaid, int64(0)) ht.AssertAmountPaid("Alice(local) => Bob(remote)", bob, chanPointAlice, int64(0), amountPaid+(baseFee*numPayments)) ht.AssertAmountPaid("Alice(local) => Bob(remote)", alice, chanPointAlice, amountPaid+(baseFee*numPayments), int64(0)) } // testSendToRouteErrorPropagation tests propagation of errors that occur // while processing a multi-hop payment through an unknown route. func testSendToRouteErrorPropagation(ht *lntest.HarnessTest) { const chanAmt = btcutil.Amount(100000) // Open a channel with 100k satoshis between Alice and Bob with Alice // being the sole funder of the channel. alice, bob := ht.Alice, ht.Bob chanPointAlice := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{Amt: chanAmt}, ) // Create a new nodes (Carol and Charlie), load her with some funds, // then establish a connection between Carol and Charlie with a channel // that has identical capacity to the one created above.Then we will // get route via queryroutes call which will be fake route for Alice -> // Bob graph. // // The network topology should now look like: // Alice -> Bob; Carol -> Charlie. carol := ht.NewNode("Carol", nil) ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) charlie := ht.NewNode("Charlie", nil) ht.FundCoins(btcutil.SatoshiPerBitcoin, charlie) ht.ConnectNodes(carol, charlie) ht.OpenChannel(carol, charlie, lntest.OpenChannelParams{Amt: chanAmt}) // Query routes from Carol to Charlie which will be an invalid route // for Alice -> Bob. fakeReq := &lnrpc.QueryRoutesRequest{ PubKey: charlie.PubKeyStr, Amt: int64(1), } fakeRoute := carol.RPC.QueryRoutes(fakeReq) // Create 1 invoice for Bob, which expect a payment from Alice for 1k // satoshis. const paymentAmt = 1000 invoice := &lnrpc.Invoice{ Memo: "testing", Value: paymentAmt, } resp := bob.RPC.AddInvoice(invoice) rHash := resp.RHash // Using Alice as the source, pay to the invoice from Bob. alicePayStream := alice.RPC.SendToRoute() sendReq := &lnrpc.SendToRouteRequest{ PaymentHash: rHash, Route: fakeRoute.Routes[0], } err := alicePayStream.Send(sendReq) require.NoError(ht, err, "unable to send payment") // At this place we should get an rpc error with notification // that edge is not found on hop(0) event, err := ht.ReceiveSendToRouteUpdate(alicePayStream) require.NoError(ht, err, "payment stream has been closed but fake "+ "route has consumed") require.Contains(ht, event.PaymentError, "UnknownNextPeer") ht.CloseChannel(alice, chanPointAlice) } // testPrivateChannels tests that a private channel can be used for // routing by the two endpoints of the channel, but is not known by // the rest of the nodes in the graph. func testPrivateChannels(ht *lntest.HarnessTest) { const chanAmt = btcutil.Amount(100000) // We create the following topology: // // Dave --100k--> Alice --200k--> Bob // ^ ^ // | | // 100k 100k // | | // +---- Carol ----+ // // where the 100k channel between Carol and Alice is private. // Open a channel with 200k satoshis between Alice and Bob. alice, bob := ht.Alice, ht.Bob chanPointAlice := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{Amt: chanAmt * 2}, ) // Create Dave, and a channel to Alice of 100k. dave := ht.NewNode("Dave", nil) ht.ConnectNodes(dave, alice) ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) chanPointDave := ht.OpenChannel( dave, alice, lntest.OpenChannelParams{Amt: chanAmt}, ) // Next, we'll create Carol and establish a channel from her to // Dave of 100k. carol := ht.NewNode("Carol", nil) ht.ConnectNodes(carol, dave) ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) chanPointCarol := ht.OpenChannel( carol, dave, lntest.OpenChannelParams{Amt: chanAmt}, ) // Now create a _private_ channel directly between Carol and // Alice of 100k. ht.ConnectNodes(carol, alice) chanPointPrivate := ht.OpenChannel( carol, alice, lntest.OpenChannelParams{ Amt: chanAmt, Private: true, }, ) // The channel should be available for payments between Carol and Alice. // We check this by sending payments from Carol to Bob, that // collectively would deplete at least one of Carol's channels. // Create 2 invoices for Bob, each of 70k satoshis. Since each of // Carol's channels is of size 100k, these payments cannot succeed // by only using one of the channels. const numPayments = 2 const paymentAmt = 70000 payReqs, _, _ := ht.CreatePayReqs(bob, paymentAmt, numPayments) // Let Carol pay the invoices. ht.CompletePaymentRequests(carol, payReqs) // 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 // Bob should have received 140k satoshis from Alice. ht.AssertAmountPaid("Alice(local) => Bob(remote)", bob, chanPointAlice, int64(0), 2*paymentAmt) // Alice sent 140k to Bob. ht.AssertAmountPaid("Alice(local) => Bob(remote)", alice, chanPointAlice, 2*paymentAmt, int64(0)) // Alice received 70k + fee from Dave. ht.AssertAmountPaid("Dave(local) => Alice(remote)", alice, chanPointDave, int64(0), paymentAmt+baseFee) // Dave sent 70k+fee to Alice. ht.AssertAmountPaid("Dave(local) => Alice(remote)", dave, chanPointDave, paymentAmt+baseFee, int64(0)) // Dave received 70k+fee of two hops from Carol. ht.AssertAmountPaid("Carol(local) => Dave(remote)", dave, chanPointCarol, int64(0), paymentAmt+baseFee*2) // Carol sent 70k+fee of two hops to Dave. ht.AssertAmountPaid("Carol(local) => Dave(remote)", carol, chanPointCarol, paymentAmt+baseFee*2, int64(0)) // Alice received 70k+fee from Carol. ht.AssertAmountPaid("Carol(local) [private=>] Alice(remote)", alice, chanPointPrivate, int64(0), paymentAmt+baseFee) // Carol sent 70k+fee to Alice. ht.AssertAmountPaid("Carol(local) [private=>] Alice(remote)", carol, chanPointPrivate, paymentAmt+baseFee, int64(0)) // Alice should also be able to route payments using this channel, // so send two payments of 60k back to Carol. const paymentAmt60k = 60000 payReqs, _, _ = ht.CreatePayReqs(carol, paymentAmt60k, numPayments) // Let Bob pay the invoices. ht.CompletePaymentRequests(alice, payReqs) // Carol and Alice should know about 4, while Bob and Dave should only // know about 3, since one channel is private. ht.AssertNumEdges(alice, 4, true) ht.AssertNumEdges(alice, 3, false) ht.AssertNumEdges(bob, 3, true) ht.AssertNumEdges(carol, 4, true) ht.AssertNumEdges(carol, 3, false) ht.AssertNumEdges(dave, 3, true) // Close all channels. ht.CloseChannel(alice, chanPointAlice) ht.CloseChannel(dave, chanPointDave) ht.CloseChannel(carol, chanPointCarol) ht.CloseChannel(carol, chanPointPrivate) } // testInvoiceRoutingHints tests that the routing hints for an invoice are // created properly. func testInvoiceRoutingHints(ht *lntest.HarnessTest) { const chanAmt = btcutil.Amount(100000) // Throughout this test, we'll be opening a channel between Alice and // several other parties. // // First, we'll create a private channel between Alice and Bob. This // will be the only channel that will be considered as a routing hint // throughout this test. We'll include a push amount since we currently // require channels to have enough remote balance to cover the // invoice's payment. alice, bob := ht.Alice, ht.Bob chanPointBob := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, Private: true, }, ) // Then, we'll create Carol's node and open a public channel between // her and Alice. This channel will not be considered as a routing hint // due to it being public. carol := ht.NewNode("Carol", nil) ht.ConnectNodes(alice, carol) chanPointCarol := ht.OpenChannel( alice, carol, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, }, ) // We'll also create a public channel between Bob and Carol to ensure // that Bob gets selected as the only routing hint. We do this as we // should only include routing hints for nodes that are publicly // advertised, otherwise we'd end up leaking information about nodes // that wish to stay unadvertised. ht.ConnectNodes(bob, carol) chanPointBobCarol := ht.OpenChannel( bob, carol, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, }, ) // Then, we'll create Dave's node and open a private channel between // him and Alice. We will not include a push amount in order to not // consider this channel as a routing hint as it will not have enough // remote balance for the invoice's amount. dave := ht.NewNode("Dave", nil) ht.ConnectNodes(alice, dave) chanPointDave := ht.OpenChannel( alice, dave, lntest.OpenChannelParams{ Amt: chanAmt, Private: true, }, ) // Finally, we'll create Eve's node and open a private channel between // her and Alice. This time though, we'll take Eve's node down after // the channel has been created to avoid populating routing hints for // inactive channels. eve := ht.NewNode("Eve", nil) ht.ConnectNodes(alice, eve) chanPointEve := ht.OpenChannel( alice, eve, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, Private: true, }, ) // Now that the channels are open, we'll disconnect the connection // between Alice and Eve and then take down Eve's node. ht.DisconnectNodes(alice, eve) ht.Shutdown(eve) // We'll need the short channel ID of the channel between Alice and Bob // to make sure the routing hint is for this channel. aliceBobChanID := ht.QueryChannelByChanPoint(alice, chanPointBob).ChanId checkInvoiceHints := func(invoice *lnrpc.Invoice) { // Due to the way the channels were set up above, the channel // between Alice and Bob should be the only channel used as a // routing hint. var decoded *lnrpc.PayReq err := wait.NoError(func() error { resp := alice.RPC.AddInvoice(invoice) // We'll decode the invoice's payment request to // determine which channels were used as routing hints. decoded = alice.RPC.DecodePayReq(resp.PaymentRequest) if len(decoded.RouteHints) != 1 { return fmt.Errorf("expected one route hint, "+ "got %d", len(decoded.RouteHints)) } return nil }, defaultTimeout) require.NoError(ht, err, "timeout checking invoice hints") hops := decoded.RouteHints[0].HopHints require.Len(ht, hops, 1, "expected one hop in route hint") chanID := hops[0].ChanId require.Equal(ht, aliceBobChanID, chanID, "chanID mismatch") } // Create an invoice for Alice that will populate the routing hints. invoice := &lnrpc.Invoice{ Memo: "routing hints", Value: int64(chanAmt / 4), Private: true, } checkInvoiceHints(invoice) // Create another invoice for Alice with no value and ensure it still // populates routing hints. invoice = &lnrpc.Invoice{ Memo: "routing hints with no amount", Value: 0, Private: true, } checkInvoiceHints(invoice) // Now that we've confirmed the routing hints were added correctly, we // can close all the channels and shut down all the nodes created. ht.CloseChannel(alice, chanPointBob) ht.CloseChannel(alice, chanPointCarol) ht.CloseChannel(bob, chanPointBobCarol) ht.CloseChannel(alice, chanPointDave) // The channel between Alice and Eve should be force closed since Eve // is offline. ht.ForceCloseChannel(alice, chanPointEve) } // testMultiHopOverPrivateChannels tests that private channels can be used as // intermediate hops in a route for payments. func testMultiHopOverPrivateChannels(ht *lntest.HarnessTest) { // We'll test that multi-hop payments over private channels work as // intended. To do so, we'll create the following topology: // private public private // Alice <--100k--> Bob <--100k--> Carol <--100k--> Dave const chanAmt = btcutil.Amount(100000) // First, we'll open a private channel between Alice and Bob with Alice // being the funder. alice, bob := ht.Alice, ht.Bob chanPointAlice := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{ Amt: chanAmt, Private: true, }, ) // Next, we'll create Carol's node and open a public channel between // her and Bob with Bob being the funder. carol := ht.NewNode("Carol", nil) ht.ConnectNodes(bob, carol) chanPointBob := ht.OpenChannel( bob, carol, lntest.OpenChannelParams{ Amt: chanAmt, }, ) // Alice should know the new channel from Bob. ht.AssertTopologyChannelOpen(alice, chanPointBob) // Next, we'll create Dave's node and open a private channel between // him and Carol with Carol being the funder. dave := ht.NewNode("Dave", nil) ht.ConnectNodes(carol, dave) ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) chanPointCarol := ht.OpenChannel( carol, dave, lntest.OpenChannelParams{ Amt: chanAmt, Private: true, }, ) // Dave should know the channel[Bob<->Carol] from Carol. ht.AssertTopologyChannelOpen(dave, chanPointBob) // Now that all the channels are set up according to the topology from // above, we can proceed to test payments. We'll create an invoice for // Dave of 20k satoshis and pay it with Alice. Since there is no public // route from Alice to Dave, we'll need to use the private channel // between Carol and Dave as a routing hint encoded in the invoice. const paymentAmt = 20000 // Create the invoice for Dave. invoice := &lnrpc.Invoice{ Memo: "two hopz!", Value: paymentAmt, Private: true, } resp := dave.RPC.AddInvoice(invoice) // Let Alice pay the invoice. payReqs := []string{resp.PaymentRequest} ht.CompletePaymentRequests(alice, payReqs) // When asserting the amount of satoshis moved, we'll factor in the // default base fee, as we didn't modify the fee structure when opening // the channels. const baseFee = 1 // Dave should have received 20k satoshis from Carol. ht.AssertAmountPaid("Carol(local) [private=>] Dave(remote)", dave, chanPointCarol, 0, paymentAmt) // Carol should have sent 20k satoshis to Dave. ht.AssertAmountPaid("Carol(local) [private=>] Dave(remote)", carol, chanPointCarol, paymentAmt, 0) // Carol should have received 20k satoshis + fee for one hop from Bob. ht.AssertAmountPaid("Bob(local) => Carol(remote)", carol, chanPointBob, 0, paymentAmt+baseFee) // Bob should have sent 20k satoshis + fee for one hop to Carol. ht.AssertAmountPaid("Bob(local) => Carol(remote)", bob, chanPointBob, paymentAmt+baseFee, 0) // Bob should have received 20k satoshis + fee for two hops from Alice. ht.AssertAmountPaid("Alice(local) [private=>] Bob(remote)", bob, chanPointAlice, 0, paymentAmt+baseFee*2) // Alice should have sent 20k satoshis + fee for two hops to Bob. ht.AssertAmountPaid("Alice(local) [private=>] Bob(remote)", alice, chanPointAlice, paymentAmt+baseFee*2, 0) // At this point, the payment was successful. We can now close all the // channels and shutdown the nodes created throughout this test. ht.CloseChannel(alice, chanPointAlice) ht.CloseChannel(bob, chanPointBob) ht.CloseChannel(carol, chanPointCarol) } // testQueryRoutes checks the response of queryroutes. // We'll create the following network topology: // // Alice --> Bob --> Carol --> Dave // // and query the daemon for routes from Alice to Dave. func testQueryRoutes(ht *lntest.HarnessTest) { const chanAmt = btcutil.Amount(100000) // Grab Alice and Bob from the standby nodes. alice, bob := ht.Alice, ht.Bob // Create Carol and connect her to Bob. We also send her some coins for // channel opening. carol := ht.NewNode("Carol", nil) ht.ConnectNodes(carol, bob) ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) // Create Dave and connect him to Carol. dave := ht.NewNode("Dave", nil) ht.ConnectNodes(dave, carol) // We now proceed to open channels: // Alice=>Bob, Bob=>Carol and Carol=>Dave. p := lntest.OpenChannelParams{Amt: chanAmt} reqs := []*lntest.OpenChannelRequest{ {Local: alice, Remote: bob, Param: p}, {Local: bob, Remote: carol, Param: p}, {Local: carol, Remote: dave, Param: p}, } resp := ht.OpenMultiChannelsAsync(reqs) // Extract channel points from the response. chanPointAlice := resp[0] chanPointBob := resp[1] chanPointCarol := resp[2] // Before we continue, give Alice some time to catch up with the newly // opened channels. ht.AssertTopologyChannelOpen(alice, chanPointBob) ht.AssertTopologyChannelOpen(alice, chanPointCarol) // Query for routes to pay from Alice to Dave. const paymentAmt = 1000 routesReq := &lnrpc.QueryRoutesRequest{ PubKey: dave.PubKeyStr, Amt: paymentAmt, } routesRes := ht.QueryRoutesAndRetry(alice, routesReq) const mSat = 1000 feePerHopMSat := computeFee(1000, 1, paymentAmt*mSat) for i, route := range routesRes.Routes { expectedTotalFeesMSat := lnwire.MilliSatoshi(len(route.Hops)-1) * feePerHopMSat expectedTotalAmtMSat := (paymentAmt * mSat) + expectedTotalFeesMSat if route.TotalFees != route.TotalFeesMsat/mSat { ht.Fatalf("route %v: total fees %v (msat) does not "+ "round down to %v (sat)", i, route.TotalFeesMsat, route.TotalFees) } if route.TotalFeesMsat != int64(expectedTotalFeesMSat) { ht.Fatalf("route %v: total fees in msat expected %v "+ "got %v", i, expectedTotalFeesMSat, route.TotalFeesMsat) } if route.TotalAmt != route.TotalAmtMsat/mSat { ht.Fatalf("route %v: total amt %v (msat) does not "+ "round down to %v (sat)", i, route.TotalAmtMsat, route.TotalAmt) } if route.TotalAmtMsat != int64(expectedTotalAmtMSat) { ht.Fatalf("route %v: total amt in msat expected %v "+ "got %v", i, expectedTotalAmtMSat, route.TotalAmtMsat) } // For all hops except the last, we check that fee equals // feePerHop and amount to forward deducts feePerHop on each // hop. expectedAmtToForwardMSat := expectedTotalAmtMSat for j, hop := range route.Hops[:len(route.Hops)-1] { expectedAmtToForwardMSat -= feePerHopMSat if hop.Fee != hop.FeeMsat/mSat { ht.Fatalf("route %v hop %v: fee %v (msat) "+ "does not round down to %v (sat)", i, j, hop.FeeMsat, hop.Fee) } if hop.FeeMsat != int64(feePerHopMSat) { ht.Fatalf("route %v hop %v: fee in msat "+ "expected %v got %v", i, j, feePerHopMSat, hop.FeeMsat) } if hop.AmtToForward != hop.AmtToForwardMsat/mSat { ht.Fatalf("route %v hop %v: amt to forward %v"+ "(msat) does not round down to %v(sat)", i, j, hop.AmtToForwardMsat, hop.AmtToForward) } if hop.AmtToForwardMsat != int64(expectedAmtToForwardMSat) { ht.Fatalf("route %v hop %v: amt to forward "+ "in msat expected %v got %v", i, j, expectedAmtToForwardMSat, hop.AmtToForwardMsat) } } // Last hop should have zero fee and amount to forward should // equal payment amount. hop := route.Hops[len(route.Hops)-1] if hop.Fee != 0 || hop.FeeMsat != 0 { ht.Fatalf("route %v hop %v: fee expected 0 got %v "+ "(sat) %v (msat)", i, len(route.Hops)-1, hop.Fee, hop.FeeMsat) } if hop.AmtToForward != hop.AmtToForwardMsat/mSat { ht.Fatalf("route %v hop %v: amt to forward %v (msat) "+ "does not round down to %v (sat)", i, len(route.Hops)-1, hop.AmtToForwardMsat, hop.AmtToForward) } if hop.AmtToForwardMsat != paymentAmt*mSat { ht.Fatalf("route %v hop %v: amt to forward in msat "+ "expected %v got %v", i, len(route.Hops)-1, paymentAmt*mSat, hop.AmtToForwardMsat) } } // While we're here, we test updating mission control's config values // and assert that they are correctly updated and check that our mission // control import function updates appropriately. testMissionControlCfg(ht.T, alice) testMissionControlImport(ht, alice, bob.PubKey[:], carol.PubKey[:]) // We clean up the test case by closing channels that were created for // the duration of the tests. ht.CloseChannel(alice, chanPointAlice) ht.CloseChannel(bob, chanPointBob) ht.CloseChannel(carol, chanPointCarol) } // testMissionControlCfg tests getting and setting of a node's mission control // config, resetting to the original values after testing so that no other // tests are affected. func testMissionControlCfg(t *testing.T, hn *node.HarnessNode) { t.Helper() // Getting and setting does not alter the configuration. startCfg := hn.RPC.GetMissionControlConfig().Config hn.RPC.SetMissionControlConfig(startCfg) resp := hn.RPC.GetMissionControlConfig() require.True(t, proto.Equal(startCfg, resp.Config)) // We test that setting and getting leads to the same config if all // fields are set. cfg := &routerrpc.MissionControlConfig{ MaximumPaymentResults: 30, MinimumFailureRelaxInterval: 60, Model: routerrpc. MissionControlConfig_APRIORI, EstimatorConfig: &routerrpc.MissionControlConfig_Apriori{ Apriori: &routerrpc.AprioriParameters{ HalfLifeSeconds: 8000, HopProbability: 0.8, Weight: 0.3, CapacityFraction: 0.8, }, }, } hn.RPC.SetMissionControlConfig(cfg) // The deprecated fields should be populated. cfg.HalfLifeSeconds = 8000 cfg.HopProbability = 0.8 cfg.Weight = 0.3 respCfg := hn.RPC.GetMissionControlConfig().Config require.True(t, proto.Equal(cfg, respCfg)) // Switching to another estimator is possible. cfg = &routerrpc.MissionControlConfig{ Model: routerrpc. MissionControlConfig_BIMODAL, EstimatorConfig: &routerrpc.MissionControlConfig_Bimodal{ Bimodal: &routerrpc.BimodalParameters{ ScaleMsat: 1_000, DecayTime: 500, }, }, } hn.RPC.SetMissionControlConfig(cfg) respCfg = hn.RPC.GetMissionControlConfig().Config require.NotNil(t, respCfg.GetBimodal()) // If parameters are not set in the request, they will have zero values // after. require.Zero(t, respCfg.MaximumPaymentResults) require.Zero(t, respCfg.MinimumFailureRelaxInterval) require.Zero(t, respCfg.GetBimodal().NodeWeight) // Setting deprecated values will initialize the apriori estimator. cfg = &routerrpc.MissionControlConfig{ MaximumPaymentResults: 30, MinimumFailureRelaxInterval: 60, HopProbability: 0.8, Weight: 0.3, HalfLifeSeconds: 8000, } hn.RPC.SetMissionControlConfig(cfg) respCfg = hn.RPC.GetMissionControlConfig().Config require.NotNil(t, respCfg.GetApriori()) // The default capacity fraction is set. require.Equal(t, routing.DefaultCapacityFraction, respCfg.GetApriori().CapacityFraction) // Setting the wrong config results in an error. cfg = &routerrpc.MissionControlConfig{ Model: routerrpc. MissionControlConfig_APRIORI, EstimatorConfig: &routerrpc.MissionControlConfig_Bimodal{ Bimodal: &routerrpc.BimodalParameters{ ScaleMsat: 1_000, }, }, } hn.RPC.SetMissionControlConfigAssertErr(cfg) // Undo any changes. hn.RPC.SetMissionControlConfig(startCfg) resp = hn.RPC.GetMissionControlConfig() require.True(t, proto.Equal(startCfg, resp.Config)) } // testMissionControlImport tests import of mission control results from an // external source. func testMissionControlImport(ht *lntest.HarnessTest, hn *node.HarnessNode, fromNode, toNode []byte) { // Reset mission control so that our query will return the default // probability for our first request. hn.RPC.ResetMissionControl() // Get our baseline probability for a 10 msat hop between our target // nodes. var amount int64 = 10 probReq := &routerrpc.QueryProbabilityRequest{ FromNode: fromNode, ToNode: toNode, AmtMsat: amount, } importHistory := &routerrpc.PairData{ FailTime: time.Now().Unix(), FailAmtMsat: amount, } // Assert that our history is not already equal to the value we want to // set. This should not happen because we have just cleared our state. resp1 := hn.RPC.QueryProbability(probReq) require.Zero(ht, resp1.History.FailTime) require.Zero(ht, resp1.History.FailAmtMsat) // Now, we import a single entry which tracks a failure of the amount // we want to query between our nodes. req := &routerrpc.XImportMissionControlRequest{ Pairs: []*routerrpc.PairHistory{ { NodeFrom: fromNode, NodeTo: toNode, History: importHistory, }, }, } hn.RPC.XImportMissionControl(req) resp2 := hn.RPC.QueryProbability(probReq) require.Equal(ht, importHistory.FailTime, resp2.History.FailTime) require.Equal(ht, importHistory.FailAmtMsat, resp2.History.FailAmtMsat) // Finally, check that we will fail if inconsistent sat/msat values are // set. importHistory.FailAmtSat = amount * 2 hn.RPC.XImportMissionControlAssertErr(req) } // testRouteFeeCutoff tests that we are able to prevent querying routes and // sending payments that incur a fee higher than the fee limit. func testRouteFeeCutoff(ht *lntest.HarnessTest) { // For this test, we'll create the following topology: // // --- Bob --- // / \ // Alice ---- ---- Dave // \ / // -- Carol -- // // Alice will attempt to send payments to Dave that should not incur a // fee greater than the fee limit expressed as a percentage of the // amount and as a fixed amount of satoshis. const chanAmt = btcutil.Amount(100000) // Open a channel between Alice and Bob. alice, bob := ht.Alice, ht.Bob chanPointAliceBob := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{Amt: chanAmt}, ) // Create Carol's node and open a channel between her and Alice with // Alice being the funder. carol := ht.NewNode("Carol", nil) ht.ConnectNodes(carol, alice) ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) chanPointAliceCarol := ht.OpenChannel( alice, carol, lntest.OpenChannelParams{Amt: chanAmt}, ) // Create Dave's node and open a channel between him and Bob with Bob // being the funder. dave := ht.NewNode("Dave", nil) ht.ConnectNodes(dave, bob) chanPointBobDave := ht.OpenChannel( bob, dave, lntest.OpenChannelParams{Amt: chanAmt}, ) // Open a channel between Carol and Dave. ht.ConnectNodes(carol, dave) chanPointCarolDave := ht.OpenChannel( carol, dave, lntest.OpenChannelParams{Amt: chanAmt}, ) // Now that all the channels were set up, we'll wait for all the nodes // to have seen all the channels. nodes := []*node.HarnessNode{alice, bob, carol, dave} networkChans := []*lnrpc.ChannelPoint{ chanPointAliceBob, chanPointAliceCarol, chanPointBobDave, chanPointCarolDave, } for _, chanPoint := range networkChans { for _, node := range nodes { ht.AssertTopologyChannelOpen(node, chanPoint) } } // The payments should only be successful across the route: // Alice -> Bob -> Dave // Therefore, we'll update the fee policy on Carol's side for the // channel between her and Dave to invalidate the route: // Alice -> Carol -> Dave baseFee := int64(10000) feeRate := int64(5) timeLockDelta := uint32(chainreg.DefaultBitcoinTimeLockDelta) maxHtlc := lntest.CalculateMaxHtlc(chanAmt) expectedPolicy := &lnrpc.RoutingPolicy{ FeeBaseMsat: baseFee, FeeRateMilliMsat: testFeeBase * feeRate, TimeLockDelta: timeLockDelta, MinHtlc: 1000, // default value MaxHtlcMsat: maxHtlc, } updateFeeReq := &lnrpc.PolicyUpdateRequest{ BaseFeeMsat: baseFee, FeeRate: float64(feeRate), TimeLockDelta: timeLockDelta, MaxHtlcMsat: maxHtlc, Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ ChanPoint: chanPointCarolDave, }, } carol.RPC.UpdateChannelPolicy(updateFeeReq) // Wait for Alice to receive the channel update from Carol. ht.AssertChannelPolicyUpdate( alice, carol, expectedPolicy, chanPointCarolDave, false, ) // We'll also need the channel IDs for Bob's channels in order to // confirm the route of the payments. channel := ht.QueryChannelByChanPoint(bob, chanPointAliceBob) aliceBobChanID := channel.ChanId require.NotZero(ht, aliceBobChanID, "channel between alice and bob not found") channel = ht.QueryChannelByChanPoint(bob, chanPointBobDave) bobDaveChanID := channel.ChanId require.NotZero(ht, bobDaveChanID, "channel between bob and dave not found") hopChanIDs := []uint64{aliceBobChanID, bobDaveChanID} // checkRoute is a helper closure to ensure the route contains the // correct intermediate hops. checkRoute := func(route *lnrpc.Route) { require.Len(ht, route.Hops, 2, "expected two hops") for i, hop := range route.Hops { require.Equal(ht, hopChanIDs[i], hop.ChanId, "hop chan id not match") } } // We'll be attempting to send two payments from Alice to Dave. One will // have a fee cutoff expressed as a percentage of the amount and the // other will have it expressed as a fixed amount of satoshis. const paymentAmt = 100 carolFee := computeFee(lnwire.MilliSatoshi(baseFee), 1, paymentAmt) // testFeeCutoff is a helper closure that will ensure the different // types of fee limits work as intended when querying routes and sending // payments. testFeeCutoff := func(feeLimit *lnrpc.FeeLimit) { queryRoutesReq := &lnrpc.QueryRoutesRequest{ PubKey: dave.PubKeyStr, Amt: paymentAmt, FeeLimit: feeLimit, } routesResp := alice.RPC.QueryRoutes(queryRoutesReq) checkRoute(routesResp.Routes[0]) invoice := &lnrpc.Invoice{Value: paymentAmt} invoiceResp := dave.RPC.AddInvoice(invoice) sendReq := &routerrpc.SendPaymentRequest{ PaymentRequest: invoiceResp.PaymentRequest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } switch limit := feeLimit.Limit.(type) { case *lnrpc.FeeLimit_Fixed: sendReq.FeeLimitMsat = 1000 * limit.Fixed case *lnrpc.FeeLimit_Percent: sendReq.FeeLimitMsat = 1000 * paymentAmt * limit.Percent / 100 } result := ht.SendPaymentAssertSettled(alice, sendReq) checkRoute(result.Htlcs[0].Route) } // We'll start off using percentages first. Since the fee along the // route using Carol as an intermediate hop is 10% of the payment's // amount, we'll use a lower percentage in order to invalid that route. feeLimitPercent := &lnrpc.FeeLimit{ Limit: &lnrpc.FeeLimit_Percent{ Percent: baseFee/1000 - 1, }, } testFeeCutoff(feeLimitPercent) // Now we'll test using fixed fee limit amounts. Since we computed the // fee for the route using Carol as an intermediate hop earlier, we can // use a smaller value in order to invalidate that route. feeLimitFixed := &lnrpc.FeeLimit{ Limit: &lnrpc.FeeLimit_Fixed{ Fixed: int64(carolFee.ToSatoshis()) - 1, }, } testFeeCutoff(feeLimitFixed) // TODO(yy): remove the sleep once the following bug is fixed. When the // payment is reported as settled by Carol, it's expected the // commitment dance is finished and all subsequent states have been // updated. Yet we'd receive the error `cannot co-op close channel with // active htlcs` or `link failed to shutdown` if we close the channel. // We need to investigate the order of settling the payments and // updating commitments to understand and fix . time.Sleep(2 * time.Second) // Once we're done, close the channels and shut down the nodes created // throughout this test. ht.CloseChannel(alice, chanPointAliceBob) ht.CloseChannel(alice, chanPointAliceCarol) ht.CloseChannel(bob, chanPointBobDave) ht.CloseChannel(carol, chanPointCarolDave) } // computeFee calculates the payment fee as specified in BOLT07. func computeFee(baseFee, feeRate, amt lnwire.MilliSatoshi) lnwire.MilliSatoshi { return baseFee + amt*feeRate/1000000 }