Skip to content

Commit ea5cfb0

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 ea5cfb0

File tree

3 files changed

+524
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)