diff --git a/docs/release-notes/release-notes-0.20.0.md b/docs/release-notes/release-notes-0.20.0.md index 19cff1f38..f1bbd1efe 100644 --- a/docs/release-notes/release-notes-0.20.0.md +++ b/docs/release-notes/release-notes-0.20.0.md @@ -37,7 +37,8 @@ circuit. The indices are only available for forwarding events saved after v0.20. * The `lncli addinvoice --blind` command now has the option to include a - [chained channels](https://github.com/lightningnetwork/lnd/pull/9127) + chained channels [1](https://github.com/lightningnetwork/lnd/pull/9127) + [2](https://github.com/lightningnetwork/lnd/pull/9925) incoming list `--blinded_path_incoming_channel_list` which gives users the control of specifying the channels they prefer to receive the payment on. With the option to specify multiple channels this control can be extended to diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index 2aadb5755..0e3c61f61 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -1428,25 +1428,55 @@ func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) { // pre-specified. func testPartiallySpecifiedBlindedPath(ht *lntest.HarnessTest) { // Create a six hop network: - // Alice -> Bob -> Carol -> Dave -> Eve -> Frank. - chanAmt := btcutil.Amount(100000) - cfgs := [][]string{nil, nil, nil, nil, nil, nil} + // Alice -> Bob -> Carol -> Dave -> Eve -> Frank. + // Carol will be the node generating invoices containing blinded paths. chanPoints, nodes := ht.CreateSimpleNetwork( - cfgs, lntest.OpenChannelParams{Amt: chanAmt}, + [][]string{nil, nil, nil, nil, nil, nil}, + lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: chanAmt / 2, + }, ) - alice, bob, carol, dave, eve := nodes[0], nodes[1], nodes[2], nodes[3], - nodes[4] - - chanPointAliceBob, chanPointBobCarol, chanPointCarolDave := - chanPoints[0], chanPoints[1], chanPoints[2] + var ( + alice = nodes[0] + bob = nodes[1] + carol = nodes[2] + dave = nodes[3] + eve = nodes[4] + frank = nodes[5] + ) // Lookup full channel info so that we have channel ids for our route. - aliceBobChan := ht.GetChannelByChanPoint(alice, chanPointAliceBob) - bobCarolChan := ht.GetChannelByChanPoint(bob, chanPointBobCarol) - carolDaveChan := ht.GetChannelByChanPoint(carol, chanPointCarolDave) + aliceBobChan := ht.GetChannelByChanPoint(alice, chanPoints[0]) + bobCarolChan := ht.GetChannelByChanPoint(bob, chanPoints[1]) + carolDaveChan := ht.GetChannelByChanPoint(carol, chanPoints[2]) - // Let carol set no incoming channel restrictions. + // assertPathDetails is a helper function that asserts the details of + // the blinded paths returned in the invoice. + assertPathDetails := func(invoice *lnrpc.AddInvoiceResponse, + expPathLengths int, expIntroNodes ...[]byte) { + + payReq := carol.RPC.DecodePayReq(invoice.PaymentRequest) + paths := payReq.BlindedPaths + + introNodes := make([][]byte, 0, len(paths)) + for _, p := range paths { + require.Len( + ht, p.BlindedPath.BlindedHops, expPathLengths, + ) + + introNodes = append( + introNodes, p.BlindedPath.IntroductionNode, + ) + } + require.ElementsMatch(ht, introNodes, expIntroNodes) + } + + // Let carol set no incoming channel restrictions. With a min of 1 hop, + // we expect the following blinded paths to be included: + // 1) Bob->Carol + // 2) Dave->Carol var ( minNumRealHops uint32 = 1 numHops uint32 = 1 @@ -1460,122 +1490,94 @@ func testPartiallySpecifiedBlindedPath(ht *lntest.HarnessTest) { NumHops: &numHops, }, }) + assertPathDetails(invoice, 2, bob.PubKey[:], dave.PubKey[:]) - introNodesFound := make([][]byte, 0) - introNodesExpected := [][]byte{bob.PubKey[:], dave.PubKey[:]} - - // Assert that it contains two blinded path with only 2 hops each one. - payReq := carol.RPC.DecodePayReq(invoice.PaymentRequest) - require.Len(ht, payReq.BlindedPaths, 2) - path := payReq.BlindedPaths[0].BlindedPath - require.Len(ht, path.BlindedHops, 2) - introNodesFound = append(introNodesFound, path.IntroductionNode) - path = payReq.BlindedPaths[1].BlindedPath - require.Len(ht, path.BlindedHops, 2) - introNodesFound = append(introNodesFound, path.IntroductionNode) - - // Assert the introduction nodes without caring about the routes order. - require.ElementsMatch(ht, introNodesExpected, introNodesFound) - - // Let carol choose the wrong incoming channel. - var ( - incomingChannelList = []uint64{aliceBobChan.ChanId} - ) - - err := fmt.Sprintf("incoming channel %v is not seen as owned by node", - aliceBobChan.ChanId) - + // Let carol choose an invalid incoming channel list for the blinded + // path. It is invalid since it does not connect to her node. + // Assert that the expected error is returned. carol.RPC.AddInvoiceAssertErr( &lnrpc.Invoice{ Memo: "test", ValueMsat: 10_000_000, IsBlinded: true, BlindedPathConfig: &lnrpc.BlindedPathConfig{ - MinNumRealHops: &minNumRealHops, - NumHops: &numHops, - IncomingChannelList: incomingChannelList, + MinNumRealHops: &minNumRealHops, + NumHops: &numHops, + IncomingChannelList: []uint64{ + aliceBobChan.ChanId, + }, }, }, - err, + fmt.Sprintf("incoming channel %v is not seen as owned by node", + aliceBobChan.ChanId), ) // Let Carol set the incoming channel list greater than minimum number // of real hops. - incomingChannelList = []uint64{aliceBobChan.ChanId, bobCarolChan.ChanId} - err = fmt.Sprintf("minimum number of blinded path hops (%d) must be "+ - "greater than or equal to the number of hops specified on "+ - "the chained channels (%d)", minNumRealHops, - len(incomingChannelList), - ) - + // Assert that the expected error is returned. carol.RPC.AddInvoiceAssertErr( &lnrpc.Invoice{ Memo: "test", ValueMsat: 10_000_000, IsBlinded: true, BlindedPathConfig: &lnrpc.BlindedPathConfig{ - MinNumRealHops: &minNumRealHops, - NumHops: &numHops, - IncomingChannelList: incomingChannelList, + MinNumRealHops: &minNumRealHops, + NumHops: &numHops, + IncomingChannelList: []uint64{ + aliceBobChan.ChanId, + bobCarolChan.ChanId, + }, }, }, - err, + "must be greater than or equal to the number of hops "+ + "specified on the chained channels", ) // Let Carol choose an incoming channel that points to a node in the // omission set. - incomingChannelList = []uint64{bobCarolChan.ChanId} - var nodeOmissionList [][]byte - nodeOmissionList = append(nodeOmissionList, bob.PubKey[:]) - - err = fmt.Sprintf("cannot simultaneously be included in the omission " + - "set and in the partially specified path", - ) - carol.RPC.AddInvoiceAssertErr( &lnrpc.Invoice{ Memo: "test", ValueMsat: 10_000_000, IsBlinded: true, BlindedPathConfig: &lnrpc.BlindedPathConfig{ - MinNumRealHops: &minNumRealHops, - NumHops: &numHops, - IncomingChannelList: incomingChannelList, - NodeOmissionList: nodeOmissionList, + MinNumRealHops: &minNumRealHops, + NumHops: &numHops, + IncomingChannelList: []uint64{ + bobCarolChan.ChanId, + }, + NodeOmissionList: [][]byte{ + bob.PubKey[:], + }, }, }, - err, + "cannot simultaneously be included in the omission set and "+ + "in the partially specified path", ) // Let carol restrict bob as incoming channel. - incomingChannelList = []uint64{bobCarolChan.ChanId} - invoice = carol.RPC.AddInvoice(&lnrpc.Invoice{ Memo: "test", ValueMsat: 10_000_000, IsBlinded: true, BlindedPathConfig: &lnrpc.BlindedPathConfig{ - MinNumRealHops: &minNumRealHops, - NumHops: &numHops, - IncomingChannelList: incomingChannelList, + MinNumRealHops: &minNumRealHops, + NumHops: &numHops, + IncomingChannelList: []uint64{ + bobCarolChan.ChanId, + }, }, }) // Assert that it contains a single blinded path with only // 2 hops, with bob as the introduction node. - payReq = carol.RPC.DecodePayReq(invoice.PaymentRequest) - require.Len(ht, payReq.BlindedPaths, 1) - path = payReq.BlindedPaths[0].BlindedPath - require.Len(ht, path.BlindedHops, 2) - require.EqualValues(ht, path.IntroductionNode, bob.PubKey[:]) + assertPathDetails(invoice, 2, bob.PubKey[:]) - // Now let alice pay the invoice. + // Check that Alice can pay the invoice. ht.CompletePaymentRequests(alice, []string{invoice.PaymentRequest}) - // Let carol restrict dave as incoming channel and max Hops as 2 + // Let carol restrict dave as incoming channel and max hops as 2. numHops = 2 - incomingChannelList = []uint64{carolDaveChan.ChanId} - invoice = carol.RPC.AddInvoice(&lnrpc.Invoice{ Memo: "test", ValueMsat: 10_000_000, @@ -1583,16 +1585,15 @@ func testPartiallySpecifiedBlindedPath(ht *lntest.HarnessTest) { BlindedPathConfig: &lnrpc.BlindedPathConfig{ MinNumRealHops: &minNumRealHops, NumHops: &numHops, - IncomingChannelList: incomingChannelList, + IncomingChannelList: []uint64{carolDaveChan.ChanId}, }, }) - // Assert that it contains one path with 3 hops, with dave as the - // introduction node. The path alice -> bob -> carol is discarded - // because alice is a dead-end. - payReq = carol.RPC.DecodePayReq(invoice.PaymentRequest) - require.Len(ht, payReq.BlindedPaths, 1) - path = payReq.BlindedPaths[0].BlindedPath - require.Len(ht, path.BlindedHops, 3) - require.EqualValues(ht, path.IntroductionNode, eve.PubKey[:]) + // Assert that it contains two paths: one 3 hop one starting at Eve and + // a 3 hop one starting at Dave (this one will be padded with a dummy + // hop) in order to keep all the paths equidistant. + assertPathDetails(invoice, 3, eve.PubKey[:], dave.PubKey[:]) + + // Check that Frank can pay the invoice. + ht.CompletePaymentRequests(frank, []string{invoice.PaymentRequest}) } diff --git a/routing/pathfind.go b/routing/pathfind.go index ace174fef..f489173f9 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -1227,24 +1227,46 @@ func findBlindedPaths(g Graph, target route.Vertex, } var ( - // The target node is always the last hop in the path. - incomingPath = []blindedHop{{vertex: target}} - whiteListedNodes = map[route.Vertex]bool{target: true} + // The target node is always the last hop in the path, and so + // we add it to the incoming path from the get-go. Any additions + // to the slice should be prepended. + incomingPath = []blindedHop{{ + vertex: target, + }} + + // supportsRouteBlinding is a list of nodes that we can assume + // support route blinding without needing to rely on the feature + // bits announced in their node announcement. Since we are + // finding a path to the target node, we can assume it supports + // route blinding. + supportsRouteBlinding = map[route.Vertex]bool{ + target: true, + } + visited = make(map[route.Vertex]bool) - errChanFound = errors.New("found incoming channel") nextTarget = target + haveIncomingPath = len(restrictions.incomingChainedChannels) > 0 + + // errChanFound is an error variable we return from the DB + // iteration call below when we have found the channel we are + // looking for. This lets us exit the iteration early. + errChanFound = errors.New("found incoming channel") ) for _, chanID := range restrictions.incomingChainedChannels { + // Mark that we have visited this node so that we don't revisit + // it later on when we call "processNodeForBlindedPath". visited[nextTarget] = true err := g.ForEachNodeDirectedChannel(nextTarget, func(channel *graphdb.DirectedChannel) error { - // Not the right channel, continue to the node's - // other channels. + // This is not the right channel, continue to + // the node's other channels. if channel.ChannelID != chanID { return nil } + // We found the channel in question. Prepend it + // to the incoming path. incomingPath = append([]blindedHop{ { vertex: channel.OtherNode, @@ -1259,39 +1281,37 @@ func findBlindedPaths(g Graph, target route.Vertex, return errChanFound }, ) - if err == nil { + // We expect errChanFound to be returned if the channel in + // question was found. + if !errors.Is(err, errChanFound) && err != nil { + return nil, err + } else if err == nil { return nil, fmt.Errorf("incoming channel %d is not "+ "seen as owned by node %v", chanID, nextTarget) } - if !errors.Is(err, errChanFound) { - return nil, err - } // Check that the user didn't accidentally add a channel that - // is owned by a node in the node omission set + // is owned by a node in the node omission set. if restrictions.nodeOmissionSet.Contains(nextTarget) { return nil, fmt.Errorf("node %v cannot simultaneously "+ "be included in the omission set and in the "+ "partially specified path", nextTarget) } - if whiteListedNodes[nextTarget] { + // Check that we have not already visited the next target node + // since this would mean a circular incoming path. + if visited[nextTarget] { return nil, fmt.Errorf("a circular route cannot be " + "specified for the incoming blinded path") } - whiteListedNodes[nextTarget] = true + + supportsRouteBlinding[nextTarget] = true } - // If the node is not the destination node, then it is required that the - // node advertise the route blinding feature-bit in order for it to be - // chosen as a node on the blinded path. - // We skip checking the target node, as accepting blinded payments - // (via invoice) doesn't imply support for routing them (via node - // announcement). - // We skip checking incomingChainedChannels nodes, as we might not yet - // have an updated node announcement for them. - supportsRouteBlinding := func(node route.Vertex) (bool, error) { - if whiteListedNodes[node] { + // A helper closure which checks if the node in question has advertised + // that it supports route blinding. + nodeSupportsRouteBlinding := func(node route.Vertex) (bool, error) { + if supportsRouteBlinding[node] { return true, nil } @@ -1308,39 +1328,46 @@ func findBlindedPaths(g Graph, target route.Vertex, // conditions such as: The maxHops number being reached or reaching // a node that doesn't have any other edges - in that final case, the // whole path should be ignored. + // + // NOTE: any paths returned will end at the "nextTarget" node meaning + // that if we have a fixed list of incoming chained channels, then this + // fixed list must be appended to any of the returned paths. paths, _, err := processNodeForBlindedPath( - g, nextTarget, supportsRouteBlinding, visited, restrictions, + g, nextTarget, nodeSupportsRouteBlinding, visited, restrictions, ) if err != nil { return nil, err } - orderedPaths := make([][]blindedHop, len(paths)) + // Reverse each path so that the order is correct (from introduction + // node to last hop node) and then append the incoming path (if any was + // specified) to the end of the path. + orderedPaths := make([][]blindedHop, 0, len(paths)) + for _, path := range paths { + sort.Slice(path, func(i, j int) bool { + return j < i + }) - // When there is no path to add, but incomingChainedChannels can be - // used. - lenChainedChannels := int8(len(restrictions.incomingChainedChannels)) - if len(paths) == 0 && lenChainedChannels > 0 { - orderedPaths = [][]blindedHop{incomingPath} - } else { - // Reverse each path so that the order is correct (from - // introduction node to last hop node) and then append this node - // on as the destination of each path. - for i, path := range paths { - sort.Slice(path, func(i, j int) bool { - return j < i - }) + orderedPaths = append( + orderedPaths, append(path, incomingPath...), + ) + } - orderedPaths[i] = append(path, incomingPath...) - } + // There is a chance we have an incoming path that by itself satisfies + // the minimum hop restriction. In that case, we add it as its own path. + if haveIncomingPath && + len(incomingPath) > int(restrictions.minNumHops) { + + orderedPaths = append(orderedPaths, incomingPath) } // Handle the special case that allows a blinded path with the - // introduction node as the destination node. - if restrictions.minNumHops == 0 && lenChainedChannels == 0 { + // introduction node as the destination node. This only applies if no + // incoming path was specified since in that case, by definition, the + // caller wants a non-zero length blinded path. + if restrictions.minNumHops == 0 && !haveIncomingPath { singleHopPath := [][]blindedHop{{{vertex: target}}} - //nolint:makezero orderedPaths = append( orderedPaths, singleHopPath..., ) diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index e1962f5d9..6a7821003 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -538,6 +538,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, aliasMap := make(map[string]route.Vertex) privKeyMap := make(map[string]*btcec.PrivateKey) + channelIDs := make(map[route.Vertex]map[route.Vertex]uint64) nodeIndex := byte(0) addNodeWithAlias := func(alias string, features *lnwire.FeatureVector) ( @@ -652,6 +653,16 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, node1Vertex, node2Vertex = node2Vertex, node1Vertex } + if _, ok := channelIDs[node1Vertex]; !ok { + channelIDs[node1Vertex] = map[route.Vertex]uint64{} + } + channelIDs[node1Vertex][node2Vertex] = channelID + + if _, ok := channelIDs[node2Vertex]; !ok { + channelIDs[node2Vertex] = map[route.Vertex]uint64{} + } + channelIDs[node2Vertex][node1Vertex] = channelID + // We first insert the existence of the edge between the two // nodes. edgeInfo := models.ChannelEdgeInfo{ @@ -765,6 +776,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, mcBackend: graphBackend, aliasMap: aliasMap, privKeyMap: privKeyMap, + channelIDs: channelIDs, links: links, }, nil } @@ -3192,6 +3204,16 @@ func newPathFindingTestContext(t *testing.T, useCache bool, return ctx } +func (c *pathFindingTestContext) nodePairChannel(alias1, alias2 string) uint64 { + node1 := c.keyFromAlias(alias1) + node2 := c.keyFromAlias(alias2) + + channel, ok := c.testGraphInstance.channelIDs[node1][node2] + require.True(c.t, ok) + + return channel +} + func (c *pathFindingTestContext) keyFromAlias(alias string) route.Vertex { return c.testGraphInstance.aliasMap[alias] } @@ -3876,7 +3898,7 @@ func TestFindBlindedPaths(t *testing.T) { "eve,bob,dave", }) - // 5) Finally, we will test the special case where the destination node + // 5) We will also test the special case where the destination node // is also the recipient. paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{ minNumHops: 0, @@ -3888,93 +3910,138 @@ func TestFindBlindedPaths(t *testing.T) { "dave", }) - // 6) Restrict the min & max path length such that we only include paths - // with one being only the intro-node and the others with one hop other - // than the destination hop. + // 6) Now, we will test some cases where the user manually specifies + // the first few incoming channels of a route. + // + // 6.1) Let the user specify the B-D channel as the last hop with a + // max of 1 hop. paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{ - minNumHops: 0, + minNumHops: 1, maxNumHops: 1, + incomingChainedChannels: []uint64{ + ctx.nodePairChannel("bob", "dave"), + }, + }) + require.NoError(t, err) + + // If the max number of hops is 1, then only the B->D path is chosen + assertPaths(paths, []string{ + "bob,dave", + }) + + // 6.2) Extend the search to include 2 hops along with the B-D channel + // restriction. + paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{ + minNumHops: 1, + maxNumHops: 2, + incomingChainedChannels: []uint64{ + ctx.nodePairChannel("bob", "dave"), + }, }) require.NoError(t, err) // We expect the following paths: - // - D // - B, D - // - C, D - // 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{ - "dave", - "bob,dave", - "charlie,dave", - }) - - // 7) Restrict the min & max path length such that we only include paths - // with one hop other than the destination hop. Now with bob-dave as the - // incoming channel. - paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{ - minNumHops: 1, - maxNumHops: 1, - incomingChainedChannels: []uint64{2}, - }) - require.NoError(t, err) - - // Expect only B->D path. - assertPaths(paths, []string{ - "bob,dave", - }) - - // 8) Extend the search to include 2 hops other than the destination, - // with bob-dave as the incoming channel. - paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{ - minNumHops: 1, - maxNumHops: 2, - incomingChainedChannels: []uint64{2}, - }) - require.NoError(t, err) - - // We expect the following paths: // - F, B, D // - E, B, D assertPaths(paths, []string{ + "bob,dave", "frank,bob,dave", "eve,bob,dave", }) - // 9) Extend the search even further and also increase the minimum path - // length, but this time with charlie-dave as the incoming channel. - paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{ - minNumHops: 2, - maxNumHops: 3, - incomingChainedChannels: []uint64{3}, - }) - require.NoError(t, err) - - // We expect the following paths: - // - E, C, D - // - B, E, C, D - assertPaths(paths, []string{ - "eve,charlie,dave", - "bob,eve,charlie,dave", - }) - - // 10) Repeat the above test but instruct the function to never use - // charlie. + // 6.3) Repeat the above test but instruct the function to never use + // bob. This should fail since bob owns one of the channels in the + // partially specified path. _, err = ctx.findBlindedPaths(&blindedPathRestrictions{ - minNumHops: 2, - maxNumHops: 3, - nodeOmissionSet: fn.NewSet(ctx.keyFromAlias("charlie")), - incomingChainedChannels: []uint64{3}, + minNumHops: 1, + maxNumHops: 2, + nodeOmissionSet: fn.NewSet(ctx.keyFromAlias("bob")), + incomingChainedChannels: []uint64{ + ctx.nodePairChannel("bob", "dave"), + }, }) require.ErrorContains(t, err, "cannot simultaneously be included in "+ "the omission set and in the partially specified path") - // 11) Test the circular route error. - _, err = ctx.findBlindedPaths(&blindedPathRestrictions{ - minNumHops: 2, - maxNumHops: 3, - incomingChainedChannels: []uint64{2, 7, 6, 3}, + // 6.4) Repeat it again but this time omit frank and demonstrate that + // the resulting set contains all the results from 6.2 except for the + // frank path. + paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{ + minNumHops: 1, + maxNumHops: 2, + nodeOmissionSet: fn.NewSet(ctx.keyFromAlias("frank")), + incomingChainedChannels: []uint64{ + ctx.nodePairChannel("bob", "dave"), + }, + }) + require.NoError(t, err) + + // We expect the following paths: + // - B, D + // - E, B, D + assertPaths(paths, []string{ + "bob,dave", + "eve,bob,dave", + }) + + // 6.5) Users may specify channels to nodes that do not signal route + // blinding (like A). So if we specify the A-D channel, we should get + // valid paths. + paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{ + minNumHops: 1, + maxNumHops: 4, + incomingChainedChannels: []uint64{ + ctx.nodePairChannel("dave", "alice"), + }, + }) + require.NoError(t, err) + + // We expect the following paths: + // - A, D + // - F, A, D + // - B, F, A, D + // - E, B, F, A, D + assertPaths(paths, []string{ + "alice,dave", + "frank,alice,dave", + "bob,frank,alice,dave", + "eve,bob,frank,alice,dave", + }) + + // 6.6) Assert that an error is returned if a user accidentally tries + // to force a circular path. + _, err = ctx.findBlindedPaths(&blindedPathRestrictions{ + minNumHops: 2, + maxNumHops: 3, + incomingChainedChannels: []uint64{ + ctx.nodePairChannel("dave", "alice"), + ctx.nodePairChannel("alice", "frank"), + ctx.nodePairChannel("frank", "bob"), + ctx.nodePairChannel("bob", "dave"), + }, + }) + require.ErrorContains(t, err, "circular route") + + // 6.7) Test specifying a chain of incoming channels. We specify + // the following incoming list: [A->D, F->A]. + paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{ + minNumHops: 1, + maxNumHops: 4, + incomingChainedChannels: []uint64{ + ctx.nodePairChannel("dave", "alice"), + ctx.nodePairChannel("alice", "frank"), + }, + }) + require.NoError(t, err) + + // We expect the following paths: + // - F, A, D + // - B, F, A, D + // - E, B, F, A, D + assertPaths(paths, []string{ + "frank,alice,dave", + "bob,frank,alice,dave", + "eve,bob,frank,alice,dave", }) - require.ErrorContains(t, err, "a circular route cannot be specified") } diff --git a/routing/router.go b/routing/router.go index 0ce9a1ca8..b0c6bd323 100644 --- a/routing/router.go +++ b/routing/router.go @@ -699,8 +699,8 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex, "path since it resulted in an low "+ "probability path(%.3f)", route.ChanIDString(routeWithProbability.route), - routeWithProbability.probability, - ) + routeWithProbability.probability) + continue }