Skip to content

Commit 11b2f79

Browse files
committed
sphinx_test: add blinded onion message test
This commit adds the spec test vector for blinded onion messages. It also adds a test that tests BuildBlindedRoute, decryptBlindedHopData and NextEphemeral against this vector.
1 parent 6ab4f04 commit 11b2f79

File tree

2 files changed

+360
-2
lines changed

2 files changed

+360
-2
lines changed

path_test.go

Lines changed: 217 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,17 @@ import (
1212
)
1313

1414
const (
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.
122289
func 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+
226397
type 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+
232409
type 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+
238422
type 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+
244434
type 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+
252447
type 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+
265475
type 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+
270485
func equalPubKeys(pkStr string, pk *btcec.PublicKey) bool {
271486
return hex.EncodeToString(pk.SerializeCompressed()) == pkStr
272487
}

0 commit comments

Comments
 (0)