Skip to content

Commit 673b9c4

Browse files
committed
onionmessage: add BFS pathfinding for onion messages
In this commit we add FindPath, a BFS-based shsortest-path algorithm that finds routes through the channel graph for onion messages. The search filters nodes by the OnionMessage feature bits (38/39). We also add a unit tests covering: direct neighbor routing, multi-hop paths, feature-bit filtering, missing destination nodes, destination without onion support, max hop limits, cycle handling, and shortest-path selection. choice of BFS is because there isn't any weight involve.
1 parent 167f03b commit 673b9c4

3 files changed

Lines changed: 534 additions & 0 deletions

File tree

onionmessage/errors.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,16 @@ var (
1414
// ErrSCIDEmpty is returned when the short channel ID is missing from
1515
// the route data.
1616
ErrSCIDEmpty = errors.New("short channel ID empty")
17+
18+
// ErrNoPathFound is returned when no path exists between the source
19+
// and destination nodes that supports onion messaging.
20+
ErrNoPathFound = errors.New("no path found to destination")
21+
22+
// ErrDestinationNoOnionSupport is returned when the destination node
23+
// does not advertise support for onion messages.
24+
ErrDestinationNoOnionSupport = errors.New("destination does not " +
25+
"support onion messages")
26+
27+
// ErrNodeNotFound is returned when the node is not found in the graph.
28+
ErrNodeNotFound = errors.New("node not found in graph")
1729
)

onionmessage/pathfind.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package onionmessage
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
graphdb "github.com/lightningnetwork/lnd/graph/db"
8+
"github.com/lightningnetwork/lnd/lnwire"
9+
"github.com/lightningnetwork/lnd/routing/route"
10+
)
11+
12+
// onionMessagePath represents a route found for an onion message. It is a slice
13+
// of vertices ordered from the first-hop peer to the destination.
14+
type onionMessagePath []route.Vertex
15+
16+
// FindPath finds the shortest path (by hop count) from source to destination
17+
// through nodes that support onion messaging (feature bit 38/39). It uses a
18+
// standard BFS on the channel graph filtered by the OnionMessagesOptional
19+
// feature bit.
20+
func FindPath(ctx context.Context, graph graphdb.NodeTraverser, source,
21+
destination route.Vertex, maxHops int) (onionMessagePath, error) {
22+
23+
// Check that the destination supports onion messaging.
24+
destFeatures, err := graph.FetchNodeFeatures(ctx, destination)
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
// An empty feature vector means the node is absent from our graph.
30+
// In that case, we send back a NotFound error.
31+
if len(destFeatures.Features()) == 0 {
32+
return nil, ErrNodeNotFound
33+
}
34+
35+
if !destFeatures.HasFeature(lnwire.OnionMessagesOptional) {
36+
return nil, ErrDestinationNoOnionSupport
37+
}
38+
39+
// If source == destination, return empty path.
40+
if source == destination {
41+
return onionMessagePath{}, nil
42+
}
43+
44+
parent := make(map[route.Vertex]route.Vertex)
45+
visited := make(map[route.Vertex]bool)
46+
47+
visited[source] = true
48+
49+
queue := []route.Vertex{source}
50+
depth := 0
51+
52+
for len(queue) > 0 {
53+
depth++
54+
if depth > maxHops {
55+
break
56+
}
57+
58+
nextQueue := make([]route.Vertex, 0)
59+
60+
for _, current := range queue {
61+
err := graph.ForEachNodeDirectedChannel(ctx, current,
62+
func(channel *graphdb.DirectedChannel) error {
63+
neighbor := channel.OtherNode
64+
65+
if visited[neighbor] {
66+
return nil
67+
}
68+
69+
// Mark visited before the feature check
70+
// so we never fetch features for the
71+
// same node twice.
72+
visited[neighbor] = true
73+
74+
// Skip nodes that don't support onion
75+
// messaging.
76+
feats, err := graph.FetchNodeFeatures(
77+
ctx, neighbor,
78+
)
79+
if err != nil {
80+
// If the context is canceled or
81+
// deadline exceeded, propagate
82+
// the error.
83+
84+
if ctx.Err() != nil {
85+
return err
86+
}
87+
88+
log.Tracef("Unable to fetch "+
89+
"features for node "+
90+
"%v: %v",
91+
neighbor.String(), err)
92+
93+
return nil
94+
}
95+
96+
if !feats.HasFeature(
97+
lnwire.OnionMessagesOptional,
98+
) {
99+
100+
return nil
101+
}
102+
103+
parent[neighbor] = current
104+
105+
if neighbor == destination {
106+
return errBFSDone
107+
}
108+
109+
nextQueue = append(
110+
nextQueue, neighbor,
111+
)
112+
113+
return nil
114+
},
115+
func() {},
116+
)
117+
118+
// Check if we found the destination.
119+
if errors.Is(err, errBFSDone) {
120+
return reconstructPath(
121+
parent, source, destination,
122+
), nil
123+
}
124+
125+
if err != nil {
126+
return nil, err
127+
}
128+
}
129+
130+
queue = nextQueue
131+
}
132+
133+
return nil, ErrNoPathFound
134+
}
135+
136+
// errBFSDone is a sentinel error used internally to break out of the
137+
// ForEachNodeDirectedChannel callback when the destination is found.
138+
var errBFSDone = errors.New("bfs done")
139+
140+
// reconstructPath rebuilds the path from destination back to source using the
141+
// parent map, returning the hops in forward order (excluding source).
142+
func reconstructPath(parent map[route.Vertex]route.Vertex,
143+
source, destination route.Vertex) onionMessagePath {
144+
145+
// Calculate path length to pre-allocate the slice.
146+
pathLen := 0
147+
for curr := destination; curr != source; curr = parent[curr] {
148+
pathLen++
149+
}
150+
151+
// Populate the path in correct order, avoiding a separate reversal
152+
// step.
153+
path := make(onionMessagePath, pathLen)
154+
curr := destination
155+
for i := pathLen - 1; i >= 0; i-- {
156+
path[i] = curr
157+
curr = parent[curr]
158+
}
159+
160+
return path
161+
}

0 commit comments

Comments
 (0)