mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-20 02:27:21 +01:00
1039aedd0c
This commit adds a new function, `findBlindedPaths`, that does a depth first search from the target node to find a set of blinded paths to the target node given the set of restrictions. This function will select and return any candidate path. A candidate path is a path to the target node with a size determined by the given hop number constraints where all the nodes on the path signal the route blinding feature _and_ the introduction node for the path has more than one public channel. Any filtering of paths based on payment value or success probabilities is left to the caller.
3695 lines
109 KiB
Go
3695 lines
109 KiB
Go
package routing
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/wire"
|
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/channeldb/models"
|
|
"github.com/lightningnetwork/lnd/htlcswitch"
|
|
switchhop "github.com/lightningnetwork/lnd/htlcswitch/hop"
|
|
"github.com/lightningnetwork/lnd/kvdb"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/record"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
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"
|
|
|
|
// 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 (
|
|
noRestrictions = &RestrictParams{
|
|
FeeLimit: noFeeLimit,
|
|
ProbabilitySource: noProbabilitySource,
|
|
CltvLimit: math.MaxUint32,
|
|
}
|
|
|
|
testPathFindingConfig = &PathFindingConfig{}
|
|
|
|
tlvFeatures = lnwire.NewFeatureVector(
|
|
lnwire.NewRawFeatureVector(
|
|
lnwire.TLVOnionPayloadRequired,
|
|
), lnwire.Features,
|
|
)
|
|
|
|
payAddrFeatures = lnwire.NewFeatureVector(
|
|
lnwire.NewRawFeatureVector(
|
|
lnwire.PaymentAddrOptional,
|
|
), lnwire.Features,
|
|
)
|
|
|
|
tlvPayAddrFeatures = lnwire.NewFeatureVector(
|
|
lnwire.NewRawFeatureVector(
|
|
lnwire.TLVOnionPayloadRequired,
|
|
lnwire.PaymentAddrOptional,
|
|
), lnwire.Features,
|
|
)
|
|
|
|
mppFeatures = lnwire.NewRawFeatureVector(
|
|
lnwire.TLVOnionPayloadRequired,
|
|
lnwire.PaymentAddrOptional,
|
|
lnwire.MPPOptional,
|
|
)
|
|
|
|
unknownRequiredFeatures = lnwire.NewFeatureVector(
|
|
lnwire.NewRawFeatureVector(100), lnwire.Features,
|
|
)
|
|
)
|
|
|
|
var (
|
|
testRBytes, _ = hex.DecodeString("8ce2bc69281ce27da07e6683571319d18e949ddfa2965fb6caa1bf0314f882d7")
|
|
testSBytes, _ = hex.DecodeString("299105481d63e0f4bc2a88121167221b6700d72a0ead154c03be696a292d24ae")
|
|
testRScalar = new(btcec.ModNScalar)
|
|
testSScalar = new(btcec.ModNScalar)
|
|
_ = testRScalar.SetByteSlice(testRBytes)
|
|
_ = testSScalar.SetByteSlice(testSBytes)
|
|
testSig = ecdsa.NewSignature(testRScalar, testSScalar)
|
|
|
|
testAuthProof = models.ChannelAuthProof{
|
|
NodeSig1Bytes: testSig.Serialize(),
|
|
NodeSig2Bytes: testSig.Serialize(),
|
|
BitcoinSig1Bytes: testSig.Serialize(),
|
|
BitcoinSig2Bytes: testSig.Serialize(),
|
|
}
|
|
)
|
|
|
|
// noProbabilitySource is used in testing to return the same probability 1 for
|
|
// all edges.
|
|
func noProbabilitySource(route.Vertex, route.Vertex, lnwire.MilliSatoshi,
|
|
btcutil.Amount) float64 {
|
|
|
|
return 1
|
|
}
|
|
|
|
// 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. Private keys are optional. If set, they should be consistent
|
|
// with the public key. The private key is used to sign error messages
|
|
// sent from the node.
|
|
type testNode struct {
|
|
Source bool `json:"source"`
|
|
PubKey string `json:"pubkey"`
|
|
PrivKey string `json:"privkey"`
|
|
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"`
|
|
ChannelFlags uint8 `json:"channel_flags"`
|
|
MessageFlags uint8 `json:"message_flags"`
|
|
Expiry uint16 `json:"expiry"`
|
|
MinHTLC int64 `json:"min_htlc"`
|
|
MaxHTLC int64 `json:"max_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.
|
|
func makeTestGraph(t *testing.T, useCache bool) (*channeldb.ChannelGraph,
|
|
kvdb.Backend, error) {
|
|
|
|
// Create channelgraph for the first time.
|
|
backend, backendCleanup, err := kvdb.GetTestBackend(t.TempDir(), "cgr")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
t.Cleanup(backendCleanup)
|
|
|
|
opts := channeldb.DefaultOptions()
|
|
graph, err := channeldb.NewChannelGraph(
|
|
backend, opts.RejectCacheSize, opts.ChannelCacheSize,
|
|
opts.BatchCommitInterval, opts.PreAllocCacheNumNodes,
|
|
useCache, false,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return graph, backend, nil
|
|
}
|
|
|
|
// parseTestGraph returns a fully populated ChannelGraph given a path to a JSON
|
|
// file which encodes a test graph.
|
|
func parseTestGraph(t *testing.T, useCache bool, path string) (
|
|
*testGraphInstance, error) {
|
|
|
|
graphJSON, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return 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, 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, err
|
|
}
|
|
testAddrs = append(testAddrs, testAddr)
|
|
|
|
// Next, create a temporary graph database for usage within the test.
|
|
graph, graphBackend, err := makeTestGraph(t, useCache)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
aliasMap := make(map[string]route.Vertex)
|
|
privKeyMap := make(map[string]*btcec.PrivateKey)
|
|
channelIDs := make(map[route.Vertex]map[route.Vertex]uint64)
|
|
links := make(map[lnwire.ShortChannelID]htlcswitch.ChannelLink)
|
|
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, err
|
|
}
|
|
|
|
dbNode := &channeldb.LightningNode{
|
|
HaveNodeAnnouncement: true,
|
|
AuthSigBytes: testSig.Serialize(),
|
|
LastUpdate: testTime,
|
|
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, 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] = dbNode.PubKeyBytes
|
|
|
|
// private keys are needed for signing error messages. If set
|
|
// check the consistency with the public key.
|
|
privBytes, err := hex.DecodeString(node.PrivKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(privBytes) > 0 {
|
|
key, derivedPub := btcec.PrivKeyFromBytes(
|
|
privBytes,
|
|
)
|
|
|
|
if !bytes.Equal(
|
|
pubBytes, derivedPub.SerializeCompressed(),
|
|
) {
|
|
|
|
return nil, fmt.Errorf("%s public key and "+
|
|
"private key are inconsistent\n"+
|
|
"got %x\nwant %x\n",
|
|
node.Alias,
|
|
derivedPub.SerializeCompressed(),
|
|
pubBytes,
|
|
)
|
|
}
|
|
|
|
privKeyMap[node.Alias] = key
|
|
}
|
|
|
|
// 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, 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, err
|
|
}
|
|
}
|
|
|
|
if source != nil {
|
|
// Set the selected source node
|
|
if err := graph.SetSourceNode(source); err != nil {
|
|
return 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, err
|
|
}
|
|
|
|
node2Bytes, err := hex.DecodeString(edge.Node2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if bytes.Compare(node1Bytes, node2Bytes) == 1 {
|
|
return nil, fmt.Errorf(
|
|
"channel %v node order incorrect",
|
|
edge.ChannelID,
|
|
)
|
|
}
|
|
|
|
fundingTXID := strings.Split(edge.ChannelPoint, ":")[0]
|
|
txidBytes, err := chainhash.NewHashFromStr(fundingTXID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fundingPoint := wire.OutPoint{
|
|
Hash: *txidBytes,
|
|
Index: 0,
|
|
}
|
|
|
|
// We first insert the existence of the edge between the two
|
|
// nodes.
|
|
edgeInfo := models.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)
|
|
|
|
shortID := lnwire.NewShortChanIDFromInt(edge.ChannelID)
|
|
links[shortID] = &mockLink{
|
|
bandwidth: lnwire.MilliSatoshi(
|
|
edgeInfo.Capacity * 1000,
|
|
),
|
|
}
|
|
|
|
err = graph.AddChannelEdge(&edgeInfo)
|
|
if err != nil && err != channeldb.ErrEdgeAlreadyExist {
|
|
return nil, err
|
|
}
|
|
|
|
channelFlags := lnwire.ChanUpdateChanFlags(edge.ChannelFlags)
|
|
isUpdate1 := channelFlags&lnwire.ChanUpdateDirection == 0
|
|
targetNode := edgeInfo.NodeKey1Bytes
|
|
if isUpdate1 {
|
|
targetNode = edgeInfo.NodeKey2Bytes
|
|
}
|
|
|
|
edgePolicy := &models.ChannelEdgePolicy{
|
|
SigBytes: testSig.Serialize(),
|
|
MessageFlags: lnwire.ChanUpdateMsgFlags(edge.MessageFlags),
|
|
ChannelFlags: channelFlags,
|
|
ChannelID: edge.ChannelID,
|
|
LastUpdate: testTime,
|
|
TimeLockDelta: edge.Expiry,
|
|
MinHTLC: lnwire.MilliSatoshi(edge.MinHTLC),
|
|
MaxHTLC: lnwire.MilliSatoshi(edge.MaxHTLC),
|
|
FeeBaseMSat: lnwire.MilliSatoshi(edge.FeeBaseMsat),
|
|
FeeProportionalMillionths: lnwire.MilliSatoshi(edge.FeeRate),
|
|
ToNode: targetNode,
|
|
}
|
|
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We also store the channel IDs info for each of the node.
|
|
node1Vertex, err := route.NewVertexFromBytes(node1Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
node2Vertex, err := route.NewVertexFromBytes(node2Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, ok := channelIDs[node1Vertex]; !ok {
|
|
channelIDs[node1Vertex] = map[route.Vertex]uint64{}
|
|
}
|
|
channelIDs[node1Vertex][node2Vertex] = edge.ChannelID
|
|
|
|
if _, ok := channelIDs[node2Vertex]; !ok {
|
|
channelIDs[node2Vertex] = map[route.Vertex]uint64{}
|
|
}
|
|
channelIDs[node2Vertex][node1Vertex] = edge.ChannelID
|
|
}
|
|
|
|
return &testGraphInstance{
|
|
graph: graph,
|
|
graphBackend: graphBackend,
|
|
aliasMap: aliasMap,
|
|
privKeyMap: privKeyMap,
|
|
channelIDs: channelIDs,
|
|
links: links,
|
|
}, nil
|
|
}
|
|
|
|
type testChannelPolicy struct {
|
|
Expiry uint16
|
|
MinHTLC lnwire.MilliSatoshi
|
|
MaxHTLC lnwire.MilliSatoshi
|
|
FeeBaseMsat lnwire.MilliSatoshi
|
|
FeeRate lnwire.MilliSatoshi
|
|
InboundFeeBaseMsat int64
|
|
InboundFeeRate int64
|
|
LastUpdate time.Time
|
|
Disabled bool
|
|
Features *lnwire.FeatureVector
|
|
}
|
|
|
|
type testChannelEnd struct {
|
|
Alias string
|
|
*testChannelPolicy
|
|
}
|
|
|
|
func symmetricTestChannel(alias1, alias2 string, capacity btcutil.Amount,
|
|
policy *testChannelPolicy, chanID ...uint64) *testChannel {
|
|
|
|
// Leaving id zero will result in auto-generation of a channel id during
|
|
// graph construction.
|
|
var id uint64
|
|
if len(chanID) > 0 {
|
|
id = chanID[0]
|
|
}
|
|
|
|
policy2 := *policy
|
|
|
|
return asymmetricTestChannel(
|
|
alias1, alias2, capacity, policy, &policy2, id,
|
|
)
|
|
}
|
|
|
|
func asymmetricTestChannel(alias1, alias2 string, capacity btcutil.Amount,
|
|
policy1, policy2 *testChannelPolicy, id uint64) *testChannel {
|
|
|
|
return &testChannel{
|
|
Capacity: capacity,
|
|
Node1: &testChannelEnd{
|
|
Alias: alias1,
|
|
testChannelPolicy: policy1,
|
|
},
|
|
Node2: &testChannelEnd{
|
|
Alias: alias2,
|
|
testChannelPolicy: policy2,
|
|
},
|
|
ChannelID: id,
|
|
}
|
|
}
|
|
|
|
type testChannel struct {
|
|
Node1 *testChannelEnd
|
|
Node2 *testChannelEnd
|
|
Capacity btcutil.Amount
|
|
ChannelID uint64
|
|
}
|
|
|
|
type testGraphInstance struct {
|
|
graph *channeldb.ChannelGraph
|
|
graphBackend kvdb.Backend
|
|
|
|
// 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.
|
|
aliasMap map[string]route.Vertex
|
|
|
|
// privKeyMap maps a node alias to its private key. This is used to be
|
|
// able to mock a remote node's signing behaviour.
|
|
privKeyMap map[string]*btcec.PrivateKey
|
|
|
|
// channelIDs stores the channel ID for each node.
|
|
channelIDs map[route.Vertex]map[route.Vertex]uint64
|
|
|
|
// links maps channel ids to a mock channel update handler.
|
|
links map[lnwire.ShortChannelID]htlcswitch.ChannelLink
|
|
}
|
|
|
|
// getLink is a mocked link lookup function which looks up links in our test
|
|
// graph.
|
|
func (g *testGraphInstance) getLink(chanID lnwire.ShortChannelID) (
|
|
htlcswitch.ChannelLink, error) {
|
|
|
|
link, ok := g.links[chanID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("link not found in mock: %v", chanID)
|
|
}
|
|
|
|
return link, nil
|
|
}
|
|
|
|
// createTestGraphFromChannels returns a fully populated ChannelGraph based on a set of
|
|
// test channels. Additional required information like keys are derived in
|
|
// a deterministic 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 createTestGraphFromChannels(t *testing.T, useCache bool,
|
|
testChannels []*testChannel, source string,
|
|
sourceFeatureBits ...lnwire.FeatureBit) (*testGraphInstance, 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, err
|
|
}
|
|
testAddrs = append(testAddrs, testAddr)
|
|
|
|
// Next, create a temporary graph database for usage within the test.
|
|
graph, graphBackend, err := makeTestGraph(t, useCache)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
aliasMap := make(map[string]route.Vertex)
|
|
privKeyMap := make(map[string]*btcec.PrivateKey)
|
|
|
|
nodeIndex := byte(0)
|
|
addNodeWithAlias := func(alias string, features *lnwire.FeatureVector) (
|
|
*channeldb.LightningNode, error) {
|
|
|
|
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,
|
|
}
|
|
|
|
privKey, pubKey := btcec.PrivKeyFromBytes(keyBytes)
|
|
|
|
if features == nil {
|
|
features = lnwire.EmptyFeatureVector()
|
|
}
|
|
|
|
dbNode := &channeldb.LightningNode{
|
|
HaveNodeAnnouncement: true,
|
|
AuthSigBytes: testSig.Serialize(),
|
|
LastUpdate: testTime,
|
|
Addresses: testAddrs,
|
|
Alias: alias,
|
|
Features: features,
|
|
}
|
|
|
|
copy(dbNode.PubKeyBytes[:], pubKey.SerializeCompressed())
|
|
|
|
privKeyMap[alias] = privKey
|
|
|
|
// 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] = dbNode.PubKeyBytes
|
|
nodeIndex++
|
|
|
|
return dbNode, nil
|
|
}
|
|
|
|
// Add the source node.
|
|
dbNode, err := addNodeWithAlias(
|
|
source, lnwire.NewFeatureVector(
|
|
lnwire.NewRawFeatureVector(sourceFeatureBits...),
|
|
lnwire.Features,
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
if err = graph.SetSourceNode(dbNode); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Initialize variable that keeps track of the next channel id to assign
|
|
// if none is specified.
|
|
nextUnassignedChannelID := uint64(100000)
|
|
|
|
links := make(map[lnwire.ShortChannelID]htlcswitch.ChannelLink)
|
|
|
|
for _, testChannel := range testChannels {
|
|
for _, node := range []*testChannelEnd{
|
|
testChannel.Node1, testChannel.Node2,
|
|
} {
|
|
_, exists := aliasMap[node.Alias]
|
|
if !exists {
|
|
var features *lnwire.FeatureVector
|
|
if node.testChannelPolicy != nil {
|
|
features =
|
|
node.testChannelPolicy.Features
|
|
}
|
|
_, err := addNodeWithAlias(
|
|
node.Alias, features,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
channelID := testChannel.ChannelID
|
|
|
|
// If no channel id is specified, generate an id.
|
|
if channelID == 0 {
|
|
channelID = nextUnassignedChannelID
|
|
nextUnassignedChannelID++
|
|
}
|
|
|
|
var hash [sha256.Size]byte
|
|
hash[len(hash)-1] = byte(channelID)
|
|
|
|
fundingPoint := &wire.OutPoint{
|
|
Hash: chainhash.Hash(hash),
|
|
Index: 0,
|
|
}
|
|
|
|
capacity := lnwire.MilliSatoshi(testChannel.Capacity * 1000)
|
|
shortID := lnwire.NewShortChanIDFromInt(channelID)
|
|
links[shortID] = &mockLink{
|
|
bandwidth: capacity,
|
|
}
|
|
|
|
// Sort nodes
|
|
node1 := testChannel.Node1
|
|
node2 := testChannel.Node2
|
|
node1Vertex := aliasMap[node1.Alias]
|
|
node2Vertex := aliasMap[node2.Alias]
|
|
if bytes.Compare(node1Vertex[:], node2Vertex[:]) == 1 {
|
|
node1, node2 = node2, node1
|
|
node1Vertex, node2Vertex = node2Vertex, node1Vertex
|
|
}
|
|
|
|
// We first insert the existence of the edge between the two
|
|
// nodes.
|
|
edgeInfo := models.ChannelEdgeInfo{
|
|
ChannelID: channelID,
|
|
AuthProof: &testAuthProof,
|
|
ChannelPoint: *fundingPoint,
|
|
Capacity: testChannel.Capacity,
|
|
|
|
NodeKey1Bytes: node1Vertex,
|
|
BitcoinKey1Bytes: node1Vertex,
|
|
NodeKey2Bytes: node2Vertex,
|
|
BitcoinKey2Bytes: node2Vertex,
|
|
}
|
|
|
|
err = graph.AddChannelEdge(&edgeInfo)
|
|
if err != nil && err != channeldb.ErrEdgeAlreadyExist {
|
|
return nil, err
|
|
}
|
|
|
|
getExtraData := func(
|
|
end *testChannelEnd) lnwire.ExtraOpaqueData {
|
|
|
|
var extraData lnwire.ExtraOpaqueData
|
|
inboundFee := lnwire.Fee{
|
|
BaseFee: int32(end.InboundFeeBaseMsat),
|
|
FeeRate: int32(end.InboundFeeRate),
|
|
}
|
|
require.NoError(t, extraData.PackRecords(&inboundFee))
|
|
|
|
return extraData
|
|
}
|
|
|
|
if node1.testChannelPolicy != nil {
|
|
var msgFlags lnwire.ChanUpdateMsgFlags
|
|
if node1.MaxHTLC != 0 {
|
|
msgFlags |= lnwire.ChanUpdateRequiredMaxHtlc
|
|
}
|
|
var channelFlags lnwire.ChanUpdateChanFlags
|
|
if node1.Disabled {
|
|
channelFlags |= lnwire.ChanUpdateDisabled
|
|
}
|
|
|
|
edgePolicy := &models.ChannelEdgePolicy{
|
|
SigBytes: testSig.Serialize(),
|
|
MessageFlags: msgFlags,
|
|
ChannelFlags: channelFlags,
|
|
ChannelID: channelID,
|
|
LastUpdate: node1.LastUpdate,
|
|
TimeLockDelta: node1.Expiry,
|
|
MinHTLC: node1.MinHTLC,
|
|
MaxHTLC: node1.MaxHTLC,
|
|
FeeBaseMSat: node1.FeeBaseMsat,
|
|
FeeProportionalMillionths: node1.FeeRate,
|
|
ToNode: node2Vertex,
|
|
ExtraOpaqueData: getExtraData(node1),
|
|
}
|
|
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if node2.testChannelPolicy != nil {
|
|
var msgFlags lnwire.ChanUpdateMsgFlags
|
|
if node2.MaxHTLC != 0 {
|
|
msgFlags |= lnwire.ChanUpdateRequiredMaxHtlc
|
|
}
|
|
var channelFlags lnwire.ChanUpdateChanFlags
|
|
if node2.Disabled {
|
|
channelFlags |= lnwire.ChanUpdateDisabled
|
|
}
|
|
channelFlags |= lnwire.ChanUpdateDirection
|
|
|
|
edgePolicy := &models.ChannelEdgePolicy{
|
|
SigBytes: testSig.Serialize(),
|
|
MessageFlags: msgFlags,
|
|
ChannelFlags: channelFlags,
|
|
ChannelID: channelID,
|
|
LastUpdate: node2.LastUpdate,
|
|
TimeLockDelta: node2.Expiry,
|
|
MinHTLC: node2.MinHTLC,
|
|
MaxHTLC: node2.MaxHTLC,
|
|
FeeBaseMSat: node2.FeeBaseMsat,
|
|
FeeProportionalMillionths: node2.FeeRate,
|
|
ToNode: node1Vertex,
|
|
ExtraOpaqueData: getExtraData(node2),
|
|
}
|
|
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
channelID++
|
|
}
|
|
|
|
return &testGraphInstance{
|
|
graph: graph,
|
|
graphBackend: graphBackend,
|
|
aliasMap: aliasMap,
|
|
privKeyMap: privKeyMap,
|
|
links: links,
|
|
}, nil
|
|
}
|
|
|
|
// TestPathFinding tests all path finding related cases both with the in-memory
|
|
// graph cached turned on and off.
|
|
func TestPathFinding(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
fn func(t *testing.T, useCache bool)
|
|
}{{
|
|
name: "lowest fee path",
|
|
fn: runFindLowestFeePath,
|
|
}, {
|
|
name: "basic graph path finding",
|
|
fn: runBasicGraphPathFinding,
|
|
}, {
|
|
name: "path finding with additional edges",
|
|
fn: runPathFindingWithAdditionalEdges,
|
|
}, {
|
|
name: "path finding with redundant additional edges",
|
|
fn: runPathFindingWithRedundantAdditionalEdges,
|
|
}, {
|
|
name: "new route path too long",
|
|
fn: runNewRoutePathTooLong,
|
|
}, {
|
|
name: "path not available",
|
|
fn: runPathNotAvailable,
|
|
}, {
|
|
name: "destination tlv graph fallback",
|
|
fn: runDestTLVGraphFallback,
|
|
}, {
|
|
name: "missing feature dependency",
|
|
fn: runMissingFeatureDep,
|
|
}, {
|
|
name: "unknown required features",
|
|
fn: runUnknownRequiredFeatures,
|
|
}, {
|
|
name: "destination payment address",
|
|
fn: runDestPaymentAddr,
|
|
}, {
|
|
name: "path insufficient capacity",
|
|
fn: runPathInsufficientCapacity,
|
|
}, {
|
|
name: "route fail min HTLC",
|
|
fn: runRouteFailMinHTLC,
|
|
}, {
|
|
name: "route fail max HTLC",
|
|
fn: runRouteFailMaxHTLC,
|
|
}, {
|
|
name: "route fail disabled edge",
|
|
fn: runRouteFailDisabledEdge,
|
|
}, {
|
|
name: "path source edges bandwidth",
|
|
fn: runPathSourceEdgesBandwidth,
|
|
}, {
|
|
name: "restrict outgoing channel",
|
|
fn: runRestrictOutgoingChannel,
|
|
}, {
|
|
name: "restrict last hop",
|
|
fn: runRestrictLastHop,
|
|
}, {
|
|
name: "CLTV limit",
|
|
fn: runCltvLimit,
|
|
}, {
|
|
name: "probability routing",
|
|
fn: runProbabilityRouting,
|
|
}, {
|
|
name: "equal cost route selection",
|
|
fn: runEqualCostRouteSelection,
|
|
}, {
|
|
name: "no cycle",
|
|
fn: runNoCycle,
|
|
}, {
|
|
name: "route to self",
|
|
fn: runRouteToSelf,
|
|
}, {
|
|
name: "with metadata",
|
|
fn: runFindPathWithMetadata,
|
|
}, {
|
|
name: "inbound fees",
|
|
fn: runInboundFees,
|
|
}}
|
|
|
|
// Run with graph cache enabled.
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
|
|
t.Run("cache=true/"+tc.name, func(tt *testing.T) {
|
|
tt.Parallel()
|
|
|
|
tc.fn(tt, true)
|
|
})
|
|
}
|
|
|
|
// And with the DB fallback to make sure everything works the same
|
|
// still.
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
|
|
t.Run("cache=false/"+tc.name, func(tt *testing.T) {
|
|
tt.Parallel()
|
|
|
|
tc.fn(tt, false)
|
|
})
|
|
}
|
|
}
|
|
|
|
// runFindPathWithMetadata tests that metadata is taken into account during
|
|
// pathfinding.
|
|
func runFindPathWithMetadata(t *testing.T, useCache bool) {
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("alice", "bob", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "alice")
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
target := ctx.keyFromAlias("bob")
|
|
|
|
// Assert that a path is found when metadata is specified.
|
|
ctx.restrictParams.Metadata = []byte{1, 2, 3}
|
|
ctx.restrictParams.DestFeatures = tlvFeatures
|
|
|
|
path, err := ctx.findPath(target, paymentAmt)
|
|
require.NoError(t, err)
|
|
require.Len(t, path, 1)
|
|
|
|
// Assert that no path is found when metadata is too large.
|
|
ctx.restrictParams.Metadata = make([]byte, 2000)
|
|
|
|
_, err = ctx.findPath(target, paymentAmt)
|
|
require.ErrorIs(t, errNoPathFound, err)
|
|
}
|
|
|
|
// runFindLowestFeePath 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 runFindLowestFeePath(t *testing.T, useCache bool) {
|
|
// 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,
|
|
MaxHTLC: 100000000,
|
|
}),
|
|
symmetricTestChannel("first", "a", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}),
|
|
symmetricTestChannel("a", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}),
|
|
symmetricTestChannel("first", "b", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 100,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}),
|
|
symmetricTestChannel("b", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 600,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "roasbeef")
|
|
|
|
const (
|
|
startingHeight = 100
|
|
finalHopCLTV = 1
|
|
)
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
target := ctx.keyFromAlias("target")
|
|
path, err := ctx.findPath(target, paymentAmt)
|
|
require.NoError(t, err, "unable to find path")
|
|
route, err := newRoute(
|
|
ctx.source, path, startingHeight,
|
|
finalHopParams{
|
|
amt: paymentAmt,
|
|
cltvDelta: finalHopCLTV,
|
|
records: nil,
|
|
}, nil,
|
|
)
|
|
require.NoError(t, err, "unable to create path")
|
|
|
|
// Assert that the lowest fee route is returned.
|
|
if route.Hops[1].PubKeyBytes != ctx.keyFromAlias("b") {
|
|
t.Fatalf("expected route to pass through b, "+
|
|
"but got a route through %v",
|
|
ctx.aliasFromKey(route.Hops[1].PubKeyBytes))
|
|
}
|
|
}
|
|
|
|
func getAliasFromPubKey(pubKey route.Vertex,
|
|
aliases map[string]route.Vertex) string {
|
|
|
|
for alias, key := range aliases {
|
|
if key == pubKey {
|
|
return alias
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
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 runBasicGraphPathFinding(t *testing.T, useCache bool) {
|
|
testGraphInstance, err := parseTestGraph(t, useCache, basicGraphFilePath)
|
|
require.NoError(t, err, "unable to create graph")
|
|
|
|
// 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, testGraphInstance, &testCase)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstance,
|
|
test *basicGraphPathFindingTestCase) {
|
|
|
|
aliases := graphInstance.aliasMap
|
|
expectedHops := test.expectedHops
|
|
expectedHopCount := len(expectedHops)
|
|
|
|
sourceNode, err := graphInstance.graph.SourceNode()
|
|
require.NoError(t, err, "unable to fetch source node")
|
|
sourceVertex := route.Vertex(sourceNode.PubKeyBytes)
|
|
|
|
const (
|
|
startingHeight = 100
|
|
finalHopCLTV = 1
|
|
)
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(test.paymentAmt)
|
|
target := graphInstance.aliasMap[test.target]
|
|
path, err := dbFindPath(
|
|
graphInstance.graph, nil, &mockBandwidthHints{},
|
|
&RestrictParams{
|
|
FeeLimit: test.feeLimit,
|
|
ProbabilitySource: noProbabilitySource,
|
|
CltvLimit: math.MaxUint32,
|
|
},
|
|
testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, target, paymentAmt, 0,
|
|
startingHeight+finalHopCLTV,
|
|
)
|
|
if test.expectFailureNoPath {
|
|
if err == nil {
|
|
t.Fatal("expected no path to be found")
|
|
}
|
|
return
|
|
}
|
|
require.NoError(t, err, "unable to find path")
|
|
|
|
route, err := newRoute(
|
|
sourceVertex, path, startingHeight,
|
|
finalHopParams{
|
|
amt: paymentAmt,
|
|
cltvDelta: finalHopCLTV,
|
|
records: nil,
|
|
}, nil,
|
|
)
|
|
require.NoError(t, err, "unable to create path")
|
|
|
|
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 route.Hops[i].PubKeyBytes != aliases[expectedHops[i].alias] {
|
|
|
|
t.Fatalf("%v-th hop should be %v, is instead: %v",
|
|
i, expectedHops[i],
|
|
getAliasFromPubKey(route.Hops[i].PubKeyBytes,
|
|
aliases))
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
sphinxPath, err := route.ToSphinxPath()
|
|
require.NoError(t, err, "unable to make sphinx path")
|
|
if sphinxPath.TrueRouteLength() != expectedHopCount {
|
|
t.Fatalf("incorrect number of hop payloads: expected %v, got %v",
|
|
expectedHopCount, sphinxPath.TrueRouteLength())
|
|
}
|
|
|
|
// Hops should point to the next hop
|
|
for i := 0; i < len(expectedHops)-1; i++ {
|
|
payload, _, err := switchhop.ParseTLVPayload(
|
|
bytes.NewReader(sphinxPath[i].HopPayload.Payload),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(
|
|
t, route.Hops[i+1].ChannelID,
|
|
payload.FwdInfo.NextHop.ToUint64(),
|
|
)
|
|
}
|
|
|
|
lastHopIndex := len(expectedHops) - 1
|
|
|
|
payload, _, err := switchhop.ParseTLVPayload(
|
|
bytes.NewReader(sphinxPath[lastHopIndex].HopPayload.Payload),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// The final hop should have a next hop value of all zeroes in order
|
|
// to indicate it's the exit hop.
|
|
require.Zero(t, payload.FwdInfo.NextHop.ToUint64())
|
|
|
|
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.
|
|
|
|
fee := route.HopFee(i)
|
|
if fee != expectedHops[i].fee {
|
|
t.Fatalf("fee incorrect for hop %v: expected %v, got %v",
|
|
i, expectedHops[i].fee, 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)
|
|
}
|
|
}
|
|
|
|
// runPathFindingWithAdditionalEdges asserts that we are able to find paths to
|
|
// nodes that do not exist in the graph by way of hop hints. We also test that
|
|
// the path can support custom TLV records for the receiver under the
|
|
// appropriate circumstances.
|
|
func runPathFindingWithAdditionalEdges(t *testing.T, useCache bool) {
|
|
graph, err := parseTestGraph(t, useCache, basicGraphFilePath)
|
|
require.NoError(t, err, "unable to create graph")
|
|
|
|
sourceNode, err := graph.graph.SourceNode()
|
|
require.NoError(t, err, "unable to fetch source node")
|
|
|
|
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)
|
|
require.NoError(t, err, "unable to decode public key")
|
|
dogePubKey, err := btcec.ParsePubKey(dogePubKeyBytes)
|
|
require.NoError(t, err, "unable to parse public key from bytes")
|
|
|
|
doge := &channeldb.LightningNode{}
|
|
doge.AddPubKey(dogePubKey)
|
|
doge.Alias = "doge"
|
|
copy(doge.PubKeyBytes[:], dogePubKeyBytes)
|
|
graph.aliasMap["doge"] = doge.PubKeyBytes
|
|
|
|
// Create the channel edge going from songoku to doge and include it in
|
|
// our map of additional edges.
|
|
songokuToDogePolicy := &models.CachedEdgePolicy{
|
|
ToNodePubKey: func() route.Vertex {
|
|
return doge.PubKeyBytes
|
|
},
|
|
ToNodeFeatures: lnwire.EmptyFeatureVector(),
|
|
ChannelID: 1337,
|
|
FeeBaseMSat: 1,
|
|
FeeProportionalMillionths: 1000,
|
|
TimeLockDelta: 9,
|
|
}
|
|
|
|
additionalEdges := map[route.Vertex][]AdditionalEdge{
|
|
graph.aliasMap["songoku"]: {&PrivateEdge{
|
|
policy: songokuToDogePolicy,
|
|
}},
|
|
}
|
|
|
|
find := func(r *RestrictParams) (
|
|
[]*unifiedEdge, error) {
|
|
|
|
return dbFindPath(
|
|
graph.graph, additionalEdges, &mockBandwidthHints{},
|
|
r, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, doge.PubKeyBytes, paymentAmt,
|
|
0, 0,
|
|
)
|
|
}
|
|
|
|
// We should now be able to find a path from roasbeef to doge.
|
|
path, err := find(noRestrictions)
|
|
require.NoError(t, err, "unable to find private path to doge")
|
|
|
|
// The path should represent the following hops:
|
|
// roasbeef -> songoku -> doge
|
|
assertExpectedPath(t, graph.aliasMap, path, "songoku", "doge")
|
|
|
|
// Now, set custom records for the final hop. This should fail since no
|
|
// dest features are set, and we won't have a node ann to fall back on.
|
|
restrictions := *noRestrictions
|
|
restrictions.DestCustomRecords = record.CustomSet{70000: []byte{}}
|
|
|
|
// Finally, set the tlv feature in the payload and assert we found the
|
|
// same path as before.
|
|
restrictions.DestFeatures = tlvFeatures
|
|
|
|
path, err = find(&restrictions)
|
|
require.NoError(t, err, "path should have been found")
|
|
assertExpectedPath(t, graph.aliasMap, path, "songoku", "doge")
|
|
}
|
|
|
|
// runPathFindingWithRedundantAdditionalEdges asserts that we are able to find
|
|
// paths to nodes ignoring additional edges that are already known by self node.
|
|
func runPathFindingWithRedundantAdditionalEdges(t *testing.T, useCache bool) {
|
|
t.Helper()
|
|
|
|
var realChannelID uint64 = 3145
|
|
var hintChannelID uint64 = 1618
|
|
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("alice", "bob", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}, realChannelID),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "alice")
|
|
|
|
target := ctx.keyFromAlias("bob")
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
|
|
// Create the channel edge going from alice to bob and include it in
|
|
// our map of additional edges.
|
|
aliceToBobPolicy := &models.CachedEdgePolicy{
|
|
ToNodePubKey: func() route.Vertex {
|
|
return target
|
|
},
|
|
ToNodeFeatures: lnwire.EmptyFeatureVector(),
|
|
ChannelID: hintChannelID,
|
|
FeeBaseMSat: 1,
|
|
FeeProportionalMillionths: 1000,
|
|
TimeLockDelta: 9,
|
|
}
|
|
|
|
additionalEdges := map[route.Vertex][]AdditionalEdge{
|
|
ctx.source: {&PrivateEdge{
|
|
policy: aliceToBobPolicy,
|
|
}},
|
|
}
|
|
|
|
path, err := dbFindPath(
|
|
ctx.graph, additionalEdges, ctx.bandwidthHints,
|
|
&ctx.restrictParams, &ctx.pathFindingConfig, ctx.source, target,
|
|
paymentAmt, ctx.timePref, 0,
|
|
)
|
|
|
|
require.NoError(t, err, "unable to find path to bob")
|
|
require.Len(t, path, 1)
|
|
|
|
require.Equal(t, realChannelID, path[0].policy.ChannelID,
|
|
"additional edge for known edge wasn't ignored")
|
|
}
|
|
|
|
// TestNewRoute tests whether the construction of hop payloads by newRoute
|
|
// is executed correctly.
|
|
func TestNewRoute(t *testing.T) {
|
|
var sourceKey [33]byte
|
|
sourceVertex := route.Vertex(sourceKey)
|
|
|
|
testPaymentAddr := [32]byte{0x01, 0x02, 0x03}
|
|
|
|
const (
|
|
startingHeight = 100
|
|
finalHopCLTV = 1
|
|
)
|
|
|
|
createHop := func(baseFee lnwire.MilliSatoshi,
|
|
feeRate lnwire.MilliSatoshi,
|
|
bandwidth lnwire.MilliSatoshi,
|
|
timeLockDelta uint16) *models.CachedEdgePolicy {
|
|
|
|
return &models.CachedEdgePolicy{
|
|
ToNodePubKey: func() route.Vertex {
|
|
return route.Vertex{}
|
|
},
|
|
ToNodeFeatures: lnwire.NewFeatureVector(nil, nil),
|
|
FeeProportionalMillionths: feeRate,
|
|
FeeBaseMSat: baseFee,
|
|
TimeLockDelta: timeLockDelta,
|
|
}
|
|
}
|
|
|
|
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 []*models.CachedEdgePolicy
|
|
|
|
// paymentAmount is the amount that is send into the route
|
|
// indicated by hops.
|
|
paymentAmount lnwire.MilliSatoshi
|
|
|
|
// destFeatures is a feature vector, that if non-nil, will
|
|
// overwrite the final hop's feature vector in the graph.
|
|
destFeatures *lnwire.FeatureVector
|
|
|
|
paymentAddr *[32]byte
|
|
|
|
// metadata is the payment metadata to attach to the route.
|
|
metadata []byte
|
|
|
|
// 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
|
|
|
|
expectedMPP *record.MPP
|
|
}{
|
|
{
|
|
// For a single hop payment, no fees are expected to be paid.
|
|
name: "single hop",
|
|
paymentAmount: 100000,
|
|
hops: []*models.CachedEdgePolicy{
|
|
createHop(100, 1000, 1000000, 10),
|
|
},
|
|
metadata: []byte{1, 2, 3},
|
|
expectedFees: []lnwire.MilliSatoshi{0},
|
|
expectedTimeLocks: []uint32{1},
|
|
expectedTotalAmount: 100000,
|
|
expectedTotalTimeLock: 1,
|
|
}, {
|
|
// 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: []*models.CachedEdgePolicy{
|
|
createHop(0, 1000, 1000000, 10),
|
|
createHop(30, 1000, 1000000, 5),
|
|
},
|
|
expectedFees: []lnwire.MilliSatoshi{130, 0},
|
|
expectedTimeLocks: []uint32{1, 1},
|
|
expectedTotalAmount: 100130,
|
|
expectedTotalTimeLock: 6,
|
|
}, {
|
|
// 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 tlv onion feature",
|
|
destFeatures: tlvFeatures,
|
|
paymentAmount: 100000,
|
|
hops: []*models.CachedEdgePolicy{
|
|
createHop(0, 1000, 1000000, 10),
|
|
createHop(30, 1000, 1000000, 5),
|
|
},
|
|
expectedFees: []lnwire.MilliSatoshi{130, 0},
|
|
expectedTimeLocks: []uint32{1, 1},
|
|
expectedTotalAmount: 100130,
|
|
expectedTotalTimeLock: 6,
|
|
}, {
|
|
// 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 single shot mpp",
|
|
destFeatures: tlvPayAddrFeatures,
|
|
paymentAddr: &testPaymentAddr,
|
|
paymentAmount: 100000,
|
|
hops: []*models.CachedEdgePolicy{
|
|
createHop(0, 1000, 1000000, 10),
|
|
createHop(30, 1000, 1000000, 5),
|
|
},
|
|
expectedFees: []lnwire.MilliSatoshi{130, 0},
|
|
expectedTimeLocks: []uint32{1, 1},
|
|
expectedTotalAmount: 100130,
|
|
expectedTotalTimeLock: 6,
|
|
expectedMPP: record.NewMPP(
|
|
100000, testPaymentAddr,
|
|
),
|
|
}, {
|
|
// 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: []*models.CachedEdgePolicy{
|
|
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,
|
|
}, {
|
|
// 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: []*models.CachedEdgePolicy{
|
|
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,
|
|
}, {
|
|
// 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: []*models.CachedEdgePolicy{
|
|
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,
|
|
}}
|
|
|
|
for _, testCase := range testCases {
|
|
testCase := testCase
|
|
|
|
// Overwrite the final hop's features if the test requires a
|
|
// custom feature vector.
|
|
if testCase.destFeatures != nil {
|
|
finalHop := testCase.hops[len(testCase.hops)-1]
|
|
finalHop.ToNodeFeatures = testCase.destFeatures
|
|
}
|
|
|
|
assertRoute := func(t *testing.T, route *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++ {
|
|
fee := route.HopFee(i)
|
|
if testCase.expectedFees[i] != fee {
|
|
|
|
t.Errorf("Expected fee for hop %v to "+
|
|
"be %v, but got %v instead",
|
|
i, testCase.expectedFees[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)
|
|
}
|
|
}
|
|
|
|
finalHop := route.Hops[len(route.Hops)-1]
|
|
|
|
if !reflect.DeepEqual(
|
|
finalHop.MPP, testCase.expectedMPP,
|
|
) {
|
|
|
|
t.Errorf("Expected final hop mpp field: %v, "+
|
|
" but got: %v instead",
|
|
testCase.expectedMPP, finalHop.MPP)
|
|
}
|
|
|
|
if !bytes.Equal(finalHop.Metadata, testCase.metadata) {
|
|
t.Errorf("Expected final metadata field: %v, "+
|
|
" but got: %v instead",
|
|
testCase.metadata, finalHop.Metadata)
|
|
}
|
|
}
|
|
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
var unifiedHops []*unifiedEdge
|
|
for _, hop := range testCase.hops {
|
|
unifiedHops = append(unifiedHops,
|
|
&unifiedEdge{
|
|
policy: hop,
|
|
},
|
|
)
|
|
}
|
|
|
|
route, err := newRoute(
|
|
sourceVertex, unifiedHops, startingHeight,
|
|
finalHopParams{
|
|
amt: testCase.paymentAmount,
|
|
totalAmt: testCase.paymentAmount,
|
|
cltvDelta: finalHopCLTV,
|
|
records: nil,
|
|
paymentAddr: testCase.paymentAddr,
|
|
metadata: testCase.metadata,
|
|
}, nil,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
assertRoute(t, route)
|
|
})
|
|
}
|
|
}
|
|
|
|
func runNewRoutePathTooLong(t *testing.T, useCache bool) {
|
|
var testChannels []*testChannel
|
|
|
|
// Setup a linear network of 26 hops.
|
|
fromNode := "start"
|
|
for i := 0; i < 26; i++ {
|
|
toNode := fmt.Sprintf("node-%v", i+1)
|
|
c := symmetricTestChannel(fromNode, toNode, 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000001,
|
|
})
|
|
testChannels = append(testChannels, c)
|
|
|
|
fromNode = toNode
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "start")
|
|
|
|
// Assert that we can find 25 hop routes.
|
|
node25 := ctx.keyFromAlias("node-25")
|
|
payAmt := lnwire.MilliSatoshi(100001)
|
|
_, err := ctx.findPath(node25, payAmt)
|
|
require.NoError(t, err, "unexpected pathfinding failure")
|
|
|
|
// Assert that finding a 26 hop route fails.
|
|
node26 := ctx.keyFromAlias("node-26")
|
|
_, err = ctx.findPath(node26, payAmt)
|
|
require.ErrorIs(t, err, errNoPathFound)
|
|
|
|
// Assert that we can't find a 20 hop route if custom records make it
|
|
// exceed the maximum payload size.
|
|
ctx.restrictParams.DestFeatures = tlvFeatures
|
|
ctx.restrictParams.DestCustomRecords = map[uint64][]byte{
|
|
100000: bytes.Repeat([]byte{1}, 100),
|
|
}
|
|
_, err = ctx.findPath(node25, payAmt)
|
|
if err != errNoPathFound {
|
|
t.Fatalf("not route error expected, but got %v", err)
|
|
}
|
|
}
|
|
|
|
func runPathNotAvailable(t *testing.T, useCache bool) {
|
|
graph, err := parseTestGraph(t, useCache, basicGraphFilePath)
|
|
require.NoError(t, err, "unable to create graph")
|
|
|
|
sourceNode, err := graph.graph.SourceNode()
|
|
require.NoError(t, err, "unable to fetch source node")
|
|
|
|
// 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)
|
|
require.NoError(t, err, "unable to parse bytes")
|
|
var unknownNode route.Vertex
|
|
copy(unknownNode[:], unknownNodeBytes)
|
|
|
|
_, err = dbFindPath(
|
|
graph.graph, nil, &mockBandwidthHints{},
|
|
noRestrictions, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, unknownNode, 100, 0, 0,
|
|
)
|
|
if err != errNoPathFound {
|
|
t.Fatalf("path shouldn't have been found: %v", err)
|
|
}
|
|
}
|
|
|
|
// runDestTLVGraphFallback asserts that we properly detect when we can send TLV
|
|
// records to a receiver, and also that we fallback to the receiver's node
|
|
// announcement if we don't have an invoice features.
|
|
func runDestTLVGraphFallback(t *testing.T, useCache bool) {
|
|
testChannels := []*testChannel{
|
|
asymmetricTestChannel("roasbeef", "luoji", 100000,
|
|
&testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}, 0),
|
|
asymmetricTestChannel("roasbeef", "satoshi", 100000,
|
|
&testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
Features: tlvFeatures,
|
|
}, 0),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "roasbeef")
|
|
|
|
sourceNode, err := ctx.graph.SourceNode()
|
|
require.NoError(t, err, "unable to fetch source node")
|
|
|
|
find := func(r *RestrictParams,
|
|
target route.Vertex) ([]*unifiedEdge, error) {
|
|
|
|
return dbFindPath(
|
|
ctx.graph, nil, &mockBandwidthHints{},
|
|
r, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, target, 100, 0, 0,
|
|
)
|
|
}
|
|
|
|
// Luoji's node ann has an empty feature vector.
|
|
luoji := ctx.testGraphInstance.aliasMap["luoji"]
|
|
|
|
// Satoshi's node ann supports TLV.
|
|
satoshi := ctx.testGraphInstance.aliasMap["satoshi"]
|
|
|
|
restrictions := *noRestrictions
|
|
|
|
// Add custom records w/o any dest features.
|
|
restrictions.DestCustomRecords = record.CustomSet{70000: []byte{}}
|
|
|
|
// However, path to satoshi should succeed via the fallback because his
|
|
// node ann features have the TLV bit.
|
|
path, err := find(&restrictions, satoshi)
|
|
require.NoError(t, err, "path should have been found")
|
|
assertExpectedPath(t, ctx.testGraphInstance.aliasMap, path, "satoshi")
|
|
|
|
// Finally, set the TLV dest feature. We should succeed in finding a
|
|
// path to luoji.
|
|
restrictions.DestFeatures = tlvFeatures
|
|
|
|
path, err = find(&restrictions, luoji)
|
|
require.NoError(t, err, "path should have been found")
|
|
assertExpectedPath(t, ctx.testGraphInstance.aliasMap, path, "luoji")
|
|
}
|
|
|
|
// runMissingFeatureDep asserts that we fail path finding when the
|
|
// destination's features are broken, in that the feature vector doesn't signal
|
|
// all transitive dependencies.
|
|
func runMissingFeatureDep(t *testing.T, useCache bool) {
|
|
testChannels := []*testChannel{
|
|
asymmetricTestChannel("roasbeef", "conner", 100000,
|
|
&testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
},
|
|
&testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
Features: payAddrFeatures,
|
|
}, 0,
|
|
),
|
|
asymmetricTestChannel("conner", "joost", 100000,
|
|
&testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
Features: payAddrFeatures,
|
|
},
|
|
&testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}, 0,
|
|
),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "roasbeef")
|
|
|
|
// Conner's node in the graph has a broken feature vector, since it
|
|
// signals payment addresses without signaling tlv onions. Pathfinding
|
|
// should fail since we validate transitive feature dependencies for the
|
|
// final node.
|
|
conner := ctx.keyFromAlias("conner")
|
|
joost := ctx.keyFromAlias("joost")
|
|
|
|
_, err := ctx.findPath(conner, 100)
|
|
if err != errMissingDependentFeature {
|
|
t.Fatalf("path shouldn't have been found: %v", err)
|
|
}
|
|
|
|
// Now, set the TLV and payment addresses features to override the
|
|
// broken features found in the graph. We should succeed in finding a
|
|
// path to conner.
|
|
ctx.restrictParams.DestFeatures = tlvPayAddrFeatures
|
|
|
|
path, err := ctx.findPath(conner, 100)
|
|
require.NoError(t, err, "path should have been found")
|
|
assertExpectedPath(t, ctx.testGraphInstance.aliasMap, path, "conner")
|
|
|
|
// Finally, try to find a route to joost through conner. The
|
|
// destination features are set properly from the previous assertions,
|
|
// but conner's feature vector in the graph is still broken. We expect
|
|
// errNoPathFound and not the missing feature dep err above since
|
|
// intermediate hops are simply skipped if they have invalid feature
|
|
// vectors, leaving no possible route to joost.
|
|
_, err = ctx.findPath(joost, 100)
|
|
if err != errNoPathFound {
|
|
t.Fatalf("path shouldn't have been found: %v", err)
|
|
}
|
|
}
|
|
|
|
// runUnknownRequiredFeatures asserts that we fail path finding when the
|
|
// destination requires an unknown required feature, and that we skip
|
|
// intermediaries that signal unknown required features.
|
|
func runUnknownRequiredFeatures(t *testing.T, useCache bool) {
|
|
testChannels := []*testChannel{
|
|
asymmetricTestChannel("roasbeef", "conner", 100000,
|
|
&testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
},
|
|
&testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
Features: unknownRequiredFeatures,
|
|
}, 0,
|
|
),
|
|
asymmetricTestChannel("conner", "joost", 100000,
|
|
&testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
Features: unknownRequiredFeatures,
|
|
},
|
|
&testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}, 0,
|
|
),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "roasbeef")
|
|
|
|
conner := ctx.keyFromAlias("conner")
|
|
joost := ctx.keyFromAlias("joost")
|
|
|
|
// Conner's node in the graph has an unknown required feature (100).
|
|
// Pathfinding should fail since we check the destination's features for
|
|
// unknown required features before beginning pathfinding.
|
|
_, err := ctx.findPath(conner, 100)
|
|
if !reflect.DeepEqual(err, errUnknownRequiredFeature) {
|
|
t.Fatalf("path shouldn't have been found: %v", err)
|
|
}
|
|
|
|
// Now, try to find a route to joost through conner. The destination
|
|
// features are valid, but conner's feature vector in the graph still
|
|
// requires feature 100. We expect errNoPathFound and not the error
|
|
// above since intermediate hops are simply skipped if they have invalid
|
|
// feature vectors, leaving no possible route to joost. This asserts
|
|
// that we don't try to route _through_ nodes with unknown required
|
|
// features.
|
|
_, err = ctx.findPath(joost, 100)
|
|
if err != errNoPathFound {
|
|
t.Fatalf("path shouldn't have been found: %v", err)
|
|
}
|
|
}
|
|
|
|
// runDestPaymentAddr asserts that we properly detect when we can send a
|
|
// payment address to a receiver, and also that we fallback to the receiver's
|
|
// node announcement if we don't have an invoice features.
|
|
func runDestPaymentAddr(t *testing.T, useCache bool) {
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("roasbeef", "luoji", 100000,
|
|
&testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
},
|
|
),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "roasbeef")
|
|
|
|
luoji := ctx.keyFromAlias("luoji")
|
|
|
|
// Add payment address w/o any invoice features.
|
|
ctx.restrictParams.PaymentAddr = &[32]byte{1}
|
|
|
|
// Add empty destination features. This should cause us to fail, since
|
|
// this overrides anything in the graph.
|
|
ctx.restrictParams.DestFeatures = lnwire.EmptyFeatureVector()
|
|
|
|
_, err := ctx.findPath(luoji, 100)
|
|
if err != errNoPaymentAddr {
|
|
t.Fatalf("path shouldn't have been found: %v", err)
|
|
}
|
|
|
|
// Now, set the TLV and payment address features for the destination. We
|
|
// should succeed in finding a path to luoji.
|
|
ctx.restrictParams.DestFeatures = tlvPayAddrFeatures
|
|
|
|
path, err := ctx.findPath(luoji, 100)
|
|
require.NoError(t, err, "path should have been found")
|
|
assertExpectedPath(t, ctx.testGraphInstance.aliasMap, path, "luoji")
|
|
}
|
|
|
|
func runPathInsufficientCapacity(t *testing.T, useCache bool) {
|
|
graph, err := parseTestGraph(t, useCache, basicGraphFilePath)
|
|
require.NoError(t, err, "unable to create graph")
|
|
|
|
sourceNode, err := graph.graph.SourceNode()
|
|
require.NoError(t, err, "unable to fetch source node")
|
|
|
|
// 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 := graph.aliasMap["sophon"]
|
|
|
|
payAmt := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
|
|
_, err = dbFindPath(
|
|
graph.graph, nil, &mockBandwidthHints{},
|
|
noRestrictions, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, target, payAmt, 0, 0,
|
|
)
|
|
if err != errInsufficientBalance {
|
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
|
}
|
|
}
|
|
|
|
// runRouteFailMinHTLC tests that if we attempt to route an HTLC which is
|
|
// smaller than the advertised minHTLC of an edge, then path finding fails.
|
|
func runRouteFailMinHTLC(t *testing.T, useCache bool) {
|
|
graph, err := parseTestGraph(t, useCache, basicGraphFilePath)
|
|
require.NoError(t, err, "unable to create graph")
|
|
|
|
sourceNode, err := graph.graph.SourceNode()
|
|
require.NoError(t, err, "unable to fetch source node")
|
|
|
|
// 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 := graph.aliasMap["songoku"]
|
|
payAmt := lnwire.MilliSatoshi(10)
|
|
_, err = dbFindPath(
|
|
graph.graph, nil, &mockBandwidthHints{},
|
|
noRestrictions, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, target, payAmt, 0, 0,
|
|
)
|
|
if err != errNoPathFound {
|
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
|
}
|
|
}
|
|
|
|
// runRouteFailMaxHTLC tests that if we attempt to route an HTLC which is
|
|
// larger than the advertised max HTLC of an edge, then path finding fails.
|
|
func runRouteFailMaxHTLC(t *testing.T, useCache bool) {
|
|
// Set up a test graph:
|
|
// roasbeef <--> firstHop <--> secondHop <--> target
|
|
// We will be adjusting the max HTLC of the edge between the first and
|
|
// second hops.
|
|
var firstToSecondID uint64 = 1
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("roasbeef", "first", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000001,
|
|
}),
|
|
symmetricTestChannel("first", "second", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000002,
|
|
}, firstToSecondID),
|
|
symmetricTestChannel("second", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000003,
|
|
}),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "roasbeef")
|
|
|
|
// First, attempt to send a payment greater than the max HTLC we are
|
|
// about to set, which should succeed.
|
|
target := ctx.keyFromAlias("target")
|
|
payAmt := lnwire.MilliSatoshi(100001)
|
|
_, err := ctx.findPath(target, payAmt)
|
|
require.NoError(t, err, "graph should've been able to support payment")
|
|
|
|
// Next, update the middle edge policy to only allow payments up to 100k
|
|
// msat.
|
|
graph := ctx.testGraphInstance.graph
|
|
_, midEdge, _, err := graph.FetchChannelEdgesByID(firstToSecondID)
|
|
require.NoError(t, err, "unable to fetch channel edges by ID")
|
|
midEdge.MessageFlags = 1
|
|
midEdge.MaxHTLC = payAmt - 1
|
|
if err := graph.UpdateEdgePolicy(midEdge); err != nil {
|
|
t.Fatalf("unable to update edge: %v", err)
|
|
}
|
|
|
|
// We'll now attempt to route through that edge with a payment above
|
|
// 100k msat, which should fail.
|
|
_, err = ctx.findPath(target, payAmt)
|
|
if err != errNoPathFound {
|
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
|
}
|
|
}
|
|
|
|
// runRouteFailDisabledEdge tests that if we attempt to route to an edge
|
|
// that's disabled, then that edge is disqualified, and the routing attempt
|
|
// will fail. We also test that this is true only for non-local edges, as we'll
|
|
// ignore the disable flags, with the assumption that the correct bandwidth is
|
|
// found among the bandwidth hints.
|
|
func runRouteFailDisabledEdge(t *testing.T, useCache bool) {
|
|
graph, err := parseTestGraph(t, useCache, basicGraphFilePath)
|
|
require.NoError(t, err, "unable to create graph")
|
|
|
|
sourceNode, err := graph.graph.SourceNode()
|
|
require.NoError(t, err, "unable to fetch source node")
|
|
|
|
// First, we'll try to route from roasbeef -> sophon. This should
|
|
// succeed without issue, and return a single path via phamnuwen
|
|
target := graph.aliasMap["sophon"]
|
|
payAmt := lnwire.NewMSatFromSatoshis(105000)
|
|
_, err = dbFindPath(
|
|
graph.graph, nil, &mockBandwidthHints{},
|
|
noRestrictions, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, target, payAmt, 0, 0,
|
|
)
|
|
require.NoError(t, err, "unable to find path")
|
|
|
|
// Disable the edge roasbeef->phamnuwen. This should not impact the
|
|
// path finding, as we don't consider the disable flag for local
|
|
// channels (and roasbeef is the source).
|
|
roasToPham := uint64(999991)
|
|
_, e1, e2, err := graph.graph.FetchChannelEdgesByID(roasToPham)
|
|
require.NoError(t, err, "unable to fetch edge")
|
|
e1.ChannelFlags |= lnwire.ChanUpdateDisabled
|
|
if err := graph.graph.UpdateEdgePolicy(e1); err != nil {
|
|
t.Fatalf("unable to update edge: %v", err)
|
|
}
|
|
e2.ChannelFlags |= lnwire.ChanUpdateDisabled
|
|
if err := graph.graph.UpdateEdgePolicy(e2); err != nil {
|
|
t.Fatalf("unable to update edge: %v", err)
|
|
}
|
|
|
|
_, err = dbFindPath(
|
|
graph.graph, nil, &mockBandwidthHints{},
|
|
noRestrictions, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, target, payAmt, 0, 0,
|
|
)
|
|
require.NoError(t, err, "unable to find path")
|
|
|
|
// Now, we'll modify the edge from phamnuwen -> sophon, to read that
|
|
// it's disabled.
|
|
phamToSophon := uint64(99999)
|
|
_, e, _, err := graph.graph.FetchChannelEdgesByID(phamToSophon)
|
|
require.NoError(t, err, "unable to fetch edge")
|
|
e.ChannelFlags |= lnwire.ChanUpdateDisabled
|
|
if err := graph.graph.UpdateEdgePolicy(e); err != nil {
|
|
t.Fatalf("unable to update edge: %v", err)
|
|
}
|
|
|
|
// If we attempt to route through that edge, we should get a failure as
|
|
// it is no longer eligible.
|
|
_, err = dbFindPath(
|
|
graph.graph, nil, &mockBandwidthHints{},
|
|
noRestrictions, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, target, payAmt, 0, 0,
|
|
)
|
|
if err != errNoPathFound {
|
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
|
}
|
|
}
|
|
|
|
// runPathSourceEdgesBandwidth tests that explicitly passing in a set of
|
|
// bandwidth hints is used by the path finding algorithm to consider whether to
|
|
// use a local channel.
|
|
func runPathSourceEdgesBandwidth(t *testing.T, useCache bool) {
|
|
graph, err := parseTestGraph(t, useCache, basicGraphFilePath)
|
|
require.NoError(t, err, "unable to create graph")
|
|
|
|
sourceNode, err := graph.graph.SourceNode()
|
|
require.NoError(t, err, "unable to fetch source node")
|
|
|
|
// First, we'll try to route from roasbeef -> sophon. This should
|
|
// succeed without issue, and return a path via songoku, as that's the
|
|
// cheapest path.
|
|
target := graph.aliasMap["sophon"]
|
|
payAmt := lnwire.NewMSatFromSatoshis(50000)
|
|
path, err := dbFindPath(
|
|
graph.graph, nil, &mockBandwidthHints{},
|
|
noRestrictions, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, target, payAmt, 0, 0,
|
|
)
|
|
require.NoError(t, err, "unable to find path")
|
|
assertExpectedPath(t, graph.aliasMap, path, "songoku", "sophon")
|
|
|
|
// Now we'll set the bandwidth of the edge roasbeef->songoku and
|
|
// roasbeef->phamnuwen to 0.
|
|
roasToSongoku := uint64(12345)
|
|
roasToPham := uint64(999991)
|
|
bandwidths := &mockBandwidthHints{
|
|
hints: map[uint64]lnwire.MilliSatoshi{
|
|
roasToSongoku: 0,
|
|
roasToPham: 0,
|
|
},
|
|
}
|
|
|
|
// Since both these edges has a bandwidth of zero, no path should be
|
|
// found.
|
|
_, err = dbFindPath(
|
|
graph.graph, nil, bandwidths,
|
|
noRestrictions, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, target, payAmt, 0, 0,
|
|
)
|
|
if err != errNoPathFound {
|
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
|
}
|
|
|
|
// Set the bandwidth of roasbeef->phamnuwen high enough to carry the
|
|
// payment.
|
|
bandwidths.hints[roasToPham] = 2 * payAmt
|
|
|
|
// Now, if we attempt to route again, we should find the path via
|
|
// phamnuven, as the other source edge won't be considered.
|
|
path, err = dbFindPath(
|
|
graph.graph, nil, bandwidths,
|
|
noRestrictions, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, target, payAmt, 0, 0,
|
|
)
|
|
require.NoError(t, err, "unable to find path")
|
|
assertExpectedPath(t, graph.aliasMap, path, "phamnuwen", "sophon")
|
|
|
|
// Finally, set the roasbeef->songoku bandwidth, but also set its
|
|
// disable flag.
|
|
bandwidths.hints[roasToSongoku] = 2 * payAmt
|
|
_, e1, e2, err := graph.graph.FetchChannelEdgesByID(roasToSongoku)
|
|
require.NoError(t, err, "unable to fetch edge")
|
|
e1.ChannelFlags |= lnwire.ChanUpdateDisabled
|
|
if err := graph.graph.UpdateEdgePolicy(e1); err != nil {
|
|
t.Fatalf("unable to update edge: %v", err)
|
|
}
|
|
e2.ChannelFlags |= lnwire.ChanUpdateDisabled
|
|
if err := graph.graph.UpdateEdgePolicy(e2); err != nil {
|
|
t.Fatalf("unable to update edge: %v", err)
|
|
}
|
|
|
|
// Since we ignore disable flags for local channels, a path should
|
|
// still be found.
|
|
path, err = dbFindPath(
|
|
graph.graph, nil, bandwidths,
|
|
noRestrictions, testPathFindingConfig,
|
|
sourceNode.PubKeyBytes, target, payAmt, 0, 0,
|
|
)
|
|
require.NoError(t, err, "unable to find path")
|
|
assertExpectedPath(t, graph.aliasMap, path, "songoku", "sophon")
|
|
}
|
|
|
|
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 := createTestCtxFromFile(t, startingHeight, specExampleFilePath)
|
|
|
|
// 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"]
|
|
|
|
// Query for a route of 4,999,999 mSAT to carol.
|
|
carol := ctx.aliases["C"]
|
|
const amt lnwire.MilliSatoshi = 4999999
|
|
req, err := NewRouteRequest(
|
|
bob, &carol, amt, 0, noRestrictions, nil, nil,
|
|
nil, MinCLTVDelta,
|
|
)
|
|
require.NoError(t, err, "invalid route request")
|
|
|
|
route, _, err := ctx.router.FindRoute(req)
|
|
require.NoError(t, err, "unable to find route")
|
|
|
|
// Now we'll examine the route returned for correctness.
|
|
//
|
|
// It should be sending the exact payment amount as there are no
|
|
// additional hops.
|
|
require.Equal(t, amt, route.TotalAmount)
|
|
require.Equal(t, amt, route.Hops[0].AmtToForward)
|
|
require.Zero(t, route.HopFee(0))
|
|
|
|
// The CLTV expiry should be the current height plus 18 (the expiry for
|
|
// the B -> C channel.
|
|
require.EqualValues(t, startingHeight+MinCLTVDelta, route.TotalTimeLock)
|
|
|
|
// 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"]
|
|
ctx.router.cfg.SelfNode = alice
|
|
|
|
// We'll now request a route from A -> B -> C.
|
|
req, err = NewRouteRequest(
|
|
alice, &carol, amt, 0, noRestrictions, nil, nil, nil,
|
|
MinCLTVDelta,
|
|
)
|
|
require.NoError(t, err, "invalid route request")
|
|
|
|
route, _, err = ctx.router.FindRoute(req)
|
|
require.NoError(t, err, "unable to find routes")
|
|
|
|
// The route should be two hops.
|
|
require.Len(t, route.Hops, 2)
|
|
|
|
// The total amount should factor in a fee of 10199 and also use a CLTV
|
|
// delta total of 38 (20 + 18),
|
|
expectedAmt := lnwire.MilliSatoshi(5010198)
|
|
require.Equal(t, expectedAmt, route.TotalAmount)
|
|
|
|
expectedDelta := uint32(20 + MinCLTVDelta)
|
|
require.Equal(t, startingHeight+expectedDelta, route.TotalTimeLock)
|
|
|
|
// Ensure that the hops of the route are properly crafted.
|
|
//
|
|
// After taking the fee, Bob should be forwarding the remainder which
|
|
// is the exact payment to Bob.
|
|
require.Equal(t, amt, route.Hops[0].AmtToForward)
|
|
|
|
// 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
|
|
require.EqualValues(t, 10199, route.HopFee(0))
|
|
|
|
// While for the final hop, as there's no additional hop afterwards, we
|
|
// pay no fee.
|
|
require.Zero(t, route.HopFee(1))
|
|
|
|
// The outgoing CLTV value itself should be the current height plus 30
|
|
// to meet Carol's requirements.
|
|
require.EqualValues(t, startingHeight+MinCLTVDelta,
|
|
route.Hops[0].OutgoingTimeLock)
|
|
|
|
// For B -> C, we assert that the final hop also has the proper
|
|
// parameters.
|
|
lastHop := route.Hops[1]
|
|
require.EqualValues(t, amt, lastHop.AmtToForward)
|
|
require.EqualValues(t, startingHeight+MinCLTVDelta,
|
|
lastHop.OutgoingTimeLock)
|
|
}
|
|
|
|
func assertExpectedPath(t *testing.T, aliasMap map[string]route.Vertex,
|
|
path []*unifiedEdge, nodeAliases ...string) {
|
|
|
|
require.Len(t, path, len(nodeAliases))
|
|
|
|
for i, hop := range path {
|
|
require.Equal(t, aliasMap[nodeAliases[i]],
|
|
hop.policy.ToNodePubKey())
|
|
}
|
|
}
|
|
|
|
// TestNewRouteFromEmptyHops tests that the NewRouteFromHops function returns an
|
|
// error when the hop list is empty.
|
|
func TestNewRouteFromEmptyHops(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var source route.Vertex
|
|
_, err := route.NewRouteFromHops(0, 0, source, []*route.Hop{})
|
|
require.ErrorIs(t, err, route.ErrNoRouteHopsProvided)
|
|
}
|
|
|
|
// runRestrictOutgoingChannel asserts that a outgoing channel restriction is
|
|
// obeyed by the path finding algorithm.
|
|
func runRestrictOutgoingChannel(t *testing.T, useCache bool) {
|
|
// Define channel id constants
|
|
const (
|
|
chanSourceA = 1
|
|
chanATarget = 4
|
|
chanSourceB1 = 2
|
|
chanSourceB2 = 3
|
|
chanBTarget = 5
|
|
chanSourceTarget = 6
|
|
)
|
|
|
|
// Set up a test graph with three possible paths from roasbeef to
|
|
// target. The path through chanSourceB1 is the highest cost path.
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("roasbeef", "a", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
}, chanSourceA),
|
|
symmetricTestChannel("a", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
}, chanATarget),
|
|
symmetricTestChannel("roasbeef", "b", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
}, chanSourceB1),
|
|
symmetricTestChannel("roasbeef", "b", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
}, chanSourceB2),
|
|
symmetricTestChannel("b", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 800,
|
|
}, chanBTarget),
|
|
symmetricTestChannel("roasbeef", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
}, chanSourceTarget),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "roasbeef")
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
target := ctx.keyFromAlias("target")
|
|
outgoingChannelID := uint64(chanSourceB1)
|
|
|
|
// Find the best path given the restriction to only use channel 2 as the
|
|
// outgoing channel.
|
|
ctx.restrictParams.OutgoingChannelIDs = []uint64{outgoingChannelID}
|
|
path, err := ctx.findPath(target, paymentAmt)
|
|
require.NoError(t, err, "unable to find path")
|
|
|
|
// Assert that the route starts with channel chanSourceB1, in line with
|
|
// the specified restriction.
|
|
if path[0].policy.ChannelID != chanSourceB1 {
|
|
t.Fatalf("expected route to pass through channel %v, "+
|
|
"but channel %v was selected instead", chanSourceB1,
|
|
path[0].policy.ChannelID)
|
|
}
|
|
|
|
// If a direct channel to target is allowed as well, that channel is
|
|
// expected to be selected because the routing fees are zero.
|
|
ctx.restrictParams.OutgoingChannelIDs = []uint64{
|
|
chanSourceB1, chanSourceTarget,
|
|
}
|
|
path, err = ctx.findPath(target, paymentAmt)
|
|
require.NoError(t, err, "unable to find path")
|
|
if path[0].policy.ChannelID != chanSourceTarget {
|
|
t.Fatalf("expected route to pass through channel %v",
|
|
chanSourceTarget)
|
|
}
|
|
}
|
|
|
|
// runRestrictLastHop asserts that a last hop restriction is obeyed by the path
|
|
// finding algorithm.
|
|
func runRestrictLastHop(t *testing.T, useCache bool) {
|
|
// Set up a test graph with three possible paths from roasbeef to
|
|
// target. The path via channel 1 and 2 is the lowest cost path.
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("source", "a", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
}, 1),
|
|
symmetricTestChannel("a", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
}, 2),
|
|
symmetricTestChannel("source", "b", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
}, 3),
|
|
symmetricTestChannel("b", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 800,
|
|
}, 4),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "source")
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
target := ctx.keyFromAlias("target")
|
|
lastHop := ctx.keyFromAlias("b")
|
|
|
|
// Find the best path given the restriction to use b as the last hop.
|
|
// This should force pathfinding to not take the lowest cost option.
|
|
ctx.restrictParams.LastHop = &lastHop
|
|
path, err := ctx.findPath(target, paymentAmt)
|
|
require.NoError(t, err, "unable to find path")
|
|
if path[0].policy.ChannelID != 3 {
|
|
t.Fatalf("expected route to pass through channel 3, "+
|
|
"but channel %v was selected instead",
|
|
path[0].policy.ChannelID)
|
|
}
|
|
}
|
|
|
|
// runCltvLimit asserts that a cltv limit is obeyed by the path finding
|
|
// algorithm.
|
|
func runCltvLimit(t *testing.T, useCache bool) {
|
|
t.Run("no limit", func(t *testing.T) {
|
|
testCltvLimit(t, useCache, 2016, 1)
|
|
})
|
|
t.Run("no path", func(t *testing.T) {
|
|
testCltvLimit(t, useCache, 50, 0)
|
|
})
|
|
t.Run("force high cost", func(t *testing.T) {
|
|
testCltvLimit(t, useCache, 80, 3)
|
|
})
|
|
}
|
|
|
|
func testCltvLimit(t *testing.T, useCache bool, limit uint32,
|
|
expectedChannel uint64) {
|
|
|
|
t.Parallel()
|
|
|
|
// Set up a test graph with three possible paths to the target. The path
|
|
// through a is the lowest cost with a high time lock (144). The path
|
|
// through b has a higher cost but a lower time lock (100). That path
|
|
// through c and d (two hops) has the same case as the path through b,
|
|
// but the total time lock is lower (60).
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("roasbeef", "a", 100000, &testChannelPolicy{}, 1),
|
|
symmetricTestChannel("a", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeBaseMsat: 10000,
|
|
MinHTLC: 1,
|
|
}),
|
|
symmetricTestChannel("roasbeef", "b", 100000, &testChannelPolicy{}, 2),
|
|
symmetricTestChannel("b", "target", 100000, &testChannelPolicy{
|
|
Expiry: 100,
|
|
FeeBaseMsat: 20000,
|
|
MinHTLC: 1,
|
|
}),
|
|
symmetricTestChannel("roasbeef", "c", 100000, &testChannelPolicy{}, 3),
|
|
symmetricTestChannel("c", "d", 100000, &testChannelPolicy{
|
|
Expiry: 30,
|
|
FeeBaseMsat: 10000,
|
|
MinHTLC: 1,
|
|
}),
|
|
symmetricTestChannel("d", "target", 100000, &testChannelPolicy{
|
|
Expiry: 30,
|
|
FeeBaseMsat: 10000,
|
|
MinHTLC: 1,
|
|
}),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "roasbeef")
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
target := ctx.keyFromAlias("target")
|
|
|
|
ctx.restrictParams.CltvLimit = limit
|
|
path, err := ctx.findPath(target, paymentAmt)
|
|
if expectedChannel == 0 {
|
|
// Finish test if we expect no route.
|
|
if err == errNoPathFound {
|
|
return
|
|
}
|
|
t.Fatal("expected no path to be found")
|
|
}
|
|
require.NoError(t, err, "unable to find path")
|
|
|
|
const (
|
|
startingHeight = 100
|
|
finalHopCLTV = 1
|
|
)
|
|
route, err := newRoute(
|
|
ctx.source, path, startingHeight,
|
|
finalHopParams{
|
|
amt: paymentAmt,
|
|
cltvDelta: finalHopCLTV,
|
|
records: nil,
|
|
}, nil,
|
|
)
|
|
require.NoError(t, err, "unable to create path")
|
|
|
|
// Assert that the route starts with the expected channel.
|
|
if route.Hops[0].ChannelID != expectedChannel {
|
|
t.Fatalf("expected route to pass through channel %v, "+
|
|
"but channel %v was selected instead", expectedChannel,
|
|
route.Hops[0].ChannelID)
|
|
}
|
|
}
|
|
|
|
// runProbabilityRouting asserts that path finding not only takes into account
|
|
// fees but also success probability.
|
|
func runProbabilityRouting(t *testing.T, useCache bool) {
|
|
testCases := []struct {
|
|
name string
|
|
p10, p11, p20 float64
|
|
minProbability float64
|
|
expectedChan uint64
|
|
amount btcutil.Amount
|
|
timePref float64
|
|
}{
|
|
// Test two variations with probabilities that should multiply
|
|
// to the same total route probability. In both cases the three
|
|
// hop route should be the best route. The three hop route has a
|
|
// probability of 0.5 * 0.8 = 0.4. The fee is 5 (chan 10) + 8
|
|
// (chan 11) = 13. The attempt cost is 9 + 1% * 100 = 10. Path
|
|
// finding distance should work out to: 13 + 10 (attempt
|
|
// penalty) / 0.4 = 38. The two hop route is 25 + 10 / 0.7 = 39.
|
|
{
|
|
name: "three hop 1",
|
|
p10: 0.8, p11: 0.5, p20: 0.7,
|
|
minProbability: 0.1,
|
|
expectedChan: 10,
|
|
amount: 100,
|
|
},
|
|
{
|
|
name: "three hop 2",
|
|
p10: 0.5, p11: 0.8, p20: 0.7,
|
|
minProbability: 0.1,
|
|
expectedChan: 10,
|
|
amount: 100,
|
|
},
|
|
|
|
// If we increase the time preference, we expect the algorithm
|
|
// to choose - with everything else being equal - the more
|
|
// expensive, more reliable route.
|
|
{
|
|
name: "three hop timepref",
|
|
p10: 0.5, p11: 0.8, p20: 0.7,
|
|
minProbability: 0.1,
|
|
expectedChan: 20,
|
|
amount: 100,
|
|
timePref: 1,
|
|
},
|
|
|
|
// If a larger amount is sent, the effect of the proportional
|
|
// attempt cost becomes more noticeable. This amount in this
|
|
// test brings the attempt cost to 9 + 1% * 300 = 12 sat. The
|
|
// three hop path finding distance should work out to: 13 + 12
|
|
// (attempt penalty) / 0.4 = 43. The two hop route is 25 + 12 /
|
|
// 0.7 = 42. For this higher amount, the two hop route is
|
|
// expected to be selected.
|
|
{
|
|
name: "two hop high amount",
|
|
p10: 0.8, p11: 0.5, p20: 0.7,
|
|
minProbability: 0.1,
|
|
expectedChan: 20,
|
|
amount: 300,
|
|
},
|
|
|
|
// If the probability of the two hop route is increased, its
|
|
// distance becomes 25 + 10 / 0.85 = 37. This is less than the
|
|
// three hop route with its distance 38. So with an attempt
|
|
// penalty of 10, the higher fee route is chosen because of the
|
|
// compensation for success probability.
|
|
{
|
|
name: "two hop higher cost",
|
|
p10: 0.5, p11: 0.8, p20: 0.85,
|
|
minProbability: 0.1,
|
|
expectedChan: 20,
|
|
amount: 100,
|
|
},
|
|
|
|
// If the same probabilities are used with a probability lower bound of
|
|
// 0.5, we expect the three hop route with probability 0.4 to be
|
|
// excluded and the two hop route to be picked.
|
|
{
|
|
name: "probability limit",
|
|
p10: 0.8, p11: 0.5, p20: 0.7,
|
|
minProbability: 0.5,
|
|
expectedChan: 20,
|
|
amount: 100,
|
|
},
|
|
|
|
// With a probability limit above the probability of both routes, we
|
|
// expect no route to be returned. This expectation is signaled by using
|
|
// expected channel 0.
|
|
{
|
|
name: "probability limit no routes",
|
|
p10: 0.8, p11: 0.5, p20: 0.7,
|
|
minProbability: 0.8,
|
|
expectedChan: 0,
|
|
amount: 100,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
testProbabilityRouting(
|
|
t, useCache, tc.amount, tc.timePref,
|
|
tc.p10, tc.p11, tc.p20,
|
|
tc.minProbability, tc.expectedChan,
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testProbabilityRouting(t *testing.T, useCache bool,
|
|
paymentAmt btcutil.Amount, timePref float64,
|
|
p10, p11, p20, minProbability float64,
|
|
expectedChan uint64) {
|
|
|
|
t.Parallel()
|
|
|
|
// Set up a test graph with two possible paths to the target: a three
|
|
// hop path (via channels 10 and 11) and a two hop path (via channel
|
|
// 20).
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("roasbeef", "a1", 100000, &testChannelPolicy{}),
|
|
symmetricTestChannel("roasbeef", "b", 100000, &testChannelPolicy{}),
|
|
symmetricTestChannel("a1", "a2", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeBaseMsat: lnwire.NewMSatFromSatoshis(5),
|
|
MinHTLC: 1,
|
|
}, 10),
|
|
symmetricTestChannel("a2", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeBaseMsat: lnwire.NewMSatFromSatoshis(8),
|
|
MinHTLC: 1,
|
|
}, 11),
|
|
symmetricTestChannel("b", "target", 100000, &testChannelPolicy{
|
|
Expiry: 100,
|
|
FeeBaseMsat: lnwire.NewMSatFromSatoshis(25),
|
|
MinHTLC: 1,
|
|
}, 20),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "roasbeef")
|
|
|
|
alias := ctx.testGraphInstance.aliasMap
|
|
|
|
target := ctx.testGraphInstance.aliasMap["target"]
|
|
|
|
// Configure a probability source with the test parameters.
|
|
ctx.restrictParams.ProbabilitySource = func(fromNode,
|
|
toNode route.Vertex, amt lnwire.MilliSatoshi,
|
|
capacity btcutil.Amount) float64 {
|
|
|
|
if amt == 0 {
|
|
t.Fatal("expected non-zero amount")
|
|
}
|
|
|
|
switch {
|
|
case fromNode == alias["a1"] && toNode == alias["a2"]:
|
|
return p10
|
|
case fromNode == alias["a2"] && toNode == alias["target"]:
|
|
return p11
|
|
case fromNode == alias["b"] && toNode == alias["target"]:
|
|
return p20
|
|
default:
|
|
return 1
|
|
}
|
|
}
|
|
|
|
ctx.pathFindingConfig = PathFindingConfig{
|
|
AttemptCost: lnwire.NewMSatFromSatoshis(9),
|
|
AttemptCostPPM: 10000,
|
|
MinProbability: minProbability,
|
|
}
|
|
|
|
ctx.timePref = timePref
|
|
|
|
path, err := ctx.findPath(
|
|
target, lnwire.NewMSatFromSatoshis(paymentAmt),
|
|
)
|
|
if expectedChan == 0 {
|
|
if err != errNoPathFound {
|
|
t.Fatalf("expected no path found, but got %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Assert that the route passes through the expected channel.
|
|
if path[1].policy.ChannelID != expectedChan {
|
|
t.Fatalf("expected route to pass through channel %v, "+
|
|
"but channel %v was selected instead", expectedChan,
|
|
path[1].policy.ChannelID)
|
|
}
|
|
}
|
|
|
|
// runEqualCostRouteSelection asserts that route probability will be used as a
|
|
// tie breaker in case the path finding probabilities are equal.
|
|
func runEqualCostRouteSelection(t *testing.T, useCache bool) {
|
|
// Set up a test graph with two possible paths to the target: via a and
|
|
// via b. The routing fees and probabilities are chosen such that the
|
|
// algorithm will first explore target->a->source (backwards search).
|
|
// This route has fee 6 and a penalty of 4 for the 25% success
|
|
// probability. The algorithm will then proceed with evaluating
|
|
// target->b->source, which has a fee of 8 and a penalty of 2 for the
|
|
// 50% success probability. Both routes have the same path finding cost
|
|
// of 10. It is expected that in that case, the highest probability
|
|
// route (through b) is chosen.
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("source", "a", 100000, &testChannelPolicy{}),
|
|
symmetricTestChannel("source", "b", 100000, &testChannelPolicy{}),
|
|
symmetricTestChannel("a", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeBaseMsat: lnwire.NewMSatFromSatoshis(6),
|
|
MinHTLC: 1,
|
|
}, 1),
|
|
symmetricTestChannel("b", "target", 100000, &testChannelPolicy{
|
|
Expiry: 100,
|
|
FeeBaseMsat: lnwire.NewMSatFromSatoshis(8),
|
|
MinHTLC: 1,
|
|
}, 2),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "source")
|
|
|
|
alias := ctx.testGraphInstance.aliasMap
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
target := ctx.testGraphInstance.aliasMap["target"]
|
|
|
|
ctx.restrictParams.ProbabilitySource = func(fromNode,
|
|
toNode route.Vertex, amt lnwire.MilliSatoshi,
|
|
capacity btcutil.Amount) float64 {
|
|
|
|
switch {
|
|
case fromNode == alias["source"] && toNode == alias["a"]:
|
|
return 0.25
|
|
case fromNode == alias["source"] && toNode == alias["b"]:
|
|
return 0.5
|
|
default:
|
|
return 1
|
|
}
|
|
}
|
|
|
|
ctx.pathFindingConfig = PathFindingConfig{
|
|
AttemptCost: lnwire.NewMSatFromSatoshis(1),
|
|
}
|
|
|
|
path, err := ctx.findPath(target, paymentAmt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if path[1].policy.ChannelID != 2 {
|
|
t.Fatalf("expected route to pass through channel %v, "+
|
|
"but channel %v was selected instead", 2,
|
|
path[1].policy.ChannelID)
|
|
}
|
|
}
|
|
|
|
// runNoCycle tries to guide the path finding algorithm into reconstructing an
|
|
// endless route. It asserts that the algorithm is able to handle this properly.
|
|
func runNoCycle(t *testing.T, useCache bool) {
|
|
// Set up a test graph with two paths: source->a->target and
|
|
// source->b->c->target. The fees are setup such that, searching
|
|
// backwards, the algorithm will evaluate the following end of the route
|
|
// first: ->target->c->target. This does not make sense, because if
|
|
// target is reached, there is no need to continue to c. A proper
|
|
// implementation will then go on with alternative routes. It will then
|
|
// consider ->a->target because its cost is lower than the alternative
|
|
// ->b->c->target and finally find source->a->target as the best route.
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("source", "a", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
}, 1),
|
|
symmetricTestChannel("source", "b", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
}, 2),
|
|
symmetricTestChannel("b", "c", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeBaseMsat: 2000,
|
|
}, 3),
|
|
symmetricTestChannel("c", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeBaseMsat: 0,
|
|
}, 4),
|
|
symmetricTestChannel("a", "target", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeBaseMsat: 600,
|
|
}, 5),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "source")
|
|
|
|
const (
|
|
startingHeight = 100
|
|
finalHopCLTV = 1
|
|
)
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
target := ctx.keyFromAlias("target")
|
|
|
|
// Find the best path given the restriction to only use channel 2 as the
|
|
// outgoing channel.
|
|
path, err := ctx.findPath(target, paymentAmt)
|
|
require.NoError(t, err, "unable to find path")
|
|
route, err := newRoute(
|
|
ctx.source, path, startingHeight,
|
|
finalHopParams{
|
|
amt: paymentAmt,
|
|
cltvDelta: finalHopCLTV,
|
|
records: nil,
|
|
}, nil,
|
|
)
|
|
require.NoError(t, err, "unable to create path")
|
|
|
|
if len(route.Hops) != 2 {
|
|
t.Fatalf("unexpected route")
|
|
}
|
|
if route.Hops[0].ChannelID != 1 {
|
|
t.Fatalf("unexpected first hop")
|
|
}
|
|
if route.Hops[1].ChannelID != 5 {
|
|
t.Fatalf("unexpected second hop")
|
|
}
|
|
}
|
|
|
|
// runRouteToSelf tests that it is possible to find a route to the self node.
|
|
func runRouteToSelf(t *testing.T, useCache bool) {
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel("source", "a", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeBaseMsat: 500,
|
|
}, 1),
|
|
symmetricTestChannel("source", "b", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeBaseMsat: 1000,
|
|
}, 2),
|
|
symmetricTestChannel("a", "b", 100000, &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeBaseMsat: 1000,
|
|
}, 3),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "source")
|
|
|
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
|
target := ctx.source
|
|
|
|
// Find the best path to self. We expect this to be source->a->source,
|
|
// because a charges the lowest forwarding fee.
|
|
path, err := ctx.findPath(target, paymentAmt)
|
|
require.NoError(t, err, "unable to find path")
|
|
ctx.assertPath(path, []uint64{1, 1})
|
|
|
|
outgoingChanID := uint64(1)
|
|
lastHop := ctx.keyFromAlias("b")
|
|
ctx.restrictParams.OutgoingChannelIDs = []uint64{outgoingChanID}
|
|
ctx.restrictParams.LastHop = &lastHop
|
|
|
|
// Find the best path to self given that we want to go out via channel 1
|
|
// and return through node b.
|
|
path, err = ctx.findPath(target, paymentAmt)
|
|
require.NoError(t, err, "unable to find path")
|
|
ctx.assertPath(path, []uint64{1, 3, 2})
|
|
}
|
|
|
|
// runInboundFees tests whether correct routes are built when inbound fees
|
|
// apply.
|
|
func runInboundFees(t *testing.T, useCache bool) {
|
|
// Setup a test network.
|
|
chanCapSat := btcutil.Amount(100000)
|
|
features := lnwire.NewFeatureVector(
|
|
lnwire.NewRawFeatureVector(
|
|
lnwire.PaymentAddrOptional,
|
|
lnwire.TLVOnionPayloadRequired,
|
|
),
|
|
lnwire.Features,
|
|
)
|
|
|
|
testChannels := []*testChannel{
|
|
asymmetricTestChannel("a", "b", chanCapSat,
|
|
&testChannelPolicy{
|
|
MinHTLC: lnwire.NewMSatFromSatoshis(2),
|
|
Features: features,
|
|
},
|
|
&testChannelPolicy{
|
|
Features: features,
|
|
InboundFeeRate: -60000,
|
|
InboundFeeBaseMsat: -5000,
|
|
}, 10,
|
|
),
|
|
asymmetricTestChannel("b", "c", chanCapSat,
|
|
&testChannelPolicy{
|
|
Expiry: 20,
|
|
FeeRate: 50000,
|
|
FeeBaseMsat: 1000,
|
|
MinHTLC: lnwire.NewMSatFromSatoshis(2),
|
|
Features: features,
|
|
},
|
|
&testChannelPolicy{
|
|
Features: features,
|
|
InboundFeeRate: 0,
|
|
InboundFeeBaseMsat: 5000,
|
|
}, 11,
|
|
),
|
|
asymmetricTestChannel("c", "d", chanCapSat,
|
|
&testChannelPolicy{
|
|
Expiry: 20,
|
|
FeeRate: 50000,
|
|
FeeBaseMsat: 0,
|
|
MinHTLC: lnwire.NewMSatFromSatoshis(2),
|
|
Features: features,
|
|
},
|
|
&testChannelPolicy{
|
|
Features: features,
|
|
InboundFeeRate: -50000,
|
|
InboundFeeBaseMsat: -8000,
|
|
}, 12,
|
|
),
|
|
asymmetricTestChannel("d", "e", chanCapSat,
|
|
&testChannelPolicy{
|
|
Expiry: 20,
|
|
FeeRate: 100000,
|
|
FeeBaseMsat: 9000,
|
|
MinHTLC: lnwire.NewMSatFromSatoshis(2),
|
|
Features: features,
|
|
},
|
|
&testChannelPolicy{
|
|
Features: features,
|
|
InboundFeeRate: 80000,
|
|
InboundFeeBaseMsat: 2000,
|
|
}, 13,
|
|
),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(t, useCache, testChannels, "a")
|
|
|
|
payAddr := [32]byte{1}
|
|
ctx.restrictParams.PaymentAddr = &payAddr
|
|
ctx.restrictParams.DestFeatures = tlvPayAddrFeatures
|
|
|
|
const (
|
|
startingHeight = 100
|
|
finalHopCLTV = 1
|
|
)
|
|
|
|
paymentAmt := lnwire.MilliSatoshi(100_000)
|
|
target := ctx.keyFromAlias("e")
|
|
path, err := ctx.findPath(target, paymentAmt)
|
|
require.NoError(t, err, "unable to find path")
|
|
|
|
rt, err := newRoute(
|
|
ctx.source, path, startingHeight,
|
|
finalHopParams{
|
|
amt: paymentAmt,
|
|
cltvDelta: finalHopCLTV,
|
|
records: nil,
|
|
paymentAddr: &payAddr,
|
|
totalAmt: paymentAmt,
|
|
},
|
|
nil,
|
|
)
|
|
require.NoError(t, err, "unable to create path")
|
|
|
|
expectedHops := []*route.Hop{
|
|
{
|
|
PubKeyBytes: ctx.keyFromAlias("b"),
|
|
ChannelID: 10,
|
|
// The amount that c forwards (105_050) plus the out fee
|
|
// (5_252) and in fee (5_000) of c.
|
|
AmtToForward: 115_302,
|
|
OutgoingTimeLock: 141,
|
|
},
|
|
{
|
|
PubKeyBytes: ctx.keyFromAlias("c"),
|
|
ChannelID: 11,
|
|
// The amount that d forwards (100_000) plus the out fee
|
|
// (19_000) and in fee (-13_950) of d.
|
|
AmtToForward: 105_050,
|
|
OutgoingTimeLock: 121,
|
|
},
|
|
{
|
|
PubKeyBytes: ctx.keyFromAlias("d"),
|
|
ChannelID: 12,
|
|
AmtToForward: 100_000,
|
|
OutgoingTimeLock: 101,
|
|
},
|
|
{
|
|
PubKeyBytes: ctx.keyFromAlias("e"),
|
|
ChannelID: 13,
|
|
AmtToForward: 100_000,
|
|
OutgoingTimeLock: 101,
|
|
MPP: record.NewMPP(100_000, payAddr),
|
|
},
|
|
}
|
|
|
|
expectedRt := &route.Route{
|
|
// The amount that b forwards (115_302) plus the out fee (6_765)
|
|
// and in fee (-12324) of b. The total fee is floored at zero.
|
|
TotalAmount: 115_302,
|
|
TotalTimeLock: 161,
|
|
SourcePubKey: ctx.keyFromAlias("a"),
|
|
Hops: expectedHops,
|
|
}
|
|
require.Equal(t, expectedRt, rt)
|
|
}
|
|
|
|
type pathFindingTestContext struct {
|
|
t *testing.T
|
|
graph *channeldb.ChannelGraph
|
|
restrictParams RestrictParams
|
|
bandwidthHints bandwidthHints
|
|
pathFindingConfig PathFindingConfig
|
|
testGraphInstance *testGraphInstance
|
|
source route.Vertex
|
|
timePref float64
|
|
}
|
|
|
|
func newPathFindingTestContext(t *testing.T, useCache bool,
|
|
testChannels []*testChannel, source string,
|
|
sourceFeatureBits ...lnwire.FeatureBit) *pathFindingTestContext {
|
|
|
|
testGraphInstance, err := createTestGraphFromChannels(
|
|
t, useCache, testChannels, source, sourceFeatureBits...,
|
|
)
|
|
require.NoError(t, err, "unable to create graph")
|
|
|
|
sourceNode, err := testGraphInstance.graph.SourceNode()
|
|
require.NoError(t, err, "unable to fetch source node")
|
|
|
|
ctx := &pathFindingTestContext{
|
|
t: t,
|
|
testGraphInstance: testGraphInstance,
|
|
source: route.Vertex(sourceNode.PubKeyBytes),
|
|
pathFindingConfig: *testPathFindingConfig,
|
|
graph: testGraphInstance.graph,
|
|
restrictParams: *noRestrictions,
|
|
bandwidthHints: &mockBandwidthHints{},
|
|
}
|
|
|
|
return ctx
|
|
}
|
|
|
|
func (c *pathFindingTestContext) keyFromAlias(alias string) route.Vertex {
|
|
return c.testGraphInstance.aliasMap[alias]
|
|
}
|
|
|
|
func (c *pathFindingTestContext) aliasFromKey(pubKey route.Vertex) string {
|
|
for alias, key := range c.testGraphInstance.aliasMap {
|
|
if key == pubKey {
|
|
return alias
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *pathFindingTestContext) findPath(target route.Vertex,
|
|
amt lnwire.MilliSatoshi) ([]*unifiedEdge,
|
|
error) {
|
|
|
|
return dbFindPath(
|
|
c.graph, nil, c.bandwidthHints, &c.restrictParams,
|
|
&c.pathFindingConfig, c.source, target, amt, c.timePref, 0,
|
|
)
|
|
}
|
|
|
|
func (c *pathFindingTestContext) findBlindedPaths(
|
|
restrictions *blindedPathRestrictions) ([][]blindedHop, error) {
|
|
|
|
return dbFindBlindedPaths(c.graph, restrictions)
|
|
}
|
|
|
|
func (c *pathFindingTestContext) assertPath(path []*unifiedEdge,
|
|
expected []uint64) {
|
|
|
|
if len(path) != len(expected) {
|
|
c.t.Fatalf("expected path of length %v, but got %v",
|
|
len(expected), len(path))
|
|
}
|
|
|
|
for i, edge := range path {
|
|
if edge.policy.ChannelID != expected[i] {
|
|
c.t.Fatalf("expected hop %v to be channel %v, "+
|
|
"but got %v", i, expected[i],
|
|
edge.policy.ChannelID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// dbFindPath calls findPath after getting a db transaction from the database
|
|
// graph.
|
|
func dbFindPath(graph *channeldb.ChannelGraph,
|
|
additionalEdges map[route.Vertex][]AdditionalEdge,
|
|
bandwidthHints bandwidthHints,
|
|
r *RestrictParams, cfg *PathFindingConfig,
|
|
source, target route.Vertex, amt lnwire.MilliSatoshi, timePref float64,
|
|
finalHtlcExpiry int32) ([]*unifiedEdge, error) {
|
|
|
|
sourceNode, err := graph.SourceNode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
graphSessFactory := newMockGraphSessionFactoryFromChanDB(graph)
|
|
|
|
graphSess, closeGraphSess, err := graphSessFactory.NewGraphSession()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
if err := closeGraphSess(); err != nil {
|
|
log.Errorf("Error closing graph session: %v", err)
|
|
}
|
|
}()
|
|
|
|
route, _, err := findPath(
|
|
&graphParams{
|
|
additionalEdges: additionalEdges,
|
|
bandwidthHints: bandwidthHints,
|
|
graph: graphSess,
|
|
},
|
|
r, cfg, sourceNode.PubKeyBytes, source, target, amt, timePref,
|
|
finalHtlcExpiry,
|
|
)
|
|
|
|
return route, err
|
|
}
|
|
|
|
// dbFindBlindedPaths calls findBlindedPaths after getting a db transaction from
|
|
// the database graph.
|
|
func dbFindBlindedPaths(graph *channeldb.ChannelGraph,
|
|
restrictions *blindedPathRestrictions) ([][]blindedHop, error) {
|
|
|
|
sourceNode, err := graph.SourceNode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return findBlindedPaths(
|
|
newMockGraphSessionChanDB(graph), sourceNode.PubKeyBytes,
|
|
restrictions,
|
|
)
|
|
}
|
|
|
|
// TestBlindedRouteConstruction tests creation of a blinded route with the
|
|
// following topology:
|
|
//
|
|
// A -- B -- C (introduction point) - D (blinded) - E (blinded).
|
|
func TestBlindedRouteConstruction(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
// We need valid pubkeys for the blinded portion of our route,
|
|
// so we just produce all of our pubkeys in the same way.
|
|
_, alicePk = btcec.PrivKeyFromBytes([]byte{1})
|
|
_, bobPk = btcec.PrivKeyFromBytes([]byte{2})
|
|
_, carolPk = btcec.PrivKeyFromBytes([]byte{3})
|
|
_, daveBlindedPk = btcec.PrivKeyFromBytes([]byte{4})
|
|
_, eveBlindedPk = btcec.PrivKeyFromBytes([]byte{5})
|
|
|
|
_, blindingPk = btcec.PrivKeyFromBytes([]byte{9})
|
|
|
|
// Convenient type conversions for the pieces of code that use
|
|
// vertexes.
|
|
sourceVertex = route.NewVertex(alicePk)
|
|
bobVertex = route.NewVertex(bobPk)
|
|
carolVertex = route.NewVertex(carolPk)
|
|
daveBlindedVertex = route.NewVertex(daveBlindedPk)
|
|
eveBlindedVertex = route.NewVertex(eveBlindedPk)
|
|
|
|
currentHeight uint32 = 100
|
|
|
|
// Create arbitrary encrypted data for each hop (we don't need
|
|
// to actually read this data to test route construction, since
|
|
// it's only used by forwarding nodes) and metadata for the
|
|
// final hop.
|
|
metadata = []byte{1, 2, 3}
|
|
carolEncryptedData = []byte{4, 5, 6}
|
|
daveEncryptedData = []byte{7, 8, 9}
|
|
eveEncryptedData = []byte{10, 11, 12}
|
|
|
|
// Create a blinded route with carol as the introduction point.
|
|
blindedPath = &sphinx.BlindedPath{
|
|
IntroductionPoint: carolPk,
|
|
BlindingPoint: blindingPk,
|
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
|
{
|
|
// Note: no pubkey is provided for
|
|
// Carol because we use the pubkey
|
|
// given by IntroductionPoint.
|
|
CipherText: carolEncryptedData,
|
|
},
|
|
{
|
|
BlindedNodePub: daveBlindedPk,
|
|
CipherText: daveEncryptedData,
|
|
},
|
|
{
|
|
BlindedNodePub: eveBlindedPk,
|
|
CipherText: eveEncryptedData,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create a blinded payment, which contains the aggregate relay
|
|
// information and constraints for the blinded portion of the
|
|
// path.
|
|
blindedPayment = &BlindedPayment{
|
|
BlindedPath: blindedPath,
|
|
CltvExpiryDelta: 120,
|
|
// Set only a base fee for easier calculations.
|
|
BaseFee: 5000,
|
|
Features: tlvFeatures,
|
|
}
|
|
|
|
// Create channel edges for the unblinded portion of our
|
|
// route. Proportional fees are omitted for easy test
|
|
// calculations, but non-zero base fees ensure our fee is
|
|
// still accounted for.
|
|
aliceBobEdge = &models.CachedEdgePolicy{
|
|
ChannelID: 1,
|
|
// We won't actually use this timelock / fee (since
|
|
// it's the sender's outbound channel), but we include
|
|
// it with different values so that the test will trip
|
|
// up if we were to include the fee/delay.
|
|
TimeLockDelta: 10,
|
|
FeeBaseMSat: 50,
|
|
ToNodePubKey: func() route.Vertex {
|
|
return bobVertex
|
|
},
|
|
ToNodeFeatures: tlvFeatures,
|
|
}
|
|
|
|
bobCarolEdge = &models.CachedEdgePolicy{
|
|
ChannelID: 2,
|
|
TimeLockDelta: 15,
|
|
FeeBaseMSat: 20,
|
|
ToNodePubKey: func() route.Vertex {
|
|
return carolVertex
|
|
},
|
|
ToNodeFeatures: tlvFeatures,
|
|
}
|
|
|
|
// Create final hop parameters for payment amount = 110.
|
|
totalAmt lnwire.MilliSatoshi = 110
|
|
finalHopParams = finalHopParams{
|
|
amt: totalAmt,
|
|
totalAmt: totalAmt,
|
|
metadata: metadata,
|
|
|
|
// We set a CLTV delta here just to test that this will
|
|
// be ignored by newRoute since this is a blinded path
|
|
// where the accumulated CLTV delta for the route
|
|
// communicated in the blinded path should be assumed to
|
|
// include the CLTV delta of the final hop.
|
|
cltvDelta: MaxCLTVDelta,
|
|
}
|
|
)
|
|
|
|
require.NoError(t, blindedPayment.Validate())
|
|
|
|
// Generate route hints from our blinded payment and a set of edges
|
|
// that make up the graph we'll give to route construction. The hints
|
|
// map is keyed by source node, so we can retrieve our blinded edges
|
|
// accordingly.
|
|
blindedEdges, err := blindedPayment.toRouteHints()
|
|
require.NoError(t, err)
|
|
|
|
carolDaveEdge := blindedEdges[carolVertex][0]
|
|
daveEveEdge := blindedEdges[daveBlindedVertex][0]
|
|
|
|
edges := []*unifiedEdge{
|
|
{policy: aliceBobEdge},
|
|
{policy: bobCarolEdge},
|
|
{policy: carolDaveEdge.EdgePolicy()},
|
|
{policy: daveEveEdge.EdgePolicy()},
|
|
}
|
|
|
|
// Total timelock for the route should include:
|
|
// - Starting block height
|
|
// - CLTV delta for Bob -> Carol (unblinded hop)
|
|
// - Aggregate cltv from blinded path, which includes
|
|
// - CLTV delta for Carol -> Dave -> Eve (blinded route)
|
|
// - Eve's chosen final CLTV delta
|
|
totalTimelock := currentHeight +
|
|
uint32(bobCarolEdge.TimeLockDelta) +
|
|
uint32(blindedPayment.CltvExpiryDelta)
|
|
|
|
// Total amount for the route should include:
|
|
// - Total amount being sent
|
|
// - Fee for Bob -> Carol (unblinded hop)
|
|
// - Fee for Carol -> Dave -> Eve (blinded route)
|
|
totalAmount := totalAmt + bobCarolEdge.FeeBaseMSat +
|
|
lnwire.MilliSatoshi(blindedPayment.BaseFee)
|
|
|
|
// Bob's outgoing timelock and amount are the total less his own
|
|
// outgoing channel's delta and fee.
|
|
bobTimelock := currentHeight + uint32(blindedPayment.CltvExpiryDelta)
|
|
bobAmount := totalAmt + lnwire.MilliSatoshi(
|
|
blindedPayment.BaseFee,
|
|
)
|
|
|
|
aliceBobRouteHop := &route.Hop{
|
|
PubKeyBytes: bobVertex,
|
|
ChannelID: aliceBobEdge.ChannelID,
|
|
// Alice -> Bob is a regular hop, so it should include all the
|
|
// regular forwarding values for Bob to send an outgoing HTLC
|
|
// to Carol.
|
|
OutgoingTimeLock: bobTimelock,
|
|
AmtToForward: bobAmount,
|
|
}
|
|
|
|
bobCarolRouteHop := &route.Hop{
|
|
PubKeyBytes: carolVertex,
|
|
ChannelID: bobCarolEdge.ChannelID,
|
|
// Bob -> Carol sends the HTLC to the introduction node, so
|
|
// it should not have forwarding values for Carol (these will
|
|
// be obtained from the encrypted data) and should include both
|
|
// the blinding point and encrypted data to be passed to Carol.
|
|
OutgoingTimeLock: 0,
|
|
AmtToForward: 0,
|
|
BlindingPoint: blindingPk,
|
|
EncryptedData: carolEncryptedData,
|
|
}
|
|
|
|
carolDaveRouteHop := &route.Hop{
|
|
PubKeyBytes: daveBlindedVertex,
|
|
// Carol -> Dave is within the blinded route, so should not
|
|
// set any outgoing values but must include Dave's encrypted
|
|
// data.
|
|
OutgoingTimeLock: 0,
|
|
AmtToForward: 0,
|
|
EncryptedData: daveEncryptedData,
|
|
}
|
|
|
|
daveEveRouteHop := &route.Hop{
|
|
PubKeyBytes: eveBlindedVertex,
|
|
// Dave -> Eve is the final hop in a blinded route, so it
|
|
// should include outgoing values for the final value, along
|
|
// with any encrypted data for Eve. Since we have not added
|
|
// any block padding to the final hop, this value is _just_
|
|
// the current height (as the final CTLV delta is covered in
|
|
// the blinded path.
|
|
OutgoingTimeLock: currentHeight,
|
|
AmtToForward: totalAmt,
|
|
EncryptedData: eveEncryptedData,
|
|
// The last hop should also contain final-hop fields such as
|
|
// metadata and total amount.
|
|
Metadata: metadata,
|
|
TotalAmtMsat: totalAmt,
|
|
}
|
|
|
|
expectedRoute := &route.Route{
|
|
SourcePubKey: sourceVertex,
|
|
Hops: []*route.Hop{
|
|
aliceBobRouteHop,
|
|
bobCarolRouteHop,
|
|
carolDaveRouteHop,
|
|
daveEveRouteHop,
|
|
},
|
|
TotalTimeLock: totalTimelock,
|
|
TotalAmount: totalAmount,
|
|
}
|
|
|
|
route, err := newRoute(
|
|
sourceVertex, edges, currentHeight, finalHopParams,
|
|
blindedPath,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedRoute, route)
|
|
}
|
|
|
|
// TestLastHopPayloadSize tests the final hop payload size. The final hop
|
|
// payload structure differes from the intermediate hop payload for both the
|
|
// non-blinded and blinded case.
|
|
func TestLastHopPayloadSize(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
metadata = []byte{21, 22}
|
|
customRecords = map[uint64][]byte{
|
|
record.CustomTypeStart: {1, 2, 3},
|
|
}
|
|
sizeEncryptedData = 100
|
|
encrypedData = bytes.Repeat(
|
|
[]byte{1}, sizeEncryptedData,
|
|
)
|
|
_, blindedPoint = btcec.PrivKeyFromBytes([]byte{5})
|
|
paymentAddr = &[32]byte{1}
|
|
ampOptions = &Options{}
|
|
amtToForward = lnwire.MilliSatoshi(10000)
|
|
finalHopExpiry int32 = 144
|
|
|
|
oneHopBlindedPayment = &BlindedPayment{
|
|
BlindedPath: &sphinx.BlindedPath{
|
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
|
{
|
|
CipherText: encrypedData,
|
|
},
|
|
},
|
|
BlindingPoint: blindedPoint,
|
|
},
|
|
}
|
|
twoHopBlindedPayment = &BlindedPayment{
|
|
BlindedPath: &sphinx.BlindedPath{
|
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
|
{
|
|
CipherText: encrypedData,
|
|
},
|
|
{
|
|
CipherText: encrypedData,
|
|
},
|
|
},
|
|
BlindingPoint: blindedPoint,
|
|
},
|
|
}
|
|
)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
restrictions *RestrictParams
|
|
finalHopExpiry int32
|
|
amount lnwire.MilliSatoshi
|
|
}{
|
|
{
|
|
name: "Non blinded final hop",
|
|
restrictions: &RestrictParams{
|
|
PaymentAddr: paymentAddr,
|
|
DestCustomRecords: customRecords,
|
|
Metadata: metadata,
|
|
Amp: ampOptions,
|
|
},
|
|
amount: amtToForward,
|
|
finalHopExpiry: finalHopExpiry,
|
|
},
|
|
{
|
|
name: "Blinded final hop introduction point",
|
|
restrictions: &RestrictParams{
|
|
BlindedPayment: oneHopBlindedPayment,
|
|
},
|
|
amount: amtToForward,
|
|
finalHopExpiry: finalHopExpiry,
|
|
},
|
|
{
|
|
name: "Blinded final hop of a two hop payment",
|
|
restrictions: &RestrictParams{
|
|
BlindedPayment: twoHopBlindedPayment,
|
|
},
|
|
amount: amtToForward,
|
|
finalHopExpiry: finalHopExpiry,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var mpp *record.MPP
|
|
if tc.restrictions.PaymentAddr != nil {
|
|
mpp = record.NewMPP(
|
|
tc.amount, *tc.restrictions.PaymentAddr,
|
|
)
|
|
}
|
|
|
|
// In case it's an AMP payment we use the max AMP record
|
|
// size to estimate the final hop size.
|
|
var amp *record.AMP
|
|
if tc.restrictions.Amp != nil {
|
|
amp = &record.MaxAmpPayLoadSize
|
|
}
|
|
|
|
var finalHop route.Hop
|
|
if tc.restrictions.BlindedPayment != nil {
|
|
blindedPath := tc.restrictions.BlindedPayment.
|
|
BlindedPath.BlindedHops
|
|
|
|
blindedPoint := tc.restrictions.BlindedPayment.
|
|
BlindedPath.BlindingPoint
|
|
|
|
//nolint:lll
|
|
finalHop = route.Hop{
|
|
AmtToForward: tc.amount,
|
|
OutgoingTimeLock: uint32(tc.finalHopExpiry),
|
|
EncryptedData: blindedPath[len(blindedPath)-1].CipherText,
|
|
}
|
|
if len(blindedPath) == 1 {
|
|
finalHop.BlindingPoint = blindedPoint
|
|
}
|
|
} else {
|
|
//nolint:lll
|
|
finalHop = route.Hop{
|
|
AmtToForward: tc.amount,
|
|
OutgoingTimeLock: uint32(tc.finalHopExpiry),
|
|
Metadata: tc.restrictions.Metadata,
|
|
MPP: mpp,
|
|
AMP: amp,
|
|
CustomRecords: tc.restrictions.DestCustomRecords,
|
|
}
|
|
}
|
|
|
|
payLoad, err := createHopPayload(finalHop, 0, true)
|
|
require.NoErrorf(t, err, "failed to create hop payload")
|
|
|
|
expectedPayloadSize := lastHopPayloadSize(
|
|
tc.restrictions, tc.finalHopExpiry,
|
|
tc.amount,
|
|
)
|
|
|
|
require.Equal(
|
|
t, expectedPayloadSize,
|
|
uint64(payLoad.NumBytes()),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFindBlindedPaths tests that the findBlindedPaths function correctly
|
|
// selects a set of blinded paths to a destination node given various
|
|
// restrictions.
|
|
func TestFindBlindedPaths(t *testing.T) {
|
|
featuresWithRouteBlinding := lnwire.NewFeatureVector(
|
|
lnwire.NewRawFeatureVector(lnwire.RouteBlindingOptional),
|
|
lnwire.Features,
|
|
)
|
|
|
|
policyWithRouteBlinding := &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
Features: featuresWithRouteBlinding,
|
|
}
|
|
|
|
policyWithoutRouteBlinding := &testChannelPolicy{
|
|
Expiry: 144,
|
|
FeeRate: 400,
|
|
MinHTLC: 1,
|
|
MaxHTLC: 100000000,
|
|
}
|
|
|
|
// Set up the following graph where Dave will be our destination node.
|
|
// All the nodes except for A will signal the Route Blinding feature
|
|
// bit.
|
|
//
|
|
// A --- F
|
|
// | |
|
|
// G --- D --- B --- E
|
|
// | |
|
|
// C-----------/
|
|
//
|
|
testChannels := []*testChannel{
|
|
symmetricTestChannel(
|
|
"dave", "alice", 100000, policyWithoutRouteBlinding, 1,
|
|
),
|
|
symmetricTestChannel(
|
|
"dave", "bob", 100000, policyWithRouteBlinding, 2,
|
|
),
|
|
symmetricTestChannel(
|
|
"dave", "charlie", 100000, policyWithRouteBlinding, 3,
|
|
),
|
|
symmetricTestChannel(
|
|
"alice", "frank", 100000, policyWithRouteBlinding, 4,
|
|
),
|
|
symmetricTestChannel(
|
|
"bob", "frank", 100000, policyWithRouteBlinding, 5,
|
|
),
|
|
symmetricTestChannel(
|
|
"eve", "charlie", 100000, policyWithRouteBlinding, 6,
|
|
),
|
|
symmetricTestChannel(
|
|
"bob", "eve", 100000, policyWithRouteBlinding, 7,
|
|
),
|
|
symmetricTestChannel(
|
|
"dave", "george", 100000, policyWithRouteBlinding, 8,
|
|
),
|
|
}
|
|
|
|
ctx := newPathFindingTestContext(
|
|
t, true, testChannels, "dave", lnwire.RouteBlindingOptional,
|
|
)
|
|
|
|
// assertPaths checks that the set of selected paths contains all the
|
|
// expected paths.
|
|
assertPaths := func(paths [][]blindedHop, expectedPaths []string) {
|
|
require.Len(t, paths, len(expectedPaths))
|
|
|
|
actualPaths := make(map[string]bool)
|
|
|
|
for _, path := range paths {
|
|
var label string
|
|
for _, hop := range path {
|
|
label += ctx.aliasFromKey(hop.vertex) + ","
|
|
}
|
|
|
|
actualPaths[strings.TrimRight(label, ",")] = true
|
|
}
|
|
|
|
for _, path := range expectedPaths {
|
|
require.True(t, actualPaths[path])
|
|
}
|
|
}
|
|
|
|
// 1) Restrict the min & max path length such that we only include paths
|
|
// with one hop other than the destination hop.
|
|
paths, err := ctx.findBlindedPaths(&blindedPathRestrictions{
|
|
minNumHops: 1,
|
|
maxNumHops: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// We expect the B->D and C->D paths to be chosen.
|
|
// The A->D path is not chosen since A does not advertise the route
|
|
// blinding feature bit. The G->D path is not chosen since G does not
|
|
// have any other known channels.
|
|
assertPaths(paths, []string{
|
|
"bob,dave",
|
|
"charlie,dave",
|
|
})
|
|
|
|
// 2) Extend the search to include 2 hops other than the destination.
|
|
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
|
minNumHops: 1,
|
|
maxNumHops: 2,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// We expect the following paths:
|
|
// - B, D
|
|
// - F, B, D
|
|
// - E, B, D
|
|
// - C, D
|
|
// - E, C, D
|
|
assertPaths(paths, []string{
|
|
"bob,dave",
|
|
"frank,bob,dave",
|
|
"eve,bob,dave",
|
|
"charlie,dave",
|
|
"eve,charlie,dave",
|
|
})
|
|
|
|
// 3) Extend the search even further and also increase the minimum path
|
|
// length.
|
|
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
|
minNumHops: 2,
|
|
maxNumHops: 3,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// We expect the following paths:
|
|
// - F, B, D
|
|
// - E, B, D
|
|
// - E, C, D
|
|
// - B, E, C, D
|
|
// - C, E, B, D
|
|
assertPaths(paths, []string{
|
|
"frank,bob,dave",
|
|
"eve,bob,dave",
|
|
"eve,charlie,dave",
|
|
"bob,eve,charlie,dave",
|
|
"charlie,eve,bob,dave",
|
|
})
|
|
|
|
// 4) Finally, we will test the special case where the destination node
|
|
// is also the recipient.
|
|
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
|
minNumHops: 0,
|
|
maxNumHops: 0,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assertPaths(paths, []string{
|
|
"dave",
|
|
})
|
|
}
|