@@ -12,8 +12,17 @@ import (
1212)
1313
1414const (
15- routeBlindingTestFileName = "testdata/route-blinding-test.json"
16- onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json"
15+ routeBlindingTestFileName = "testdata/route-blinding-test.json"
16+ onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json"
17+ blindedOnionMessageOnionTestFileName = "testdata/blinded-onion-message-onion-test.json"
18+ )
19+
20+ var (
21+ // bolt4PubKeys contains the public keys used in the Bolt 4 spec test
22+ // vectors. We convert them variables named after the commonly used
23+ // names in cryptography.
24+ alicePubKey = bolt4PubKeys [0 ]
25+ bobPubKey = bolt4PubKeys [1 ]
1726)
1827
1928// TestBuildBlindedRoute tests BuildBlindedRoute and decryptBlindedHopData against
@@ -117,6 +126,164 @@ func TestBuildBlindedRoute(t *testing.T) {
117126 }
118127}
119128
129+ // TestBuildOnionMessageBlindedRoute tests the construction of a blinded route
130+ // for an onion message, specifically the concatenation of two blinded paths,
131+ // against the spec test vectors in `blinded-onion-message-onion-test.json`. It
132+ // verifies the correctness of BuildBlindedPath, decryptBlindedHopData, and
133+ // NextEphemeral.
134+ //
135+ // The test setup involves several parties and two distinct blinded paths that
136+ // are combined to form the full route:
137+ //
138+ // 1. Path from Dave: Dave (the receiver) first constructs a blinded path for a
139+ // message to be sent from Bob to himself (Dave).
140+ // The path is: Bob -> Carol -> Dave
141+ //
142+ // 2. Path from Sender: Dave gives his blinded path to a Sender. The Sender
143+ // then creates their own blinded path from themselves to Bob, passing
144+ // through Alice. The path is: Sender -> Alice -> Bob
145+ //
146+ // 3. Path Concatenation: The Sender prepends their path to Dave's path,
147+ // creating a final, concatenated route:
148+ // Sender -> Alice -> Bob -> Carol -> Dave
149+ // To link the two paths, the Sender includes a `next_path_key_override`
150+ // in the payload for Alice. This override is set to the first path key
151+ // (blinding point) of Dave's path, instructing Alice to use it for the next
152+ // hop (Bob) instead of the key that she could derive herself.
153+ //
154+ // The test then asserts that the generated concatenated path matches the test
155+ // vector's expected route. Finally, it simulates the decryption process at each
156+ // hop, verifying that each node can correctly decrypt its payload and derive
157+ // the correct next ephemeral key.
158+ func TestBuildOnionMessageBlindedRoute (t * testing.T ) {
159+ t .Parallel ()
160+
161+ // First, we'll read out the raw Json file at the target location.
162+ jsonBytes , err := os .ReadFile (blindedOnionMessageOnionTestFileName )
163+ require .NoError (t , err )
164+
165+ // Once we have the raw file, we'll unpack it into our
166+ // onionMessageJsonTestCase struct defined below.
167+ testCase := & onionMessageJsonTestCase {}
168+ require .NoError (t , json .Unmarshal (jsonBytes , testCase ))
169+ require .Len (t , testCase .Generate .Hops , 4 )
170+
171+ // buildMessagePath is a helper closure used to convert
172+ // hopOnionMessageData objects into HopInfo objects.
173+ buildMessagePath := func (h []hopOnionMessageData ,
174+ initialHopID string ) []* HopInfo {
175+
176+ path := make ([]* HopInfo , len (h ))
177+ // The json test vector doesn't properly specify the current
178+ // node id, so we need the initial Node ID as a starting point.
179+ currentHop := initialHopID
180+ for i , hop := range h {
181+ nodeIDStr , err := hex .DecodeString (currentHop )
182+ require .NoError (t , err )
183+ nodeID , err := btcec .ParsePubKey (nodeIDStr )
184+ require .NoError (t , err )
185+ payload , err := hex .DecodeString (hop .EncryptedDataTlv )
186+ require .NoError (t , err )
187+
188+ path [i ] = & HopInfo {
189+ NodePub : nodeID ,
190+ PlainText : payload ,
191+ }
192+
193+ // The json test vector doesn't properly specify the
194+ // current node id. It does specify the next node id. So
195+ // to get the current node id for the next iteration, we
196+ // get the next node id here.
197+ currentHop = hop .EncodedOnionMessageTLVs .NextNodeID
198+ }
199+ return path
200+ }
201+
202+ // First, Dave will build a blinded path from Bob to itself.
203+ daveSessKey := privKeyFromString (
204+ testCase .Generate .Hops [1 ].PathKeySecret ,
205+ )
206+ daveBobPath := buildMessagePath (
207+ testCase .Generate .Hops [1 :], bobPubKey ,
208+ )
209+ daveBobBlindedPath , err := BuildBlindedPath (daveSessKey , daveBobPath )
210+ require .NoError (t , err )
211+
212+ // At this point, Dave will give his blinded path to the Sender who will
213+ // then build its own blinded route from itself to Bob via Alice. The
214+ // sender will then concatenate the two paths. Note that in the payload
215+ // for Alice, the `next_path_key_override` field is added which is set
216+ // to the first path key in Dave's blinded route. This will indicate to
217+ // Alice that she should use this point for the next path key instead of
218+ // the next path key that she derives.
219+ // Path created by Dave: Bob -> Carol -> Dave
220+ // Path that the Sender will build: Sender -> Alice -> Bob
221+ aliceBobPath := buildMessagePath (
222+ testCase .Generate .Hops [:1 ], alicePubKey ,
223+ )
224+ senderSessKey := privKeyFromString (
225+ testCase .Generate .Hops [0 ].PathKeySecret ,
226+ )
227+ aliceBobBlindedPath , err := BuildBlindedPath (
228+ senderSessKey , aliceBobPath ,
229+ )
230+ require .NoError (t , err )
231+
232+ // Construct the concatenated path.
233+ path := & BlindedPath {
234+ IntroductionPoint : aliceBobBlindedPath .Path .IntroductionPoint ,
235+ BlindingPoint : aliceBobBlindedPath .Path .BlindingPoint ,
236+ BlindedHops : append (
237+ aliceBobBlindedPath .Path .BlindedHops ,
238+ daveBobBlindedPath .Path .BlindedHops ... ,
239+ ),
240+ }
241+
242+ // Check that the constructed path is equal to the test vector path.
243+ require .True (t , equalPubKeys (
244+ testCase .Route .FirstNodeId , path .IntroductionPoint ,
245+ ))
246+ require .True (t , equalPubKeys (
247+ testCase .Route .FirstPathKey , path .BlindingPoint ,
248+ ))
249+
250+ for i , hop := range testCase .Route .Hops {
251+ require .True (t , equalPubKeys (
252+ hop .BlindedNodeID , path .BlindedHops [i ].BlindedNodePub ,
253+ ))
254+
255+ data , _ := hex .DecodeString (hop .EncryptedRecipientData )
256+ require .Equal (t , data , path .BlindedHops [i ].CipherText )
257+ }
258+
259+ // Assert that each hop is able to decode the encrypted data meant for
260+ // it.
261+ for i , hop := range testCase .Decrypt .Hops {
262+ genData := testCase .Generate .Hops [i ]
263+ priv := privKeyFromString (hop .PrivKey )
264+ ephem := pubKeyFromString (genData .EphemeralPubKey )
265+
266+ // Now we'll decrypt the blinded hop data using the private key
267+ // and the ephemeral public key.
268+ data , err := decryptBlindedHopData (
269+ & PrivKeyECDH {PrivKey : priv }, ephem ,
270+ path .BlindedHops [i ].CipherText ,
271+ )
272+ require .NoError (t , err )
273+
274+ // Check if the decrypted data is what we expect it to be.
275+ dataExpected , _ := hex .DecodeString (genData .EncryptedDataTlv )
276+ require .Equal (t , data , dataExpected )
277+
278+ nextEphem , err := NextEphemeral (& PrivKeyECDH {priv }, ephem )
279+ require .NoError (t , err )
280+
281+ nextE := privKeyFromString (genData .NextEphemeralPrivKey )
282+
283+ require .Equal (t , nextE .PubKey (), nextEphem )
284+ }
285+ }
286+
120287// TestOnionRouteBlinding tests that an onion packet can correctly be processed
121288// by a node in a blinded route.
122289func TestOnionRouteBlinding (t * testing.T ) {
@@ -223,24 +390,47 @@ type decryptData struct {
223390 Hops []decryptHops `json:"hops"`
224391}
225392
393+ type decryptOnionMessageData struct {
394+ Hops []decryptOnionMessageHops `json:"hops"`
395+ }
396+
226397type decryptHops struct {
227398 Onion string `json:"onion"`
228399 NodePrivKey string `json:"node_privkey"`
229400 NextBlinding string `json:"next_blinding"`
230401}
231402
403+ type decryptOnionMessageHops struct {
404+ OnionMessage string `json:"onion_message"`
405+ PrivKey string `json:"privkey"`
406+ NextNodeID string `json:"next_node_id"`
407+ }
408+
232409type blindingJsonTestCase struct {
233410 Generate generateData `json:"generate"`
234411 Route routeData `json:"route"`
235412 Unblind unblindData `json:"unblind"`
236413}
237414
415+ type onionMessageJsonTestCase struct {
416+ Generate generateOnionMessageData `json:"generate"`
417+ Route routeOnionMessageData `json:"route"`
418+ // OnionMessage onionMessageData `json:"onionmessage"`
419+ Decrypt decryptOnionMessageData `json:"decrypt"`
420+ }
421+
238422type routeData struct {
239423 IntroductionNodeID string `json:"introduction_node_id"`
240424 Blinding string `json:"blinding"`
241425 Hops []blindedHop `json:"hops"`
242426}
243427
428+ type routeOnionMessageData struct {
429+ FirstNodeId string `json:"first_node_id"`
430+ FirstPathKey string `json:"first_path_key"`
431+ Hops []blindedOnionMessageHop `json:"hops"`
432+ }
433+
244434type unblindData struct {
245435 Hops []unblindedHop `json:"hops"`
246436}
@@ -249,6 +439,11 @@ type generateData struct {
249439 Hops []hopData `json:"hops"`
250440}
251441
442+ type generateOnionMessageData struct {
443+ SessionKey string `json:"session_key"`
444+ Hops []hopOnionMessageData `json:"hops"`
445+ }
446+
252447type unblindedHop struct {
253448 NodePrivKey string `json:"node_privkey"`
254449 EphemeralPubKey string `json:"ephemeral_pubkey"`
@@ -262,11 +457,31 @@ type hopData struct {
262457 EncodedTLVs string `json:"encoded_tlvs"`
263458}
264459
460+ type hopOnionMessageData struct {
461+ PathKeySecret string `json:"path_key_secret"`
462+ EncodedOnionMessageTLVs encodedOnionMessageTLVs `json:"tlvs"`
463+ EncryptedDataTlv string `json:"encrypted_data_tlv"`
464+ EphemeralPubKey string `json:"E"`
465+ NextEphemeralPrivKey string `json:"next_e"`
466+ }
467+
468+ type encodedOnionMessageTLVs struct {
469+ NextNodeID string `json:"next_node_id"`
470+ NextPathKeyOverride string `json:"next_path_key_override"`
471+ PathKeyOverrideSecret string `json:"path_key_override_secret"`
472+ PathID string `json:"path_id"`
473+ }
474+
265475type blindedHop struct {
266476 BlindedNodeID string `json:"blinded_node_id"`
267477 EncryptedData string `json:"encrypted_data"`
268478}
269479
480+ type blindedOnionMessageHop struct {
481+ BlindedNodeID string `json:"blinded_node_id"`
482+ EncryptedRecipientData string `json:"encrypted_recipient_data"`
483+ }
484+
270485func equalPubKeys (pkStr string , pk * btcec.PublicKey ) bool {
271486 return hex .EncodeToString (pk .SerializeCompressed ()) == pkStr
272487}
0 commit comments