Skip to content

Commit 3f475ce

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 fd45a21 commit 3f475ce

File tree

2 files changed

+263
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)