Skip to content

Commit e1738ce

Browse files
committed
itest: test onion message forwarding
Adds the new integration test file to test forwarding of onion messages through a multi-hop path.
1 parent 7213f3d commit e1738ce

File tree

2 files changed

+272
-0
lines changed

2 files changed

+272
-0
lines changed

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,10 @@ var allTestCases = []*lntest.TestCase{
535535
Name: "onion message",
536536
TestFunc: testOnionMessage,
537537
},
538+
{
539+
Name: "onion message forwarding",
540+
TestFunc: testOnionMessageForwarding,
541+
},
538542
{
539543
Name: "sign verify message with addr",
540544
TestFunc: testSignVerifyMessageWithAddr,
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
package itest
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"time"
7+
8+
"github.com/btcsuite/btcd/btcec/v2"
9+
sphinx "github.com/lightningnetwork/lightning-onion"
10+
"github.com/lightningnetwork/lnd/lnrpc"
11+
"github.com/lightningnetwork/lnd/lntest"
12+
"github.com/lightningnetwork/lnd/lnwire"
13+
"github.com/lightningnetwork/lnd/record"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
// testOnionMessage tests forwarding of onion messages.
18+
func testOnionMessageForwarding(ht *lntest.HarnessTest) {
19+
// Spin up a three node because we will need a three-hop network for
20+
// this test.
21+
alice := ht.NewNodeWithCoins("Alice", nil)
22+
bob := ht.NewNodeWithCoins("Bob", nil)
23+
carol := ht.NewNode("Carol", nil)
24+
25+
// Create a session key for the blinded path.
26+
blindingKey, err := btcec.NewPrivateKey()
27+
require.NoError(ht.T, err)
28+
29+
sessionKey, err := btcec.NewPrivateKey()
30+
require.NoError(ht.T, err)
31+
32+
// Connect nodes before channel opening so that they can share gossip.
33+
ht.ConnectNodesPerm(alice, bob)
34+
ht.ConnectNodesPerm(bob, carol)
35+
36+
// Open channels: Alice --- Bob --- Carol and wait for each node to
37+
// sync the network graph.
38+
aliceBobChanPoint := ht.OpenChannel(
39+
alice, bob, lntest.OpenChannelParams{
40+
Amt: 500_000,
41+
},
42+
)
43+
ht.AssertNumChannelUpdates(carol, aliceBobChanPoint, 2)
44+
45+
bobCarolChanPoint := ht.OpenChannel(
46+
bob, carol, lntest.OpenChannelParams{
47+
Amt: 500_000,
48+
},
49+
)
50+
ht.AssertNumChannelUpdates(alice, bobCarolChanPoint, 2)
51+
52+
// Create a blinded route
53+
54+
// Create a set of 2 blinded hops for our path.
55+
hopsToBlind := make([]*sphinx.HopInfo, 2)
56+
57+
// Our path is: Alice -> Bob -> Carol
58+
// So Bob needs to receive the public Key of Carol.
59+
60+
carolPubKey, err := btcec.ParsePubKey(carol.PubKey[:])
61+
require.NoError(ht.T, err)
62+
data0 := record.NewNonFinalBlindedRouteDataOnionMessage(
63+
carolPubKey, nil, nil, nil,
64+
)
65+
encoded0, err := record.EncodeBlindedRouteData(data0)
66+
require.NoError(ht.T, err)
67+
68+
data1 := &record.BlindedRouteData{}
69+
encoded1, err := record.EncodeBlindedRouteData(data1)
70+
require.NoError(ht.T, err)
71+
72+
bobPubKey, err := btcec.ParsePubKey(bob.PubKey[:])
73+
require.NoError(ht.T, err)
74+
75+
// The first hop is for Bob. This will be blinded at a later stage.
76+
hopsToBlind[0] = &sphinx.HopInfo{
77+
NodePub: bobPubKey,
78+
PlainText: encoded0,
79+
}
80+
// The second hop is to Carol.
81+
hopsToBlind[1] = &sphinx.HopInfo{
82+
NodePub: carolPubKey,
83+
PlainText: encoded1,
84+
}
85+
86+
blindedPath, err := sphinx.BuildBlindedPath(blindingKey, hopsToBlind)
87+
require.NoError(ht.T, err)
88+
89+
finalHopPayload := &lnwire.FinalHopPayload{
90+
TLVType: lnwire.InvoiceRequestNamespaceType,
91+
Value: []byte{1, 2, 3},
92+
}
93+
94+
// Convert that blinded path to a sphinx path and add a final payload.
95+
sphinxPath, err := blindedToSphinx(
96+
blindedPath.Path, nil, nil, []*lnwire.FinalHopPayload{
97+
finalHopPayload,
98+
},
99+
)
100+
require.NoError(ht.T, err)
101+
102+
// Create an onion packet with no associated data.
103+
onionPacket, err := sphinx.NewOnionPacket(
104+
sphinxPath, sessionKey, nil, sphinx.DeterministicPacketFiller,
105+
sphinx.WithMaxPayloadSize(sphinx.MaxRoutingPayloadSize),
106+
)
107+
require.NoError(ht.T, err, "new onion packet")
108+
109+
buf := new(bytes.Buffer)
110+
err = onionPacket.Encode(buf)
111+
require.NoError(ht.T, err, "encode onion packet")
112+
113+
// Subscribe Carol to onion messages before we send any, so that we
114+
// don't miss any.
115+
msgClient, cancel := carol.RPC.SubscribeOnionMessages()
116+
defer cancel()
117+
118+
// Create a channel to receive onion messages on.
119+
messages := make(chan *lnrpc.OnionMessageUpdate)
120+
go func() {
121+
for {
122+
// If we fail to receive, just exit. The test should
123+
// fail elsewhere if it doesn't get a message that it
124+
// was expecting.
125+
msg, err := msgClient.Recv()
126+
if err != nil {
127+
return
128+
}
129+
130+
// Deliver the message into our channel or exit if the
131+
// test is shutting down.
132+
select {
133+
case messages <- msg:
134+
case <-ht.Context().Done():
135+
return
136+
}
137+
}
138+
}()
139+
140+
pathKey := blindingKey.PubKey().SerializeCompressed()
141+
142+
// Send it from Alice to Bob.
143+
aliceMsg := &lnrpc.SendOnionMessageRequest{
144+
Peer: bob.PubKey[:],
145+
PathKey: pathKey,
146+
Onion: buf.Bytes(),
147+
}
148+
alice.RPC.SendOnionMessage(aliceMsg)
149+
150+
// Wait for Carol to receive the message.
151+
select {
152+
case msg := <-messages:
153+
// Check our type and data and (sanity) check the peer we got
154+
// it from.
155+
require.Equal(ht, bob.PubKey[:], msg.Peer, "msg peer wrong")
156+
require.NotEmpty(ht, msg.CustomRecords)
157+
customRecordsKey := uint64(lnwire.InvoiceRequestNamespaceType)
158+
require.NotNil(ht, msg.CustomRecords[customRecordsKey])
159+
require.Equal(
160+
ht, []byte{1, 2, 3},
161+
msg.CustomRecords[customRecordsKey],
162+
)
163+
164+
case <-time.After(lntest.DefaultTimeout):
165+
ht.Fatalf("carol did not receive onion message: %v", aliceMsg)
166+
}
167+
168+
ht.CloseChannel(alice, aliceBobChanPoint)
169+
ht.CloseChannel(bob, bobCarolChanPoint)
170+
}
171+
172+
// blindedToSphinx converts the blinded path provided to a sphinx path that can
173+
// be wrapped up in an onion, encoding the TLV payload for each hop along the
174+
// way.
175+
func blindedToSphinx(blindedRoute *sphinx.BlindedPath,
176+
extraHops []*lnwire.BlindedHop, replyPath *lnwire.ReplyPath,
177+
finalPayloads []*lnwire.FinalHopPayload) (
178+
*sphinx.PaymentPath, error) {
179+
180+
var (
181+
sphinxPath sphinx.PaymentPath
182+
183+
ourHopCount = len(blindedRoute.BlindedHops)
184+
extraHopCount = len(extraHops)
185+
)
186+
187+
// Fill in the blinded node id and encrypted data for all hops. This
188+
// requirement differs from blinded hops used for payments, where we
189+
// don't use the blinded introduction node id. However, since onion
190+
// messages are fully blinded by default, we use the blinded
191+
// introduction node id.
192+
for i := 0; i < ourHopCount; i++ {
193+
// Create an onion message payload with the encrypted data for
194+
// this hop.
195+
payload := &lnwire.OnionMessagePayload{
196+
EncryptedData: blindedRoute.BlindedHops[i].CipherText,
197+
}
198+
199+
// If we're on the final hop and there are no extra hops to add
200+
// onto our path, include the tlvs intended for the final hop
201+
// and the reply path (if provided).
202+
if i == ourHopCount-1 && extraHopCount == 0 {
203+
payload.FinalHopPayloads = finalPayloads
204+
payload.ReplyPath = replyPath
205+
}
206+
207+
// Encode the tlv stream for inclusion in our message.
208+
hop, err := createSphinxHop(
209+
*blindedRoute.BlindedHops[i].BlindedNodePub, payload,
210+
)
211+
if err != nil {
212+
return nil, fmt.Errorf("sphinx hop %v: %w", i, err)
213+
}
214+
sphinxPath[i] = *hop
215+
}
216+
217+
// If we don't have any more hops to append to our path, just return
218+
// it as-is here.
219+
if extraHopCount == 0 {
220+
return &sphinxPath, nil
221+
}
222+
223+
for i := 0; i < extraHopCount; i++ {
224+
payload := &lnwire.OnionMessagePayload{
225+
EncryptedData: extraHops[i].EncryptedData,
226+
}
227+
228+
// If we're on the last hop, add our optional final payload
229+
// and reply path.
230+
if i == extraHopCount-1 {
231+
payload.FinalHopPayloads = finalPayloads
232+
payload.ReplyPath = replyPath
233+
}
234+
235+
hop, err := createSphinxHop(
236+
*extraHops[i].BlindedNodeID, payload,
237+
)
238+
if err != nil {
239+
return nil, fmt.Errorf("sphinx hop %v: %w", i, err)
240+
}
241+
242+
// We need to offset our index in the sphinx path by the
243+
// number of hops that we added in the loop above.
244+
sphinxIndex := i + ourHopCount
245+
sphinxPath[sphinxIndex] = *hop
246+
}
247+
248+
return &sphinxPath, nil
249+
}
250+
251+
// createSphinxHop encodes an onion message payload and produces a sphinx
252+
// onion hop for it.
253+
func createSphinxHop(nodeID btcec.PublicKey,
254+
payload *lnwire.OnionMessagePayload) (*sphinx.OnionHop, error) {
255+
256+
payloadTLVs, err := payload.Encode()
257+
if err != nil {
258+
return nil, fmt.Errorf("payload: encode: %w", err)
259+
}
260+
261+
return &sphinx.OnionHop{
262+
NodePub: nodeID,
263+
HopPayload: sphinx.HopPayload{
264+
Type: sphinx.PayloadTLV,
265+
Payload: payloadTLVs,
266+
},
267+
}, nil
268+
}

0 commit comments

Comments
 (0)