20aba8060f
This commit modifies our modified version of Dijkstra's to include sufficient link capacity within the algorithm’s relaxation condition. With this change, we’ll now avoid exploring the graph in the direction of a link that has insufficient capacity, allowing us to terminate path finding more quickly.
446 lines
14 KiB
Go
446 lines
14 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)
|
|
}
|
|
|
|
// 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"]
|
|
route, err := findRoute(graph, target, paymentAmt)
|
|
if err != nil {
|
|
t.Fatalf("unable to find route: %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)
|
|
}
|
|
|
|
// WE shoul
|
|
|
|
// 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"]
|
|
route, err = findRoute(graph, target, paymentAmt)
|
|
if err != nil {
|
|
t.Fatalf("unable to find route: %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 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)
|
|
}
|
|
|
|
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"]
|
|
route, err := findRoute(graph, target, 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"]
|
|
route, err = findRoute(graph, target, 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(route.Hops))
|
|
}
|
|
|
|
}
|
|
|
|
func TestPathNotAvailable(t *testing.T) {
|
|
graph, cleanUp, _, err := parseTestGraph(basicGraphFilePath)
|
|
defer cleanUp()
|
|
if err != nil {
|
|
t.Fatalf("unable to create graph: %v", err)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
if _, err := findRoute(graph, unknownNode, 100); 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)
|
|
}
|
|
|
|
// 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 = findRoute(graph, target, 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
|
|
}
|