routing: path finding test refactored

This commit is contained in:
Joost Jager 2018-06-07 11:00:58 +02:00 committed by Olaoluwa Osuntokun
parent 6c918a1806
commit e52d829168
2 changed files with 221 additions and 185 deletions

@ -558,6 +558,53 @@ func TestFindLowestFeePath(t *testing.T) {
}
}
type expectedHop struct {
alias string
fee lnwire.MilliSatoshi
fwdAmount lnwire.MilliSatoshi
timeLock uint32
}
type basicGraphPathFindingTestCase struct {
target string
paymentAmt btcutil.Amount
totalAmt lnwire.MilliSatoshi
totalTimeLock uint32
expectedHops []expectedHop
}
var basicGraphPathFindingTests = []basicGraphPathFindingTestCase{
// Basic route with one intermediate hop
{target: "sophon", paymentAmt: 100, totalTimeLock: 102, totalAmt: 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, totalTimeLock: 101, totalAmt: 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, totalTimeLock: 103, totalAmt: 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, totalTimeLock: 103, totalAmt: 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},
}}}
func TestBasicGraphPathFinding(t *testing.T) {
t.Parallel()
@ -567,6 +614,24 @@ func TestBasicGraphPathFinding(t *testing.T) {
t.Fatalf("unable to create graph: %v", err)
}
// With the test graph loaded, we'll test some basic path finding using
// the pre-generated graph. Consult the testdata/basic_graph.json file
// to follow along with the assumptions we'll use to test the path
// finding.
for _, testCase := range basicGraphPathFindingTests {
t.Run(testCase.target, func(subT *testing.T) {
testBasicGraphPathFindingCase(subT, graph, aliases, &testCase)
})
}
}
func testBasicGraphPathFindingCase(t *testing.T, graph *channeldb.ChannelGraph,
aliases aliasMap, test *basicGraphPathFindingTestCase) {
expectedHops := test.expectedHops
expectedHopCount := len(expectedHops)
sourceNode, err := graph.SourceNode()
if err != nil {
t.Fatalf("unable to fetch source node: %v", err)
@ -576,17 +641,13 @@ func TestBasicGraphPathFinding(t *testing.T) {
ignoredEdges := make(map[uint64]struct{})
ignoredVertexes := make(map[Vertex]struct{})
// With the test graph loaded, we'll test some basic path finding using
// the pre-generated graph. Consult the testdata/basic_graph.json file
// to follow along with the assumptions we'll use to test the path
// finding.
const (
startingHeight = 100
finalHopCLTV = 1
)
paymentAmt := lnwire.NewMSatFromSatoshis(100)
target := aliases["sophon"]
paymentAmt := lnwire.NewMSatFromSatoshis(test.paymentAmt)
target := aliases[test.target]
path, err := findPath(
nil, graph, nil, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt, nil,
@ -603,171 +664,116 @@ func TestBasicGraphPathFinding(t *testing.T) {
t.Fatalf("unable to create path: %v", err)
}
// The length of the route selected should be of exactly length two.
if len(route.Hops) != 2 {
t.Fatalf("route is of incorrect length, expected %v got %v", 2,
len(route.Hops))
if len(route.Hops) != len(expectedHops) {
t.Fatalf("route is of incorrect length, expected %v got %v",
expectedHopCount, len(route.Hops))
}
// As each hop only decrements a single block from the time-lock, the
// total time lock value should two more than our starting block
// height.
if route.TotalTimeLock != 102 {
t.Fatalf("expected time lock of %v, instead have %v", 2,
route.TotalTimeLock)
}
// Check hop nodes
for i := 0; i < len(expectedHops); i++ {
if !bytes.Equal(route.Hops[i].Channel.Node.PubKeyBytes[:],
aliases[expectedHops[i].alias].SerializeCompressed()) {
// The first hop in the path should be an edge from roasbeef to goku.
if !bytes.Equal(route.Hops[0].Channel.Node.PubKeyBytes[:],
aliases["songoku"].SerializeCompressed()) {
t.Fatalf("first hop should be goku, is instead: %v",
route.Hops[0].Channel.Node.Alias)
}
// The second hop should be from goku to sophon.
if !bytes.Equal(route.Hops[1].Channel.Node.PubKeyBytes[:],
aliases["sophon"].SerializeCompressed()) {
t.Fatalf("second hop should be sophon, is instead: %v",
route.Hops[0].Channel.Node.Alias)
t.Fatalf("%v-th hop should be %v, is instead: %v",
i, expectedHops[i], route.Hops[i].Channel.Node.Alias)
}
}
// Next, we'll assert that the "next hop" field in each route payload
// properly points to the channel ID that the HTLC should be forwarded
// along.
hopPayloads := route.ToHopPayloads()
if len(hopPayloads) != 2 {
if len(hopPayloads) != expectedHopCount {
t.Fatalf("incorrect number of hop payloads: expected %v, got %v",
2, len(hopPayloads))
expectedHopCount, len(hopPayloads))
}
// The first hop should point to the second hop.
var expectedHop [8]byte
binary.BigEndian.PutUint64(expectedHop[:], route.Hops[1].Channel.ChannelID)
if !bytes.Equal(hopPayloads[0].NextAddress[:], expectedHop[:]) {
t.Fatalf("first hop has incorrect next hop: expected %x, got %x",
expectedHop[:], hopPayloads[0].NextAddress)
// Hops should point to the next hop
for i := 0; i < len(expectedHops)-1; i++ {
var expectedHop [8]byte
binary.BigEndian.PutUint64(expectedHop[:], route.Hops[i+1].Channel.ChannelID)
if !bytes.Equal(hopPayloads[i].NextAddress[:], expectedHop[:]) {
t.Fatalf("first hop has incorrect next hop: expected %x, got %x",
expectedHop[:], hopPayloads[i].NextAddress)
}
}
// The second hop should have a next hop value of all zeroes in order
// The final hop should have a next hop value of all zeroes in order
// to indicate it's the exit hop.
var exitHop [8]byte
if !bytes.Equal(hopPayloads[1].NextAddress[:], exitHop[:]) {
lastHopIndex := len(expectedHops) - 1
if !bytes.Equal(hopPayloads[lastHopIndex].NextAddress[:], exitHop[:]) {
t.Fatalf("first hop has incorrect next hop: expected %x, got %x",
exitHop[:], hopPayloads[0].NextAddress)
exitHop[:], hopPayloads[lastHopIndex].NextAddress)
}
// We'll also assert that the outgoing CLTV value for each hop was set
// accordingly.
if route.Hops[0].OutgoingTimeLock != 101 {
t.Fatalf("expected outgoing time-lock of %v, instead have %v",
1, route.Hops[0].OutgoingTimeLock)
}
if route.Hops[1].OutgoingTimeLock != 101 {
t.Fatalf("outgoing time-lock for final hop is incorrect: "+
"expected %v, got %v", 1, route.Hops[1].OutgoingTimeLock)
var expectedTotalFee lnwire.MilliSatoshi = 0
for i := 0; i < expectedHopCount; i++ {
// We'll ensure that the amount to forward, and fees
// computed for each hop are correct.
if route.Hops[i].Fee != expectedHops[i].fee {
t.Fatalf("fee incorrect for hop %v: expected %v, got %v",
i, expectedHops[i].fee, route.Hops[i].Fee)
}
if route.Hops[i].AmtToForward != expectedHops[i].fwdAmount {
t.Fatalf("forwarding amount for hop %v incorrect: "+
"expected %v, got %v",
i, expectedHops[i].fwdAmount,
route.Hops[i].AmtToForward)
}
// We'll also assert that the outgoing CLTV value for each
// hop was set accordingly.
if route.Hops[i].OutgoingTimeLock != expectedHops[i].timeLock {
t.Fatalf("outgoing time-lock for hop %v is incorrect: "+
"expected %v, got %v", i,
expectedHops[i].timeLock,
route.Hops[i].OutgoingTimeLock)
}
expectedTotalFee += expectedHops[i].fee
}
// Additionally, we'll ensure that the amount to forward, and fees
// computed for each hop are correct.
firstHopFee := computeFee(
paymentAmt, route.Hops[1].Channel.ChannelEdgePolicy,
)
if route.Hops[0].Fee != firstHopFee {
t.Fatalf("first hop fee incorrect: expected %v, got %v",
firstHopFee, route.Hops[0].Fee)
if route.TotalAmount != test.totalAmt {
t.Fatalf("total amount incorrect: "+
"expected %v, got %v",
test.totalAmt, route.TotalAmount)
}
if route.TotalAmount != paymentAmt+firstHopFee {
t.Fatalf("first hop forwarding amount incorrect: expected %v, got %v",
paymentAmt+firstHopFee, route.TotalAmount)
}
if route.Hops[1].Fee != 0 {
t.Fatalf("first hop fee incorrect: expected %v, got %v",
firstHopFee, 0)
}
if route.Hops[1].AmtToForward != paymentAmt {
t.Fatalf("second hop forwarding amount incorrect: expected %v, got %v",
paymentAmt+firstHopFee, route.Hops[1].AmtToForward)
}
// Finally, the next and prev hop maps should be properly set.
//
// The previous hop from goku should be the channel from roasbeef, and
// the next hop should be the channel to sophon.
gokuPrevChan, ok := route.prevHopChannel(aliases["songoku"])
if !ok {
t.Fatalf("goku didn't have next chan but should have")
}
if gokuPrevChan.ChannelID != route.Hops[0].Channel.ChannelID {
t.Fatalf("incorrect prev chan: expected %v, got %v",
gokuPrevChan.ChannelID, route.Hops[0].Channel.ChannelID)
}
gokuNextChan, ok := route.nextHopChannel(aliases["songoku"])
if !ok {
t.Fatalf("goku didn't have prev chan but should have")
}
if gokuNextChan.ChannelID != route.Hops[1].Channel.ChannelID {
t.Fatalf("incorrect prev chan: expected %v, got %v",
gokuNextChan.ChannelID, route.Hops[1].Channel.ChannelID)
}
// Sophon shouldn't have a next chan, but she should have a prev chan.
if _, ok := route.nextHopChannel(aliases["sophon"]); ok {
t.Fatalf("incorrect next hop map, no vertexes should " +
"be after sophon")
}
sophonPrevEdge, ok := route.prevHopChannel(aliases["sophon"])
if !ok {
t.Fatalf("sophon didn't have prev chan but should have")
}
if sophonPrevEdge.ChannelID != route.Hops[1].Channel.ChannelID {
t.Fatalf("incorrect prev chan: expected %v, got %v",
sophonPrevEdge.ChannelID, route.Hops[1].Channel.ChannelID)
}
// Next, attempt to query for a path to Luo Ji for 100 satoshis, there
// exist two possible paths in the graph, but the shorter (1 hop) path
// should be selected.
target = aliases["luoji"]
path, err = findPath(
nil, graph, nil, sourceNode, target, ignoredVertexes,
ignoredEdges, paymentAmt, nil,
)
if err != nil {
t.Fatalf("unable to find route: %v", err)
}
route, err = newRoute(
paymentAmt, noFeeLimit, sourceVertex, path, startingHeight,
finalHopCLTV,
)
if err != nil {
t.Fatalf("unable to create path: %v", err)
}
// The length of the path should be exactly one hop as it's the
// "shortest" known path in the graph.
if len(route.Hops) != 1 {
t.Fatalf("shortest path not selected, should be of length 1, "+
"is instead: %v", len(route.Hops))
}
// As we have a direct path, the total time lock value should be
// exactly the current block height plus one.
if route.TotalTimeLock != 101 {
t.Fatalf("expected time lock of %v, instead have %v", 1,
if route.TotalTimeLock != test.totalTimeLock {
t.Fatalf("expected time lock of %v, instead have %v", 2,
route.TotalTimeLock)
}
// Additionally, since this is a single-hop payment, we shouldn't have
// to pay any fees in total, so the total amount should be the payment
// amount.
if route.TotalAmount != paymentAmt {
t.Fatalf("incorrect total amount, expected %v got %v",
paymentAmt, route.TotalAmount)
// The next and prev hop maps should be properly set.
for i := 0; i < expectedHopCount; i++ {
prevChan, ok := route.prevHopChannel(aliases[expectedHops[i].alias])
if !ok {
t.Fatalf("hop didn't have prev chan but should have")
}
if prevChan.ChannelID != route.Hops[i].Channel.ChannelID {
t.Fatalf("incorrect prev chan: expected %v, got %v",
prevChan.ChannelID, route.Hops[i].Channel.ChannelID)
}
}
for i := 0; i < expectedHopCount-1; i++ {
nextChan, ok := route.nextHopChannel(aliases[expectedHops[i].alias])
if !ok {
t.Fatalf("hop didn't have prev chan but should have")
}
if nextChan.ChannelID != route.Hops[i+1].Channel.ChannelID {
t.Fatalf("incorrect prev chan: expected %v, got %v",
nextChan.ChannelID, route.Hops[i+1].Channel.ChannelID)
}
}
// Final hop shouldn't have a next chan
if _, ok := route.nextHopChannel(aliases[expectedHops[lastHopIndex].alias]); ok {
t.Fatalf("incorrect next hop map, no vertexes should " +
"be after sophon")
}
}
@ -1289,10 +1295,10 @@ func TestRouteFailDisabledEdge(t *testing.T) {
ignoredEdges := make(map[uint64]struct{})
ignoredVertexes := make(map[Vertex]struct{})
// First, we'll try to route from roasbeef -> songoku. This should
// succeed without issue, and return a single path.
target := aliases["songoku"]
payAmt := lnwire.NewMSatFromSatoshis(10000)
// First, we'll try to route from roasbeef -> sophon. This should
// succeed without issue, and return a single path via phamnuwen
target := aliases["sophon"]
payAmt := lnwire.NewMSatFromSatoshis(120000)
_, err = findPath(
nil, graph, nil, sourceNode, target, ignoredVertexes,
ignoredEdges, payAmt, nil,
@ -1301,14 +1307,14 @@ func TestRouteFailDisabledEdge(t *testing.T) {
t.Fatalf("unable to find path: %v", err)
}
// First, we'll modify the edge from roasbeef -> songoku, to read that
// First, we'll modify the edge from roasbeef -> phamnuwen, to read that
// it's disabled.
_, gokuEdge, _, err := graph.FetchChannelEdgesByID(12345)
_, _, phamnuwenEdge, err := graph.FetchChannelEdgesByID(999991)
if err != nil {
t.Fatalf("unable to fetch goku's edge: %v", err)
}
gokuEdge.Flags = lnwire.ChanUpdateDisabled
if err := graph.UpdateEdgePolicy(gokuEdge); err != nil {
phamnuwenEdge.Flags = lnwire.ChanUpdateDisabled | lnwire.ChanUpdateDirection
if err := graph.UpdateEdgePolicy(phamnuwenEdge); err != nil {
t.Fatalf("unable to update edge: %v", err)
}

@ -2,32 +2,33 @@
"info": [
"This file encodes a basic graph that resembles the following ascii graph:",
"",
" 50k satoshis ┌──────┐ ",
" 50k satoshis ┌──────┐ ",
" ┌───────────────────▶│luo ji│◀─┐ ",
" │ └──────┘ │ ",
" │ │ ",
" │ │ ",
" │ │ ",
" │ │ ",
" │ │ ",
" │ └──────┘ │ ┌──────┐ ",
" │ │ | elst | ",
" │ │ └──────┘ ",
" │ │ ",
" │ │ | 100k sat ",
" │ │ ",
" ▼ │ ┌──────┐ ",
" ┌────────┐ │ │sophon│◀┐ ",
" │satoshi │ │ └──────┘ │ ",
" └────────┘ │ │ ",
" ▲ │ │ 500 satoshis ",
" │ ┌───────────────────┘ │ ",
" │ │ 100k satoshis │ ",
" │ │ │ ",
" │ │ │ ┌────────┐ ",
" └──────────┤ └─▶│son goku│ ",
" 10k satoshis │ └────────┘ ",
" │ ▲ ",
" │ │ ",
" │ │ ",
" ▼ │ ",
" ┌──────────┐ │ ",
" │ roasbeef │◀─────────────────────────────────────┘ ",
" └──────────┘ 100k satoshis ",
" └────────┘ │ ▲ │ ",
" ▲ │ | │ 110k satoshis ",
" │ ┌───────────────────┘ | │ ",
" │ │ 100k satoshis | │ ",
" │ │ | │ ",
" │ │ 120k sat | │ ┌────────┐ ",
" └──────────┤ (hi fee) ▼ └─▶│son goku│ ",
" 10k satoshis │ ┌────────────┐ └────────┘ ",
" │ | pham nuwen | ▲ ",
" │ └────────────┘ │ ",
" │ ▲ │ ",
" ▼ | 120k sat (hi fee) │ ",
" ┌──────────┐ | │ ",
" │ roasbeef │◀──────────────┴──────────────────────┘ ",
" └──────────┘ 100k satoshis ",
" the graph also includes a channel from roasbeef to sophon via pham nuwen"
],
"nodes": [
@ -60,9 +61,38 @@
"source": false,
"pubkey": "02a1d2856be336a58af08989aea0d8c41e072ccc392c46f8ce0e6e069f002035f3",
"alias": "phamnuwen"
},
{
"source": false,
"pubkey": "02a4b236b69b09b8efe6ccf822fa95ee95a0196451f4d066a450b7489e2e354a64",
"alias": "elst"
}
],
"edges": [
{
"node_1": "02a4b236b69b09b8efe6ccf822fa95ee95a0196451f4d066a450b7489e2e354a64",
"node_2": "036264734b40c9e91d3d990a8cdfbbe23b5b0b7ad3cd0e080a25dcd05d39eeb7eb",
"channel_id": 15433,
"channel_point": "33bd5d49a50e284221561b91e781f1fca0d60341c9f9dd785b5e379a6d88af3d:0",
"flags": 1,
"expiry": 1,
"min_htlc": 1000,
"fee_base_msat": 200,
"fee_rate": 0,
"capacity": 100000
},
{
"node_1": "02a4b236b69b09b8efe6ccf822fa95ee95a0196451f4d066a450b7489e2e354a64",
"node_2": "036264734b40c9e91d3d990a8cdfbbe23b5b0b7ad3cd0e080a25dcd05d39eeb7eb",
"channel_id": 15433,
"channel_point": "33bd5d49a50e284221561b91e781f1fca0d60341c9f9dd785b5e379a6d88af3d:0",
"flags": 0,
"expiry": 1,
"min_htlc": 1000,
"fee_base_msat": 200,
"fee_rate": 0,
"capacity": 100000
},
{
"node_1": "02a1d2856be336a58af08989aea0d8c41e072ccc392c46f8ce0e6e069f002035f3",
"node_2": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6",
@ -72,8 +102,8 @@
"expiry": 1,
"min_htlc": 1000,
"fee_base_msat": 10000,
"fee_rate": 1000000,
"capacity": 100000
"fee_rate": 100000,
"capacity": 120000
},
{
"node_1": "02a1d2856be336a58af08989aea0d8c41e072ccc392c46f8ce0e6e069f002035f3",
@ -84,8 +114,8 @@
"expiry": 1,
"min_htlc": 1000,
"fee_base_msat": 10000,
"fee_rate": 1000000,
"capacity": 100000
"fee_rate": 100000,
"capacity": 120000
},
{
"node_1": "02a1d2856be336a58af08989aea0d8c41e072ccc392c46f8ce0e6e069f002035f3",
@ -96,8 +126,8 @@
"expiry": 1,
"min_htlc": 1000,
"fee_base_msat": 10000,
"fee_rate": 1000000,
"capacity": 100000
"fee_rate": 100000,
"capacity": 120000
},
{
"node_1": "02a1d2856be336a58af08989aea0d8c41e072ccc392c46f8ce0e6e069f002035f3",
@ -108,8 +138,8 @@
"expiry": 1,
"min_htlc": 1000,
"fee_base_msat": 10000,
"fee_rate": 1000000,
"capacity": 100000
"fee_rate": 100000,
"capacity": 120000
},
{
"node_1": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6",
@ -145,7 +175,7 @@
"min_htlc": 1,
"fee_base_msat": 10,
"fee_rate": 1000,
"capacity": 500
"capacity": 110000
},
{
"node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add",
@ -157,7 +187,7 @@
"min_htlc": 1,
"fee_base_msat": 10,
"fee_rate": 1000,
"capacity": 500
"capacity": 110000
},
{
"node_1": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6",