lnd.xprv/routing/pathfind_test.go
Olaoluwa Osuntokun aaa04bb2e5
routing: add findPaths function whcih implements Yen's Algorithm
With this commit we make our routing more robust by looking for the
k-shortest paths rather than a single shortest path and using that
unconditionally. In a nut shell Yen’s algorithm does the following:
   * Find the shortest path from the source to the destination
   * From k=1…K, walk the kth shortest path and find possible
divergence from each node in the path

Our version of Yen’s implemented is slightly modified, rather than
actually increasing the edge weights or remove vertexes from the graph,
we instead use two black-lists: one for edges and the other for
vertexes. Our modified version of Djikstra’s algorithm is then made
aware of these black lists in order to more efficiently implement the
path iteration via spur and root node.
2017-03-21 12:20:37 -07:00

552 lines
16 KiB
Go

package routing
import (
"encoding/hex"
"encoding/json"
"errors"
"io/ioutil"
"math/big"
"net"
"os"
"strings"
"testing"
"time"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcd/chaincfg/chainhash"
"github.com/roasbeef/btcd/wire"
"github.com/roasbeef/btcutil"
prand "math/rand"
)
const (
// basicGraphFilePath is the file path for a basic graph used within
// the tests. The basic graph consists of 5 nodes with 5 channels
// connecting them.
basicGraphFilePath = "testdata/basic_graph.json"
// excessiveHopsGraphFilePath is a file path which stores the JSON dump
// of a graph which was previously triggering an erroneous excessive
// hops error. The error has since been fixed, but a test case
// exercising it is kept around to guard against regressions.
excessiveHopsGraphFilePath = "testdata/excessive_hops.json"
)
var (
randSource = prand.NewSource(time.Now().Unix())
randInts = prand.New(randSource)
testSig = &btcec.Signature{
R: new(big.Int),
S: new(big.Int),
}
_, _ = testSig.R.SetString("63724406601629180062774974542967536251589935445068131219452686511677818569431", 10)
_, _ = testSig.S.SetString("18801056069249825825291287104931333862866033135609736119018462340006816851118", 10)
testAuthProof = channeldb.ChannelAuthProof{
NodeSig1: testSig,
NodeSig2: testSig,
BitcoinSig1: testSig,
BitcoinSig2: testSig,
}
)
// testGraph is the struct which corresponds to the JSON format used to encode
// graphs within the files in the testdata directory.
//
// TODO(roasbeef): add test graph auto-generator
type testGraph struct {
Info []string `json:"info"`
Nodes []testNode `json:"nodes"`
Edges []testChan `json:"edges"`
}
// testNode represents a node within the test graph above. We skip certain
// information such as the node's IP address as that information isn't needed
// for our tests.
type testNode struct {
Source bool `json:"source"`
PubKey string `json:"pubkey"`
Alias string `json:"alias"`
}
// testChan represents the JSON version of a payment channel. This struct
// matches the Json that's encoded under the "edges" key within the test graph.
type testChan struct {
Node1 string `json:"node_1"`
Node2 string `json:"node_2"`
ChannelID uint64 `json:"channel_id"`
ChannelPoint string `json:"channel_point"`
Flags uint16 `json:"flags"`
Expiry uint16 `json:"expiry"`
MinHTLC int64 `json:"min_htlc"`
FeeBaseMsat int64 `json:"fee_base_msat"`
FeeRate float64 `json:"fee_rate"`
Capacity int64 `json:"capacity"`
}
// makeTestGraph creates a new instance of a channeldb.ChannelGraph for testing
// purposes. A callback which cleans up the created temporary directories is
// also returned and intended to be executed after the test completes.
func makeTestGraph() (*channeldb.ChannelGraph, func(), error) {
// First, create a temporary directory to be used for the duration of
// this test.
tempDirName, err := ioutil.TempDir("", "channeldb")
if err != nil {
return nil, nil, err
}
// Next, create channeldb for the first time.
cdb, err := channeldb.Open(tempDirName)
if err != nil {
return nil, nil, err
}
cleanUp := func() {
cdb.Close()
os.RemoveAll(tempDirName)
}
return cdb.ChannelGraph(), cleanUp, nil
}
// aliasMap is a map from a node's alias to its public key. This type is
// provided in order to allow easily look up from the human rememberable alias
// to an exact node's public key.
type aliasMap map[string]*btcec.PublicKey
// parseTestGraph returns a fully populated ChannelGraph given a path to a JSON
// file which encodes a test graph.
func parseTestGraph(path string) (*channeldb.ChannelGraph, func(), aliasMap, error) {
graphJSON, err := ioutil.ReadFile(path)
if err != nil {
return nil, nil, nil, err
}
// First unmarshal the JSON graph into an instance of the testGraph
// struct. Using the struct tags created above in the struct, the JSON
// will be properly parsed into the struct above.
var g testGraph
if err := json.Unmarshal(graphJSON, &g); err != nil {
return nil, nil, nil, err
}
// We'll use this fake address for the IP address of all the nodes in
// our tests. This value isn't needed for path finding so it doesn't
// need to be unique.
testAddr, err := net.ResolveTCPAddr("tcp", "192.0.0.1:8888")
if err != nil {
return nil, nil, nil, err
}
// Next, create a temporary graph database for usage within the test.
graph, cleanUp, err := makeTestGraph()
if err != nil {
return nil, nil, nil, err
}
aliasMap := make(map[string]*btcec.PublicKey)
var source *channeldb.LightningNode
// First we insert all the nodes within the graph as vertexes.
for _, node := range g.Nodes {
pubBytes, err := hex.DecodeString(node.PubKey)
if err != nil {
return nil, nil, nil, err
}
pub, err := btcec.ParsePubKey(pubBytes, btcec.S256())
if err != nil {
return nil, nil, nil, err
}
dbNode := &channeldb.LightningNode{
LastUpdate: time.Now(),
Address: testAddr,
PubKey: pub,
Alias: node.Alias,
}
// We require all aliases within the graph to be unique for our
// tests.
if _, ok := aliasMap[node.Alias]; ok {
return nil, nil, nil, errors.New("aliases for nodes " +
"must be unique!")
}
// If the alias is unique, then add the node to the
// alias map for easy lookup.
aliasMap[node.Alias] = pub
// If the node is tagged as the source, then we create a
// pointer to is so we can mark the source in the graph
// properly.
if node.Source {
// If we come across a node that's marked as the
// source, and we've already set the source in a prior
// iteration, then the JSON has an error as only ONE
// node can be the source in the graph.
if source != nil {
return nil, nil, nil, errors.New("JSON is invalid " +
"multiple nodes are tagged as the source")
}
source = dbNode
}
// With the node fully parsed, add it as a vertex within the
// graph.
if err := graph.AddLightningNode(dbNode); err != nil {
return nil, nil, nil, err
}
}
// Set the selected source node
if err := graph.SetSourceNode(source); err != nil {
return nil, nil, nil, err
}
// With all the vertexes inserted, we can now insert the edges into the
// test graph.
for _, edge := range g.Edges {
node1Bytes, err := hex.DecodeString(edge.Node1)
if err != nil {
return nil, nil, nil, err
}
node1Pub, err := btcec.ParsePubKey(node1Bytes, btcec.S256())
if err != nil {
return nil, nil, nil, err
}
node2Bytes, err := hex.DecodeString(edge.Node2)
if err != nil {
return nil, nil, nil, err
}
node2Pub, err := btcec.ParsePubKey(node2Bytes, btcec.S256())
if err != nil {
return nil, nil, nil, err
}
fundingTXID := strings.Split(edge.ChannelPoint, ":")[0]
txidBytes, err := chainhash.NewHashFromStr(fundingTXID)
if err != nil {
return nil, nil, nil, err
}
fundingPoint := wire.OutPoint{
Hash: *txidBytes,
Index: 0,
}
// We first insert the existence of the edge between the two
// nodes.
edgeInfo := channeldb.ChannelEdgeInfo{
ChannelID: edge.ChannelID,
NodeKey1: node1Pub,
NodeKey2: node2Pub,
BitcoinKey1: node1Pub,
BitcoinKey2: node2Pub,
AuthProof: &testAuthProof,
ChannelPoint: fundingPoint,
Capacity: btcutil.Amount(edge.Capacity),
}
if err := graph.AddChannelEdge(&edgeInfo); err != nil {
return nil, nil, nil, err
}
edgePolicy := &channeldb.ChannelEdgePolicy{
ChannelID: edge.ChannelID,
LastUpdate: time.Now(),
TimeLockDelta: edge.Expiry,
MinHTLC: btcutil.Amount(edge.MinHTLC),
FeeBaseMSat: btcutil.Amount(edge.FeeBaseMsat),
FeeProportionalMillionths: btcutil.Amount(edge.FeeRate),
}
// As the graph itself is directed, we need to insert two edges
// into the graph: one from node1->node2 and one from
// node2->node1. A flag of 0 indicates this is the routing
// policy for the first node, and a flag of 1 indicates its the
// information for the second node.
edgePolicy.Flags = 0
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
return nil, nil, nil, err
}
edgePolicy.Flags = 1
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
return nil, nil, nil, err
}
}
return graph, cleanUp, aliasMap, nil
}
func TestBasicGraphPathFinding(t *testing.T) {
graph, cleanUp, aliases, err := parseTestGraph(basicGraphFilePath)
defer cleanUp()
if err != nil {
t.Fatalf("unable to create graph: %v", err)
}
sourceNode, err := graph.SourceNode()
if err != nil {
t.Fatalf("unable to fetch source node: %v", err)
}
ignoredEdges := make(map[uint64]struct{})
ignoredVertexes := make(map[vertex]struct{})
// With the test graph loaded, we'll test some basic path finding using
// the pre-generated graph. Consult the testdata/basic_graph.json file
// to follow along with the assumptions we'll use to test the path
// finding.
const paymentAmt = btcutil.Amount(100)
target := aliases["sophon"]
path, err := findPath(graph, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt)
if err != nil {
t.Fatalf("unable to find path: %v", err)
}
route, err := newRoute(paymentAmt, path)
if err != nil {
t.Fatalf("unable to create path: %v", err)
}
// The length of the route selected should be of exactly length two.
if len(route.Hops) != 2 {
t.Fatalf("route is of incorrect length, expected %v got %v", 2,
len(route.Hops))
}
// As each hop only decrements a single block from the time-lock, the
// total time lock value should be two.
if route.TotalTimeLock != 2 {
t.Fatalf("expected time lock of %v, instead have %v", 2,
route.TotalTimeLock)
}
// The first hop in the path should be an edge from roasbeef to goku.
if !route.Hops[0].Channel.Node.PubKey.IsEqual(aliases["songoku"]) {
t.Fatalf("first hop should be goku, is instead: %v",
route.Hops[0].Channel.Node.Alias)
}
// The second hop should be from goku to sophon.
if !route.Hops[1].Channel.Node.PubKey.IsEqual(aliases["sophon"]) {
t.Fatalf("second hop should be sophon, is instead: %v",
route.Hops[0].Channel.Node.Alias)
}
// Next, attempt to query for a path to Luo Ji for 100 satoshis, there
// exist two possible paths in the graph, but the shorter (1 hop) path
// should be selected.
target = aliases["luoji"]
path, err = findPath(graph, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt)
if err != nil {
t.Fatalf("unable to find route: %v", err)
}
route, err = newRoute(paymentAmt, path)
if err != nil {
t.Fatalf("unable to create path: %v", err)
}
// The length of the path should be exactly one hop as it's the
// "shortest" known path in the graph.
if len(route.Hops) != 1 {
t.Fatalf("shortest path not selected, should be of length 1, "+
"is instead: %v", len(route.Hops))
}
// As we have a direct path, the total time lock value should be
// exactly one.
if route.TotalTimeLock != 1 {
t.Fatalf("expected time lock of %v, instead have %v", 1,
route.TotalTimeLock)
}
// Additionally, since this is a single-hop payment, we shouldn't have
// to pay any fees in total, so the total amount should be the payment
// amount.
if route.TotalAmount != paymentAmt {
t.Fatalf("incorrect total amount, expected %v got %v",
paymentAmt, route.TotalAmount)
}
}
func TestKShortestPathFinding(t *testing.T) {
graph, cleanUp, aliases, err := parseTestGraph(basicGraphFilePath)
defer cleanUp()
if err != nil {
t.Fatalf("unable to create graph: %v", err)
}
sourceNode, err := graph.SourceNode()
if err != nil {
t.Fatalf("unable to fetch source node: %v", err)
}
// In this test we'd like to ensure that our algoirthm to find the
// k-shortest paths from a given source node to any destination node
// works as exepcted.
// In our basic_graph.json, there exist two paths from roasbeef to luo
// ji. Our algorithm should properly find both paths, and also rank
// them in order of their total "distance".
const paymentAmt = btcutil.Amount(100)
target := aliases["luoji"]
paths, err := findPaths(graph, sourceNode, target, paymentAmt)
if err != nil {
t.Fatalf("unable to find paths between roasbeef and "+
"luo ji: %v", err)
}
// The algorithm should've found two paths from roasbeef to luo ji.
if len(paths) != 2 {
t.Fatalf("two path shouldn't been found, instead %v were",
len(paths))
}
// Additinoally, the total hop length of the first path returned should
// be _less_ than that of the second path returned.
if len(paths[0]) > len(paths[1]) {
t.Fatalf("paths found not ordered properly")
}
// Finally, we'll assert the exact expected ordering of both paths
// found.
assertExpectedPath := func(path []*ChannelHop, nodeAliases ...string) {
for i, hop := range path {
if hop.Node.Alias != nodeAliases[i] {
t.Fatalf("expected %v to be pos #%v in hop, "+
"instead %v was", nodeAliases[i], i,
hop.Node.Alias)
}
}
}
// The first route should be a direct route to luo ji.
assertExpectedPath(paths[0], "roasbeef", "luoji")
// The second route should be a route to luo ji via satoshi.
assertExpectedPath(paths[1], "roasbeef", "satoshi", "luoji")
}
func TestNewRoutePathTooLong(t *testing.T) {
// Ensure that potential paths which are over the maximum hop-limit are
// rejected.
graph, cleanUp, aliases, err := parseTestGraph(excessiveHopsGraphFilePath)
defer cleanUp()
if err != nil {
t.Fatalf("unable to create graph: %v", err)
}
sourceNode, err := graph.SourceNode()
if err != nil {
t.Fatalf("unable to fetch source node: %v", err)
}
ignoredEdges := make(map[uint64]struct{})
ignoredVertexes := make(map[vertex]struct{})
const paymentAmt = btcutil.Amount(100)
// We start by confirminig that routing a payment 20 hops away is possible.
// Alice should be able to find a valid route to ursula.
target := aliases["ursula"]
_, err = findPath(graph, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt)
if err != nil {
t.Fatalf("path should have been found")
}
// Vincent is 21 hops away from Alice, and thus no valid route should be
// presented to Alice.
target = aliases["vincent"]
path, err := findPath(graph, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt)
if err == nil {
t.Fatalf("should not have been able to find path, supposed to be "+
"greater than 20 hops, found route with %v hops",
len(path))
}
}
func TestPathNotAvailable(t *testing.T) {
graph, cleanUp, _, err := parseTestGraph(basicGraphFilePath)
defer cleanUp()
if err != nil {
t.Fatalf("unable to create graph: %v", err)
}
sourceNode, err := graph.SourceNode()
if err != nil {
t.Fatalf("unable to fetch source node: %v", err)
}
ignoredEdges := make(map[uint64]struct{})
ignoredVertexes := make(map[vertex]struct{})
// With the test graph loaded, we'll test that queries for target that
// are either unreachable within the graph, or unknown result in an
// error.
unknownNodeStr := "03dd46ff29a6941b4a2607525b043ec9b020b3f318a1bf281536fd7011ec59c882"
unknownNodeBytes, err := hex.DecodeString(unknownNodeStr)
if err != nil {
t.Fatalf("unable to parse bytes: %v", err)
}
unknownNode, err := btcec.ParsePubKey(unknownNodeBytes, btcec.S256())
if err != nil {
t.Fatalf("unable to parse pubkey: %v", err)
}
_, err = findPath(graph, sourceNode, unknownNode, ignoredVertexes,
ignoredEdges, 100)
if err != ErrNoPathFound {
t.Fatalf("path shouldn't have been found: %v", err)
}
}
func TestPathInsufficientCapacity(t *testing.T) {
graph, cleanUp, aliases, err := parseTestGraph(basicGraphFilePath)
defer cleanUp()
if err != nil {
t.Fatalf("unable to create graph: %v", err)
}
sourceNode, err := graph.SourceNode()
if err != nil {
t.Fatalf("unable to fetch source node: %v", err)
}
ignoredEdges := make(map[uint64]struct{})
ignoredVertexes := make(map[vertex]struct{})
// Next, test that attempting to find a path in which the current
// channel graph cannot support due to insufficient capacity triggers
// an error.
// To test his we'll attempt to make a payment of 1 BTC, or 100 million
// satoshis. The largest channel in the basic graph is of size 100k
// satoshis, so we shouldn't be able to find a path to sophon even
// though we have a 2-hop link.
target := aliases["sophon"]
const payAmt = btcutil.SatoshiPerBitcoin
_, err = findPath(graph, sourceNode, target, ignoredVertexes,
ignoredEdges, payAmt)
if err != ErrNoPathFound {
t.Fatalf("graph shouldn't be able to support payment: %v", err)
}
}
func TestPathInsufficientCapacityWithFee(t *testing.T) {
// TODO(roasbeef): encode live graph to json
// TODO(roasbeef): need to add a case, or modify the fee ratio for one
// to ensure that has going forward, but when fees are applied doesn't
// work
}