lnd/itest/lnd_routing_test.go
2023-06-13 11:58:33 +02:00

1347 lines
43 KiB
Go

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,
},
)
defer ht.CloseChannel(carol, chanPointPrivate)
// 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)
}
// 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
}