Skip to content

Commit 6727f67

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 78b104e commit 6727f67

3 files changed

Lines changed: 519 additions & 0 deletions

File tree

onionmessage/errors.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,13 @@ 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")
1726
)

onionmessage/pathfind.go

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

0 commit comments

Comments
 (0)