Skip to content

Commit e027f49

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 d344984 commit e027f49

File tree

3 files changed

+529
-0
lines changed

3 files changed

+529
-0
lines changed

onionmessage/errors.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,22 @@ 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+
// ErrNilReceptionist is returned when a nil receptionist is provided.
19+
ErrNilReceptionist = errors.New("receptionist cannot be nil")
20+
21+
// ErrNilRouter is returned when a nil router is provided.
22+
ErrNilRouter = errors.New("router cannot be nil")
23+
24+
// ErrNilResolver is returned when a nil resolver is provided.
25+
ErrNilResolver = errors.New("resolver cannot be nil")
26+
27+
// ErrNoPathFound is returned when no path exists between the source
28+
// and destination nodes that supports onion messaging.
29+
ErrNoPathFound = errors.New("no path found to destination")
30+
31+
// ErrDestinationNoOnionSupport is returned when the destination node
32+
// does not advertise support for onion messages.
33+
ErrDestinationNoOnionSupport = errors.New("destination does not " +
34+
"support onion messages")
1735
)

onionmessage/pathfind.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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,
22+
source, destination route.Vertex,
23+
maxHops int) (*OnionMessagePath, error) {
24+
25+
// Check that the destination supports onion messaging.
26+
destFeatures, err := graph.FetchNodeFeatures(ctx, destination)
27+
if err != nil {
28+
return nil, err
29+
}
30+
31+
if !destFeatures.HasFeature(lnwire.OnionMessagesOptional) {
32+
return nil, ErrDestinationNoOnionSupport
33+
}
34+
35+
// If source == destination, return empty path.
36+
if source == destination {
37+
return &OnionMessagePath{}, nil
38+
}
39+
40+
parent := make(map[route.Vertex]route.Vertex)
41+
visited := make(map[route.Vertex]bool)
42+
featureCache := make(map[route.Vertex]bool)
43+
44+
// Cache the destination as supporting onion messages (checked above).
45+
featureCache[destination] = true
46+
47+
// supportsOnionMessages is a closure that checks (with caching) whether
48+
// a node advertises the onion messages feature bit.
49+
supportsOnionMessages := func(node route.Vertex) bool {
50+
if cached, ok := featureCache[node]; ok {
51+
return cached
52+
}
53+
54+
features, err := graph.FetchNodeFeatures(ctx, node)
55+
if err != nil {
56+
log.Tracef("Unable to fetch features for node %v: %v",
57+
node.String(), err)
58+
featureCache[node] = false
59+
return false
60+
}
61+
62+
supports := features.HasFeature(
63+
lnwire.OnionMessagesOptional,
64+
)
65+
66+
featureCache[node] = supports
67+
68+
return supports
69+
}
70+
71+
visited[source] = true
72+
73+
queue := []route.Vertex{source}
74+
depth := 0
75+
76+
for len(queue) > 0 {
77+
depth++
78+
if depth > maxHops {
79+
break
80+
}
81+
82+
nextQueue := make([]route.Vertex, 0)
83+
84+
for _, current := range queue {
85+
err := graph.ForEachNodeDirectedChannel(ctx, current,
86+
func(channel *graphdb.DirectedChannel) error {
87+
neighbor := channel.OtherNode
88+
89+
if visited[neighbor] {
90+
return nil
91+
}
92+
93+
// Skip nodes that don't support
94+
// onion messaging.
95+
if !supportsOnionMessages(neighbor) {
96+
return nil
97+
}
98+
99+
visited[neighbor] = true
100+
parent[neighbor] = current
101+
102+
if neighbor == destination {
103+
return errBFSDone
104+
}
105+
106+
nextQueue = append(
107+
nextQueue, neighbor,
108+
)
109+
110+
return nil
111+
},
112+
func() {
113+
// Reset callback - nothing to
114+
// reset for BFS.
115+
},
116+
)
117+
118+
// Check if we found the destination.
119+
if 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 = &bfsDoneError{}
139+
140+
type bfsDoneError struct{}
141+
142+
func (e *bfsDoneError) Error() string { return "bfs done" }
143+
144+
// reconstructPath rebuilds the path from destination back to source using the
145+
// parent map, returning the hops in forward order (excluding source).
146+
func reconstructPath(parent map[route.Vertex]route.Vertex,
147+
source, destination route.Vertex) *OnionMessagePath {
148+
149+
var path []route.Vertex
150+
151+
current := destination
152+
for current != source {
153+
path = append(path, current)
154+
current = parent[current]
155+
}
156+
157+
// Reverse the path to get source-to-destination order.
158+
for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
159+
path[i], path[j] = path[j], path[i]
160+
}
161+
162+
return &OnionMessagePath{Hops: path}
163+
}

0 commit comments

Comments
 (0)