routing: add incoming channel chain to blinded paths
In this commit, the blindedPathRestrictions are expanded to include a list of incoming channels that must be included in any blinded path. The unit tests are expanded to test this new logic.
This commit is contained in:
3 files changed
+186
-14
No files matched your search
+85
-11
@@ -1192,6 +1192,11 @@ type blindedPathRestrictions struct {
|
|||||||
// nodeOmissionSet holds a set of node IDs of nodes that we should
|
// nodeOmissionSet holds a set of node IDs of nodes that we should
|
||||||
// ignore during blinded path selection.
|
// ignore during blinded path selection.
|
||||||
nodeOmissionSet fn.Set[route.Vertex]
|
nodeOmissionSet fn.Set[route.Vertex]
|
||||||
|
|
||||||
|
// incomingChainedChannels holds the chained channels list specified
|
||||||
|
// via scid (short channel id) starting from a channel which points to
|
||||||
|
// the receiver node.
|
||||||
|
incomingChainedChannels []uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// blindedHop holds the information about a hop we have selected for a blinded
|
// blindedHop holds the information about a hop we have selected for a blinded
|
||||||
@@ -1221,11 +1226,72 @@ func findBlindedPaths(g Graph, target route.Vertex,
|
|||||||
restrictions.minNumHops)
|
restrictions.minNumHops)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// The target node is always the last hop in the path.
|
||||||
|
incomingPath = []blindedHop{{vertex: target}}
|
||||||
|
whiteListedNodes = map[route.Vertex]bool{target: true}
|
||||||
|
visited = make(map[route.Vertex]bool)
|
||||||
|
errChanFound = errors.New("found incoming channel")
|
||||||
|
nextTarget = target
|
||||||
|
)
|
||||||
|
for _, chanID := range restrictions.incomingChainedChannels {
|
||||||
|
visited[nextTarget] = true
|
||||||
|
|
||||||
|
err := g.ForEachNodeDirectedChannel(nextTarget,
|
||||||
|
func(channel *graphdb.DirectedChannel) error {
|
||||||
|
// Not the right channel, continue to the node's
|
||||||
|
// other channels.
|
||||||
|
if channel.ChannelID != chanID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
incomingPath = append([]blindedHop{
|
||||||
|
{
|
||||||
|
vertex: channel.OtherNode,
|
||||||
|
channelID: channel.ChannelID,
|
||||||
|
edgeCapacity: channel.Capacity,
|
||||||
|
},
|
||||||
|
}, incomingPath...)
|
||||||
|
|
||||||
|
// Update the target node.
|
||||||
|
nextTarget = channel.OtherNode
|
||||||
|
|
||||||
|
return errChanFound
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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
|
||||||
|
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] {
|
||||||
|
return nil, fmt.Errorf("a circular route cannot be " +
|
||||||
|
"specified for the incoming blinded path")
|
||||||
|
}
|
||||||
|
whiteListedNodes[nextTarget] = true
|
||||||
|
}
|
||||||
|
|
||||||
// If the node is not the destination node, then it is required that the
|
// 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
|
// node advertise the route blinding feature-bit in order for it to be
|
||||||
// chosen as a node on the blinded path.
|
// 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) {
|
supportsRouteBlinding := func(node route.Vertex) (bool, error) {
|
||||||
if node == target {
|
if whiteListedNodes[node] {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1243,27 +1309,35 @@ func findBlindedPaths(g Graph, target route.Vertex,
|
|||||||
// a node that doesn't have any other edges - in that final case, the
|
// a node that doesn't have any other edges - in that final case, the
|
||||||
// whole path should be ignored.
|
// whole path should be ignored.
|
||||||
paths, _, err := processNodeForBlindedPath(
|
paths, _, err := processNodeForBlindedPath(
|
||||||
g, target, supportsRouteBlinding, nil, restrictions,
|
g, nextTarget, supportsRouteBlinding, visited, restrictions,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
|
||||||
orderedPaths := make([][]blindedHop, len(paths))
|
orderedPaths := make([][]blindedHop, len(paths))
|
||||||
for i, path := range paths {
|
|
||||||
sort.Slice(path, func(i, j int) bool {
|
|
||||||
return j < i
|
|
||||||
})
|
|
||||||
|
|
||||||
orderedPaths[i] = append(path, blindedHop{vertex: target})
|
// 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[i] = append(path, incomingPath...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the special case that allows a blinded path with the
|
// Handle the special case that allows a blinded path with the
|
||||||
// introduction node as the destination node.
|
// introduction node as the destination node.
|
||||||
if restrictions.minNumHops == 0 {
|
if restrictions.minNumHops == 0 && lenChainedChannels == 0 {
|
||||||
singleHopPath := [][]blindedHop{{{vertex: target}}}
|
singleHopPath := [][]blindedHop{{{vertex: target}}}
|
||||||
|
|
||||||
//nolint:makezero
|
//nolint:makezero
|
||||||
|
|||||||
@@ -3860,4 +3860,94 @@ func TestFindBlindedPaths(t *testing.T) {
|
|||||||
assertPaths(paths, []string{
|
assertPaths(paths, []string{
|
||||||
"dave",
|
"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.
|
||||||
|
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
|
minNumHops: 0,
|
||||||
|
maxNumHops: 1,
|
||||||
|
})
|
||||||
|
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{
|
||||||
|
"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.
|
||||||
|
_, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||||
|
minNumHops: 2,
|
||||||
|
maxNumHops: 3,
|
||||||
|
nodeOmissionSet: fn.NewSet(ctx.keyFromAlias("charlie")),
|
||||||
|
incomingChainedChannels: []uint64{3},
|
||||||
|
})
|
||||||
|
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},
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "a circular route cannot be specified")
|
||||||
}
|
}
|
||||||
+11
-3
@@ -610,6 +610,11 @@ type BlindedPathRestrictions struct {
|
|||||||
// NodeOmissionSet is a set of nodes that should not be used within any
|
// NodeOmissionSet is a set of nodes that should not be used within any
|
||||||
// of the blinded paths that we generate.
|
// of the blinded paths that we generate.
|
||||||
NodeOmissionSet fn.Set[route.Vertex]
|
NodeOmissionSet fn.Set[route.Vertex]
|
||||||
|
|
||||||
|
// IncomingChainedChannels holds the chained channels list (specified
|
||||||
|
// via channel id) starting from a channel which points to the receiver
|
||||||
|
// node.
|
||||||
|
IncomingChainedChannels []uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindBlindedPaths finds a selection of paths to the destination node that can
|
// FindBlindedPaths finds a selection of paths to the destination node that can
|
||||||
@@ -620,11 +625,14 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
|
|||||||
|
|
||||||
// First, find a set of candidate paths given the destination node and
|
// First, find a set of candidate paths given the destination node and
|
||||||
// path length restrictions.
|
// path length restrictions.
|
||||||
|
incomingChainedChannels := restrictions.IncomingChainedChannels
|
||||||
|
minDistanceFromIntroNode := restrictions.MinDistanceFromIntroNode
|
||||||
paths, err := findBlindedPaths(
|
paths, err := findBlindedPaths(
|
||||||
r.cfg.RoutingGraph, destination, &blindedPathRestrictions{
|
r.cfg.RoutingGraph, destination, &blindedPathRestrictions{
|
||||||
minNumHops: restrictions.MinDistanceFromIntroNode,
|
minNumHops: minDistanceFromIntroNode,
|
||||||
maxNumHops: restrictions.NumHops,
|
maxNumHops: restrictions.NumHops,
|
||||||
nodeOmissionSet: restrictions.NodeOmissionSet,
|
nodeOmissionSet: restrictions.NodeOmissionSet,
|
||||||
|
incomingChainedChannels: incomingChainedChannels,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in new issue
Block a user