lnd/routing/router_test.go

565 lines
16 KiB
Go

package routing
import (
"bytes"
"errors"
"fmt"
"image/color"
"testing"
"time"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/roasbeef/btcd/wire"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/roasbeef/btcd/btcec"
)
type testCtx struct {
router *ChannelRouter
graph *channeldb.ChannelGraph
aliases map[string]*btcec.PublicKey
chain *mockChain
chainView *mockChainView
}
func createTestCtx(startingHeight uint32, testGraph ...string) (*testCtx, func(), error) {
var (
graph *channeldb.ChannelGraph
sourceNode *channeldb.LightningNode
cleanup func()
err error
)
aliasMap := make(map[string]*btcec.PublicKey)
// If the testGraph isn't set, then we'll create an empty graph to
// start out with. Our usage of a variadic parameter allows caller to
// omit the testGraph argument all together if they wish to start with
// a blank graph.
if testGraph == nil {
// First we'll set up a test graph for usage within the test.
graph, cleanup, err = makeTestGraph()
if err != nil {
return nil, nil, fmt.Errorf("unable to create test graph: %v", err)
}
sourceNode, err = createTestNode()
if err != nil {
return nil, nil, fmt.Errorf("unable to create source node: %v", err)
}
if err = graph.SetSourceNode(sourceNode); err != nil {
return nil, nil, fmt.Errorf("unable to set source node: %v", err)
}
} else {
// Otherwise, we'll attempt to locate and parse out the file
// that encodes the graph that our tests should be run against.
graph, cleanup, aliasMap, err = parseTestGraph(testGraph[0])
if err != nil {
return nil, nil, fmt.Errorf("unable to create test graph: %v", err)
}
sourceNode, err = graph.SourceNode()
if err != nil {
return nil, nil, fmt.Errorf("unable to fetch source node: %v", err)
}
}
// Next we'll initialize an instance of the channel router with mock
// versions of the chain and channel notifier. As we don't need to test
// any p2p functionality, the peer send and switch send messages won't
// be populated.
chain := newMockChain(startingHeight)
chainView := newMockChainView()
router, err := New(Config{
Graph: graph,
Chain: chain,
ChainView: chainView,
SendToSwitch: func(_ *btcec.PublicKey,
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) {
return [32]byte{}, nil
},
})
if err != nil {
return nil, nil, fmt.Errorf("unable to create router %v", err)
}
if err := router.Start(); err != nil {
return nil, nil, fmt.Errorf("unable to start router: %v", err)
}
cleanUp := func() {
router.Stop()
cleanup()
}
return &testCtx{
router: router,
graph: graph,
aliases: aliasMap,
chain: chain,
chainView: chainView,
}, cleanUp, nil
}
// TestFindRoutesFeeSorting asserts that routes found by the FindRoutes method
// within the channel router are properly returned in a sorted order, with the
// lowest fee route coming first.
func TestFindRoutesFeeSorting(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtx(startingBlockHeight, basicGraphFilePath)
defer cleanUp()
if err != nil {
t.Fatalf("unable to create router: %v", err)
}
// In this test we'd like to ensure proper integration of the various
// functions that are involved in path finding, and also route
// selection.
// Execute a query for all possible routes between roasbeef and luo ji.
paymentAmt := lnwire.NewMSatFromSatoshis(100)
target := ctx.aliases["luoji"]
routes, err := ctx.router.FindRoutes(target, paymentAmt)
if err != nil {
t.Fatalf("unable to find any routes: %v", err)
}
// Exactly, two such paths should be found.
if len(routes) != 2 {
t.Fatalf("2 routes shouldn't been selected, instead %v were: %v",
len(routes), spew.Sdump(routes))
}
// The paths should properly be ranked according to their total fee
// rate.
if routes[0].TotalFees > routes[1].TotalFees {
t.Fatalf("routes not ranked by total fee: %v",
spew.Sdump(routes))
}
}
// TestSendPaymentRouteFailureFallback tests that when sending a payment, if
// one of the target routes is seen as unavailable, then the next route in the
// queue is used instead. This process should continue until either a payment
// succeeds, or all routes have been exhausted.
func TestSendPaymentRouteFailureFallback(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtx(startingBlockHeight, basicGraphFilePath)
defer cleanUp()
if err != nil {
t.Fatalf("unable to create router: %v", err)
}
// Craft a LightningPayment struct that'll send a payment from roasbeef
// to luo ji for 100 satoshis.
var payHash [32]byte
payment := LightningPayment{
Target: ctx.aliases["luoji"],
Amount: lnwire.NewMSatFromSatoshis(1000),
PaymentHash: payHash,
}
var preImage [32]byte
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
// We'll modify the SendToSwitch method that's been set within the
// router's configuration to ignore the path that has luo ji as the
// first hop. This should force the router to instead take the
// available two hop path (through satoshi).
ctx.router.cfg.SendToSwitch = func(n *btcec.PublicKey,
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) {
if ctx.aliases["luoji"].IsEqual(n) {
return [32]byte{}, errors.New("send error")
}
return preImage, nil
}
// Send off the payment request to the router, route through satoshi
// should've been selected as a fall back and succeeded correctly.
paymentPreImage, route, err := ctx.router.SendPayment(&payment)
if err != nil {
t.Fatalf("unable to send payment: %v", err)
}
// The route selected should have two hops
if len(route.Hops) != 2 {
t.Fatalf("incorrect route length: expected %v got %v", 2,
len(route.Hops))
}
// The preimage should match up with the once created above.
if !bytes.Equal(paymentPreImage[:], preImage[:]) {
t.Fatalf("incorrect preimage used: expected %x got %x",
preImage[:], paymentPreImage[:])
}
// The route should have satoshi as the first hop.
if route.Hops[0].Channel.Node.Alias != "satoshi" {
t.Fatalf("route should go through satoshi as first hop, "+
"instead passes through: %v",
route.Hops[0].Channel.Node.Alias)
}
}
// TestAddProof checks that we can update the channel proof after channel
// info was added to the database.
func TestAddProof(t *testing.T) {
t.Parallel()
ctx, cleanup, err := createTestCtx(0)
if err != nil {
t.Fatal(err)
}
defer cleanup()
// Before creating out edge, we'll create two new nodes within the
// network that the channel will connect.
node1, err := createTestNode()
if err != nil {
t.Fatal(err)
}
node2, err := createTestNode()
if err != nil {
t.Fatal(err)
}
// In order to be able to add the edge we should have a valid funding
// UTXO within the blockchain.
fundingTx, _, chanID, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(), bitcoinKey2.SerializeCompressed(),
100, 0)
if err != nil {
t.Fatalf("unable create channel edge: %v", err)
}
fundingBlock := &wire.MsgBlock{
Transactions: []*wire.MsgTx{fundingTx},
}
ctx.chain.addBlock(fundingBlock, chanID.BlockHeight)
// After utxo was recreated adding the edge without the proof.
edge := &channeldb.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
NodeKey1: node1.PubKey,
NodeKey2: node2.PubKey,
BitcoinKey1: bitcoinKey1,
BitcoinKey2: bitcoinKey2,
AuthProof: nil,
}
if err := ctx.router.AddEdge(edge); err != nil {
t.Fatalf("unable to add edge: %v", err)
}
// Now we'll attempt to update the proof and check that it has been
// properly updated.
if err := ctx.router.AddProof(*chanID, &testAuthProof); err != nil {
t.Fatalf("unable to add proof: %v", err)
}
info, _, _, err := ctx.router.GetChannelByID(*chanID)
if info.AuthProof == nil {
t.Fatal("proof have been updated")
}
}
// TestIgnoreNodeAnnouncement tests that adding a node to the router that is
// not known from any channel annoucement, leads to the annoucement being
// ignored.
func TestIgnoreNodeAnnouncement(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtx(startingBlockHeight,
basicGraphFilePath)
defer cleanUp()
if err != nil {
t.Fatalf("unable to create router: %v", err)
}
node := &channeldb.LightningNode{
HaveNodeAnnouncement: true,
LastUpdate: time.Unix(123, 0),
Addresses: testAddrs,
PubKey: priv1.PubKey(),
Color: color.RGBA{1, 2, 3, 0},
Alias: "node11",
AuthSig: testSig,
Features: testFeatures,
}
err = ctx.router.AddNode(node)
if !IsError(err, ErrIgnored) {
t.Fatalf("expected to get ErrIgnore, instead got: %v", err)
}
}
// TestAddEdgeUnknownVertexes tests that if an edge is added that contains two
// vertexes which we don't know of, the edge should be available for use
// regardless. This is due to the fact that we don't actually need node
// announcements for the channel vertexes to be able to use the channel.
func TestAddEdgeUnknownVertexes(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtx(startingBlockHeight,
basicGraphFilePath)
defer cleanUp()
if err != nil {
t.Fatalf("unable to create router: %v", err)
}
// The two nodes we are about to add should not exist yet.
_, exists1, err := ctx.graph.HasLightningNode(priv1.PubKey())
if err != nil {
t.Fatalf("unable to query graph: %v", err)
}
if exists1 {
t.Fatalf("node already existed")
}
_, exists2, err := ctx.graph.HasLightningNode(priv2.PubKey())
if err != nil {
t.Fatalf("unable to query graph: %v", err)
}
if exists2 {
t.Fatalf("node already existed")
}
// Add the edge between the two unknown nodes to the graph, and check
// that the nodes are found after the fact.
fundingTx, _, chanID, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(),
10000, 500)
if err != nil {
t.Fatalf("unable to create channel edge: %v", err)
}
fundingBlock := &wire.MsgBlock{
Transactions: []*wire.MsgTx{fundingTx},
}
ctx.chain.addBlock(fundingBlock, chanID.BlockHeight)
edge := &channeldb.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
NodeKey1: priv1.PubKey(),
NodeKey2: priv2.PubKey(),
BitcoinKey1: bitcoinKey1,
BitcoinKey2: bitcoinKey2,
AuthProof: nil,
}
if err := ctx.router.AddEdge(edge); err != nil {
t.Fatalf("expected to be able to add edge to the channel graph,"+
" even though the vertexes were unknown: %v.", err)
}
// We must add the edge policy to be able to use the edge for route
// finding.
edgePolicy := &channeldb.ChannelEdgePolicy{
Signature: testSig,
ChannelID: edge.ChannelID,
LastUpdate: time.Now(),
TimeLockDelta: 10,
MinHTLC: 1,
FeeBaseMSat: 10,
FeeProportionalMillionths: 10000,
}
edgePolicy.Flags = 0
if err := ctx.router.UpdateEdge(edgePolicy); err != nil {
t.Fatalf("unable to update edge policy: %v", err)
}
// Create edge in the other direction as well.
edgePolicy = &channeldb.ChannelEdgePolicy{
Signature: testSig,
ChannelID: edge.ChannelID,
LastUpdate: time.Now(),
TimeLockDelta: 10,
MinHTLC: 1,
FeeBaseMSat: 10,
FeeProportionalMillionths: 10000,
}
edgePolicy.Flags = 1
if err := ctx.router.UpdateEdge(edgePolicy); err != nil {
t.Fatalf("unable to update edge policy: %v", err)
}
// After adding the edge between the two previously unknown nodes, they
// should have been added to the graph.
_, exists1, err = ctx.graph.HasLightningNode(priv1.PubKey())
if err != nil {
t.Fatalf("unable to query graph: %v", err)
}
if !exists1 {
t.Fatalf("node1 was not added to the graph")
}
_, exists2, err = ctx.graph.HasLightningNode(priv2.PubKey())
if err != nil {
t.Fatalf("unable to query graph: %v", err)
}
if !exists2 {
t.Fatalf("node2 was not added to the graph")
}
// We will connect node1 to the rest of the test graph, and make sure
// we can find a route to node2, which will use the just added channel
// edge.
// We will connect node 1 to "sophon"
connectNode := ctx.aliases["sophon"]
if connectNode == nil {
t.Fatalf("could not find node to connect to")
}
var (
pubKey1 *btcec.PublicKey
pubKey2 *btcec.PublicKey
)
node1Bytes := priv1.PubKey().SerializeCompressed()
node2Bytes := connectNode.SerializeCompressed()
if bytes.Compare(node1Bytes, node2Bytes) == -1 {
pubKey1 = priv1.PubKey()
pubKey2 = connectNode
} else {
pubKey1 = connectNode
pubKey2 = priv1.PubKey()
}
fundingTx, _, chanID, err = createChannelEdge(ctx,
pubKey1.SerializeCompressed(), pubKey2.SerializeCompressed(),
10000, 510)
if err != nil {
t.Fatalf("unable to create channel edge: %v", err)
}
fundingBlock = &wire.MsgBlock{
Transactions: []*wire.MsgTx{fundingTx},
}
ctx.chain.addBlock(fundingBlock, chanID.BlockHeight)
edge = &channeldb.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
NodeKey1: pubKey1,
NodeKey2: pubKey2,
BitcoinKey1: pubKey1,
BitcoinKey2: pubKey2,
AuthProof: nil,
}
if err := ctx.router.AddEdge(edge); err != nil {
t.Fatalf("unable to add edge to the channel graph: %v.", err)
}
edgePolicy = &channeldb.ChannelEdgePolicy{
Signature: testSig,
ChannelID: edge.ChannelID,
LastUpdate: time.Now(),
TimeLockDelta: 10,
MinHTLC: 1,
FeeBaseMSat: 10,
FeeProportionalMillionths: 10000,
}
edgePolicy.Flags = 0
if err := ctx.router.UpdateEdge(edgePolicy); err != nil {
t.Fatalf("unable to update edge policy: %v", err)
}
edgePolicy = &channeldb.ChannelEdgePolicy{
Signature: testSig,
ChannelID: edge.ChannelID,
LastUpdate: time.Now(),
TimeLockDelta: 10,
MinHTLC: 1,
FeeBaseMSat: 10,
FeeProportionalMillionths: 10000,
}
edgePolicy.Flags = 1
if err := ctx.router.UpdateEdge(edgePolicy); err != nil {
t.Fatalf("unable to update edge policy: %v", err)
}
// We should now be able to find one route to node 2.
paymentAmt := lnwire.NewMSatFromSatoshis(100)
targetNode := priv2.PubKey()
routes, err := ctx.router.FindRoutes(targetNode, paymentAmt)
if err != nil {
t.Fatalf("unable to find any routes: %v", err)
}
if len(routes) != 1 {
t.Fatalf("expected to find 1 route, found: %v", len(routes))
}
// Now check that we can update the node info for the partial node
// without messing up the channel graph.
n1 := &channeldb.LightningNode{
HaveNodeAnnouncement: true,
LastUpdate: time.Unix(123, 0),
Addresses: testAddrs,
PubKey: priv1.PubKey(),
Color: color.RGBA{1, 2, 3, 0},
Alias: "node11",
AuthSig: testSig,
Features: testFeatures,
}
if err := ctx.router.AddNode(n1); err != nil {
t.Fatalf("could not add node: %v", err)
}
n2 := &channeldb.LightningNode{
HaveNodeAnnouncement: true,
LastUpdate: time.Unix(123, 0),
Addresses: testAddrs,
PubKey: priv2.PubKey(),
Color: color.RGBA{1, 2, 3, 0},
Alias: "node22",
AuthSig: testSig,
Features: testFeatures,
}
if err := ctx.router.AddNode(n2); err != nil {
t.Fatalf("could not add node: %v", err)
}
// Should still be able to find the route, and the info should be
// updated.
routes, err = ctx.router.FindRoutes(targetNode, paymentAmt)
if err != nil {
t.Fatalf("unable to find any routes: %v", err)
}
if len(routes) != 1 {
t.Fatalf("expected to find 1 route, found: %v", len(routes))
}
copy1, err := ctx.graph.FetchLightningNode(priv1.PubKey())
if err != nil {
t.Fatalf("unable to fetch node: %v", err)
}
if copy1.Alias != n1.Alias {
t.Fatalf("fetched node not equal to original")
}
copy2, err := ctx.graph.FetchLightningNode(priv2.PubKey())
if err != nil {
t.Fatalf("unable to fetch node: %v", err)
}
if copy2.Alias != n2.Alias {
t.Fatalf("fetched node not equal to original")
}
}