|
1 | 1 | package itest |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "testing" |
5 | 4 | "time" |
6 | 5 |
|
7 | | - "github.com/btcsuite/btcd/btcec/v2" |
8 | | - sphinx "github.com/lightningnetwork/lightning-onion" |
9 | | - "github.com/lightningnetwork/lnd/fn/v2" |
10 | 6 | "github.com/lightningnetwork/lnd/lnrpc" |
11 | 7 | "github.com/lightningnetwork/lnd/lntest" |
12 | | - "github.com/lightningnetwork/lnd/lntest/node" |
13 | 8 | "github.com/lightningnetwork/lnd/lnwire" |
14 | | - "github.com/lightningnetwork/lnd/onionmessage" |
15 | | - "github.com/lightningnetwork/lnd/record" |
16 | 9 | "github.com/stretchr/testify/require" |
17 | 10 | ) |
18 | 11 |
|
19 | | -// onionMessageTestCase defines a test case for onion message forwarding. |
20 | | -type onionMessageTestCase struct { |
21 | | - name string |
22 | | - |
23 | | - // setup is called before building the blinded path to perform any |
24 | | - // additional setup (e.g., opening channels for SCID tests). |
25 | | - setup func(ht *lntest.HarnessTest, alice, bob, carol *node.HarnessNode) |
26 | | - |
27 | | - // buildPath builds the blinded path for the test. It returns the |
28 | | - // blinded path info, the final hop payloads, the first hop node, |
29 | | - // and the expected receiving peer pubkey for validation. |
30 | | - buildPath func(ht *lntest.HarnessTest, alice, bob, |
31 | | - carol *node.HarnessNode) ( |
32 | | - blindedPath *sphinx.BlindedPathInfo, |
33 | | - finalHopTLVs []*lnwire.FinalHopTLV, |
34 | | - firstHop *node.HarnessNode, |
35 | | - expectedPeer []byte, |
36 | | - ) |
37 | | -} |
38 | | - |
39 | | -// testOnionMessageForwarding tests forwarding of onion messages across |
40 | | -// multiple scenarios including forwarding by node ID, by SCID, and with |
41 | | -// concatenated blinded paths. |
| 12 | +// testOnionMessageForwarding tests that onion messages are correctly forwarded |
| 13 | +// across multiple hops. Alice sends to Carol; Bob relays the message. |
| 14 | +// |
| 15 | +//nolint:ll |
42 | 16 | func testOnionMessageForwarding(ht *lntest.HarnessTest) { |
43 | | - // Spin up three nodes for the test network. |
44 | 17 | alice := ht.NewNodeWithCoins("Alice", nil) |
45 | 18 | bob := ht.NewNodeWithCoins("Bob", nil) |
46 | 19 | carol := ht.NewNode("Carol", nil) |
47 | 20 |
|
48 | | - // Connect nodes so they can share gossip and forward messages. |
| 21 | + // Connect nodes so they can forward messages. |
49 | 22 | ht.ConnectNodesPerm(alice, bob) |
50 | 23 | ht.ConnectNodesPerm(bob, carol) |
51 | 24 |
|
52 | | - testCases := []onionMessageTestCase{ |
53 | | - { |
54 | | - name: "forward via next node id", |
55 | | - buildPath: func(ht *lntest.HarnessTest, alice, bob, |
56 | | - carol *node.HarnessNode) ( |
57 | | - *sphinx.BlindedPathInfo, |
58 | | - []*lnwire.FinalHopTLV, |
59 | | - *node.HarnessNode, []byte, |
60 | | - ) { |
61 | | - |
62 | | - return buildForwardNextNodePath( |
63 | | - ht, bob, carol, |
64 | | - ) |
65 | | - }, |
66 | | - }, |
67 | | - { |
68 | | - name: "forward via scid", |
69 | | - setup: func(ht *lntest.HarnessTest, alice, bob, |
70 | | - carol *node.HarnessNode) { |
71 | | - |
72 | | - // Open a channel between Bob and Carol so we |
73 | | - // have an SCID to use. |
74 | | - chanPoint := ht.OpenChannel( |
75 | | - bob, carol, |
76 | | - lntest.OpenChannelParams{Amt: 100000}, |
77 | | - ) |
78 | | - |
79 | | - // Wait for the channel to be in the graph so |
80 | | - // the SCID can be resolved. |
81 | | - ht.AssertChannelInGraph(bob, chanPoint) |
82 | | - }, |
83 | | - buildPath: func(ht *lntest.HarnessTest, alice, bob, |
84 | | - carol *node.HarnessNode) ( |
85 | | - *sphinx.BlindedPathInfo, |
86 | | - []*lnwire.FinalHopTLV, |
87 | | - *node.HarnessNode, []byte, |
88 | | - ) { |
89 | | - |
90 | | - return buildForwardSCIDPath(ht, bob, carol) |
91 | | - }, |
92 | | - }, |
93 | | - { |
94 | | - name: "forward concatenated path", |
95 | | - buildPath: buildConcatenatedPath, |
96 | | - }, |
97 | | - } |
98 | | - |
99 | | - for _, tc := range testCases { |
100 | | - success := ht.Run(tc.name, func(t *testing.T) { |
101 | | - // Run optional setup. |
102 | | - if tc.setup != nil { |
103 | | - tc.setup(ht, alice, bob, carol) |
104 | | - } |
105 | | - |
106 | | - // Build the blinded path for this test case. |
107 | | - blindedPath, finalPayloads, firstHop, expectedPeer := |
108 | | - tc.buildPath(ht, alice, bob, carol) |
109 | | - |
110 | | - // Build the onion message. |
111 | | - onionMsg, _ := onionmessage.BuildOnionMessage( |
112 | | - ht.T, blindedPath, finalPayloads, |
113 | | - ) |
114 | | - |
115 | | - // Subscribe to onion messages on Carol before sending. |
116 | | - msgClient, cancel := carol.RPC.SubscribeOnionMessages() |
117 | | - defer cancel() |
118 | | - |
119 | | - messages := make(chan *lnrpc.OnionMessageUpdate) |
120 | | - go func() { |
121 | | - for { |
122 | | - msg, err := msgClient.Recv() |
123 | | - if err != nil { |
124 | | - return |
125 | | - } |
126 | | - select { |
127 | | - case messages <- msg: |
128 | | - case <-ht.Context().Done(): |
129 | | - return |
130 | | - } |
131 | | - } |
132 | | - }() |
| 25 | + // Open channels so that all three nodes appear in each other's channel |
| 26 | + // graph with edges. Without graph edges the BFS pathfinder cannot |
| 27 | + // discover a route from Alice through Bob to Carol. |
| 28 | + chanPointAB := ht.OpenChannel( |
| 29 | + alice, bob, lntest.OpenChannelParams{Amt: 100_000}, |
| 30 | + ) |
| 31 | + chanPointBC := ht.OpenChannel( |
| 32 | + bob, carol, lntest.OpenChannelParams{Amt: 100_000}, |
| 33 | + ) |
133 | 34 |
|
134 | | - // Send the message from Alice to the first hop. |
135 | | - pathKey := blindedPath.SessionKey.PubKey(). |
136 | | - SerializeCompressed() |
137 | | - aliceMsg := &lnrpc.SendOnionMessageRequest{ |
138 | | - Peer: firstHop.PubKey[:], |
139 | | - PathKey: pathKey, |
140 | | - Onion: onionMsg.OnionBlob, |
| 35 | + // Wait until Alice has both edges in her graph so that pathfinding |
| 36 | + // can traverse Alice → Bob → Carol. |
| 37 | + ht.AssertChannelInGraph(alice, chanPointAB) |
| 38 | + ht.AssertChannelInGraph(alice, chanPointBC) |
| 39 | + |
| 40 | + // Subscribe to onion messages on Carol before sending. |
| 41 | + msgClient, cancel := carol.RPC.SubscribeOnionMessages() |
| 42 | + defer cancel() |
| 43 | + |
| 44 | + messages := make(chan *lnrpc.OnionMessageUpdate) |
| 45 | + go func() { |
| 46 | + for { |
| 47 | + msg, err := msgClient.Recv() |
| 48 | + if err != nil { |
| 49 | + return |
141 | 50 | } |
142 | | - alice.RPC.SendOnionMessage(aliceMsg) |
143 | | - |
144 | | - // Wait for Carol to receive the message. |
145 | 51 | select { |
146 | | - case msg := <-messages: |
147 | | - require.Equal( |
148 | | - ht, expectedPeer, msg.Peer, |
149 | | - "unexpected peer", |
150 | | - ) |
151 | | - |
152 | | - // Verify final payload if provided. |
153 | | - for _, fp := range finalPayloads { |
154 | | - tlvType := uint64(fp.TLVType) |
155 | | - require.Equal( |
156 | | - ht, fp.Value, |
157 | | - msg.CustomRecords[tlvType], |
158 | | - ) |
159 | | - } |
160 | | - |
161 | | - case <-time.After(lntest.DefaultTimeout): |
162 | | - ht.Fatalf("carol did not receive onion message") |
| 52 | + case messages <- msg: |
| 53 | + case <-ht.Context().Done(): |
| 54 | + return |
163 | 55 | } |
164 | | - }) |
165 | | - if !success { |
166 | | - break |
167 | 56 | } |
168 | | - } |
169 | | -} |
170 | | - |
171 | | -// buildForwardNextNodePath builds a blinded path for forwarding via explicit |
172 | | -// next node ID. Path: Alice -> Bob -> Carol. |
173 | | -func buildForwardNextNodePath(ht *lntest.HarnessTest, bob, |
174 | | - carol *node.HarnessNode) ( |
175 | | - *sphinx.BlindedPathInfo, []*lnwire.FinalHopTLV, |
176 | | - *node.HarnessNode, []byte, |
177 | | -) { |
178 | | - |
179 | | - bobPubKey, err := btcec.ParsePubKey(bob.PubKey[:]) |
180 | | - require.NoError(ht.T, err) |
181 | | - |
182 | | - carolPubKey, err := btcec.ParsePubKey(carol.PubKey[:]) |
183 | | - require.NoError(ht.T, err) |
184 | | - |
185 | | - // Bob's payload: forward to Carol via node ID. |
186 | | - nextNode := fn.NewLeft[*btcec.PublicKey, lnwire.ShortChannelID]( |
187 | | - carolPubKey, |
188 | | - ) |
189 | | - bobData := record.NewNonFinalBlindedRouteDataOnionMessage( |
190 | | - nextNode, nil, nil, |
191 | | - ) |
192 | | - |
193 | | - // Carol's payload: final hop (empty route data). |
194 | | - carolData := &record.BlindedRouteData{} |
195 | | - |
196 | | - hops := []*sphinx.HopInfo{ |
197 | | - { |
198 | | - NodePub: bobPubKey, |
199 | | - PlainText: onionmessage.EncodeBlindedRouteData( |
200 | | - ht.T, bobData, |
201 | | - ), |
202 | | - }, |
203 | | - { |
204 | | - NodePub: carolPubKey, |
205 | | - PlainText: onionmessage.EncodeBlindedRouteData( |
206 | | - ht.T, carolData, |
207 | | - ), |
208 | | - }, |
209 | | - } |
210 | | - |
211 | | - blindedPath := onionmessage.BuildBlindedPath(ht.T, hops) |
212 | | - |
213 | | - finalHopTLVs := []*lnwire.FinalHopTLV{ |
214 | | - { |
215 | | - TLVType: lnwire.InvoiceRequestNamespaceType, |
216 | | - Value: []byte{1, 2, 3}, |
217 | | - }, |
218 | | - } |
219 | | - |
220 | | - return blindedPath, finalHopTLVs, bob, bob.PubKey[:] |
221 | | -} |
222 | | - |
223 | | -// buildForwardSCIDPath builds a blinded path for forwarding via SCID. |
224 | | -// Requires a channel between Bob and Carol to exist. |
225 | | -// Path: Alice -> Bob -> Carol (Bob uses SCID to identify Carol). |
226 | | -func buildForwardSCIDPath(ht *lntest.HarnessTest, bob, |
227 | | - carol *node.HarnessNode) ( |
228 | | - *sphinx.BlindedPathInfo, []*lnwire.FinalHopTLV, |
229 | | - *node.HarnessNode, []byte, |
230 | | -) { |
231 | | - |
232 | | - bobPubKey, err := btcec.ParsePubKey(bob.PubKey[:]) |
233 | | - require.NoError(ht.T, err) |
234 | | - |
235 | | - carolPubKey, err := btcec.ParsePubKey(carol.PubKey[:]) |
236 | | - require.NoError(ht.T, err) |
237 | | - |
238 | | - // Get the SCID of the Bob-Carol channel from Bob's perspective. |
239 | | - channels := bob.RPC.ListChannels(&lnrpc.ListChannelsRequest{ |
240 | | - Peer: carol.PubKey[:], |
241 | | - }) |
242 | | - require.Len(ht.T, channels.Channels, 1, "expected one channel") |
243 | | - scid := lnwire.NewShortChanIDFromInt(channels.Channels[0].ChanId) |
244 | | - |
245 | | - // Bob's payload: forward to Carol via SCID. |
246 | | - nextNode := fn.NewRight[*btcec.PublicKey](scid) |
247 | | - bobData := record.NewNonFinalBlindedRouteDataOnionMessage( |
248 | | - nextNode, nil, nil, |
249 | | - ) |
250 | | - |
251 | | - // Carol's payload: final hop (empty route data). |
252 | | - carolData := &record.BlindedRouteData{} |
253 | | - |
254 | | - hops := []*sphinx.HopInfo{ |
255 | | - { |
256 | | - NodePub: bobPubKey, |
257 | | - PlainText: onionmessage.EncodeBlindedRouteData( |
258 | | - ht.T, bobData, |
259 | | - ), |
260 | | - }, |
261 | | - { |
262 | | - NodePub: carolPubKey, |
263 | | - PlainText: onionmessage.EncodeBlindedRouteData( |
264 | | - ht.T, carolData, |
265 | | - ), |
266 | | - }, |
267 | | - } |
268 | | - |
269 | | - blindedPath := onionmessage.BuildBlindedPath(ht.T, hops) |
270 | | - |
271 | | - finalHopTLVs := []*lnwire.FinalHopTLV{ |
272 | | - { |
273 | | - TLVType: lnwire.InvoiceRequestNamespaceType, |
274 | | - Value: []byte{4, 5, 6}, |
275 | | - }, |
276 | | - } |
277 | | - |
278 | | - return blindedPath, finalHopTLVs, bob, bob.PubKey[:] |
279 | | -} |
280 | | - |
281 | | -// buildConcatenatedPath builds a concatenated blinded path scenario. |
282 | | -// Alice builds a path to Bob, Carol provides a blinded path starting at Bob. |
283 | | -// Bob's payload includes NextBlindingOverride to switch to Carol's path. |
284 | | -// Path: Alice -> Bob (intro) -> Carol. |
285 | | -func buildConcatenatedPath(ht *lntest.HarnessTest, alice, bob, |
286 | | - carol *node.HarnessNode) ( |
287 | | - *sphinx.BlindedPathInfo, []*lnwire.FinalHopTLV, |
288 | | - *node.HarnessNode, []byte, |
289 | | -) { |
290 | | - |
291 | | - bobPubKey, err := btcec.ParsePubKey(bob.PubKey[:]) |
292 | | - require.NoError(ht.T, err) |
293 | | - |
294 | | - carolPubKey, err := btcec.ParsePubKey(carol.PubKey[:]) |
295 | | - require.NoError(ht.T, err) |
296 | | - |
297 | | - // Carol creates a blinded path starting at Bob (introduction node). |
298 | | - // Carol's route data: final hop. |
299 | | - carolData := &record.BlindedRouteData{} |
300 | | - |
301 | | - receiverHops := []*sphinx.HopInfo{ |
302 | | - { |
303 | | - NodePub: carolPubKey, |
304 | | - PlainText: onionmessage.EncodeBlindedRouteData( |
305 | | - ht.T, carolData, |
306 | | - ), |
| 57 | + }() |
| 58 | + |
| 59 | + // Alice sends a message to Carol. The server routes through Bob. |
| 60 | + finalPayload := []byte{1, 2, 3} |
| 61 | + aliceMsg := &lnrpc.SendOnionMessageRequest{ |
| 62 | + Destination: carol.PubKey[:], |
| 63 | + FinalHopTlvs: map[uint64][]byte{ |
| 64 | + uint64(lnwire.InvoiceRequestNamespaceType): finalPayload, |
307 | 65 | }, |
308 | 66 | } |
309 | | - receiverPath := onionmessage.BuildBlindedPath(ht.T, receiverHops) |
310 | | - |
311 | | - // Alice creates a path to Bob with NextBlindingOverride pointing to |
312 | | - // Carol's blinding point. |
313 | | - nextNode := fn.NewLeft[*btcec.PublicKey, lnwire.ShortChannelID]( |
314 | | - carolPubKey, |
315 | | - ) |
316 | | - bobData := record.NewNonFinalBlindedRouteDataOnionMessage( |
317 | | - nextNode, receiverPath.Path.BlindingPoint, nil, |
318 | | - ) |
319 | | - |
320 | | - senderHops := []*sphinx.HopInfo{ |
321 | | - { |
322 | | - NodePub: bobPubKey, |
323 | | - PlainText: onionmessage.EncodeBlindedRouteData( |
324 | | - ht.T, bobData, |
325 | | - ), |
326 | | - }, |
| 67 | + alice.RPC.SendOnionMessage(aliceMsg) |
| 68 | + |
| 69 | + // Wait for Carol to receive the message. |
| 70 | + select { |
| 71 | + case msg := <-messages: |
| 72 | + // Carol should receive the message from Bob (the last relay). |
| 73 | + require.Equal(ht, bob.PubKey[:], msg.Peer, "unexpected peer") |
| 74 | + require.Equal( |
| 75 | + ht, finalPayload, |
| 76 | + msg.CustomRecords[uint64(lnwire.InvoiceRequestNamespaceType)], |
| 77 | + ) |
| 78 | + |
| 79 | + case <-time.After(lntest.DefaultTimeout): |
| 80 | + ht.Fatalf("carol did not receive onion message") |
327 | 81 | } |
328 | | - senderPath := onionmessage.BuildBlindedPath(ht.T, senderHops) |
329 | | - |
330 | | - // Concatenate the paths. |
331 | | - concatenatedPath := onionmessage.ConcatBlindedPaths( |
332 | | - ht.T, senderPath, receiverPath, |
333 | | - ) |
334 | | - |
335 | | - finalHopTLVs := []*lnwire.FinalHopTLV{ |
336 | | - { |
337 | | - TLVType: lnwire.InvoiceRequestNamespaceType, |
338 | | - Value: []byte{7, 8, 9}, |
339 | | - }, |
340 | | - } |
341 | | - |
342 | | - return concatenatedPath, finalHopTLVs, bob, bob.PubKey[:] |
343 | 82 | } |
0 commit comments