1654 lines
50 KiB
Go
1654 lines
50 KiB
Go
package routing
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"io/ioutil"
|
|
"math"
|
|
"math/big"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
|
|
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"
|
|
|
|
// specExampleFilePath is a file path which stores an example which
|
|
// implementations will use in order to ensure that they're calculating
|
|
// the payload for each hop in path properly.
|
|
specExampleFilePath = "testdata/spec_example.json"
|
|
|
|
// noFeeLimit is the maximum value of a payment through Lightning. We
|
|
// can use this value to signal there is no fee limit since payments
|
|
// should never be larger than this.
|
|
noFeeLimit = lnwire.MilliSatoshi(math.MaxUint32)
|
|
)
|
|
|
|
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{
|
|
NodeSig1Bytes: testSig.Serialize(),
|
|
NodeSig2Bytes: testSig.Serialize(),
|
|
BitcoinSig1Bytes: testSig.Serialize(),
|
|
BitcoinSig2Bytes: testSig.Serialize(),
|
|
}
|
|
)
|
|
|
|
// 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 int64 `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 memorable 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.
|
|
var testAddrs []net.Addr
|
|
testAddr, err := net.ResolveTCPAddr("tcp", "192.0.0.1:8888")
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
testAddrs = append(testAddrs, testAddr)
|
|
|
|
// 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
|
|
}
|
|
|
|
dbNode := &channeldb.LightningNode{
|
|
HaveNodeAnnouncement: true,
|
|
AuthSigBytes: testSig.Serialize(),
|
|
LastUpdate: time.Now(),
|
|
Addresses: testAddrs,
|
|
Alias: node.Alias,
|
|
Features: testFeatures,
|
|
}
|
|
copy(dbNode.PubKeyBytes[:], pubBytes)
|
|
|
|
// 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!")
|
|
}
|
|
|
|
pub, err := btcec.ParsePubKey(pubBytes, btcec.S256())
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
if source != nil {
|
|
// 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
|
|
}
|
|
|
|
node2Bytes, err := hex.DecodeString(edge.Node2)
|
|
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,
|
|
AuthProof: &testAuthProof,
|
|
ChannelPoint: fundingPoint,
|
|
Capacity: btcutil.Amount(edge.Capacity),
|
|
}
|
|
|
|
copy(edgeInfo.NodeKey1Bytes[:], node1Bytes)
|
|
copy(edgeInfo.NodeKey2Bytes[:], node2Bytes)
|
|
copy(edgeInfo.BitcoinKey1Bytes[:], node1Bytes)
|
|
copy(edgeInfo.BitcoinKey2Bytes[:], node2Bytes)
|
|
|
|
err = graph.AddChannelEdge(&edgeInfo)
|
|
if err != nil && err != channeldb.ErrEdgeAlreadyExist {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
edgePolicy := &channeldb.ChannelEdgePolicy{
|
|
SigBytes: testSig.Serialize(),
|
|
Flags: lnwire.ChanUpdateFlag(edge.Flags),
|
|
ChannelID: edge.ChannelID,
|
|
LastUpdate: time.Now(),
|
|
TimeLockDelta: edge.Expiry,
|
|
MinHTLC: lnwire.MilliSatoshi(edge.MinHTLC),
|
|
FeeBaseMSat: lnwire.MilliSatoshi(edge.FeeBaseMsat),
|
|
FeeProportionalMillionths: lnwire.MilliSatoshi(edge.FeeRate),
|
|
}
|
|
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
}
|
|
|
|
return graph, cleanUp, aliasMap, nil
|
|
}
|
|
|
|
type testChannelPolicy struct {
|
|
Expiry uint16
|
|
MinHTLC lnwire.MilliSatoshi
|
|
FeeBaseMsat lnwire.MilliSatoshi
|
|
FeeRate lnwire.MilliSatoshi
|
|
}
|
|
|
|
type testChannelEnd struct {
|
|
Alias string
|
|
testChannelPolicy
|
|
}
|
|
|
|
func defaultTestChannelEnd(alias string) *testChannelEnd {
|
|
return &testChannelEnd{
|
|
Alias: alias,
|
|
testChannelPolicy: testChannelPolicy{
|
|
Expiry: 144,
|
|
MinHTLC: lnwire.MilliSatoshi(1000),
|
|
FeeBaseMsat: lnwire.MilliSatoshi(1000),
|
|
FeeRate: lnwire.MilliSatoshi(1),
|
|
},
|
|
}
|
|
}
|
|
|
|
func symmetricTestChannel(alias1 string, alias2 string, capacity btcutil.Amount,
|
|
policy *testChannelPolicy) *testChannel {
|
|
return &testChannel{
|
|
Capacity: capacity,
|
|
Node1: &testChannelEnd{
|
|
Alias: alias1,
|
|
testChannelPolicy: *policy,
|
|
},
|
|
Node2: &testChannelEnd{
|
|
Alias: alias2,
|
|
testChannelPolicy: *policy,
|
|
},
|
|
}
|
|
}
|
|
|
|
type testChannel struct {
|
|
Node1 *testChannelEnd
|
|
Node2 *testChannelEnd
|
|
Capacity btcutil.Amount
|
|
}
|
|
|
|
// createTestGraph returns a fully populated ChannelGraph based on a set of
|
|
// test channels. Additional required information like keys are derived in
|
|
// a deterministical way and added to the channel graph. A list of nodes is
|
|
// not required and derived from the channel data. The goal is to keep
|
|
// instantiating a test channel graph as light weight as possible.
|
|
func createTestGraph(testChannels []*testChannel) (*channeldb.ChannelGraph, func(), aliasMap, error) {
|
|
// 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.
|
|
var testAddrs []net.Addr
|
|
testAddr, err := net.ResolveTCPAddr("tcp", "192.0.0.1:8888")
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
testAddrs = append(testAddrs, testAddr)
|
|
|
|
// 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)
|
|
|
|
nodeIndex := byte(0)
|
|
addNodeWithAlias := func(alias string) (*channeldb.LightningNode, error) {
|
|
keyBytes := make([]byte, 32)
|
|
keyBytes = []byte{
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, nodeIndex + 1,
|
|
}
|
|
|
|
_, pubKey := btcec.PrivKeyFromBytes(btcec.S256(),
|
|
keyBytes)
|
|
|
|
dbNode := &channeldb.LightningNode{
|
|
HaveNodeAnnouncement: true,
|
|
AuthSigBytes: testSig.Serialize(),
|
|
LastUpdate: time.Now(),
|
|
Addresses: testAddrs,
|
|
Alias: alias,
|
|
Features: testFeatures,
|
|
}
|
|
|
|
copy(dbNode.PubKeyBytes[:], pubKey.SerializeCompressed())
|
|
|
|
// With the node fully parsed, add it as a vertex within the
|
|
// graph.
|
|
if err := graph.AddLightningNode(dbNode); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
aliasMap[alias] = pubKey
|
|
nodeIndex++
|
|
|
|
return dbNode, nil
|
|
}
|
|
|
|
var source *channeldb.LightningNode
|
|
if source, err = addNodeWithAlias("roasbeef"); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
// Set the source node
|
|
if err := graph.SetSourceNode(source); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
channelID := uint64(0)
|
|
for _, testChannel := range testChannels {
|
|
for _, alias := range []string{
|
|
testChannel.Node1.Alias, testChannel.Node2.Alias} {
|
|
|
|
_, exists := aliasMap[alias]
|
|
if !exists {
|
|
addNodeWithAlias(alias)
|
|
}
|
|
}
|
|
|
|
var hash [sha256.Size]byte
|
|
hash[len(hash)-1] = byte(channelID)
|
|
|
|
fundingPoint := &wire.OutPoint{
|
|
Hash: chainhash.Hash(hash),
|
|
Index: 0,
|
|
}
|
|
|
|
// We first insert the existence of the edge between the two
|
|
// nodes.
|
|
edgeInfo := channeldb.ChannelEdgeInfo{
|
|
ChannelID: channelID,
|
|
AuthProof: &testAuthProof,
|
|
ChannelPoint: *fundingPoint,
|
|
Capacity: testChannel.Capacity,
|
|
}
|
|
|
|
node1Bytes := aliasMap[testChannel.Node1.Alias].SerializeCompressed()
|
|
node2Bytes := aliasMap[testChannel.Node2.Alias].SerializeCompressed()
|
|
|
|
copy(edgeInfo.NodeKey1Bytes[:], node1Bytes)
|
|
copy(edgeInfo.NodeKey2Bytes[:], node2Bytes)
|
|
copy(edgeInfo.BitcoinKey1Bytes[:], node1Bytes)
|
|
copy(edgeInfo.BitcoinKey2Bytes[:], node2Bytes)
|
|
|
|
err = graph.AddChannelEdge(&edgeInfo)
|
|
if err != nil && err != channeldb.ErrEdgeAlreadyExist {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
edgePolicy := &channeldb.ChannelEdgePolicy{
|
|
SigBytes: testSig.Serialize(),
|
|
Flags: lnwire.ChanUpdateFlag(0),
|
|
ChannelID: channelID,
|
|
LastUpdate: time.Now(),
|
|
TimeLockDelta: testChannel.Node1.Expiry,
|
|
MinHTLC: testChannel.Node1.MinHTLC,
|
|
FeeBaseMSat: testChannel.Node1.FeeBaseMsat,
|
|
FeeProportionalMillionths: testChannel.Node1.FeeRate,
|
|
}
|
|
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
edgePolicy = &channeldb.ChannelEdgePolicy{
|
|
SigBytes: testSig.Serialize(),
|
|
Flags: lnwire.ChanUpdateFlag(lnwire.ChanUpdateDirection),
|
|
ChannelID: channelID,
|
|
LastUpdate: time.Now(),
|
|
TimeLockDelta: testChannel.Node2.Expiry,
|
|
MinHTLC: testChannel.Node2.MinHTLC,
|
|
FeeBaseMSat: testChannel.Node2.FeeBaseMsat,
|
|
FeeProportionalMillionths: testChannel.Node2.FeeRate,
|
|
}
|
|
|
|
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
channelID++
|
|
}
|
|
|
|
return graph, cleanUp, aliasMap, nil
|
|
}
|
|
|
|
// TestFindLowestFeePath tests that out of two routes with identical total
|
|
// time lock values, the route with the lowest total fee should be returned.
|
|
// The fee rates are chosen such that the test failed on the previous edge
|
|
// weight function where one of the terms was fee squared.
|
|
func TestFindLowestFeePath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Set up a test graph with two paths from roasbeef to target. Both
|
|
// paths have equal total time locks, but the path through b has lower
|
|
// fees (700 compared to 800 for the path through a).
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("roasbeef", "first", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
}),
|
|
symmetricTestChannel("first", "a", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
}),
|
|
symmetricTestChannel("a", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
}),
|
|
symmetricTestChannel("first", "b", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 100,
|
|
MinHTLC: 1,
|
|
}),
|
|
symmetricTestChannel("b", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 600,
|
|
MinHTLC: 1,
|
|
}),
|
|
}
|
|
|
|
graph, cleanUp, aliases, err := createTestGraph(testChannels)
|
|
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)
|
|
}
|
|
sourceVertex := Vertex(sourceNode.PubKeyBytes)
|
|
|
|
ignoredEdges := make(map[uint64]struct{})
|
|
ignoredVertexes := make(map[Vertex]struct{})
|
|
|
|
const (
|
|
startingHeight = 100
|
|
finalHopCLTV = 1
|
|
)
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
target := aliases["target"]
|
|
path, err := findPath(
|
|
nil, graph, nil, sourceNode, target, ignoredVertexes,
|
|
ignoredEdges, paymentAmt, noFeeLimit, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unable to find path: %v", err)
|
|
}
|
|
route, err := newRoute(
|
|
paymentAmt, infinity, sourceVertex, path, startingHeight,
|
|
finalHopCLTV)
|
|
if err != nil {
|
|
t.Fatalf("unable to create path: %v", err)
|
|
}
|
|
|
|
// Assert that the lowest fee route is returned.
|
|
if !bytes.Equal(route.Hops[1].Channel.Node.PubKeyBytes[:],
|
|
aliases["b"].SerializeCompressed()) {
|
|
t.Fatalf("expected route to pass through b, "+
|
|
"but got a route through %v",
|
|
route.Hops[1].Channel.Node.Alias)
|
|
}
|
|
}
|
|
|
|
type expectedHop struct {
|
|
alias string
|
|
fee lnwire.MilliSatoshi
|
|
fwdAmount lnwire.MilliSatoshi
|
|
timeLock uint32
|
|
}
|
|
|
|
type basicGraphPathFindingTestCase struct {
|
|
target string
|
|
paymentAmt btcutil.Amount
|
|
feeLimit lnwire.MilliSatoshi
|
|
expectedTotalAmt lnwire.MilliSatoshi
|
|
expectedTotalTimeLock uint32
|
|
expectedHops []expectedHop
|
|
expectFailureNoPath bool
|
|
}
|
|
|
|
var basicGraphPathFindingTests = []basicGraphPathFindingTestCase{
|
|
// Basic route with one intermediate hop.
|
|
{target: "sophon", paymentAmt: 100, feeLimit: noFeeLimit,
|
|
expectedTotalTimeLock: 102, expectedTotalAmt: 100110,
|
|
expectedHops: []expectedHop{
|
|
{alias: "songoku", fwdAmount: 100000, fee: 110, timeLock: 101},
|
|
{alias: "sophon", fwdAmount: 100000, fee: 0, timeLock: 101},
|
|
}},
|
|
|
|
// Basic direct (one hop) route.
|
|
{target: "luoji", paymentAmt: 100, feeLimit: noFeeLimit,
|
|
expectedTotalTimeLock: 101, expectedTotalAmt: 100000,
|
|
expectedHops: []expectedHop{
|
|
{alias: "luoji", fwdAmount: 100000, fee: 0, timeLock: 101},
|
|
}},
|
|
|
|
// Three hop route where fees need to be added in to the forwarding amount.
|
|
// The high fee hop phamnewun should be avoided.
|
|
{target: "elst", paymentAmt: 50000, feeLimit: noFeeLimit,
|
|
expectedTotalTimeLock: 103, expectedTotalAmt: 50050210,
|
|
expectedHops: []expectedHop{
|
|
{alias: "songoku", fwdAmount: 50000200, fee: 50010, timeLock: 102},
|
|
{alias: "sophon", fwdAmount: 50000000, fee: 200, timeLock: 101},
|
|
{alias: "elst", fwdAmount: 50000000, fee: 0, timeLock: 101},
|
|
}},
|
|
// Three hop route where fees need to be added in to the forwarding amount.
|
|
// However this time the fwdAmount becomes too large for the roasbeef <->
|
|
// songoku channel. Then there is no other option than to choose the
|
|
// expensive phamnuwen channel. This test case was failing before
|
|
// the route search was executed backwards.
|
|
{target: "elst", paymentAmt: 100000, feeLimit: noFeeLimit,
|
|
expectedTotalTimeLock: 103, expectedTotalAmt: 110010220,
|
|
expectedHops: []expectedHop{
|
|
{alias: "phamnuwen", fwdAmount: 100000200, fee: 10010020, timeLock: 102},
|
|
{alias: "sophon", fwdAmount: 100000000, fee: 200, timeLock: 101},
|
|
{alias: "elst", fwdAmount: 100000000, fee: 0, timeLock: 101},
|
|
}},
|
|
|
|
// Basic route with fee limit.
|
|
{target: "sophon", paymentAmt: 100, feeLimit: 50,
|
|
expectFailureNoPath: true,
|
|
}}
|
|
|
|
func TestBasicGraphPathFinding(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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.
|
|
|
|
for _, testCase := range basicGraphPathFindingTests {
|
|
t.Run(testCase.target, func(subT *testing.T) {
|
|
testBasicGraphPathFindingCase(subT, graph, aliases, &testCase)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testBasicGraphPathFindingCase(t *testing.T, graph *channeldb.ChannelGraph,
|
|
aliases aliasMap, test *basicGraphPathFindingTestCase) {
|
|
|
|
expectedHops := test.expectedHops
|
|
expectedHopCount := len(expectedHops)
|
|
|
|
sourceNode, err := graph.SourceNode()
|
|
if err != nil {
|
|
t.Fatalf("unable to fetch source node: %v", err)
|
|
}
|
|
sourceVertex := Vertex(sourceNode.PubKeyBytes)
|
|
|
|
ignoredEdges := make(map[uint64]struct{})
|
|
ignoredVertexes := make(map[Vertex]struct{})
|
|
|
|
const (
|
|
startingHeight = 100
|
|
finalHopCLTV = 1
|
|
)
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(test.paymentAmt)
|
|
target := aliases[test.target]
|
|
path, err := findPath(
|
|
nil, graph, nil, sourceNode, target, ignoredVertexes,
|
|
ignoredEdges, paymentAmt, test.feeLimit, nil,
|
|
)
|
|
if test.expectFailureNoPath {
|
|
if err == nil {
|
|
t.Fatal("expected no path to be found")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unable to find path: %v", err)
|
|
}
|
|
|
|
route, err := newRoute(
|
|
paymentAmt, test.feeLimit, sourceVertex, path, startingHeight,
|
|
finalHopCLTV,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unable to create path: %v", err)
|
|
}
|
|
|
|
if len(route.Hops) != len(expectedHops) {
|
|
t.Fatalf("route is of incorrect length, expected %v got %v",
|
|
expectedHopCount, len(route.Hops))
|
|
}
|
|
|
|
// Check hop nodes
|
|
for i := 0; i < len(expectedHops); i++ {
|
|
if !bytes.Equal(route.Hops[i].Channel.Node.PubKeyBytes[:],
|
|
aliases[expectedHops[i].alias].SerializeCompressed()) {
|
|
|
|
t.Fatalf("%v-th hop should be %v, is instead: %v",
|
|
i, expectedHops[i], route.Hops[i].Channel.Node.Alias)
|
|
}
|
|
}
|
|
|
|
// Next, we'll assert that the "next hop" field in each route payload
|
|
// properly points to the channel ID that the HTLC should be forwarded
|
|
// along.
|
|
hopPayloads := route.ToHopPayloads()
|
|
if len(hopPayloads) != expectedHopCount {
|
|
t.Fatalf("incorrect number of hop payloads: expected %v, got %v",
|
|
expectedHopCount, len(hopPayloads))
|
|
}
|
|
|
|
// Hops should point to the next hop
|
|
for i := 0; i < len(expectedHops)-1; i++ {
|
|
var expectedHop [8]byte
|
|
binary.BigEndian.PutUint64(expectedHop[:], route.Hops[i+1].Channel.ChannelID)
|
|
if !bytes.Equal(hopPayloads[i].NextAddress[:], expectedHop[:]) {
|
|
t.Fatalf("first hop has incorrect next hop: expected %x, got %x",
|
|
expectedHop[:], hopPayloads[i].NextAddress)
|
|
}
|
|
}
|
|
|
|
// The final hop should have a next hop value of all zeroes in order
|
|
// to indicate it's the exit hop.
|
|
var exitHop [8]byte
|
|
lastHopIndex := len(expectedHops) - 1
|
|
if !bytes.Equal(hopPayloads[lastHopIndex].NextAddress[:], exitHop[:]) {
|
|
t.Fatalf("first hop has incorrect next hop: expected %x, got %x",
|
|
exitHop[:], hopPayloads[lastHopIndex].NextAddress)
|
|
}
|
|
|
|
var expectedTotalFee lnwire.MilliSatoshi
|
|
for i := 0; i < expectedHopCount; i++ {
|
|
// We'll ensure that the amount to forward, and fees
|
|
// computed for each hop are correct.
|
|
|
|
if route.Hops[i].Fee != expectedHops[i].fee {
|
|
t.Fatalf("fee incorrect for hop %v: expected %v, got %v",
|
|
i, expectedHops[i].fee, route.Hops[i].Fee)
|
|
}
|
|
|
|
if route.Hops[i].AmtToForward != expectedHops[i].fwdAmount {
|
|
t.Fatalf("forwarding amount for hop %v incorrect: "+
|
|
"expected %v, got %v",
|
|
i, expectedHops[i].fwdAmount,
|
|
route.Hops[i].AmtToForward)
|
|
}
|
|
|
|
// We'll also assert that the outgoing CLTV value for each
|
|
// hop was set accordingly.
|
|
if route.Hops[i].OutgoingTimeLock != expectedHops[i].timeLock {
|
|
t.Fatalf("outgoing time-lock for hop %v is incorrect: "+
|
|
"expected %v, got %v", i,
|
|
expectedHops[i].timeLock,
|
|
route.Hops[i].OutgoingTimeLock)
|
|
}
|
|
|
|
expectedTotalFee += expectedHops[i].fee
|
|
}
|
|
|
|
if route.TotalAmount != test.expectedTotalAmt {
|
|
t.Fatalf("total amount incorrect: "+
|
|
"expected %v, got %v",
|
|
test.expectedTotalAmt, route.TotalAmount)
|
|
}
|
|
|
|
if route.TotalTimeLock != test.expectedTotalTimeLock {
|
|
t.Fatalf("expected time lock of %v, instead have %v", 2,
|
|
route.TotalTimeLock)
|
|
}
|
|
|
|
// The next and prev hop maps should be properly set.
|
|
for i := 0; i < expectedHopCount; i++ {
|
|
prevChan, ok := route.prevHopChannel(aliases[expectedHops[i].alias])
|
|
if !ok {
|
|
t.Fatalf("hop didn't have prev chan but should have")
|
|
}
|
|
if prevChan.ChannelID != route.Hops[i].Channel.ChannelID {
|
|
t.Fatalf("incorrect prev chan: expected %v, got %v",
|
|
prevChan.ChannelID, route.Hops[i].Channel.ChannelID)
|
|
}
|
|
}
|
|
|
|
for i := 0; i < expectedHopCount-1; i++ {
|
|
nextChan, ok := route.nextHopChannel(aliases[expectedHops[i].alias])
|
|
if !ok {
|
|
t.Fatalf("hop didn't have prev chan but should have")
|
|
}
|
|
if nextChan.ChannelID != route.Hops[i+1].Channel.ChannelID {
|
|
t.Fatalf("incorrect prev chan: expected %v, got %v",
|
|
nextChan.ChannelID, route.Hops[i+1].Channel.ChannelID)
|
|
}
|
|
}
|
|
|
|
// Final hop shouldn't have a next chan
|
|
if _, ok := route.nextHopChannel(aliases[expectedHops[lastHopIndex].alias]); ok {
|
|
t.Fatalf("incorrect next hop map, no vertexes should " +
|
|
"be after sophon")
|
|
}
|
|
}
|
|
|
|
func TestPathFindingWithAdditionalEdges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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)
|
|
}
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
|
|
// In this test, we'll test that we're able to find paths through
|
|
// private channels when providing them as additional edges in our path
|
|
// finding algorithm. To do so, we'll create a new node, doge, and
|
|
// create a private channel between it and songoku. We'll then attempt
|
|
// to find a path from our source node, roasbeef, to doge.
|
|
dogePubKeyHex := "03dd46ff29a6941b4a2607525b043ec9b020b3f318a1bf281536fd7011ec59c882"
|
|
dogePubKeyBytes, err := hex.DecodeString(dogePubKeyHex)
|
|
if err != nil {
|
|
t.Fatalf("unable to decode public key: %v", err)
|
|
}
|
|
dogePubKey, err := btcec.ParsePubKey(dogePubKeyBytes, btcec.S256())
|
|
if err != nil {
|
|
t.Fatalf("unable to parse public key from bytes: %v", err)
|
|
}
|
|
|
|
doge := &channeldb.LightningNode{}
|
|
doge.AddPubKey(dogePubKey)
|
|
doge.Alias = "doge"
|
|
|
|
// Create the channel edge going from songoku to doge and include it in
|
|
// our map of additional edges.
|
|
songokuToDoge := &channeldb.ChannelEdgePolicy{
|
|
Node: doge,
|
|
ChannelID: 1337,
|
|
FeeBaseMSat: 1,
|
|
FeeProportionalMillionths: 1000,
|
|
TimeLockDelta: 9,
|
|
}
|
|
|
|
additionalEdges := map[Vertex][]*channeldb.ChannelEdgePolicy{
|
|
NewVertex(aliases["songoku"]): {songokuToDoge},
|
|
}
|
|
|
|
// We should now be able to find a path from roasbeef to doge.
|
|
path, err := findPath(
|
|
nil, graph, additionalEdges, sourceNode, dogePubKey, nil, nil,
|
|
paymentAmt, noFeeLimit, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unable to find private path to doge: %v", err)
|
|
}
|
|
|
|
// The path should represent the following hops:
|
|
// roasbeef -> songoku -> doge
|
|
assertExpectedPath(t, path, "songoku", "doge")
|
|
}
|
|
|
|
func TestKShortestPathFinding(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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 algorithm to find the
|
|
// k-shortest paths from a given source node to any destination node
|
|
// works as expected.
|
|
|
|
// 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".
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
target := aliases["luoji"]
|
|
paths, err := findPaths(
|
|
nil, graph, sourceNode, target, paymentAmt, noFeeLimit, 100,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unable to find paths between roasbeef and "+
|
|
"luo ji: %v", err)
|
|
}
|
|
|
|
// The algorithm should have 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))
|
|
}
|
|
|
|
// Additionally, 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")
|
|
}
|
|
|
|
// The first route should be a direct route to luo ji.
|
|
assertExpectedPath(t, paths[0], "roasbeef", "luoji")
|
|
|
|
// The second route should be a route to luo ji via satoshi.
|
|
assertExpectedPath(t, paths[1], "roasbeef", "satoshi", "luoji")
|
|
}
|
|
|
|
// TestNewRoute tests whether the construction of hop payloads by newRoute
|
|
// is executed correctly.
|
|
func TestNewRoute(t *testing.T) {
|
|
|
|
var sourceKey [33]byte
|
|
sourceVertex := Vertex(sourceKey)
|
|
|
|
const (
|
|
startingHeight = 100
|
|
finalHopCLTV = 1
|
|
)
|
|
|
|
createHop := func(baseFee lnwire.MilliSatoshi,
|
|
feeRate lnwire.MilliSatoshi,
|
|
bandwidth lnwire.MilliSatoshi,
|
|
timeLockDelta uint16) *ChannelHop {
|
|
|
|
return &ChannelHop{
|
|
ChannelEdgePolicy: &channeldb.ChannelEdgePolicy{
|
|
Node: &channeldb.LightningNode{},
|
|
FeeProportionalMillionths: feeRate,
|
|
FeeBaseMSat: baseFee,
|
|
TimeLockDelta: timeLockDelta,
|
|
},
|
|
Bandwidth: bandwidth,
|
|
}
|
|
}
|
|
|
|
testCases := []struct {
|
|
// name identifies the test case in the test output.
|
|
name string
|
|
|
|
// hops is the list of hops (the route) that gets passed into
|
|
// the call to newRoute.
|
|
hops []*ChannelHop
|
|
|
|
// paymentAmount is the amount that is send into the route
|
|
// indicated by hops.
|
|
paymentAmount lnwire.MilliSatoshi
|
|
|
|
// expectedFees is a list of fees that every hop is expected
|
|
// to charge for forwarding.
|
|
expectedFees []lnwire.MilliSatoshi
|
|
|
|
// expectedTimeLocks is a list of time lock values that every
|
|
// hop is expected to specify in its outgoing HTLC. The time
|
|
// lock values in this list are relative to the current block
|
|
// height.
|
|
expectedTimeLocks []uint32
|
|
|
|
// expectedTotalAmount is the total amount that is expected to
|
|
// be returned from newRoute. This amount should include all
|
|
// the fees to be paid to intermediate hops.
|
|
expectedTotalAmount lnwire.MilliSatoshi
|
|
|
|
// expectedTotalTimeLock is the time lock that is expected to
|
|
// be returned from newRoute. This is the time lock that should
|
|
// be specified in the HTLC that is sent by the source node.
|
|
// expectedTotalTimeLock is relative to the current block height.
|
|
expectedTotalTimeLock uint32
|
|
|
|
// expectError indicates whether the newRoute call is expected
|
|
// to fail or succeed.
|
|
expectError bool
|
|
|
|
// expectedErrorCode indicates the expected error code when
|
|
// expectError is true.
|
|
expectedErrorCode errorCode
|
|
|
|
feeLimit lnwire.MilliSatoshi
|
|
}{
|
|
{
|
|
// For a single hop payment, no fees are expected to be paid.
|
|
name: "single hop",
|
|
paymentAmount: 100000,
|
|
hops: []*ChannelHop{
|
|
createHop(100, 1000, 1000000, 10),
|
|
},
|
|
expectedFees: []lnwire.MilliSatoshi{0},
|
|
expectedTimeLocks: []uint32{1},
|
|
expectedTotalAmount: 100000,
|
|
expectedTotalTimeLock: 1,
|
|
feeLimit: noFeeLimit,
|
|
}, {
|
|
// For a two hop payment, only the fee for the first hop
|
|
// needs to be paid. The destination hop does not require
|
|
// a fee to receive the payment.
|
|
name: "two hop",
|
|
paymentAmount: 100000,
|
|
hops: []*ChannelHop{
|
|
createHop(0, 1000, 1000000, 10),
|
|
createHop(30, 1000, 1000000, 5),
|
|
},
|
|
expectedFees: []lnwire.MilliSatoshi{130, 0},
|
|
expectedTimeLocks: []uint32{1, 1},
|
|
expectedTotalAmount: 100130,
|
|
expectedTotalTimeLock: 6,
|
|
feeLimit: noFeeLimit,
|
|
}, {
|
|
// Insufficient capacity in first channel when fees are added.
|
|
name: "two hop insufficient",
|
|
paymentAmount: 100000,
|
|
hops: []*ChannelHop{
|
|
createHop(0, 1000, 100000, 10),
|
|
createHop(0, 1000, 1000000, 5),
|
|
},
|
|
feeLimit: noFeeLimit,
|
|
expectError: true,
|
|
expectedErrorCode: ErrInsufficientCapacity,
|
|
}, {
|
|
// A three hop payment where the first and second hop
|
|
// will both charge 1 msat. The fee for the first hop
|
|
// is actually slightly higher than 1, because the amount
|
|
// to forward also includes the fee for the second hop. This
|
|
// gets rounded down to 1.
|
|
name: "three hop",
|
|
paymentAmount: 100000,
|
|
hops: []*ChannelHop{
|
|
createHop(0, 10, 1000000, 10),
|
|
createHop(0, 10, 1000000, 5),
|
|
createHop(0, 10, 1000000, 3),
|
|
},
|
|
expectedFees: []lnwire.MilliSatoshi{1, 1, 0},
|
|
expectedTotalAmount: 100002,
|
|
expectedTimeLocks: []uint32{4, 1, 1},
|
|
expectedTotalTimeLock: 9,
|
|
feeLimit: noFeeLimit,
|
|
}, {
|
|
// A three hop payment where the fee of the first hop
|
|
// is slightly higher (11) than the fee at the second hop,
|
|
// because of the increase amount to forward.
|
|
name: "three hop with fee carry over",
|
|
paymentAmount: 100000,
|
|
hops: []*ChannelHop{
|
|
createHop(0, 10000, 1000000, 10),
|
|
createHop(0, 10000, 1000000, 5),
|
|
createHop(0, 10000, 1000000, 3),
|
|
},
|
|
expectedFees: []lnwire.MilliSatoshi{1010, 1000, 0},
|
|
expectedTotalAmount: 102010,
|
|
expectedTimeLocks: []uint32{4, 1, 1},
|
|
expectedTotalTimeLock: 9,
|
|
feeLimit: noFeeLimit,
|
|
}, {
|
|
// A three hop payment where the fee policies of the first and
|
|
// second hop are just high enough to show the fee carry over
|
|
// effect.
|
|
name: "three hop with minimal fees for carry over",
|
|
paymentAmount: 100000,
|
|
hops: []*ChannelHop{
|
|
createHop(0, 10000, 1000000, 10),
|
|
|
|
// First hop charges 0.1% so the second hop fee
|
|
// should show up in the first hop fee as 1 msat
|
|
// extra.
|
|
createHop(0, 1000, 1000000, 5),
|
|
|
|
// Second hop charges a fixed 1000 msat.
|
|
createHop(1000, 0, 1000000, 3),
|
|
},
|
|
expectedFees: []lnwire.MilliSatoshi{101, 1000, 0},
|
|
expectedTotalAmount: 101101,
|
|
expectedTimeLocks: []uint32{4, 1, 1},
|
|
expectedTotalTimeLock: 9,
|
|
feeLimit: noFeeLimit,
|
|
},
|
|
// Check fee limit behaviour
|
|
{
|
|
name: "two hop success with fee limit (greater)",
|
|
paymentAmount: 100000,
|
|
hops: []*ChannelHop{
|
|
createHop(0, 1000, 1000000, 144),
|
|
createHop(0, 1000, 1000000, 144),
|
|
},
|
|
expectedTotalAmount: 100100,
|
|
expectedFees: []lnwire.MilliSatoshi{100, 0},
|
|
expectedTimeLocks: []uint32{1, 1},
|
|
expectedTotalTimeLock: 145,
|
|
feeLimit: 150,
|
|
}, {
|
|
name: "two hop success with fee limit (equal)",
|
|
paymentAmount: 100000,
|
|
hops: []*ChannelHop{
|
|
createHop(0, 1000, 1000000, 144),
|
|
createHop(0, 1000, 1000000, 144),
|
|
},
|
|
expectedTotalAmount: 100100,
|
|
expectedFees: []lnwire.MilliSatoshi{100, 0},
|
|
expectedTimeLocks: []uint32{1, 1},
|
|
expectedTotalTimeLock: 145,
|
|
feeLimit: 100,
|
|
}, {
|
|
name: "two hop failure with fee limit (smaller)",
|
|
paymentAmount: 100000,
|
|
hops: []*ChannelHop{
|
|
createHop(0, 1000, 1000000, 144),
|
|
createHop(0, 1000, 1000000, 144),
|
|
},
|
|
feeLimit: 50,
|
|
expectError: true,
|
|
expectedErrorCode: ErrFeeLimitExceeded,
|
|
}, {
|
|
name: "two hop failure with fee limit (zero)",
|
|
paymentAmount: 100000,
|
|
hops: []*ChannelHop{
|
|
createHop(0, 1000, 1000000, 144),
|
|
createHop(0, 1000, 1000000, 144),
|
|
},
|
|
feeLimit: 0,
|
|
expectError: true,
|
|
expectedErrorCode: ErrFeeLimitExceeded,
|
|
}}
|
|
|
|
for _, testCase := range testCases {
|
|
assertRoute := func(t *testing.T, route *Route) {
|
|
if route.TotalAmount != testCase.expectedTotalAmount {
|
|
t.Errorf("Expected total amount is be %v"+
|
|
", but got %v instead",
|
|
testCase.expectedTotalAmount,
|
|
route.TotalAmount)
|
|
}
|
|
|
|
for i := 0; i < len(testCase.expectedFees); i++ {
|
|
if testCase.expectedFees[i] !=
|
|
route.Hops[i].Fee {
|
|
|
|
t.Errorf("Expected fee for hop %v to "+
|
|
"be %v, but got %v instead",
|
|
i, testCase.expectedFees[i],
|
|
route.Hops[i].Fee)
|
|
}
|
|
}
|
|
|
|
expectedTimeLockHeight := startingHeight +
|
|
testCase.expectedTotalTimeLock
|
|
|
|
if route.TotalTimeLock != expectedTimeLockHeight {
|
|
|
|
t.Errorf("Expected total time lock to be %v"+
|
|
", but got %v instead",
|
|
expectedTimeLockHeight,
|
|
route.TotalTimeLock)
|
|
}
|
|
|
|
for i := 0; i < len(testCase.expectedTimeLocks); i++ {
|
|
expectedTimeLockHeight := startingHeight +
|
|
testCase.expectedTimeLocks[i]
|
|
|
|
if expectedTimeLockHeight !=
|
|
route.Hops[i].OutgoingTimeLock {
|
|
|
|
t.Errorf("Expected time lock for hop "+
|
|
"%v to be %v, but got %v instead",
|
|
i, expectedTimeLockHeight,
|
|
route.Hops[i].OutgoingTimeLock)
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
route, err := newRoute(testCase.paymentAmount,
|
|
testCase.feeLimit,
|
|
sourceVertex, testCase.hops, startingHeight,
|
|
finalHopCLTV)
|
|
|
|
if testCase.expectError {
|
|
expectedCode := testCase.expectedErrorCode
|
|
if err == nil || !IsError(err, expectedCode) {
|
|
t.Fatalf("expected newRoute to fail "+
|
|
"with error code %v but got "+
|
|
"%v instead",
|
|
expectedCode, err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unable to create path: %v", err)
|
|
return
|
|
}
|
|
|
|
assertRoute(t, route)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewRoutePathTooLong(t *testing.T) {
|
|
t.Skip()
|
|
|
|
// 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{})
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
|
|
// We start by confirming 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(
|
|
nil, graph, nil, sourceNode, target, ignoredVertexes,
|
|
ignoredEdges, paymentAmt, noFeeLimit, nil,
|
|
)
|
|
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(
|
|
nil, graph, nil, sourceNode, target, ignoredVertexes,
|
|
ignoredEdges, paymentAmt, noFeeLimit, nil,
|
|
)
|
|
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) {
|
|
t.Parallel()
|
|
|
|
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(
|
|
nil, graph, nil, sourceNode, unknownNode, ignoredVertexes,
|
|
ignoredEdges, 100, noFeeLimit, nil,
|
|
)
|
|
if !IsError(err, ErrNoPathFound) {
|
|
t.Fatalf("path shouldn't have been found: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPathInsufficientCapacity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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"]
|
|
|
|
payAmt := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
|
|
_, err = findPath(
|
|
nil, graph, nil, sourceNode, target, ignoredVertexes,
|
|
ignoredEdges, payAmt, noFeeLimit, nil,
|
|
)
|
|
if !IsError(err, ErrNoPathFound) {
|
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestRouteFailMinHTLC tests that if we attempt to route an HTLC which is
|
|
// smaller than the advertised minHTLC of an edge, then path finding fails.
|
|
func TestRouteFailMinHTLC(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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{})
|
|
|
|
// We'll not attempt to route an HTLC of 10 SAT from roasbeef to Son
|
|
// Goku. However, the min HTLC of Son Goku is 1k SAT, as a result, this
|
|
// attempt should fail.
|
|
target := aliases["songoku"]
|
|
payAmt := lnwire.MilliSatoshi(10)
|
|
_, err = findPath(
|
|
nil, graph, nil, sourceNode, target, ignoredVertexes,
|
|
ignoredEdges, payAmt, noFeeLimit, nil,
|
|
)
|
|
if !IsError(err, ErrNoPathFound) {
|
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestRouteFailDisabledEdge tests that if we attempt to route to an edge
|
|
// that's disabled, then that edge is disqualified, and the routing attempt
|
|
// will fail.
|
|
func TestRouteFailDisabledEdge(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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{})
|
|
|
|
// First, we'll try to route from roasbeef -> sophon. This should
|
|
// succeed without issue, and return a single path via phamnuwen
|
|
target := aliases["sophon"]
|
|
payAmt := lnwire.NewMSatFromSatoshis(105000)
|
|
_, err = findPath(
|
|
nil, graph, nil, sourceNode, target, ignoredVertexes,
|
|
ignoredEdges, payAmt, noFeeLimit, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unable to find path: %v", err)
|
|
}
|
|
|
|
// First, we'll modify the edge from roasbeef -> phamnuwen, to read that
|
|
// it's disabled.
|
|
_, _, phamnuwenEdge, err := graph.FetchChannelEdgesByID(999991)
|
|
if err != nil {
|
|
t.Fatalf("unable to fetch goku's edge: %v", err)
|
|
}
|
|
phamnuwenEdge.Flags = lnwire.ChanUpdateDisabled | lnwire.ChanUpdateDirection
|
|
if err := graph.UpdateEdgePolicy(phamnuwenEdge); err != nil {
|
|
t.Fatalf("unable to update edge: %v", err)
|
|
}
|
|
|
|
// Now, if we attempt to route through that edge, we should get a
|
|
// failure as it is no longer eligible.
|
|
_, err = findPath(
|
|
nil, graph, nil, sourceNode, target, ignoredVertexes,
|
|
ignoredEdges, payAmt, noFeeLimit, nil,
|
|
)
|
|
if !IsError(err, ErrNoPathFound) {
|
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPathInsufficientCapacityWithFee(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// 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
|
|
}
|
|
|
|
func TestPathFindSpecExample(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// All our path finding tests will assume a starting height of 100, so
|
|
// we'll pass that in to ensure that the router uses 100 as the current
|
|
// height.
|
|
const startingHeight = 100
|
|
ctx, cleanUp, err := createTestCtx(startingHeight, specExampleFilePath)
|
|
defer cleanUp()
|
|
if err != nil {
|
|
t.Fatalf("unable to create router: %v", err)
|
|
}
|
|
|
|
const (
|
|
aliceFinalCLTV = 10
|
|
bobFinalCLTV = 20
|
|
carolFinalCLTV = 30
|
|
daveFinalCLTV = 40
|
|
)
|
|
|
|
// We'll first exercise the scenario of a direct payment from Bob to
|
|
// Carol, so we set "B" as the source node so path finding starts from
|
|
// Bob.
|
|
bob := ctx.aliases["B"]
|
|
bobNode, err := ctx.graph.FetchLightningNode(bob)
|
|
if err != nil {
|
|
t.Fatalf("unable to find bob: %v", err)
|
|
}
|
|
if err := ctx.graph.SetSourceNode(bobNode); err != nil {
|
|
t.Fatalf("unable to set source node: %v", err)
|
|
}
|
|
|
|
// Query for a route of 4,999,999 mSAT to carol.
|
|
carol := ctx.aliases["C"]
|
|
const amt lnwire.MilliSatoshi = 4999999
|
|
routes, err := ctx.router.FindRoutes(carol, amt, noFeeLimit, 100)
|
|
if err != nil {
|
|
t.Fatalf("unable to find route: %v", err)
|
|
}
|
|
|
|
// We should come back with _exactly_ two routes.
|
|
if len(routes) != 2 {
|
|
t.Fatalf("expected %v routes, instead have: %v", 2,
|
|
len(routes))
|
|
}
|
|
|
|
// Now we'll examine the first route returned for correctness.
|
|
//
|
|
// It should be sending the exact payment amount as there are no
|
|
// additional hops.
|
|
firstRoute := routes[0]
|
|
if firstRoute.TotalAmount != amt {
|
|
t.Fatalf("wrong total amount: got %v, expected %v",
|
|
firstRoute.TotalAmount, amt)
|
|
}
|
|
if firstRoute.Hops[0].AmtToForward != amt {
|
|
t.Fatalf("wrong forward amount: got %v, expected %v",
|
|
firstRoute.Hops[0].AmtToForward, amt)
|
|
}
|
|
if firstRoute.Hops[0].Fee != 0 {
|
|
t.Fatalf("wrong hop fee: got %v, expected %v",
|
|
firstRoute.Hops[0].Fee, 0)
|
|
}
|
|
|
|
// The CLTV expiry should be the current height plus 9 (the expiry for
|
|
// the B -> C channel.
|
|
if firstRoute.TotalTimeLock !=
|
|
startingHeight+DefaultFinalCLTVDelta {
|
|
|
|
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
|
firstRoute.TotalTimeLock,
|
|
startingHeight+DefaultFinalCLTVDelta)
|
|
}
|
|
|
|
// Next, we'll set A as the source node so we can assert that we create
|
|
// the proper route for any queries starting with Alice.
|
|
alice := ctx.aliases["A"]
|
|
aliceNode, err := ctx.graph.FetchLightningNode(alice)
|
|
if err != nil {
|
|
t.Fatalf("unable to find alice: %v", err)
|
|
}
|
|
if err := ctx.graph.SetSourceNode(aliceNode); err != nil {
|
|
t.Fatalf("unable to set source node: %v", err)
|
|
}
|
|
ctx.router.selfNode = aliceNode
|
|
source, err := ctx.graph.SourceNode()
|
|
if err != nil {
|
|
t.Fatalf("unable to retrieve source node: %v", err)
|
|
}
|
|
if !bytes.Equal(source.PubKeyBytes[:], alice.SerializeCompressed()) {
|
|
t.Fatalf("source node not set")
|
|
}
|
|
|
|
// We'll now request a route from A -> B -> C.
|
|
ctx.router.routeCache = make(map[routeTuple][]*Route)
|
|
routes, err = ctx.router.FindRoutes(carol, amt, noFeeLimit, 100)
|
|
if err != nil {
|
|
t.Fatalf("unable to find routes: %v", err)
|
|
}
|
|
|
|
// We should come back with _exactly_ two routes.
|
|
if len(routes) != 2 {
|
|
t.Fatalf("expected %v routes, instead have: %v", 2,
|
|
len(routes))
|
|
}
|
|
|
|
// Both routes should be two hops.
|
|
if len(routes[0].Hops) != 2 {
|
|
t.Fatalf("route should be %v hops, is instead %v", 2,
|
|
len(routes[0].Hops))
|
|
}
|
|
if len(routes[1].Hops) != 2 {
|
|
t.Fatalf("route should be %v hops, is instead %v", 2,
|
|
len(routes[1].Hops))
|
|
}
|
|
|
|
// The total amount should factor in a fee of 10199 and also use a CLTV
|
|
// delta total of 29 (20 + 9),
|
|
expectedAmt := lnwire.MilliSatoshi(5010198)
|
|
if routes[0].TotalAmount != expectedAmt {
|
|
t.Fatalf("wrong amount: got %v, expected %v",
|
|
routes[0].TotalAmount, expectedAmt)
|
|
}
|
|
if routes[0].TotalTimeLock != startingHeight+29 {
|
|
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
|
routes[0].TotalTimeLock, startingHeight+29)
|
|
}
|
|
|
|
// Ensure that the hops of the first route are properly crafted.
|
|
//
|
|
// After taking the fee, Bob should be forwarding the remainder which
|
|
// is the exact payment to Bob.
|
|
if routes[0].Hops[0].AmtToForward != amt {
|
|
t.Fatalf("wrong forward amount: got %v, expected %v",
|
|
routes[0].Hops[0].AmtToForward, amt)
|
|
}
|
|
|
|
// We shouldn't pay any fee for the first, hop, but the fee for the
|
|
// second hop posted fee should be exactly:
|
|
|
|
// The fee that we pay for the second hop will be "applied to the first
|
|
// hop, so we should get a fee of exactly:
|
|
//
|
|
// * 200 + 4999999 * 2000 / 1000000 = 10199
|
|
if routes[0].Hops[0].Fee != 10199 {
|
|
t.Fatalf("wrong hop fee: got %v, expected %v",
|
|
routes[0].Hops[0].Fee, 10199)
|
|
}
|
|
|
|
// While for the final hop, as there's no additional hop afterwards, we
|
|
// pay no fee.
|
|
if routes[0].Hops[1].Fee != 0 {
|
|
t.Fatalf("wrong hop fee: got %v, expected %v",
|
|
routes[0].Hops[0].Fee, 0)
|
|
}
|
|
|
|
// The outgoing CLTV value itself should be the current height plus 30
|
|
// to meet Carol's requirements.
|
|
if routes[0].Hops[0].OutgoingTimeLock !=
|
|
startingHeight+DefaultFinalCLTVDelta {
|
|
|
|
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
|
routes[0].Hops[0].OutgoingTimeLock,
|
|
startingHeight+DefaultFinalCLTVDelta)
|
|
}
|
|
|
|
// For B -> C, we assert that the final hop also has the proper
|
|
// parameters.
|
|
lastHop := routes[0].Hops[1]
|
|
if lastHop.AmtToForward != amt {
|
|
t.Fatalf("wrong forward amount: got %v, expected %v",
|
|
lastHop.AmtToForward, amt)
|
|
}
|
|
if lastHop.OutgoingTimeLock !=
|
|
startingHeight+DefaultFinalCLTVDelta {
|
|
|
|
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
|
lastHop.OutgoingTimeLock,
|
|
startingHeight+DefaultFinalCLTVDelta)
|
|
}
|
|
|
|
// We'll also make similar assertions for the second route from A to C
|
|
// via D.
|
|
secondRoute := routes[1]
|
|
expectedAmt = 5020398
|
|
if secondRoute.TotalAmount != expectedAmt {
|
|
t.Fatalf("wrong amount: got %v, expected %v",
|
|
secondRoute.TotalAmount, expectedAmt)
|
|
}
|
|
expectedTimeLock := startingHeight + daveFinalCLTV + DefaultFinalCLTVDelta
|
|
if secondRoute.TotalTimeLock != uint32(expectedTimeLock) {
|
|
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
|
secondRoute.TotalTimeLock, expectedTimeLock)
|
|
}
|
|
onionPayload := secondRoute.Hops[0]
|
|
if onionPayload.AmtToForward != amt {
|
|
t.Fatalf("wrong forward amount: got %v, expected %v",
|
|
onionPayload.AmtToForward, amt)
|
|
}
|
|
expectedTimeLock = startingHeight + DefaultFinalCLTVDelta
|
|
if onionPayload.OutgoingTimeLock != uint32(expectedTimeLock) {
|
|
t.Fatalf("wrong outgoing time lock: got %v, expecting %v",
|
|
onionPayload.OutgoingTimeLock,
|
|
expectedTimeLock)
|
|
}
|
|
|
|
// The B -> C hop should also be identical as the prior cases.
|
|
lastHop = secondRoute.Hops[1]
|
|
if lastHop.AmtToForward != amt {
|
|
t.Fatalf("wrong forward amount: got %v, expected %v",
|
|
lastHop.AmtToForward, amt)
|
|
}
|
|
if lastHop.OutgoingTimeLock !=
|
|
startingHeight+DefaultFinalCLTVDelta {
|
|
|
|
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
|
lastHop.OutgoingTimeLock,
|
|
startingHeight+DefaultFinalCLTVDelta)
|
|
}
|
|
}
|
|
|
|
func assertExpectedPath(t *testing.T, path []*ChannelHop, nodeAliases ...string) {
|
|
if len(path) != len(nodeAliases) {
|
|
t.Fatal("number of hops and number of aliases do not match")
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|