Merge pull request #9127 from MPins/issue-8993

Add the option on path creator to specify the incoming channel on blinded path
This commit is contained in:
Oliver Gugger authored and GitHub committed 2025-06-05 08:47:05 +02:00
commit fc906f2a65
15 files changed
+1694 -1197

No files matched your search

+85 -11
View File
@@ -1192,6 +1192,11 @@ type blindedPathRestrictions struct {
// nodeOmissionSet holds a set of node IDs of nodes that we should
// ignore during blinded path selection.
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
@@ -1221,11 +1226,72 @@ func findBlindedPaths(g Graph, target route.Vertex,
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
// 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 node == target {
if whiteListedNodes[node] {
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
// whole path should be ignored.
paths, _, err := processNodeForBlindedPath(
g, target, supportsRouteBlinding, nil, restrictions,
g, nextTarget, supportsRouteBlinding, visited, restrictions,
)
if err != nil {
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))
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
// introduction node as the destination node.
if restrictions.minNumHops == 0 {
if restrictions.minNumHops == 0 && lenChainedChannels == 0 {
singleHopPath := [][]blindedHop{{{vertex: target}}}
//nolint:makezero
+90
View File
@@ -3855,4 +3855,94 @@ func TestFindBlindedPaths(t *testing.T) {
assertPaths(paths, []string{
"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")
}
+16
View File
@@ -773,3 +773,19 @@ func (r *Route) String() string {
b.String(), r.TotalTimeLock,
)
}
// ChanIDString returns the route's channel IDs as a formatted string.
func ChanIDString(r *Route) string {
var b strings.Builder
for i, hop := range r.Hops {
b.WriteString(fmt.Sprintf("%v",
strconv.FormatUint(hop.ChannelID, 10),
))
if i != len(r.Hops)-1 {
b.WriteString(" -> ")
}
}
return b.String()
}
+27 -11
View File
@@ -610,6 +610,11 @@ type BlindedPathRestrictions struct {
// NodeOmissionSet is a set of nodes that should not be used within any
// of the blinded paths that we generate.
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
@@ -620,11 +625,14 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
// First, find a set of candidate paths given the destination node and
// path length restrictions.
incomingChainedChannels := restrictions.IncomingChainedChannels
minDistanceFromIntroNode := restrictions.MinDistanceFromIntroNode
paths, err := findBlindedPaths(
r.cfg.RoutingGraph, destination, &blindedPathRestrictions{
minNumHops: restrictions.MinDistanceFromIntroNode,
maxNumHops: restrictions.NumHops,
nodeOmissionSet: restrictions.NodeOmissionSet,
minNumHops: minDistanceFromIntroNode,
maxNumHops: restrictions.NumHops,
nodeOmissionSet: restrictions.NodeOmissionSet,
incomingChainedChannels: incomingChainedChannels,
},
)
if err != nil {
@@ -676,19 +684,27 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
prevNode = path[j].vertex
}
// Don't bother adding a route if its success probability less
// minimum that can be assigned to any single pair.
if totalRouteProbability <= DefaultMinRouteProbability {
continue
}
routes = append(routes, &routeWithProbability{
routeWithProbability := &routeWithProbability{
route: &route.Route{
SourcePubKey: introNode,
Hops: hops,
},
probability: totalRouteProbability,
})
}
// Don't bother adding a route if its success probability less
// minimum that can be assigned to any single pair.
if totalRouteProbability <= DefaultMinRouteProbability {
log.Debugf("Not using route (%v) as a blinded "+
"path since it resulted in an low "+
"probability path(%.3f)",
route.ChanIDString(routeWithProbability.route),
routeWithProbability.probability,
)
continue
}
routes = append(routes, routeWithProbability)
}
// Sort the routes based on probability.