@@ -11,9 +11,19 @@ import (
1111 "github.com/stretchr/testify/require"
1212)
1313
14+ //nolint:lll
1415const (
15- routeBlindingTestFileName = "testdata/route-blinding-test.json"
16- onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json"
16+ routeBlindingTestFileName = "testdata/route-blinding-test.json"
17+ onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json"
18+ blindedOnionMessageOnionTestFileName = "testdata/blinded-onion-message-onion-test.json"
19+ )
20+
21+ var (
22+ // bolt4PubKeys contains the public keys used in the Bolt 4 spec. test
23+ // vectors. We convert them to variables named after the commonly used
24+ // names in cryptography.
25+ alicePubKey = bolt4PubKeys [0 ]
26+ bobPubKey = bolt4PubKeys [1 ]
1727)
1828
1929// TestBuildBlindedRoute tests BuildBlindedRoute and decryptBlindedHopData against
@@ -117,6 +127,181 @@ func TestBuildBlindedRoute(t *testing.T) {
117127 }
118128}
119129
130+ // TestBuildOnionMessageBlindedRoute tests the construction of a blinded route
131+ // for an onion message, specifically the concatenation of two blinded paths,
132+ // against the spec. test vectors in `blinded-onion-message-onion-test.json`. It
133+ // verifies the correctness of BuildBlindedPath, decryptBlindedHopData, and
134+ // NextEphemeral.
135+ //
136+ // The test setup involves several parties and two distinct blinded paths that
137+ // are combined to form the full route:
138+ //
139+ // 1. Path from Dave: Dave (the receiver) first constructs a blinded path for a
140+ // message to be sent from Bob to himself (Dave).
141+ // The path is: Bob -> Carol -> Dave
142+ //
143+ // 2. Path from Sender: Dave gives his blinded path to a Sender. The Sender
144+ // then creates their own blinded path from themselves to Bob, passing
145+ // through Alice. The path is: Sender -> Alice -> Bob
146+ //
147+ // 3. Path Concatenation: The Sender prepends their path to Dave's path,
148+ // creating a final, concatenated route:
149+ // Sender -> Alice -> Bob -> Carol -> Dave
150+ // To link the two paths, the Sender includes a `next_path_key_override`
151+ // in the payload for Alice. This override is set to the first path key
152+ // (blinding point) of Dave's path, instructing Alice to use it for the next
153+ // hop (Bob) instead of the key that she could derive herself.
154+ //
155+ // The test then asserts that the generated concatenated path matches the test
156+ // vector's expected route. It simulates the decryption process at each hop,
157+ // verifying that at each hop encrypted_recipient_data is what we expect it to
158+ // be and that it correctly decrypts to the encrypted_data_tlv stream. Finally,
159+ // we verify the derivation of the next ephemeral key.
160+ func TestBuildOnionMessageBlindedRoute (t * testing.T ) {
161+ t .Parallel ()
162+
163+ // First, we'll read out the raw Json file at the target location.
164+ jsonBytes , err := os .ReadFile (blindedOnionMessageOnionTestFileName )
165+ require .NoError (t , err )
166+
167+ // Once we have the raw file, we'll unpack it into our
168+ // onionMessageJsonTestCase struct defined below.
169+ testCase := & onionMessageJsonTestCase {}
170+ require .NoError (t , json .Unmarshal (jsonBytes , testCase ))
171+ require .Len (t , testCase .Generate .Hops , 4 )
172+
173+ // buildMessagePath is a helper closure used to convert
174+ // hopOnionMessageData objects into HopInfo objects.
175+ buildMessagePath := func (h []hopOnionMessageData ,
176+ initialHopID string ) []* HopInfo {
177+
178+ path := make ([]* HopInfo , len (h ))
179+
180+ // The json test vector doesn't properly specify the current
181+ // node id, so we need the initial Node ID as a starting point.
182+ currentHop := initialHopID
183+ for i , hop := range h {
184+ nodeIDStr , err := hex .DecodeString (currentHop )
185+ require .NoError (t , err )
186+ nodeID , err := btcec .ParsePubKey (nodeIDStr )
187+ require .NoError (t , err )
188+ payload , err := hex .DecodeString (hop .EncryptedDataTlv )
189+ require .NoError (t , err )
190+
191+ path [i ] = & HopInfo {
192+ NodePub : nodeID ,
193+ PlainText : payload ,
194+ }
195+
196+ // The json test vector doesn't properly specify the
197+ // current node id. It does specify the next node id. So
198+ // to get the current node id for the next iteration, we
199+ // get the next node id here.
200+ currentHop = hop .EncodedOnionMessageTLVs .NextNodeID
201+ }
202+
203+ return path
204+ }
205+
206+ // First, Dave will build a blinded path from Bob to itself.
207+ receiverSessKey := privKeyFromString (
208+ testCase .Generate .Hops [1 ].PathKeySecret ,
209+ )
210+ daveBobPath := buildMessagePath (
211+ testCase .Generate .Hops [1 :], bobPubKey ,
212+ )
213+ daveBobBlindedPath , err := BuildBlindedPath (
214+ receiverSessKey , daveBobPath ,
215+ )
216+ require .NoError (t , err )
217+
218+ // At this point, Dave will give his blinded path to the Sender who will
219+ // then build its own blinded route from itself to Bob via Alice. The
220+ // sender will then concatenate the two paths. Note that in the payload
221+ // for Alice, the `next_path_key_override` field is added which is set
222+ // to the first path key in Dave's blinded route. This will indicate to
223+ // Alice that she should use this point for the next path key instead of
224+ // the next path key that she derives.
225+ // Path created by Dave: Bob -> Carol -> Dave
226+ // Path that the Sender will build: Sender -> Alice -> Bob
227+ aliceBobPath := buildMessagePath (
228+ testCase .Generate .Hops [:1 ], alicePubKey ,
229+ )
230+ senderSessKey := privKeyFromString (
231+ testCase .Generate .Hops [0 ].PathKeySecret ,
232+ )
233+ aliceBobBlindedPath , err := BuildBlindedPath (
234+ senderSessKey , aliceBobPath ,
235+ )
236+ require .NoError (t , err )
237+
238+ // Construct the concatenated path.
239+ path := & BlindedPath {
240+ IntroductionPoint : aliceBobBlindedPath .Path .IntroductionPoint ,
241+ BlindingPoint : aliceBobBlindedPath .Path .BlindingPoint ,
242+ BlindedHops : append (
243+ aliceBobBlindedPath .Path .BlindedHops ,
244+ daveBobBlindedPath .Path .BlindedHops ... ,
245+ ),
246+ }
247+
248+ // Check that the constructed path is equal to the test vector path.
249+ require .True (t , equalPubKeys (
250+ testCase .Route .FirstNodeId , path .IntroductionPoint ,
251+ ))
252+ require .True (t , equalPubKeys (
253+ testCase .Route .FirstPathKey , path .BlindingPoint ,
254+ ))
255+
256+ for i , hop := range testCase .Route .Hops {
257+ require .True (t , equalPubKeys (
258+ hop .BlindedNodeID , path .BlindedHops [i ].BlindedNodePub ,
259+ ))
260+
261+ data , _ := hex .DecodeString (hop .EncryptedRecipientData )
262+ require .Equal (t , data , path .BlindedHops [i ].CipherText )
263+ }
264+
265+ // Assert that each hop is able to decode the encrypted data meant for
266+ // it.
267+ for i , hop := range testCase .Decrypt .Hops {
268+ genData := testCase .Generate .Hops [i ]
269+ priv := privKeyFromString (hop .PrivKey )
270+ ephem := pubKeyFromString (genData .EphemeralPubKey )
271+
272+ // Check if the encrypted_recipient_data is what we expect it to
273+ // be.
274+ encRecipientDataExpected , err := hex .DecodeString (
275+ genData .EncryptedRecipientData ,
276+ )
277+ require .NoError (t , err )
278+ require .Equal (
279+ t , encRecipientDataExpected ,
280+ path .BlindedHops [i ].CipherText ,
281+ )
282+
283+ // Now we'll decrypt the blinded hop data using the private key
284+ // and the ephemeral public key.
285+ data , err := decryptBlindedHopData (
286+ & PrivKeyECDH {PrivKey : priv }, ephem ,
287+ path .BlindedHops [i ].CipherText ,
288+ )
289+ require .NoError (t , err )
290+
291+ // Check if the decrypted data is what we expect it to be.
292+ dataExpected , err := hex .DecodeString (genData .EncryptedDataTlv )
293+ require .NoError (t , err )
294+ require .Equal (t , dataExpected , data )
295+
296+ nextEphem , err := NextEphemeral (& PrivKeyECDH {priv }, ephem )
297+ require .NoError (t , err )
298+
299+ nextE := privKeyFromString (genData .NextEphemeralPrivKey )
300+
301+ require .Equal (t , nextE .PubKey (), nextEphem )
302+ }
303+ }
304+
120305// TestOnionRouteBlinding tests that an onion packet can correctly be processed
121306// by a node in a blinded route.
122307func TestOnionRouteBlinding (t * testing.T ) {
@@ -223,24 +408,51 @@ type decryptData struct {
223408 Hops []decryptHops `json:"hops"`
224409}
225410
411+ type decryptOnionMessageData struct {
412+ Hops []decryptOnionMessageHops `json:"hops"`
413+ }
414+
226415type decryptHops struct {
416+ //nolint:tagliatelle
227417 Onion string `json:"onion"`
228418 NodePrivKey string `json:"node_privkey"`
229419 NextBlinding string `json:"next_blinding"`
230420}
231421
422+ //nolint:tagliatelle
423+ type decryptOnionMessageHops struct {
424+ OnionMessage string `json:"onion_message"`
425+ PrivKey string `json:"privkey"`
426+ NextNodeID string `json:"next_node_id"`
427+ }
428+
232429type blindingJsonTestCase struct {
233430 Generate generateData `json:"generate"`
234431 Route routeData `json:"route"`
235432 Unblind unblindData `json:"unblind"`
236433}
237434
435+ type onionMessageJsonTestCase struct {
436+ Generate generateOnionMessageData `json:"generate"`
437+ Route routeOnionMessageData `json:"route"`
438+ // OnionMessage onionMessageData `json:"onionmessage"`
439+ Decrypt decryptOnionMessageData `json:"decrypt"`
440+ }
441+
238442type routeData struct {
443+ //nolint:tagliatelle
239444 IntroductionNodeID string `json:"introduction_node_id"`
240445 Blinding string `json:"blinding"`
241446 Hops []blindedHop `json:"hops"`
242447}
243448
449+ //nolint:tagliatelle
450+ type routeOnionMessageData struct {
451+ FirstNodeId string `json:"first_node_id"`
452+ FirstPathKey string `json:"first_path_key"`
453+ Hops []blindedOnionMessageHop `json:"hops"`
454+ }
455+
244456type unblindData struct {
245457 Hops []unblindedHop `json:"hops"`
246458}
@@ -249,8 +461,15 @@ type generateData struct {
249461 Hops []hopData `json:"hops"`
250462}
251463
464+ //nolint:tagliatelle
465+ type generateOnionMessageData struct {
466+ SessionKey string `json:"session_key"`
467+ Hops []hopOnionMessageData `json:"hops"`
468+ }
469+
252470type unblindedHop struct {
253471 NodePrivKey string `json:"node_privkey"`
472+ //nolint:tagliatelle
254473 EphemeralPubKey string `json:"ephemeral_pubkey"`
255474 DecryptedData string `json:"decrypted_data"`
256475 NextEphemeralPubKey string `json:"next_ephemeral_pubkey"`
@@ -262,11 +481,36 @@ type hopData struct {
262481 EncodedTLVs string `json:"encoded_tlvs"`
263482}
264483
484+ //nolint:tagliatelle
485+ type hopOnionMessageData struct {
486+ PathKeySecret string `json:"path_key_secret"`
487+ EncodedOnionMessageTLVs encodedOnionMessageTLVs `json:"tlvs"`
488+ EncryptedDataTlv string `json:"encrypted_data_tlv"` //nolint:lll
489+ EphemeralPubKey string `json:"E"`
490+ NextEphemeralPrivKey string `json:"next_e"`
491+ EncryptedRecipientData string `json:"encrypted_recipient_data"` //nolint:lll
492+ }
493+
494+ //nolint:tagliatelle
495+ type encodedOnionMessageTLVs struct {
496+ NextNodeID string `json:"next_node_id"`
497+ NextPathKeyOverride string `json:"next_path_key_override"`
498+ PathKeyOverrideSecret string `json:"path_key_override_secret"`
499+ PathID string `json:"path_id"`
500+ }
501+
502+ //nolint:tagliatelle
265503type blindedHop struct {
266504 BlindedNodeID string `json:"blinded_node_id"`
267505 EncryptedData string `json:"encrypted_data"`
268506}
269507
508+ //nolint:tagliatelle
509+ type blindedOnionMessageHop struct {
510+ BlindedNodeID string `json:"blinded_node_id"`
511+ EncryptedRecipientData string `json:"encrypted_recipient_data"`
512+ }
513+
270514func equalPubKeys (pkStr string , pk * btcec.PublicKey ) bool {
271515 return hex .EncodeToString (pk .SerializeCompressed ()) == pkStr
272516}
0 commit comments